# Enum Metadata 최적화 - 코딩 계획 > 작성일: 2026-05-08 > 상태: 진행 중 > 기반 문서: [`plans/enum-metadata-optimization.md`](plans/enum-metadata-optimization.md) > 목적: `state0~7descriptor` 제거, `desc`/`area`만 유지, pv 값 파싱 로직 추가 --- ## 작업 Todo 리스트 각 단계는 독립적으로 완료 여부를 추적할 수 있다. 체크박스를 사용하여 진행 상황을 기록한다. - [ ] STEP 1 — 백업: 수정 대상 파일들을 `.rooBackup/`에 백업 - [ ] STEP 2 — `MetadataLoaderService.cs`: `MetaAttributes` 배열에서 state0~7descriptor 제거 - [ ] STEP 2.5 — `MetadataLoaderService.cs`: 클래스 주석 업데이트 - [ ] STEP 3 — `MetadataLoaderService.cs` + `ExperionDbContext.cs`: 빌드 검증 - [ ] STEP 4 — `ExperionDbContext.cs`: `v_tag_summary` 뷰에서 state descriptor JOIN 제거 - [ ] STEP 4.5 — `tag_metadata` 고아 데이터 삭제 (선택적) - [ ] STEP 5 — `ExperionDbContext.cs` 변경 후 빌드 검증 - [ ] STEP 6 — `app.js`: pv 값 파싱 헬퍼 함수 `parseEnumPv()` 추가 - [ ] STEP 6.5 — NL2SQL DB_SCHEMA 동기화 (`server.py` + `nl2sql_worker.py`) - [ ] STEP 7 — `app.js`: pv 값 표시 관련 코드 모두 `parseEnumPv()` 적용 - [ ] STEP 8 — End-to-End 검증: 전체 빌드 + UI 테스트 - [ ] STEP 9 — git 커밋 및 정리 --- ## 변경 대상 파일 목록 | # | 파일 | 변경 내용 | 영향 범위 | |---|------|-----------|-----------| | 1 | `src/Infrastructure/OpcUa/MetadataLoaderService.cs` | `MetaAttributes` 배열 수정 + 주석 업데이트 | 메타데이터 로딩 | | 2 | `src/Infrastructure/Database/ExperionDbContext.cs` | `v_tag_summary` 뷰 단순화 + 고아 데이터 삭제 | DB 뷰 | | 3 | `src/Web/wwwroot/js/app.js` | pv 파싱 헬퍼 + 표시 로직 변경 | 프론트엔드 UI | | 4 | `mcp-server/server.py` | DB_SCHEMA에서 state0~2_descriptor 제거 | NL2SQL | | 5 | `mcp-server/worker/nl2sql_worker.py` | DB_SCHEMA에서 state0~2_descriptor 제거 | NL2SQL | --- ## 각 단계 상세 계획 --- ### STEP 1 — 백업: 수정 대상 파일들을 `.rooBackup/`에 백업 **목적**: 수정 전 원본 보존. 실패 시 복원 가능. **실행 명령**: ```bash TIMESTAMP=$(date +%Y%m%d%H%M) mkdir -p .rooBackup/enum-opt-$TIMESTAMP/src/Infrastructure/OpcUa mkdir -p .rooBackup/enum-opt-$TIMESTAMP/src/Infrastructure/Database mkdir -p .rooBackup/enum-opt-$TIMESTAMP/src/Web/wwwroot/js mkdir -p .rooBackup/enum-opt-$TIMESTAMP/mcp-server/worker cp src/Infrastructure/OpcUa/MetadataLoaderService.cs .rooBackup/enum-opt-$TIMESTAMP/src/Infrastructure/OpcUa/ cp src/Infrastructure/Database/ExperionDbContext.cs .rooBackup/enum-opt-$TIMESTAMP/src/Infrastructure/Database/ cp src/Web/wwwroot/js/app.js .rooBackup/enum-opt-$TIMESTAMP/src/Web/wwwroot/js/ cp mcp-server/server.py .rooBackup/enum-opt-$TIMESTAMP/mcp-server/ cp mcp-server/worker/nl2sql_worker.py .rooBackup/enum-opt-$TIMESTAMP/mcp-server/worker/ ``` **검증 기준**: - [ ] `.rooBackup/enum-opt-YYYYMMDDHHMM/` 폴더가 생성되고 5개 파일이 복사됨 - [ ] 원본 파일과 백업 파일의 체크섬이 일치함 (`md5sum` 비교) **enum-metadata-optimization.md 규칙 매핑**: - 섹션 3.3: 수정이 필요한 코드 — `MetadataLoaderService.cs`, `ExperionDbContext.cs`, `app.js` --- ### STEP 2 — `MetadataLoaderService.cs`: `MetaAttributes` 배열에서 state0~7descriptor 제거 **파일**: [`MetadataLoaderService.cs`](src/Infrastructure/OpcUa/MetadataLoaderService.cs:20) **변경 위치**: 20~26줄 (`MetaAttributes` 배열 정의) **변경 전 코드**: ```csharp // 로드할 메타데이터 속성 목록 private static readonly string[] MetaAttributes = { "desc", "area", "state0descriptor", "state1descriptor", "state2descriptor", "state3descriptor", "state4descriptor", "state5descriptor", "state6descriptor", "state7descriptor" }; ``` **변경 후 코드**: ```csharp // 로드할 메타데이터 속성 목록 (state0~7descriptor 제거 — pv 값에서 파싱) private static readonly string[] MetaAttributes = { "desc", "area" }; ``` **diff**: ```diff -// 로드할 메타데이터 속성 목록 +// 로드할 메타데이터 속성 목록 (state0~7descriptor 제거 — pv 값에서 파싱) private static readonly string[] MetaAttributes = { - "desc", "area", - "state0descriptor", "state1descriptor", "state2descriptor", - "state3descriptor", "state4descriptor", "state5descriptor", - "state6descriptor", "state7descriptor" + "desc", "area" }; ``` **변경 이유**: - pv 값 `{코드 | DisplayName | }`에 이미 state descriptor 정보가 포함되어 있으므로 중복 저장 제거 - OPC UA 읽기 요청 80% 감소 (10개 속성 → 2개 속성) - tag_metadata 행 80% 감소 (태그당 10행 → 2행) **영향 분석**: - `LoadMetadataAsync()` 메서드: `MetaAttributes.Contains(n.Name)`으로 `node_map_master`에서 필터링하므로 state descriptor 노드는 더 이상 조회되지 않음 - `ReadTagsAsync()`: 8개 menos의 nodeId로 배치 읽기 → 네트워크 트래픽 감소 - UPSERT 쿼리: 8개 menos의 행 삽입 → DB 부하 감소 - 클래스 주석 (11줄): `state0~7descriptor` 언급도 함께 제거 필요 **검증 기준**: - [ ] `MetaAttributes` 배열이 `["desc", "area"]` 두 개만 포함 - [ ] 컴파일 오류 없음 - [ ] `LoadMetadataAsync()` 메서드의 로직 변경 없이 배열 변경만으로 동작 **enum-metadata-optimization.md 규칙 매핑**: - 섹션 3.2: desc, area만 저장 - 섹션 3.3: `MetadataLoaderService.cs` 수정 — `MetaAttributes` 배열에서 state0~7descriptor 제거 - 섹션 4.2: tag_metadata 저장 속성 — desc, area 만 --- ### STEP 2.5 — `MetadataLoaderService.cs`: 클래스 주석 업데이트 **파일**: [`MetadataLoaderService.cs`](src/Infrastructure/OpcUa/MetadataLoaderService.cs:11) **변경 위치**: 11줄 (클래스 XML 주석) **변경 전 코드** (11줄): ```csharp /// 메타데이터(desc, area, state0~7descriptor)를 OPC UA에서 읽어서 tag_metadata 테이블에 저장/갱신 ``` **변경 후 코드**: ```csharp /// 메타데이터(desc, area)를 OPC UA에서 읽어서 tag_metadata 테이블에 저장/갱신 ``` **diff**: ```diff -/// 메타데이터(desc, area, state0~7descriptor)를 OPC UA에서 읽어서 tag_metadata 테이블에 저장/갱신 +/// 메타데이터(desc, area)를 OPC UA에서 읽어서 tag_metadata 테이블에 저장/갱신 ``` **변경 이유**: - STEP 2에서 `state0~7descriptor`를 제거했는데 주석에는 여전히 남아 있음 - 주석과 코드 불일치로 인한 혼란 방지 **검증 기준**: - [ ] 주석이 `desc, area`만 언급함 - [ ] 컴파일 오류 없음 (주석 변경이므로 영향 없음) --- ### STEP 3 — `MetadataLoaderService.cs` 변경 후 빌드 검증 **목적**: STEP 2 변경이 컴파일 오류 없이 통과하는지 확인 **실행 명령**: ```bash dotnet build src/Web/ExperionCrawler.csproj --no-restore -v q ``` **검증 기준**: - [ ] 빌드 성공 (exit code 0) - [ ] 경고 없음 (또는 기존 경고만) - [ ] `MetaAttributes` 배열 참조하는 다른 코드가 없는지 확인 **실패 시 대응**: - 컴파일 오류가 발생하면 오류 메시지를 읽고 원인 분석 - `MetaAttributes` 길이에 의존하는 코드가 있으면 해당 코드도 수정 --- ### STEP 4 — `ExperionDbContext.cs`: `v_tag_summary` 뷰에서 state descriptor JOIN 제거 **파일**: [`ExperionDbContext.cs`](src/Infrastructure/Database/ExperionDbContext.cs:302) **변경 위치**: 302~330줄 (`v_tag_summary` 뷰 생성 SQL) **변경 전 코드** (315~329줄): ```sql desc_md.value AS description, area_md.value AS area, s0d_md.value AS state0_descriptor, s1d_md.value AS state1_descriptor, s2d_md.value AS state2_descriptor FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv' LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp' LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op' LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0' LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1' LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2' LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc' LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area' LEFT JOIN tag_metadata s0d_md ON s0d_md.base_tag = rt_base.base_tag AND s0d_md.attribute = 'state0descriptor' LEFT JOIN tag_metadata s1d_md ON s1d_md.base_tag = rt_base.base_tag AND s1d_md.attribute = 'state1descriptor' LEFT JOIN tag_metadata s2d_md ON s2d_md.base_tag = rt_base.base_tag AND s2d_md.attribute = 'state2descriptor' ``` **변경 후 코드**: ```sql desc_md.value AS description, area_md.value AS area FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv' LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp' LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op' LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0' LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1' LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2' LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc' LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area' ``` **diff**: ```diff desc_md.value AS description, area_md.value AS area, - s0d_md.value AS state0_descriptor, - s1d_md.value AS state1_descriptor, - s2d_md.value AS state2_descriptor + area_md.value AS area FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv' LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp' LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op' LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0' LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1' LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2' LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc' LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area' - LEFT JOIN tag_metadata s0d_md ON s0d_md.base_tag = rt_base.base_tag AND s0d_md.attribute = 'state0descriptor' - LEFT JOIN tag_metadata s1d_md ON s1d_md.base_tag = rt_base.base_tag AND s1d_md.attribute = 'state1descriptor' - LEFT JOIN tag_metadata s2d_md ON s2d_md.base_tag = rt_base.base_tag AND s2d_md.attribute = 'state2descriptor' """); ``` **변경 이유**: - state0~2descriptor가 더 이상 tag_metadata에 저장되지 않으므로 JOIN 제거 - 뷰 조회 성능 향상 (3개 LEFT JOIN 제거) **영향 분석**: - `v_tag_summary` 뷰를 조회하는 코드가 `state0_descriptor`, `state1_descriptor`, `state2_descriptor` 컬럼을 참조하면 NULL 반환 - 이 뷰를 사용하는 곳이 있는지 검색 필요 (현재는 DB 초기화 시에만 사용) **검증 기준**: - [ ] `v_tag_summary` 뷰 SQL에서 state descriptor 관련 JOIN 3개 제거됨 - [ ] `area_md.value AS area` 뒤 쉼표 제거 (마지막 SELECT 컬럼) - [ ] SQL 문법 오류 없음 **enum-metadata-optimization.md 규칙 매핑**: - 섹션 3.3: `ExperionDbContext.cs` 수정 — v_tag_summary 뷰에서 state descriptor JOIN 제거 --- ### STEP 4.5 — `tag_metadata` 고아 데이터 삭제 (선택적) **파일**: [`ExperionDbContext.cs`](src/Infrastructure/Database/ExperionDbContext.cs:185) **변경 위치**: `InitializeAsync()` 메서드 내 `v_tag_summary` 뷰 생성 직후 (330줄 이후) **추가할 코드**: ```csharp // state descriptor 고아 데이터 정리 (state0~7descriptor는 더 이상 로딩하지 않음) await _ctx.Database.ExecuteSqlRawAsync(""" DELETE FROM tag_metadata WHERE attribute IN ( 'state0descriptor', 'state1descriptor', 'state2descriptor', 'state3descriptor', 'state4descriptor', 'state5descriptor', 'state6descriptor', 'state7descriptor' ) """); ``` **삽입 위치 상세**: - 330줄 (`""");` — v_tag_summary 뷰 생성 종료) 바로 다음에 삽입 - `CREATE EXTENSION IF NOT EXISTS timescaledb` 이전 **변경 이유**: - `MetaAttributes`에서 state descriptor가 제거되면 더 이상 갱신되지 않으나, 기존 데이터는 영구히 남음 - 테이블 크기와 불필요한 JOIN 결과 방지 **검증 기준**: - [ ] 실행 시 기존 state descriptor 행이 삭제됨 - [ ] `SELECT COUNT(*) FROM tag_metadata WHERE attribute LIKE 'state%descriptor'` → 0 반환 - [ ] desc/area 행은 영향 없음 **참고**: 기존 데이터를 보존해야 한다면 이 스텝을 스킵 가능. 하지만 `v_tag_summary` 뷰에서 해당 컬럼이 제거되었으므로 조회 자체가 불가능해짐. --- ### STEP 5 — `ExperionDbContext.cs` 변경 후 빌드 검증 **목적**: STEP 4 변경이 컴파일 오류 없이 통과하는지 확인 **실행 명령**: ```bash dotnet build src/Web/ExperionCrawler.csproj --no-restore -v q ``` **검증 기준**: - [ ] 빌드 성공 (exit code 0) - [ ] SQL 문자열 문법 오류 없음 (raw string literal 안에서 SQL 구문 확인) - [ ] 쉼표 누락/과잉 없음 **실패 시 대응**: - SQL 구문 오류가 발생하면 SELECT 컬럼 목록의 쉼표 확인 - `area_md.value AS area`가 마지막 컬럼이므로 쉼표 제거했는지 확인 --- ### STEP 6 — `app.js`: pv 값 파싱 헬퍼 함수 `parseEnumPv()` 추가 **파일**: [`app.js`](src/Web/wwwroot/js/app.js:2126) **삽입 위치**: `fmtVal()` 함수 바로 아래 (2132줄 이후) **추가할 코드**: ```javascript /** * OPC UA EnumValueType pv 값 파싱 * "{0 | L-STpv | }" → "L-STpv" * "{0 | MID | }" → "MID" * 일반 값은 그대로 반환 */ function parseEnumPv(v) { if (v == null) return v; const s = String(v); // "{코드 | DisplayName | }" 패턴 매칭 const m = s.match(/^\{\s*\d+\s*\|\s*([^|]+?)\s*\|\s*\}$/); return m ? m[1].trim() : s; } ``` **삽입 위치 상세**: - 2132줄 (`}` — `fmtVal` 함수 종료) 바로 다음에 삽입 - 기존 `fmtVal` 함수는 변경하지 않음 **기능 설명**: - 입력: pv 값 문자열 (예: `"{0 | L-STpv | }"`) - 출력: DisplayName 부분만 (예: `"L-STpv"`) - EnumValueType 형식이 아닌 일반 값은 그대로 반환 **정규식 설명**: - `^\{` — `{`로 시작 - `\s*\d+\s*` — 정수 코드 (공백 허용) - `\|\s*` — `|` 구분자 - `([^|]+?)` — DisplayName (첫 번째 캡처 그룹) - `\s*\|\s*\}$` — `| }`로 끝남 **검증 기준**: - [ ] `parseEnumPv("{0 | L-STpv | }")` → `"L-STpv"` 반환 - [ ] `parseEnumPv("{0 | MID | }")` → `"MID"` 반환 - [ ] `parseEnumPv("123.45")` → `"123.45"` 반환 (변경 없음) - [ ] `parseEnumPv(null)` → `null` 반환 - [ ] 브라우저 콘솔에서 수동 테스트 가능 **enum-metadata-optimization.md 규칙 매핑**: - 섹션 3.2: pv 값 파싱 방법 — `{코드 | 이름 | }`에서 DisplayName 추출 - 섹션 3.3: `app.js` — pv 값 표시 시 파싱 로직 추가 --- ### STEP 6.5 — NL2SQL DB_SCHEMA 동기화 (`server.py` + `nl2sql_worker.py`) **파일**: [`mcp-server/server.py`](mcp-server/server.py:447) / [`mcp-server/worker/nl2sql_worker.py`](mcp-server/worker/nl2sql_worker.py:78) **변경 위치**: 두 파일의 `DB_SCHEMA` 문자열 내 `tag_metadata` / `v_tag_summary` 설명 부분 **변경 전 코드** (`server.py:447` + `462-464` + `470` + `474-475`): ``` attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor', ...) ... state0_descriptor TEXT - 비트 0 의미 (예: "Run/Stop") state1_descriptor TEXT - 비트 1 의미 (예: "Remote/Local") state2_descriptor TEXT - 비트 2 의미 (예: "Trip/Normal") ... - 메타데이터: desc (String), area (Enum), state0descriptor~7 (String) ... - state0descriptor~7은 해당 비트의 의미 설명 - instate0=true이고 state0descriptor="Run/Stop"이면 → "Run" 상태 ``` **변경 후 코드**: ``` attribute TEXT - 속성명 ('desc', 'area') ... description TEXT - 장비 설명 (tag_metadata.desc) area TEXT - 소속 플랜트 (tag_metadata.area) ... - 메타데이터: desc (String), area (Enum) ... - pv 값이 EnumValueType 형식인 경우 `{코드 | DisplayName | }`에서 DisplayName으로 상태 확인 가능 - v_tag_summary 뷰를 사용하면 실시간값+메타데이터 한 번에 조회 가능 ``` **diff** (`server.py` 기준, `nl2sql_worker.py`는 동일): ```diff - attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor', ...) + attribute TEXT - 속성명 ('desc', 'area') value TEXT - 메타데이터 값 node_id TEXT - OPC UA 노드 ID loaded_at TIMESTAMPTZ - 마지막 로드 시각 뷰: v_tag_summary (실시간값 + 메타데이터 통합 뷰) ... description TEXT - 장비 설명 (tag_metadata.desc) area TEXT - 소속 플랜트 (tag_metadata.area) - state0_descriptor TEXT - 비트 0 의미 (예: "Run/Stop") - state1_descriptor TEXT - 비트 1 의미 (예: "Remote/Local") - state2_descriptor TEXT - 비트 2 의미 (예: "Trip/Normal") 새로운 태그 타입: - 아날로그: ficq-6101.pv/sp/op (Double) - 디지털 XV: xv-6124.pv/op (Int32), xv-6124.instate0~7 (Boolean) - Pump: p-6102.pv/op (Int32), p-6102.instate0~7 (Boolean) - - 메타데이터: desc (String), area (Enum), state0descriptor~7 (String) + - 메타데이터: desc (String), area (Enum) BCD 상태 조회 팁: - instate0~7은 Boolean (true/false) - - state0descriptor~7은 해당 비트의 의미 설명 - - instate0=true이고 state0descriptor="Run/Stop"이면 → "Run" 상태 + - pv 값이 EnumValueType 형식인 경우 `{코드 | DisplayName | }`에서 DisplayName으로 상태 확인 가능 - v_tag_summary 뷰를 사용하면 실시간값+메타데이터 한 번에 조회 가능 ``` **변경 이유**: - `v_tag_summary` 뷰에서 `state0~2_descriptor` 컬럼이 제거되면 LLM이 해당 컬럼을 SELECT하는 SQL을 생성하면 실패함 - DB_SCHEMA는 LLM의 시스템 프롬프트로 사용되므로 실제 DB 스키마와 반드시 일치해야 함 **검증 기준**: - [ ] `server.py` DB_SCHEMA에서 `state0_descriptor` / `state1_descriptor` / `state2_descriptor` 언급 없음 - [ ] `nl2sql_worker.py` DB_SCHEMA에서 동일하게 제거됨 - [ ] `attribute` 설명이 `'desc', 'area'`만 포함 - [ ] MCP 서버 재시작 후 NL2SQL 쿼리가 정상 동작 (state descriptor 없이) **enum-metadata-optimization.md 규칙 매핑**: - 섹션 3.1: pv 값이 EnumValueType 형식인 경우 DisplayName 파싱으로 상태 확인 --- ### STEP 7 — `app.js`: pv 값 표시 관련 코드 모두 `parseEnumPv()` 적용 **파일**: [`app.js`](src/Web/wwwroot/js/app.js:633) **변경 위치**: `pbRender()` 함수 내 liveValue 표시 부분 (633줄) **변경 전 코드** (633줄): ```javascript