fix(report): 카드 state = 운전모드 단일 진실원 + 이송 메시지 컬럼별 정정
- ★카드 state를 production_total(누적적산)이 아니라 운전모드(순간 feed/product/vacuum)에서 도출. 적산기가 평평해도(데모) 유량 흐르면 normal로 표시 — "살아있는데 미가동" 모순 해결. DetectModeAsync(idle/cleaning/drawdown/normal/no_data) = 카드 state·알람 공통 진실원. idle = feed≈0 AND product≈0(진짜 미가동)일 때만. 버퍼에 없으면 no_data(idle 아님). - 알람도 같은 모드 사용(state-알람 진실원 일치). 이송 메시지는 Report:Transfer config로 컬럼별 정정 — C-9111↔C-10111만 "이송" 언급, 그 외는 "인벤토리 인출"만. 배너 룰접두사 중복 제거. 검증: C-6111 state=normal(이전 idle 모순 해결), C-9111 no_data(버퍼밖 정직표시), C-8111 drawdown 메시지 이송 미언급. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,10 @@
|
||||
}
|
||||
},
|
||||
"Report": {
|
||||
"Transfer": {
|
||||
"C-9111": "C-10111",
|
||||
"C-10111": "C-9111"
|
||||
},
|
||||
"Historian": {
|
||||
"Enabled": true,
|
||||
"IntervalSeconds": 1,
|
||||
|
||||
@@ -237,6 +237,6 @@
|
||||
<script src="/js/trend.js?v=20260611"></script>
|
||||
<script src="/js/ff.js?v=20260604"></script>
|
||||
<script src="/js/steam.js?v=20260606"></script>
|
||||
<script src="/js/reports.js?v=20260615b"></script>
|
||||
<script src="/js/reports.js?v=20260615d"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -123,8 +123,8 @@ function renderSummary(d) {
|
||||
}
|
||||
|
||||
/* ── ① 라이브 모니터 (상태 카드 그리드) ─────────────────────── */
|
||||
const RM_COLOR = { normal:'#2ea043', idle:'#8b949e', error:'#e5534b', cleaning:'#3a6ea5', drawdown:'#d29922' };
|
||||
const RM_LABEL = { normal:'정상', idle:'미가동', error:'오류', cleaning:'세정', drawdown:'drawdown' };
|
||||
const RM_COLOR = { normal:'#2ea043', idle:'#8b949e', no_data:'#6e7681', error:'#e5534b', cleaning:'#3a6ea5', drawdown:'#d29922' };
|
||||
const RM_LABEL = { normal:'정상', idle:'미가동', no_data:'데이터없음', error:'오류', cleaning:'세정', drawdown:'인출' };
|
||||
|
||||
function rmStart() {
|
||||
const auto = document.getElementById('rmAuto');
|
||||
@@ -156,7 +156,7 @@ async function rmRefresh() {
|
||||
banner.innerHTML = al.length === 0 ? ''
|
||||
: `<div style="border:1px solid #d29922;border-radius:8px;padding:8px 12px;background:rgba(210,153,34,.08)">
|
||||
<b>알람 ${al.length}</b> ` +
|
||||
al.map(a => `<span style="margin-right:14px">${a.Severity === 'warning' ? '🟠' : '🟡'} <b>${a.Column}</b> ${a.Rule} — ${esc2(a.Message || '')}</span>`).join('') + `</div>`;
|
||||
al.map(a => `<span style="margin-right:14px">${a.Severity === 'warning' ? '🟠' : '🟡'} <b>${a.Column}</b> · ${esc2(a.Message || '')}</span>`).join('') + `</div>`;
|
||||
|
||||
// 카드 그리드 (컬럼 정렬)
|
||||
const cols = Object.keys(byCol).sort();
|
||||
|
||||
@@ -69,10 +69,8 @@ public class Hc900LiveKpiService : BackgroundService
|
||||
results.Add(await metrics.ComputeAsync(new MetricRequestDto
|
||||
{ Column = col, Metric = kpi, PeriodDateKst = todayKst, SourceTable = _source }, stoppingToken));
|
||||
|
||||
// 컬럼 상태: error=계산오류, 생산>0=normal, 그 외(0·no_data)=idle(미가동)
|
||||
var prod = results.First(r => r.Metric == "production_total");
|
||||
string state = prod.Status == "error" ? "error"
|
||||
: prod.Value is > 0 ? "normal" : "idle";
|
||||
// ★카드 state 단일 진실원 = 운전모드(순간 feed/product/vacuum). 누적생산이 아님.
|
||||
var (state, feedNow) = await DetectModeAsync(conn, col, stoppingToken);
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
@@ -91,8 +89,8 @@ public class Hc900LiveKpiService : BackgroundService
|
||||
await UpsertAsync(conn, col, "out_total", todayKst, otv, "kg", state, null, "ok", stoppingToken);
|
||||
}
|
||||
|
||||
// P1d: 실시간 알람 (운전모드 + 폐합 이탈)
|
||||
await EvaluateAlertsAsync(conn, col, closure, stoppingToken);
|
||||
// P1d: 실시간 알람 — 같은 모드 기반(state와 진실원 일치)
|
||||
await EvaluateAlertsAsync(conn, col, state, feedNow, closure, stoppingToken);
|
||||
}
|
||||
_logger.LogDebug("[LiveKpi] {N}개 KPI 갱신 @ {Day}", written, todayKst);
|
||||
}
|
||||
@@ -138,11 +136,10 @@ CREATE TABLE IF NOT EXISTS hc900.kpi_alert (
|
||||
await cmd.ExecuteNonQueryAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>P1d 알람: 최신 샘플로 운전모드(cleaning/drawdown/normal) 판정 + 폐합 이탈 룰 → kpi_alert edge-trigger.</summary>
|
||||
private async Task EvaluateAlertsAsync(System.Data.Common.DbConnection conn, string col, MetricResultDto closure, CancellationToken ct)
|
||||
/// <summary>★운전모드 단일 진실원: 최신 feed/product/vacuum 샘플로 idle/cleaning/drawdown/normal 분류. 카드 state·알람 공통.</summary>
|
||||
private async Task<(string mode, double? feed)> DetectModeAsync(System.Data.Common.DbConnection conn, string col, CancellationToken ct)
|
||||
{
|
||||
if (!_map.TryResolveCleaning(col, out var cl) || cl is null) return;
|
||||
|
||||
if (!_map.TryResolveCleaning(col, out var cl) || cl is null) return ("no_data", null);
|
||||
double? vac = null, prod = null, feed = null;
|
||||
await using (var q = conn.CreateCommand())
|
||||
{
|
||||
@@ -160,18 +157,31 @@ 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);
|
||||
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"; // 둘 다 흐름 + 진공 정상
|
||||
return (mode, feed);
|
||||
}
|
||||
|
||||
/// <summary>P1d 알람: 운전모드(카드 state와 동일 진실원) + 폐합 이탈 → kpi_alert edge-trigger.</summary>
|
||||
private async Task EvaluateAlertsAsync(System.Data.Common.DbConnection conn, string col, string mode, double? feed,
|
||||
MetricResultDto closure, CancellationToken ct)
|
||||
{
|
||||
var desired = new List<(string rule, string sev, string msg, double? val)>();
|
||||
if (prod is not null || feed is not null) // 데이터 있을 때만 평가(없으면 전부 해제)
|
||||
if (mode == "cleaning") desired.Add(("cleaning", "info", "진공 高 또는 제품~0 (세정/비운전)", null));
|
||||
else if (mode == "drawdown")
|
||||
{
|
||||
bool isClean = (prod ?? 0) < cl.ProductMin || (vac.HasValue && vac > cl.VacMax);
|
||||
bool isDraw = (feed ?? 0) < cl.FeedMin && (prod ?? 0) >= cl.ProductMin;
|
||||
bool isNormal = (feed ?? 0) >= cl.FeedMin && (prod ?? 0) >= cl.ProductMin && (!vac.HasValue || vac <= cl.VacMax);
|
||||
if (isClean) desired.Add(("cleaning", "info", "세정/비운전 진입(진공 高 또는 제품~0)", null));
|
||||
else if (isDraw) desired.Add(("drawdown", "info", "drawdown — feed≈0 인데 출력(인벤토리 인출/컬럼간 이송)", feed));
|
||||
if (isNormal && closure.Status == "ok" && closure.Value is double cv && Math.Abs(cv - 100) > _closureTol)
|
||||
desired.Add(("closure_deviation", "warning", $"폐합 {cv:F1}% (100±{_closureTol}% 이탈 — 계량 드리프트/누설/이송 점검)", cv));
|
||||
var partner = _map.TransferPartner(col);
|
||||
var msg = partner is null ? "feed≈0 인데 출력 — 인벤토리 인출"
|
||||
: $"feed≈0 인데 출력 — 인벤토리 인출 또는 {partner} 이송";
|
||||
desired.Add(("drawdown", "info", msg, feed));
|
||||
}
|
||||
else if (mode == "normal" && closure.Status == "ok" && closure.Value is double cv && Math.Abs(cv - 100) > _closureTol)
|
||||
desired.Add(("closure_deviation", "warning", $"폐합 {cv:F1}% (100±{_closureTol}% 이탈 — 계량 드리프트/누설/이송 점검)", cv));
|
||||
|
||||
// 디바운스: 조건이 _debounceSec 이상 연속 유지된 룰만 confirm(발화). flapping 방지.
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
@@ -70,6 +70,9 @@ public sealed class ReportColumnMap
|
||||
/// <summary>민감단(품질) 온도 태그 — 스파크라인/트렌드용.</summary>
|
||||
public string? TcTag(string column) => Norm(_config[$"SteamAdvisor:Columns:{column}:TC"]);
|
||||
|
||||
/// <summary>컬럼간 이송 라인 상대 컬럼(있으면). 예: C-9111↔C-10111. 없으면 null(=순수 인벤토리 인출).</summary>
|
||||
public string? TransferPartner(string column) => _config[$"Report:Transfer:{column}"];
|
||||
|
||||
/// <summary>동특성 대상 루프(하부온도 TICA-*A): PV vs OP(스팀밸브). 밸브 stiction/hunting 진단용.</summary>
|
||||
public bool TryResolveDynamics(string column, out string? pvTag, out string? opTag)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user