[개발기 #3] PDF 파싱이라는 '멋진 실수', 그리고 제가 찾은 '진짜' 자동화

반응형

모든 자동화 개발자는 꿈을 꿉니다. 어떤 형태의 문서든, 마법처럼 데이터를 빨아들여 깔끔하게 정리하는 완벽한 파이프라인을 말이죠. 저 역시 PDF 자동화라는 멋진 꿈을 꿨습니다. 하지만 몇 주간의 실험 끝에, 저는 그 꿈에서 깨어나기로 결정했습니다. 이 글은 그 결정의 이유와, 꿈보다 더 값진 '현실'을 얻게 된 기록입니다.


🚩 PDF 파싱의 '멋짐'과 '현실'

PDF 파싱은 겉보기엔 멋집니다. 몇 줄의 코드로 문서에서 표를 뽑아내고, 텍스트를 긁어오면 끝날 것 같죠. 하지만 실무에서 마주친 벽은 다음과 같았습니다.

우측하단의 재료표를 금방 인식할 줄 알았습니다.

  1. 서식 지옥: 표가 표가 아니다
    • 줄 간격, 병합 셀, 숨은 경계선, 회전 텍스트… 형식이 제각각이라 테이블 인식률이 불안정합니다.
    • 한 업체는 W1500×H1200, 다른 업체는 1500x1200, 또 다른 곳은 1500*1200(mm)로 표기합니다.
  2. 유지보수 비용 폭증
    • 공급사 서식이 바뀔 때마다 파서(rule)를 수정해야 합니다.
    • “이번 달 서식”을 맞춰도, 다음 달에 다시 깨집니다.
  3. 검증 어려움
    • 추출 결과가 맞는지 자동 검증 포인트를 잡기 어렵습니다.
    • 숫자 한 칸 밀려 들어가도 육안 확인 없이는 놓치기 쉽습니다.

요약: PDF는 사람이 보기엔 ‘문서’지만, 기계에게는 ‘그림’에 가깝습니다.


✅ Excel 기반으로 전환한 이유 (실무형 체크리스트)

다음 조건을 충족시키기 위해 저는 Excel(또는 CSV, 스프레드시트) 기반으로 전환했습니다.

  • 스키마 고정: 컬럼명(예: material_name, standard, unit, qty, unit_price)을 합의하고 그대로 받기
  • 단위 정규화: ㎡, m², M2M2로 자동 매핑, 파이프는 m로 통일
  • 파싱 대신 로딩: 구조가 명확한 테이블은 읽기(load) 만 하면 끝
  • 검증 가능성: 누락/이상치 체크를 룰로 만들기 (예: 단가 음수 금지, qty=0 금지)

물론 '협력사에게 서식을 바꿔달라고 요청하는 건 현실적으로 어렵다'는 반론이 나올 수 있습니다.
이것이 바로 저의 '역제안'이 들어간 지점입니다. 저는 그들에게 변화를 '요구'하는 대신,
그들의 일을 '도와주는' 엑셀 템플릿을 먼저 제공했습니다.
'여기에 복사/붙여넣기만 하시면 됩니다'라는 제안을 거절할 협력사는 없었습니다.


🔁 전환 후 파이프라인 (간단/견고/예측 가능)

flowchart LR
  A[템플릿 Excel 수신] --> B[데이터 로딩 (pandas.read_excel)]
  B --> C[정규화 (단위/문자열/폭 파싱)]
  C --> D[유효성 검증 (누락/이상치)]
  D --> E[보고서 생성 (견적/발주/자재내역서)]
  E --> F[WIP 시스템 연동 (진행률/통계)]
  • 로딩: pd.read_excel() 한 번이면 끝납니다.
  • 정규화: 예) 폭 파싱 parse_width_m_from_standard("W1500×H1200") -> 1.5
  • 검증: 룰 위반 시 즉시 메시지 표시 → 현장에서 바로 수정
  • 생성: Openpyxl로 템플릿 셀에 값 채워넣기 → 서식 유지/다운로드 버튼 제공

🧩 핵심 코드 스니펫 (발췌)

1) 폭(m) 파싱 & 단위 정규화

import re

UNIT_MAP = {"㎡":"M2", "m²":"M2", "M²":"M2", "m2":"M2"}

def normalize_unit(u: str) -> str:
    return UNIT_MAP.get(str(u).strip(), str(u).strip().upper())

def parse_width_m_from_standard(std: str, default_m=2.0) -> float:
    if not std:
        return default_m
    s = str(std).upper()
    # 숫자 3~4자리 (mm) 우선 추출
    m = re.search(r"(\d{3,4})\s*(?:MM)?", s)
    if m:
        mm = float(m.group(1))
        return round(mm / 1000.0, 3)
    return default_m

2) 파이프 환산 비고 (“6m × n본”)

def pipe_notes(length_m: float, stock_len_m: float = 6.0) -> str:
    if length_m <= 0:
        return ""
    n = max(1, round(length_m / stock_len_m))
    return f"{int(stock_len_m)}m × {n}본"

3) 템플릿 쓰기 (Openpyxl)

from openpyxl import load_workbook

wb = load_workbook("templates/발주서템플릿.xlsx")
ws = wb.active

ws["F3"] = round(bom_df["길이(m)"].sum(), 3)  # 모델폭 총합(m)
for i, row in enumerate(result.itertuples(), start=10):
    ws[f"A{i}"] = row.material_name
    ws[f"B{i}"] = row.standard
    ws[f"C{i}"] = normalize_unit(row.unit)
    ws[f"D{i}"] = row.quantity
    ws[f"E{i}"] = row.unit_price
    ws[f"F{i}"] = pipe_notes(row.length_m)

wb.save("output/발주서_자동생성.xlsx")

📊 전환 효과 (숫자로 보는 성과)

  • 구현 속도: PDF 파서 튜닝에 주 23회 소요 → **Excel 템플릿 합의 후 유지보수 010분/주**
  • 오류율: 파싱 오류로 인한 필드 누락/오인식 사라짐 → 입력 검증 중심으로 변경
  • 확장성: 새로운 업체 추가 시 “템플릿 공유”로 끝 → 파서 추가 구현 불필요
  • 체감 효율: 견적/발주/자재내역서 생성 4시간 → 10분 (개발기 #1 참조)

💡 Aegis_BIMer의 실무형 기술 선택 4원칙

  • 데이터는 '파싱(Parsing)'하는가, '로딩(Loading)'하는가?
  • 서식은 '대응(Reactive)'하는가, '주도(Proactive)'하는가?
  • 검증은 '수동(Manual)'인가, '자동(Automated)'인가?
  • 협력사는 '불편'해지는가, '편리'해지는가?

결론: 가장 멋진 기술이 아니라, 가장 문제를 잘 푸는 기술이 최고의 기술이다.”


마무리

PDF 파싱을 버린 건 “포기”가 아니라 “선택”이었습니다.
Project Aegis가 해결해야 하는 문제는 정확하고 빠른 견적/발주/자재 관리입니다. 그 목표를 가장 단단하게 달성할 수 있는 길이 Excel 기반이었고, 실제로 속도/정확성/유지보수성 모두에서 성과를 얻었습니다.

 

다음 글에서는 이 Excel 파이프라인을 WIP(공정 관리) 시스템과 어떻게 연결했는지, 데이터가 견적 → 발주 → 진행률로 흐르는 구조를 사례와 함께 공유드리겠습니다.

반응형