34 KiB
P&ID 추출 PREFIX 분류 — tag_dcs 컬럼 도입 플랜
작성일: 2026-05-27
작성자: Sonnet4.6
목적:pid_prefix_rules와pid_equipment두 테이블에tag_dcs BOOLEAN컬럼을 추가해,
P&ID 추출 시작 시점부터 현장 계기(field instrument)와 DCS 태그(DCS function block)를 구별한다.
상세 진단 리포트 (실제 코드 대비 교차 검증)
진단일: 2026-05-27
진단자: big-pickle
방법:diagnosis-checklist.md8-Step — 전체 코드베이스 read + 교차검증
읽은 파일 (STEP 3 — 코드 읽기)
레이어 파일 비고 Domain PidPrefixRule.cs,PidEquipment.cs엔티티 현재 상태 DTO PidPrefixRuleDto.cs,PidEquipmentDto.csRecord 정의 Interface IExperionServices.csIPidExtractorService 시그니처 Service PidExtractorService.cs(1105줄 전체)핵심 — MatchCategory/ClassifyTagClass/Export/Import/CRUD DbContext ExperionDbContext.cs(DDL + Fluent API + Seed + Views)DDL, seed 46개 prefix, v_pump_signal_map Controller PidController.csGetPrefixRules/GetEquipment 응답 형식 Web UI pid.js,panes/pid.html,css/pid.cssPrefix rules UI + equipment table Python MCP server.py(전체)upsert_pid_connection, _classify_pid_tag, _DB_SCHEMA, trace_connections Python worker/sql_prompt.pyNL2SQL DB_SCHEMA Prompt prompts/plant_context.mdLLM 시스템 프롬프트 기타 validators.py,infer.py영향 없음 확인 호출 계층 (STEP 4)
[Prefix Rule CRUD] POST/PUT/GET /api/pid/prefix-rules → PidController → PidExtractorService.Get/Create/Update/DeletePrefixRulesAsync → DbContext.PidPrefixRules → InvalidateRulesCache() [P&ID 추출] ExtractFromStreamAsync() → MatchCategoryAsync(tagNo) ← string? 만 반환 (tagDcs 없음) → ClassifyTagClass(tagNo, cat, hasLink) ← ISA heuristic → PidEquipment { Category, TagClass, ... } 저장 [Category Backfill] ApplyCategoriesToExistingAsync() → MatchCategoryAsync() + ClassifyTagClass() (2회, line 902/922) [CSV/Excel Export] ExportToCsvAsync() ← 14개 컬럼, TagClass는 있음 ExportToExcelAsync() ← 17개 컬럼 (col16=TagClass, col17=id) ImportFromExcelAsync() ← col17=id → hasIdCol 감지 [MCP Python] upsert_pid_connection() ← 직접 SQL (UPDATE/INSERT) _classify_pid_tag() ← 자체 prefix 상수 (DB 미조회) trace_connections() ← SELECT tag_no, from_tag, to_tag, role
🔴 HIGH — 즉시 수정 필요 (실제 장애 발생)
H1. Excel 17번열 충돌 —
tag_dcs추가 시id(안정 키) 덮어쓰여 데이터 손실문제: 계획서 §2.4-변경4가 "Excel 열 추가 (17번 열)"라고 명시했으나, 현재
ExportToExcelAsync는 col17을 **라운드트립용 안정 키id**로 사용 중.근거:
PidExtractorService.cs:535—worksheet.Cells[1, 17].Value = "id"PidExtractorService.cs:623—hasIdCol감지:ws.Cells[1, 17].Text=="id"PidExtractorService.cs:684-691—hasIdCol=true일 때만 id 기반 in-place UPDATE 수행영향: col17에
"DCS태그"가 들어가면hasIdCol=false→ 모든 행이 TagNo fallback(old format) 매칭으로 폴백. 같은 TagNo에 다중 경로가 있으면 전부 동일한 값으로 덮어써져 다중경로 데이터 손실. 수동으로 교정한connection_locked행도 초기화 위험.수정 방향:
tag_dcs는 col18에 배치, col17=id유지ExportToExcelAsync:535—worksheet.Cells[1, 18].Value = "DCS태그"추가ExportToExcelAsync:562-568— row write에 col18 추가ImportFromExcelAsync— col18 읽기 +Apply()에e.TagDcs설정- 계획서 §2.4-변경5의 Excel import 설명에 col18 반영
H2.
upsert_pid_connection— boolean 인자 + SQL 전면 수정 필요문제: MCP에서
pid_equipment를 직접 SQL 조작하는 유일한 경로.tag_dcs컬럼 추가로 SELECT/UPDATE/INSERT 3개 SQL 모두 수정 + Python boolean 타입 핸들링 필요.근거:
mcp-server/server.py:989-1101수정 항목 상세:
위치 현재 코드 변경 함수 시그니처 (990-997) tag_class: str | None = Nonetag_dcs: bool | None = None추가_n()처리 (1035)tag_class = _n(tag_class)tag_dcs = bool(tag_dcs) if tag_dcs is not None else None—_n()은 str 전용이므로 bool은 별도 처리_SNAP(1037)9개 항목 "tag_dcs"추가 (10개)SELECT 스냅샷 (1040) SELECT tag_no, ..., connection_lockedtag_dcs추가UPDATE SET (1078-1084) tag_class=COALESCE(%s, tag_class)tag_dcs=COALESCE(%s, tag_dcs)추가 (COALESCE는 boolean도 정상 동작)INSERT 컬럼 리스트 (1094-1096) (tag_no, ..., tag_class, connection_locked)tag_dcs추가 — None이면 DEFAULT FALSEINSERT param (1101) 9개 param +1 param 위험:
COALESCE(%s, tag_dcs)에서tag_dcs=False는False로 정상 전달되므로 문제 없음. 단,_n()함수 사용 시 주의 — boolean은str(v)후strip()하면 안 됨.
🟠 MED — 조건부 장애 (특정 상황에서 발생)
M1.
_classify_pid_tag— DCS prefix vs instrument prefix 이중 정의 (동기화 위험)문제:
server.py:_classify_pid_tag()(257-270)는 DBpid_prefix_rules를 전혀 참조하지 않고 자체 Python 상수(_PID_EQUIPMENT_PREFIX,_PID_INSTRUMENT_FIRST)로 분류. 계획서 §2.9-변경1이 여기에_DCS_PREFIXES집합을 추가하면 C# seed와 Python 상수 간 동기화가 수동으로 유지되어야 함.근거:
mcp-server/server.py:257-270—_classify_pid_tag()는 DB 미조회, 자체 prefix 분류mcp-server/server.py:263—if prefix in _PID_EQUIPMENT_PREFIX:- 계획서 §2.9-변경1:
_DCS_PREFIXES = {"FIC","TIC","PIC","LIC","FY","TY","PY","LY","FV","TV","PV","LV"}- C# seed:
ExperionDbContext.cs:632-696— DB에 저장된 prefix 목록영향: DXF 파싱 결과(server.py)와 DB extraction(C#)의
tag_dcs판정이 불일치할 수 있음. 예: Python에만 DCS prefix를 추가/제거하면 DXF 추출 결과와 DB 추출 결과의 분류가 달라짐.수정 방향:
_classify_pid_tag의tag_dcs필드는 DXF 추출 결과에만 사용되며 DB 저장 시 C#에서 다시 판정하므로, 일시적 불일치는 허용됨. 단, Phase 6 검증 시 DXF ↔ DB 분류 일관성 확인 필요. 또는_DCS_PREFIXES를 별도 공유 모듈로 분리 고려.
M2.
ClassifyTagClass— Override 우선순위 모호 (tag_dcs vs hasExperionLink)문제: 계획서 §4는
tag_dcs=TRUE → TagClass='system'을 prefix rule이 ground truth로 제안. 그러나 현재ClassifyTagClass(812-831)는hasExperionLink(Experion DB 연결 존재)를 최우선 확정 신호로 사용.tag_dcs=FALSE인데hasExperionLink=true인 경계 사례에서 모순 발생.근거:
PidExtractorService.cs:817-818— 현재 1순위:if (hasExperionLink) return TagClassSystem;// 경계 사례: prefix는 FT(전송기, tag_dcs=FALSE)지만 Experion DB 연결이 있음 // tag_dcs 기준: "field" vs hasExperionLink 기준: "system" // 어느 쪽 우선?영향: field instrument가
hasExperionLink=true로 인해TagClass="system"이 되는 기존 동작이tag_dcs도입 후에도 유지된다면,tag_dcs컬럼 도입 의미 반감.수정 방향: 설계 결정 명확화:
- 옵션 A (계획서 §4):
tag_dcs=TRUE가TagClassSystem을 강제. 경계 사례에서tag_dcs우선.- 옵션 B:
tag_dcs는 정보용 flag,TagClass는 기존 로직 유지.tag_dcs는 prefix 기반 빠른 필터로만 사용.
M3. Seed 데이터 — 46개 prefix DCS 분류 + compound형 누락
문제:
ExperionDbContext.cs:632-696의 46개 seed prefix를 DCS 여부로 분류해야 하나, 계획서의 DCS 목록은 12개 기본형(FIC/TIC/PIC/LIC/FY/TY/PY/LY/FV/TV/PV/LV)만 포함. ISA 후속문자{I, C, A, Q, Y, R}를 가진 compound prefix(FICQ, FICA, TICQ, PICA, LICA, FICR 등)는 시드에 없음.근거:
ClassifyTagClass.cs:803—_systemFuncLetters = {'I', 'C', 'A', 'Q', 'Y', 'R'}- 현재 시드:
FIC,TIC,PIC,LIC만 있음 —FICQ,FICA,TICQ등 없음PidExtractorService.cs:793—MatchCategoryAsync는StartsWith매칭이므로FICQ-6113→FICrule에 매칭되어 category = 'instrument', tag_dcs = TRUE는 되지 않음- 계획서 §5에서 인지 ("FICQ 등 suffix prefix")
영향: Phase 2.1 UPDATE SQL이
prefix IN ('FIC',...)으로만 조건을 걸면FICQ등은 UPDATE되지 않아tag_dcs=FALSE(default)로 남음.수정 방향: UPDATE SQL을
StartsWith기반으로 변경하거나:UPDATE pid_prefix_rules SET tag_dcs = TRUE WHERE prefix IN ('FIC','TIC','PIC','LIC','FY','TY','PY','LY','FV','TV','PV','LV') OR prefix LIKE 'FI%' -- FIC, FICA, FICQ 등 OR prefix LIKE 'TI%' -- TIC, TICA, TICQ 등또는 seed INSERT에 compound형을 명시적으로 추가. 단, 이 방식은 prefix가
StartsWith매칭이므로FI만 있어도FIC/FICA/FICQ/FIR등을 모두 커버 —FIC를 포함한FIprefix면 충분.
🟡 LOW — 동작에 영향 없음 (유지보수성)
L1.
ApplyCategoriesToExistingAsync— backfill에tag_dcs누락문제:
ApplyCategoriesToExistingAsync(884-929)는 기존Category == null인 행과TagClass == null인 행을 backfill.tag_dcs도입 시 이 backfill에도 포함되어야 함.근거:
PidExtractorService.cs:884-929— 2개 배치 루프:
- 1차 (890-907): Category = null인 행 → MatchCategoryAsync + ClassifyTagClass
- 2차 (912-927): Category='instrument' AND TagClass = null인 행 → ClassifyTagClass
수정: 각 루프에서
tag_dcs도 함께 조회/설정.ClassifyTagClass시그니처에tagDcs파라미터 추가 시 이 호출부(902, 922) 함께 수정.
L2.
MatchCategoryAsync—(category, tagDcs)튜플 반환 또는 분리문제: 현재
MatchCategoryAsync(787-795)는string?(category만) 반환.tag_dcs가 필요하면 별도 API 필요. 계획서 §2.4-변경1에서 이슈 인지.근거:
PidExtractorService.cs:90—var category = await MatchCategoryAsync(item.TagNo);PidExtractorService.cs:787-795— returnstring?GetRulesCachedAsync(754)가 이미List<PidPrefixRule>를 캐싱하므로tag_dcs도 함께 사용 가능수정 방향: 3가지 옵션
MatchCategoryAsync→(string? category, bool tagDcs)튜플: 리팩터 범위 큼 (호출부 3곳)- 별도
ResolveTagDcsAsync(tagNo)추가: 최소 침습GetRulesCachedAsync()결과에서 직접 조회: 캐시 직접 접근 → 캡슐화 위반
L3.
PidEquipmentDto— TagMappingService 미반영문제:
PidEquipmentDto(3-19)는TagClass도 없고TagDcs도 없음. 매핑 탭에서 이 DTO를 사용하므로, 매핑 UI에도 DCS 여부를 표시하려면 추가 필요.근거:
src/Core/Application/DTOs/PidEquipmentDto.cs:3-19— 16개 필드, TagClass/TagDcs 없음수정: 선택사항 — 매핑 탭에 표시 불필요 시 생략 가능.
L4.
_DB_SCHEMA/sql_prompt.py— pid_equipment 미등록문제:
_DB_SCHEMA(server.py:678)와DB_SCHEMA(sql_prompt.py:9) 모두pid_equipment테이블이 없음. LLM이 NL2SQL로 "DCS 태그 몇 개?" 같은 질문을 SQL 변환해도 실행 불가.근거:
mcp-server/server.py:678-694— history_table, realtime_table, tag_metadata, event_history_table만 있음mcp-server/worker/sql_prompt.py:9-104— 동일- 계획서 §2.9-변경2, §2.9-변경3에서 server.py / sql_prompt.py 수정 언급
수정: 계획서 §2.9-변경2/3, §2.10에 따라 추가. 단,
_DB_SCHEMA추가 시 SQL injection 방지를 위해 테이블/컬럼명을 LLM 프롬프트에만 포함하고 실제 쿼리는 MCP 도구로 제한하는 현재 아키텍처 유지.
🔵 영향 없음 (No Change)
파일 이유 근거 IExperionServices.cs인터페이스 시그니처 = DTO 타입 그대로 DTO 변경만으로 자동 반영 PidController.cs:GetPrefixRules이미 anonymous object camelCase 반환 tagDcs: r.TagDcs추가만 필요ExperionDbContext.cs:v_pump_signal_mapFROM pid_equipment ft—ft.from_tag, ft.tag_no, ft.category만 조회tag_dcs 미사용 server.py:trace_connectionsSELECT tag_no, from_tag, to_tag, roletag_dcs 미조회 validators.py:37SELECT DISTINCT tag_no FROM pid_equipmenttag_dcs 무관 instrument_inference/infer.py독립 실행 모듈, DB 미의존 계획서 §2.11 판단 올바름 SeedSubAreaAsyncSQLFROM pid_equipment WHERE role ILIKE '%공용%'tag_dcs 무관
수정 필요 파일 요약 (총 19곳)
등급 파일명 변경 내용 🔴 H1 PidExtractorService.csExcel col17→col18 재배치 + Import hasIdCol 보호 + Apply() TagDcs 🔴 H2 server.py:upsert_pid_connectiontag_dcsbool 인자 + SELECT/UPDATE/INSERT 3개 SQL🟠 M1 server.py:_classify_pid_tag_DCS_PREFIXES추가 — C# seed와 동기화 문서화🟠 M2 PidExtractorService.cs:ClassifyTagClasstag_dcs가중치 결정 및 시그니처 변경🟠 M3 ExperionDbContext.cs:seed46개 prefix DCS 분류 + compound형(FICQ 등) 🟡 L1 PidExtractorService.cs:ApplyCategoriesToExistingAsyncbackfill에 tag_dcs 추가 🟡 L2 PidExtractorService.cs:MatchCategoryAsynctagDcs 반환 경로 추가 🟡 L3 PidEquipmentDto.cs선택적 TagDcs 추가 🟡 L4 server.py:_DB_SCHEMA+sql_prompt.pypid_equipment + tag_dcs 설명 추가 🟢 PidPrefixRule.csTagDcsproperty 추가🟢 PidEquipment.csTagDcsproperty 추가🟢 PidPrefixRuleDto.cs3개 record에 bool TagDcs🟢 PidController.cs:GetPrefixRules/GetEquipmenttagDcs필드 추가🟢 PidExtractorService.cs:ExtractFromStreamAsync추출 시 TagDcs 저장 🟢 PidExtractorService.cs:Create/UpdatePrefixRuleAsyncTagDcs 전달 🟢 PidExtractorService.cs:ExportToCsvAsync헤더+행 열 추가 🟢 ExperionDbContext.csDDL + ALTER TABLE IF NOT EXISTS + Fluent API 🟢 wwwroot/js/pid.jsPrefix UI checkbox + Add/Update body 🟢 wwwroot/panes/pid.html선택적 열 헤더 🟢 prompts/plant_context.mdtag_dcs 설명 추가
0. 배경 및 문제
현재 구조의 문제
현재 pid_prefix_rules.category = 'instrument' 아래에 두 종류가 혼재:
| 종류 | 예시 prefix | 실제 의미 |
|---|---|---|
| 현장 계기 (field) | FT, PT, LT, TT, FCV, PCV, PSV, XV, FG, PG | 물리적 기기, 현장 설치 |
| DCS 함수블록 (system) | FIC, TIC, PIC, LIC, FY, TY, PY, LY | DCS/SCADA 내부 연산 블록, 물리 기기 없음 |
기존 tag_class = 'field'/'system' 컬럼이 이를 구별하려 했으나:
- 추출 후 후처리에서 판정 (ISA 후속문자 분석 + Experion 연결 여부)
- PREFIX 정의 UI에서는 전혀 보이지 않아 운전원이 구별 불가
hasExperionLink를 "DCS 태그 여부"의 proxy로 잘못 사용 — FT 전송기도 Experion에 연결되므로 'system'으로 오분류됨- LLM이 pid_equipment 조회 시 instrument를 한꺼번에 가져와 혼동
목표
pid_prefix_rules 테이블에 tag_dcs BOOLEAN 추가 → PREFIX 분류 정의 시점부터 DCS 여부 명시.
pid_equipment 테이블에도 동일 컬럼 전파 → 추출 결과 전체에 flag 유지.
1. DCS vs Field 분류 기준
판단 원칙: 물리 기기 존재 여부
FT-6113(전송기)은 Experion에 연결되어 있어도 **현장 계기(field)**다.
Experion 연결 = 현장 신호를 DCS가 읽어오는 것이지, 기기 자체가 DCS 소프트웨어가 되는 게 아님.
DCS 태그 (tag_dcs = TRUE) — 물리 기기 없음, DCS 함수블록만 존재
| Prefix | 설명 | ISA 후속문자 |
|---|---|---|
| FIC, FICA, FICQ, FICR | Flow Indicator Controller (+ Alarm/Totalizer/Recorder 변형) | C, A, Q, R |
| TIC, TICA, TICQ | Temperature Indicator Controller 변형 | C, A, Q |
| PIC, PICA | Pressure Indicator Controller 변형 | C, A |
| LIC, LICA | Level Indicator Controller 변형 | C, A |
| FY, TY, PY, LY | Relay/Converter/Computing | Y |
| FV, TV, PV, LV | Valve function block output (DCS 출력 전용, 물리 FCV와 구별) | V(fb) |
_systemFuncLetters = {I, C, A, Q, Y, R}— ISA 표준 제어시스템 후속문자 전체 포함
주의: FCV/PCV/LCV/TCV는 물리적 제어밸브 →
tag_dcs = FALSE(field 유지)
현장 계기 (tag_dcs = FALSE) — 물리 기기
| Prefix | 설명 |
|---|---|
| FT, TT, PT, LT | 1차 측정 전송기 (Transmitter) — Experion 연결 여부 무관하게 field |
| FG, TG, PG, LG | 게이지류 (Gauge) |
| FCV, TCV, PCV, LCV | 제어밸브 (물리 기기) |
| PSV | 안전밸브 |
| XV | 차단밸브 |
| VIP, VIT | 진동 프로브/전송기 |
| DP | 차압계 |
| BV | 볼/버터플라이 밸브 |
2. 영향 범위 전체 목록
2.1 데이터베이스 (4곳)
| 대상 | 변경 내용 |
|---|---|
pid_prefix_rules 테이블 |
tag_dcs BOOLEAN NOT NULL DEFAULT FALSE 컬럼 추가 |
pid_prefix_rules 시드 |
DCS prefix에 tag_dcs = TRUE UPDATE |
pid_equipment 테이블 |
tag_dcs BOOLEAN NOT NULL DEFAULT FALSE 컬럼 추가 |
pid_equipment 기존 행 |
prefix rule StartsWith 기반 backfill |
마이그레이션 SQL (Boot DDL에 추가, 재기동 시 자동 실행):
-- Step 1: pid_prefix_rules 컬럼 추가
ALTER TABLE pid_prefix_rules
ADD COLUMN IF NOT EXISTS tag_dcs BOOLEAN NOT NULL DEFAULT FALSE;
-- Step 2: DCS prefix 마킹 (기본형 — compound형은 Step 4에서 커버)
UPDATE pid_prefix_rules
SET tag_dcs = TRUE
WHERE prefix IN ('FIC','TIC','PIC','LIC','FY','TY','PY','LY','FV','TV','PV','LV');
-- Step 3: pid_equipment 컬럼 추가
ALTER TABLE pid_equipment
ADD COLUMN IF NOT EXISTS tag_dcs BOOLEAN NOT NULL DEFAULT FALSE;
-- Step 4: 기존 행 backfill — StartsWith 매칭 (compound형 FICQ/FICA 등 자동 포함)
-- pid_equipment.instrument_type LIKE 'FIC%' → FIC, FICA, FICQ, FICR 모두 해당
UPDATE pid_equipment pe
SET tag_dcs = TRUE
FROM pid_prefix_rules pr
WHERE pe.instrument_type LIKE (pr.prefix || '%')
AND pr.tag_dcs = TRUE;
compound prefix 처리:
MatchCategoryAsync가 이미 StartsWith 매칭이므로 C# 추출 경로에서는FICQ-6113→FICrule의tag_dcs=TRUE가 자동 상속됨. backfill SQL도 동일 방식 적용.
2.2 C# 도메인 엔티티 (2파일)
src/Core/Domain/Entities/PidPrefixRule.cs
// 추가
[Column("tag_dcs")]
public bool TagDcs { get; set; } = false;
src/Core/Domain/Entities/PidEquipment.cs
// 추가 (tag_class 아래)
[Column("tag_dcs")]
public bool TagDcs { get; set; } = false;
2.3 DTOs (1파일, 3개 record)
src/Core/Application/DTOs/PidPrefixRuleDto.cs
// 수정 후 (TagDcs 추가)
public record PidPrefixRuleDto(
int Id, string Prefix, string Category, bool TagDcs,
string? Description, int SortOrder, DateTime CreatedAt);
public record CreatePidPrefixRuleRequest(
string Prefix, string Category,
bool TagDcs = false, string? Description = null, int SortOrder = 0);
public record UpdatePidPrefixRuleRequest(
string Prefix, string Category,
bool TagDcs = false, string? Description = null, int SortOrder = 0);
PidEquipmentDto(PidEquipmentDto.cs)는 매핑 탭 전용이며TagClass도 없으므로
이번 범위에서 제외 (선택사항, 매핑 탭 DCS 배지 표시 필요 시 별도 추가).
2.4 Application Services (1파일)
src/Core/Application/Services/PidExtractorService.cs
변경 1: tag_dcs 조회 경로 — 별도 ResolveTagDcsAsync(tagNo) 추가 (L2 결정)
// MatchCategoryAsync 시그니처 유지, 별도 메서드 추가 (최소 침습)
private async Task<bool> ResolveTagDcsAsync(string tagNo)
{
var rules = await GetRulesCachedAsync();
var upper = tagNo.ToUpperInvariant();
// StartsWith 매칭 (compound형 자동 포함)
var rule = rules
.Where(r => upper.StartsWith(r.Prefix.ToUpperInvariant()))
.OrderByDescending(r => r.Prefix.Length) // 가장 긴 prefix 우선
.FirstOrDefault();
return rule?.TagDcs ?? false;
}
변경 2: ClassifyTagClass() 재설계 — tag_dcs 우선, hasExperionLink 역할 변경 (M2 결정: Option A)
// 수정 후 시그니처 — tagDcs 파라미터 추가, hasExperionLink는 fallback용으로만 유지
private static string? ClassifyTagClass(string tagNo, string? category, bool tagDcs, bool hasExperionLink)
{
if (category != PidEquipment.CategoryInstrument) return null;
// tag_dcs가 true면 prefix rule이 ground truth → system 확정
// (FT 전송기가 Experion에 연결돼도 field — hasExperionLink 무관)
if (tagDcs) return PidEquipment.TagClassSystem;
// tag_dcs=FALSE: 현장 계기 → field
// hasExperionLink는 더 이상 TagClass 결정에 사용하지 않음
// (ExperionTagId FK로 연결 정보는 보존됨)
return PidEquipment.TagClassField;
}
변경 3: 추출 저장 시 TagDcs 채우기
var category = await MatchCategoryAsync(item.TagNo);
var tagDcs = await ResolveTagDcsAsync(item.TagNo);
var tagClass = ClassifyTagClass(item.TagNo, category, tagDcs, experionTag != null);
item.Category = category;
item.TagDcs = tagDcs;
item.TagClass = tagClass;
변경 4: CSV/Excel export — col18에 TagDcs 추가 (col17=id 보호 ⚠️ H1)
// ExportToExcelAsync
worksheet.Cells[1, 17].Value = "id"; // 기존 유지 — hasIdCol 감지 키
worksheet.Cells[1, 18].Value = "DCS태그"; // 신규 추가
// row write
worksheet.Cells[row, 17].Value = item.Id;
worksheet.Cells[row, 18].Value = item.TagDcs ? "DCS" : "현장";
변경 5: Excel import — col18 읽기 + Apply()에 TagDcs 설정
// ImportFromExcelAsync
// hasIdCol 감지: col17 = "id" (기존 동일)
var hasDcsCol = ws.Cells[1, 18].Text == "DCS태그";
// Apply() 내부
if (hasDcsCol)
{
var dcsVal = ws.Cells[row, 18].Text.Trim();
e.TagDcs = dcsVal == "DCS";
}
변경 6: ApplyCategoriesToExistingAsync() — 두 backfill 루프에 tag_dcs 추가 (L1)
// 1차 루프 (Category=null 행)
item.Category = category;
item.TagDcs = await ResolveTagDcsAsync(item.TagNo); // 추가
item.TagClass = ClassifyTagClass(item.TagNo, category, item.TagDcs, item.ExperionTagId != null);
// 2차 루프 (Category='instrument' AND TagClass=null 행)
item.TagDcs = await ResolveTagDcsAsync(item.TagNo); // 추가
item.TagClass = ClassifyTagClass(item.TagNo, item.Category, item.TagDcs, item.ExperionTagId != null);
메서드명 정정: 계획서 초안의
BackfillTagClassAsync()오기 → 실제 메서드는ApplyCategoriesToExistingAsync()(line 884)
변경 7: CreatePrefixRuleAsync / UpdatePrefixRuleAsync — request.TagDcs → rule.TagDcs 저장 후 InvalidateRulesCache() 호출 (기존 패턴 동일).
2.5 인터페이스 (변경 없음)
src/Core/Application/Interfaces/IExperionServices.cs
DTO record 변경만으로 CreatePrefixRuleAsync / UpdatePrefixRuleAsync 시그니처 자동 반영. 추가 수정 불필요.
2.6 EF Core DbContext (1파일)
src/Infrastructure/Database/ExperionDbContext.cs
변경 1: Boot DDL에 §2.1 마이그레이션 SQL 4개 Step 추가 (재기동 시 자동 실행)
변경 2: 시드 INSERT는 ON CONFLICT DO NOTHING → 기존 행 미반영.
Step 2 UPDATE로 기존 행의 tag_dcs 갱신 (Boot DDL에서 연속 실행).
변경 3: modelBuilder.Entity<PidPrefixRule>() — Column attribute로 자동 매핑되므로 Fluent API 추가 불필요.
2.7 Web Controllers (1파일)
src/Web/Controllers/PidController.cs
변경 1: GetPrefixRules — 익명객체 camelCase 반환 방식이므로 tagDcs: r.TagDcs 1줄 추가.
(진단 확인: 이미 anonymous object camelCase 사용 중 → 별도 [JsonPropertyName] 불필요)
변경 2: CreatePrefixRule / UpdatePrefixRule — DTO에 TagDcs 추가되므로 컨트롤러 수정 불필요.
2.8 Web UI (2파일)
src/Web/wwwroot/js/pid.js
변경 1: PREFIX 그룹 렌더링 (pidRenderPrefixGroups) — 각 행에 DCS/현장 배지 추가:
// view row
`<td><span class="badge ${r.tagDcs ? 'warn' : 'ok'}">${r.tagDcs ? 'DCS' : '현장'}</span></td>`
// edit row
`<input type="checkbox" id="pid-dcs-${r.id}" ${r.tagDcs ? 'checked' : ''} />`
`<label for="pid-dcs-${r.id}">DCS</label>`
변경 2: pidAddPrefixRule(category) 요청 body에 tagDcs 추가
변경 3: pidUpdatePrefixRule(id) 요청 body에 tagDcs 추가
src/Web/wwwroot/panes/pid.html
- PREFIX 분류 정의 패널 테이블 헤더에 "DCS태그" 열 추가
2.9 MCP Server Python (2파일)
mcp-server/server.py
변경 1: _DCS_PREFIXES 상수 추가 + _classify_pid_tag() 반환에 tag_dcs 포함
# compound형 포함 — ISA _systemFuncLetters 기준 확장
_DCS_PREFIXES: frozenset[str] = frozenset({
"FIC", "FICA", "FICQ", "FICR",
"TIC", "TICA", "TICQ",
"PIC", "PICA",
"LIC", "LICA",
"FY", "TY", "PY", "LY",
"FV", "TV", "PV", "LV",
})
# _classify_pid_tag() 반환
return {
"kind": "instrument",
"prefix": prefix,
"type": type_name,
"tag_dcs": prefix in _DCS_PREFIXES,
}
⚠️ M1 동기화 주의:
_DCS_PREFIXES(Python)와 C# seed UPDATE 목록은 수동 동기화 필요.
양쪽 변경 시 함께 수정. 향후 공유 모듈(dcs_prefixes.py) 분리 고려.
변경 2: _DB_SCHEMA에 pid_equipment 테이블 추가 (L4)
_DB_SCHEMA = """
...
테이블: pid_equipment (P&ID 추출 장비/계기)
tag_no TEXT - 태그번호 (예: FIC-6113, FT-6113)
category TEXT - 'instrument' / 'power_equipment' / 'storage_equipment' / ...
tag_dcs BOOL - TRUE=DCS 함수블록(FIC/TIC/PIC류), FALSE=현장 물리 계기(FT/FCV류)
tag_class TEXT - 'field'(현장) / 'system'(DCS) — tag_dcs 기반
instrument_type TEXT - ISA prefix (FT/FIC/P 등)
from_tag TEXT - 연결 상류 태그
to_tag TEXT - 연결 하류 태그
...
"""
변경 3: upsert_pid_connection — tag_dcs bool 인자 추가 + SQL 3곳 수정 (H2 확정)
| 위치 | 변경 내용 |
|---|---|
| 함수 시그니처 (line 990-997) | tag_dcs: bool | None = None 파라미터 추가 |
_n() 처리 (line 1035) |
bool은 _n() 미사용 — tag_dcs = bool(tag_dcs) if tag_dcs is not None else None |
_SNAP 목록 (line 1037) |
"tag_dcs" 추가 |
| SELECT 스냅샷 (line 1040) | tag_dcs 컬럼 추가 |
| UPDATE SET (line 1078) | tag_dcs=COALESCE(%s, tag_dcs) 추가 |
| INSERT 컬럼/값 (line 1094) | tag_dcs 추가, None이면 DEFAULT FALSE |
# bool 처리 예시
tag_dcs_val = bool(tag_dcs) if tag_dcs is not None else None
# COALESCE는 boolean도 정상 동작 — False 전달 시 False로 저장됨
mcp-server/worker/sql_prompt.py
DB_SCHEMA 상수에 pid_equipment 추가 (L4):
테이블: pid_equipment(tag_no TEXT, category TEXT, tag_dcs BOOL, tag_class TEXT,
instrument_type TEXT, from_tag TEXT, to_tag TEXT)
※ tag_dcs=TRUE: DCS 함수블록(FIC/TIC/PIC류), FALSE: 현장 물리 계기(FT/FCV류)
※ 연결 추적: from_tag(상류) → tag_no → to_tag(하류)
2.10 프롬프트 / 지식 파일 (1파일)
prompts/plant_context.md
## pid_equipment.tag_dcs — 현장 계기 vs DCS 함수블록 구별
- tag_dcs = TRUE: DCS 내부 함수블록 (FIC, TIC, PIC, LIC, FY, TY, PY, LY 등 compound형 포함)
- 물리 기기 없음. Experion DB 포인트로만 존재
- tag_dcs = FALSE: 현장 물리 계기 (FT, PT, LT, FCV, PSV, XV 등)
- P&ID 도면에 기기 심벌로 표시되는 실물. Experion 연결 여부 무관하게 field
쿼리 예:
- "DCS 태그 몇 개?" → SELECT COUNT(*) FROM pid_equipment WHERE tag_dcs=TRUE
- "현장 계기 목록" → SELECT * FROM pid_equipment WHERE tag_dcs=FALSE AND category='instrument'
- "FIC-6113이 DCS 태그인가?" → SELECT tag_dcs FROM pid_equipment WHERE tag_no='FIC-6113'
2.11 instrument_inference — 변경 없음
infer.py는 독립 실행 모듈(DB 미의존). 내부 _dcs_internal_roles 로직은 infer 전용으로 유지.
2.12 영향 없음 확인 (진단 결과)
| 파일 | 근거 |
|---|---|
IExperionServices.cs |
DTO 변경으로 자동 반영 |
ExperionDbContext.cs:v_pump_signal_map |
from_tag, tag_no, category만 조회 |
server.py:trace_connections |
tag_no, from_tag, to_tag, role만 조회 |
verifier/validators.py |
SELECT DISTINCT tag_no FROM pid_equipment |
SeedSubAreaAsync SQL |
WHERE role ILIKE '%공용%' |
3. 단계별 구현 순서
Phase 1: DB 스키마 (재기동으로 자동 적용)
ExperionDbContext.csBoot DDL에 §2.1 마이그레이션 SQL(Step 1~4) 추가- 웹 서버 재기동 → ALTER TABLE + UPDATE 자동 실행
Phase 2: 도메인/DTO/서비스 (C# 코어)
PidPrefixRule.cs—TagDcs bool프로퍼티 추가PidEquipment.cs—TagDcs bool프로퍼티 추가PidPrefixRuleDto.cs— 3개 record에TagDcs추가PidExtractorService.cs:ResolveTagDcsAsync()신규 추가ClassifyTagClass()시그니처 변경 + hasExperionLink 역할 변경ExtractFromStreamAsync()TagDcs 저장ExportToExcelAsync()col18 추가ImportFromExcelAsync()col18 읽기ApplyCategoriesToExistingAsync()두 루프에 tag_dcs 추가CreatePrefixRuleAsync()/UpdatePrefixRuleAsync()TagDcs 전달
dotnet build— 경고 0/에러 0 확인
Phase 3: Web Controller
PidController.cs:GetPrefixRules— 익명객체에tagDcs: r.TagDcs추가
Phase 4: Web UI
pid.js— PREFIX 그룹 렌더링 DCS/현장 배지 + Add/Update bodypanes/pid.html— "DCS태그" 열 헤더
Phase 5: MCP / LLM 경로
server.py—_DCS_PREFIXES+_classify_pid_tag+_DB_SCHEMA+upsert_pid_connectionworker/sql_prompt.py—pid_equipment테이블 추가prompts/plant_context.md— tag_dcs 설명 추가
Phase 6: 검증
dotnet build— 경고 0/에러 0python3 -m py_compile mcp-server/server.py— OK- DB 확인:
SELECT tag_dcs, COUNT(*) FROM pid_equipment GROUP BY tag_dcs - Excel 라운드트립: export → 열기 → col17=
id확인 → import → hasIdCol=true 확인 - 웹 UI: PREFIX 분류 탭 DCS/현장 배지 정상 표시
ApplyCategoriesToExistingAsyncAPI 호출 후tag_dcsbackfill 확인
4. 설계 결정
| 항목 | 결정 | 근거 |
|---|---|---|
| 컬럼 타입 | tag_dcs BOOLEAN (별도 category 아님) |
category 변경 시 뷰/필터 전파 과도. Boolean이 최소 침습적 |
tag_class 유지 |
유지 (deprecated 아님) | ExperionTagId FK와 함께 연결 증거 보존. tag_dcs는 prefix 기반 빠른 flag |
| M2: ClassifyTagClass 우선순위 | tag_dcs 우선 (Option A) | 전송기(FT)는 Experion 연결 여부와 무관하게 현장 계기. hasExperionLink는 DCS 함수블록 판별의 proxy로 부정확 |
hasExperionLink 역할 |
TagClass 결정에서 제외 | 연결 정보는 ExperionTagId로 보존됨. 더 이상 TagClass 결정에 사용 안 함 |
| FCV/PCV/LCV/TCV | tag_dcs = FALSE |
물리 제어밸브. DCS가 제어하지만 기기 자체는 현장 |
| FV/TV/PV/LV | tag_dcs = TRUE |
ISA 표준 "Valve function block output" — 물리 기기 아닌 DCS 출력 |
| compound prefix | LIKE StartsWith 매칭으로 자동 커버 | FICQ LIKE 'FIC%' = TRUE. 시드에 compound형 개별 추가 불필요 |
| Excel 열 위치 | col18 (col17=id 보호) |
col17 덮어쓰면 hasIdCol=false → 다중경로 데이터 손실 (H1) |
MatchCategoryAsync |
시그니처 유지, 별도 ResolveTagDcsAsync() 추가 |
호출부 3곳 리팩터 없이 최소 침습 (L2 Option 2) |
| backfill 트리거 | Boot DDL(SQL) + API(ApplyCategoriesToExistingAsync) 양쪽 |
Boot DDL은 컬럼+초기 UPDATE, 추출 오류 행은 API로 재실행 |
PidEquipmentDto |
이번 범위 제외 | 매핑 탭은 TagClass도 없음. 필요 시 별도 추가 |
5. 잔여/고려사항
-
_DCS_PREFIXES동기화: Pythonserver.py와 C# Boot DDL UPDATE 목록은 수동 동기화 필요.
양쪽 변경 시 함께 수정. 향후mcp-server/worker/dcs_prefixes.py분리로 단일 소스화 가능. -
compound prefix 시드 미등록:
pid_prefix_rules에FICQ가 없으면 UI에서 직접 추가한 경우tag_dcs=FALSE(default)로 저장될 수 있음. 운전원이 UI에서 FICQ를 추가할 때 DCS 체크박스를 직접 체크해야 함. -
ApplyCategoriesToExistingAsync수동 실행: Boot DDL의 Step 4 UPDATE로 기존 데이터 초기 backfill은 완료됨. 이후 신규 추출/오류 행은 P&ID 탭의 "Category 재적용" 버튼으로 API 호출. -
재추출 여부: 불필요. backfill SQL로 충분.