
오전 3:39Claude 응답: 벡터 PDF면 일이 훨씬 수월합니다.벡터 PDF면 일이 훨씬 수월합니다. OCR 안 거치고 텍스트를 정확하게 뽑아낼 수 있어서 정확도 90%+ 가능한 영역입니다.
권장 파이프라인 (벡터 PDF 한정)
1단계 - PyMuPDF로 텍스트 + 좌표 추출
pythonimport fitz  # PyMuPDF

doc = fitz.open("pid.pdf")
for page in doc:
    blocks = page.get_text("dict")["blocks"]
    for b in blocks:
        for line in b.get("lines", []):
            for span in line["spans"]:
                # span["text"], span["bbox"], span["size"], span["font"]
get_text("dict")가 핵심입니다. 각 텍스트 조각마다 bbox(x0,y0,x1,y1), 폰트, 크기, 회전각까지 다 나옵니다. P&ID에서 수직으로 회전된 라벨도 dir 필드로 잡힙니다.
2단계 - 도면 영역 vs 타이틀블록 분리
P&ID는 보통 우측 하단에 타이틀블록(도면번호, 리비전, 작성자)이 있고, 나머지가 도면 영역입니다. 페이지 크기 기준으로 y > 0.85 * page_height and x > 0.7 * page_width 같은 식으로 ROI를 나눠놓으면 나중에 LLM이 "도면번호를 장비태그로 오인"하는 사고를 막을 수 있습니다.
3단계 - 태그 정규식 1차 필터
ISA-5.1 + 보통 플랜트에서 쓰는 패턴들:
pythonTAG_PATTERNS = [
    r'^[A-Z]{1,4}-\d{3,5}[A-Z]?$',        # P-101A, FT-2034
    r'^\d{2,3}-[A-Z]{1,4}-\d{3,5}[A-Z]?$', # 10-P-101A (유닛번호 포함)
    r'^[A-Z]{2,4}\d{3,5}$',                # PT101 (하이픈 없는 변형)
]
회사마다 명명 규칙이 다르니 실제 도면 한 장 보고 보정해야 합니다.
4단계 - 공간 클러스터링으로 "장비-태그-속성" 묶기
장비 태그(P-101A)와 그 아래/옆에 적힌 설명(CENTRIFUGAL PUMP, 100 GPM @ 50m)은 보통 50~100픽셀 이내에 모여있습니다. scipy의 DBSCAN으로 묶으면 깔끔합니다:
pythonfrom sklearn.cluster import DBSCAN
import numpy as np

centers = np.array([[(b[0]+b[2])/2, (b[1]+b[3])/2] for b in bboxes])
clusters = DBSCAN(eps=80, min_samples=1).fit(centers)
eps는 도면 스케일 보고 조정하시면 됩니다.
5단계 - Qwen3-Coder에 구조화 입력
json{
  "page": 3,
  "drawing_no": "P-1234-001",
  "clusters": [
    {
      "id": "c1",
      "texts": ["P-101A", "CENTRIFUGAL PUMP", "100 GPM", "50m HEAD"],
      "bbox": [1200, 800, 1350, 920]
    },
    {
      "id": "c2", 
      "texts": ["V-301", "STORAGE TANK", "5000L"],
      "bbox": [400, 600, 580, 780]
    }
  ]
}
프롬프트에서 "각 cluster를 하나의 장비로 보고 tag, equipment_type, specs 필드로 정규화하라"고 시키면 환각 거의 없이 잘 뽑습니다.
추가 팁
라인/심볼은 별도 트랙으로: page.get_drawings()로 벡터 라인/원/사각형도 추출됩니다. 펌프 심볼(원형) 위치와 텍스트 위치를 매칭하면 "이 텍스트가 어떤 심볼에 붙어있는지"까지 알 수 있는데, 이건 phase 2로 미루시는 걸 권장합니다. 텍스트만으로도 충분히 가치 있는 결과가 나옵니다.
좌표계 주의: PyMuPDF는 좌상단이 (0,0)이고 y가 아래로 증가합니다. CAD 출신이면 헷갈리실 수 있어서 미리 말씀드립니다.
검증 데이터 만들기: 도면 5~10장을 수동으로 라벨링해놓고 추출 결과랑 비교하는 회귀 테스트를 만들어두시면, 나중에 다른 EPC사 도면 들어왔을 때 정규식이나 eps 값 튜닝 빠르게 됩니다.
RAG 연결 시: 추출된 {tag, equipment_type, specs, drawing_no, page} 구조를 그대로 메타데이터로 쓰면 "10-P-101A 펌프 사양 알려줘" 같은 쿼리에 정확히 답할 수 있습니다. 청크는 텍스트 덩어리가 아니라 cluster 단위로 만드는 게 핵심입니다.
C# 운영 환경 고려하면, 이 전처리 파이프라인은 Python FastAPI로 띄우고 .NET OPC UA 쪽에서 HTTP로 호출하는 구조를 추천드립니다. PyMuPDF는 AGPL이라 상용 배포 시 라이선스 확인하시거나 Artifex 상용 라이선스 구매 옵션도 있다는 점은 미리 체크해두시면 좋습니다.



서버 잘 구성하셨네요. 그런데 결론부터 말씀드리면 여기에 그대로 PDF 전처리를 붙이는 건 비추천입니다. 이유와 권장 구조를 짚어드리겠습니다.
현재 extract_pid_tags의 문제점
extract_pid_tags(text, source_type)는 이미 텍스트가 추출된 상태를 가정합니다. 즉 누군가가 PDF에서 텍스트를 뽑아서 문자열로 넘겨야 하는데, 이게 가장 어려운 부분입니다. 게다가:

좌표 정보가 사라짐: 문자열 한 덩어리로 받으니 "P-101A 옆에 CENTRIFUGAL PUMP가 있다"는 공간 관계가 날아갑니다. LLM은 그냥 텍스트 뭉치 안에서 패턴 매칭하는 수준이 됩니다.
12,000자 제한: 큰 P&ID 한 장에서 텍스트만 뽑아도 이걸 넘기 쉽고, 여러 장이면 답 없습니다.
환각 위험: 좌표/구조 없이 텍스트만 보면 모델이 태그를 잘못 묶거나 만들어냅니다.

