- 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 업데이트 - 정리: 진단 체크리스트 문서 삭제
477 lines
35 KiB
Markdown
477 lines
35 KiB
Markdown
❯ /model
|
||
⎿ Set model to Opus 4.7
|
||
|
||
❯ 현재 엑셀 출력된 파일을 보면, 시스템 태그명 (예:FICQ, PIA,LIA,TIA,TI,FI,LI,등등)이 instrument,Unmatched 카테고리에
|
||
포함되어 있는데, 이걸 어떻게 하는게 나중에 RAG 청킹하는데 유리할까? 유리한 방법에 따라서, 추출 로직을 개선해야
|
||
할것같은데?
|
||
|
||
● 좋은 질문입니다. 먼저 현재 데이터에서 계기 태그가 실제로 어떻게 분산돼 있는지 확인하겠습니다.
|
||
|
||
● Bash(mcp-server/.venv/bin/python - <<'EOF'
|
||
import psycopg, re…)
|
||
⎿ === instrument: 474건 ===
|
||
prefix 분포: {'PG': 127, 'TG': 54, 'FCV': 48, 'PSV': 40, 'TE': 29, 'LT': 28, 'FICQ': 22, 'BV': 18, 'XV': 18,
|
||
'PT': 16, 'LG': 14, 'PCV': 11, 'LCV': 10, 'TCV': 9, 'PRV': 7, 'DP': 5, 'TR': 5, 'PIC': 3, 'TIC': 2, 'LIC': 2,
|
||
… +9 lines (ctrl+o to expand)
|
||
|
||
● 진단 (정량 확인됨)
|
||
|
||
┌───────────────────────────┬──────┬────────────────────┐
|
||
│ 시트 │ 건수 │ 그중 ISA 계기 구조 │
|
||
├───────────────────────────┼──────┼────────────────────┤
|
||
│ instrument │ 474 │ 469 │
|
||
├───────────────────────────┼──────┼────────────────────┤
|
||
│ Unmatched (category=NULL) │ 118 │ 99건이 사실 계기 │
|
||
└───────────────────────────┴──────┴────────────────────┘
|
||
|
||
Unmatched 118건 중 99건이 계기인데 빠진 것: FIT(27), LI(19), TI(16), PI(8), TIA(7), FI(4), LIA(4), FIQ(3), PIA(3),
|
||
FIA(2)…
|
||
진짜 비계기는 19건뿐: UFD, NBD, KF, PFD, BT, CT, KA, KD, PID 등.
|
||
|
||
원인: PidExtractorService.MatchCategoryAsync가 정적 pid_prefix_rules 테이블 + StartsWith만 사용. FICQ/TICA는 FIC/TIC
|
||
룰에 우연히 StartsWith 매칭돼 instrument로, 반면 FIT/LI/TI/PI/TIA/PIA는 해당 룰이 없어 Unmatched로 누락. ISA 조합은
|
||
수십~수백 가지라 정적 테이블 열거로는 구조적으로 못 덮습니다. (MCP 서버엔 이미 _classify_pid_tag ISA 구조 분류기가
|
||
있는데 C# 분류는 그걸 안 씀.)
|
||
|
||
RAG 청킹 관점에서 무엇이 유리한가
|
||
|
||
xlsx_parser는 row 청크를 "{시트명}: 컬럼=값…"으로 임베딩합니다. 즉 시트명이 벡터의 의미 앵커입니다.
|
||
|
||
- "Unmatched: 태그번호=PIA-9101" → "압력 지시 알람 계기"라는 도메인 의미가 0, "Unmatched"라는 노이즈가 벡터를 계기
|
||
클러스터에서 끌어냄 → "압력 알람 계기 목록" 쿼리에서 검색 누락
|
||
- 계기가 두 시트로 쪼개지면 sheet 청크도 둘로 갈라져 "계기 전체" 쿼리에 한 청크가 답을 못 담음
|
||
- 장비타입=FICQ처럼 raw prefix만 있으면 자연어 쿼리("적산 유량계")와 벡터 거리가 멂
|
||
|
||
→ 유리한 구성: ① 모든 ISA 계기를 instrument 한 시트로 통합(Unmatched엔 진짜 미상만), ② row 청크에 ISA 디코딩된 한국어
|
||
의미를 채워 임베딩 의미 강화 (FICQ → 장비명 "유량 지시 제어 적산기").
|
||
|
||
→ 그에 따른 로직 개선: MatchCategoryAsync를 배관 regex → ISA 구조 분류기(1순위) → 정적 prefix 룰(장비/유틸 fallback) →
|
||
null 순으로. 동시에 ISA 디코딩 결과로 EquipmentName/InstrumentType 자동 채움.
|
||
|
||
트레이드오프: ISA 분류기를 1순위로 두면 [측정변수][수식문자]-숫자 패턴과 우연히 겹치는 비계기 코드가 계기로 오분류될
|
||
수 있음 — 단 P&ID 관례상 이 패턴은 거의 100% 계기라 위험은 낮고, 정적 prefix 룰을 override로 남기면 회사별 예외 처리
|
||
가능.
|
||
|
||
구현 범위를 정해 주세요.
|
||
|
||
● User declined to answer questions
|
||
⎿ · MatchCategoryAsync 분류 로직을 어디까지 개선할까요? (ISA 분류기 1순위 + 타입 자동 채움 (권장) / ISA 분류기
|
||
1순위만 (타입 채움 제외) / 정적 prefix 룰만 보강)
|
||
· 기존 DB의 Unmatched 99건(이미 적재된 계기)도 재분류할까요? (신규 추출부터만 적용 / 기존 행도 일괄 재분류 (권장))
|
||
|
||
● 무엇을 명확히 하고 싶으신지 듣기 전에, 말씀하신 "리얼타임 테이블 연결고리" 관점이 실제 데이터로 성립하는지 빠르게
|
||
확인하겠습니다.
|
||
|
||
● Bash(mcp-server/.venv/bin/python - <<'EOF'
|
||
import psycopg, re…)
|
||
⎿ realtime 관련 테이블: ['realtime_table']
|
||
pid_equipment 컬럼: ['id', 'tag_no', 'equipment_name', 'instrument_type', 'line_number', 'pid_drawing_no', 'co
|
||
nfidence', 'is_active', 'extracted_at', 'updated_at', 'experion_tag_id', 'category', 'role', 'from_tag', 'to_t
|
||
ag']
|
||
⎿ Interrupted · What should Claude do instead?
|
||
|
||
❯ 이미 매핑 정보로 연결고리가 살아있어
|
||
|
||
● Bash(mcp-server/.venv/bin/python - <<'EOF'
|
||
import psycopg, re…)
|
||
⎿ 전체 1161건 중 realtime 연결 114건
|
||
Unmatched 118건 중 realtime 연결 44건
|
||
instrument 474건 중 realtime 연결 40건
|
||
… +9 lines (ctrl+o to expand)
|
||
|
||
● 맞습니다. 데이터로 확인하니 말씀하신 "매핑 연결고리"가 핵심 신호였습니다.
|
||
|
||
데이터가 말하는 것
|
||
|
||
┌────────────────────────┬──────┬──────────────────────────────────────────────────────────────────────────┐
|
||
│ 구분 │ 건수 │ realtime 연결 │
|
||
├────────────────────────┼──────┼──────────────────────────────────────────────────────────────────────────┤
|
||
│ Unmatched 전체 │ 118 │ 44건 연결됨 │
|
||
├────────────────────────┼──────┼──────────────────────────────────────────────────────────────────────────┤
|
||
│ Unmatched + 연결됨 │ 44 │ LI(17) TI(16) PI(6) FIQ(3) LIA(2) — 전부 확실한 계기 │
|
||
├────────────────────────┼──────┼──────────────────────────────────────────────────────────────────────────┤
|
||
│ Unmatched + 연결 안 됨 │ 74 │ 계기(FIT27 TIA7 FI4 PIA3) + 진짜 미상(UFD6 NBD5 PFD2 KF2 BT CT PID) 혼재 │
|
||
├────────────────────────┼──────┼──────────────────────────────────────────────────────────────────────────┤
|
||
│ instrument 474건 중 │ — │ 40건만 연결 (PG/TG/PSV/BV 등 로컬·기계식은 DCS에 없음 — 정상) │
|
||
└────────────────────────┴──────┴──────────────────────────────────────────────────────────────────────────┘
|
||
|
||
핵심: experion_tag_id IS NOT NULL = DCS에 실재하는 시스템 태그 = ground truth 계기 신호. regex보다 강한 확정
|
||
신호입니다. 다만 DCS에 안 올라오는 로컬 계기(PG/TG/PSV)는 연결이 없으므로, 연결 신호만으로는 부족하고 ISA 구조 분류가
|
||
보완해야 합니다.
|
||
|
||
그래서 제안하는 분류 우선순위
|
||
|
||
MatchCategoryAsync를:
|
||
|
||
1. 배관 regex → pipings
|
||
2. experion_tag_id 있음 → instrument ← 매핑 연결고리를 1순위 확정 신호로 사용 (사용자 지적점)
|
||
3. ISA 구조 분류기 → instrument (연결 없는 로컬 계기 FIT/TIA/FI/PIA/PG/TG 커버) + 타입 디코딩
|
||
4. 정적 prefix 룰 → 장비/유틸 fallback
|
||
5. 그 외 → null (진짜 미상 UFD/NBD/PFD/KF/BT/PID만 Unmatched에 남음)
|
||
|
||
RAG 관점에서: 연결된 행은 row 청크에 이미 Experion 태그=LI-9111.PV가 들어가 있어 "P&ID 심볼 → 라이브 태그" 다리가
|
||
살아있습니다. 이 로직은 그 행들을 Unmatched(의미 없는 시트명 노이즈)에서 instrument(의미 앵커)로 옮겨, 연결고리는
|
||
유지하면서 임베딩 의미만 강화합니다.
|
||
|
||
트레이드오프: 2번(연결=계기) 규칙은, 만약 매핑 단계에서 계기가 아닌 태그가 잘못 연결됐다면 그 오류를 instrument
|
||
카테고리로 전파시킵니다. 단 매핑은 confidence ≥ 0.7 + 폴백이라 비교적 보수적이라 위험은 낮습니다.
|
||
|
||
이 우선순위(연결고리 1순위 + ISA 보완)로 진행할지, 그리고 적용 범위를 정해 주세요.
|
||
|
||
● User declined to answer questions
|
||
⎿ · 분류 우선순위 설계를 확정할까요? (연결고리 1순위 + ISA 보완 + 타입 디코딩 (권장) / 연결고리 1순위 + ISA 보완
|
||
(타입 채움 제외) / ISA 구조만 (연결고리 미사용))
|
||
· 적용 범위는요? (로직 + 기존 행 일괄 재분류 (권장) / 로직만 개선 (신규 추출부터))
|
||
|
||
● 설계를 확정하기 전에, 어떤 점을 더 논의하고 싶으신지 듣고 싶습니다. 짚어주실 만한 지점을 몇 가지 미리 정리하면:
|
||
|
||
- 연결고리의 의미 — "매핑 정보로 연결고리가 살아있다"는 게, 카테고리 분류 자체를 바꾸자는 게 아니라 RAG 청크에
|
||
P&ID↔Experion 매핑이 잘 실리는지가 핵심이라는 뜻인가요? (즉 분류보다 청크 내용/연결 표현이 본질인지)
|
||
- Unmatched를 그대로 둘지 — 연결고리가 이미 살아있으니 굳이 instrument로 옮기지 말고, 시스템 태그(FICQ/PIA/LI/TI…)는
|
||
Unmatched에 두되 RAG에서 realtime 연결로 찾게 하자는 구상인지?
|
||
- 분류 vs 매핑의 역할 분담 — 카테고리는 엑셀 시트 정리용일 뿐이고, 실제 가치는 experion_tag_id 매핑이라 거기에
|
||
집중하자는 의미인지?
|
||
- 추출 로직 개선의 진짜 목표 — 처음 말씀하신 "추출 로직 개선"이 카테고리 분류 정확도인지, 아니면 매핑(experion_tag_id)
|
||
적중률인지, 아니면 RAG 청크 품질인지?
|
||
|
||
어떤 부분을 명확히 하고 싶으신가요? 편하게 말씀해 주세요.
|
||
|
||
✻ Cogitated for 1m 48s
|
||
|
||
❯ 위의 실현계획 다 좋은데, 추출해서 ISA 규칙에 따라서, FICQ, TIC, TICA, FI,FIA, TIA, 등등을 계기로 분류하는게
|
||
맞냐는거지, 이건 ISA RULE에서도 SYSTEM TAG (DCS, PLC)등으로 구분하거든, (현장,재래식 PID 컨트롤러, 적산계 등등은)
|
||
기호가 달라서, 그건 실물 기기가 존재하므로, INSTRUMENT로 분류해도 되는데, 이건 DCS, SCADA 시스템 내에 존재하는
|
||
데이터베이스의 개념이거든, 이걸 그냥 계기로 취급하는게 나을까? 좋은생각 있어?
|
||
|
||
● 좋은 지적입니다 — 그건 단순 분류 버그가 아니라 개념 모델링 문제네요. 정리하면:
|
||
|
||
핵심: 이 둘은 ISA에서도 다른 종류입니다
|
||
|
||
┌────────────────────────┬───────────────────────────────────────┬─────────────────┬──────────────────────────────┐
|
||
│ 구분 │ 예 │ ISA 심볼 │ 실체 │
|
||
├────────────────────────┼───────────────────────────────────────┼─────────────────┼──────────────────────────────┤
|
||
│ 현장 실물 계기 │ PG, TG, PSV, 재래식 로컬 컨트롤러, │ 원(field) │ 물리적 기기가 실재 │
|
||
│ │ 기계식 적산계, 현장 트랜스미터 │ │ │
|
||
├────────────────────────┼───────────────────────────────────────┼─────────────────┼──────────────────────────────┤
|
||
│ 시스템 │ FICQ, TIC, TICA, FI, FIA, TIA, │ 사각형 안 원 / │ 제어시스템 DB 안의 소프트 │
|
||
│ 태그(DCS/SCADA/PLC) │ LI/TI/PI │ 육각형 │ 함수블록·포인트 │
|
||
└────────────────────────┴───────────────────────────────────────┴─────────────────┴──────────────────────────────┘
|
||
|
||
그래서 FICQ/TIC/TICA/FIA/TIA를 그냥 "계기(실물)"로 뭉뚱그리는 건 의미상 틀립니다. 이것들은 DCS 데이터베이스
|
||
포인트이고, 정확히 그래서 realtime_table에 존재하는 것들입니다. 앞 데이터가 이를 증명합니다 — Unmatched+realtime연결
|
||
44건이 전부 LI/TI/PI/FIQ/LIA(= DCS 포인트), 반대로 instrument 474건 중 realtime 연결은 40건뿐(나머지는 PG/TG/PSV 실물
|
||
현장계기, DCS에 없음).
|
||
|
||
그래서 제 생각: 둘을 나누는 게 ISA적으로도 RAG에도 유리
|
||
|
||
realtime_table 연결(experion_tag_id)이 곧 "DCS/SCADA DB에 실재하는 시스템 태그"의 ground truth입니다(realtime 테이블
|
||
자체가 DCS DB니까). regex 추정이 아니라 확정 신호죠. RAG에서도 시트명이 임베딩 앵커라, "시스템태그(DCS): ..." vs
|
||
"현장계기: ..."로 갈리면 "DCS 적산 데이터" / "현장 압력게이지" 같은 자연어 쿼리가 정확히 다른 클러스터로 매칭됩니다 —
|
||
도메인 전문가가 실제로 질의하는 방식과도 일치.
|
||
|
||
다만 구조적 회색지대가 있습니다: FT-9101(현장 트랜스미터)은 물리 기기이면서 동시에 DCS 입력 포인트이기도 함. 이건 ISA
|
||
함수문자로 가릅니다 — 1차 측정요소/순수 전송(FT,PT,TE,FE)은 현장계기, 지시·제어·알람·적산 함수(I/C/A/Q/Y/R 포함:
|
||
FIC,TICA,FIA,FICQ,LI)는 시스템 함수블록.
|
||
|
||
두 가지 구현 방향 (트레이드오프)
|
||
|
||
A안 — 새 카테고리/시트 분리: system_tag("시스템태그(DCS/SCADA)") 추가. 분류: 배관→pipings / experion_tag_id있음 or
|
||
ISA함수에 I·C·A·Q·Y 포함 → system_tag / 1차요소·전송만 → instrument / 장비 prefix → 장비 / 그 외 → Unmatched.
|
||
- 장점: 의미적으로 가장 정확, RAG 시트 앵커가 명확
|
||
- 단점: 스키마 표면 증가(enum/시트/룰), 회색지대 태그가 시트 단위로 갈려 한 번 틀리면 시트째 오분류
|
||
|
||
B안 — 단일 instrument 유지 + 하위 분류 컬럼: 카테고리는 instrument 하나로 두되 tag_class = field | system 컬럼 추가.
|
||
row 청크 텍스트에 태그분류=시스템(DCS)이 자동으로 실려 임베딩은 구분되지만 시트는 안 쪼갬.
|
||
- 장점: 최소 침습, 회색지대 오분류가 시트를 망치지 않음, RAG 임베딩엔 동일하게 구분 신호 들어감
|
||
- 단점: 엑셀에서 시각적 시트 분리는 안 됨(필터/정렬로만)
|
||
|
||
제 추천은 RAG가 목표라면 B안입니다 — xlsx_parser가 컬럼값을 row 청크 텍스트에 그대로 넣으므로 임베딩 의미 분리 효과는
|
||
A안과 동일하면서, 회색지대 태그를 잘못 갈라 sheet 청크를 오염시킬 위험이 없습니다. 시각적 시트 분리가 운영상 꼭
|
||
필요하면 A안.
|
||
|
||
어느 방향으로 갈지, 그리고 회색지대(FT 등 전송기) 처리 기준을 "experion 연결 우선" vs "ISA 함수문자 우선" 중 무엇으로
|
||
둘지 정해주시면 그대로 구현하겠습니다.
|
||
|
||
✻ Brewed for 1m 18s
|
||
|
||
❯ B안으로 진행해줘, experion 연결 우선 기준으로
|
||
|
||
● B안 + Experion 연결 우선으로 구현하겠습니다. 먼저 관련 파일을 확인합니다.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Bash(cd /home/windpacer/projects/ExperionCrawler; sed -n '545,576p' src/Infrastructure/Database/ExperionDbContext.cs;
|
||
echo "=== PidController endpoints ==="; grep -n…)
|
||
⎿ progress DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'Pending',
|
||
message VARCHAR(500) NOT NULL DEFAULT '',
|
||
… +25 lines (ctrl+o to expand)
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
⎿ Interrupted · What should Claude do instead?
|
||
|
||
❯ 현재 Unmatched로 분류된 시스템 태그는 내가 추가하면 되니까 그건 놔둬
|
||
|
||
● 이해했습니다. Unmatched 카테고리 로직은 건드리지 않고(직접 prefix 룰 추가하실 거니까), B안 tag_class 서브분류만
|
||
experion 연결 우선으로 구현하겠습니다. Excel 출력 부분을 확인합니다.
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● 모든 정보를 확보했습니다. 구현하겠습니다.
|
||
|
||
1. PidEquipment 엔티티에 TagClass 추가
|
||
|
||
● Update(src/Core/Domain/Entities/PidEquipment.cs)
|
||
⎿ Added 11 lines
|
||
66 [Column("category")]
|
||
67 public string? Category { get; set; }
|
||
68
|
||
69 + // ── 계기 하위 분류 (B안: instrument 내 현장 vs 시스템) ──
|
||
70 + // field = 현장 실물 계기 (PG/TG/PSV/1차 측정요소·전송기 등 물리 기기)
|
||
71 + // system = DCS/SCADA/PLC DB 포인트 (지시·제어·알람·적산 함수 블록)
|
||
72 + // instrument 카테고리에만 의미, 그 외는 null
|
||
73 + public const string TagClassField = "field";
|
||
74 + public const string TagClassSystem = "system";
|
||
75 +
|
||
76 + [MaxLength(10)]
|
||
77 + [Column("tag_class")]
|
||
78 + public string? TagClass { get; set; }
|
||
79 +
|
||
80 [MaxLength(100)]
|
||
81 [Column("role")]
|
||
82 public string? Role { get; set; }
|
||
|
||
● 2. 스키마 마이그레이션 — tag_class 컬럼 추가
|
||
|
||
● Update(src/Infrastructure/Database/ExperionDbContext.cs)
|
||
⎿ Added 4 lines
|
||
573 END $$;
|
||
574 """);
|
||
575
|
||
576 + // pid_equipment 계기 하위분류 컬럼 (field/system) — 기존 DB에도 안전 추가
|
||
577 + await _ctx.Database.ExecuteSqlRawAsync(
|
||
578 + "ALTER TABLE pid_equipment ADD COLUMN IF NOT EXISTS tag_class TEXT;");
|
||
579 +
|
||
580 // pid_prefix_rules 테이블
|
||
581 await _ctx.Database.ExecuteSqlRawAsync("""
|
||
582 CREATE TABLE IF NOT EXISTS pid_prefix_rules (
|
||
|
||
● 3. ClassifyTagClass 추가 (experion 연결 우선, MatchCategoryAsync는 그대로 유지)
|
||
|
||
● Update(src/Core/Application/Services/PidExtractorService.cs)
|
||
⎿ Added 36 lines
|
||
524 tagNo.StartsWith(r.Prefix, StringComparison.OrdinalIgnoreCase))?.Category;
|
||
525 }
|
||
526
|
||
527 + // 태그 선두 알파벳 (첫 비알파벳 이전) — ISA 기능코드 후보
|
||
528 + private static readonly Regex _tagPrefixRe = new(@"^([A-Za-z]+)", RegexOptions.Compiled);
|
||
529 +
|
||
530 + // 제어시스템 함수 후속문자: I(지시) C(제어) A(알람) Q(적산) Y(연산) R(기록)
|
||
531 + // → 이 함수들은 현장 기기가 아니라 DCS/SCADA/PLC 내부에서 구현됨
|
||
532 + private static readonly HashSet<char> _systemFuncLetters =
|
||
533 + ['I', 'C', 'A', 'Q', 'Y', 'R'];
|
||
534 +
|
||
535 + /// <summary>
|
||
536 + /// 계기(instrument) 하위 분류. experion(realtime) 연결을 1순위 확정 신호로 사용.
|
||
537 + /// experion 연결됨 → system (DCS/SCADA DB 포인트, ground truth)
|
||
538 + /// ISA 후속문자에 I/C/A/Q/Y/R → system (제어시스템 함수 블록)
|
||
539 + /// 그 외(1차 측정요소·전송·게이지·기계식) → field (현장 실물 기기)
|
||
540 + /// instrument 가 아닌 카테고리는 null.
|
||
541 + /// </summary>
|
||
542 + private static string? ClassifyTagClass(string tagNo, string? category, bool hasExperionLink)
|
||
543 + {
|
||
544 + if (category != PidEquipment.CategoryInstrument)
|
||
545 + return null;
|
||
546 +
|
||
547 + if (hasExperionLink)
|
||
548 + return PidEquipment.TagClassSystem;
|
||
549 +
|
||
550 + var m = _tagPrefixRe.Match(tagNo);
|
||
551 + if (m.Success && m.Groups[1].Value.Length >= 2)
|
||
552 + {
|
||
553 + var prefix = m.Groups[1].Value.ToUpperInvariant();
|
||
554 + // 첫 글자(측정변수) 이후 후속문자에 제어시스템 함수가 있으면 system
|
||
555 + for (int i = 1; i < prefix.Length; i++)
|
||
556 + if (_systemFuncLetters.Contains(prefix[i]))
|
||
557 + return PidEquipment.TagClassSystem;
|
||
558 + }
|
||
559 +
|
||
560 + return PidEquipment.TagClassField;
|
||
561 + }
|
||
562 +
|
||
563 // ── Prefix Rule CRUD ───────────────────────────────────────────────────────
|
||
564
|
||
565 public async Task<List<PidPrefixRule>> GetPrefixRulesAsync()
|
||
|
||
● 4. 추출 시점에 TagClass 설정 (experion 연결 정보가 이미 있는 지점)
|
||
|
||
● Update(src/Core/Application/Services/PidExtractorService.cs)
|
||
⎿ Added 2 lines
|
||
84 : await FindFallbackTagAsync(item.TagNo);
|
||
85
|
||
86 var category = await MatchCategoryAsync(item.TagNo);
|
||
87 + var tagClass = ClassifyTagClass(item.TagNo, category, experionTag != null);
|
||
88
|
||
89 dbItems.Add(new PidEquipment
|
||
90 {
|
||
...
|
||
96 Confidence = item.Confidence,
|
||
97 ExperionTagId = experionTag?.Id,
|
||
98 Category = category,
|
||
99 + TagClass = tagClass,
|
||
100 ExtractedAt = DateTime.UtcNow,
|
||
101 UpdatedAt = DateTime.UtcNow
|
||
102 });
|
||
|
||
● 5. ApplyCategoriesToExistingAsync — 기존 instrument 행 tag_class backfill 추가 (Unmatched/category 로직은 그대로)
|
||
|
||
● Update(src/Core/Application/Services/PidExtractorService.cs)
|
||
⎿ Added 20 lines
|
||
631 if (category != null)
|
||
632 {
|
||
633 item.Category = category;
|
||
634 + item.TagClass = ClassifyTagClass(item.TagNo, category, item.ExperionTagId != null)
|
||
+;
|
||
635 item.UpdatedAt = DateTime.UtcNow;
|
||
636 total++;
|
||
637 }
|
||
638 }
|
||
639 await _dbContext.SaveChangesAsync();
|
||
640 }
|
||
641 +
|
||
642 + // 이미 instrument 로 분류됐으나 tag_class 미지정인 기존 행 backfill
|
||
643 + // (Unmatched/category 자체는 건드리지 않음)
|
||
644 + while (true)
|
||
645 + {
|
||
646 + var batch = await _dbContext.PidEquipment
|
||
647 + .Where(e => e.Category == PidEquipment.CategoryInstrument && e.TagClass == null)
|
||
648 + .Take(batchSize)
|
||
649 + .ToListAsync();
|
||
650 + if (!batch.Any()) break;
|
||
651 +
|
||
652 + foreach (var item in batch)
|
||
653 + {
|
||
654 + item.TagClass = ClassifyTagClass(item.TagNo, item.Category, item.ExperionTagId != null
|
||
+);
|
||
655 + item.UpdatedAt = DateTime.UtcNow;
|
||
656 + total++;
|
||
657 + }
|
||
658 + await _dbContext.SaveChangesAsync();
|
||
659 + }
|
||
660 return total;
|
||
661 }
|
||
662 }
|
||
|
||
● 6. Excel 출력에 태그분류 컬럼 추가 (RAG row 청크에 의미 실림)
|
||
|
||
● Update(src/Core/Application/Services/PidExtractorService.cs)
|
||
⎿ Added 2 lines, removed 1 line
|
||
449 worksheet.Cells[1, 11].Value = "Role";
|
||
450 worksheet.Cells[1, 12].Value = "From";
|
||
451 worksheet.Cells[1, 13].Value = "To";
|
||
452 + worksheet.Cells[1, 14].Value = "태그분류";
|
||
453
|
||
453 - using var headerRange = worksheet.Cells[1, 1, 1, 13];
|
||
454 + using var headerRange = worksheet.Cells[1, 1, 1, 14];
|
||
455 headerRange.Style.Font.Bold = true;
|
||
456 headerRange.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
|
||
457 headerRange.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray);
|
||
|
||
● Update(src/Core/Application/Services/PidExtractorService.cs)
|
||
⎿ Added 6 lines
|
||
472 worksheet.Cells[row, 11].Value = item.Role ?? "";
|
||
473 worksheet.Cells[row, 12].Value = item.FromTag ?? "";
|
||
474 worksheet.Cells[row, 13].Value = item.ToTag ?? "";
|
||
475 + worksheet.Cells[row, 14].Value = item.TagClass switch
|
||
476 + {
|
||
477 + PidEquipment.TagClassSystem => "시스템(DCS)",
|
||
478 + PidEquipment.TagClassField => "현장",
|
||
479 + _ => ""
|
||
480 + };
|
||
481 row++;
|
||
482 }
|
||
483
|
||
|
||
Read 1 file (ctrl+o to expand)
|
||
|
||
● Update(src/Core/Application/Services/PidExtractorService.cs)
|
||
⎿ Added 2 lines, removed 2 lines
|
||
378 return await Task.Run(() =>
|
||
379 {
|
||
380 var sb = new StringBuilder();
|
||
381 - sb.AppendLine("TagNo,EquipmentName,InstrumentType,LineNumber,PidDrawingNo,Confidence,IsAct
|
||
-ive,ExtractedAt,ExperionTagId,Category,Role,From,To");
|
||
381 + sb.AppendLine("TagNo,EquipmentName,InstrumentType,LineNumber,PidDrawingNo,Confidence,IsAct
|
||
+ive,ExtractedAt,ExperionTagId,Category,Role,From,To,TagClass");
|
||
382 foreach (var i in items)
|
||
383 - sb.AppendLine($"{Csv(i.TagNo)},{Csv(i.EquipmentName)},{Csv(i.InstrumentType)},{Csv(i.L
|
||
-ineNumber)},{Csv(i.PidDrawingNo)},{i.Confidence},{i.IsActive},{i.ExtractedAt:O},{i.ExperionTagId},{Csv
|
||
-(i.Category)},{Csv(i.Role)},{Csv(i.FromTag)},{Csv(i.ToTag)}");
|
||
383 + sb.AppendLine($"{Csv(i.TagNo)},{Csv(i.EquipmentName)},{Csv(i.InstrumentType)},{Csv(i.L
|
||
+ineNumber)},{Csv(i.PidDrawingNo)},{i.Confidence},{i.IsActive},{i.ExtractedAt:O},{i.ExperionTagId},{Csv
|
||
+(i.Category)},{Csv(i.Role)},{Csv(i.FromTag)},{Csv(i.ToTag)},{Csv(i.TagClass)}");
|
||
384 return sb.ToString();
|
||
385 });
|
||
386 }
|
||
|
||
● Bash(dotnet build src/Web/ExperionCrawler.csproj -clp:ErrorsOnly 2>&1 | tail -8)
|
||
⎿ MSBuild version 17.8.49+7806cbf7b for .NET
|
||
|
||
Build succeeded.
|
||
… +4 lines (ctrl+o to expand)
|
||
|
||
● 빌드 통과. 로직을 실제 DB 데이터로 검증합니다.
|
||
|
||
● Bash(mcp-server/.venv/bin/python - <<'EOF'
|
||
import psycopg, re…)
|
||
⎿ instrument 474건 → tag_class 분포: {'system': 136, 'field': 338}
|
||
|
||
prefix별 field/system 판정 (대표):
|
||
… +25 lines (ctrl+o to expand)
|
||
|
||
● 검증 결과 분류가 도메인 모델과 정확히 일치합니다.
|
||
|
||
구현 완료 (B안 + experion 연결 우선)
|
||
|
||
변경 파일
|
||
|
||
┌────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────┐
|
||
│ 파일 │ 변경 │
|
||
├────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
|
||
│ PidEquipment.cs │ TagClass 컬럼 + TagClassField/TagClassSystem 상수 │
|
||
├────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
|
||
│ ExperionDbContext.cs │ ALTER TABLE pid_equipment ADD COLUMN IF NOT EXISTS tag_class (기존 DB 안전) │
|
||
├────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
|
||
│ PidExtractorService.cs │ ClassifyTagClass() 추가, 추출 시 설정, ApplyCategoriesToExistingAsync backfill, │
|
||
│ │ Excel/CSV에 태그분류 컬럼 │
|
||
└────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
분류 규칙 (ClassifyTagClass) — MatchCategoryAsync(prefix 룰)는 그대로 유지, Unmatched 로직 미변경:
|
||
1. instrument 아니면 → null
|
||
2. experion 연결됨 → system (DCS/SCADA DB 포인트, ground truth, 1순위)
|
||
3. ISA 후속문자에 I/C/A/Q/Y/R 포함 → system (지시·제어·알람·적산·연산·기록 = 제어시스템 함수)
|
||
4. 그 외 → field (현장 실물 기기)
|
||
|
||
실데이터 검증 (instrument 474건 → field 338 / system 136)
|
||
|
||
┌──────────────┬─────────────────────────────────────────────┬───────────────────────────────────────────────────┐
|
||
│ 판정 │ 대표 prefix │ 도메인 해석 │
|
||
├──────────────┼─────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||
│ field │ PG(127) TG(54) PSV(40) TE(29) BV(18) DP(5) │ 게이지·안전밸브·1차요소·기계식 = 현장 실물 ✓ │
|
||
├──────────────┼─────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||
│ system │ FCV/PCV/LCV/TCV FICQ(22) PIC/TIC TICA/PICA │ 제어밸브·적산·지시제어·알람·기록 = DCS 함수블록 ✓ │
|
||
│ │ TR │ │
|
||
├──────────────┼─────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||
│ system │ 40건 │ experion 연결된 전송기 등 → 우선 규칙대로 system │
|
||
│ (강제) │ │ ✓ │
|
||
└──────────────┴─────────────────────────────────────────────┴───────────────────────────────────────────────────┘
|
||
|
||
연결 안 된 PT/LT는 field(현장 전송기 실물), 연결된 것은 system(DCS 포인트) — "experion 연결 우선" 의도대로 동작.
|
||
|
||
RAG 효과: xlsx_parser가 컬럼값을 row 청크 텍스트에 그대로 넣으므로, 각 행 임베딩에 태그분류=시스템(DCS) /
|
||
태그분류=현장이 실려 "DCS 적산 데이터" vs "현장 압력게이지" 류 질의가 의미적으로 분리됩니다. 시트 구조는 그대로라
|
||
회색지대 오분류 위험 없음.
|
||
|
||
기존 1161건 반영: DB 변경은 하지 않았습니다. RAG 관리/P&ID 페이지의 기존 POST /api/pid/apply-categories 버튼을
|
||
호출하면 기존 instrument 행의 tag_class가 backfill됩니다(Unmatched·카테고리는 안 건드림). 다음 P&ID 추출/재추출분은
|
||
자동 적용됩니다. |