203 lines
7.5 KiB
Plaintext
203 lines
7.5 KiB
Plaintext
@page "/client-logs"
|
|
@page "/admin/client-logs"
|
|
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
|
@attribute [Authorize]
|
|
@using Microsoft.AspNetCore.Components.Web
|
|
@inject HttpClient Http
|
|
|
|
<PageTitle>클라이언트 로그</PageTitle>
|
|
|
|
<AdminPageHeader Title="클라이언트 로그" Eyebrow="Diagnostics" Subtitle="브라우저 오류와 JS interop 실패를 최근 순서로 확인합니다.">
|
|
</AdminPageHeader>
|
|
|
|
<section class="admin-simple-page">
|
|
|
|
<div class="admin-simple-card">
|
|
@if (_summary is not null)
|
|
{
|
|
<div class="admin-log-summary">
|
|
<div><strong>Total</strong> @_summary.Total</div>
|
|
<div><strong>Error</strong> @_summary.ErrorCount</div>
|
|
<div><strong>Warning</strong> @_summary.WarningCount</div>
|
|
<div><strong>Info</strong> @_summary.InfoCount</div>
|
|
</div>
|
|
<div class="admin-log-summary-breakdown">
|
|
<section>
|
|
<strong>Top Sources</strong>
|
|
<ul>
|
|
@foreach (var item in _summary.TopSources)
|
|
{
|
|
<li>@item.Key (@item.Value)</li>
|
|
}
|
|
</ul>
|
|
</section>
|
|
<section>
|
|
<strong>Top Routes</strong>
|
|
<ul>
|
|
@foreach (var item in _summary.TopRoutes)
|
|
{
|
|
<li>@item.Key (@item.Value)</li>
|
|
}
|
|
</ul>
|
|
</section>
|
|
</div>
|
|
}
|
|
<div class="admin-log-filter-row">
|
|
<input class="mud-input-slot" placeholder="검색어" @bind="_query" @bind:event="oninput" />
|
|
<select class="mud-input-slot" @bind="_level">
|
|
<option value="">전체 수준</option>
|
|
<option value="error">error</option>
|
|
<option value="warning">warning</option>
|
|
<option value="info">info</option>
|
|
</select>
|
|
<input class="mud-input-slot" placeholder="source" @bind="_source" @bind:event="oninput" />
|
|
<input class="mud-input-slot" placeholder="route" @bind="_route" @bind:event="oninput" />
|
|
<input class="mud-input-slot" placeholder="시작 ISO 시각" @bind="_start" @bind:event="oninput" />
|
|
<input class="mud-input-slot" placeholder="종료 ISO 시각" @bind="_end" @bind:event="oninput" />
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadAsync">새로고침</MudButton>
|
|
</div>
|
|
@if (_loading)
|
|
{
|
|
<p>불러오는 중...</p>
|
|
}
|
|
else if (_entries.Count == 0)
|
|
{
|
|
<p>최근 클라이언트 로그가 없습니다.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="admin-log-list">
|
|
@foreach (var entry in _entries)
|
|
{
|
|
<article class="admin-log-item">
|
|
<div class="admin-log-item-header">
|
|
<strong>@(entry.Level ?? "error")</strong>
|
|
<span>@entry.Timestamp</span>
|
|
<span>@entry.Source</span>
|
|
</div>
|
|
<p>@entry.Message</p>
|
|
<small>@entry.Route</small>
|
|
</article>
|
|
}
|
|
</div>
|
|
}
|
|
@if (_groups.Count > 0)
|
|
{
|
|
<details class="admin-log-group-details">
|
|
<summary>반복 오류 그룹 보기</summary>
|
|
<div class="admin-log-group-list">
|
|
@foreach (var group in _groups)
|
|
{
|
|
<article class="admin-log-group-item" role="button" tabindex="0" @onclick="() => ApplyGroupFilter(group)" @onkeydown="e => HandleGroupKeydown(e, group)">
|
|
<div class="admin-log-item-header">
|
|
<strong>@group.Count</strong>
|
|
<span>@group.Level</span>
|
|
<span>@group.Source</span>
|
|
<span>@group.Route</span>
|
|
</div>
|
|
<p>@group.Message</p>
|
|
<small>@group.LastSeen</small>
|
|
</article>
|
|
}
|
|
</div>
|
|
</details>
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
@code {
|
|
private readonly List<ClientLogDto> _entries = [];
|
|
private readonly List<ClientLogGroupDto> _groups = [];
|
|
private bool _loading = true;
|
|
private string _query = "";
|
|
private string _level = "";
|
|
private string _source = "";
|
|
private string _route = "";
|
|
private string _start = "";
|
|
private string _end = "";
|
|
private ClientLogSummaryDto? _summary;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadAsync();
|
|
}
|
|
|
|
private async Task LoadAsync()
|
|
{
|
|
_loading = true;
|
|
_entries.Clear();
|
|
_groups.Clear();
|
|
|
|
try
|
|
{
|
|
_summary = await Http.GetFromJsonAsync<ClientLogSummaryDto>("/api/client-logs/summary");
|
|
var url = $"/api/client-logs/recent?q={Uri.EscapeDataString(_query ?? string.Empty)}&level={Uri.EscapeDataString(_level ?? string.Empty)}&source={Uri.EscapeDataString(_source ?? string.Empty)}&route={Uri.EscapeDataString(_route ?? string.Empty)}&start={Uri.EscapeDataString(_start ?? string.Empty)}&end={Uri.EscapeDataString(_end ?? string.Empty)}";
|
|
var result = await Http.GetFromJsonAsync<List<ClientLogDto>>(url);
|
|
var grouped = await Http.GetFromJsonAsync<List<ClientLogGroupDto>>("/api/client-logs/groups");
|
|
if (result is not null)
|
|
{
|
|
_entries.AddRange(result);
|
|
}
|
|
if (grouped is not null)
|
|
{
|
|
_groups.AddRange(grouped);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task ApplyGroupFilter(ClientLogGroupDto group)
|
|
{
|
|
_query = group.Message ?? string.Empty;
|
|
_level = group.Level ?? string.Empty;
|
|
_source = group.Source ?? string.Empty;
|
|
_route = group.Route ?? string.Empty;
|
|
await LoadAsync();
|
|
}
|
|
|
|
private async Task HandleGroupKeydown(KeyboardEventArgs e, ClientLogGroupDto group)
|
|
{
|
|
if (e.Key is "Enter" or " ")
|
|
{
|
|
await ApplyGroupFilter(group);
|
|
}
|
|
}
|
|
|
|
private sealed class ClientLogDto
|
|
{
|
|
public string? Timestamp { get; set; }
|
|
public string? Level { get; set; }
|
|
public string? Source { get; set; }
|
|
public string? Message { get; set; }
|
|
public string? Route { get; set; }
|
|
}
|
|
|
|
private sealed class ClientLogSummaryDto
|
|
{
|
|
public int Total { get; set; }
|
|
public int ErrorCount { get; set; }
|
|
public int WarningCount { get; set; }
|
|
public int InfoCount { get; set; }
|
|
public List<KeyValuePair<string, int>> TopSources { get; set; } = [];
|
|
public List<KeyValuePair<string, int>> TopRoutes { get; set; } = [];
|
|
}
|
|
|
|
private sealed class ClientLogGroupDto
|
|
{
|
|
public string? Key { get; set; }
|
|
public string? Level { get; set; }
|
|
public string? Source { get; set; }
|
|
public string? Route { get; set; }
|
|
public string? Message { get; set; }
|
|
public int Count { get; set; }
|
|
public string? LastSeen { get; set; }
|
|
}
|
|
}
|