Files
HC900-Crawler/src/Hc900Crawler/Controllers/ReportController.cs
windpacer 30a3286d35 feat(report): 적산(.QV) 정확 메트릭 + 물질수지 폐합 + 웹 대시보드
PV-중앙값 근사 대신 유량 적산(.QV)으로 정확한 적분 메트릭 추가, 웹에서 바로 보기.

- QV 메트릭 4종: production_total·yield_qv·energy_intensity_qv·mass_balance_closure.
  적산 Δ 헬퍼(QvDeltaAsync)는 값 급감(reset/wrap)으로 구간분할 후 구간별 (마지막-처음)
  합산 → gap·양자화 강건, DCS 일일적산과 동일 의미(노이즈 과대계상 없음).
- 매핑은 appsettings SteamAdvisor:Columns 재사용(.QV 치환, 7컬럼). 폐합 스트림은
  Report:Closure:C-6111(Feed + Outputs[제품·경비물·중비물]).
- 웹 대시보드: GET /api/report/columns, /api/report/summary →
  리포트 탭에서 컬럼·날짜 선택 시 물질수지 신뢰블록(IN/OUT/폐합%, 98~101% 색상)
  + 메트릭 표를 즉시 렌더. 엑셀 export와 병존.
- 물리적 타당성 게이트: 수율>1.5·에너지원단위>5·≤0 → no_data+사유(garbage 차단).

검증(2026-05-15 C-6111): 생산 8455kg·수율 0.8739·에너지 0.7791·폐합 99.04%
(feed 9675/out 9582), 17주 폐합 99.1~99.8%. C-9111 garbage 수율 → no_data 처리 확인.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 07:54:34 +09:00

79 lines
3.6 KiB
C#

using Hc900Crawler.Core.Application.DTOs;
using Hc900Crawler.Core.Application.Interfaces;
using Hc900Crawler.Infrastructure.Reporting;
using Microsoft.AspNetCore.Mvc;
namespace Hc900Crawler.Web.Controllers;
[ApiController]
[Route("api/report")]
public class ReportController : ControllerBase
{
private readonly IReportMetricService _metrics;
private readonly ReportFillService _fill;
private readonly IReportTemplateStore _store;
private readonly ReportColumnMap _map;
// 웹 대시보드 기본 메트릭 세트
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; }
/// <summary>설정된 컬럼 목록(웹 UI 셀렉트용).</summary>
[HttpGet("columns")]
public IActionResult Columns()
=> Ok(_map.Columns().Select(c => new { Column = c, HasClosure = _map.HasClosure(c) }));
/// <summary>단건 메트릭(미리보기/디버그).</summary>
[HttpPost("metric")]
public async Task<IActionResult> Metric([FromBody] MetricRequestDto req, CancellationToken ct)
=> Ok(await _metrics.ComputeAsync(req, ct));
/// <summary>웹에서 바로 보기 — 한 컬럼·날짜의 전 메트릭을 한 번에.</summary>
[HttpGet("summary")]
public async Task<IActionResult> Summary(string column = "C-6111", DateTime? date = null,
string source = "history_table", int? sessionId = null, CancellationToken ct = default)
{
var d = (date ?? DateTime.UtcNow.AddHours(9).AddDays(-1)).Date; // 기본 = 어제(KST)
var results = new List<MetricResultDto>();
foreach (var m in SUMMARY_METRICS)
results.Add(await _metrics.ComputeAsync(new MetricRequestDto
{
Column = column, Metric = m, PeriodDateKst = d,
SourceTable = source, SessionId = sessionId
}, ct));
return Ok(new { Column = column, Date = d.ToString("yyyy-MM-dd"), Source = source, Metrics = results });
}
/// <summary>엑셀 템플릿 등록.</summary>
[HttpPost("template")]
public async Task<IActionResult> Upload([FromForm] IFormFile file, [FromForm] string name,
[FromForm] string? owner, CancellationToken ct)
{
if (file == null || file.Length == 0) return BadRequest(new { Error = "파일 없음" });
using var ms = new MemoryStream();
await file.CopyToAsync(ms, ct);
var id = await _store.CreateAsync(name, owner, ms.ToArray(), ct);
return Ok(new { Id = id });
}
/// <summary>★템플릿+날짜 → 채워진 xlsx 다운로드.</summary>
[HttpGet("generate")]
public async Task<IActionResult> Generate(int templateId, DateTime date,
string source = "history_table", int? sessionId = null, CancellationToken ct = default)
{
var tpl = await _store.GetBlobAsync(templateId, ct);
if (tpl == null) return NotFound(new { Error = $"템플릿 {templateId} 없음" });
var (xlsx, cells, status) = await _fill.FillAsync(tpl, date, source, sessionId, ct);
await _store.RecordRunAsync(templateId, "DAILY", date, source, status, cells, xlsx, ct);
Response.Headers["X-Report-Status"] = status;
var fname = $"report_{templateId}_{date:yyyyMMdd}.xlsx";
return File(xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fname);
}
}