Files
ExperionCrawler/DXF-추출-개선2-코딩-byClaudeSonnet.md
windpacer 302183c97e feat: P&ID 연결 분석, LLM 에이전트 모드, KB 확장, MCP 서버 리팩토링
- P&ID: 연결 분석 API, Prefix 규칙 관리, 카테고리 분류, DXF 그래프 빌드
- LLM: 대화 요약, tool card 영구 보존, 시계열 차트(uPlot), 에이전트 모드
- KB: 청크 미리보기, Field Instrument Inference, 인증/Qdrant 클라이언트
- MCP: 서버 기능 확장, 파이프라인 수정, timeout 개선
- Frontend: P&ID UI, LLM UI, KB UI, OPC UA Write 탭 추가
- 설정: AGENTS.md, plant_context, README, opencode.json 업데이트
- 정리: 진단 체크리스트 문서 삭제
2026-05-21 23:36:57 +09:00

9.3 KiB

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

기존 로직:

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

# 기존 (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 반환값도 그룹 수에 맞게 단순화:

# 기존: 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

# 기존: 레이어 이름 == '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

_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 읽기 추가는 별도 검토 필요