From cdf2719a5e165805cf6821bd76abdafbd8b55b6a Mon Sep 17 00:00:00 2001 From: windpacer Date: Tue, 16 Jun 2026 21:41:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(report):=20=EC=9A=B4=EC=A0=84=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=EC=97=90=20=ED=8E=8C=ED=94=84=20=EA=B0=80=EB=8F=99(RU?= =?UTF-8?q?N/STOP)=20=EB=B0=98=EC=98=81=20+=20/live=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=20window=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 펌프 게이트: 펌프 정지(state STOP/TRIP/FAULT) → 잔류 유량과 무관하게 idle. 펌프 가동(L-RUN=1/R-RUN=5) → 흐름 기반(normal/cleaning/drawdown). DetectModeAsync 통합. 인디케이터 펌프 = Report:Pumps:{col} 또는 제품펌프 P-{제품번호} 유도. realtime_table 현재 상태로 판정. - /live: DISTINCT ON (column_id,kpi) 최신 window_start만 반환 — 과거 날짜 잔여 행이 stale state로 표시되던 버그 수정. 검증: C-6111 펌프RUN→normal, C-6211 펌프STOP→idle(잔류유량 12.8 있어도). 펌프 상태값 5=R-RUN/1=L-RUN/0=L-STOP (tag_metadata state 라벨 확정). /live 컬럼당 state 단일. Co-Authored-By: Claude Opus 4.8 --- .../Controllers/ReportController.cs | 6 +++-- src/Hc900Crawler/appsettings.json | 4 +++ .../Hc900/Hc900LiveKpiService.cs | 25 ++++++++++++++----- .../Reporting/ReportColumnMap.cs | 12 +++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/Hc900Crawler/Controllers/ReportController.cs b/src/Hc900Crawler/Controllers/ReportController.cs index baefc74..61abfb9 100644 --- a/src/Hc900Crawler/Controllers/ReportController.cs +++ b/src/Hc900Crawler/Controllers/ReportController.cs @@ -33,9 +33,11 @@ public class ReportController : ControllerBase var conn = _db.Database.GetDbConnection(); if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct); await using var cmd = conn.CreateCommand(); - cmd.CommandText = @"SELECT column_id, kpi, value, unit, state, excluded_min, status, window_start, updated_at + // (column_id,kpi)별 최신 window_start만 — 과거 날짜 잔여 행으로 인한 stale 표시 방지 + cmd.CommandText = @"SELECT DISTINCT ON (column_id, kpi) + column_id, kpi, value, unit, state, excluded_min, status, window_start, updated_at FROM hc900.live_kpi" + (column == null ? "" : " WHERE column_id=@col") + - " ORDER BY column_id, kpi"; + " ORDER BY column_id, kpi, window_start DESC"; if (column != null) { var p = cmd.CreateParameter(); p.ParameterName = "@col"; p.Value = column; cmd.Parameters.Add(p); } var items = new List(); await using var rd = await cmd.ExecuteReaderAsync(ct); diff --git a/src/Hc900Crawler/appsettings.json b/src/Hc900Crawler/appsettings.json index 8ccda04..f7a053e 100644 --- a/src/Hc900Crawler/appsettings.json +++ b/src/Hc900Crawler/appsettings.json @@ -91,6 +91,10 @@ "C-9111": "C-10111", "C-10111": "C-9111" }, + "Pumps": { + "C-6111": [ "P-6102", "P-6118" ], + "C-6211": [ "P-6201", "P-6218" ] + }, "Historian": { "Enabled": true, "IntervalSeconds": 1, diff --git a/src/Infrastructure/Hc900/Hc900LiveKpiService.cs b/src/Infrastructure/Hc900/Hc900LiveKpiService.cs index 25b2e56..b0255e4 100644 --- a/src/Infrastructure/Hc900/Hc900LiveKpiService.cs +++ b/src/Infrastructure/Hc900/Hc900LiveKpiService.cs @@ -157,13 +157,26 @@ CREATE TABLE IF NOT EXISTS hc900.kpi_alert ( feed = rd.IsDBNull(2) ? null : rd.GetDouble(2); } } - if (prod is null && feed is null) return ("no_data", feed); + // 펌프 가동 여부(realtime_table 현재 상태) — 정지면 잔류 흐름과 무관하게 idle. + var pumps = _map.PumpTags(col); + bool pumpKnown = pumps.Count > 0, pumpRunning = false; + if (pumpKnown) + { + await using var pq = conn.CreateCommand(); + pq.CommandText = "SELECT count(*) FROM hc900.realtime_table WHERE tagname = ANY(@p) AND livevalue IN ('1','5','L-RUN','R-RUN')"; + var pp = pq.CreateParameter(); pp.ParameterName = "@p"; pp.Value = pumps.ToArray(); pq.Parameters.Add(pp); + pumpRunning = Convert.ToInt64(await pq.ExecuteScalarAsync(ct)) > 0; + } + + if (prod is null && feed is null && !pumpRunning) return ("no_data", feed); bool fLow = (feed ?? 0) < cl.FeedMin, pLow = (prod ?? 0) < cl.ProductMin, vHigh = vac.HasValue && vac > cl.VacMax; - string mode = (fLow && pLow) ? "idle" // 아무것도 안 흐름 = 진짜 미가동 - : vHigh ? "cleaning" // 진공 깨짐 = 세정/비운전 - : (fLow) ? "drawdown" // feed≈0 + product 흐름 = 인벤토리 인출 - : (pLow) ? "cleaning" // feed 흐름 + product≈0 = 세정/startup - : "normal"; // 둘 다 흐름 + 진공 정상 + string mode = + (pumpKnown && !pumpRunning) ? "idle" // ★펌프 정지 = 셧다운(흐름 잔류 무관) + : (fLow && pLow) ? (pumpRunning ? "normal" : "idle") // 펌프 도는데 흐름↓ = 가동(저율/startup) + : vHigh ? "cleaning" + : (fLow) ? "drawdown" + : (pLow) ? "cleaning" + : "normal"; return (mode, feed); } diff --git a/src/Infrastructure/Reporting/ReportColumnMap.cs b/src/Infrastructure/Reporting/ReportColumnMap.cs index 0c25014..5934c65 100644 --- a/src/Infrastructure/Reporting/ReportColumnMap.cs +++ b/src/Infrastructure/Reporting/ReportColumnMap.cs @@ -73,6 +73,18 @@ public sealed class ReportColumnMap /// 컬럼간 이송 라인 상대 컬럼(있으면). 예: C-9111↔C-10111. 없으면 null(=순수 인벤토리 인출). public string? TransferPartner(string column) => _config[$"Report:Transfer:{column}"]; + /// 가동 판정용 펌프 상태태그(.PV). Report:Pumps:{col} 우선, 없으면 제품펌프 P-{제품번호} 유도. + public IReadOnlyList PumpTags(string column) + { + var configured = _config.GetSection($"Report:Pumps:{column}").Get(); + if (configured is { Length: > 0 }) + return configured.Select(t => t.Contains('.') ? t : t + ".PV").ToList(); + var prod = _config[$"SteamAdvisor:Columns:{column}:Product"]; // 예: FICQ-6118.PV + if (string.IsNullOrWhiteSpace(prod)) return Array.Empty(); + var num = StripAttr(prod!).Split('-').Last(); // 6118 + return new[] { $"P-{num}.PV" }; // 제품펌프 + } + /// 동특성 대상 루프(하부온도 TICA-*A): PV vs OP(스팀밸브). 밸브 stiction/hunting 진단용. public bool TryResolveDynamics(string column, out string? pvTag, out string? opTag) {