Files
ExperionCrawler/plans/P&ID-추출-PREFIX-DB-수정플랜-bySonnet.md

34 KiB

P&ID 추출 PREFIX 분류 — tag_dcs 컬럼 도입 플랜

작성일: 2026-05-27
작성자: Sonnet4.6
목적: pid_prefix_rulespid_equipment 두 테이블에 tag_dcs BOOLEAN 컬럼을 추가해,
P&ID 추출 시작 시점부터 현장 계기(field instrument)와 DCS 태그(DCS function block)를 구별한다.


상세 진단 리포트 (실제 코드 대비 교차 검증)

진단일: 2026-05-27
진단자: big-pickle
방법: diagnosis-checklist.md 8-Step — 전체 코드베이스 read + 교차검증


읽은 파일 (STEP 3 — 코드 읽기)

레이어 파일 비고
Domain PidPrefixRule.cs, PidEquipment.cs 엔티티 현재 상태
DTO PidPrefixRuleDto.cs, PidEquipmentDto.cs Record 정의
Interface IExperionServices.cs IPidExtractorService 시그니처
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.cs GetPrefixRules/GetEquipment 응답 형식
Web UI pid.js, panes/pid.html, css/pid.css Prefix rules UI + equipment table
Python MCP server.py (전체) upsert_pid_connection, _classify_pid_tag, _DB_SCHEMA, trace_connections
Python worker/sql_prompt.py NL2SQL DB_SCHEMA
Prompt prompts/plant_context.md LLM 시스템 프롬프트
기타 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:535worksheet.Cells[1, 17].Value = "id"
  • PidExtractorService.cs:623hasIdCol 감지: ws.Cells[1, 17].Text == "id"
  • PidExtractorService.cs:684-691hasIdCol=true일 때만 id 기반 in-place UPDATE 수행

영향: col17에 "DCS태그"가 들어가면 hasIdCol=false모든 행이 TagNo fallback(old format) 매칭으로 폴백. 같은 TagNo에 다중 경로가 있으면 전부 동일한 값으로 덮어써져 다중경로 데이터 손실. 수동으로 교정한 connection_locked 행도 초기화 위험.

수정 방향:

  1. tag_dcscol18에 배치, col17=id 유지
  2. ExportToExcelAsync:535worksheet.Cells[1, 18].Value = "DCS태그" 추가
  3. ExportToExcelAsync:562-568 — row write에 col18 추가
  4. ImportFromExcelAsync — col18 읽기 + Apply()e.TagDcs 설정
  5. 계획서 §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 = None tag_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_locked tag_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 FALSE
INSERT param (1101) 9개 param +1 param

위험: COALESCE(%s, tag_dcs)에서 tag_dcs=FalseFalse로 정상 전달되므로 문제 없음. 단, _n() 함수 사용 시 주의 — boolean은 str(v)strip() 하면 안 됨.


🟠 MED — 조건부 장애 (특정 상황에서 발생)

M1. _classify_pid_tag — DCS prefix vs instrument prefix 이중 정의 (동기화 위험)

문제: server.py:_classify_pid_tag() (257-270)는 DB pid_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:263if 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_tagtag_dcs 필드는 DXF 추출 결과에만 사용되며 DB 저장 시 C#에서 다시 판정하므로, 일시적 불일치는 허용됨. 단, Phase 6 검증 시 DXF ↔ DB 분류 일관성 확인 필요. 또는 _DCS_PREFIXES를 별도 공유 모듈로 분리 고려.


문제: 계획서 §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=TRUETagClassSystem을 강제. 경계 사례에서 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:793MatchCategoryAsyncStartsWith 매칭이므로 FICQ-6113FIC rule에 매칭되어 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를 포함한 FI prefix면 충분.


🟡 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:90var category = await MatchCategoryAsync(item.TagNo);
  • PidExtractorService.cs:787-795 — return string?
  • GetRulesCachedAsync(754)가 이미 List<PidPrefixRule>를 캐싱하므로 tag_dcs도 함께 사용 가능

수정 방향: 3가지 옵션

  1. MatchCategoryAsync(string? category, bool tagDcs) 튜플: 리팩터 범위 큼 (호출부 3곳)
  2. 별도 ResolveTagDcsAsync(tagNo) 추가: 최소 침습
  3. 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_map FROM pid_equipment ftft.from_tag, ft.tag_no, ft.category만 조회 tag_dcs 미사용
server.py:trace_connections SELECT tag_no, from_tag, to_tag, role tag_dcs 미조회
validators.py:37 SELECT DISTINCT tag_no FROM pid_equipment tag_dcs 무관
instrument_inference/infer.py 독립 실행 모듈, DB 미의존 계획서 §2.11 판단 올바름
SeedSubAreaAsync SQL FROM pid_equipment WHERE role ILIKE '%공용%' tag_dcs 무관

수정 필요 파일 요약 (총 19곳)

