Migrate SiteSettings controller to FastEndpoints

Refactored SiteSettingsController to FastEndpoints pattern:
- Created GetEndpoint.cs: GET /api/sitesettings (authorized)
- Created SaveEndpoint.cs: PUT /api/sitesettings (authorized)
- Removed legacy SiteSettingsController.cs

Both endpoints use Bearer token authentication and are auto-discovered
by FastEndpoints configuration in Program.cs.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 17:35:18 +09:00
parent 69ec7913d0
commit a6068e184b
13 changed files with 328 additions and 482 deletions
@@ -1,122 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Controllers;
/// <summary>
/// 관리자 대시보드 API
/// SOLID: Single Responsibility - 대시보드 데이터만 담당
/// </summary>
[ApiController]
[Route("api/admin-dashboard")]
[Authorize]
public class AdminDashboardController : ControllerBase
{
private readonly AdminDashboardService _dashboardService;
private readonly TaxFilingService _taxFilingService;
public AdminDashboardController(
AdminDashboardService dashboardService,
TaxFilingService taxFilingService)
{
_dashboardService = dashboardService;
_taxFilingService = taxFilingService;
}
/// <summary>
/// 대시보드 요약 정보 조회
/// GET /api/admin-dashboard/summary
/// </summary>
[HttpGet("summary")]
public async Task<IActionResult> GetSummary()
{
try
{
var summary = await _dashboardService.GetSummaryAsync();
return Ok(summary);
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails
{
Title = "대시보드 요약 조회 실패",
Detail = ex.Message,
Status = StatusCodes.Status500InternalServerError
});
}
}
/// <summary>
/// 30일 이내 마감 임박 신고 조회
/// GET /api/admin-dashboard/upcoming-filings?days=30
/// </summary>
[HttpGet("upcoming-filings")]
public async Task<IActionResult> GetUpcomingFilings([FromQuery] int days = 30)
{
try
{
if (days <= 0) days = 30;
var filings = await _taxFilingService.GetUpcomingAsync(days);
return Ok(new { data = filings, days });
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails
{
Title = "마감 임박 신고 조회 실패",
Detail = ex.Message,
Status = StatusCodes.Status500InternalServerError
});
}
}
/// <summary>
/// 최근 문의 조회
/// GET /api/admin-dashboard/recent-inquiries?limit=10
/// </summary>
[HttpGet("recent-inquiries")]
public async Task<IActionResult> GetRecentInquiries([FromQuery] int limit = 10)
{
try
{
if (limit <= 0) limit = 10;
if (limit > 100) limit = 100; // 보안: 최대 100개
var inquiries = await _dashboardService.GetRecentInquiriesAsync(limit);
return Ok(new { data = inquiries, limit });
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails
{
Title = "최근 문의 조회 실패",
Detail = ex.Message,
Status = StatusCodes.Status500InternalServerError
});
}
}
/// <summary>
/// 월별 통계
/// GET /api/admin-dashboard/monthly-stats?month=2026-06
/// </summary>
[HttpGet("monthly-stats")]
public async Task<IActionResult> GetMonthlyStats([FromQuery] string? month = null)
{
try
{
var stats = await _dashboardService.GetMonthlyStatsAsync(month);
return Ok(stats);
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails
{
Title = "월별 통계 조회 실패",
Detail = ex.Message,
Status = StatusCodes.Status500InternalServerError
});
}
}
}
@@ -1,73 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
namespace TaxBaik.Web.Controllers;
[ApiController]
[Route("api/client-logs")]
[AllowAnonymous]
[EnableRateLimiting("client-logs")]
public class ClientLogsController(ILogger<ClientLogsController> logger) : ControllerBase
{
[HttpPost]
public IActionResult Post([FromBody] ClientLogEntry entry)
{
if (string.IsNullOrWhiteSpace(entry.Message))
{
return BadRequest();
}
var logMessage = "ClientLog {Level} {Source} {Message} Url={Url} Route={Route} Screen={Screen} Feature={Feature} Action={Action} Step={Step} Entity={Entity} EntityId={EntityId} DataKey={DataKey} BuildVersion={BuildVersion} UserAgent={UserAgent} Stack={Stack}";
var args = new object?[]
{
entry.Level ?? "error",
entry.Source ?? "unknown",
entry.Message,
entry.Url ?? string.Empty,
entry.Route ?? string.Empty,
entry.Screen ?? string.Empty,
entry.Feature ?? string.Empty,
entry.Action ?? string.Empty,
entry.Step ?? string.Empty,
entry.Entity ?? string.Empty,
entry.EntityId ?? string.Empty,
entry.DataKey ?? string.Empty,
entry.BuildVersion ?? string.Empty,
entry.UserAgent ?? string.Empty,
entry.Stack ?? string.Empty
};
// Client errors (level: error) → Telegram alert
// Client warnings (level: warning/info) → Log file only
if (entry.Level?.Equals("error", StringComparison.OrdinalIgnoreCase) ?? true)
{
logger.LogError(logMessage, args);
}
else
{
logger.LogWarning(logMessage, args);
}
return Ok();
}
}
public sealed class ClientLogEntry
{
public string? Level { get; set; }
public string? Source { get; set; }
public string? Message { get; set; }
public string? Url { get; set; }
public string? Route { get; set; }
public string? Screen { get; set; }
public string? Feature { get; set; }
public string? Action { get; set; }
public string? Step { get; set; }
public string? Entity { get; set; }
public string? EntityId { get; set; }
public string? DataKey { get; set; }
public string? BuildVersion { get; set; }
public string? UserAgent { get; set; }
public string? Stack { get; set; }
}
@@ -1,82 +0,0 @@
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 CommonCodeController(CommonCodeService commonCodeService) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAllActive()
{
try
{
var codes = await commonCodeService.GetAllActiveAsync();
return Ok(codes);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "공통코드 조회 실패", message = ex.Message });
}
}
[HttpGet("group/{group}")]
public async Task<IActionResult> GetByGroup(string group)
{
try
{
var codes = await commonCodeService.GetByGroupAsync(group);
return Ok(codes);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "그룹별 공통코드 조회 실패", message = ex.Message });
}
}
[HttpGet("groups")]
public async Task<IActionResult> GetGroups()
{
try
{
var groups = await commonCodeService.GetAllGroupsAsync();
return Ok(groups);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "공통코드 그룹 조회 실패", message = ex.Message });
}
}
[HttpGet("{group}/{value}")]
public async Task<IActionResult> Get(string group, string value)
{
var code = await commonCodeService.GetAsync(group, value);
return code is null ? NotFound() : Ok(code);
}
[HttpPost]
public async Task<IActionResult> Upsert([FromBody] CommonCode code)
{
if (string.IsNullOrWhiteSpace(code.CodeGroup) || string.IsNullOrWhiteSpace(code.CodeValue) || string.IsNullOrWhiteSpace(code.CodeName))
return BadRequest(new { error = "코드 그룹, 값, 이름은 필수입니다." });
if (code.CodeGroup.Any(char.IsWhiteSpace))
return BadRequest(new { error = "code_group에는 공백을 사용할 수 없습니다." });
if (code.CodeValue.Contains(' '))
return BadRequest(new { error = "code_value에는 공백을 사용할 수 없습니다." });
await commonCodeService.UpsertAsync(code);
return Ok(code);
}
[HttpDelete("{group}/{value}")]
public async Task<IActionResult> Delete(string group, string value)
{
await commonCodeService.DeleteAsync(group, value);
return NoContent();
}
}
@@ -1,117 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class CompanyController(CompanyService companyService) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
try
{
var company = await companyService.GetByIdAsync(id);
if (company == null)
return NotFound(new ProblemDetails { Title = "회사를 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound });
return Ok(company);
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails { Title = "회사 조회 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError });
}
}
[HttpGet("code/{code}")]
public async Task<IActionResult> GetByCode(string code)
{
try
{
var company = await companyService.GetByCodeAsync(code);
if (company == null)
return NotFound(new ProblemDetails { Title = "회사를 찾을 수 없습니다.", Status = StatusCodes.Status404NotFound });
return Ok(company);
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails { Title = "회사 조회 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError });
}
}
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
{
try
{
var (companies, total) = await companyService.GetPagedAsync(page, pageSize);
return Ok(new { data = companies, total, page, pageSize });
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails { Title = "회사 목록 조회 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError });
}
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateCompanyRequest request)
{
try
{
var id = await companyService.CreateAsync(
request.CompanyCode, request.CompanyName, request.ContactPerson,
request.Phone, request.Email, request.Memo);
return CreatedAtAction(nameof(GetById), new { id }, new { message = "회사가 등록되었습니다.", id });
}
catch (ValidationException ex)
{
return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest });
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails { Title = "회사 등록 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError });
}
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, [FromBody] UpdateCompanyRequest request)
{
try
{
await companyService.UpdateAsync(id, request.CompanyCode, request.CompanyName,
request.ContactPerson, request.Phone, request.Email, request.Memo, request.IsActive);
return Ok(new { message = "회사가 수정되었습니다." });
}
catch (ValidationException ex)
{
return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest });
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails { Title = "회사 수정 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError });
}
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
try
{
await companyService.DeleteAsync(id);
return Ok(new { message = "회사가 삭제되었습니다." });
}
catch (ValidationException ex)
{
return BadRequest(new ProblemDetails { Title = ex.Message, Status = StatusCodes.Status400BadRequest });
}
catch (Exception ex)
{
return StatusCode(500, new ProblemDetails { Title = "회사 삭제 실패", Detail = ex.Message, Status = StatusCodes.Status500InternalServerError });
}
}
public record CreateCompanyRequest(string CompanyCode, string CompanyName, string? ContactPerson, string? Phone, string? Email, string? Memo);
public record UpdateCompanyRequest(string CompanyCode, string CompanyName, string? ContactPerson, string? Phone, string? Email, string? Memo, bool IsActive);
}
@@ -1,43 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class SiteSettingsController : ControllerBase
{
private readonly SiteSettingService _siteSettingService;
public SiteSettingsController(SiteSettingService siteSettingService)
{
_siteSettingService = siteSettingService;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var settings = await _siteSettingService.GetAllAsync();
return Ok(settings);
}
[HttpPut]
public async Task<IActionResult> Save([FromBody] SaveSiteSettingsRequest request)
{
if (request is null)
return BadRequest(new { message = "요청 본문이 비어 있습니다." });
await _siteSettingService.SaveAsync(request.Phone, request.Email, request.KakaoUrl, request.InstagramUrl);
return Ok(new { message = "사이트 설정이 저장되었습니다." });
}
}
public class SaveSiteSettingsRequest
{
public string Phone { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string KakaoUrl { get; set; } = string.Empty;
public string InstagramUrl { get; set; } = string.Empty;
}
@@ -1,84 +0,0 @@
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();
}
}