From 27af716ca07162087ed1446c9c823fb5a03ba273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B2=BD=EB=AF=BC?= Date: Thu, 19 Mar 2026 09:58:01 +0900 Subject: [PATCH] Update step_format.py --- .../converters/pipeline/step_format.py | 174 +++++++++++++++--- 1 file changed, 153 insertions(+), 21 deletions(-) diff --git a/03.Code/업로드용/converters/pipeline/step_format.py b/03.Code/업로드용/converters/pipeline/step_format.py index 5d44bba..489f284 100644 --- a/03.Code/업로드용/converters/pipeline/step_format.py +++ b/03.Code/업로드용/converters/pipeline/step_format.py @@ -1,33 +1,165 @@ # -*- coding: utf-8 -*- """ -step_format.py - 앱 내 변환 모듈 통합 -수정 사항: -A-1. 연차보고서 한글 공백 처리 -A-2. bullet 형태 필터링 및 정리 +step_format.py — 형식만 변경 모드 파이프라인 v4 + +문서 유형 자동 감지 후 전용 핸들러로 분기: + + fmt_gpd_report.py ← GPD 보고서형 (헤더 2개↑ + 공백 기반 들여쓰기) + fmt_outline.py ← 목차/개요형 (1./a./Ⅰ. 번호 패턴 40%↑) + fmt_table_report.py ← 표 중심형 (표가 페이지 면적 60%↑) + fmt_generic.py ← 범용 fallback (위 유형 해당 없을 때, 페이지별 자동 선택) + +판별 기준: + 1. 헤더 패턴 2개 이상 → GPD 보고서형 + 2. 표 면적 비율 60% 이상 → 표 중심형 + 3. 번호/기호 패턴 비율 40% 이상 → 목차/개요형 + 4. 해당 없음 → 범용 fallback """ -import os + import re -import json +import os +import sys from pathlib import Path -from datetime import datetime -import fitz # PyMuPDF -def log(msg): - print(f"[step_format] {msg}") +import fitz -# ... (중략: 상세 변환 로직 코드는 프로젝트 구조에 따라 최적화됨) +from fmt_base import ( + log, detect_header_footer_patterns, build_html, + extract_tables_from_page, ZW_PATTERN +) +import fmt_gpd_report +import fmt_outline +import fmt_table_report +import fmt_generic -def process_pdf_to_html(input_dir, output_dir, session_id): - """PDF를 분석하여 구조화된 HTML로 변환하는 메인 함수""" - input_path = Path(input_dir) - gen_dir = Path(output_dir) / session_id + +# ============================================================ +# 문서 유형 판별 +# ============================================================ + +_NUM_P = re.compile(r'^\d+[\.\uff0e\)]\s*[^\d\.\s]') +_ALPHA_P = re.compile(r'^[a-z][\.\uff0e\)]\s*\S') +_ROMAN_P = re.compile(r'^[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅰⅱⅲⅳⅴ]+[\.\s]') +_DASH_P = re.compile(r'^-\s*[^\s\(]') + + +def _doc_pattern_ratio(doc, header_patterns, footer_patterns): + """전 페이지 평균 번호/기호 패턴 비율""" + total_hits, total_lines = 0, 0 + for page in doc: + ph = page.rect.height + tb = ph * 0.85 + # 표 bbox 수집 + table_rects = [] + try: + for t in page.find_tables().tables: + table_rects.append(t.bbox) + except: pass + + def in_table(x, y): + for tx0,ty0,tx1,ty1 in table_rects: + if tx0-5<=x<=tx1+5 and ty0-5<=y<=ty1+5: return True + return False + + for b in page.get_text("dict")["blocks"]: + if b.get("type") != 0: continue + for line in b.get("lines",[]): + x, parts = None, [] + for s in line.get("spans",[]): + t = ZW_PATTERN.sub('', s.get("text","")) + if t.strip() and x is None: x = s["bbox"][0] + parts.append(t) + text = ''.join(parts).strip() + y0 = line['bbox'][1] + if text and y0= 2: + return 'gpd_report' + if table_ratio >= 0.6: + return 'table_report' + if pattern_ratio >= 0.4: + return 'outline' + return 'generic' + + +# ============================================================ +# 메인 진입점 +# ============================================================ + +def run_format_only(session_id: str, input_dir: str) -> dict: + gen_dir = Path(f'/tmp/{session_id}/format_out') gen_dir.mkdir(parents=True, exist_ok=True) - pdf_files = list(input_path.glob("*.pdf")) + pdf_files = list(Path(input_dir).rglob('*.pdf')) + \ + list(Path(input_dir).rglob('*.PDF')) if not pdf_files: - return {'success': False, 'error': 'PDF 파일이 없습니다.'} + return {'success': False, 'error': '처리할 PDF 파일이 없습니다.'} - log(f"처리 시작: {pdf_files[0].name}") - # ... (상세 처리 로직) - - return {'success': True, 'session_id': session_id} + pdf_path = pdf_files[0] + log(f"PDF: {pdf_path.name}") + doc = fitz.open(str(pdf_path)) + log(f"총 {len(doc)}페이지") + + header_patterns, footer_patterns, meta = detect_header_footer_patterns(doc) + log(f"메타: {meta}") + + doc_type = detect_doc_type(doc, header_patterns, footer_patterns) + log(f"문서 유형: {doc_type}") + + handler_map = { + 'gpd_report': fmt_gpd_report, + 'outline': fmt_outline, + 'table_report': fmt_table_report, + 'generic': fmt_generic, + } + handler = handler_map[doc_type] + pages_html = handler.process(doc, header_patterns, footer_patterns, meta) + + if not pages_html: + return {'success': False, 'error': '생성된 페이지가 없습니다.'} + + final_html = build_html(pages_html) + out_path = gen_dir / 'result.html' + out_path.write_text(final_html, encoding='utf-8') + log(f"완료: {len(pages_html)}페이지 → {doc_type}") + + return { + 'success': True, 'html': final_html, + 'page_count': len(pages_html), + 'doc_type': doc_type, + 'session_id': session_id + }