Files
QuantEngineByItz/src/dotnet/QuantEngine.Web/Client/Pages/DataCollectionMonitoring.razor
T
kjh2064 055b7b3082
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 7s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 13s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Successful in 2m19s
fix: 코드리뷰 즉시수정 4건 + 모니터링 실제 API 연동
[1] NavMenu.razor — 하드코딩 'v2.1.0-Release' 버전 블록 완전 제거
    버전 표시는 MainLayout의 version.json 단일 소스로 통일

[2] MainLayout.razor — 로그아웃 URL 버그 수정
    /Account/Login?handler=Logout → /Account/Login
    (Razor Pages GET 핸들러는 쿼리스트링 ?handler=로 호출되지 않음)

[3] Dashboard.razor — AllowAnonymous 제거, debug 코드 정리
    - @attribute [AllowAnonymous] 삭제
    - DEBUG MARKER div 삭제
    - TEMPORARY 주석·Console.WriteLine 정리
    - 미인증 시 /Account/Login 리다이렉트 활성화

[4] DataCollectionMonitoring.razor — 전체 하드코딩 더미 데이터 제거
    - 'RUN-2026-07-05-002 진행중 30분+' 등 모든 더미 데이터 제거
    - /api/collection/runs + /api/collection/state 실제 API 연동
    - 로딩 스피너, 새로고침 버튼, 실제 상태 카운트 구현
2026-07-06 17:59:31 +09:00

230 lines
9.9 KiB
Plaintext