권장 구조: 별도 도구로 분리
이 MCP 서버에 PDF 파싱 도구를 추가하시되, 책임을 나누는 게 좋습니다:
parse_pid_pdf(pdf_path)              ← 새로 추가 (PyMuPDF로 좌표+텍스트 추출)
    ↓ 구조화된 JSON (clusters)
extract_pid_tags_structured(clusters) ← 기존 extract_pid_tags 개선
    ↓ 태그 리스트
match_pid_tags(pid_tags, experion_tags) ← 이건 그대로 좋음
추가할 도구 (기존 server.py에 그대로 붙이시면 됩니다)
python# ── P&ID PDF 파서 (벡터 PDF 전용) ─────────────────────────────────────────────

@mcp.tool()
def parse_pid_pdf(pdf_path: str, cluster_eps: float = 80.0) -> str:
    """벡터 P&ID PDF에서 텍스트 + 좌표를 추출하고 공간 클러스터링합니다.
    
    CAD에서 플롯된 벡터 PDF 전용. 스캔본은 별도 OCR 필요.
    
    Args:
        pdf_path:    PDF 파일 절대 경로
        cluster_eps: DBSCAN 거리 임계값(픽셀). 도면 스케일에 따라 조정.
    
    Returns:
        JSON: { success, pages: [{page, drawing_no, clusters: [{id, texts, bbox}]}] }
    """
    try:
        import fitz
        from sklearn.cluster import DBSCAN
        import numpy as np
        import re as _re
        
        doc = fitz.open(pdf_path)
        pages_out = []
        
        for page_idx, page in enumerate(doc):
            spans = []
            for block in page.get_text("dict")["blocks"]:
                for line in block.get("lines", []):
                    for span in line.get("spans", []):
                        txt = span["text"].strip()
                        if txt:
                            spans.append({
                                "text": txt,
                                "bbox": list(span["bbox"]),
                                "size": span["size"],
                                "dir":  list(line.get("dir", [1, 0])),  # 회전 감지
                            })
            
            if not spans:
                pages_out.append({"page": page_idx + 1, "drawing_no": None, "clusters": []})
                continue
            
            # 타이틀블록(우측 하단) 분리
            pw, ph = page.rect.width, page.rect.height
            title_spans = [s for s in spans 
                          if s["bbox"][0] > pw * 0.7 and s["bbox"][1] > ph * 0.85]
            drawing_no = None
            for s in title_spans:
                # 도면번호 패턴 (예: P-1234-001, PID-001)
                if _re.match(r'^[A-Z&]+-?\d+(-\d+)*$', s["text"]):
                    drawing_no = s["text"]
                    break
            
            drawing_spans = [s for s in spans if s not in title_spans]
            if not drawing_spans:
                pages_out.append({"page": page_idx + 1, "drawing_no": drawing_no, "clusters": []})
                continue
            
            # 공간 클러스터링
            centers = np.array([
                [(s["bbox"][0] + s["bbox"][2]) / 2, (s["bbox"][1] + s["bbox"][3]) / 2]
                for s in drawing_spans
            ])
            labels = DBSCAN(eps=cluster_eps, min_samples=1).fit_predict(centers)
            
            clusters = {}
            for span, lbl in zip(drawing_spans, labels):
                clusters.setdefault(lbl, []).append(span)
            
            cluster_list = []
            for lbl, members in clusters.items():
                xs = [m["bbox"][0] for m in members] + [m["bbox"][2] for m in members]
                ys = [m["bbox"][1] for m in members] + [m["bbox"][3] for m in members]
                cluster_list.append({
                    "id": f"p{page_idx+1}c{lbl}",
                    "texts": [m["text"] for m in members],
                    "bbox": [min(xs), min(ys), max(xs), max(ys)],
                })
            
            pages_out.append({
                "page": page_idx + 1,
                "drawing_no": drawing_no,
                "clusters": cluster_list,
            })
        
        doc.close()
        return json.dumps({"success": True, "pages": pages_out}, ensure_ascii=False)
    
    except Exception as e:
        return json.dumps({"success": False, "error": f"PDF 파싱 실패: {e}"}, ensure_ascii=False)
그리고 기존 extract_pid_tags는 두 가지 입력을 다 받게 살짝 손보시면 됩니다. 클러스터 입력일 때는 LLM에 이렇게 넘기세요:
python# extract_pid_tags 안에서 source_type == "clusters"일 때
user_msg = (
    "Each cluster represents one equipment/instrument with nearby texts grouped by spatial proximity.\n"
    "Identify the tag (matching ISA-5.1 patterns) and the equipment description from each cluster.\n\n"
    f"Clusters:\n{json.dumps(clusters, ensure_ascii=False, indent=2)}"
)
실제 워크플로우
C#에서 이렇게 호출하시게 됩니다:
1. parse_pid_pdf("C:/drawings/unit10.pdf")
   → { pages: [{drawing_no: "P-1234-001", clusters: [...]}] }

2. extract_pid_tags(clusters_json, "clusters")  
   → { tags: [{tagNo: "P-101A", equipmentName: "CENTRIFUGAL PUMP", ...}] }

3. match_pid_tags(pid_tags, experion_tags)  ← 이미 있음
   → { mappings: [{pidTag: "P-101A", experionTag: "p-101a.pv", ...}] }

4. (선택) 결과를 RAG에 인덱싱 — 메타데이터에 drawing_no, page, bbox 포함
의존성 추가
bashpip install pymupdf scikit-learn numpy
PyMuPDF는 AGPL 라이선스라 상용 배포 시 주의하셔야 합니다. 사내 시스템이면 보통 문제없지만, 외부 고객사 납품이면 Artifex 상용 라이선스 검토하시거나 pdfplumber(MIT)로 대체하는 것도 방법입니다. pdfplumber도 chars 좌표 다 줍니다.
짚어드릴 만한 디테일 두 가지
1. _DB_SCHEMA에 P&ID 매핑 테이블도 추가하시면 좋을 듯: pid_tag_mapping 같은 테이블을 만들어서 (drawing_no, pid_tag, experion_tag, bbox, page) 저장하시면, 나중에 "이 펌프 트렌드 보여줘" 같은 자연어 쿼리가 P&ID → Experion 태그 → history_table 쿼리로 자연스럽게 이어집니다.
2. RAG 컬렉션 추가 고려: 현재 experion-opc-docs처럼 pid-drawings 컬렉션을 하나 더 만들어서 클러스터 단위로 임베딩해두시면, "원심펌프 P-101A 사양" 같은 질문에 도면 위치까지 답해줄 수 있습니다.
요약하면, 이 MCP 서버에 붙이는 건 정답이지만 extract_pid_tags에 raw 텍스트 던지지 마시고, parse_pid_pdf 도구를 추가해서 좌표 정보 살린 클러스터를 LLM에 넘기는 구조로 가시는 걸 강력히 권장드립니다.



