From fbdbbc7a1fb54f84468dd5d15aa5d18cabfdbf4f Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 28 Jun 2026 11:08:43 +0900 Subject: [PATCH] refactor: Phase 7-3 - Clients + TaxFilings API-First (WIP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Clients Migration Complete:** - ClientController: GET /api/client (paged), POST/PUT/DELETE - ClientBrowserClient: IClientBrowserClient interface + implementation - ClientList.razor: Service → API client - ClientEdit.razor: Service → API client (Create/Update) **TaxFilings API Framework Ready:** - TaxFilingController: GET upcoming, GET by client, POST/PUT/DELETE - TaxFilingBrowserClient: ITaxFilingBrowserClient interface + impl - Registered in Program.cs with TokenRefreshHandler **SOLID Applied:** ✓ Separation of concerns (Controller → Service → Repository) ✓ Dependency inversion (Blazor → Browser clients, not services) ✓ Interface segregation (Specialized clients per domain) **Status:** - Clients Blazor: ✅ ClientList + ClientEdit refactored - TaxFilings Blazor: ⏳ Pending refactor (pages exist) - Faqs: ⏳ API + Blazor pending - Announcements: ⏳ API + Blazor pending - Phase 6 SignalR: ⏳ Deferred Next: Refactor TaxFilings Blazor pages, then Faqs & Announcements Build: ✅ Success (0 errors, 2 warnings in Dashboard) Co-Authored-By: Claude Sonnet 4.6 --- .../Admin/Pages/Clients/ClientEdit.razor | 57 +++++--- .../Admin/Pages/Clients/ClientList.razor | 47 +++++-- TaxBaik.Web/Controllers/ClientController.cs | 80 +++++++++++ .../Controllers/TaxFilingController.cs | 84 +++++++++++ TaxBaik.Web/Program.cs | 10 ++ TaxBaik.Web/Services/ClientBrowserClient.cs | 125 +++++++++++++++++ .../Services/TaxFilingBrowserClient.cs | 132 ++++++++++++++++++ 7 files changed, 503 insertions(+), 32 deletions(-) create mode 100644 TaxBaik.Web/Controllers/ClientController.cs create mode 100644 TaxBaik.Web/Controllers/TaxFilingController.cs create mode 100644 TaxBaik.Web/Services/ClientBrowserClient.cs create mode 100644 TaxBaik.Web/Services/TaxFilingBrowserClient.cs diff --git a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientEdit.razor b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientEdit.razor index 2db9dbc..c31862e 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientEdit.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientEdit.razor @@ -2,9 +2,9 @@ @page "/admin/clients/{Id:int}/edit" @attribute [Authorize] @using TaxBaik.Application.DTOs -@using TaxBaik.Application.Services +@using TaxBaik.Web.Services @using TaxBaik.Domain.Entities -@inject ClientService ClientService +@inject IClientBrowserClient ClientClient @inject NavigationManager Navigation @inject ISnackbar Snackbar @@ -124,25 +124,34 @@ { if (Id.HasValue) { - var client = await ClientService.GetByIdAsync(Id.Value); - if (client is null) + try { - Snackbar.Add("고객을 찾을 수 없습니다.", Severity.Error); + var client = await ClientClient.GetByIdAsync(Id.Value); + if (client is null) + { + Snackbar.Add("고객을 찾을 수 없습니다.", Severity.Error); + Navigation.NavigateTo("/taxbaik/admin/clients"); + return; + } + dto = new CreateClientDto + { + Name = client.Name, + CompanyName = client.CompanyName, + Phone = client.Phone, + Email = client.Email, + ServiceType = client.ServiceType, + TaxType = client.TaxType, + Status = client.Status, + Source = client.Source, + Memo = client.Memo + }; + } + catch (Exception ex) + { + Snackbar.Add($"오류: {ex.Message}", Severity.Error); Navigation.NavigateTo("/taxbaik/admin/clients"); return; } - dto = new CreateClientDto - { - Name = client.Name, - CompanyName = client.CompanyName, - Phone = client.Phone, - Email = client.Email, - ServiceType = client.ServiceType, - TaxType = client.TaxType, - Status = client.Status, - Source = client.Source, - Memo = client.Memo - }; } isLoading = false; } @@ -157,13 +166,19 @@ { if (Id.HasValue) { - await ClientService.UpdateAsync(Id.Value, dto); - Snackbar.Add("고객 정보가 수정되었습니다.", Severity.Success); + var result = await ClientClient.UpdateAsync(Id.Value, dto); + if (result != null) + Snackbar.Add("고객 정보가 수정되었습니다.", Severity.Success); + else + Snackbar.Add("수정에 실패했습니다.", Severity.Error); } else { - var newId = await ClientService.CreateAsync(dto); - Snackbar.Add("고객이 등록되었습니다.", Severity.Success); + var result = await ClientClient.CreateAsync(dto); + if (result != null) + Snackbar.Add("고객이 등록되었습니다.", Severity.Success); + else + Snackbar.Add("등록에 실패했습니다.", Severity.Error); } Navigation.NavigateTo("/taxbaik/admin/clients"); } diff --git a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientList.razor b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientList.razor index d1fc4eb..c872440 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Clients/ClientList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Clients/ClientList.razor @@ -1,8 +1,8 @@ @page "/admin/clients" @attribute [Authorize] -@using TaxBaik.Application.Services +@using TaxBaik.Web.Services @using TaxBaik.Domain.Entities -@inject ClientService ClientService +@inject IClientBrowserClient ClientClient @inject NavigationManager Navigation @inject IDialogService DialogService @inject ISnackbar Snackbar @@ -141,14 +141,24 @@ private async Task LoadAsync() { - var (items, total) = await ClientService.GetPagedAsync( - currentPage, PageSize, - string.IsNullOrEmpty(statusFilter) ? null : statusFilter, - string.IsNullOrEmpty(searchText) ? null : searchText); + try + { + var (items, total) = await ClientClient.GetPagedAsync( + currentPage, PageSize, + string.IsNullOrEmpty(statusFilter) ? null : statusFilter, + string.IsNullOrEmpty(searchText) ? null : searchText); - clients = items.ToList(); - totalCount = total; - totalPages = (int)Math.Ceiling((double)total / PageSize); + clients = items.ToList(); + totalCount = total; + totalPages = (int)Math.Ceiling((double)total / PageSize); + } + catch (Exception ex) + { + Snackbar.Add($"오류: {ex.Message}", Severity.Error); + clients = []; + totalCount = 0; + totalPages = 0; + } } private async Task SearchAsync() @@ -185,8 +195,23 @@ if (confirmed != true) return; - await ClientService.DeleteAsync(client.Id); - Snackbar.Add($"{client.Name} 고객이 삭제되었습니다.", Severity.Success); + try + { + var success = await ClientClient.DeleteAsync(client.Id); + if (success) + { + Snackbar.Add($"{client.Name} 고객이 삭제되었습니다.", Severity.Success); + await LoadAsync(); + } + else + { + Snackbar.Add("삭제에 실패했습니다.", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"오류: {ex.Message}", Severity.Error); + } await LoadAsync(); } } diff --git a/TaxBaik.Web/Controllers/ClientController.cs b/TaxBaik.Web/Controllers/ClientController.cs new file mode 100644 index 0000000..d08b722 --- /dev/null +++ b/TaxBaik.Web/Controllers/ClientController.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TaxBaik.Application.DTOs; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class ClientController : ControllerBase +{ + private readonly ClientService _clientService; + + public ClientController(ClientService clientService) + { + _clientService = clientService; + } + + [HttpGet] + public async Task GetPaged( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromQuery] string? status = null, + [FromQuery] string? search = null) + { + var (clients, total) = await _clientService.GetPagedAsync(page, pageSize, status, search); + return Ok(new { data = clients, total, page, pageSize }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var client = await _clientService.GetByIdAsync(id); + if (client == null) + return NotFound(new ProblemDetails { Title = "고객을 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + + return Ok(client); + } + + [HttpPost] + public async Task Create([FromBody] CreateClientDto dto) + { + try + { + var clientId = await _clientService.CreateAsync(dto); + var client = await _clientService.GetByIdAsync(clientId); + return CreatedAtAction(nameof(GetById), new { id = clientId }, client); + } + catch (ValidationException ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + } + + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] CreateClientDto dto) + { + try + { + await _clientService.UpdateAsync(id, dto); + var client = await _clientService.GetByIdAsync(id); + if (client == null) + return NotFound(new ProblemDetails { Title = "고객을 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + + return Ok(client); + } + catch (ValidationException ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + await _clientService.DeleteAsync(id); + return NoContent(); + } +} diff --git a/TaxBaik.Web/Controllers/TaxFilingController.cs b/TaxBaik.Web/Controllers/TaxFilingController.cs new file mode 100644 index 0000000..1727b0c --- /dev/null +++ b/TaxBaik.Web/Controllers/TaxFilingController.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TaxBaik.Application.Services; +using TaxBaik.Domain.Entities; + +namespace TaxBaik.Web.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class TaxFilingController : ControllerBase +{ + private readonly TaxFilingService _taxFilingService; + + public TaxFilingController(TaxFilingService taxFilingService) + { + _taxFilingService = taxFilingService; + } + + [HttpGet("upcoming")] + public async Task GetUpcoming([FromQuery] int daysAhead = 30) + { + var filings = await _taxFilingService.GetUpcomingAsync(daysAhead); + return Ok(new { data = filings }); + } + + [HttpGet("client/{clientId}")] + public async Task GetByClientId(int clientId) + { + var filings = await _taxFilingService.GetByClientIdAsync(clientId); + return Ok(new { data = filings }); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var filing = await _taxFilingService.GetByIdAsync(id); + if (filing == null) + return NotFound(new ProblemDetails { Title = "신고 일정을 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + + return Ok(filing); + } + + [HttpPost] + public async Task Create([FromBody] TaxFiling filing) + { + try + { + var filingId = await _taxFilingService.CreateAsync(filing); + var result = await _taxFilingService.GetByIdAsync(filingId); + return CreatedAtAction(nameof(GetById), new { id = filingId }, result); + } + catch (Exception ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + } + + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] TaxFiling filing) + { + filing.Id = id; + try + { + await _taxFilingService.UpdateAsync(filing); + var result = await _taxFilingService.GetByIdAsync(id); + if (result == null) + return NotFound(new ProblemDetails { Title = "신고 일정을 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + await _taxFilingService.DeleteAsync(id); + return NoContent(); + } +} diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs index 03cda51..e80b998 100644 --- a/TaxBaik.Web/Program.cs +++ b/TaxBaik.Web/Program.cs @@ -76,6 +76,16 @@ builder.Services.AddHttpClient(clie }) .AddHttpMessageHandler(); builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/"); +}) + .AddHttpMessageHandler(); +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/"); +}) + .AddHttpMessageHandler(); +builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/"); }) diff --git a/TaxBaik.Web/Services/ClientBrowserClient.cs b/TaxBaik.Web/Services/ClientBrowserClient.cs new file mode 100644 index 0000000..c2cc048 --- /dev/null +++ b/TaxBaik.Web/Services/ClientBrowserClient.cs @@ -0,0 +1,125 @@ +namespace TaxBaik.Web.Services; + +using System.Net.Http.Json; +using TaxBaik.Application.DTOs; +using TaxBaik.Domain.Entities; + +/// +/// Client API Client for Admin Blazor +/// SOLID: Single Responsibility - Client API calls only +/// +public interface IClientBrowserClient +{ + Task<(IEnumerable Items, int Total)> GetPagedAsync( + int page = 1, int pageSize = 20, string? status = null, string? search = null, CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task CreateAsync(CreateClientDto dto, CancellationToken ct = default); + Task UpdateAsync(int id, CreateClientDto dto, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class ClientBrowserClient : IClientBrowserClient +{ + private readonly HttpClient _http; + private readonly ILogger _logger; + + public ClientBrowserClient(HttpClient http, ILogger logger) + { + _http = http; + _logger = logger; + } + + public async Task<(IEnumerable Items, int Total)> GetPagedAsync( + int page = 1, int pageSize = 20, string? status = null, string? search = null, CancellationToken ct = default) + { + try + { + var query = $"client?page={page}&pageSize={pageSize}"; + if (!string.IsNullOrEmpty(status)) + query += $"&status={status}"; + if (!string.IsNullOrEmpty(search)) + query += $"&search={Uri.EscapeDataString(search)}"; + + var result = await _http.GetFromJsonAsync(query, cancellationToken: ct); + return result != null ? (result.Data, result.Total) : ([], 0); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch clients"); + throw; + } + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + try + { + return await _http.GetFromJsonAsync($"client/{id}", cancellationToken: ct); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch client {ClientId}", id); + throw; + } + } + + public async Task CreateAsync(CreateClientDto dto, CancellationToken ct = default) + { + try + { + var response = await _http.PostAsJsonAsync("client", dto, cancellationToken: ct); + if (!response.IsSuccessStatusCode) + return null; + + var content = await response.Content.ReadAsStringAsync(ct); + return System.Text.Json.JsonSerializer.Deserialize( + content, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to create client"); + throw; + } + } + + public async Task UpdateAsync(int id, CreateClientDto dto, CancellationToken ct = default) + { + try + { + var response = await _http.PutAsJsonAsync($"client/{id}", dto, cancellationToken: ct); + if (!response.IsSuccessStatusCode) + return null; + + var content = await response.Content.ReadAsStringAsync(ct); + return System.Text.Json.JsonSerializer.Deserialize( + content, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to update client {ClientId}", id); + throw; + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await _http.DeleteAsync($"client/{id}", cancellationToken: ct); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to delete client {ClientId}", id); + throw; + } + } + + private class ClientPagedResponse + { + public List Data { get; set; } = []; + public int Total { get; set; } + } +} diff --git a/TaxBaik.Web/Services/TaxFilingBrowserClient.cs b/TaxBaik.Web/Services/TaxFilingBrowserClient.cs new file mode 100644 index 0000000..91c0aac --- /dev/null +++ b/TaxBaik.Web/Services/TaxFilingBrowserClient.cs @@ -0,0 +1,132 @@ +namespace TaxBaik.Web.Services; + +using System.Net.Http.Json; +using TaxBaik.Domain.Entities; + +/// +/// TaxFiling API Client for Admin Blazor +/// +public interface ITaxFilingBrowserClient +{ + Task> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default); + Task> GetByClientIdAsync(int clientId, CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task CreateAsync(TaxFiling filing, CancellationToken ct = default); + Task UpdateAsync(int id, TaxFiling filing, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class TaxFilingBrowserClient : ITaxFilingBrowserClient +{ + private readonly HttpClient _http; + private readonly ILogger _logger; + + public TaxFilingBrowserClient(HttpClient http, ILogger logger) + { + _http = http; + _logger = logger; + } + + public async Task> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default) + { + try + { + var result = await _http.GetFromJsonAsync( + $"tax-filing/upcoming?daysAhead={daysAhead}", cancellationToken: ct); + return result?.Data ?? []; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch upcoming filings"); + throw; + } + } + + public async Task> GetByClientIdAsync(int clientId, CancellationToken ct = default) + { + try + { + var result = await _http.GetFromJsonAsync( + $"tax-filing/client/{clientId}", cancellationToken: ct); + return result?.Data ?? []; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch filings for client {ClientId}", clientId); + throw; + } + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + try + { + return await _http.GetFromJsonAsync( + $"tax-filing/{id}", cancellationToken: ct); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch filing {FilingId}", id); + throw; + } + } + + public async Task CreateAsync(TaxFiling filing, CancellationToken ct = default) + { + try + { + var response = await _http.PostAsJsonAsync("tax-filing", filing, cancellationToken: ct); + if (!response.IsSuccessStatusCode) + return null; + + var content = await response.Content.ReadAsStringAsync(ct); + return System.Text.Json.JsonSerializer.Deserialize( + content, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to create filing"); + throw; + } + } + + public async Task UpdateAsync(int id, TaxFiling filing, CancellationToken ct = default) + { + try + { + var response = await _http.PutAsJsonAsync($"tax-filing/{id}", filing, cancellationToken: ct); + if (!response.IsSuccessStatusCode) + return null; + + var content = await response.Content.ReadAsStringAsync(ct); + return System.Text.Json.JsonSerializer.Deserialize( + content, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to update filing {FilingId}", id); + throw; + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await _http.DeleteAsync($"tax-filing/{id}", cancellationToken: ct); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to delete filing {FilingId}", id); + throw; + } + } + + private class TaxFilingListResponse + { + public List Data { get; set; } = []; + } +}