diff --git a/scripts/sql/p1_historian.sql b/scripts/sql/p1_historian.sql
index 70ac2fb..4168050 100644
--- a/scripts/sql/p1_historian.sql
+++ b/scripts/sql/p1_historian.sql
@@ -33,3 +33,13 @@ WITH NO DATA;
SELECT add_continuous_aggregate_policy('hc900.history_1min',
start_offset => INTERVAL '3 hours', end_offset => INTERVAL '10 minutes',
schedule_interval => INTERVAL '5 minutes', if_not_exists => TRUE);
+
+-- P1c: 메트릭엔진 드롭인 소스(bucket→recorded_at) + 온라인 KPI 누적 테이블
+CREATE OR REPLACE VIEW hc900.history_1min_src AS
+ SELECT tagname, bucket AS recorded_at, value, controller_id FROM hc900.history_1min;
+
+CREATE TABLE IF NOT EXISTS hc900.live_kpi (
+ column_id text NOT NULL, kpi text NOT NULL, window_start date NOT NULL,
+ value double precision, unit text, state text, excluded_min int, status text,
+ updated_at timestamptz NOT NULL DEFAULT now(),
+ PRIMARY KEY (column_id, kpi, window_start));
diff --git a/src/Hc900Crawler/Controllers/ReportController.cs b/src/Hc900Crawler/Controllers/ReportController.cs
index d9d0460..d35ab3f 100644
--- a/src/Hc900Crawler/Controllers/ReportController.cs
+++ b/src/Hc900Crawler/Controllers/ReportController.cs
@@ -1,7 +1,10 @@
+using System.Data;
using Hc900Crawler.Core.Application.DTOs;
using Hc900Crawler.Core.Application.Interfaces;
+using Hc900Crawler.Infrastructure.Database;
using Hc900Crawler.Infrastructure.Reporting;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
namespace Hc900Crawler.Web.Controllers;
@@ -13,14 +16,42 @@ public class ReportController : ControllerBase
private readonly ReportFillService _fill;
private readonly IReportTemplateStore _store;
private readonly ReportColumnMap _map;
+ private readonly Hc900DbContext _db;
// 웹 대시보드 기본 메트릭 세트
private static readonly string[] SUMMARY_METRICS =
{ "production_total", "yield_qv", "energy_intensity_qv", "mass_balance_closure", "control_residual" };
public ReportController(IReportMetricService metrics, ReportFillService fill,
- IReportTemplateStore store, ReportColumnMap map)
- { _metrics = metrics; _fill = fill; _store = store; _map = map; }
+ IReportTemplateStore store, ReportColumnMap map, Hc900DbContext db)
+ { _metrics = metrics; _fill = fill; _store = store; _map = map; _db = db; }
+
+ /// 온라인 KPI(live_kpi) 직독 — 누적기가 history_1s에서 갱신한 당일 실시간 값.
+ [HttpGet("live")]
+ public async Task Live(string? column = null, CancellationToken ct = default)
+ {
+ 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
+ FROM hc900.live_kpi" + (column == null ? "" : " WHERE column_id=@col") +
+ " ORDER BY column_id, kpi";
+ if (column != null) { var p = cmd.CreateParameter(); p.ParameterName = "@col"; p.Value = column; cmd.Parameters.Add(p); }
+ var items = new List