From 0334a5f6071cb3fbe11bfb722c0f46a83ac4c3c1 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 28 Jun 2026 10:47:29 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20Phase=204=20-=20Dashboard=20Blazor?= =?UTF-8?q?=20=E2=86=92=20API=20client=20(Service=20Locator=20=E2=86=92=20?= =?UTF-8?q?Dependency=20Injection)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Implementation:** - AdminDashboardClient: HTTP API client interface - GetSummaryAsync: Fetch dashboard metrics - GetUpcomingFilingsAsync: 30-day filings forecast - GetRecentInquiriesAsync: Latest inquiries - GetMonthlyStatsAsync: Monthly statistics - Program.cs: Register IAdminDashboardClient - Dashboard.razor: Replace service injection with API client - Remove: Direct AdminDashboardService/TaxFilingService injection - Add: IAdminDashboardClient injection - Add: Error handling & loading state - Change: OnInitializedAsync() calls API endpoints **SOLID Principles Applied:** ✓ D (Dependency Inversion): Blazor depends on IAdminDashboardClient abstraction ✓ S (Single Responsibility): Client handles only HTTP communication ✓ O (Open/Closed): Can extend API without changing Blazor component **Architecture Pattern:** - Before: Blazor → Service (server-side logic) → Repository → DB - After: Blazor → HTTP → API → Service → Repository → DB **Benefits:** - Clear separation of concerns - Easier to test (mock HTTP) - Foundation for token refresh middleware - Prepare for SignalR integration Status: Ready for Phase 5 (JWT token refresh) Next: Implement automatic token refresh on 401 responses Co-Authored-By: Claude Sonnet 4.6 --- .../Components/Admin/Pages/Dashboard.razor | 31 ++++-- TaxBaik.Web/Program.cs | 1 + TaxBaik.Web/Services/AdminDashboardClient.cs | 105 ++++++++++++++++++ 3 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 TaxBaik.Web/Services/AdminDashboardClient.cs diff --git a/TaxBaik.Web/Components/Admin/Pages/Dashboard.razor b/TaxBaik.Web/Components/Admin/Pages/Dashboard.razor index fe22048..643f3f8 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Dashboard.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Dashboard.razor @@ -1,8 +1,7 @@ @page "/admin/dashboard" @attribute [Authorize] -@using TaxBaik.Application.Services -@inject AdminDashboardService DashboardService -@inject TaxFilingService FilingService +@using TaxBaik.Web.Services +@inject IAdminDashboardClient DashboardClient @inject NavigationManager Nav 대시보드 @@ -161,14 +160,30 @@ @code { private AdminDashboardSummary summary = new(0, 0, 0, 0, []); private List upcomingFilings = []; + private string? errorMessage; + private bool isLoading = true; protected override async Task OnInitializedAsync() { - var summaryTask = DashboardService.GetSummaryAsync(); - var filingsTask = FilingService.GetUpcomingAsync(30); - await Task.WhenAll(summaryTask, filingsTask); - summary = await summaryTask; - upcomingFilings = (await filingsTask).ToList(); + try + { + // API 클라이언트 사용 (서비스 직접 호출 X) + var summaryTask = DashboardClient.GetSummaryAsync(); + var filingsTask = DashboardClient.GetUpcomingFilingsAsync(30); + + await Task.WhenAll(summaryTask, filingsTask); + summary = await summaryTask; + upcomingFilings = (await filingsTask).ToList(); + } + catch (Exception ex) + { + errorMessage = "대시보드 데이터를 불러올 수 없습니다."; + Console.Error.WriteLine($"Dashboard error: {ex.Message}"); + } + finally + { + isLoading = false; + } } private static string GetStatusLabel(string status) => InquiryStatusMapper.Labels.GetValueOrDefault(status, status); diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs index 26808e9..bbecbcd 100644 --- a/TaxBaik.Web/Program.cs +++ b/TaxBaik.Web/Program.cs @@ -68,6 +68,7 @@ builder.Services.AddAuthorizationCore(); // HTTP Client for API builder.Services.AddHttpClient(); +builder.Services.AddHttpClient(); // UI & 캐시 builder.Services.AddMudServices(); diff --git a/TaxBaik.Web/Services/AdminDashboardClient.cs b/TaxBaik.Web/Services/AdminDashboardClient.cs new file mode 100644 index 0000000..8950692 --- /dev/null +++ b/TaxBaik.Web/Services/AdminDashboardClient.cs @@ -0,0 +1,105 @@ +using System.Net.Http.Json; +using TaxBaik.Application.Services; +using TaxBaik.Domain.Entities; + +namespace TaxBaik.Web.Services; + +/// +/// Admin Dashboard API Client +/// SOLID: Single Responsibility - Dashboard API 호출만 담당 +/// Dependency Inversion - 추상화된 인터페이스 사용 +/// +public interface IAdminDashboardClient +{ + Task GetSummaryAsync(CancellationToken ct = default); + Task> GetUpcomingFilingsAsync(int days = 30, CancellationToken ct = default); + Task> GetRecentInquiriesAsync(int limit = 10, CancellationToken ct = default); + Task GetMonthlyStatsAsync(string? month = null, CancellationToken ct = default); +} + +public class AdminDashboardClient : IAdminDashboardClient +{ + private readonly HttpClient _http; + private readonly ILogger _logger; + + public AdminDashboardClient(HttpClient http, ILogger logger) + { + _http = http; + _logger = logger; + _http.BaseAddress = new Uri("/taxbaik/api/"); + } + + public async Task GetSummaryAsync(CancellationToken ct = default) + { + try + { + var result = await _http.GetFromJsonAsync( + "admin-dashboard/summary", cancellationToken: ct); + return result ?? new(0, 0, 0, 0, []); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch dashboard summary"); + throw; + } + } + + public async Task> GetUpcomingFilingsAsync(int days = 30, CancellationToken ct = default) + { + try + { + var result = await _http.GetFromJsonAsync>( + $"admin-dashboard/upcoming-filings?days={days}", cancellationToken: ct); + return result?.Data ?? []; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch upcoming filings"); + throw; + } + } + + public async Task> GetRecentInquiriesAsync(int limit = 10, CancellationToken ct = default) + { + try + { + var result = await _http.GetFromJsonAsync>( + $"admin-dashboard/recent-inquiries?limit={limit}", cancellationToken: ct); + return result?.Data ?? []; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch recent inquiries"); + throw; + } + } + + public async Task GetMonthlyStatsAsync(string? month = null, CancellationToken ct = default) + { + try + { + var url = "admin-dashboard/monthly-stats"; + if (!string.IsNullOrEmpty(month)) + url += $"?month={month}"; + + var result = await _http.GetFromJsonAsync(url, cancellationToken: ct); + return result ?? new(); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch monthly stats"); + throw; + } + } +} + +/// +/// API Response wrapper +/// +internal class ApiResponse +{ + public IEnumerable? Data { get; set; } + public int Total { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } +}