refactor: Phase 7-3 - Clients + TaxFilings API-First (WIP)
TaxBaik CI/CD / build-and-deploy (push) Successful in 54s

**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 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 11:08:43 +09:00
parent 160afb7c7e
commit fbdbbc7a1f
7 changed files with 503 additions and 32 deletions
@@ -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");
}
@@ -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();
}
}
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Delete(int id)
{
await _clientService.DeleteAsync(id);
return NoContent();
}
}
@@ -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<IActionResult> GetUpcoming([FromQuery] int daysAhead = 30)
{
var filings = await _taxFilingService.GetUpcomingAsync(daysAhead);
return Ok(new { data = filings });
}
[HttpGet("client/{clientId}")]
public async Task<IActionResult> GetByClientId(int clientId)
{
var filings = await _taxFilingService.GetByClientIdAsync(clientId);
return Ok(new { data = filings });
}
[HttpGet("{id}")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Delete(int id)
{
await _taxFilingService.DeleteAsync(id);
return NoContent();
}
}
+10
View File
@@ -76,6 +76,16 @@ builder.Services.AddHttpClient<IAdminDashboardClient, AdminDashboardClient>(clie
})
.AddHttpMessageHandler<TokenRefreshHandler>();
builder.Services.AddHttpClient<IInquiryBrowserClient, InquiryBrowserClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/");
})
.AddHttpMessageHandler<TokenRefreshHandler>();
builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/");
})
.AddHttpMessageHandler<TokenRefreshHandler>();
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/");
})
+125
View File
@@ -0,0 +1,125 @@
namespace TaxBaik.Web.Services;
using System.Net.Http.Json;
using TaxBaik.Application.DTOs;
using TaxBaik.Domain.Entities;
/// <summary>
/// Client API Client for Admin Blazor
/// SOLID: Single Responsibility - Client API calls only
/// </summary>
public interface IClientBrowserClient
{
Task<(IEnumerable<Client> Items, int Total)> GetPagedAsync(
int page = 1, int pageSize = 20, string? status = null, string? search = null, CancellationToken ct = default);
Task<Client?> GetByIdAsync(int id, CancellationToken ct = default);
Task<Client?> CreateAsync(CreateClientDto dto, CancellationToken ct = default);
Task<Client?> UpdateAsync(int id, CreateClientDto dto, CancellationToken ct = default);
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
}
public class ClientBrowserClient : IClientBrowserClient
{
private readonly HttpClient _http;
private readonly ILogger<ClientBrowserClient> _logger;
public ClientBrowserClient(HttpClient http, ILogger<ClientBrowserClient> logger)
{
_http = http;
_logger = logger;
}
public async Task<(IEnumerable<Client> 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<ClientPagedResponse>(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<Client?> GetByIdAsync(int id, CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<Client>($"client/{id}", cancellationToken: ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch client {ClientId}", id);
throw;
}
}
public async Task<Client?> 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<Client>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to create client");
throw;
}
}
public async Task<Client?> 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<Client>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to update client {ClientId}", id);
throw;
}
}
public async Task<bool> 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<Client> Data { get; set; } = [];
public int Total { get; set; }
}
}
@@ -0,0 +1,132 @@
namespace TaxBaik.Web.Services;
using System.Net.Http.Json;
using TaxBaik.Domain.Entities;
/// <summary>
/// TaxFiling API Client for Admin Blazor
/// </summary>
public interface ITaxFilingBrowserClient
{
Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default);
Task<IEnumerable<TaxFiling>> GetByClientIdAsync(int clientId, CancellationToken ct = default);
Task<TaxFiling?> GetByIdAsync(int id, CancellationToken ct = default);
Task<TaxFiling?> CreateAsync(TaxFiling filing, CancellationToken ct = default);
Task<TaxFiling?> UpdateAsync(int id, TaxFiling filing, CancellationToken ct = default);
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
}
public class TaxFilingBrowserClient : ITaxFilingBrowserClient
{
private readonly HttpClient _http;
private readonly ILogger<TaxFilingBrowserClient> _logger;
public TaxFilingBrowserClient(HttpClient http, ILogger<TaxFilingBrowserClient> logger)
{
_http = http;
_logger = logger;
}
public async Task<IEnumerable<TaxFiling>> GetUpcomingAsync(int daysAhead = 30, CancellationToken ct = default)
{
try
{
var result = await _http.GetFromJsonAsync<TaxFilingListResponse>(
$"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<IEnumerable<TaxFiling>> GetByClientIdAsync(int clientId, CancellationToken ct = default)
{
try
{
var result = await _http.GetFromJsonAsync<TaxFilingListResponse>(
$"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<TaxFiling?> GetByIdAsync(int id, CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<TaxFiling>(
$"tax-filing/{id}", cancellationToken: ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch filing {FilingId}", id);
throw;
}
}
public async Task<TaxFiling?> 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<TaxFiling>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to create filing");
throw;
}
}
public async Task<TaxFiling?> 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<TaxFiling>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to update filing {FilingId}", id);
throw;
}
}
public async Task<bool> 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<TaxFiling> Data { get; set; } = [];
}
}