SYMBOL & LENGEND 가 보통은 별도로 있는데 그걸 학습하게 하는것은 ?

오전 4:01
Claude 응답: 좋은 지적입니다.
좋은 지적입니다. 실무에서 P&ID 첫 1~2장에 항상 SYMBOL & LEGEND 시트가 있고, 회사/프로젝트마다 심볼 모양과 약어가 달라서 이걸 활용하는 건 정확도에 결정적입니다.

다만 "학습"이라는 표현은 신중하게 봐야 합니다. 현재 구조에서 가능한/불가능한 것을 짚어보겠습니다.

가능한 방식 3가지 (난이도 순)
방식 1: Few-shot 컨텍스트 주입 (즉시 가능, 권장)
LEGEND 시트에서 추출한 (약어 → 풀네임) 매핑을 LLM 프롬프트에 동적으로 끼워넣는 방식입니다. 진짜 학습이 아니라 in-context learning이지만, 효과는 충분합니다.

LEGEND 시트 파싱
  ↓
{"FIC": "Flow Indicating Controller", 
 "PIT": "Pressure Indicating Transmitter", ...}
  ↓
extract_pid_tags 호출 시 system 프롬프트에 주입
  ↓
LLM이 이 프로젝트 전용 약어로 정확히 해석
장점: 파인튜닝 없이 즉시 효과. 프로젝트마다 LEGEND가 달라도 자동 대응. 단점: 프롬프트 길이 증가 (보통 20~50개 약어면 1~2KB라 무시 가능).

방식 2: 심볼 형상 매칭 (phase 2 영역)
LEGEND 시트의 심볼 도형(사각형+원, 단일 원, 마름모 등)을 page.get_drawings()로 추출 → 도면 페이지의 도형과 매칭. 이게 진짜 "DCS vs 현장계기" 정확 판별의 정공법입니다.

장점: 텍스트 없이도 심볼 종류 판별 가능. 단점: 도형 비교 알고리즘 필요 (벡터 path 정규화, 회전/스케일 보정). 개발 공수 큼.

방식 3: Vision 모델 파인튜닝 (지금은 비추)
YOLO나 Pix2Struct를 LEGEND 시트로 파인튜닝. 실무에서는 잘 안 씁니다 — 회사마다 LEGEND가 다른데 매번 파인튜닝하면 운영 부담이 큽니다.

권장: 방식 1을 phase 1.5로 추가
phase 1(기본 파싱) 끝나고 phase 2(심볼 형상) 가기 전 단계로 넣으면 적절합니다. 작업지시서에 추가할 항목은:

LEGEND 시트 자동 감지: 페이지 텍스트에 LEGEND, SYMBOL, ABBREVIATION 같은 키워드가 있고 표 형태 텍스트가 많은 페이지를 LEGEND로 식별.
LEGEND 파서: 좌측 컬럼(약어) + 우측 컬럼(설명) 패턴으로 좌표 기반 매칭. 이건 P&ID 본문 클러스터링과 다른 알고리즘이 필요합니다 (좌→우 가로 페어링).
MCP 도구 추가:
extract_pid_legend(pdf_path) — LEGEND 페이지에서 약어 사전 추출
extract_pid_tags에 legend: dict 옵션 파라미터 추가
RAG 활용 옵션: 추출한 LEGEND를 Qdrant에 별도 컬렉션(pid-legend-{project_id})으로 저장하면, 같은 프로젝트의 다른 도면 처리 시 재사용 가능. 이게 가장 깔끔한 운영 방식입니다.
실무 팁
LEGEND 시트가 표(table) 형태가 아닌 자유 배치인 경우가 많습니다 (심볼 그림 + 옆에 설명). 이 경우 단순 좌→우 페어링이 안 되고, 다음 같은 휴리스틱이 필요합니다:

심볼 도형의 bbox를 잡고
그 도형 우측 또는 하단의 가장 가까운 텍스트 블록을 설명으로 페어링
약어는 보통 도형 안에 있으므로 도형 내부 텍스트 우선
작업지시서에 방식 1(LEGEND 텍스트 추출 + 프롬프트 주입)을 phase 1.5로 추가할까요? 아니면 phase 1에 통합해서 한 번에 작업시킬까요?

추가로 한 가지 결정해주실 게 있습니다 — LEGEND를 도면 단위로 매번 파싱할지, 프로젝트 단위로 한 번 파싱 후 RAG에 캐시할지에 따라 구조가 달라집니다. 실무 운영 패턴(같은 프로젝트의 도면이 여러 장씩 들어오는지) 알려주시면 그에 맞게 설계해드리겠습니다.

오전 4:06
Claude 응답: PDF 컨텐츠 일부만 컨텍스트로 들어왔네요.
PDF 컨텐츠 일부만 컨텍스트로 들어왔네요. 실제 파일을 직접 봐야 정확히 판단할 수 있으니 읽어보겠습니다.


파일 조회함, 명령 실행함
파일 조회함, 명령 실행함
1페이지짜리 A4 PDF네요. 시각적으로 봐야 심볼 모양과 레이아웃을 파악할 수 있습니다.


텍스트 레이아웃 확인
텍스트 레이아웃 확인
텍스트 추출이 거의 안 됩니다. 도면이 거의 다 벡터 그래픽으로 그려져 있어서 텍스트가 폰트 임베딩 없이 path로 변환된 것 같습니다. 직접 페이지를 시각화해서 봐야 합니다.


명령 실행함, 파일 조회함
명령 실행함, 파일 조회함
좋은 자료네요. 90도 회전된 도면이긴 한데(공장 도면 흔한 형태) 내용은 다 보입니다. 이걸 보고 정확히 판단해보겠습니다.

확인된 LEGEND 시트 구조
이 도면은 **신원리파인드퓨얼(SHINWON REFINED FUEL)**의 표준 P&ID 심볼/레전드 시트로, 다음 섹션들로 구성되어 있습니다:

