# P&ID 재설계 - 코딩 계획 (독립 프로세스 병렬 아키텍처) > 작성일: 2026-05-05 > 상태: 진행 중 > 목표: `No-10_Plant_PID.dxf`(28,819 엔티티) 처리 시 30분 타임아웃 해결 > 아키텍처: pid_worker.py(코디네이터) + 5개 독립 추출 프로세스 --- ## 제안 아키텍처 ``` ┌─────────────────────────────────────────────────────────────────┐ │ pid_worker.py (코디네이터) │ │ │ │ Phase 1: 도면 분할 + 기하 추출 (순차) │ │ ├─ extractor.split_drawings() → 9개 도면 영역 │ │ └─ extractor.extract_and_save() → geo.json │ │ │ │ Phase 2: 전체 텍스트 1회 추출 │ │ └─ DXF에서 TEXT/MTEXT → full_text.txt (1회만 읽기) │ │ │ │ Phase 3: 5개 독립 프로세스 병렬 실행 ← 진짜 병렬 │ │ ├─ subprocess: pid_extract_sensor.py → results/sensor.json │ │ ├─ subprocess: pid_extract_valve.py → results/valve.json │ │ ├─ subprocess: pid_extract_system.py → results/system.json │ │ ├─ subprocess: pid_extract_gauge.py → results/gauge.json │ │ └─ subprocess: pid_extract_pump.py → results/pump.json │ │ │ │ Phase 4: pid_worker가 결과 파일 읽어서 통합 │ │ ├─ 5개 JSON 파일 로드 │ │ ├─ 중복 제거 (tagNo 기준) │ │ └─ 위상 그래프 빌드 + 태그 매핑 │ │ │ │ Phase 5: 저장 + 응답 │ └─────────────────────────────────────────────────────────────────┘ ``` ## 기존 접근 vs 개선안 비교 | 항목 | 기존 (asyncio.gather) | 개선안 (독립 프로세스) | |------|----------------------|----------------------| | vLLM 요청 | 단일 프로세스 → 단일 GPU | 5개 프로세스 → 5개 GPU/큐 병렬 | | 실제 병렬 | ❌ 가짜 (순차 처리) | ✅ 진짜 동시 처리 | | 메모리 | pid_worker가 모든 텍스트 보유 | 각 프로세스 독립 | | 실패 격리 | 하나 실패 → 전체 실패 | 하나 실패 → 나머지 결과 활용 | | 테스트 | 통합 테스트만 가능 | 각 추출기 독립 테스트 가능 | --- ## 변경 대상 파일 | 파일 | 변경 내용 | |------|-----------| | `mcp-server/pipeline/extractor.py` | 도면 분할 로직 추가 | | `mcp-server/worker/pid_worker.py` | 코디네이터 로직: 프로세스 관리, 결과 통합 | | `mcp-server/worker/pid_extract_sensor.py` | 신규: 센서 전용 추출기 (독립 프로세스) | | `mcp-server/worker/pid_extract_valve.py` | 신규: 밸브 전용 추출기 (독립 프로세스) | | `mcp-server/worker/pid_extract_system.py` | 신규: 시스템 전용 추출기 (독립 프로세스) | | `mcp-server/worker/pid_extract_gauge.py` | 신규: 게이지 전용 추출기 (독립 프로세스) | | `mcp-server/worker/pid_extract_pump.py` | 신규: 펌프 전용 추출기 (독립 프로세스) | | `mcp-server/pipeline/mapper.py` | 기존 유지 (이미 배치 처리 구현됨) | | `mcp-server/pipeline/topology.py` | 기존 유지 (이미 SpatialGrid 구현됨) | | `mcp-server/pipeline/legend_parser.py` | 기존 유지 (이미 계측기 그룹 정의됨) | --- ## Phase 2: 독립 추출기 공통 템플릿 ### 2-1. 공통 추출기 템플릿 작성 - **파일**: `mcp-server/worker/pid_extract_template.py` (신규) - **목표**: 5개 추출기가 공유하는 공통 로직 템플릿 - **작업 내용**: - CLI 인자 파싱 (input_text, output_path, system_prompt) - vLLM HTTP 클라이언트 연결 (환경 변수 VLLM_ENDPOINT) - LLM 호출 → JSON 파싱 → 태그 목록 반환 - `max_tokens=65536` 적용 - finish_reason=length 복구 로직 - 결과를 JSON 파일로 쓰기 - **완료 기준**: 템플릿 스크립트가 단독 실행 가능 ### 2-2. 계측기 유형별 프롬프트 정의 - **파일**: 각 추출기 스크립트 상단에 상수로 정의 - **목표**: 각 유형별 전용 프롬프트 - **프롬프트 목록**: - `_SENSOR_PROMPT` — FT, FIT, LT, PT, TE, PG, LG, TG - `_VALVE_PROMPT` — FCV, TCV, LCV, PCV, XV, FV, LV, PV, TV - `_SYSTEM_PROMPT` — LI, PI, TI, FIQ, FICQ, TICA, PICA, LICA - `_GAUGE_PROMPT` — PG, TG, LG - `_PUMP_PROMPT` — P-10101, VP-10117, DP-10101 등 펌프 - **완료 기준**: 5개 프롬프트 상수 정의 완료 --- ## Phase 3: 5개 독립 추출기 스크립트 생성 ### 3-1. pid_extract_sensor.py - **파일**: `mcp-server/worker/pid_extract_sensor.py` (신규) - **목표**: 센서/계측기 전용 추출 - **작업 내용**: Phase 2 템플릿 기반, _SENSOR_PROMPT 적용 - **완료 기준**: 단독 실행 시 sensor.json 출력 ### 3-2. pid_extract_valve.py - **파일**: `mcp-server/worker/pid_extract_valve.py` (신규) - **목표**: 밸브 전용 추출 - **작업 내용**: Phase 2 템플릿 기반, _VALVE_PROMPT 적용 - **완료 기준**: 단독 실행 시 valve.json 출력 ### 3-3. pid_extract_system.py - **파일**: `mcp-server/worker/pid_extract_system.py` (신규) - **목표**: 시스템/제어기 전용 추출 - **작업 내용**: Phase 2 템플릿 기반, _SYSTEM_PROMPT 적용 - **완료 기준**: 단독 실행 시 system.json 출력 ### 3-4. pid_extract_gauge.py - **파일**: `mcp-server/worker/pid_extract_gauge.py` (신규) - **목표**: 게이지 전용 추출 - **작업 내용**: Phase 2 템플릿 기반, _GAUGE_PROMPT 적용 - **완료 기준**: 단독 실행 시 gauge.json 출력 ### 3-5. pid_extract_pump.py - **파일**: `mcp-server/worker/pid_extract_pump.py` (신규) - **목표**: 펌프 전용 추출 - **작업 내용**: Phase 2 템플릿 기반, _PUMP_PROMPT 적용 - **완료 기준**: 단독 실행 시 pump.json 출력 ### 3-6. 개별 추출기 테스트 - **파일**: `test_individual_extractors.py` (신규) - **목표**: 5개 추출기 각각 단독 실행 테스트 - **작업 내용**: - full_text.txt를 각 추출기에 입력 - 출력 JSON 검증 (schema, tagNo 필수 필드) - 처리 시간 측정 - **완료 기준**: 5개 모두 정상 출력, 각각 < 60초 --- ## Phase 4: pid_worker.py 코디네이터 리팩토링 ### 4-1. 전체 텍스트 1회 추출 로직 - **파일**: `mcp-server/worker/pid_worker.py` - **목표**: DXF에서 TEXT/MTEXT를 한 번만 읽어 full_text.txt 생성 - **작업 내용**: - ezdxf로 DXF 로드 → TEXT/MTEXT 엔티티 순회 - 좌표 순 정렬 후 텍스트 연결 - 결과를 임시 디렉토리의 `full_text.txt`에 저장 - **완료 기준**: full_text.txt 생성, 파일 크기 < 10MB ### 4-2. 5개 프로세스 병렬 실행 로직 - **파일**: `mcp-server/worker/pid_worker.py` - **목표**: `subprocess.Popen`으로 5개 추출기 동시 실행 - **작업 내용**: - 임시 디렉토리 생성 (`results/` 폴더) - 5개 스크립트 경로 확인 (동일 디렉토리) - `subprocess.Popen()`으로 5개 프로세스 동시 시작 - `proc.wait()`로 전체 완료 대기 (timeout=300초) - 각 프로세스 returncode 확인 (실패 시 로깅 + 계속) - **완료 기준**: 5개 프로세스 동시 실행, results/에 5개 JSON 생성 ### 4-3. 결과 통합 + 중복 제거 - **파일**: `mcp-server/worker/pid_worker.py` - **목표**: 5개 JSON 파일 로드 → tagNo 기준 중복 제거 - **작업 내용**: - `results/*.json` 로드 (실패한 파일 스킵) - tagNo를 키로 하는 딕셔너리에 병합 (첫 번째 우선) - 통합 통계 출력 (총 태그 수, 유형별 수) - **완료 기준**: 중복 없는 통합 태그 목록 생성 ### 4-4. 위상 그래프 빌드 + 태그 매핑 호출 - **파일**: `mcp-server/worker/pid_worker.py` - **목표**: 기존 topology.py, mapper.py 호출 - **작업 내용**: - 통합 태그 목록을 `IntelligentMapper`에 전달 - 매핑 결과를 `TopologyBuilder`에 전달 - 그래프 JSON 생성 - **완료 기준**: 기존 API 호환 유지 ### 4-5. `_build_pid_graph_parallel()` 전체 리팩토링 - **파일**: `mcp-server/worker/pid_worker.py` - **목표**: 기존 함수를 새 아키텍처로 교체 - **전체 흐름**: ``` 1. 도면 분할 (extractor.split_drawings()) 2. 기하 추출 (extractor.extract_and_save() → geo.json) 3. 전체 텍스트 1회 추출 (→ full_text.txt) 4. 5개 프로세스 병렬 실행 (→ results/*.json) 5. 결과 통합 + 중복 제거 6. 위상 그래프 빌드 + 태그 매핑 7. 저장 + 응답 ``` - **완료 기준**: 전체 처리 < 5분, 기존 API 호환 --- ## Phase 1: 도면 분할 로직 (extractor.py) ### 1-1. 도면 분할 테스트 스크립트 작성 - **파일**: `test_drawing_split.py` (신규) - **목표**: `No-10_Plant_PID.dxf`를 TITLE 레이어 LINE으로 분할하는 로직 프로토타입 - **작업 내용**: - ezdxf로 DXF 로드 - TITLE 레이어의 LINE 엔티티 탐색 - 수직 LINE(X 좌표가 일정)을 도면 경계로 감지 - 각 도면별 X/Y 범위 출력 - **검증**: 9개 도면 영역이 올바르게 분리되는지 확인 - **완료 기준**: 콘솔에 9개 도면의 X/Y 범위가 출력됨 ### 1-2. PidGeometricExtractor에 도면 분할 메서드 추가 - **파일**: `mcp-server/pipeline/extractor.py` - **목표**: `split_drawings()` 메서드 추가 - **작업 내용**: - `split_drawings() -> List[DrawingRegion]` 메서드 추가 - DrawingRegion 데이터클래스 정의 (drawing_no, x_min, x_max, y_min, y_max) - TITLE 레이어 LINE 기반 경계 감지 - 레전드 페이지(X < 2000) 제외 - FFD 페이지도 제외 (최상단 텍스트 기반) - **완료 기준**: `split_drawings()` 호출 시 9개 DrawingRegion 반환 ### 1-3. 영역별 추출 메서드 추가 - **파일**: `mcp-server/pipeline/extractor.py` - **목표**: `extract_region()` 메서드 추가 - **작업 내용**: - `extract_region(region: DrawingRegion) -> List[GeometricEntity]` 메서드 추가 - bbox가 region 범위 내에 있는 엔티티만 필터링 - 기존 `extract_and_save()` 로직 재사용 - **완료 기준**: 각 도면별 엔티티 수 합계가 전체 엔티티 수와 일치 ### 1-4. 도면 분할 통합 테스트 - **파일**: `test_drawing_split.py` - **목표**: 전체 파이프라인 테스트 - **작업 내용**: - DXF 로드 → 분할 → 영역별 추출 → 결과 검증 - 각 도면별 엔티티 수, 태그 수 확인 - 처리 시간 측정 - **완료 기준**: 9개 도면 모두 정상 추출, 총 처리 시간 < 30초 --- ## Phase 5: 통합 테스트 + 검증 ### 5-1. 전체 파이프라인 통합 테스트 - **파일**: `test_full_pipeline_parallel.py` (신규) - **목표**: `No-10_Plant_PID.dxf` 전체 처리 - **작업 내용**: - `_build_pid_graph_parallel()` 호출 - 각 단계별 시간 측정 (Phase 1~5) - 추출된 태그 수, 매핑 수, 그래프 노드/에지 수 확인 - **완료 기준**: 전체 처리 < 5분, 태그 추출 수 >= 기존 ### 5-2. 실패 격리 테스트 - **파일**: `test_failure_isolation.py` (신규) - **목표**: 일부 추출기 실패 시 나머지 결과 활용 확인 - **작업 내용**: - 하나의 추출기 스크립트 고의 실패 유도 - 나머지 4개 결과로 정상 통합되는지 확인 - 에러 로깅 확인 - **완료 기준**: 4/5 성공 시 정상 통합, 에러 로그 출력 ### 5-3. 메모리 사용량 측정 - **파일**: `test_memory_usage.py` (신규) - **목표**: 각 프로세스 메모리 독립성 확인 - **작업 내용**: - 5개 프로세스 각각 메모리 사용량 측정 - pid_worker 메모리 사용량 확인 (텍스트 불보유) - **완료 기준**: pid_worker 메모리 < 500MB, 각 추출기 < 1GB --- ## 실행 순서 및 의존성 ``` Phase 1 (도면 분할) ├── 1-1 → 1-2 → 1-3 → 1-4 (순차) │ Phase 2 (공통 템플릿) ├── 2-1 → 2-2 (순차, Phase 1 완료 후) │ Phase 3 (5개 추출기 생성) ├── 3-1 ~ 3-5 (병렬 가능, Phase 2 완료 후) ├── 3-6 (순차, 3-1~3-5 완료 후) │ Phase 4 (pid_worker 코디네이터) ├── 4-1 → 4-2 → 4-3 → 4-4 → 4-5 (순차, Phase 3 완료 후) │ Phase 5 (통합 테스트) ├── 5-1 → 5-2 → 5-3 (순차, Phase 4 완료 후) ``` --- ## 각 단계 완료 기준 및 예상 시간 | 단계 | 완료 기준 | 예상 시간 | |------|-----------|-----------| | 1-1 | 9개 도면 영역 콘솔 출력 | 30분 | | 1-2 | `split_drawings()` 9개 Region 반환 | 1시간 | | 1-3 | `extract_region()` 정상 동작 | 30분 | | 1-4 | 전체 분할 테스트 통과 | 30분 | | 2-1 | 템플릿 스크립트 단독 실행 가능 | 1시간 | | 2-2 | 5개 프롬프트 상수 정의 | 30분 | | 3-1~3-5 | 5개 추출기 각각 JSON 출력 | 각 30분 (병렬 가능) | | 3-6 | 5개 모두 단독 테스트 통과 | 30분 | | 4-1 | full_text.txt 생성 | 30분 | | 4-2 | 5개 프로세스 동시 실행 | 1시간 | | 4-3 | 중복 없는 통합 태그 목록 | 30분 | | 4-4 | 그래프 빌드 + 매핑 호출 | 30분 | | 4-5 | 전체 리팩토링 완료, API 호환 | 1시간 | | 5-1 | 전체 처리 < 5분 | 1시간 | | 5-2 | 실패 격리 테스트 통과 | 30분 | | 5-3 | 메모리 사용량 확인 | 30분 | **총 예상 시간: 약 14시간** --- ## 체크리스트 ### Phase 1: 도면 분할 - [ ] 1-1. 도면 분할 테스트 스크립트 작성 - [ ] 1-2. PidGeometricExtractor에 도면 분할 메서드 추가 - [ ] 1-3. 영역별 추출 메서드 추가 - [ ] 1-4. 도면 분할 통합 테스트 ### Phase 2: 독립 추출기 공통 템플릿 - [ ] 2-1. 공통 추출기 템플릿 작성 - [ ] 2-2. 계측기 유형별 프롬프트 정의 ### Phase 3: 5개 독립 추출기 스크립트 - [ ] 3-1. pid_extract_sensor.py - [ ] 3-2. pid_extract_valve.py - [ ] 3-3. pid_extract_system.py - [ ] 3-4. pid_extract_gauge.py - [ ] 3-5. pid_extract_pump.py - [ ] 3-6. 개별 추출기 테스트 ### Phase 4: pid_worker.py 코디네이터 - [ ] 4-1. 전체 텍스트 1회 추출 로직 - [ ] 4-2. 5개 프로세스 병렬 실행 로직 - [ ] 4-3. 결과 통합 + 중복 제거 - [ ] 4-4. 위상 그래프 빌드 + 태그 매핑 호출 - [ ] 4-5. `_build_pid_graph_parallel()` 전체 리팩토링 ### Phase 5: 통합 테스트 - [ ] 5-1. 전체 파이프라인 통합 테스트 - [ ] 5-2. 실패 격리 테스트 - [ ] 5-3. 메모리 사용량 측정 --- ## 주의 사항 1. **백업 필수**: 각 파일 수정 전 `.rooBackup/`에 백업 2. **diff 제시**: 변경 내용 diff 형식으로 제시 후 확인 3. **작은 단계**: 각 단계를 독립적으로 완료하고 검증 4. **테스트 우선**: 테스트 스크립트 먼저 작성 후 구현 5. **기존 코드 유지**: topology.py, mapper.py는 기존 유지 (이미 잘 구현됨) 6. **프로세스 간 통신**: 파일 기반 (JSON)으로만 통신. 공유 메모리 금지 7. **임시 파일 정리**: 각 요청 완료 후 임시 디렉토리 삭제 --- ## 다음 시작 시 1. 이 파일의 체크리스트에서 첫 번째 미완료 항목부터 시작 2. 각 단계 완료 시 체크리스트 업데이트 3. 문제가 발생하면 해당 단계에서 중단하고 원인 분석 4. 완료 기준을 충족해야 다음 단계로 진행