Files
ExperionCrawler/plans/realtime-tag-expansion-design.md
2026-05-08 17:22:10 +09:00

21 KiB

실시간 태그 확장 설계안

1. 배경

현재 ExperionCrawler는 아날로그 태그의 .pv, .sp, .op, .qv.valuerealtime_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 통신 부담을 줄입니다.

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하여 태그별 속성을 한눈에 보기 위한 뷰:

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<TagMetadata> 추가, 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 상태 알려줘" 같은 자연어 질문에 의미 있는 답변을 제공하려면 상태 의미 정보가 필요함.

해결 방안:

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에서 계속 구독하면 불필요한 트래픽 발생.

해결 방안:

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: 자동화 (향후)

  1. 자동 속성 제안: 상위 Object 선택 시 하위 속성 자동 제안
  2. tag_registry 테이블: BCD 상태 의미 저장, 장비 타입 관리

Phase 3: 지능형 처리 (장기)

  1. 장비 타입별 처리: BCD 상태 해석, UI 컴포넌트별 렌더링
  2. desc/area 최적화: 별도 테이블 + 변경 감지 폴링
  3. Text-to-SQL 연동: 자연어 질문 → BCD 상태 해석 → 의미 있는 답변

8. NL2SQL 변경 사항

현재 NL2SQL worker(mcp-server/worker/nl2sql_worker.py)는 history_tablerealtime_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_SCHEMAtag_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 테이블 구조

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화를 위한 걸 기본으로 설계되어야 한다.