Files
ExperionCrawler/plans/운전판정-고도화-플랜.md
windpacer 2e844abf11 feat: 운전판정 고도화 — realtime stall 수정 + 교차검증 + 단위/레인지
- ExperionRealtimeService를 단일 SuperviseAsync supervisor로 재설계:
  비블로킹 부팅, PublishingStopped/KeepAliveStopped 워치독으로 silent
  stall 감지, 30초 주기 무한 재연결, flush 루프 단일화
- RealtimeServiceStatus에 LastDataAgeSeconds/Stalled 추가, History는
  Stalled 시 스냅샷 skip
- v_plant_running_state에 진공펌프(vp-) 포함 + 교차검증 4객체
  (pump_corroboration_manual, v_pump_signal_map,
  v_plant_running_state_corroborated, v_plant_running_state_agg)
  + v_instrument_range 뷰 (boot DDL)
- MetadataLoaderService에 euhi/eulo/units 메타속성 추가
- generate_status_report에 agg 조회 연동 + sample/focus 버그 수정
- plant_context.md에 펌프 prefix(p-/vp-) + 교차검증 뷰 사용법

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 16:47:20 +09:00

28 KiB
Raw Blame History

운전판정 고도화 플랜 — 유량계·진공압 교차검증(Corroboration) 도입

문서 상태: 초안 + 감리 진단 반영 (2026-05-24) — §0이 §1~§8 초안보다 우선 작성일: 2026-05-24 관련 시스템: ExperionCrawler v_plant_running_state, MCP server, pid_equipment


0. 감리 진단 결과 (2026-05-24, 실 DB·pid_equipment·코드 검증)

초안(§1~§8)을 실제 데이터로 검증한 결과다. 본문과 충돌 시 이 절의 정정/보완이 우선한다.

0.1 검증된 사실 (초안 대비 정정)

