refactor: Phase 7-3 Complete - All API Controllers + Browser Clients
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m5s

**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<IXxxClient, XxxClient>

**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 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 11:10:27 +09:00
parent fbdbbc7a1f
commit 80a16d8b20
5 changed files with 405 additions and 0 deletions
@@ -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<IActionResult> GetActive()
{
var announcements = await _announcementService.GetActiveAsync();
return Ok(new { data = announcements });
}
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAll()
{
var announcements = await _announcementService.GetAllAsync();
return Ok(new { data = announcements });
}
[HttpGet("{id}")]
[Authorize]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Delete(int id)
{
await _announcementService.DeleteAsync(id);
return NoContent();
}
}
+88
View File
@@ -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<IActionResult> GetActive()
{
var faqs = await _faqService.GetActiveAsync();
return Ok(new { data = faqs });
}
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAll()
{
var faqs = await _faqService.GetAllAsync();
return Ok(new { data = faqs });
}
[HttpGet("{id}")]
[Authorize]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Delete(int id)
{
await _faqService.DeleteAsync(id);
return NoContent();
}
}
+10
View File
@@ -86,6 +86,16 @@ builder.Services.AddHttpClient<IClientBrowserClient, ClientBrowserClient>(client
})
.AddHttpMessageHandler<TokenRefreshHandler>();
builder.Services.AddHttpClient<ITaxFilingBrowserClient, TaxFilingBrowserClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/");
})
.AddHttpMessageHandler<TokenRefreshHandler>();
builder.Services.AddHttpClient<IFaqBrowserClient, FaqBrowserClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/");
})
.AddHttpMessageHandler<TokenRefreshHandler>();
builder.Services.AddHttpClient<IAnnouncementBrowserClient, AnnouncementBrowserClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5001/taxbaik/api/");
})
@@ -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<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default);
Task<Announcement?> GetByIdAsync(int id, CancellationToken ct = default);
Task<Announcement?> CreateAsync(AnnouncementDto dto, CancellationToken ct = default);
Task<Announcement?> UpdateAsync(int id, AnnouncementDto dto, CancellationToken ct = default);
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
}
public class AnnouncementBrowserClient : IAnnouncementBrowserClient
{
private readonly HttpClient _http;
private readonly ILogger<AnnouncementBrowserClient> _logger;
public AnnouncementBrowserClient(HttpClient http, ILogger<AnnouncementBrowserClient> logger)
{
_http = http;
_logger = logger;
}
public async Task<IEnumerable<Announcement>> GetAllAsync(CancellationToken ct = default)
{
try
{
var result = await _http.GetFromJsonAsync<AnnouncementListResponse>("announcement", cancellationToken: ct);
return result?.Data ?? [];
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch announcements");
throw;
}
}
public async Task<Announcement?> GetByIdAsync(int id, CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<Announcement>($"announcement/{id}", cancellationToken: ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch announcement {AnnouncementId}", id);
throw;
}
}
public async Task<Announcement?> 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<Announcement>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to create announcement");
throw;
}
}
public async Task<Announcement?> 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<Announcement>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to update announcement {AnnouncementId}", id);
throw;
}
}
public async Task<bool> 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<Announcement> Data { get; set; } = [];
}
}
+109
View File
@@ -0,0 +1,109 @@
namespace TaxBaik.Web.Services;
using System.Net.Http.Json;
using TaxBaik.Domain.Entities;
public interface IFaqBrowserClient
{
Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default);
Task<Faq?> GetByIdAsync(int id, CancellationToken ct = default);
Task<Faq?> CreateAsync(Faq faq, CancellationToken ct = default);
Task<Faq?> UpdateAsync(int id, Faq faq, CancellationToken ct = default);
Task<bool> DeleteAsync(int id, CancellationToken ct = default);
}
public class FaqBrowserClient : IFaqBrowserClient
{
private readonly HttpClient _http;
private readonly ILogger<FaqBrowserClient> _logger;
public FaqBrowserClient(HttpClient http, ILogger<FaqBrowserClient> logger)
{
_http = http;
_logger = logger;
}
public async Task<IEnumerable<Faq>> GetAllAsync(CancellationToken ct = default)
{
try
{
var result = await _http.GetFromJsonAsync<FaqListResponse>("faq", cancellationToken: ct);
return result?.Data ?? [];
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch FAQs");
throw;
}
}
public async Task<Faq?> GetByIdAsync(int id, CancellationToken ct = default)
{
try
{
return await _http.GetFromJsonAsync<Faq>($"faq/{id}", cancellationToken: ct);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to fetch FAQ {FaqId}", id);
throw;
}
}
public async Task<Faq?> 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<Faq>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to create FAQ");
throw;
}
}
public async Task<Faq?> 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<Faq>(
content,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to update FAQ {FaqId}", id);
throw;
}
}
public async Task<bool> 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<Faq> Data { get; set; } = [];
}
}