diff --git a/src/Hc900Crawler/Controllers/ReportController.cs b/src/Hc900Crawler/Controllers/ReportController.cs index ce56524..476ea4c 100644 --- a/src/Hc900Crawler/Controllers/ReportController.cs +++ b/src/Hc900Crawler/Controllers/ReportController.cs @@ -128,8 +128,11 @@ GROUP BY b ORDER BY b"; string source = "history_table", string period = "DAILY", int? sessionId = null, CancellationToken ct = default) { var d = (date ?? DateTime.UtcNow.AddHours(9).AddDays(-1)).Date; // 기본 = 어제(KST) + var metricNames = new List(SUMMARY_METRICS); + if (_map.TransferPartner(column) is not null) // 이송 연계 컬럼(C-9111↔C-10111)만 통합 폐합 추가 + metricNames.Add("mass_balance_closure_linked"); var results = new List(); - foreach (var m in SUMMARY_METRICS) + foreach (var m in metricNames) results.Add(await _metrics.ComputeAsync(new MetricRequestDto { Column = column, Metric = m, PeriodDateKst = d, Period = period, diff --git a/src/Hc900Crawler/wwwroot/js/reports.js b/src/Hc900Crawler/wwwroot/js/reports.js index 6eb7430..b2fd4dc 100644 --- a/src/Hc900Crawler/wwwroot/js/reports.js +++ b/src/Hc900Crawler/wwwroot/js/reports.js @@ -197,6 +197,29 @@ function renderSummary(d) { `; } + // 연계운전 통합 폐합 (이송으로 묶인 두 컬럼 합산) — 결과 있을 때만 + const lk = by['mass_balance_closure_linked']; + if (lk) { + const pct = lk.Status === 'ok' ? lk.Value : null; + const ok = pct != null && pct >= 98 && pct <= 101; + const color = pct == null ? '#888' : (ok ? '#2ea043' : '#e5534b'); + const e = lk.Extra || {}; + html += ` +
+
+
+
통합 물질수지 폐합 (이송 연계 컬럼 합산)
+
통합 IN ${rpFmt(e.feed_qv,'kg')}  →  통합 OUT ${rpFmt(e.out_total,'kg')} kg
+
단독 폐합이 N/A(연계운전 feed≈0)인 날 두 컬럼을 합쳐 검산
+
+
+
${pct == null ? 'N/A' : rpFmt(pct,'pct') + '%'}
+
${pct == null ? (lk.Error || '') : (ok ? '✓ 정상' : '⚠ 이탈')}
+
+
+
`; + } + // 메트릭 표 const rows = [ ['생산량 (제품 적산Δ)', by['production_total'], 'kg'], diff --git a/src/Hc900Crawler/wwwroot/panes/reports.html b/src/Hc900Crawler/wwwroot/panes/reports.html index d6e64dd..20c5859 100644 --- a/src/Hc900Crawler/wwwroot/panes/reports.html +++ b/src/Hc900Crawler/wwwroot/panes/reports.html @@ -110,6 +110,7 @@ {{ metric=mass_balance_closure; column=C-6111; field=feed_qv }} → 원료 적산 Δ (IN) {{ metric=mass_balance_closure; column=C-6111; field=out_total }} → 회수 합 (OUT) {{ metric=mass_balance_closure; column=C-6111; field=out0_qv }} → 제품(경비/중비는 out1/out2) +{{ metric=mass_balance_closure_linked; column=C-9111 }} → 통합 폐합 % (이송 연계 C-9111+C-10111 합산) ■ PV 기반 (근사) {{ metric=control_residual; column=C-6111 }} → 평균 잔차(PV-SP) diff --git a/src/Infrastructure/Reporting/ReportColumnMap.cs b/src/Infrastructure/Reporting/ReportColumnMap.cs index 5598fd9..6923fd1 100644 --- a/src/Infrastructure/Reporting/ReportColumnMap.cs +++ b/src/Infrastructure/Reporting/ReportColumnMap.cs @@ -14,6 +14,9 @@ public sealed record QvSpec(string Unit, QvKind Kind, string A, string? B, IRead /// 비정상운전(cleaning/drawdown) 제외 마스크. 진공高 또는 제품~0 또는 feed~0인 분 제외. public sealed record CleaningSpec(string? VacTag, double VacMax, string ProductTag, double ProductMin, string FeedTag, double FeedMin); +/// 연계운전 통합 폐합. 이송으로 묶인 두 컬럼의 외부 feed 합 / 최종 출력 합. 내부 이송은 비계량이라 자동 상쇄. +public sealed record LinkedClosureSpec(string Unit, IReadOnlyList Feeds, IReadOnlyList Outputs, string Partner); + /// /// 컬럼→태그 매핑. 기존 appsettings `SteamAdvisor:Columns`(Feed/Product/TC/SteamOp/SteamFlow)를 /// 단일 진실원으로 재사용한다(멀티컬럼 무료). 클린범위(스파이크/드롭아웃 경계)는 tag_metadata @@ -182,6 +185,32 @@ public sealed class ReportColumnMap } } + /// + /// 연계운전 통합 폐합 해석. Report:Transfer:{col}로 이송 파트너를 찾아, 양 컬럼의 + /// Report:Closure(Feed + Outputs).QV를 합쳐 envelope 폐합을 만든다. 파트너 없으면 false + /// (= 배관 연계 없는 단독 컬럼 → 통합 폐합 미정의). C-9111↔C-10111 한 쌍에만 설정됨. + /// + public bool TryResolveLinkedClosure(string column, out LinkedClosureSpec? spec) + { + spec = null; + var partner = _config[$"Report:Transfer:{column}"]; + if (string.IsNullOrWhiteSpace(partner)) return false; + + var feeds = new List(); + var outs = new List(); + foreach (var col in new[] { column, partner }) + { + var cl = _config.GetSection($"Report:Closure:{col}"); + if (!cl.Exists()) return false; + if (ToQv(cl["Feed"]) is string f) feeds.Add(f); + foreach (var o in cl.GetSection("Outputs").Get() ?? Array.Empty()) + if (ToQv(o) is string oq) outs.Add(oq); + } + if (feeds.Count == 0 || outs.Count == 0) return false; + spec = new LinkedClosureSpec("%", feeds, outs, partner); + return true; + } + /// cleaning/drawdown 마스크 해석. 제품·feed는 SteamAdvisor:Columns, 진공태그·임계는 Report:Cleaning. public bool TryResolveCleaning(string column, out CleaningSpec? spec) { diff --git a/src/Infrastructure/Reporting/ReportMetricService.cs b/src/Infrastructure/Reporting/ReportMetricService.cs index 877e018..615875e 100644 --- a/src/Infrastructure/Reporting/ReportMetricService.cs +++ b/src/Infrastructure/Reporting/ReportMetricService.cs @@ -77,6 +77,13 @@ public sealed class ReportMetricService : IReportMetricService { await DynamicsAsync(res, conn, tbl, req.Column, fromUtc, toUtc, isFast, req.SessionId, ct); } + else if (req.Metric == "mass_balance_closure_linked") + { + if (!_map.TryResolveLinkedClosure(req.Column, out var lspec) || lspec is null) + { res.Status = "error"; res.Error = $"연계 폐합 미정의(이송 파트너 없음): {req.Column}"; res.Value = null; return res; } + res.Unit = lspec.Unit; + await ComputeLinkedClosureAsync(res, conn, tbl, lspec, fromUtc, toUtc, isFast, req.SessionId, ct); + } else if (QV_METRICS.Contains(req.Metric)) { if (!_map.TryResolveQv(req.Column, req.Metric, out var qspec) || qspec is null) @@ -207,6 +214,33 @@ FROM seq2"; } } + // ── 연계운전 통합 폐합: 두 컬럼 외부 feed 합 / 최종 출력 합. cleaning 마스크 미적용(null) — + // 연계 시 한쪽 feed≈0은 정상(drawdown)이라 마스크를 쓰면 전구간 제외됨. 내부 이송은 + // 비계량(어느 컬럼 feed/output에도 안 잡힘)이라 envelope에서 자동 상쇄. ── + private async Task ComputeLinkedClosureAsync(MetricResultDto res, DbConnection conn, string tbl, + LinkedClosureSpec s, DateTime fromUtc, DateTime toUtc, bool isFast, int? sid, CancellationToken ct) + { + double inSum = 0; int nMin = 0; + foreach (var f in s.Feeds) + { + var (d, n, _) = await QvDeltaAsync(conn, tbl, f, null, fromUtc, toUtc, isFast, sid, ct); + inSum += d ?? 0; nMin = Math.Max(nMin, n); + } + if (inSum <= 0) { res.Status = "no_data"; res.Value = null; res.Error = "통합 IN(외부 feed 합) Δ≤0"; return; } + + double outSum = 0; + for (int i = 0; i < s.Outputs.Count; i++) + { + var (d, _, _) = await QvDeltaAsync(conn, tbl, s.Outputs[i], null, fromUtc, toUtc, isFast, sid, ct); + outSum += d ?? 0; + res.Extra[$"out{i}_qv"] = d; + } + res.Value = 100.0 * outSum / inSum; // 통합 폐합 % + res.N = nMin; + res.Extra["feed_qv"] = inSum; // 통합 IN + res.Extra["out_total"] = outSum; // 통합 OUT + } + /// /// 적산 Δ. 비정상운전 분(cleaning=진공高/제품~0, drawdown=feed~0) 제외 후, 정상구간 양(+)증분만 합산(cap 5e4). /// 리셋 3종 자동처리: 999999 wrap·cleaning 리셋·운전조건변경 리셋. cl=null이면 마스크 없이 양증분합산.