항목 초안 주장 실측 결과 판정
유량 데이터 성격 "dummy 20.27 고정" (risk #1) ficq-6113이 하루 동안 38~55로 실측 변동(history_table). 단 수집기 stall 시 전 태그 frozen(2026-05-24 09:58 KST 실제 발생, 별도 수정 완료) 라이브 / 신선도 게이트 필수
펌프번호=유량번호 "일관됨" (obs #1) P-6102→ficq-6101, P-6114→ficq-6113+6114(1:N) — 번호 불일치·1:N 오류
pica-6111.pv "20.8" (§2.1) 쿼리마다 변동(5.87/20.8…) — 고정값 아님 값 오류
VP↔진공압 매핑 "pid_equipment 기반" (§4.1.1 Step3) VP-6117: from=D-6113, to=SC-6128 → 압력계와 토폴로지 연결 없음. Step3는 사실상 하드코딩 manual ⚠️ 명칭 정정
진공압 태그 의미 "PICA.pv<50=진공유지" pi-6111="VACUUM PRESSURE", pica-6111은 PT-6111→PCV-6111 압력제어 루프(다른 점일 수). 센서 sense(절대압/진공계)·단위 미확정 ⚠️ 임계 근거 부족
실시간 유량 태그 FT 기준 매핑 (§4.1.1) realtime에 ft-6113 없음(count=0). 값은 FICQ 컨트롤러(ficq-6113.pv)에 존재 매핑 대상 오류

0.2 치명적 문제 — Phase 1 SQL 그대로면 동작하지 않음

  1. [높음] 하이픈 제거 버그: LOWER(REPLACE(ft.tag_no,'-',''))'ft6113'. realtime base_tag는 하이픈 유지(ficq-6113) → 매칭 0건 → 전부 INDETERMINATE.
  2. [높음] FT vs FICQ 대상 오류: 유량값은 ft-*가 아니라 ficq-*.pv. FT 태그로는 realtime 조인 불가.
  3. [높음] heuristic 오매핑: 'ficq-'||SUBSTRING(base_tag FROM 3) → P-6102는 ficq-6102(없음, 실제 6101), vp-6117은 ficq--6117(깨짐). 번호 가정 자체가 틀림.
  4. [중간] 1:N 손실: flow_tag 단일 컬럼 → P-6114(6113+6114) 한쪽만 저장.
  5. [중간] forward JOIN 실패: P-6102.to_tag=필터, P-6116.to_tag=C-6111/FCV → to_tag에 FT 없음. 역방향(FT.from_tag=pump)만 일부 성립.

0.3 설계 공백

  1. [최우선] 데이터 신선도 게이트 부재 — realtime 값이 stale/frozen이어도 임계 비교 → frozen 20.2667을 "유량 정상=CONFIRMED"로 오판. (이번 수집기 stall이 정확히 이 시나리오.) 판정 전 NOW()-timestamp 확인, stale면 판정 보류.
  2. [중간] 임계 0.5 절대값 — 계기별 Full Scale 무시(작은 레인지 과대·큰 레인지 과소).
  3. [중간] 집계가 1대 의심에 전체 오염 — suspicious 1대로 area가 RUNNING_WITH_SUSPICIOUS. 정상 standby/kickback(sp=0) 펌프 상시 의심 위험. corroborated_pct 분모에 INDETERMINATE 혼입.
  4. [중간] active_alarms 주입 위험 — 미검증 휴리스틱(+데이터 품질 이슈)을 운전원 알람화 → 알람 피로. 검증 전 advisory만.
  5. [낮음] VP 신호 선택pica-6111(압력제어)보다 pi-6111b("VACUUM PRESSURE", C-6111 직결)가 진공 판정에 적합해 보임. sense/단위 확인 후 결정.

0.4 권장 보완

(a) 매핑 — 토폴로지 규칙(번호 heuristic 폐기)

  • 1차: FT.from_tag가 펌프를 참조하는 행 수집(역방향, 1:N 자연 지원). 값 태그 = 같은 번호 FICQ 컨트롤러 ficq-<FT번호>.pv. 하이픈 유지(lower(tag_no)만, REPLACE 금지).
  • 2차: 펌프-FT 사이 중간설비(P-6102→F-6101A/B→FT-6101)는 토폴로지 2-hop 또는 manual 항목.
  • map 테이블은 1:N 허용(펌프당 flow_tag 복수 행) + mapping_source ∈ {topology, manual}.

(b) 신선도 게이트 (신규·최우선)

-- 값 신뢰 조건: 수집 후 N초 이내(예: 120s). realtime_table.timestamp 기준.
(NOW() - rt.timestamp) < interval '120 seconds'

신선하지 않으면 STALE(운전 여부 판정 보류). 수집기 측은 supervisor가 stall을 30초 내 자동 복구하고 RealtimeServiceStatus.Stalled로도 노출(2026-05-24 적용).

(c) 판정 상태에 STALE 추가

판정 조건
CONFIRMED_RUNNING RUN + 유량 신선 + PV>임계
SUSPICIOUS_RUNNING RUN + 유량 신선 + PV≤임계
STALE (신규) RUN + 유량 stale/frozen (수집 지연·stall)
INDETERMINATE_RUNNING RUN + 유량 매핑/데이터 없음
STOPPED / TRIPPED enum 기준

(d) 임계 — Phase 1은 절대 0.5 단독 대신 "신선 AND PV<계기군 기본임계"로 무유량/frozen만 포착. Full Scale 5%는 Phase 2 instrument_range로.

(e) 집계·알람 — overall_status는 CONFIRMED 기준으로 RUNNING 유지하고 suspicious/stale는 부가 카운트로 노출(전체 상태 오염 금지). active_alarms 주입은 운전원 검증(§6.4) 통과 후로 보류.

0.5 갱신된 의사결정 체크리스트

  • 데이터 성격: 라이브(dummy 아님), 단 stale 가능 → 신선도 게이트 필수
  • 매핑: FT.from_tag 역방향 + FICQ 값태그 + manual 보강 (번호 heuristic 폐기) ← 재결정
  • 임계: 계기군 기본값 + 신선도 동반 (절대 0.5 단독 폐기) ← 재결정
  • VP 신호: pi-6111b vs pica-6111 + sense/단위 확인 ← 미결
  • STALE 상태 도입 ← 신규 결정 필요
  • active_alarms 주입: 검증 전 보류 ← 재결정

0.6 구현 현황 (2026-05-24)

  • DB 뷰 계층 구현·검증 (ExperionDbContext boot DDL):
    • pump_corroboration_manual (수동 매핑 테이블, P6 예외 시드: p-6102→ficq-6101, vp-6117→pica-6111, vp-6217→pica-6211)
    • v_pump_signal_map (토폴로지 FT.from_tag=펌프→FICQ 1:N + 수동 UNION)
    • v_plant_running_state_corroborated (신선도 게이트 120s + STALE + 유량 > 0.5 kg/hr · 진공 < 300 torr)
    • v_plant_running_state_agg (overall은 CONFIRMED 기준 RUNNING, suspicious/stale는 부가 카운트)
    • 빌드 0/0. 라이브 검증: 현재 frozen 데이터가 전부 STALE로 분류됨 확인(게이트 정상 작동).
  • plant_context.md 교차검증 사용법 추가.
  • 단위/레인지 메타데이터화 (별도 테이블 X — 복잡도 최소화 결정): MetadataLoaderService.MetaAttributeseuhi/eulo/units 추가 → tag_metadata(EAV) 재사용. node_map_master에 점 레벨 euhi(Double FS-Hi)·eulo·units(String) 노드 존재 확인. PointBuilder 작성·수동 메타갱신 트리거에 자동 편승(스코프=구독 아날로그 ⓐ). 타입 접근은 v_instrument_range 뷰(피벗+캐스트). 값은 단위 torr / 유량 kg/hr.
  • 유량 임계 FS 5%: corroborated가 flow > COALESCE(eu_hi*0.05, 0.5 kg/hr) — 레인지 적재되면 자동 FS 5% 승급, 없으면 절대 fallback. 진공은 300 torr 절대(실 레인지 확인 후 보정).
  • OPC 복구 후 실값 적재: 현재 수집 stall이라 euhi/eulo/units 값 미적재 → 복구 후 메타갱신 1회 시 채워지며 FS 5% 자동 적용.
  • MCP 연동 보류: generate_status_reportv_plant_running_state_agg 노출은 후속. active_alarms 주입은 운전원 검증 후(§0.4e).

1. 배경 및 문제 정의

1.1 현재 상황

현재 공장 운전 판정(v_plant_running_state 뷰)은 펌프의 상태 워드(enum 값)만으로 이루어짐:

-- 현재 로직 (의사코드)
CASE
  WHEN pv ~ '[LR]-RUN'  THEN 'RUNNING'
  WHEN pv ~ '[LR]-TRIP' THEN 'TRIPPED'
  ELSE 'STOPPED'
END AS status

예: P-6102의 PV = {5 | R-RUN | }RUNNING

1.2 문제점 — 허위 운전 미검출

펌프 상태 워드가 R-RUN이어도 실질적 운전이 아닌 경우가 있음:

상황 펌프 상태 유량계 실질 운전?
정상 운전 R-RUN > 0
밸브 닫힘/Deadhead R-RUN ≈ 0 (기계 손상 위험)
커플링 파손 R-RUN ≈ 0 (무부하 운전)
센서 오류 R-RUN ≈ 0 (신호 끊김)
Kickback 순환 R-RUN 0 (메인) ⚠️ (의도된 운전, main line은 닫힘)

현재는 이 4가지 케이스를 모두 동일하게 RUNNING으로 판정 → 허위 정보 제공

1.3 진공 펌프의 특수성

진공 펌프(VP)는 유량계가 없고 진공압(PI/PICA) 으로 운전 상태를 검증:

상황 VP 상태 진공압 실질 운전?
정상 진공 유지 R-RUN 목표압 도달
펌프 고장/RUN 신호 오류 R-RUN 대기압 (≈0)
계통 누설 R-RUN 대기압 (≈0)

2. 조사 결과 — P6 데이터 기반 분석

2.1 Pump↔Flow Meter 매핑 (pid_equipment 기반)

P6-1 (C-6111 증류탑):

Pump 상태 P&ID 연결 Experion 유량계 SETPOINT
P-6101 L-STOP (미매핑, 번호 일치) ficq-6101.pv=20.3 sp=36.0
P-6102 R-RUN →F-6101A/B→FT-6101→FCV-6101 ficq-6101.pv=20.3 sp=36.0
P-6114 R-RUN →FT-6113( reflux) + FT-6114(light ends) ficq-6113.pv=20.3, ficq-6114.pv=20.3 sp=36.4 / 0
P-6116 R-RUN →FT-6116→FCV-6116 ficq-6116.pv=20.3 sp=0
P-6118 R-RUN →FT-6118→FCV-6118 ficq-6118.pv=20.3 sp=0
VP-6117 R-RUN C-6111 진공 유지 pi-6111.pv=0, pica-6111.pv=20.8
P-6120 OFF (미매핑) fiq-6120.pv=0
P-6123 L-STOP (미매핑)
P-6128a/b L-STOP (미매핑)

P6-2 (C-6211 증류탑):

Pump 상태 Experion 유량계 비고
P-6201 L-STOP ficq-6201.pv=20.8 P6-1/P6-2 공용
P-6202~6223 전부 STOP ficq-62XX.pv=20.3
VP-6217 L-STOP pi-6211.pv=0, pica-6211.pv=20.8

2.2 key observations

  1. ⚠️ (정정 §0.1) 펌프 번호 ≠ 유량계 번호: P-6102→ficq-6101, P-6114→ficq-6113+6114(1:N). 유량 번호는 stream/line 번호 → pid_equipment 토폴로지(FT.from_tag=pump)로 매핑해야 함
  2. pid_equipment.from_tag/to_tag 토폴로지로 1:N 매핑 추적 가능 (예: P-6114→FT-6113 + FT-6114)
  3. Setpoint(SP) 데이터 존재: ficq-XXXX.sp 사용 가능 — SP=0은 밸브 닫힘(킥백) 신호
  4. VP는 유량계 없음: 대신 pica-6111/sp, pi-6111로 진공압 교차검증 필요
  5. FCV-XXXX.op(밸브 위치) 데이터 없음: 현재 realtime_table에 미등록

3. 설계 결정

3.1 매핑 전략: pid_equipment 기반 + 번호 heuristic fallback

⚠️ §0.4(a) 우선 — 번호 heuristic은 오매핑(P-6102→6101 어긋남, vp- 깨짐)이라 폐기. FT.from_tag 역방향(1:N) + FICQ 값태그 + manual 보강으로 대체.

[1차] pid_equipment.from_tag/to_tag 정방향/역방향 조회
  └─ P-6114의 to_tag = FT-6113, FT-6114 → ficq-6113, ficq-6114 매핑
  
[2차] 번호 heuristic fallback (pid_equipment 커버 안 되는 경우)
  └─ P-6101 (pid_equipment 미존재) → ficq-6101 (번호 일치)

pump_corroboration_map 테이블 (신규):

CREATE TABLE pump_corroboration_map (
    pump_base_tag   TEXT PRIMARY KEY,
    flow_tag        TEXT,       -- ficq-XXXX 또는 fiq-XXXX
    flow_sp_tag     TEXT,       -- ficq-XXXX.sp (threshold 계산용)
    vacuum_tag      TEXT,       -- VP용: pica-XXXX.pv 또는 pi-XXXX.pv
    vacuum_sp_tag   TEXT,       -- VP용: pica-XXXX.sp
    mapping_source  TEXT DEFAULT 'auto',  -- 'pid_equipment' | 'heuristic' | 'manual'
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

MetadataLoaderService가 건드리지 않음 — sub_area와 동일한 원칙 (자동 덮어쓰기 금지)

3.2 Threshold 기준

구분 Phase 1 기준 Phase 2 기준
유량계 (FICQ/FIQ) PV > 0.5 (절대값) PV > 5% of Full Scale (instrument_range 테이블 도입 시)
진공압 (PICA) PV < 50 (mmHg 절대압 가정) PV < 5% of Full Scale
진공압 (PI) PV ≈ 0 (≈대기압이면 의심) 동일

Phase 1에서 SP(Setpoint)를 threshold 기준으로 사용하지 않는 이유:

운전원은 잦은 수동 밸브 조작으로 SP를 변경하지 않고 Control Valve만 조작함. 예: ficq-6101.sp=36으로 설정되어 있지만, PV가 1.2로 수동 조작 중 → SP*5%=1.8 > PV=1.2 → false SUSPICIOUS.

절대값 0.5를 기준으로 하면 PV=1.2는 정상 판정 → false positive 방지.

3.3 3단계 판정 로직

⚠️ §0.4(c): STALE 상태 추가로 4단계+STALE. 신선도 미확인 시 frozen 데이터 오판(CONFIRMED) 차단.

킥백 라인 상황(펌프 RUN + 메인 밸브 닫힘 + kickback만 순환) 고려:

판정 조건 의미
CONFIRMED_RUNNING pump RUN + flow PV > threshold 유량 있음, 실질 운전 중
SUSPICIOUS_RUNNING pump RUN + flow PV ≤ threshold RUN인데 유량 없음 (deadhead / 센서오류 / 커플링파손)
INDETERMINATE_RUNNING pump RUN + flow 데이터 없음 kickback 가능성, 추가 정보 필요
STOPPED pump STOP/TRIP/OFF 정지 또는 트립

킥백 상황(P-6114 RUN + ficq-6114.sp=0 → valve closed)은 유량 PV=0이어도 PV=0이라 SUSPICIOUS 대상이지만, 이건 의도된 운전이므로 사용자가 해석 시 고려. Phase 2에서 FCV-XXXX.op 데이터 추가 시 자동 구분 가능.

3.4 진공 펌프(VP) 교차검증

VP는 유량계가 없으므로 진공압(PICA/PI) 으로 검증:

  • VP R-RUN + PICA.pv < threshold (진공 유지 중) → CONFIRMED
  • VP R-RUN + PI.pv ≈ 0 (대기압, 진공 안 잡힘) → SUSPICIOUS
  • VP STOP/TRIP → STOPPED

4. 구현 계획 — 3 Phase

Phase 1: SQL View 확장 (즉시)

4.1.1 pump_corroboration_map 생성 및 시딩

⚠️ 아래 시딩 SQL은 현 상태로 동작하지 않음 (§0.2: ① 하이픈 제거 REPLACE(...,'-','') → 매칭 0건, ② FT/FICQ 대상 혼동, ③ 번호 heuristic 오류, ④ 1:N 손실). §0.4(a)의 토폴로지 규칙으로 대체할 것.

-- Step 1: pid_equipment 기반 pump→FT 매핑 (from_tag/to_tag 정/역방향)
INSERT INTO pump_corroboration_map (pump_base_tag, flow_tag, flow_sp_tag, mapping_source)
SELECT DISTINCT
    LOWER(REPLACE(p.tag_no, '-', '')) AS pump_base_tag,
    LOWER(REPLACE(ft.tag_no, '-', '')) || '.pv' AS flow_tag,
    LOWER(REPLACE(ft.tag_no, '-', '')) || '.sp' AS flow_sp_tag,
    'pid_equipment'
FROM pid_equipment p
JOIN pid_equipment ft ON (
    -- 정방향: pump.to_tag = FT
    p.to_tag LIKE '%' || ft.tag_no || '%'
    -- 역방향: FT.from_tag = pump  
    OR ft.from_tag LIKE '%' || p.tag_no || '%'
)
WHERE p.category = '펌프'
  AND ft.category IN ('계기', '제어')
  AND ft.tag_no ~ '^FT-|^FIC-';

-- Step 2: 번호 heuristic fallback (pid_equipment에 없는 pump)
INSERT INTO pump_corroboration_map (pump_base_tag, flow_tag, flow_sp_tag, mapping_source)
SELECT DISTINCT
    v.base_tag,
    'ficq-' || SUBSTRING(v.base_tag FROM 3) || '.pv',
    'ficq-' || SUBSTRING(v.base_tag FROM 3) || '.sp',
    'heuristic'
FROM v_tag_summary v
WHERE (v.base_tag LIKE 'p-6%' OR v.base_tag LIKE 'vp-6%')
  AND v.pv ~ '[LR]-RUN|L-STOP|R-STOP|OFF'
  AND NOT EXISTS (
      SELECT 1 FROM pump_corroboration_map m WHERE m.pump_base_tag = v.base_tag
  );

-- Step 3: VP 전용 vacuum 태그 매핑
INSERT INTO pump_corroboration_map (pump_base_tag, vacuum_tag, vacuum_sp_tag, mapping_source)
SELECT
    LOWER(REPLACE(vp.tag_no, '-', '')),
    LOWER(REPLACE(pi.tag_no, '-', '')) || '.pv',
    LOWER(REPLACE(pi.tag_no, '-', '')) || '.sp',
    'pid_equipment'
FROM pid_equipment vp
CROSS JOIN pid_equipment pi
WHERE vp.tag_no LIKE 'VP-%'
  AND pi.tag_no IN ('PICA-6111', 'PICA-6211', 'PI-6111B', 'PI-6211B');

4.1.2 v_plant_running_state_corroborated 뷰

⚠️ 신선도 게이트(§0.4b)·STALE 분기(§0.4c) 미반영. 아래 뷰에 (NOW() - rt.timestamp) < interval '120 seconds' 조건과 STALE 분기를 추가하고, flow 값은 ft-*가 아니라 ficq-*.pv(컨트롤러)에서 조인할 것.

CREATE OR REPLACE VIEW v_plant_running_state_corroborated AS
WITH pump_base AS (
    -- 기존 pump_state 로직 + corroboration 매핑 LEFT JOIN
    SELECT
        trim(split_part(v.area, '|', 2)) AS area_code,
        v.area AS area_raw,
        v.base_tag,
        v.pv,
        v.description,
        v.sub_area,
        m.flow_tag,
        m.flow_sp_tag,
        m.vacuum_tag,
        m.vacuum_sp_tag,
        m.mapping_source,
        -- 유량계 PV/SP 값 조회 (realtime_table에서)
        flow_rt.livevalue AS flow_pv,
        flow_sp_rt.livevalue AS flow_sp,
        vac_rt.livevalue AS vacuum_pv,
        vac_sp_rt.livevalue AS vacuum_sp
    FROM v_tag_summary v
    LEFT JOIN pump_corroboration_map m ON m.pump_base_tag = v.base_tag
    LEFT JOIN realtime_table flow_rt ON flow_rt.tagname = m.flow_tag
    LEFT JOIN realtime_table flow_sp_rt ON flow_sp_rt.tagname = m.flow_sp_tag
    LEFT JOIN realtime_table vac_rt ON vac_rt.tagname = m.vacuum_tag
    LEFT JOIN realtime_table vac_sp_rt ON vac_sp_rt.tagname = m.vacuum_sp_tag
    WHERE v.area IS NOT NULL
      AND (v.base_tag LIKE 'p-%' OR v.base_tag LIKE 'vp-%')
      AND v.pv ~ '\|\s*(L-RUN|R-RUN|L-STOP|R-STOP|L-TRIP|R-TRIP)\s*\|'
),
pump_with_corroboration AS (
    SELECT *,
        CASE
            -- pump STOP/TRIP/OFF
            WHEN pv ~ '\|\s*[LR]-TRIP\s*\|' THEN 'TRIPPED'
            WHEN pv ~ '\|\s*(L-STOP|R-STOP|OFF|STOP)\s*\|' THEN 'STOPPED'
            
            -- pump RUN - vacuum pump (VP)
            WHEN base_tag LIKE 'vp-%' THEN
                CASE
                    WHEN vacuum_pv IS NOT NULL 
                         AND vacuum_pv ~ '^\d+\.?\d*$' 
                         AND CAST(vacuum_pv AS DOUBLE PRECISION) < 50 
                        THEN 'CONFIRMED_RUNNING'
                    WHEN vacuum_pv IS NOT NULL
                         AND (vacuum_pv ~ '^\{' OR CAST(vacuum_pv AS DOUBLE PRECISION) >= 50)
                        THEN 'SUSPICIOUS_RUNNING'
                    ELSE 'INDETERMINATE_RUNNING'
                END
            
            -- pump RUN - 유량계 있음
            WHEN flow_pv IS NOT NULL AND flow_pv ~ '^\d+\.?\d*$' THEN
                CASE
                    WHEN CAST(flow_pv AS DOUBLE PRECISION) > 0.5 THEN 'CONFIRMED_RUNNING'
                    ELSE 'SUSPICIOUS_RUNNING'
                END
            
            -- pump RUN - 유량계 없음
            ELSE 'INDETERMINATE_RUNNING'
        END AS corroborated_status
    FROM pump_base
)
SELECT
    area_code,
    area_raw,
    base_tag,
    pv AS raw_pv,
    description,
    sub_area,
    flow_tag,
    flow_pv,
    flow_sp,
    vacuum_tag,
    vacuum_pv,
    vacuum_sp,
    mapping_source,
    corroborated_status,
    CASE
        WHEN corroborated_status = 'CONFIRMED_RUNNING' THEN TRUE
        ELSE FALSE
    END AS is_corroborated_running,
    CASE
        WHEN corroborated_status = 'SUSPICIOUS_RUNNING' THEN TRUE
        ELSE FALSE
    END AS is_suspicious_running,
    CASE
        WHEN corroborated_status = 'INDETERMINATE_RUNNING' THEN TRUE
        ELSE FALSE
    END AS is_indeterminate_running
FROM pump_with_corroboration
WHERE area_code IS NOT NULL AND area_code <> '';

4.1.3 v_plant_running_state_agg 뷰 (area별 집계)

CREATE OR REPLACE VIEW v_plant_running_state_agg AS
SELECT
    area_code,
    MAX(area_raw) AS area_raw,
    COUNT(*) AS total_pumps,
    COUNT(*) FILTER (WHERE corroborated_status = 'CONFIRMED_RUNNING') AS confirmed_running,
    COUNT(*) FILTER (WHERE corroborated_status = 'SUSPICIOUS_RUNNING') AS suspicious_running,
    COUNT(*) FILTER (WHERE corroborated_status = 'INDETERMINATE_RUNNING') AS indeterminate_running,
    COUNT(*) FILTER (WHERE corroborated_status = 'TRIPPED') AS tripped_pumps,
    COUNT(*) FILTER (WHERE corroborated_status = 'STOPPED') AS stopped_pumps,
    CASE
        WHEN COUNT(*) FILTER (WHERE corroborated_status IN ('CONFIRMED_RUNNING', 'SUSPICIOUS_RUNNING', 'INDETERMINATE_RUNNING')) > 0 
            AND COUNT(*) FILTER (WHERE corroborated_status = 'SUSPICIOUS_RUNNING') = 0
            THEN 'RUNNING'
        WHEN COUNT(*) FILTER (WHERE corroborated_status = 'SUSPICIOUS_RUNNING') > 0
            THEN 'RUNNING_WITH_SUSPICIOUS'
        WHEN COUNT(*) FILTER (WHERE corroborated_status = 'TRIPPED') > 0
            THEN 'TRIPPED'
        ELSE 'STOPPED'
    END AS overall_status,
    -- corroborated_rate: 전체 RUN 펌프 중 확인된 비율
    CASE
        WHEN COUNT(*) FILTER (WHERE corroborated_status IN ('CONFIRMED_RUNNING', 'SUSPICIOUS_RUNNING', 'INDETERMINATE_RUNNING')) > 0
        THEN ROUND(
            COUNT(*) FILTER (WHERE corroborated_status = 'CONFIRMED_RUNNING')::NUMERIC 
            / COUNT(*) FILTER (WHERE corroborated_status IN ('CONFIRMED_RUNNING', 'SUSPICIOUS_RUNNING', 'INDETERMINATE_RUNNING'))
            * 100, 1
        )
        ELSE NULL
    END AS corroborated_pct,
    array_agg(base_tag) FILTER (WHERE corroborated_status = 'SUSPICIOUS_RUNNING') AS suspicious_pump_tags,
    array_agg(base_tag) FILTER (WHERE corroborated_status = 'CONFIRMED_RUNNING') AS confirmed_running_tags
FROM v_plant_running_state_corroborated
WHERE area_code IS NOT NULL AND area_code <> ''
GROUP BY area_code
ORDER BY area_code;

Phase 1: MCP Server 통합

4.1.4 server.py — 새 뷰 조회 추가

generate_status_report 함수 내에서 v_plant_running_state_agg 조회:

# server.py — generate_status_report 내부
cur.execute("""
    SELECT area_code, overall_status, total_pumps, confirmed_running,
           suspicious_running, suspicious_pump_tags, corroborated_pct
    FROM v_plant_running_state_agg
    WHERE (%s IS NULL OR area_code = %s)
    ORDER BY area_code
""", (area, area))

응답 JSON에 추가:

{
  "active_alarms": [...],
  "recent_events": [...],
  "by_type": {...},
  "pump_corroboration": {
    "by_area": [
      {
        "area": "P6",
        "status": "RUNNING_WITH_SUSPICIOUS",
        "total_pumps": 22,
        "confirmed_running": 4,
        "suspicious_running": 1,
        "corroborated_pct": 80.0,
        "suspicious_pumps": ["p-6114"]
      }
    ]
  }
}

4.1.5 active_alarms — SUSPICIOUS_RUNNING 의심 알람 추가

-- active_alarms에 suspicious pump 추가
SELECT base_tag AS tag_name, 'SUSPICIOUS_RUNNING' AS event_type,
       '펌프 RUN 상태이나 유량 없음' AS description,
       area_code
FROM v_plant_running_state_corroborated
WHERE is_suspicious_running = TRUE;

4.1.6 trace_connections — flow_pv/run_status 노출

# 각 path 노드에 flow_pv, corroborated_status 추가
# pid_equipment tag_no → base_tag 변환 후 v_plant_running_state_corroborated 조회

Phase 1: plant_context.md 업데이트

프롬프트에 교차검증 관련 컨텍스트 추가:

## 운전 판정 교차검증 (Corroboration)

펌프의 상태 워드(R-RUN/L-RUN)만으로 운전을 판정하지 않고, 
연결된 유량계(FICQ/FIQ)의 PV 값을 교차검증하여 3단계 판정:

| 판정 | 의미 |
|------|------|
| CONFIRMED_RUNNING | 펌프 RUN + 유량계 PV > 0.5 (실질 운전) |
| SUSPICIOUS_RUNNING | 펌프 RUN + 유량계 PV ≤ 0.5 (의심: deadhead, 센서오류) |
| INDETERMINATE_RUNNING | 펌프 RUN + 유량계 데이터 없음 (킥백 가능성) |

진공펌프(VP)는 유량계 대신 진공압(PICA.pv < 50)으로 판정.

- `v_plant_running_state_corroborated`: 태그별 상세 판정
- `v_plant_running_state_agg`: area별 집계 (corroborated_pct 포함)

Phase 2: 정밀화 (Phase 1 검증 후)

항목 내용 우선순위
instrument_range 테이블 각 유량계의 Full Scale / Unit 저장 → 5% threshold 계산 높음
Plant Load Rate 기반 검증 원료투입량(FICQ-6101.pv) 대비 각 유량계 비율 계산 → 수율(Throughput) 추정 중간
FCV-XXXX.op 태그 추가 Control Valve position 실시간 감시 → kickback 자동 인식 중간
AI/통계 threshold 정상 운전 기간의 평균±3σ로 이상 감지 낮음
PumpCorroborationService C# BackgroundService로 주기적 검증 → pump_corroboration_history 테이블에 이벤트 기록 낮음

instrument_range 테이블 설계 (Phase 2)

CREATE TABLE instrument_range (
    base_tag    TEXT PRIMARY KEY,
    full_scale  DOUBLE PRECISION NOT NULL,
    unit        TEXT,
    source      TEXT DEFAULT 'manual',  -- 'pid_equipment' | 'opc_ua' | 'manual'
    updated_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Phase 2 threshold: PV > 0.05 * full_scale (5% of Full Scale)

Phase 2에서 기존 view의 threshold만 변경:

-- Phase 2 수정안
WHEN CAST(flow_pv AS DOUBLE PRECISION) > 0.05 * ir.full_scale THEN 'CONFIRMED_RUNNING'

Phase 3: 프론트엔드 대시보드

  • Area Overview에 corroborated_pct 게이지 표시
  • SUSPICIOUS_RUNNING 펌프 빨간색 하이라이트
  • 클릭 시 flow_pv / vacuum_pv 상세 표시

5. 리스크 분석

리스크 영향 대응
Flow PV가 dummy 값(정정 §0.1) 라이브 데이터 (ficq-6113 38~55 변동 확인). 단 수집기 stall 시 frozen frozen을 정상으로 오판(CONFIRMED) 신선도 게이트(§0.4b) + 수집기 supervisor 수정(2026-05-24 적용)
pid_equipment 미완성 일부 pump 교차검증 불가 번호 heuristic fallback으로 커버. 이후 수동 보강
킥백 상황 오판 SUSPICIOUS_RUNNING 과다 0.5 threshold로 PV=0 케이스만 포착. Phase 2에서 FCV-XXXX.op 추가
진공압 범위/단위 불명 threshold 값 부정확 현재 PICA.pv=20.8(임의값). 실제 단위 확인 필요 (mmHg / kPa / bar)
성능: realtime_table LEFT JOIN 뷰 조회 속도 저하 pump_corroboration_map에 인덱스. 실운영 모니터링

6. 검증 계획

  1. 유닛 테스트: 각 판정 CASE별 샘플 데이터 생성 → 예상 결과와 일치 확인
  2. DB 뷰 검증: production replica에서 v_plant_running_state_corroborated 조회, 수동 확인
  3. MCP 응답 체크: generate_status_report에 suspicious 필드 정상 포함 확인
  4. 운전원 피드백: SUSPICIOUS_RUNNING 케이스 실제 상황과 일치하는지 확인

7. 일정 (예상)

Phase 작업 예상 기간 비고
Phase 1 pump_corroboration_map 시딩 스크립트 1일
v_plant_running_state_corroborated 뷰 0.5일
v_plant_running_state_agg 뷰 0.5일
MCP server.py 통합 1일
plant_context.md 업데이트 0.5일
소계 3.5일
Phase 2 instrument_range 테이블 + 시딩 1일 Phase 1 검증 후
Plant Load Rate 계산 로직 2일
FCV-XXXX.op 추가 0.5일
통계 threshold 2일
소계 5.5일
Phase 3 프론트엔드 대시보드 2일
총계 11일

8. 결론

현재 펌프 상태 워드 단일 판정을 유량계·진공압 교차검증으로 고도화하여 허위 운전 정보 제공을 방지하고, 실질 운전 여부를 정확히 판정할 수 있음.

Phase 1은 SQL view 확장만으로 즉시 적용 가능하며, OPC UA 실제 데이터 연결 후 검증 즉시 가동 가능.

핵심 의사결정 사항:

  • Pump↔유량계 매핑 방식 → pid_equipment 기반 + 번호 heuristic fallback ( 결정)
  • Threshold 기준 → 절대값 0.5 (Phase 1), Full Scale 5% (Phase 2) ( 결정)
  • 킥백 처리 → Phase 1에서 별도 미처리, Phase 2에서 valve position 추가 ( 결정)
  • VP 교차검증 → PICA.pv < 50 기준 (단위 확인 필요)