# DXF P&ID 추출 개선 2차 — 배관번호·펌프 태그 누락 버그 수정 작성일: 2026-05-17 작성: Claude Sonnet 4.6 --- ## 문제 개요 P&ID DXF 파일에서 아래 태그들이 추출되지 않는 버그 보고: - `P-10101` (10차 펌프 장비 태그) - `P-10101-25A-F1A-n` (10차 프로세스 배관번호) - `P-9101`, `P-9102` (9차 펌프 장비 태그) - `P-9107-25A-F-n` 등 (9차 배관번호) 사용자가 "중복방지 로직에 차단된 것 아닌가" 질문함 → 실제로는 **regex 불일치** 와 **레이어 처리 누락** 이 원인. --- ## 실제 DXF 구조 조사 결과 ### 파일별 배관번호 형식 차이 | DXF 파일 | 배관번호 레이어 | 형식 예시 | 필드 수 | |----------|--------------|---------|--------| | `p9-p&id-20.03.19.dxf` (9차) | `LINENO` | `P-9107-25A-F-n` | **5필드** | | `p9-p&id-20.03.19.dxf` (9차) | `14-D-PIPELINE-LINE` | `CHR-9641-50A-F-C50` | **6필드** | | `plant-10100-only.dxf` (10차) | `LINENO` | `P-10101-25A-F1A-n` | **7필드** | | `10차플랜트-P&ID.dxf` | `LINENO` | `P-10138-600A-F2A-H100` | **7필드** | ### 펌프 태그 레이어 | DXF 파일 | 레이어 | 예시 | |----------|-------|------| | 9차 플랜트 | `0` | `P-9101`, `P-9116`, `P-9201` | | 10차 플랜트 | `0`, `1` | `P-10101`, `VP-10117`, `DP-10101` | 펌프 태그는 모두 일반 TEXT 엔티티 → 추출 자체는 가능했으나 배관번호로 오인되는 문제 있었음. --- ## 버그 분석 ### Bug 1: `_PID_LINENO_FULL_RE` regex가 9차 배관번호 형식 불일치 **파일**: `mcp-server/server.py` **기존 regex (7필드 고정)**: ``` ^([A-Z][A-Z0-9]{0,3})-(\d{3,6})-(\d{1,4}[A-Z]?)-([A-Z])(\d)([A-Z])-([A-Za-z0-9]+)$ ``` 그룹 구조: `SERVICE - LINENUM - SIZE - MATERIAL - FLANGE_DIGIT - INSUL_CODE - INSUL_THICK` | 입력 | 결과 | 이유 | |------|------|------| | `P-10101-25A-F1A-n` | ✓ MATCH | F→1→A→n 순서 맞음 | | `P-9107-25A-F-n` | ✗ FAIL | F 다음에 `\d` 기대하나 `-n` 등장 | | `CHR-9641-50A-F-C50` | ✗ FAIL | 동일 이유 | 9차 플랜트는 플랜지등급 숫자와 단열코드가 분리되지 않고 `F-n`, `F-H50` 형식으로 통합되어 있어 7필드 regex에 걸리지 않음. --- ### Bug 2: `_extract_pid_dxf_fast`가 레이어별로만 배관번호 판단 **파일**: `mcp-server/server.py` **기존 로직**: ```python if layer == 'LINENO': # LINENO 레이어만 배관번호 처리 parsed = _parse_pid_lineno(txt) ... continue if _PID_TAG_RE.match(txt): # 그 외 레이어는 TAG_RE만 체크 ... ``` 결과: - `14-D-PIPELINE-LINE` 레이어의 `CHR-9641-50A-F-C50` → TAG_RE 불일치 → **완전 누락** - 다른 도면에서 배관번호 레이어 이름이 다르면 → **모두 누락** 레이어 이름 하드코딩은 도면 간 이식성이 없음. 레이어 이름이 아닌 **regex 패턴으로 판단**해야 함. --- ### Bug 3: `build_pid_graph_parallel` pump extractor가 배관번호를 펌프로 오인 **파일**: `mcp-server/worker/pid_extract_prompts.py` LLM pump extractor 프롬프트에 5자리 예시만 있고 배관번호 제외 지시 없음: ``` Examples: P-10101, VP-10117, DP-10101, C-10201, CP-10301, BP-10401 ``` DXF 전체 텍스트에 `P-10101-25A-F1A-n`이 포함되어 있을 때 LLM이 이를 보고 `P-10101`로 잘못 추출. → Phase 4 `seen_tagnos` 중복 제거에서 실제 펌프 P-10101과 충돌 → 배관번호 P-10101-25A-F1A-n은 graph에서 완전 누락 (사용자가 의심한 "중복방지 로직 차단"은 이 케이스에 해당 — 다만 원인은 LLM의 잘못된 추출임) --- ## 수정 내용 ### Fix 1: `_PID_LINENO_FULL_RE` — 5~7필드 통합 regex **`mcp-server/server.py`** ```python # 기존 (7필드 고정) _PID_LINENO_FULL_RE = re.compile( r'^([A-Z][A-Z0-9]{0,3})-(\d{3,6})-(\d{1,4}[A-Z]?)-([A-Z])(\d)([A-Z])-([A-Za-z0-9]+)$' ) # 수정 (5~7필드 통합: pipe_spec이 F, F1A, F2A 등 가변) _PID_LINENO_FULL_RE = re.compile( r'^([A-Z][A-Z0-9]{0,3})-(\d{3,6})-(\d{1,4}[A-Z]?)-([A-Za-z][A-Za-z0-9]*)-([A-Za-z0-9]+)$' ) ``` 새 그룹: `(service, line_no, size, pipe_spec, insul)` | 입력 | 매칭 | pipe_spec | insul | |------|------|-----------|-------| | `P-9107-25A-F-n` | ✓ | F | n | | `P-9113-20A-F-H50` | ✓ | F | H50 | | `CHR-9641-50A-F-C50` | ✓ | F | C50 | | `P-10101-25A-F1A-n` | ✓ | F1A | n | | `P-10138-600A-F2A-H100` | ✓ | F2A | H100 | | `VG-6203-15A-F1A-n` | ✓ | F1A | n | `_parse_pid_lineno` 반환값도 그룹 수에 맞게 단순화: ```python # 기존: material_spec, flange_rating, insul_code, insul_thickness (4개 필드) # 수정: pipe_spec, insul (2개 필드로 통합) return { "raw": token, "service": service, "fluid": ..., "line_no": line_no, "size": size, "pipe_spec": pipe_spec, # F, F1A, F2A 등 "insul": insul, # n, H50, H100, C50 등 } ``` --- ### Fix 2: `_extract_pid_dxf_fast` — regex 우선, 레이어는 보조 힌트로만 **`mcp-server/server.py`** ```python # 기존: 레이어 이름 == 'LINENO' 이면 배관번호 if layer == 'LINENO': parsed = _parse_pid_lineno(txt) ... continue if _PID_TAG_RE.match(txt): ... # 수정: FULL_RE 매칭 → 레이어 무관 배관번호, 짧은 형식만 레이어 힌트 사용 if _PID_LINENO_FULL_RE.match(txt): # 완전한 배관번호 → 레이어 무관 parsed = _parse_pid_lineno(txt) if parsed is not None: linenos.append(parsed) continue if 'LINENO' in layer.upper(): # 레이어 이름에 LINENO 포함 → 짧은 형식도 배관번호 parsed = _parse_pid_lineno(txt) # (P-10101 같은 단순형은 펌프와 구분 불가능, if parsed is not None: # 레이어 힌트 불가피) linenos.append(parsed) continue if _PID_TAG_RE.match(txt): # 일반 장비/계기 태그 ... ``` 핵심 원칙: **완전한 배관번호는 regex로 식별, 레이어 이름에 의존하지 않음** --- ### Fix 3: pump extractor 프롬프트 개선 **`mcp-server/worker/pid_extract_prompts.py`** ```python _PUMP_PROMPT = _PROMPT_HEADER + """ Extract ONLY pumps and compressors (simple equipment tags, NO pipe size suffix). Target equipment types: P (pump), VP (vertical pump), DP (dual pump), C (compressor), CP (centrifugal pump), BP (booster pump), SP (sump pump), and their variants. Examples (4~5 digit loop numbers): P-10101, VP-10117, DP-10101, C-10201, P-9101, P-9116, VP-9201 IMPORTANT: Do NOT extract pipeline/line numbers that have a pipe size suffix (e.g. 25A, 50A, 100A). SKIP (pipeline, not a pump): P-10101-25A-F1A-n, P-9107-25A-F-n, CHR-9641-50A-F-C50 INCLUDE (pump tag): P-10101, VP-10117, P-9101 """ ``` 변경점: - 4자리 번호 예시 추가 (`P-9101`, `P-9116`, `VP-9201`) - 배관번호 제외 지시 명시 (파이프 사이즈 suffix 있으면 제외) - SKIP / INCLUDE 예시로 명확하게 구분 --- ## 검증 결과 ### regex 단위 테스트 (14/14 통과) ``` ✓ P-9107-25A-F-n → pipe (9차 5필드) ✓ P-9113-20A-F-H50 → pipe (9차 단열) ✓ P-9127-500A-F-H100 → pipe (9차 대구경) ✓ P-10101-25A-F1A-n → pipe (10차 7필드) ✓ P-10138-600A-F2A-H100→ pipe (10차 대구경) ✓ CHR-9641-50A-F-C50 → pipe (냉각수 6필드) ✓ VG-6203-15A-F1A-n → pipe (벤트가스) ✓ SW-10810-25A-F1A-E50 → pipe (소프트워터) ✓ P-10101 → tag (10차 펌프) ✓ P-9101 → tag (9차 펌프) ✓ VP-10117 → tag (진공펌프) ✓ FIT-10101 → tag (유량계) ✓ FCV-6113 → tag (유량제어밸브) ✓ PT-9101 → tag (압력계) ``` ### 실제 DXF 엔드투엔드 검증 ``` === p9-p&id-20.03.19.dxf (9차) === 배관번호 총 242개 (P-: 83개) ← 수정 전: 0개 P 배관번호 예시: P-9107-25A, P-9114-20A, P-9113-20A, ... 펌프 태그: P-9101, P-6101, P-201, P-9201, P-9116 === plant-10100-only.dxf (10차) === 배관번호 총 96개 (P-: 57개) P 배관번호 예시: P-10138-600A, P-10143-32A, P-10127-65A, ... 펌프 태그: P-10101, P-10114, P-10116, P-10118 ``` --- ## 수정 파일 목록 | 파일 | 변경 라인 | 내용 | |------|---------|------| | `mcp-server/server.py` | ~221 | `_PID_LINENO_FULL_RE` regex 교체 | | `mcp-server/server.py` | ~244 | `_parse_pid_lineno` 반환값 `pipe_spec`/`insul`로 단순화 | | `mcp-server/server.py` | ~305 | `_extract_pid_dxf_fast` 레이어/배관번호 처리 로직 수정 | | `mcp-server/server.py` | ~359 | `_extract_pid_tags_from_text` step 1 출력에 `pipeSpec`/`insul` 추가 | | `mcp-server/worker/pid_extract_prompts.py` | ~61 | `_PUMP_PROMPT` 개선 | --- ## 설계 결정 사항 | 항목 | 결정 | 이유 | |------|------|------| | regex 필드 통합 방식 | 5필드 통합 (`pipe_spec`이 F, F1A, F2A 통합) | 플랜트마다 배관 사양 코드 체계가 달라 고정 필드 분해는 취약 | | 레이어 이름 역할 | FULL_RE 불일치 시 보조 힌트로만 사용 | 레이어 이름은 회사·도면마다 다름. regex가 primary. | | 짧은 배관번호(`P-10101`) 처리 | LINENO 계열 레이어에서만 배관번호로 인식 | `P-10101`은 펌프 태그와 텍스트가 동일 → 레이어 힌트 불가피 | | C# PidExtractorService | 미수정 | 펌프 태그(TEXT 엔티티)는 기존 코드에서 정상 추출됨. ATTRIB 읽기 추가는 별도 검토 필요 |