fix: 코드리뷰 즉시수정 4건 + 모니터링 실제 API 연동
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
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
[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 연동
- 로딩 스피너, 새로고침 버튼, 실제 상태 카운트 구현
This commit is contained in:
@@ -133,7 +133,7 @@
|
|||||||
{
|
{
|
||||||
var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider;
|
var customProvider = (CustomAuthenticationStateProvider)AuthStateProvider;
|
||||||
await customProvider.LogoutFromServerAsync();
|
await customProvider.LogoutFromServerAsync();
|
||||||
NavigationManager.NavigateTo("/Account/Login?handler=Logout", forceLoad: true);
|
NavigationManager.NavigateTo("/Account/Login", forceLoad: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetFirstLetter(string? name)
|
private string GetFirstLetter(string? name)
|
||||||
|
|||||||
@@ -15,13 +15,4 @@
|
|||||||
<MudNavLink Href="/operations" Icon="@Icons.Material.Filled.PlaylistPlay" Match="NavLinkMatch.Prefix">
|
<MudNavLink Href="/operations" Icon="@Icons.Material.Filled.PlaylistPlay" Match="NavLinkMatch.Prefix">
|
||||||
운영 리포트
|
운영 리포트
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<MudDivider Class="my-2" />
|
|
||||||
|
|
||||||
<!-- System Version Status -->
|
|
||||||
<div class="px-4 py-2">
|
|
||||||
<MudText Typo="Typo.caption" Class="text-muted d-block">QuantEngine System</MudText>
|
|
||||||
<MudText Typo="Typo.caption" Class="text-muted">v2.1.0-Release</MudText>
|
|
||||||
</div>
|
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@page "/dashboard"
|
@page "/dashboard"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveWebAssembly
|
||||||
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
||||||
@using QuantEngine.Core.Infrastructure
|
@using QuantEngine.Core.Infrastructure
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
<PageTitle>QuantEngine - Admin Dashboard</PageTitle>
|
<PageTitle>QuantEngine - Admin Dashboard</PageTitle>
|
||||||
|
|
||||||
<!-- 🎯 DEBUG MARKER: DASHBOARD_RENDERING -->
|
|
||||||
<div id="dashboard-debug-marker" style="display:none;">DASHBOARD_RENDERING_ACTIVE</div>
|
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
@@ -244,26 +243,15 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// TEMPORARY: Disable auth check to verify dashboard loads
|
|
||||||
Console.WriteLine("[Dashboard] Page loading (auth check disabled for testing)");
|
|
||||||
|
|
||||||
// Check authentication
|
|
||||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||||
Console.WriteLine($"[Dashboard] Auth state: IsAuthenticated={authState.User.Identity?.IsAuthenticated}, Name={authState.User.Identity?.Name}");
|
if (!(authState.User.Identity?.IsAuthenticated ?? false))
|
||||||
|
{
|
||||||
// TEMPORARY: Skip redirect, allow page to load
|
NavManager.NavigateTo("/Account/Login", forceLoad: true);
|
||||||
// if (!authState.User.Identity?.IsAuthenticated ?? true)
|
return;
|
||||||
// {
|
}
|
||||||
// Console.WriteLine("[Dashboard] Not authenticated. Redirecting to login...");
|
|
||||||
// NavManager.NavigateTo("/login.html", forceLoad: true);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
Console.WriteLine($"[Dashboard] Page allowed to load");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Load operational report
|
|
||||||
var report = await Http.GetFromJsonAsync<OperationalReportData>("api/operational-report");
|
var report = await Http.GetFromJsonAsync<OperationalReportData>("api/operational-report");
|
||||||
if (report != null)
|
if (report != null)
|
||||||
{
|
{
|
||||||
@@ -276,7 +264,6 @@
|
|||||||
// Handle error silently
|
// Handle error silently
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load recent activities
|
|
||||||
LoadRecentActivities();
|
LoadRecentActivities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,54 @@
|
|||||||
@page "/monitoring"
|
@page "/monitoring"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
<PageTitle>QuantEngine - 데이터 수집 모니터링</PageTitle>
|
<PageTitle>QuantEngine - 데이터 수집 모니터링</PageTitle>
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="mb-6">
|
<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.h4" Class="mb-2">데이터 수집 모니터링</MudText>
|
||||||
<MudText Typo="Typo.body1" Class="text-muted">실시간 수집 작업 상태 및 에러 추적</MudText>
|
<MudText Typo="Typo.body1" Class="text-muted">실시간 수집 작업 상태 및 에러 추적</MudText>
|
||||||
</div>
|
</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 -->
|
<!-- Collection Status Cards -->
|
||||||
<MudGrid Spacing="3" Class="mb-6">
|
<MudGrid Spacing="3" Class="mb-6">
|
||||||
<MudItem xs="12" sm="6" md="3">
|
<MudItem xs="12" sm="6" md="3">
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
|
<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.caption" Class="text-muted mb-2">진행 중인 작업</MudText>
|
||||||
<MudText Typo="Typo.h5">@RunningCount</MudText>
|
<MudText Typo="Typo.h5">@_runningCount</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="3">
|
<MudItem xs="12" sm="6" md="3">
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
|
<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.caption" Class="text-muted mb-2">완료</MudText>
|
||||||
<MudText Typo="Typo.h5" Class="text-success">@CompletedCount</MudText>
|
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-success);">@_completedCount</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="3">
|
<MudItem xs="12" sm="6" md="3">
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
|
<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.caption" Class="text-muted mb-2">실패</MudText>
|
||||||
<MudText Typo="Typo.h5" Class="text-error">@FailedCount</MudText>
|
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-error);">@_failedCount</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="3">
|
<MudItem xs="12" sm="6" md="3">
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="border: 1px solid var(--mud-palette-divider);">
|
<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.caption" Class="text-muted mb-2">총 스냅샷</MudText>
|
||||||
<MudText Typo="Typo.h5" Class="text-warning">@PendingCount</MudText>
|
<MudText Typo="Typo.h5">@_totalSnapshots</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
@@ -44,30 +59,30 @@
|
|||||||
<MudTabPanel Text="최근 실행">
|
<MudTabPanel Text="최근 실행">
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
@if (RecentRuns.Count == 0)
|
@if (_recentRuns.Count == 0 && !_loading)
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Info">최근 실행 기록이 없습니다.</MudAlert>
|
<MudAlert Severity="Severity.Info">최근 실행 기록이 없습니다.</MudAlert>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudTable Items="@RecentRuns" Dense="true" Hover="true" Striped="true">
|
<MudTable Items="@_recentRuns" Dense="true" Hover="true" Striped="true">
|
||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<MudTh>실행 ID</MudTh>
|
<MudTh>실행 ID</MudTh>
|
||||||
<MudTh>시작 시간</MudTh>
|
<MudTh>시작 시간</MudTh>
|
||||||
<MudTh>종료 시간</MudTh>
|
<MudTh>종료 시간</MudTh>
|
||||||
<MudTh>상태</MudTh>
|
<MudTh>상태</MudTh>
|
||||||
<MudTh>수집된 항목</MudTh>
|
<MudTh>스냅샷</MudTh>
|
||||||
<MudTh>작업</MudTh>
|
<MudTh>에러</MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd DataLabel="Run ID">
|
<MudTd DataLabel="Run ID">
|
||||||
<MudText Typo="Typo.body2" Class="font-monospace">@context.RunId</MudText>
|
<MudText Typo="Typo.body2" Class="font-monospace">@context.RunId</MudText>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd DataLabel="Start">
|
<MudTd DataLabel="Start">
|
||||||
<MudText Typo="Typo.body2">@context.StartTime.ToString("yyyy-MM-dd HH:mm:ss")</MudText>
|
<MudText Typo="Typo.body2">@FormatTime(context.StartedAt)</MudText>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd DataLabel="End">
|
<MudTd DataLabel="End">
|
||||||
<MudText Typo="Typo.body2">@(context.EndTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-")</MudText>
|
<MudText Typo="Typo.body2">@(string.IsNullOrEmpty(context.FinishedAt) ? "-" : FormatTime(context.FinishedAt))</MudText>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd DataLabel="Status">
|
<MudTd DataLabel="Status">
|
||||||
<MudChip T="string" Label="true" Size="Size.Small"
|
<MudChip T="string" Label="true" Size="Size.Small"
|
||||||
@@ -76,14 +91,20 @@
|
|||||||
@context.Status
|
@context.Status
|
||||||
</MudChip>
|
</MudChip>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd DataLabel="Items">
|
<MudTd DataLabel="Snapshots">
|
||||||
<MudText Typo="Typo.body2">@context.ItemCount</MudText>
|
<MudText Typo="Typo.body2">@(context.TotalSnapshots?.ToString() ?? "-")</MudText>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd DataLabel="Actions">
|
<MudTd DataLabel="Errors">
|
||||||
<MudButton Variant="Variant.Text" Size="Size.Small" Color="Color.Primary"
|
@if (context.TotalErrors > 0)
|
||||||
OnClick="@(() => ViewRunDetails(context))">
|
{
|
||||||
상세
|
<MudChip T="string" Label="true" Size="Size.Small" Color="Color.Error" Variant="Variant.Outlined">
|
||||||
</MudButton>
|
@context.TotalErrors
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2">-</MudText>
|
||||||
|
}
|
||||||
</MudTd>
|
</MudTd>
|
||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
</MudTable>
|
</MudTable>
|
||||||
@@ -96,196 +117,113 @@
|
|||||||
<MudTabPanel Text="에러 로그">
|
<MudTabPanel Text="에러 로그">
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
@if (Errors.Count == 0)
|
@if (_errors.Count == 0 && !_loading)
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Success">에러가 없습니다.</MudAlert>
|
<MudAlert Severity="Severity.Success">에러가 없습니다.</MudAlert>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudStack Spacing="2">
|
<MudStack Spacing="2">
|
||||||
@foreach (var error in Errors)
|
@foreach (var error in _errors)
|
||||||
{
|
{
|
||||||
<div class="pa-3" style="border-left: 3px solid #f44336; background-color: var(--mud-palette-surface);">
|
<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">
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
<MudText Typo="Typo.body2" Class="font-weight-500">@error.Message</MudText>
|
<MudText Typo="Typo.body2" Class="font-weight-500">[@error.ErrorKind] @error.ErrorMessage</MudText>
|
||||||
<MudText Typo="Typo.caption" Class="text-muted">@error.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")</MudText>
|
<MudText Typo="Typo.caption" Class="text-muted">@FormatTime(error.CreatedAt)</MudText>
|
||||||
</div>
|
</div>
|
||||||
<MudText Typo="Typo.caption" Class="text-muted">Run ID: @error.RunId</MudText>
|
<MudText Typo="Typo.caption" Class="text-muted">Run: @error.RunId</MudText>
|
||||||
<MudText Typo="Typo.caption" Class="text-muted mt-1">@error.StackTrace</MudText>
|
@if (!string.IsNullOrEmpty(error.Ticker))
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
</MudPaper>
|
|
||||||
</div>
|
|
||||||
</MudTabPanel>
|
|
||||||
|
|
||||||
<!-- Collection Status -->
|
|
||||||
<MudTabPanel Text="수집 상태">
|
|
||||||
<div class="py-4">
|
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
|
||||||
<MudStack Spacing="3">
|
|
||||||
@foreach (var ticker in CollectionStatus)
|
|
||||||
{
|
{
|
||||||
<div class="pa-3" style="border-bottom: 1px solid var(--mud-palette-divider);">
|
<MudText Typo="Typo.caption" Class="text-muted ml-3">Ticker: @error.Ticker</MudText>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
}
|
||||||
<MudText Typo="Typo.body2" Class="font-weight-500">@ticker.Ticker</MudText>
|
|
||||||
<MudChip T="string" Label="true" Size="Size.Small"
|
|
||||||
Color="@(ticker.IsSuccessful ? Color.Success : Color.Warning)"
|
|
||||||
Variant="Variant.Filled">
|
|
||||||
@(ticker.IsSuccessful ? "성공" : "실패")
|
|
||||||
</MudChip>
|
|
||||||
</div>
|
|
||||||
<MudText Typo="Typo.caption" Class="text-muted">
|
|
||||||
마지막 수집: @ticker.LastCollectionTime.ToString("yyyy-MM-dd HH:mm:ss")
|
|
||||||
</MudText>
|
|
||||||
<MudText Typo="Typo.caption" Class="text-muted">
|
|
||||||
데이터 포인트: @(ticker.DataPointCount)개
|
|
||||||
</MudText>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</div>
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
</MudTabs>
|
</MudTabs>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
// Status counts
|
private bool _loading = false;
|
||||||
private int RunningCount = 2;
|
private int _runningCount;
|
||||||
private int CompletedCount = 156;
|
private int _completedCount;
|
||||||
private int FailedCount = 8;
|
private int _failedCount;
|
||||||
private int PendingCount = 5;
|
private int _totalSnapshots;
|
||||||
|
|
||||||
// Recent runs
|
private List<CollectionRunDto> _recentRuns = new();
|
||||||
private List<RunModel> RecentRuns = new();
|
private List<CollectionErrorDto> _errors = new();
|
||||||
|
|
||||||
// Errors
|
|
||||||
private List<ErrorModel> Errors = new();
|
|
||||||
|
|
||||||
// Collection status
|
|
||||||
private List<CollectionStatusModel> CollectionStatus = new();
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await LoadData();
|
await RefreshAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadData()
|
private async Task RefreshAsync()
|
||||||
{
|
{
|
||||||
// Load recent runs
|
_loading = true;
|
||||||
RecentRuns = new List<RunModel>
|
StateHasChanged();
|
||||||
{
|
|
||||||
new RunModel
|
|
||||||
{
|
|
||||||
RunId = "RUN-2026-07-05-001",
|
|
||||||
StartTime = DateTime.Now.AddMinutes(-45),
|
|
||||||
EndTime = DateTime.Now.AddMinutes(-40),
|
|
||||||
Status = "완료",
|
|
||||||
ItemCount = 142
|
|
||||||
},
|
|
||||||
new RunModel
|
|
||||||
{
|
|
||||||
RunId = "RUN-2026-07-05-002",
|
|
||||||
StartTime = DateTime.Now.AddMinutes(-30),
|
|
||||||
EndTime = null,
|
|
||||||
Status = "진행 중",
|
|
||||||
ItemCount = 87
|
|
||||||
},
|
|
||||||
new RunModel
|
|
||||||
{
|
|
||||||
RunId = "RUN-2026-07-04-012",
|
|
||||||
StartTime = DateTime.Now.AddHours(-8).AddMinutes(-15),
|
|
||||||
EndTime = DateTime.Now.AddHours(-8).AddMinutes(-5),
|
|
||||||
Status = "완료",
|
|
||||||
ItemCount = 189
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load errors
|
try
|
||||||
Errors = new List<ErrorModel>
|
|
||||||
{
|
{
|
||||||
new ErrorModel
|
// 최근 실행 목록 로드
|
||||||
|
var runsResponse = await Http.GetFromJsonAsync<CollectionRunsResponse>("api/collection/runs?limit=20");
|
||||||
|
if (runsResponse?.Runs is not null)
|
||||||
{
|
{
|
||||||
RunId = "RUN-2026-07-04-011",
|
_recentRuns = runsResponse.Runs;
|
||||||
Message = "API Rate Limit Exceeded",
|
_runningCount = _recentRuns.Count(r => string.Equals(r.Status, "running", StringComparison.OrdinalIgnoreCase));
|
||||||
StackTrace = "Exception at CollectionService.FetchData()",
|
_completedCount = _recentRuns.Count(r => string.Equals(r.Status, "completed", StringComparison.OrdinalIgnoreCase)
|
||||||
Timestamp = DateTime.Now.AddHours(-2)
|
|| string.Equals(r.Status, "PASS", StringComparison.OrdinalIgnoreCase));
|
||||||
},
|
_failedCount = _recentRuns.Count(r => string.Equals(r.Status, "failed", StringComparison.OrdinalIgnoreCase)
|
||||||
new ErrorModel
|
|| string.Equals(r.Status, "error", StringComparison.OrdinalIgnoreCase));
|
||||||
{
|
_totalSnapshots = _recentRuns.Sum(r => r.TotalSnapshots ?? 0);
|
||||||
RunId = "RUN-2026-07-03-015",
|
|
||||||
Message = "Connection Timeout",
|
|
||||||
StackTrace = "Exception at HttpClient.GetAsync()",
|
|
||||||
Timestamp = DateTime.Now.AddHours(-5)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load collection status
|
|
||||||
CollectionStatus = new List<CollectionStatusModel>
|
|
||||||
{
|
|
||||||
new CollectionStatusModel
|
|
||||||
{
|
|
||||||
Ticker = "005930",
|
|
||||||
IsSuccessful = true,
|
|
||||||
LastCollectionTime = DateTime.Now.AddMinutes(-2),
|
|
||||||
DataPointCount = 1450
|
|
||||||
},
|
|
||||||
new CollectionStatusModel
|
|
||||||
{
|
|
||||||
Ticker = "000660",
|
|
||||||
IsSuccessful = true,
|
|
||||||
LastCollectionTime = DateTime.Now.AddMinutes(-5),
|
|
||||||
DataPointCount = 1203
|
|
||||||
},
|
|
||||||
new CollectionStatusModel
|
|
||||||
{
|
|
||||||
Ticker = "051910",
|
|
||||||
IsSuccessful = false,
|
|
||||||
LastCollectionTime = DateTime.Now.AddHours(-1),
|
|
||||||
DataPointCount = 945
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color GetStatusColor(string status) => status switch
|
// 대시보드 상태 로드 (전체 오류 목록)
|
||||||
|
var state = await Http.GetFromJsonAsync<CollectionDashboardStateDto>("api/collection/state");
|
||||||
|
if (state?.RecentErrors is not null)
|
||||||
{
|
{
|
||||||
"완료" => Color.Success,
|
_errors = state.RecentErrors;
|
||||||
"진행 중" => Color.Info,
|
}
|
||||||
"실패" => Color.Error,
|
}
|
||||||
|
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
|
_ => Color.Warning
|
||||||
};
|
};
|
||||||
|
|
||||||
private async Task ViewRunDetails(RunModel run)
|
private string FormatTime(string? isoTime)
|
||||||
{
|
{
|
||||||
// View details dialog
|
if (string.IsNullOrEmpty(isoTime)) return "-";
|
||||||
await Task.CompletedTask;
|
return DateTimeOffset.TryParse(isoTime, out var dt)
|
||||||
|
? dt.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
: isoTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RunModel
|
// DTOs (shared with ApiClient)
|
||||||
{
|
private record CollectionRunsResponse(List<CollectionRunDto> Runs, int Count);
|
||||||
public string RunId { get; set; }
|
private record CollectionRunDto(
|
||||||
public DateTime StartTime { get; set; }
|
string RunId, string Status, string StartedAt,
|
||||||
public DateTime? EndTime { get; set; }
|
string? FinishedAt, int? TotalSnapshots, int? TotalErrors);
|
||||||
public string Status { get; set; }
|
private record CollectionDashboardStateDto(
|
||||||
public int ItemCount { get; set; }
|
string? LastRunId, string? LastRunStatus, string? LastFinishedAt,
|
||||||
}
|
int TotalSnapshots, int TotalErrors, List<CollectionErrorDto> RecentErrors);
|
||||||
|
private record CollectionErrorDto(
|
||||||
private class ErrorModel
|
string RunId, string SourceName, string ErrorKind,
|
||||||
{
|
string ErrorMessage, string? Ticker, string CreatedAt);
|
||||||
public string RunId { get; set; }
|
|
||||||
public string Message { get; set; }
|
|
||||||
public string StackTrace { get; set; }
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CollectionStatusModel
|
|
||||||
{
|
|
||||||
public string Ticker { get; set; }
|
|
||||||
public bool IsSuccessful { get; set; }
|
|
||||||
public DateTime LastCollectionTime { get; set; }
|
|
||||||
public int DataPointCount { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user