@page "/monitoring"
@attribute [Authorize]
@inject HttpClient Http
@inject ISnackbar Snackbar
<PageTitle>QuantEngine - 데이터 수집 모니터링</PageTitle>
<!-- Page Header -->
<div class="mb-6">
<div class="d-flex justify-content-between align-items-center">
<div>
<MudText Typo="Typo.h4" Class="mb-2">데이터 수집 모니터링</MudText>
<MudText Typo="Typo.body1" Class="text-muted">실시간 수집 작업 상태 및 에러 추적</MudText>
</div>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Size="Size.Small"
OnClick="RefreshAsync" Disabled="_loading">
<MudIcon Icon="@Icons.Material.Filled.Refresh" Size="Size.Small" Class="mr-2" />
새로고침
</MudButton>
</div>
</div>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="mb-4" />
}
<!-- Collection Status Cards -->
<MudGrid Spacing="3" Class="mb-6">
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-2">진행 중인 작업</MudText>
<MudText Typo="Typo.h5">@_runningCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-2">완료</MudText>
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-success);">@_completedCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-2">실패</MudText>
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-error);">@_failedCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption" Class="text-muted mb-2">총 스냅샷</MudText>
<MudText Typo="Typo.h5">@_totalSnapshots</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Tabs -->
<MudTabs Outlined="true" Class="mb-6">
<!-- Recent Runs -->
<MudTabPanel Text="최근 실행">
<div class="py-4">
<MudPaper Class="pa-4" Elevation="1">
@if (_recentRuns.Count == 0 && !_loading)
{
<MudAlert Severity="Severity.Info">최근 실행 기록이 없습니다.</MudAlert>
}
else
{
<MudTable Items="@_recentRuns" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>실행 ID</MudTh>
<MudTh>시작 시간</MudTh>
<MudTh>종료 시간</MudTh>
<MudTh>상태</MudTh>
<MudTh>스냅샷</MudTh>
<MudTh>에러</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Run ID">
<MudText Typo="Typo.body2" Class="font-monospace">@context.RunId</MudText>
</MudTd>
<MudTd DataLabel="Start">
<MudText Typo="Typo.body2">@FormatTime(context.StartedAt)</MudText>
</MudTd>
<MudTd DataLabel="End">
<MudText Typo="Typo.body2">@(string.IsNullOrEmpty(context.FinishedAt) ? "-" : FormatTime(context.FinishedAt))</MudText>
</MudTd>
<MudTd DataLabel="Status">
<MudChip T="string" Label="true" Size="Size.Small"
Color="@GetStatusColor(context.Status)"
Variant="Variant.Filled">
@context.Status
</MudChip>
</MudTd>
<MudTd DataLabel="Snapshots">
<MudText Typo="Typo.body2">@(context.TotalSnapshots?.ToString() ?? "-")</MudText>
</MudTd>
<MudTd DataLabel="Errors">
@if (context.TotalErrors > 0)
{
<MudChip T="string" Label="true" Size="Size.Small" Color="Color.Error" Variant="Variant.Outlined">
@context.TotalErrors
</MudChip>
}
else
{
<MudText Typo="Typo.body2">-</MudText>
}
</MudTd>
</RowTemplate>
</MudTable>
}
</MudPaper>
</div>
</MudTabPanel>
<!-- Error Logs -->
<MudTabPanel Text="에러 로그">
<div class="py-4">
<MudPaper Class="pa-4" Elevation="1">
@if (_errors.Count == 0 && !_loading)
{
<MudAlert Severity="Severity.Success">에러가 없습니다.</MudAlert>
}
else
{
<MudStack Spacing="2">
@foreach (var error in _errors)
{
<div class="pa-3" style="border-left: 3px solid #f44336; background-color: var(--mud-palette-surface);">
<div class="d-flex justify-content-between align-items-start mb-2">
<MudText Typo="Typo.body2" Class="font-weight-500">[@error.ErrorKind] @error.ErrorMessage</MudText>
<MudText Typo="Typo.caption" Class="text-muted">@FormatTime(error.CreatedAt)</MudText>
</div>
<MudText Typo="Typo.caption" Class="text-muted">Run: @error.RunId</MudText>
@if (!string.IsNullOrEmpty(error.Ticker))
{
<MudText Typo="Typo.caption" Class="text-muted ml-3">Ticker: @error.Ticker</MudText>
}
</div>
}
</MudStack>
}
</MudPaper>
</div>
</MudTabPanel>
</MudTabs>
@code {
private bool _loading = false;
private int _runningCount;
private int _completedCount;
private int _failedCount;
private int _totalSnapshots;
private List<CollectionRunDto> _recentRuns = new();
private List<CollectionErrorDto> _errors = new();
protected override async Task OnInitializedAsync()
{
await RefreshAsync();
}
private async Task RefreshAsync()
{
_loading = true;
StateHasChanged();
try
{
// 최근 실행 목록 로드
var runsResponse = await Http.GetFromJsonAsync<CollectionRunsResponse>("api/collection/runs?limit=20");
if (runsResponse?.Runs is not null)
{
_recentRuns = runsResponse.Runs;
_runningCount = _recentRuns.Count(r => string.Equals(r.Status, "running", StringComparison.OrdinalIgnoreCase));
_completedCount = _recentRuns.Count(r => string.Equals(r.Status, "completed", StringComparison.OrdinalIgnoreCase)
|| string.Equals(r.Status, "PASS", StringComparison.OrdinalIgnoreCase));
_failedCount = _recentRuns.Count(r => string.Equals(r.Status, "failed", StringComparison.OrdinalIgnoreCase)
|| string.Equals(r.Status, "error", StringComparison.OrdinalIgnoreCase));
_totalSnapshots = _recentRuns.Sum(r => r.TotalSnapshots ?? 0);
}
// 대시보드 상태 로드 (전체 오류 목록)
var state = await Http.GetFromJsonAsync<CollectionDashboardStateDto>("api/collection/state");
if (state?.RecentErrors is not null)
{
_errors = state.RecentErrors;
}
}
catch (Exception ex)
{
Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private Color GetStatusColor(string status) => status?.ToLowerInvariant() switch
{
"running" => Color.Info,
"completed" => Color.Success,
"pass" => Color.Success,
"failed" => Color.Error,
"error" => Color.Error,
_ => Color.Warning
};
private string FormatTime(string? isoTime)
{
if (string.IsNullOrEmpty(isoTime)) return "-";
return DateTimeOffset.TryParse(isoTime, out var dt)
? dt.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss")
: isoTime;
}
// DTOs (shared with ApiClient)
private record CollectionRunsResponse(List<CollectionRunDto> Runs, int Count);
private record CollectionRunDto(
string RunId, string Status, string StartedAt,
string? FinishedAt, int? TotalSnapshots, int? TotalErrors);
private record CollectionDashboardStateDto(
string? LastRunId, string? LastRunStatus, string? LastFinishedAt,
int TotalSnapshots, int TotalErrors, List<CollectionErrorDto> RecentErrors);
private record CollectionErrorDto(
string RunId, string SourceName, string ErrorKind,
string ErrorMessage, string? Ticker, string CreatedAt);
}