refactor: Phase 7-3 Complete - All API Controllers + Browser Clients
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m5s
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user