# 실시간 태그 확장 설계안 ## 1. 배경 현재 ExperionCrawler는 아날로그 태그의 `.pv`, `.sp`, `.op`, `.qv.value`만 `realtime_table`에 저장하고 있음. 디지털 장비(Pump, XV)의 상태 정보와 모든 태그의 `.desc`, `.area` 메타데이터를 추가하고자 함. ## 2. 현재 시스템 아키텍처 ``` Experion HS R530 OPC Server ↓ (1. 크롤링 - 전체 노드 스캔) CSV 파일 저장 ↓ (2. CSV 로드) node_map_master (530,080 행) ↓ (3. UI에서 태그+속성 선택 - 최대 8개) realtime_table (등록된 태그만) ↓ (4. OPC UA 구독) livevalue 실시간 업데이트 ↓ (5. 히스토리 저장) history_table ``` ### node_map_master 구조 특징 - `name`: 속성명만 (`pv`, `sp`, `desc`, `area`, `instate0` 등) - `node_id`: 전체 경로 (`ns=1;s=sinamserver:xv-6124.pv`) - 상위 Object (`xv-6124`)와 하위 속성 (`pv`)이 별도 행으로 존재 ## 3. 추가할 태그 ### 3.1 아날로그 태그 (기존 확장) | 태그 | 데이터 타입 | 설명 | |------|------------|------| | `ficq-6101.area` | Enum(i=7594) | 소속 플랜트/Asset | > 참고: ficq-6101은 `.desc` 속성이 없음 (DB 확인 결과) ### 3.2 디지털 태그 (신규 추가) (0~7까지 모두 추가) | 태그 | 데이터 타입 | 설명 | |------|------------|------| | `xv-6124.pv` | Int32 (BCD 조합값) | 프로세스 값 | | `xv-6124.op` | Int32 | 출력값 | | `xv-6124.desc` | String | 장비 설명 | | `xv-6124.area` | Enum(i=7594) | 소속 플랜트/Asset | | `xv-6124.instate0` | Boolean | 상태 비트 0 (현재 값) | | `xv-6124.instate1` | Boolean | 상태 비트 1 (현재 값) | | `xv-6124.instate2` | Boolean | 상태 비트 2 (현재 값) | | `xv-6124.state0descriptor` | String | 비트 0 의미 (예: "Run/Stop") | | `xv-6124.state1descriptor` | String | 비트 1 의미 (예: "Remote/Local") | | `xv-6124.state2descriptor` | String | 비트 2 의미 (예: "Trip/Normal") | > **핵심 발견:** Experion HS에 `state0descriptor`~`state7descriptor`가 이미 존재합니다. > 이는 사용자가 정의한 BCD 상태 의미를 String으로 저장한 태그로, > OPC UA에서 구독하면 `realtime_table`에 저장 가능하므로 **별도 테이블 불필요**. ### 3.3 BCD 상태값 해석 `pv`는 HC900 컨트롤러에서 Digital Input을 BCD로 조합하여 32bit Float로 전송. **직접 `pv` 값을 파싱하지 않고 `instate0~7` Boolean 태그를 사용.** | 사용 비트 | 현재 프로젝트 예시 (Pump) | 현재 프로젝트 예시 (Valve) | |-----------|---------------------------|----------------------------| | instate0 | Run(1)/Stop(0) | Open(1)/Close(0) | | instate0+1 | Run/Stop + R/L | Open/Close + R/L | | instate0+1+2 | Run/Stop + R/L + Trip/Normal | Open/Close + R/L + Fault/Normal | > **중요**: 위 값은 현재 프로젝트에서만 알려진 사용자 정의 값입니다. > 다른 프로젝트에서는 BCD 상태 의미가 다를 수 있으므로 정형화 불가능합니다. > 각 장비의 실제 비트 사용 수와 의미는Experion HS에서 사용자 정의됩니다. ## 4. 설계 결정 ### 4.1 DB 스키마 변경: **tag_metadata 테이블 추가** 메타데이터(desc, area, state0descriptor~7)는 실시간으로 자주 변경되지 않으므로, 별도 테이블에 저장하고 OPC UA 통신 부담을 줄입니다. ```sql CREATE TABLE tag_metadata ( id SERIAL PRIMARY KEY, base_tag TEXT NOT NULL, -- ficq-6101, xv-6124 attribute TEXT NOT NULL, -- desc, area, state0descriptor, ... value TEXT, -- 실제 값 node_id TEXT, -- OPC UA node_id (재로드용) loaded_at TIMESTAMP DEFAULT NOW(), -- 마지막 로드 시간 UNIQUE(base_tag, attribute) ); CREATE INDEX idx_tag_metadata_base ON tag_metadata(base_tag); ``` **데이터 분리 전략:** | 테이블 | 저장 내용 | OPC UA 구독 | |--------|----------|-------------| | `realtime_table` | pv, sp, op, instate0~7 등 **실시간 데이터** | ✅ 지속 구독 | | `tag_metadata` | desc, area, state0descriptor~7 등 **메타데이터** | ❌ 한 번 로드 | **장점:** - OPC UA 통신 부담 대폭 감소 (메타데이터는 한 번만 읽음) - 수천 개 태그에 대해 desc/area를 실시간 구독하지 않음 - UI에서 "재로드" 버튼으로 필요시 갱신 가능 ### 4.2 메타데이터 로드/재로드 시나리오 **초기 로드:** 1. UI에서 태그 선택 후 등록 시, 실시간 태그(pv, sp, op, instate0~7)는 `realtime_table`에 등록 2. 동시에 메타데이터(desc, area, state0descriptor~7)는 OPC UA에서 **한 번 읽어서** `tag_metadata` 테이블 저장 3. 읽은 메타데이터 정보로 `node_map_master`도 동시 갱신 **재로드 (UI 버튼):** 1. UI에서 "메타데이터 재로드" 버튼 클릭 2. Backend가 OPC UA 서버에 직접 연결 3. 선택된 태그(또는 전체 등록 태그)의 메타데이터 재조회 4. `tag_metadata` 테이블 UPDATE + `node_map_master` 갱신 ``` UI 재로드 버튼 ↓ Backend API: POST /api/tags/metadata/reload ↓ OPC UA Read (desc, area, state0descriptor~7) ↓ tag_metadata UPDATE node_map_master UPDATE (UPSERT) ↓ Response: 갱신된 태그 수 ``` ### 4.3 SQL 뷰 추가 `realtime_table` + `tag_metadata`를 JOIN하여 태그별 속성을 한눈에 보기 위한 뷰: ```sql CREATE OR REPLACE VIEW v_tag_summary AS SELECT rt_base.base_tag, pv_rt.livevalue AS pv, sp_rt.livevalue AS sp, op_rt.livevalue AS op, instate0_rt.livevalue AS instate0, instate1_rt.livevalue AS instate1, instate2_rt.livevalue AS instate2, 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'; ``` ### 4.4 UI 변경 | 항목 | 현재 | 변경 | |------|------|------| | 최대 선택 태그 수 | 8개 | 10개 | | 태그 검색 | name 기준 | name + node_id 기준 | | 메타데이터 재로드 | 없음 | "메타데이터 재로드" 버튼 추가 | | 속성 미리보기 | 없음 | 선택 시 하위 속성 표시 (선택사항) | ## 5. 변경 사항 요약 ### 5.1 Database - **신규 테이블**: `tag_metadata` (base_tag, attribute, value, node_id, loaded_at) - **신규 뷰**: `v_tag_summary` (realtime_table + tag_metadata JOIN) ### 5.2 Backend (`src/Infrastructure/OpcUa/`) - **신규 서비스**: `MetadataLoaderService` - `LoadMetadataAsync(base_tags)` : OPC UA에서 desc, area, state0descriptor~7 읽어서 `tag_metadata` 저장 - `ReloadMetadataAsync(base_tags)` : 재로드 시 OPC UA 재조회 + `tag_metadata` UPDATE + `node_map_master` UPSERT - **신규 API**: `POST /api/tags/metadata/reload` ### 5.3 Frontend (`src/Web/wwwroot/js/app.js`) - 태그 선택 최대 수 8 → 10개로 변경 - "메타데이터 재로드" 버튼 추가 - (선택사항) 상위 Object 선택 시 하위 속성 자동 표시 ### 5.4 Entity/DbContext - **신규 Entity**: `TagMetadata` class - **DbContext**: `DbSet` 추가, `OnModelCreating` 설정 추가 ### 5.5 사용자 작업 - UI에서 새로운 태그(Pump, XV)와 속성(pv, op, instate0~2) 선택하여 등록 - 메타데이터(desc, area, state descriptor)는 자동 로드 - 필요시 "메타데이터 재로드" 버튼으로 갱신 ## 6. 대규모 리팩토링이 필요한 시나리오 (향후 고려사항) 현재 설계안은 기존 `realtime_table` 구조를 유지하므로 저위험입니다. 하지만 아래 기능들을 추가하려면 **DB 스키마 변경 + 코드 리팩토링**이 필요합니다. ### 6.1 자동 속성 제안 기능 **문제:** 현재 UI에서 각 태그의 속성(pv, sp, op, desc, area, instate0~2)을 수동으로 선택해야 함. Pump/XV 같은 디지털 장비는 7개 이상의 속성을 선택해야 하므로 번거로움. **해결 방안:** - 상위 Object (예: `xv-6124`) 선택 시, node_map_master에서 하위 속성을 자동으로 조회하여 제안 - 장비 타입에 따라 기본 속성 조합 제안: - 아날로그 (ficq-6101): `.pv`, `.sp`, `.op`, `.area` - 디지털 XV (xv-6124): `.pv`, `.op`, `.desc`, `.area`, `.instate0`, `.instate1`, `.instate2` - Pump (p-6102): `.pv`, `.op`, `.desc`, `.area`, `.instate0`, `.instate1`, `.instate2` **변경 범위:** - Frontend: 상위 Object 선택 시 하위 속성 자동 로딩 UI - Backend: `/api/nodemap/children?parent=xv-6124` API 추가 - node_map_master에서 `node_id LIKE '%xv-6124.%'`로 하위 속성 조회 **리팩토링 위험:** 낮음 (기존 API 추가만 필요) ### 6.2 tag_registry 테이블 도입 **문제:** BCD 상태 의미(instate0=Run/Stop, instate1=R/L 등)는 사용자 정의이므로 시스템에서 자동으로 해석할 수 없음. Text-to-SQL에서 "pump-6102 상태 알려줘" 같은 자연어 질문에 의미 있는 답변을 제공하려면 상태 의미 정보가 필요함. **해결 방안:** ```sql CREATE TABLE tag_registry ( id SERIAL PRIMARY KEY, base_tag TEXT NOT NULL UNIQUE, -- xv-6124, p-6102 equipment_type TEXT, -- pump, xv, ai, ao, fi, ti bit_count INT DEFAULT 1, -- 사용하는 instate 비트 수 (1~3) state0_meaning TEXT, -- Run/Stop, Open/Close state1_meaning TEXT, -- Remote/Local state2_meaning TEXT, -- Trip/Normal, Fault/Normal area TEXT, -- 소속 플랜트/Asset description TEXT, -- 장비 설명 created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); ``` **활용 시나리오:** - BCD 상태 의미 저장: 사용자가 UI에서 상태 의미 정의 → DB 저장 - 자동 속성 제안: `equipment_type`에 따라 기본 속성 조합 제안 - Text-to-SQL: "pump-6102 현재 상태" → instate0~2를 Boolean으로 읽어서 상태 의미와 매핑하여 "Run, Remote, Normal" 표시 - P&ID 매핑: P&ID에서 추출한 장비와 Experion 태그를 `tag_registry`를 통해 연결 **리팩토링 위험:** 중간 (새 테이블 + Entity + Service 추가) ### 6.3 장비 타입별 다른 처리 로직 **문제:** 현재 `realtime_table`은 모든 태그를 동일하게 처리. 하지만 장비 타입에 따라 다른 처리가 필요할 수 있음: - 아날로그: pv/sp/op를 숫자로 표시, 단위 표시 - 디지털 XV: instate0~2를 Boolean으로 읽어서 "Open/Close", "R/L"로 표시 - Pump: instate0~2를 Boolean으로 읽어서 "Run/Stop", "R/L", "Trip/Normal"로 표시 - 알람: alarmflags, alarmvalue를 파싱해서 알람 상태 표시 **해결 방안:** - `tag_registry.equipment_type`에 따라 UI에서 다른 컴포넌트 렌더링 - Backend에서 장비 타입별 상태 해석 서비스 추가 - BCD 상태값을 Boolean 배열로 파싱하는 유틸리티 함수 **변경 범위:** - Frontend: 장비 타입별 상태 표시 컴포넌트 - Backend: `EquipmentStateService` 추가 (BCD → Boolean 배열 → 상태 문자열) - `tag_registry` 테이블과 연동 **리팩토링 위험:** 높음 (Frontend 컴포넌트 + Backend 서비스 + DB 연동) ### 6.4 desc/area 별도 테이블 **문제:** desc/area는 자주 변경되지 않지만, 현재는 `realtime_table`에 실시간 데이터로 저장. 수천 개의 태그에 대해 desc/area를 OPC UA에서 계속 구독하면 불필요한 트래픽 발생. **해결 방안:** ```sql CREATE TABLE tag_metadata ( tagname TEXT PRIMARY KEY, -- ficq-6101.area, xv-6124.desc value TEXT NOT NULL, updated_at TIMESTAMP DEFAULT NOW() ); ``` - 초기 로드 시 OPC UA에서 desc/area 한 번 읽어서 저장 - 변경 감지: 주기적 폴링 또는 이벤트 기반 업데이트 - UI 조회 시 `tag_metadata`에서 읽되, 변경 의심 시 OPC UA에서 재조회 **리팩토링 위험:** 중간 (새 테이블 + 폴링 서비스 추가) ## 7. 권장 진행 순서 ### Phase 1: 기본 확장 (현재 설계안) 1. **UI 변경**: 태그 선택 최대 수 8 → 10개 2. **테스트**: xv-6124의 pv, op, desc, area, instate0~2 등록 및 OPC UA 구독 확인 3. **SQL 뷰**: `v_tag_summary` 생성 (선택사항) ### Phase 2: 자동화 (향후) 4. **자동 속성 제안**: 상위 Object 선택 시 하위 속성 자동 제안 5. **tag_registry 테이블**: BCD 상태 의미 저장, 장비 타입 관리 ### Phase 3: 지능형 처리 (장기) 6. **장비 타입별 처리**: BCD 상태 해석, UI 컴포넌트별 렌더링 7. **desc/area 최적화**: 별도 테이블 + 변경 감지 폴링 8. **Text-to-SQL 연동**: 자연어 질문 → BCD 상태 해석 → 의미 있는 답변 ## 8. NL2SQL 변경 사항 현재 NL2SQL worker(`mcp-server/worker/nl2sql_worker.py`)는 `history_table`과 `realtime_table`만 인식합니다. 새로운 테이블과 뷰, 태그 타입을 인식하도록 `DB_SCHEMA`를 업데이트해야 합니다. ### 8.1 DB_SCHEMA 업데이트 필요 항목 **추가할 테이블 정의:** ``` 테이블: tag_metadata (태그 메타데이터 - 변경 드묾) base_tag TEXT - 기본 태그명 (예: 'ficq-6101', 'xv-6124') attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor', ...) value TEXT - 메타데이터 값 node_id TEXT - OPC UA 노드 ID loaded_at TIMESTAMPTZ - 마지막 로드 시각 뷰: v_tag_summary (실시간 + 메타데이터 통합) base_tag TEXT - 기본 태그명 pv TEXT - 현재 프로세스 값 sp TEXT - 설정값 op TEXT - 출력값 instate0 TEXT - 상태 비트 0 (true/false) instate1 TEXT - 상태 비트 1 (true/false) instate2 TEXT - 상태 비트 2 (true/false) 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") ``` ### 8.2 NL2SQL 사용 예시 (새로운 시나리오) | 자연어 질문 | 생성 SQL | |------------|----------| | "xv-6124의 현재 상태와 설명을 알려줘" | `SELECT instate0, instate1, state0_descriptor, state1_descriptor, description FROM v_tag_summary WHERE base_tag = 'xv-6124'` | | "Unit-A에 있는 모든 pump의 상태를 보여줘" | `SELECT base_tag, instate0, state0_descriptor, description FROM v_tag_summary WHERE area = 'Unit-A' AND base_tag LIKE 'p-%'` | | "ficq-6101의 현재 값과 설명은?" | `SELECT pv, description FROM v_tag_summary WHERE base_tag = 'ficq-6101'` | | "모든 XV 중 instate0이 true인 것" | `SELECT base_tag, instate0, state0_descriptor FROM v_tag_summary WHERE instate0 = 'True' AND base_tag LIKE 'xv-%'` | | "p-6102의 3개 상태 비트와 의미를 보여줘" | `SELECT instate0, instate1, instate2, state0_descriptor, state1_descriptor, state2_descriptor FROM v_tag_summary WHERE base_tag = 'p-6102'` | ### 8.3 변경 파일 목록 | 파일 | 변경 내용 | |------|----------| | `mcp-server/worker/nl2sql_worker.py` | `DB_SCHEMA`에 `tag_metadata`, `v_tag_summary` 추가 | | `mcp-server/server.py` | `_DB_SCHEMA`에 동일하게 추가 (동기용) | ### 8.4 DB_SCHEMA 추가 내용 예시 ``` 테이블: tag_metadata (태그 메타데이터 - 변경 드묾, OPC UA 폴링으로 갱신) base_tag TEXT - 기본 태그명 (예: 'ficq-6101', 'xv-6124') attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor'~'state7descriptor') value TEXT - 메타데이터 값 loaded_at TIMESTAMPTZ - 마지막 로드 시각 뷰: v_tag_summary (실시간값 + 메타데이터 통합 뷰) base_tag, pv, sp, op, instate0~2, description, area, state0~2_descriptor 새로운 태그 타입: - 아날로그: ficq-6101.pv/sp/op (Double), ficq-6101.area - 디지털 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) BCD 상태 조회 팁: - instate0~7은 Boolean (true/false) - state0descriptor~7은 해당 비트의 의미 설명 (예: "Run/Stop") - instate0=true이고 state0descriptor="Run/Stop"이면 → "Run" 상태 - v_tag_summary 뷰를 사용하면 실시간값+메타데이터 한 번에 조회 가능 ``` ## 9. Event & Alarm 테이블 (지능형 가동 상태 보고용) ### 9.1 필요성 "어제 18시부터 6차 플랜트의 가동상태 알려줘" 같은 지능형 보고를 하려면: - 펌프/밸브 상태 변화 이력 - 알람 발생/해제 시점 - 비상정지(ESHUTDOWN) 발생/해제 시점 - 유량계 적산값(qv.value)으로 생산량 계산 이 정보들은 **실시간 값이 아닌 이벤트 이력**이므로 별도 테이블 필요. ### 9.2 테이블 구조 ```sql CREATE TABLE event_alarm_log ( id BIGSERIAL PRIMARY KEY, base_tag TEXT NOT NULL, -- xv-6124, p-6102 event_type TEXT NOT NULL, -- state_change, alarm, shutdown bit_index INT, -- instate0~7 중 어떤 비트 (state_change용) old_value TEXT, -- 이전 값 new_value TEXT, -- 새 값 state_descriptor TEXT, -- state0descriptor 등 의미 alarm_priority TEXT, -- Urgent/High/Low/Journal alarm_message TEXT, -- 알람 메시지 occurred_at TIMESTAMPTZ NOT NULL, -- 발생 시각 acknowledged_at TIMESTAMPTZ, -- 확인 시각 source TEXT DEFAULT 'opcua' -- opcua, manual ); CREATE INDEX idx_eal_base_tag ON event_alarm_log(base_tag); CREATE INDEX idx_eal_occurred ON event_alarm_log(occurred_at); CREATE INDEX idx_eal_type ON event_alarm_log(event_type); ``` ### 9.3 수집 방법 **방법 1: OPC UA Event Subscription (권장)** - Experion HS의 OPC UA Event Mechanism 구독 - 알람 발생 시 실시간으로 이벤트 수신 → DB 저장 **방법 2: 폴링 기반 상태 변화 감지** - instate0~7 값을 주기적 폴링 - 이전 값과 비교하여 변화 감지 → event_alarm_log INSERT - alarmflags, alarmvalue 변화도 함께 감지 ### 9.4 활용 시나리오 | 질문 | 필요한 데이터 | |------|--------------| | "p6 플랜트 가동상태 알려줘" | pump instate0 변화 이력 + shutdown 이벤트 | | "00시에 왜 정지됐어?" | event_alarm_log WHERE event_type='shutdown' | | "오늘 생산량?" | qv.value 적산 (history_table) | | "xv-6124 알람 이력" | event_alarm_log WHERE base_tag='xv-6124' | ### 9.5 NL2SQL DB_SCHEMA 추가 내용 ``` 테이블: event_alarm_log (이벤트/알람 이력) base_tag TEXT - 기본 태그명 event_type TEXT - state_change, alarm, shutdown bit_index INT - instate 비트 인덱스 (0~7) old_value TEXT - 이전 값 new_value TEXT - 새 값 state_descriptor TEXT - 상태 의미 (예: "Trip, Fault") alarm_priority TEXT - Urgent/High/Low/Journal occurred_at TIMESTAMPTZ - 발생 시각 area - 그룹명 (플랜트 'p1~10' 에 그룹 알람 정보 찾기 쉬움) 예시: "오늘 p6 플랜트 알람 목록" SELECT base_tag, event_type, new_value, state_descriptor, alarm_priority, occurred_at FROM event_alarm_log WHERE base_tag IN (SELECT base_tag FROM tag_metadata WHERE attribute='area' AND value='p6') AND occurred_at >= date_trunc('day', NOW()) ORDER BY occurred_at ``` ##### future Plan 항목 이 디자인 플랜의 후순위 - 알람 관련 작업 :Experion HS 알람 구성 파악, 알람 테이블 긁어올 가능성 있는지 체크 - WebUI 도입관련 : 진정한 AX도입의 FINAL WORK가 될 것이며, 위의 모든 기능은 최종 AX화를 위한 걸 기본으로 설계되어야 한다.