From c92118ab32860fcf71cf588f9fffb7d58a88ced0 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Fri, 3 Jul 2026 17:17:30 +0900 Subject: [PATCH] feat: migrate BlogController to FastEndpoints Endpoints (Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMPLEMENTATION: - Create 10 FastEndpoints Endpoint classes for Blog API: - GetPublishedEndpoint: GET /api/blog (public, paginated) - GetBySlugEndpoint: GET /api/blog/{slug} (public) - GetByIdEndpoint: GET /api/blog/admin/{id} (auth) - GetAllEndpoint: GET /api/blog/admin/all (auth) - GetAdminPagedEndpoint: GET /api/blog/admin (auth, paginated) - GetArchivedPagedEndpoint: GET /api/blog/admin/archived (auth, paginated) - CreateEndpoint: POST /api/blog (auth) - UpdateEndpoint: PUT /api/blog/{id} (auth) - DeleteEndpoint: DELETE /api/blog/{id} (auth, archives post) - RestoreEndpoint: POST /api/blog/{id}/restore (auth) - Create BlogDtos.cs with shared response types: - BlogPublishedQuery / BlogAdminQuery (query parameters) - PaginatedResponse (generic pagination response) - BlogPostListResponse (list response) - MessageResponse (simple message) - Backup BlogController.cs (no longer active) ARCHITECTURE: - All endpoints use Endpoint pattern - BlogService injected via constructor DI - Proper error handling with ThrowError() - Authorization via Policies("Bearer") for protected endpoints - AllowAnonymous() for public endpoints VERIFICATION: ✅ dotnet build: 0 errors, 0 warnings ✅ dotnet test: 26/26 passed ✅ FastEndpoints auto-discovery working Next Phase: Migrate remaining Controllers (18 total - 2 done = 16 remaining) Co-Authored-By: Claude Haiku 4.5 --- ...logController.cs => BlogController.cs.bak} | 0 src/TaxBaik.Web/Endpoints/Blog/BlogDtos.cs | 33 +++++++++++++++ .../Endpoints/Blog/CreateEndpoint.cs | 34 ++++++++++++++++ .../Endpoints/Blog/DeleteEndpoint.cs | 27 +++++++++++++ .../Endpoints/Blog/GetAdminPagedEndpoint.cs | 32 +++++++++++++++ .../Endpoints/Blog/GetAllEndpoint.cs | 29 ++++++++++++++ .../Blog/GetArchivedPagedEndpoint.cs | 32 +++++++++++++++ .../Endpoints/Blog/GetByIdEndpoint.cs | 32 +++++++++++++++ .../Endpoints/Blog/GetBySlugEndpoint.cs | 32 +++++++++++++++ .../Endpoints/Blog/GetPublishedEndpoint.cs | 32 +++++++++++++++ .../Endpoints/Blog/RestoreEndpoint.cs | 27 +++++++++++++ .../Endpoints/Blog/UpdateEndpoint.cs | 40 +++++++++++++++++++ 12 files changed, 350 insertions(+) rename src/TaxBaik.Web/Controllers/{BlogController.cs => BlogController.cs.bak} (100%) create mode 100644 src/TaxBaik.Web/Endpoints/Blog/BlogDtos.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/CreateEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/DeleteEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/GetAdminPagedEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/GetAllEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/GetArchivedPagedEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/GetByIdEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/GetBySlugEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/GetPublishedEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/RestoreEndpoint.cs create mode 100644 src/TaxBaik.Web/Endpoints/Blog/UpdateEndpoint.cs diff --git a/src/TaxBaik.Web/Controllers/BlogController.cs b/src/TaxBaik.Web/Controllers/BlogController.cs.bak similarity index 100% rename from src/TaxBaik.Web/Controllers/BlogController.cs rename to src/TaxBaik.Web/Controllers/BlogController.cs.bak diff --git a/src/TaxBaik.Web/Endpoints/Blog/BlogDtos.cs b/src/TaxBaik.Web/Endpoints/Blog/BlogDtos.cs new file mode 100644 index 0000000..de13b4d --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/BlogDtos.cs @@ -0,0 +1,33 @@ +using TaxBaik.Application.DTOs; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class BlogPublishedQuery +{ + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 12; +} + +public class BlogAdminQuery +{ + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 20; +} + +public class PaginatedResponse +{ + public List Data { get; set; } = []; + public int Total { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } +} + +public class BlogPostListResponse +{ + public List Posts { get; set; } = []; +} + +public class MessageResponse +{ + public string Message { get; set; } = string.Empty; +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/CreateEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/CreateEndpoint.cs new file mode 100644 index 0000000..3632e84 --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/CreateEndpoint.cs @@ -0,0 +1,34 @@ +using FastEndpoints; +using TaxBaik.Application.DTOs; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class CreateEndpoint : Endpoint +{ + private readonly BlogService _blogService; + + public CreateEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Post("/api/blog"); + Policies("Bearer"); + } + + public override async Task HandleAsync(CreateBlogPostDto request, CancellationToken ct) + { + try + { + var result = await _blogService.CreateAsync(request); + await SendAsync(result, 201, cancellation: ct); + } + catch (ValidationException ex) + { + ThrowError(ex.Message, statusCode: 400); + } + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/DeleteEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/DeleteEndpoint.cs new file mode 100644 index 0000000..8491e47 --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/DeleteEndpoint.cs @@ -0,0 +1,27 @@ +using FastEndpoints; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class DeleteEndpoint : Endpoint +{ + private readonly BlogService _blogService; + + public DeleteEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Delete("/api/blog/{id}"); + Policies("Bearer"); + } + + public override async Task HandleAsync(EmptyRequest _, CancellationToken ct) + { + var id = Route("id"); + await _blogService.ArchiveAsync(id); + await SendAsync(new EmptyResponse(), 204, cancellation: ct); + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/GetAdminPagedEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/GetAdminPagedEndpoint.cs new file mode 100644 index 0000000..42edf43 --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/GetAdminPagedEndpoint.cs @@ -0,0 +1,32 @@ +using FastEndpoints; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class GetAdminPagedEndpoint : Endpoint> +{ + private readonly BlogService _blogService; + + public GetAdminPagedEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Get("/api/blog/admin"); + Policies("Bearer"); + } + + public override async Task HandleAsync(BlogAdminQuery request, CancellationToken ct) + { + var (items, total) = await _blogService.GetAdminPagedAsync(request.Page, request.PageSize); + await SendAsync(new PaginatedResponse + { + Data = items.Cast().ToList(), + Total = total, + Page = request.Page, + PageSize = request.PageSize + }, 200, cancellation: ct); + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/GetAllEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/GetAllEndpoint.cs new file mode 100644 index 0000000..55852e9 --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/GetAllEndpoint.cs @@ -0,0 +1,29 @@ +using FastEndpoints; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class GetAllEndpoint : Endpoint +{ + private readonly BlogService _blogService; + + public GetAllEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Get("/api/blog/admin/all"); + Policies("Bearer"); + } + + public override async Task HandleAsync(EmptyRequest _, CancellationToken ct) + { + var posts = await _blogService.GetAllAsync(); + await SendAsync(new BlogPostListResponse + { + Posts = posts.Cast().ToList() + }, 200, cancellation: ct); + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/GetArchivedPagedEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/GetArchivedPagedEndpoint.cs new file mode 100644 index 0000000..4833b74 --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/GetArchivedPagedEndpoint.cs @@ -0,0 +1,32 @@ +using FastEndpoints; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class GetArchivedPagedEndpoint : Endpoint> +{ + private readonly BlogService _blogService; + + public GetArchivedPagedEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Get("/api/blog/admin/archived"); + Policies("Bearer"); + } + + public override async Task HandleAsync(BlogAdminQuery request, CancellationToken ct) + { + var (items, total) = await _blogService.GetArchivedPagedAsync(request.Page, request.PageSize); + await SendAsync(new PaginatedResponse + { + Data = items.Cast().ToList(), + Total = total, + Page = request.Page, + PageSize = request.PageSize + }, 200, cancellation: ct); + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/GetByIdEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/GetByIdEndpoint.cs new file mode 100644 index 0000000..09d2b6d --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/GetByIdEndpoint.cs @@ -0,0 +1,32 @@ +using FastEndpoints; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class GetByIdEndpoint : Endpoint +{ + private readonly BlogService _blogService; + + public GetByIdEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Get("/api/blog/admin/{id}"); + Policies("Bearer"); + } + + public override async Task HandleAsync(EmptyRequest _, CancellationToken ct) + { + var id = Route("id"); + var post = await _blogService.GetByIdAsync(id); + if (post == null) + { + ThrowError("포스트를 찾을 수 없습니다.", statusCode: 404); + } + + await SendAsync(post, 200, cancellation: ct); + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/GetBySlugEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/GetBySlugEndpoint.cs new file mode 100644 index 0000000..ce4cdb2 --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/GetBySlugEndpoint.cs @@ -0,0 +1,32 @@ +using FastEndpoints; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class GetBySlugEndpoint : Endpoint +{ + private readonly BlogService _blogService; + + public GetBySlugEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Get("/api/blog/{slug}"); + AllowAnonymous(); + } + + public override async Task HandleAsync(EmptyRequest _, CancellationToken ct) + { + var slug = Route("slug") ?? string.Empty; + var post = await _blogService.GetBySlugAsync(slug); + if (post == null) + { + ThrowError("포스트를 찾을 수 없습니다.", statusCode: 404); + } + + await SendAsync(post, 200, cancellation: ct); + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/GetPublishedEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/GetPublishedEndpoint.cs new file mode 100644 index 0000000..58a32bc --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/GetPublishedEndpoint.cs @@ -0,0 +1,32 @@ +using FastEndpoints; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class GetPublishedEndpoint : Endpoint> +{ + private readonly BlogService _blogService; + + public GetPublishedEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Get("/api/blog"); + AllowAnonymous(); + } + + public override async Task HandleAsync(BlogPublishedQuery request, CancellationToken ct) + { + var (items, total) = await _blogService.GetPublishedPagedAsync(request.Page, request.PageSize); + await SendAsync(new PaginatedResponse + { + Data = items.Cast().ToList(), + Total = total, + Page = request.Page, + PageSize = request.PageSize + }, 200, cancellation: ct); + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/RestoreEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/RestoreEndpoint.cs new file mode 100644 index 0000000..180eef2 --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/RestoreEndpoint.cs @@ -0,0 +1,27 @@ +using FastEndpoints; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class RestoreEndpoint : Endpoint +{ + private readonly BlogService _blogService; + + public RestoreEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Post("/api/blog/{id}/restore"); + Policies("Bearer"); + } + + public override async Task HandleAsync(EmptyRequest _, CancellationToken ct) + { + var id = Route("id"); + await _blogService.RestoreAsync(id); + await SendAsync(new EmptyResponse(), 204, cancellation: ct); + } +} diff --git a/src/TaxBaik.Web/Endpoints/Blog/UpdateEndpoint.cs b/src/TaxBaik.Web/Endpoints/Blog/UpdateEndpoint.cs new file mode 100644 index 0000000..431b120 --- /dev/null +++ b/src/TaxBaik.Web/Endpoints/Blog/UpdateEndpoint.cs @@ -0,0 +1,40 @@ +using FastEndpoints; +using TaxBaik.Application.DTOs; +using TaxBaik.Application.Services; + +namespace TaxBaik.Web.Endpoints.Blog; + +public class UpdateEndpoint : Endpoint +{ + private readonly BlogService _blogService; + + public UpdateEndpoint(BlogService blogService) + { + _blogService = blogService; + } + + public override void Configure() + { + Put("/api/blog/{id}"); + Policies("Bearer"); + } + + public override async Task HandleAsync(CreateBlogPostDto request, CancellationToken ct) + { + var id = Route("id"); + try + { + var result = await _blogService.UpdateAsync(id, request); + if (result == null) + { + ThrowError("포스트를 찾을 수 없습니다.", statusCode: 404); + } + + await SendAsync(result, 200, cancellation: ct); + } + catch (ValidationException ex) + { + ThrowError(ex.Message, statusCode: 400); + } + } +}