등급 파일명 변경 내용
🔴 H1 PidExtractorService.cs Excel col17→col18 재배치 + Import hasIdCol 보호 + Apply() TagDcs
🔴 H2 server.py:upsert_pid_connection tag_dcs bool 인자 + SELECT/UPDATE/INSERT 3개 SQL
🟠 M1 server.py:_classify_pid_tag _DCS_PREFIXES 추가 — C# seed와 동기화 문서화
🟠 M2 PidExtractorService.cs:ClassifyTagClass tag_dcs 가중치 결정 및 시그니처 변경
🟠 M3 ExperionDbContext.cs:seed 46개 prefix DCS 분류 + compound형(FICQ 등)
🟡 L1 PidExtractorService.cs:ApplyCategoriesToExistingAsync backfill에 tag_dcs 추가
🟡 L2 PidExtractorService.cs:MatchCategoryAsync tagDcs 반환 경로 추가
🟡 L3 PidEquipmentDto.cs 선택적 TagDcs 추가
🟡 L4 server.py:_DB_SCHEMA + sql_prompt.py pid_equipment + tag_dcs 설명 추가
🟢 PidPrefixRule.cs TagDcs property 추가
🟢 PidEquipment.cs TagDcs property 추가
🟢 PidPrefixRuleDto.cs 3개 record에 bool TagDcs
🟢 PidController.cs:GetPrefixRules/GetEquipment tagDcs 필드 추가
🟢 PidExtractorService.cs:ExtractFromStreamAsync 추출 시 TagDcs 저장
🟢 PidExtractorService.cs:Create/UpdatePrefixRuleAsync TagDcs 전달
🟢 PidExtractorService.cs:ExportToCsvAsync 헤더+행 열 추가
🟢 ExperionDbContext.cs DDL + ALTER TABLE IF NOT EXISTS + Fluent API
🟢 wwwroot/js/pid.js Prefix UI checkbox + Add/Update body
🟢 wwwroot/panes/pid.html 선택적 열 헤더
🟢 prompts/plant_context.md tag_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-6113FIC rule의 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 — col18TagDcs 추가 (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 / UpdatePrefixRuleAsyncrequest.TagDcsrule.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_SCHEMApid_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_connectiontag_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 스키마 (재기동으로 자동 적용)

  1. ExperionDbContext.cs Boot DDL에 §2.1 마이그레이션 SQL(Step 1~4) 추가
  2. 웹 서버 재기동 → ALTER TABLE + UPDATE 자동 실행

Phase 2: 도메인/DTO/서비스 (C# 코어)

  1. PidPrefixRule.csTagDcs bool 프로퍼티 추가
  2. PidEquipment.csTagDcs bool 프로퍼티 추가
  3. PidPrefixRuleDto.cs — 3개 record에 TagDcs 추가
  4. PidExtractorService.cs:
    • ResolveTagDcsAsync() 신규 추가
    • ClassifyTagClass() 시그니처 변경 + hasExperionLink 역할 변경
    • ExtractFromStreamAsync() TagDcs 저장
    • ExportToExcelAsync() col18 추가
    • ImportFromExcelAsync() col18 읽기
    • ApplyCategoriesToExistingAsync() 두 루프에 tag_dcs 추가
    • CreatePrefixRuleAsync() / UpdatePrefixRuleAsync() TagDcs 전달
  5. dotnet build — 경고 0/에러 0 확인

Phase 3: Web Controller

  1. PidController.cs:GetPrefixRules — 익명객체에 tagDcs: r.TagDcs 추가

Phase 4: Web UI

  1. pid.js — PREFIX 그룹 렌더링 DCS/현장 배지 + Add/Update body
  2. panes/pid.html — "DCS태그" 열 헤더

Phase 5: MCP / LLM 경로

  1. server.py_DCS_PREFIXES + _classify_pid_tag + _DB_SCHEMA + upsert_pid_connection
  2. worker/sql_prompt.pypid_equipment 테이블 추가
  3. prompts/plant_context.md — tag_dcs 설명 추가

Phase 6: 검증

  1. dotnet build — 경고 0/에러 0
  2. python3 -m py_compile mcp-server/server.py — OK
  3. DB 확인: SELECT tag_dcs, COUNT(*) FROM pid_equipment GROUP BY tag_dcs
  4. Excel 라운드트립: export → 열기 → col17=id 확인 → import → hasIdCol=true 확인
  5. 웹 UI: PREFIX 분류 탭 DCS/현장 배지 정상 표시
  6. ApplyCategoriesToExistingAsync API 호출 후 tag_dcs backfill 확인

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 동기화: Python server.py와 C# Boot DDL UPDATE 목록은 수동 동기화 필요.
    양쪽 변경 시 함께 수정. 향후 mcp-server/worker/dcs_prefixes.py 분리로 단일 소스화 가능.

  • compound prefix 시드 미등록: pid_prefix_rulesFICQ가 없으면 UI에서 직접 추가한 경우 tag_dcs=FALSE(default)로 저장될 수 있음. 운전원이 UI에서 FICQ를 추가할 때 DCS 체크박스를 직접 체크해야 함.

  • ApplyCategoriesToExistingAsync 수동 실행: Boot DDL의 Step 4 UPDATE로 기존 데이터 초기 backfill은 완료됨. 이후 신규 추출/오류 행은 P&ID 탭의 "Category 재적용" 버튼으로 API 호출.

  • 재추출 여부: 불필요. backfill SQL로 충분.