❯ /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 추출/재추출분은
  자동 적용됩니다.