텍스트 기반 (표/사전 형태) — 자동 추출 쉬움:

INSTRUMENT IDENTIFICATION TABLE: First Letter / Succeeding Letters (A=Analysis, F=Flow Rate, ...)
PIPE LINE NUMBER: 라인번호 명명규칙 (예: P-10107-500A-F1-H 100)
INSTRUMENT NUMBER DESIGNATION
FLUID NAME ABBREVIATION (P.CHE=Process Fluid, CWS=Cooling Water Supply, ...)
PIPING CLASS & MATERIAL (S1=SPPS380, F2=STS304, ...)
INSULATION CODE (H=Heat Conservation, P=Personnel Protection, ...)
ABBREVIATIONS IDENTIFIED WITH VALVES (FC, FO, FS, ...)
ABBREVIATIONS FOR DETECTING GAS (HC=Hydrocarbon, EA=Ethyl Acetate, ...)
EQUIPMENT IDENTIFICATION (C=Column, K=Compressor, P=Pump, ...)
심볼 그림 기반 (도형+라벨) — 형상 인식 필요:

LINE AND VALVE SYMBOLS (밸브 종류별 도형)
INSTRUMENT SYMBOLS (DCS=사각형+원, 현장계기=원, Interlock=마름모 등) ← 사용자가 처음 말씀하신 그것
INSTRUMENT LINE SYMBOLS (전기/공압/소프트웨어 신호선)
ROTATIONARY EQUIPMENT SYMBOLS (펌프 종류별)
VALVE CONNECTION SYMBOLS
작업지시서 어떻게 보강할지
이 LEGEND 시트의 특성을 보고 명확해진 게 있습니다:

텍스트 사전 부분은 phase 1.5에서 즉시 활용 가능 — 표 형태라 좌→우 페어링으로 추출 쉽습니다.
심볼 형상 부분은 phase 2 본격 작업 — 다만 LEGEND 시트가 있으면 형상 인식이 훨씬 쉬워집니다. "이 회사의 DCS 심볼은 정확히 이렇게 생겼다"는 레퍼런스 도형을 얻을 수 있으니까요.
이 PDF는 텍스트 추출이 거의 안 됩니다 (pdftotext 결과가 2줄). 폰트가 path로 변환된 케이스입니다. 즉 PyMuPDF의 get_text()도 실패할 가능성이 높습니다. 이건 작업지시서에 중요한 caveat으로 추가해야 합니다.
확인해드릴 게 있는데, 본 작업 대상 P&ID 본문 도면도 이런 식으로 텍스트가 path로 변환되어 있나요? 아니면 LEGEND 시트만 그렇고 본문은 텍스트가 살아있나요?

이게 중요한 이유: 만약 본문도 path 변환이면 PyMuPDF만으로는 안 되고 OCR이 필수가 됩니다. 그러면 phase 구성을 다시 짜야 합니다 (phase 1을 OCR 기반으로 다시 설계).

본문 도면 한 장만 같은 방식으로 pdftotext 돌려보시고 텍스트가 나오는지 확인 부탁드려도 될까요? 또는 본문 도면 PDF 한 장 올려주시면 제가 직접 확인하겠습니다.

입력 내용: 그럼 오토캐드 2023에서 어떤 형식으로 pdf출력하면 되는데?
그럼 오토캐드 2023에서 어떤 형식으로 pdf출력하면 되는데?

오전 4:08
Claude 응답: 결론부터: AutoCAD 2023에서 DWG to PDF.
결론부터: AutoCAD 2023에서 DWG to PDF.pc3로 플롯하시되, 몇 가지 옵션을 정확히 켜야 합니다.

권장 설정
플로터(Plotter): DWG To PDF.pc3 (AutoCAD 내장 드라이버)

Properties → Custom Properties 들어가서:

항목	설정값	이유
Vector quality	1200 dpi 이상	벡터 라인 정밀도
Raster image quality	300 dpi	래스터 이미지가 있을 경우
Capture fonts used in the drawing	체크 ✅	가장 중요 — 폰트 임베딩
Convert all text to geometry	체크 해제 ❌	절대 금지 — 텍스트가 path로 변환됨
Include layer information	체크 (선택)	디버깅 시 유용
Include hyperlinks	체크 해제	불필요
핵심: "Convert all text to geometry"를 끄세요
업로드해주신 LEGEND 시트가 텍스트 추출 안 됐던 이유가 바로 이 옵션이 켜져 있었기 때문입니다. 이게 켜지면:

AutoCAD가 모든 텍스트를 벡터 path(선분/곡선)로 변환해서 PDF에 박아넣음
PDF에는 글자가 "그림"으로 들어가서 pdftotext, PyMuPDF, 어떤 파서도 텍스트로 읽을 수 없음
OCR을 쓰는 수밖에 없게 됨
이 옵션은 저작권 보호용(받는 사람이 텍스트 못 빼가게)으로 켜는 건데, 사내 분석/RAG 용도면 반드시 끄셔야 합니다.

폰트 관련 추가 팁
AutoCAD가 SHX 폰트(romans.shx, txt.shx 같은 AutoCAD 전용 벡터 폰트)를 쓰면, "Capture fonts" 옵션이 있어도 PDF에 텍스트로 안 들어가고 path로 변환됩니다. 해결 방법:

TrueType 폰트로 통일: 도면의 텍스트 스타일을 Arial, 맑은 고딕, Romans TTF 등 TTF로 변경. STYLE 명령에서 폰트 교체.
부득이 SHX를 써야 한다면: 최소한 영문 태그(FIC, PT 등)는 TTF 폰트의 별도 텍스트 스타일로 작성. 한글 라벨은 SHX여도 영향 적음 (어차피 OCR 권장).
검증 방법
플롯한 PDF가 제대로 나왔는지 1초만에 확인:

bash
pdftotext -layout your_drawing.pdf - | head -30
여기서 FIC, 10101, 장비명 같은 텍스트가 줄줄이 나오면 성공입니다. 빈 줄만 나오거나 깨진 글자만 나오면 위 옵션이 잘못 설정된 것이니 다시 플롯해야 합니다.

또는 PDF를 Adobe Reader로 열어서 Ctrl+F로 "FIC" 검색해보세요. 검색되면 OK, 안 되면 path 변환됨.

