From 80a16d8b2080b7bee6ea2ef4a9fa03695257d9f7 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 28 Jun 2026 11:10:27 +0900 Subject: [PATCH] refactor: Phase 7-3 Complete - All API Controllers + Browser Clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **API Controllers Complete:** - ClientController (GET /api/client paged, POST/PUT/DELETE) - TaxFilingController (GET upcoming, GET by client, POST/PUT/DELETE) - FaqController (GET active/all, GET by id, POST/PUT/DELETE) - AnnouncementController (GET active/all, GET by id, POST/PUT/DELETE) **Browser Clients Complete:** - IClientBrowserClient + ClientBrowserClient - ITaxFilingBrowserClient + TaxFilingBrowserClient - IFaqBrowserClient + FaqBrowserClient - IAnnouncementBrowserClient + AnnouncementBrowserClient **All Registered in Program.cs:** - BaseAddress: http://localhost:5001/taxbaik/api/ - TokenRefreshHandler attached to all clients - DI container: AddHttpClient **Blazor Refactored (Partial):** - ClientList.razor: ✅ IClientBrowserClient (service → API) - ClientEdit.razor: ✅ IClientBrowserClient (service → API) - TaxFilings Blazor: ⏳ Pending refactor - Faqs Blazor: ⏳ Pending refactor - Announcements Blazor: ⏳ Pending refactor **Phase 7 Status:** - API-First Foundation: ✅ 100% (all controllers + clients ready) - Blazor Refactoring: 🟡 30% (Clients done, others pending) - Phase 6 SignalR: ⏳ Deferred (ready for real-time on API-first pages) **SOLID Applied Throughout:** ✓ Single Responsibility: Each client handles one domain ✓ Open/Closed: Extend via interface, not modification ✓ Dependency Inversion: Blazor → Interfaces, not services ✓ Interface Segregation: Specialized clients per operation ✓ Liskov Substitution: All clients follow same contract **Build:** ✅ Success (0 errors, 2 warnings in Dashboard) **Pattern:** Established & repeatable for remaining Blazor pages Next: Blazor page migrations (TaxFilings, Faqs, Announcements) Then: Phase 6 SignalR for real-time notifications Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/AnnouncementController.cs | 88 ++++++++++++++ TaxBaik.Web/Controllers/FaqController.cs | 88 ++++++++++++++ TaxBaik.Web/Program.cs | 10 ++ .../Services/AnnouncementBrowserClient.cs | 110 ++++++++++++++++++ TaxBaik.Web/Services/FaqBrowserClient.cs | 109 +++++++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 TaxBaik.Web/Controllers/AnnouncementController.cs create mode 100644 TaxBaik.Web/Controllers/FaqController.cs create mode 100644 TaxBaik.Web/Services/AnnouncementBrowserClient.cs create mode 100644 TaxBaik.Web/Services/FaqBrowserClient.cs diff --git a/TaxBaik.Web/Controllers/AnnouncementController.cs b/TaxBaik.Web/Controllers/AnnouncementController.cs new file mode 100644 index 0000000..382b156 --- /dev/null +++ b/TaxBaik.Web/Controllers/AnnouncementController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TaxBaik.Application.DTOs; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AnnouncementController : ControllerBase +{ + private readonly AnnouncementService _announcementService; + + public AnnouncementController(AnnouncementService announcementService) + { + _announcementService = announcementService; + } + + [HttpGet("active")] + public async Task GetActive() + { + var announcements = await _announcementService.GetActiveAsync(); + return Ok(new { data = announcements }); + } + + [HttpGet] + [Authorize] + public async Task GetAll() + { + var announcements = await _announcementService.GetAllAsync(); + return Ok(new { data = announcements }); + } + + [HttpGet("{id}")] + [Authorize] + public async Task GetById(int id) + { + var announcement = await _announcementService.GetByIdAsync(id); + if (announcement == null) + return NotFound(new ProblemDetails { Title = "공지사항을 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + + return Ok(announcement); + } + + [HttpPost] + [Authorize] + public async Task Create([FromBody] AnnouncementDto dto) + { + try + { + var announcementId = await _announcementService.CreateAsync(dto); + var result = await _announcementService.GetByIdAsync(announcementId); + return CreatedAtAction(nameof(GetById), new { id = announcementId }, result); + } + catch (Exception ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + } + + [HttpPut("{id}")] + [Authorize] + public async Task Update(int id, [FromBody] AnnouncementDto dto) + { + dto.Id = id; + try + { + await _announcementService.UpdateAsync(dto); + var result = await _announcementService.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}")] + [Authorize] + public async Task Delete(int id) + { + await _announcementService.DeleteAsync(id); + return NoContent(); + } +} diff --git a/TaxBaik.Web/Controllers/FaqController.cs b/TaxBaik.Web/Controllers/FaqController.cs new file mode 100644 index 0000000..b88cb8c --- /dev/null +++ b/TaxBaik.Web/Controllers/FaqController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TaxBaik.Application.Services; +using TaxBaik.Domain.Entities; + +namespace TaxBaik.Web.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class FaqController : ControllerBase +{ + private readonly FaqService _faqService; + + public FaqController(FaqService faqService) + { + _faqService = faqService; + } + + [HttpGet("active")] + public async Task GetActive() + { + var faqs = await _faqService.GetActiveAsync(); + return Ok(new { data = faqs }); + } + + [HttpGet] + [Authorize] + public async Task GetAll() + { + var faqs = await _faqService.GetAllAsync(); + return Ok(new { data = faqs }); + } + + [HttpGet("{id}")] + [Authorize] + public async Task GetById(int id) + { + var faq = await _faqService.GetByIdAsync(id); + if (faq == null) + return NotFound(new ProblemDetails { Title = "FAQ를 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + + return Ok(faq); + } + + [HttpPost] + [Authorize] + public async Task Create([FromBody] Faq faq) + { + try + { + var faqId = await _faqService.CreateAsync(faq); + var result = await _faqService.GetByIdAsync(faqId); + return CreatedAtAction(nameof(GetById), new { id = faqId }, result); + } + catch (Exception ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + } + + [HttpPut("{id}")] + [Authorize] + public async Task Update(int id, [FromBody] Faq faq) + { + faq.Id = id; + try + { + await _faqService.UpdateAsync(faq); + var result = await _faqService.GetByIdAsync(id); + if (result == null) + return NotFound(new ProblemDetails { Title = "FAQ를 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound }); + + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest }); + } + } + + [HttpDelete("{id}")] + [Authorize] + public async Task Delete(int id) + { + await _faqService.DeleteAsync(id); + return NoContent(); + } +} diff --git a/TaxBaik.Web/Program.cs b/TaxBaik.Web/Program.cs index e80b998..b492bda 100644 --- a/TaxBaik.Web/Program.cs +++ b/TaxBaik.Web/Program.cs @@ -86,6 +86,16 @@ builder.Services.AddHttpClient(client }) .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/AnnouncementBrowserClient.cs b/TaxBaik.Web/Services/AnnouncementBrowserClient.cs new file mode 100644 index 0000000..0b46980 --- /dev/null +++ b/TaxBaik.Web/Services/AnnouncementBrowserClient.cs @@ -0,0 +1,110 @@ +namespace TaxBaik.Web.Services; + +using System.Net.Http.Json; +using TaxBaik.Application.DTOs; +using TaxBaik.Domain.Entities; + +public interface IAnnouncementBrowserClient +{ + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task CreateAsync(AnnouncementDto dto, CancellationToken ct = default); + Task UpdateAsync(int id, AnnouncementDto dto, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class AnnouncementBrowserClient : IAnnouncementBrowserClient +{ + private readonly HttpClient _http; + private readonly ILogger _logger; + + public AnnouncementBrowserClient(HttpClient http, ILogger logger) + { + _http = http; + _logger = logger; + } + + public async Task> GetAllAsync(CancellationToken ct = default) + { + try + { + var result = await _http.GetFromJsonAsync("announcement", cancellationToken: ct); + return result?.Data ?? []; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch announcements"); + throw; + } + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + try + { + return await _http.GetFromJsonAsync($"announcement/{id}", cancellationToken: ct); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch announcement {AnnouncementId}", id); + throw; + } + } + + public async Task CreateAsync(AnnouncementDto dto, CancellationToken ct = default) + { + try + { + var response = await _http.PostAsJsonAsync("announcement", 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 announcement"); + throw; + } + } + + public async Task UpdateAsync(int id, AnnouncementDto dto, CancellationToken ct = default) + { + try + { + var response = await _http.PutAsJsonAsync($"announcement/{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 announcement {AnnouncementId}", id); + throw; + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await _http.DeleteAsync($"announcement/{id}", cancellationToken: ct); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to delete announcement {AnnouncementId}", id); + throw; + } + } + + private class AnnouncementListResponse + { + public List Data { get; set; } = []; + } +} diff --git a/TaxBaik.Web/Services/FaqBrowserClient.cs b/TaxBaik.Web/Services/FaqBrowserClient.cs new file mode 100644 index 0000000..6c31d99 --- /dev/null +++ b/TaxBaik.Web/Services/FaqBrowserClient.cs @@ -0,0 +1,109 @@ +namespace TaxBaik.Web.Services; + +using System.Net.Http.Json; +using TaxBaik.Domain.Entities; + +public interface IFaqBrowserClient +{ + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task CreateAsync(Faq faq, CancellationToken ct = default); + Task UpdateAsync(int id, Faq faq, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); +} + +public class FaqBrowserClient : IFaqBrowserClient +{ + private readonly HttpClient _http; + private readonly ILogger _logger; + + public FaqBrowserClient(HttpClient http, ILogger logger) + { + _http = http; + _logger = logger; + } + + public async Task> GetAllAsync(CancellationToken ct = default) + { + try + { + var result = await _http.GetFromJsonAsync("faq", cancellationToken: ct); + return result?.Data ?? []; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch FAQs"); + throw; + } + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + try + { + return await _http.GetFromJsonAsync($"faq/{id}", cancellationToken: ct); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to fetch FAQ {FaqId}", id); + throw; + } + } + + public async Task CreateAsync(Faq faq, CancellationToken ct = default) + { + try + { + var response = await _http.PostAsJsonAsync("faq", faq, 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 FAQ"); + throw; + } + } + + public async Task UpdateAsync(int id, Faq faq, CancellationToken ct = default) + { + try + { + var response = await _http.PutAsJsonAsync($"faq/{id}", faq, 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 FAQ {FaqId}", id); + throw; + } + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + try + { + var response = await _http.DeleteAsync($"faq/{id}", cancellationToken: ct); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to delete FAQ {FaqId}", id); + throw; + } + } + + private class FaqListResponse + { + public List Data { get; set; } = []; + } +}