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
@@ -0,0 +1,51 @@
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using InquiryEntity = TaxBaik.Domain.Entities.Inquiry;
namespace TaxBaik.Web.Endpoints.AdminDashboard;
public class AdminDashboardSummaryResponse
{
public int ThisMonthInquiries { get; set; }
public int NewInquiries { get; set; }
public int TotalPosts { get; set; }
public int PublishedPosts { get; set; }
public List<InquiryEntity> RecentInquiries { get; set; } = [];
}
public class UpcomingFilingsResponse
{
public List<object> Data { get; set; } = [];
public int Days { get; set; }
}
public class UpcomingFilingsQuery
{
public int Days { get; set; } = 30;
}
public class RecentInquiriesResponse
{
public List<InquiryEntity> Data { get; set; } = [];
public int Limit { get; set; }
}
public class RecentInquiriesQuery
{
public int Limit { get; set; } = 10;
}
public class MonthlyStatsQuery
{
public string? Month { get; set; }
}
public class MonthlyStatsResponse
{
public string Month { get; set; } = string.Empty;
public int TotalInquiries { get; set; }
public int ConsultingCount { get; set; }
public int CompletedCount { get; set; }
public int NewCount { get; set; }
public double CompletionRate { get; set; }
}
@@ -0,0 +1,44 @@
using FastEndpoints;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Endpoints.AdminDashboard;
public class GetMonthlyStatsEndpoint : Endpoint<MonthlyStatsQuery, MonthlyStatsResponse>
{
private readonly AdminDashboardService _dashboardService;
public GetMonthlyStatsEndpoint(AdminDashboardService dashboardService)
{
_dashboardService = dashboardService;
}
public override void Configure()
{
Get("/api/admin-dashboard/monthly-stats");
Policies("Bearer");
}
public override async Task HandleAsync(MonthlyStatsQuery request, CancellationToken ct)
{
try
{
var stats = await _dashboardService.GetMonthlyStatsAsync(request.Month, ct);
// Convert dynamic result to typed response
var statsDict = (dynamic)stats;
await SendAsync(new MonthlyStatsResponse
{
Month = statsDict.month,
TotalInquiries = statsDict.totalInquiries,
ConsultingCount = statsDict.consultingCount,
CompletedCount = statsDict.completedCount,
NewCount = statsDict.newCount,
CompletionRate = statsDict.completionRate
}, 200, cancellation: ct);
}
catch (Exception ex)
{
await SendErrorsAsync(500, ct);
}
}
}
@@ -0,0 +1,40 @@
using FastEndpoints;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Endpoints.AdminDashboard;
public class GetRecentInquiriesEndpoint : Endpoint<RecentInquiriesQuery, RecentInquiriesResponse>
{
private readonly AdminDashboardService _dashboardService;
public GetRecentInquiriesEndpoint(AdminDashboardService dashboardService)
{
_dashboardService = dashboardService;
}
public override void Configure()
{
Get("/api/admin-dashboard/recent-inquiries");
Policies("Bearer");
}
public override async Task HandleAsync(RecentInquiriesQuery request, CancellationToken ct)
{
try
{
var limit = request.Limit <= 0 ? 10 : request.Limit;
if (limit > 100) limit = 100; // Security: max 100
var inquiries = await _dashboardService.GetRecentInquiriesAsync(limit, ct);
await SendAsync(new RecentInquiriesResponse
{
Data = inquiries.ToList(),
Limit = limit
}, 200, cancellation: ct);
}
catch (Exception ex)
{
await SendErrorsAsync(500, ct);
}
}
}
@@ -0,0 +1,40 @@
using FastEndpoints;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Endpoints.AdminDashboard;
public class GetSummaryEndpoint : EndpointWithoutRequest<AdminDashboardSummaryResponse>
{
private readonly AdminDashboardService _dashboardService;
public GetSummaryEndpoint(AdminDashboardService dashboardService)
{
_dashboardService = dashboardService;
}
public override void Configure()
{
Get("/api/admin-dashboard/summary");
Policies("Bearer");
}
public override async Task HandleAsync(CancellationToken ct)
{
try
{
var summary = await _dashboardService.GetSummaryAsync(ct);
await SendAsync(new AdminDashboardSummaryResponse
{
ThisMonthInquiries = summary.ThisMonthInquiries,
NewInquiries = summary.NewInquiries,
TotalPosts = summary.TotalPosts,
PublishedPosts = summary.PublishedPosts,
RecentInquiries = summary.RecentInquiries.ToList()
}, 200, cancellation: ct);
}
catch (Exception ex)
{
await SendErrorsAsync(500, ct);
}
}
}
@@ -0,0 +1,38 @@
using FastEndpoints;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Endpoints.AdminDashboard;
public class GetUpcomingFilingsEndpoint : Endpoint<UpcomingFilingsQuery, UpcomingFilingsResponse>
{
private readonly TaxFilingService _taxFilingService;
public GetUpcomingFilingsEndpoint(TaxFilingService taxFilingService)
{
_taxFilingService = taxFilingService;
}
public override void Configure()
{
Get("/api/admin-dashboard/upcoming-filings");
Policies("Bearer");
}
public override async Task HandleAsync(UpcomingFilingsQuery request, CancellationToken ct)
{
try
{
var days = request.Days <= 0 ? 30 : request.Days;
var filings = await _taxFilingService.GetUpcomingAsync(days);
await SendAsync(new UpcomingFilingsResponse
{
Data = filings.Cast<object>().ToList(),
Days = days
}, 200, cancellation: ct);
}
catch (Exception ex)
{
await SendErrorsAsync(500, ct);
}
}
}
@@ -0,0 +1,80 @@
using FastEndpoints;
namespace TaxBaik.Web.Endpoints.ClientLogs;
public 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; }
}
public class PostLogEndpoint : Endpoint<ClientLogEntry, EmptyResponse>
{
private readonly ILogger<PostLogEndpoint> _logger;
public PostLogEndpoint(ILogger<PostLogEndpoint> logger)
{
_logger = logger;
}
public override void Configure()
{
Post("/api/client-logs");
AllowAnonymous();
RateLimit(limit: 10, window: 60); // 10 requests per 60 seconds
}
public override async Task HandleAsync(ClientLogEntry entry, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(entry.Message))
{
ThrowError("Message is required");
}
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);
}
await SendAsync(new EmptyResponse(), 200, cancellation: ct);
}
}
@@ -0,0 +1,26 @@
using FastEndpoints;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Endpoints.SiteSettings;
public class GetEndpoint : Endpoint<EmptyRequest, IReadOnlyDictionary<string, string>>
{
private readonly SiteSettingService _siteSettingService;
public GetEndpoint(SiteSettingService siteSettingService)
{
_siteSettingService = siteSettingService;
}
public override void Configure()
{
Get("/api/sitesettings");
Policies("Bearer");
}
public override async Task HandleAsync(EmptyRequest _, CancellationToken ct)
{
var settings = await _siteSettingService.GetAllAsync(ct);
await SendAsync(settings, 200, cancellation: ct);
}
}
@@ -0,0 +1,48 @@
using FastEndpoints;
using TaxBaik.Application.Services;
namespace TaxBaik.Web.Endpoints.SiteSettings;
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;
}
public class SaveResponse
{
public string Message { get; set; } = string.Empty;
}
public class SaveEndpoint : Endpoint<SaveSiteSettingsRequest, SaveResponse>
{
private readonly SiteSettingService _siteSettingService;
public SaveEndpoint(SiteSettingService siteSettingService)
{
_siteSettingService = siteSettingService;
}
public override void Configure()
{
Put("/api/sitesettings");
Policies("Bearer");
}
public override async Task HandleAsync(SaveSiteSettingsRequest request, CancellationToken ct)
{
if (request == null)
{
ThrowError("요청 본문이 비어 있습니다.");
}
await _siteSettingService.SaveAsync(request.Phone, request.Email, request.KakaoUrl, request.InstagramUrl, ct);
await SendAsync(new SaveResponse
{
Message = "사이트 설정이 저장되었습니다."
}, 200, cancellation: ct);
}
}