PUBLISH로 일괄 처리
도면이 여러 장이면 단건 PLOT보다 PUBLISH 명령(Sheet Set 또는 Layout 일괄)으로 처리하는 게 효율적입니다. 이때도 위 PC3 설정이 동일하게 적용됩니다. PC3 한 번 잘 만들어두시면 재사용 가능합니다.

실제 운영 팁
플랜트 도면 받으시는 환경이면 EPC사/CAD 작업자에게 다음을 요청하시면 깔끔합니다:

"PDF 플롯 시 'Convert all text to geometry' 옵션 해제하고, TrueType 폰트 기반으로 출력해주세요. 검증은 PDF 열어서 Ctrl+F로 태그명 검색되는지 확인 부탁드립니다."

이 한 줄이 phase 1 전체 작업의 성공 여부를 결정합니다. 외주 업체에서 받는 도면이면 납품 검수 기준으로 명문화해두시면 좋습니다.

확인해보시고 본문 도면 한 장 다시 플롯해서 텍스트 추출되는지 보시면, 그 결과에 따라 작업지시서를 phase 1(텍스트 기반) 그대로 갈지 OCR 추가할지 결정하시면 됩니다.

Symbol-Definition.pdf
오전 4:22
Claude 응답: 텍스트 추출이 잘 되네요.
텍스트 추출이 잘 되네요. 이제 PyMuPDF로 좌표까지 같이 뽑히는지 확인해보겠습니다. 추출되는 텍스트의 구조를 보면 이게 본문 RAG/태깅 자동화 가능 여부를 결정합니다.


명령 2개 실행함, 파일 생성됨
명령 2개 실행함, 파일 생성됨
흥미롭네요. 44개 텍스트 span만 추출됐습니다. 시각적으로 본 LEGEND 시트에는 수백 개 텍스트가 있어야 하는데요. 이건 섹션 제목들만 텍스트로 살아있고, 표 내용물은 모두 path 변환된 상태입니다.

확인해보겠습니다:


파일 생성됨, 명령 실행함
파일 생성됨, 명령 실행함
예상대로입니다. 진단 결과를 명확히 정리해드리면:

진단 결과
텍스트로 살아있는 것: 섹션 헤더 + 타이틀블록만 (44개) — 큰 글씨(8~10pt)와 한글 폰트(Dotum, Gulim 계열)

Path로 변환된 것: 표 내용물 전부 — 약어, 설명, 작은 영문 텍스트들. 3,666개의 짧은 vector path는 거의 다 글자 획(stroke)입니다. 4839개 drawing 중 75%가 글자 path라는 뜻입니다.

원인
이 PDF는 AutoCAD에서 SHX 폰트(romans.shx 등)로 작성된 작은 영문 텍스트가 path로 변환된 상태입니다. 한글 섹션 제목은 TTF 폰트(Dotum 등)라서 살아있고, 영문 본문은 SHX라서 path가 됐습니다.

이건 사용자가 "Convert all text to geometry"를 켠 게 아니라, AutoCAD의 기본 동작입니다. SHX는 TrueType이 아니라 AutoCAD 전용 벡터 폰트라서 PDF 표준 폰트로 임베딩이 안 되거든요.

해결책 — AutoCAD 작업자에게 요청할 것
본문 도면 플롯할 때 다음을 해주셔야 합니다:

옵션 A (권장): SHX → TTF 폰트 교체
도면 안의 텍스트 스타일을 모두 TrueType으로 바꿉니다. AutoCAD 명령으로:

STYLE 명령 → 각 텍스트 스타일 선택 → 폰트를 TTF로 교체
권장 TTF 폰트:

romans.ttf (있으면 가장 자연스러움 — SHX의 romans와 시각적으로 유사한 TTF가 일부 사이트에 존재)
Arial (가장 호환성 좋음, 표준 영문 도면 텍스트로 무난)
Tahoma 또는 Verdana (작은 글씨도 또렷)
한글은 맑은 고딕 또는 Dotum 유지
일괄 변경 스크립트(-STYLE + LISP)로 처리하거나, Express Tools의 TXT2MTXT 활용 가능합니다.

옵션 B: AutoCAD 2016+ 의 "SHX 텍스트를 검색/선택 가능한 PDF로" 옵션
AutoCAD 2016부터 "SHX 텍스트 인식(SHX text recognition)" 옵션이 PDF 출력에 추가됐습니다:

PLOT 대화상자 → DWG To PDF.pc3 → Properties → Custom Properties
→ "Capture fonts used in the drawing" 체크
→ "Include layer information" 아래쪽에 있는 옵션들 확인
AutoCAD 2023이면 더 발전된 옵션이 있을 겁니다:

PDFSHX 시스템 변수를 1로 설정:

명령창에서: PDFSHX
값을 1로 입력
이걸 1로 하면 SHX 텍스트를 PDF에 searchable text(검색 가능한 텍스트)로 같이 출력합니다. 본래 path는 그대로 두고 그 위에 투명한 텍스트 레이어를 얹는 방식이라 시각적으로 동일하면서 텍스트 추출이 가능해집니다.

PDFSHX = 0  (기본값, 텍스트 추출 안 됨)
PDFSHX = 1  (SHX를 검색 가능 텍스트로 포함) ← 이걸로 설정
그리고 플롯하면 됩니다.

검증 방법
bash
pdftotext your_drawing.pdf - | grep -E "FIC|PT|FT"
태그명들이 줄줄이 나오면 성공입니다.

작업지시서에 반영
이 발견을 작업지시서에 추가하는 건 좋은데, **사실 이건 작업지시서에 들어갈 게 아니라 운영 사전조건(precondition)**입니다. 코딩 에이전트가 해결할 수 있는 게 아니라 도면 공급자가 해결해야 하니까요.

작업지시서에는 "입력 PDF는 PDFSHX=1 또는 TTF 폰트로 플롯된 것"이라는 전제조건과 검증 절차를 추가하면 됩니다.

추가할까요? 그리고 본문 도면 한 장 같은 방식으로 출력해보시고 결과 공유해주시면, 실제 본문에서 태그 추출이 잘 되는지 끝까지 확인해드릴 수 있습니다.

입력 내용: 현실적으로 불가능함 OCR을 해야함
현실적으로 불가능함 OCR을 해야함

