# 운전판정 고도화 플랜 — 유량계·진공압 교차검증(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 설계 공백 6. **[최우선] 데이터 신선도 게이트 부재** — realtime 값이 stale/frozen이어도 임계 비교 → frozen `20.2667`을 "유량 정상=CONFIRMED"로 **오판**. (이번 수집기 stall이 정확히 이 시나리오.) 판정 전 `NOW()-timestamp` 확인, stale면 판정 보류. 7. **[중간] 임계 0.5 절대값** — 계기별 Full Scale 무시(작은 레인지 과대·큰 레인지 과소). 8. **[중간] 집계가 1대 의심에 전체 오염** — suspicious 1대로 area가 RUNNING_WITH_SUSPICIOUS. 정상 standby/kickback(sp=0) 펌프 상시 의심 위험. corroborated_pct 분모에 INDETERMINATE 혼입. 9. **[중간] active_alarms 주입 위험** — 미검증 휴리스틱(+데이터 품질 이슈)을 운전원 알람화 → 알람 피로. 검증 전 advisory만. 10. **[낮음] VP 신호 선택** — `pica-6111`(압력제어)보다 `pi-6111b`("VACUUM PRESSURE", C-6111 직결)가 진공 판정에 적합해 보임. sense/단위 확인 후 결정. ### 0.4 권장 보완 **(a) 매핑 — 토폴로지 규칙(번호 heuristic 폐기)** - 1차: `FT.from_tag`가 펌프를 참조하는 행 수집(역방향, 1:N 자연 지원). 값 태그 = 같은 번호 **FICQ 컨트롤러** `ficq-.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) 신선도 게이트 (신규·최우선)** ```sql -- 값 신뢰 조건: 수집 후 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 갱신된 의사결정 체크리스트 - [x] 데이터 성격: **라이브**(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.MetaAttributes`에 `euhi/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_report`에 `v_plant_running_state_agg` 노출은 후속. `active_alarms` 주입은 운전원 검증 후(§0.4e). --- ## 1. 배경 및 문제 정의 ### 1.1 현재 상황 현재 공장 운전 판정(`v_plant_running_state` 뷰)은 **펌프의 상태 워드(enum 값)만**으로 이루어짐: ```sql -- 현재 로직 (의사코드) 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 테이블 (신규):** ```sql 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)의 토폴로지 규칙으로 대체할 것.** ```sql -- 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`(컨트롤러)에서 조인할 것. ```sql 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별 집계) ```sql 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` 조회: ```python # 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에 추가: ```python { "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 의심 알람 추가 ```sql -- 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 노출 ```python # 각 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) ```sql 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만 변경: ```sql -- 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 기준 (단위 확인 필요)