Add client log filters and dashboard links
TaxBaik CI/CD / build-and-deploy (push) Successful in 4m52s
TaxBaik CI/CD / build-and-deploy (push) Successful in 4m52s
This commit is contained in:
@@ -14,6 +14,16 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="admin-simple-card">
|
<div class="admin-simple-card">
|
||||||
|
<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>
|
||||||
|
<button type="button" class="mud-button-root" @onclick="LoadAsync">새로고침</button>
|
||||||
|
</div>
|
||||||
@if (_loading)
|
@if (_loading)
|
||||||
{
|
{
|
||||||
<p>불러오는 중...</p>
|
<p>불러오는 중...</p>
|
||||||
@@ -45,12 +55,23 @@
|
|||||||
@code {
|
@code {
|
||||||
private readonly List<ClientLogDto> _entries = [];
|
private readonly List<ClientLogDto> _entries = [];
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
|
private string _query = "";
|
||||||
|
private string _level = "";
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync()
|
||||||
|
{
|
||||||
|
_loading = true;
|
||||||
|
_entries.Clear();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await Http.GetFromJsonAsync<List<ClientLogDto>>("/taxbaik/api/client-logs/recent");
|
var url = $"/taxbaik/api/client-logs/recent?q={Uri.EscapeDataString(_query ?? string.Empty)}&level={Uri.EscapeDataString(_level ?? string.Empty)}";
|
||||||
|
var result = await Http.GetFromJsonAsync<List<ClientLogDto>>(url);
|
||||||
if (result is not null)
|
if (result is not null)
|
||||||
{
|
{
|
||||||
_entries.AddRange(result);
|
_entries.AddRange(result);
|
||||||
@@ -62,6 +83,7 @@
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_loading = false;
|
_loading = false;
|
||||||
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,24 @@
|
|||||||
<header class="admin-simple-header">
|
<header class="admin-simple-header">
|
||||||
<p class="admin-simple-eyebrow">Home</p>
|
<p class="admin-simple-eyebrow">Home</p>
|
||||||
<h1 class="admin-simple-title">대시보드</h1>
|
<h1 class="admin-simple-title">대시보드</h1>
|
||||||
<p class="admin-simple-subtitle">관리자 대시보드 초기화 문제를 분리하는 최소 렌더 화면입니다.</p>
|
<p class="admin-simple-subtitle">핵심 운영 상태와 최근 오류를 한 번에 확인합니다.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="admin-simple-card">
|
<div class="admin-dashboard-grid">
|
||||||
<div>대시보드 본문을 단계적으로 복원하며 `get` interop 오류를 추적합니다.</div>
|
<article class="admin-simple-card">
|
||||||
<div>현재 화면은 JS 호출 없이 렌더만 수행합니다.</div>
|
<strong>오류 진단</strong>
|
||||||
|
<div>브라우저 오류와 JS interop 실패는 클라이언트 로그에서 바로 추적합니다.</div>
|
||||||
|
<a href="/taxbaik/admin/client-logs">클라이언트 로그 보기</a>
|
||||||
|
</article>
|
||||||
|
<article class="admin-simple-card">
|
||||||
|
<strong>운영 메뉴</strong>
|
||||||
|
<div>대시보드 복원 후 CRM, 홈페이지, 문의 관리 화면을 표준 템플릿으로 확장합니다.</div>
|
||||||
|
<a href="/taxbaik/admin/inquiries">문의 관리로 이동</a>
|
||||||
|
</article>
|
||||||
|
<article class="admin-simple-card">
|
||||||
|
<strong>배포 확인</strong>
|
||||||
|
<div>하위 경로 배포와 렌더러 분리를 유지하도록 라우트 린트를 적용합니다.</div>
|
||||||
|
<a href="/taxbaik/admin/client-logs">최근 에러 확인</a>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -18,12 +18,10 @@ public sealed class RecentClientLogEntry
|
|||||||
|
|
||||||
public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList<RecentClientLogEntry>>
|
public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList<RecentClientLogEntry>>
|
||||||
{
|
{
|
||||||
private readonly ILogger<GetRecentLogsEndpoint> _logger;
|
|
||||||
private readonly IWebHostEnvironment _environment;
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
public GetRecentLogsEndpoint(ILogger<GetRecentLogsEndpoint> logger, IWebHostEnvironment environment)
|
public GetRecentLogsEndpoint(IWebHostEnvironment environment)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +33,11 @@ public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList
|
|||||||
|
|
||||||
public override async Task HandleAsync(CancellationToken ct)
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
var level = Query<string>("level");
|
||||||
|
var source = Query<string>("source");
|
||||||
|
var route = Query<string>("route");
|
||||||
|
var term = Query<string>("q");
|
||||||
|
|
||||||
var logDir = Path.Combine(_environment.ContentRootPath, "logs");
|
var logDir = Path.Combine(_environment.ContentRootPath, "logs");
|
||||||
if (!Directory.Exists(logDir))
|
if (!Directory.Exists(logDir))
|
||||||
{
|
{
|
||||||
@@ -56,7 +59,9 @@ public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList
|
|||||||
if (!line.Contains("ClientLog ", StringComparison.OrdinalIgnoreCase))
|
if (!line.Contains("ClientLog ", StringComparison.OrdinalIgnoreCase))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
entries.Add(Parse(line));
|
var entry = Parse(line);
|
||||||
|
if (Matches(entry, level, source, route, term))
|
||||||
|
entries.Add(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +85,36 @@ public sealed class GetRecentLogsEndpoint : EndpointWithoutRequest<IReadOnlyList
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool Matches(RecentClientLogEntry entry, string? level, string? source, string? route, string? term)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(level) &&
|
||||||
|
!string.Equals(entry.Level, level, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(source) &&
|
||||||
|
!(entry.Source?.Contains(source, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(route) &&
|
||||||
|
!(entry.Route?.Contains(route, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(term) &&
|
||||||
|
!(entry.Message?.Contains(term, StringComparison.OrdinalIgnoreCase) ?? false) &&
|
||||||
|
!(entry.Source?.Contains(term, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static string? Extract(string text, string start, string end)
|
private static string? Extract(string text, string start, string end)
|
||||||
{
|
{
|
||||||
var index = text.IndexOf(start, StringComparison.OrdinalIgnoreCase);
|
var index = text.IndexOf(start, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
Reference in New Issue
Block a user