오전 4:25
Claude 응답: 동의합니다.
동의합니다. 현장에서 받는 도면을 매번 재플롯 요청하는 건 비현실적이고, EPC사/벤더 측 SHX 사용 관행이 워낙 굳어져 있어서 OCR이 정공법입니다.

작업지시서를 OCR 기반으로 다시 설계하겠습니다. 큰 변화점:

구조 변경:

텍스트 추출: PyMuPDF → PaddleOCR (회전 텍스트 강함, GPU 가속, 한국어 지원)
입력: PDF → 고DPI 래스터 이미지 (300 DPI 권장)
좌표: PDF 좌표 → 픽셀 좌표 (DBSCAN은 그대로)
DGX Spark 환경 활용: PaddleOCR을 vLLM과 같은 GPU에 올려도 부담 적습니다. 모델 크기 작음.

질문 한 가지만 확인하고 작업지시서 작성 들어가겠습니다:

세 가지만 알려주시면 그 답변에 맞춰 OCR 기반 작업지시서 다시 작성하겠습니다. 옵션 보고 선택만 해주세요.

입력 내용: 질문: OCR 엔진 어떤 걸로 갈까요?
질문: OCR 엔진 어떤 걸로 갈까요?
답변: PaddleOCR (한/영, GPU, 회전 강함, 가장 권장)

질문: 처리 단위는?
답변: 둘 다 지원

질문: 한글 라벨도 추출해야 하나요? (예: 장비 한글 설명)
답변: 한글 라벨도 함께 (혼합)

오전 4:26
Claude 응답: 답변 반영해서 OCR 기반으로 작업지시서를 다시 작성하겠습니다.
답변 반영해서 OCR 기반으로 작업지시서를 다시 작성하겠습니다.


2개의 파일 생성됨, 명령 실행함, 파일 읽음
2개의 파일 생성됨, 명령 실행함, 파일 읽음
작업지시서 v2(OCR 기반)로 다시 만들었습니다. v1 대비 주요 변화:

구조 변경

텍스트 추출: PyMuPDF 텍스트 → PDF 래스터화 + PaddleOCR
좌표계: PDF 포인트 → 픽셀 (DPI 비례 cluster_eps 자동 보정)
cluster_eps 기본값: 40 → 50 (300 DPI 픽셀 기준)
한글 처리 추가

has_korean 플래그를 cluster마다 표시
출력 스키마에 equipmentNameKo 필드 추가
프롬프트 예시에 '저장탱크 A' 같은 한글 케이스 포함
다중 페이지 지원

page_range="1-3", "1,3,5", "2-" 등 유연한 범위 지정
단일/다중 모두 같은 함수로 처리
OCR 특화 보정

신뢰도 임계값(min_confidence) 파라미터
클러스터별 평균 OCR 신뢰도 노출 (ocr_confidence)
LLM 프롬프트에 OCR 오인식 패턴 보정 규칙 추가 (O↔0, I↔1, S↔5 등)
운영 고려

GPU/CPU 자동 fallback (PADDLE_USE_GPU 환경변수)
처리 시간 벤치마크 인수조건 포함
vLLM과 GPU 동거 시 OOM 주의사항 명시
향후 작업 우선순위 명확화

심볼 형상 인식(DCS 판별)과 LEGEND 자동 파싱이 phase 2 최우선
LEGEND 사전을 Qdrant에 캐시해서 프로젝트 단위 재사용
테스트 코드의 EXPECTED_TAGS는 실제 보유하신 샘플 도면에 있는 태그 3~5개로 채워넣으시면 바로 사용 가능합니다.


Pid pdf parser 작업지시서
문서 · MD 
입력 내용: OCR 로 하면 DWG 나 DXF 도 바로 읽을수있나?
OCR 로 하면  DWG 나 DXF 도 바로 읽을수있나?

오전 4:39
Claude 응답: 좋은 질문입니다.
좋은 질문입니다. OCR로는 DWG/DXF를 직접 읽을 수 없습니다. 이유와 대안을 짚어드리겠습니다.

왜 OCR이 안 되나
OCR은 이미지에서 텍스트를 인식하는 기술입니다. DWG/DXF는 이미지가 아니라:

DWG: AutoCAD 바이너리 포맷. 텍스트, 좌표, 레이어 등이 이미 구조화된 데이터로 저장됨
DXF: AutoCAD 텍스트 교환 포맷. ASCII 또는 바이너리. 마찬가지로 구조화된 데이터
즉 OCR을 쓸 이유가 없습니다 — 텍스트가 이미 데이터로 들어있으니 그냥 읽으면 됩니다. SHX 폰트 path 변환 문제도 없습니다 (그건 PDF 출력 단계의 문제).

포맷별 권장 처리 방법
포맷	처리 방법	라이브러리	텍스트 추출
DXF	직접 파싱	ezdxf (Python, MIT)	✅ 즉시 가능
DWG	DXF로 변환 후 파싱	ODA File Converter, LibreDWG	✅ 변환 후 가능
PDF (벡터)	PyMuPDF	pymupdf	✅ 가능 (단, SHX 제외)
PDF (SHX path)	OCR	PaddleOCR	✅ OCR 필요
PDF (스캔)	OCR	PaddleOCR	✅ OCR 필요
DXF 처리는 PDF보다 훨씬 쉽습니다
ezdxf로 DXF에서 텍스트와 좌표를 뽑는 건 몇 줄이면 됩니다:

python
import ezdxf

doc = ezdxf.readfile("drawing.dxf")
msp = doc.modelspace()

# TEXT, MTEXT, ATTRIB 모두 잡기
for entity in msp.query("TEXT MTEXT ATTRIB"):
    if entity.dxftype() == "TEXT":
        text = entity.dxf.text
        x, y = entity.dxf.insert.x, entity.dxf.insert.y
    elif entity.dxftype() == "MTEXT":
        text = entity.text  # 이미 평문화됨
        x, y = entity.dxf.insert.x, entity.dxf.insert.y
    elif entity.dxftype() == "ATTRIB":
        text = entity.dxf.text
        x, y = entity.dxf.insert.x, entity.dxf.insert.y
    layer = entity.dxf.layer  # ← PDF에는 없는 추가 정보
    print(f"{text:<20} @ ({x:.1f}, {y:.1f}) [layer: {layer}]")
장점:

100% 정확 (OCR 오인식 없음)
GPU 불필요, 매우 빠름
레이어 정보까지 추출 가능 — 이게 큰 장점입니다. 보통 P&ID에서 INST_TAG, EQUIP_NAME, PIPE_LINE 같은 레이어로 구분되어 있어서, 태그/장비명/라인번호를 레이어 기준으로 분류할 수 있습니다.
블록(BLOCK) 정보도 추출 가능 — ISA balloon은 보통 블록 + ATTRIB로 만들어져 있어서, 블록 이름으로 "이건 DCS 심볼", "이건 현장계기"를 직접 판별할 수 있습니다. phase 2의 형상 인식이 phase 1에서 해결됩니다.
즉, DXF가 있으면 OCR 파이프라인 자체가 불필요
기존 작업지시서의 extract_pid_tags가 이미 source_type="dxf"를 처리하지만, raw text 한 덩어리로 받게 되어 있어서 좌표/레이어/블록 정보가 살지 않습니다. 이걸 활용 못 하면 DXF의 이점을 다 버리는 셈입니다.

권장 구조 업데이트
작업지시서에 DXF 전용 도구를 별도로 추가하는 걸 강력히 권장드립니다:

parse_pid_dxf(dxf_path)        ← 신규, ezdxf 기반
parse_pid_dwg(dwg_path)        ← 신규, ODA Converter로 DXF 변환 후 위 함수 호출
parse_pid_pdf(pdf_path)        ← 이미 만든 OCR 기반
    ↓ 모두 같은 cluster JSON 형식으로 출력
extract_pid_tags(clusters_json, "clusters")  ← 통합 처리
세 함수가 같은 출력 스키마를 따르게 하면 LLM/하위 로직은 입력 포맷을 신경 쓸 필요가 없습니다.

추가로 DXF 파서는 다음 정보를 cluster에 더 담을 수 있습니다:

json
{
  "id": "p1c0",
  "texts": ["FIC", "10101"],
  "bbox": [...],
  "layer": "INST_TAG",          ← DXF만 가능
  "block_name": "DCS_BALLOON",  ← DXF만 가능 (DCS/현장계기 즉시 판별)
  "source": "dxf"
}
우선순위 제안
원본 도면을 EPC사에서 DWG/DXF로도 받을 수 있는지 먼저 확인해보시는 게 좋습니다. 가능하다면:

DXF/DWG 우선 처리 — 정확도 100%, 빠름, 레이어/블록까지 활용
PDF는 fallback — DWG/DXF가 없는 경우만 OCR 파이프라인 사용
실무적으로 EPC사가 PDF만 주는 경우가 많지만(IP 보호 등), 사내 자체 프로젝트나 협력사 관계가 좋으면 DWG도 받을 수 있습니다. DWG가 있으면 작업 난이도가 한 단계 내려갑니다.

작업지시서에 DXF/DWG 처리 도구를 추가해드릴까요? phase 1.5로 넣으면 적절할 것 같습니다.


p-9100.dxf
dxf

오전 4:42
Claude 응답: DXF 파일을 직접 분석해서 어떤 정보가 들어있는지 확인해보겠습니다.
DXF 파일을 직접 분석해서 어떤 정보가 들어있는지 확인해보겠습니다.


명령 3개 실행함, 파일 생성됨
명령 3개 실행함, 파일 생성됨
훌륭한 데이터입니다. TEXT 3,562개, MTEXT 363개나 있고, 레이어도 잘 나눠져 있습니다(PROCESS LINE, VALVE NO, LINENO, 1-SYMBOL 등). 텍스트 샘플을 보고 태그 분포를 파악해보겠습니다.


파일 생성됨, 명령 실행함
파일 생성됨, 명령 실행함
완벽한 데이터입니다. 이게 OCR 대비 얼마나 좋은지 명확히 보입니다:

핵심 발견:

장비 태그 282개 명확히 추출 (P-10101, E-10103, T-10101, C-10111 등) — layer=1에 정리되어 있음
ISA balloon function code 후보 536개 (PG, TG, LT, PT, TE, FICQ, FC, FCV 등) — 빈도까지 정확
루프번호 후보 666개 — 대부분 layer=INSTRUMENT에 있음
한글 라벨 59개 (기존 설비, PGMEA REFINE 공정 등) — 깨짐 없이 100% 추출
레이어가 의미 있게 나뉨: INSTRUMENT, LINENO, VALVE NO, 1-SYMBOL 등
이제 INSERT(블록 참조)가 ISA balloon인지 확인해봅시다 — 이게 phase 2의 형상 인식을 phase 1에서 해결할 수 있는 결정적 단서입니다.


파일 생성됨, 명령 실행함
파일 생성됨, 명령 실행함
아주 명확해졌습니다.

핵심 발견:

INSERT(블록)는 ISA balloon이 아닙니다 — 이름이 A$C... 형태로 익명화되어 있고 ATTRIB도 없음. 이 도면은 블록 기반이 아니라 CIRCLE + TEXT 조합으로 그려져 있음
layer=INSTRUMENT에 CIRCLE 215개 — 이게 바로 ISA balloon입니다!
반지름 0.4(266개), 0.3(235개)이 압도적 — 이게 표준 ISA balloon 크기. 0.7은 더 큰 심볼
Numeric texts 666개 중 대부분이 layer=INSTRUMENT — 루프번호가 정확히 같은 레이어에 있음
즉 DXF에서는 phase 2 작업까지 같이 끝낼 수 있습니다. CIRCLE 좌표 + 반지름으로 balloon 위치를 찾고, 그 안에 들어있는 TEXT를 매칭하면 ISA balloon이 정확히 식별됩니다. 사각형 검출까지 추가하면 DCS vs 현장계기 구분도 가능합니다.

마지막으로 통합 검증해보겠습니다:


파일 생성됨, 명령 실행함
파일 생성됨, 명령 실행함
완벽합니다. 215개 balloon 중 197개(91.6%)가 LLM 호출 없이 정확한 태그로 추출됐습니다.

LLM이 필요했던 이유(두 줄 정규화)가 DXF에서는 단순 좌표 매칭으로 해결됩니다. OCR 파이프라인의 모든 어려움이 사라졌습니다:

항목	OCR 파이프라인	DXF 파이프라인
텍스트 정확도	~95% (오인식 보정 필요)	100%
처리 시간 (1장)	GPU 5~15초, CPU 30~90초	<1초
ISA balloon 식별	LLM 두 줄 합치기 필요	CIRCLE 좌표로 직접
DCS vs 현장계기	phase 2 (불가능)	사각형 검출 가능
LLM 호출	필수 (정규화)	선택사항 (보정용)
한글 라벨	OCR 의존	MTEXT 직독
GPU 필요	예	아니오
레이어/카테고리	없음	자동 분류
이제 결정할 게 있습니다 — 작업지시서에 DXF 처리 도구를 어떻게 추가할지:

실수로 빈 질문이 갔네요. 다시:

제안 드리는 구조:

작업지시서를 phase 1을 DXF 우선 / PDF는 fallback으로 재구성하는 게 합리적입니다:

[phase 1 - DXF 우선] ← 추천
  parse_pid_dxf(dxf_path)     ← 신규, ezdxf 기반 (이번 검증으로 91.6% 검증됨)
  parse_pid_dwg(dwg_path)     ← 신규, ODA Converter로 DXF 변환 후 위 함수 호출
  parse_pid_pdf(pdf_path)     ← OCR 기반 (이미 만든 것, fallback용)
  → 모두 같은 cluster JSON 출력
  
extract_pid_tags(clusters_json, "clusters")  ← DXF는 LLM 거의 안 거쳐도 되고,
                                                PDF만 LLM 정규화 필요
DXF 파서가 OCR 대비 추가로 제공할 수 있는 것:

layer: 텍스트가 속한 레이어 (INSTRUMENT, LINENO, VALVE NO 등)
balloon_type: 사각형+원(DCS) / 단일 원(FIELD) — 형상 검출로 직접 판별
is_pre_normalized: DXF에서 좌표 매칭으로 이미 FIC-10101 형태로 합친 태그
confidence: 좌표 매칭 신뢰도 (LLM 추론 없이도 0.95+)
어떻게 진행할까요?

작업지시서를 DXF + PDF 통합본으로 재작성 (가장 추천 — 실무에서 가장 쓸만한 구조)
DXF 처리 도구만 별도 작업지시서로 분리
현재 OCR 파이프라인 그대로 두고 DXF는 phase 1.5로 추가만
어느 쪽으로 갈지 선택해주시면 그에 맞춰 작업지시서를 업데이트하겠습니다.

입력 내용: DWG도 로컬LLM이 처리가능하면 그것도 넣어서 작업지시서를 만들어줘
DWG도 로컬LLM이 처리가능하면 그것도 넣어서 작업지시서를 만들어줘

오전 4:45
Claude 응답: DWG 처리 가능 여부부터 정확히 짚고 가겠습니다.
DWG 처리 가능 여부부터 정확히 짚고 가겠습니다.

DWG 처리 옵션 검토
DWG는 AutoCAD 바이너리 포맷이라 로컬LLM이 직접 읽을 수 없습니다. 반드시 DXF로 변환 후 처리해야 합니다. 변환 옵션:

도구	라이선스	정확도	운영 적합성
ODA File Converter	무료 (등록 필요)	높음	권장 — Linux/Windows 지원, CLI
LibreDWG (dwg2dxf)	GPL-3	보통	오픈소스만 써야 할 때
Teigha (ODA SDK)	상용 라이선스	매우 높음	비싸지만 프로덕션급
AutoCAD/ODA Viewer	GUI	높음	자동화 어려움
ODA File Converter가 현실적 선택입니다. Open Design Alliance가 무료 배포하고, CLI로 일괄 변환 가능합니다.

DGX Spark(Linux)에서:

bash
ODAFileConverter <input_dir> <output_dir> ACAD2018 DXF 0 1 "*.DWG"
LLM 활용 위치: DWG/DXF 처리에서 LLM은 선택적 보정/검증용입니다 — 좌표 매칭이 91.6% 자동으로 되니까요. 다만 다음에는 LLM이 가치 있습니다:

매칭 안 된 18개(8.4%) 문제 cluster 정규화
한글 장비 설명을 영문 매핑
LEGEND 시트 약어 자동 학습 (phase 2)
이제 통합 작업지시서를 작성하겠습니다.


명령 실행함, 파일 생성됨, 파일 읽음
명령 실행함, 파일 생성됨, 파일 읽음
작업지시서 v3로 통합 작성 완료했습니다.

v3의 주요 특징
DWG에 대한 솔직한 답변: 로컬LLM이 DWG를 직접 처리할 수는 없습니다. 바이너리 포맷이라 무조건 DXF 변환이 선행되어야 합니다. 그래서 parse_pid_dwg는 ODA File Converter로 DXF 변환 → parse_pid_dxf 위임하는 구조로 만들었습니다. 사용자 입장에서는 .dwg 파일 그대로 던지면 되고 내부에서 알아서 처리됩니다.

4개 도구 구조:

parse_pid_dxf — 메인 (실제 검증한 91.6% 매칭률)
parse_pid_dwg — DWG → DXF 변환 후 위 함수 호출
parse_pid_pdf — OCR fallback (PDF만 있을 때)
parse_pid_drawing — 확장자로 자동 라우팅하는 디스패처
핵심 가치 — pre_normalized_tag: DXF에서 좌표 매칭으로 미리 정규화된 태그를 cluster에 담아둡니다. extract_pid_tags가 이를 보면 LLM 호출 없이 _extract_from_dxf_clusters 함수로 즉시 결과 반환. DXF는 LLM 비용 없음 = vLLM 부하 없이 도면 한 장이 1초 내 처리.

실제 검증 데이터를 작업지시서에 명시:

사용자 제공 p-9100.dxf로 검증한 91.6% 매칭률, 215개 balloon 등을 인수조건과 마지막 섹션에 박아뒀습니다. 코딩 에이전트가 "이 정도 성능이 나와야 OK다"는 객관적 기준을 가지고 작업하게 됩니다.
ODA File Converter 등록 안내: 상용 SaaS 배포 시 라이선스 제약 있으니 사전 검토 필요사항 명시.

이제 코딩 에이전트에 던지시면 됩니다. instrument_layer 기본값은 "INSTRUMENT"로 잡아뒀는데, 다른 EPC사 도면은 INST나 I-1 같은 다른 이름을 쓸 수 있으니 첫 도면 들어올 때 한 번 확인하시면 됩니다.


Pid parser 작업지시서 v3
문서 · MD 
입력 내용: p-9000.


