From b06c0f99fb56389e8d9c0fce0aa910d69fd7f22f Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Thu, 2 Jul 2026 11:08:39 +0900 Subject: [PATCH] feat(blog): add archived post restore workflow --- TaxBaik.Application.Tests/BlogServiceTests.cs | 7 +++ TaxBaik.Application/Services/BlogService.cs | 4 ++ .../Interfaces/IBlogPostRepository.cs | 2 + .../Repositories/BlogPostRepository.cs | 24 ++++++++++ .../Services/BlogBrowserClient.cs | 16 +++++++ .../Admin/Pages/Blog/BlogList.razor | 47 +++++++++++++++++-- TaxBaik.Web/Controllers/BlogController.cs | 8 ++++ 7 files changed, 105 insertions(+), 3 deletions(-) diff --git a/TaxBaik.Application.Tests/BlogServiceTests.cs b/TaxBaik.Application.Tests/BlogServiceTests.cs index 72f5ca1..4a1a6c8 100644 --- a/TaxBaik.Application.Tests/BlogServiceTests.cs +++ b/TaxBaik.Application.Tests/BlogServiceTests.cs @@ -93,6 +93,13 @@ public class BlogServiceTests return Task.FromResult<(IEnumerable, int)>((items, items.Count)); } + public Task<(IEnumerable Items, int Total)> GetArchivedPagedAsync( + int page, int pageSize, CancellationToken cancellationToken = default) + { + var items = Posts.Where(x => x.DeletedAt != null).ToList(); + return Task.FromResult<(IEnumerable, int)>((items, items.Count)); + } + public Task CreateAsync(BlogPost post, CancellationToken cancellationToken = default) { post.Id = Posts.Count + 1; diff --git a/TaxBaik.Application/Services/BlogService.cs b/TaxBaik.Application/Services/BlogService.cs index 89b7e91..e95a170 100644 --- a/TaxBaik.Application/Services/BlogService.cs +++ b/TaxBaik.Application/Services/BlogService.cs @@ -42,6 +42,10 @@ public class BlogService(IBlogPostRepository repository, IMemoryCache memoryCach int page, int pageSize, CancellationToken ct = default) => await repository.GetAdminPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), ct); + public async Task<(IEnumerable, int)> GetArchivedPagedAsync( + int page, int pageSize, CancellationToken ct = default) => + await repository.GetArchivedPagedAsync(NormalizePage(page), NormalizePageSize(pageSize), ct); + public async Task CreateAsync(BlogPost post, CancellationToken ct = default) { ValidatePost(post); diff --git a/TaxBaik.Domain/Interfaces/IBlogPostRepository.cs b/TaxBaik.Domain/Interfaces/IBlogPostRepository.cs index 48ebce5..b2a1dce 100644 --- a/TaxBaik.Domain/Interfaces/IBlogPostRepository.cs +++ b/TaxBaik.Domain/Interfaces/IBlogPostRepository.cs @@ -12,6 +12,8 @@ public interface IBlogPostRepository Task> GetAllForAdminAsync(CancellationToken cancellationToken = default); Task<(IEnumerable Items, int Total)> GetAdminPagedAsync( int page, int pageSize, CancellationToken cancellationToken = default); + Task<(IEnumerable Items, int Total)> GetArchivedPagedAsync( + int page, int pageSize, CancellationToken cancellationToken = default); Task CreateAsync(BlogPost post, CancellationToken cancellationToken = default); Task UpdateAsync(BlogPost post, CancellationToken cancellationToken = default); Task DeleteAsync(int id, CancellationToken cancellationToken = default); diff --git a/TaxBaik.Infrastructure/Repositories/BlogPostRepository.cs b/TaxBaik.Infrastructure/Repositories/BlogPostRepository.cs index 79b08f5..bebf8b6 100644 --- a/TaxBaik.Infrastructure/Repositories/BlogPostRepository.cs +++ b/TaxBaik.Infrastructure/Repositories/BlogPostRepository.cs @@ -111,6 +111,30 @@ public class BlogPostRepository(IDbConnectionFactory connectionFactory) : BaseRe return (items, total); } + public async Task<(IEnumerable Items, int Total)> GetArchivedPagedAsync( + int page, int pageSize, CancellationToken cancellationToken = default) + { + using var conn = Conn(); + var offset = (page - 1) * pageSize; + + using var reader = await conn.QueryMultipleAsync( + @"SELECT bp.id, bp.title, bp.content, bp.slug, bp.category_id, bp.tags, bp.author_id, + bp.published_at, bp.view_count, bp.seo_title, bp.seo_description, bp.thumbnail_url, + bp.is_published, bp.created_at, bp.updated_at, bp.deleted_at, c.name AS category_name + FROM blog_posts bp + LEFT JOIN categories c ON bp.category_id = c.id + WHERE bp.deleted_at IS NOT NULL + ORDER BY bp.deleted_at DESC + LIMIT @PageSize OFFSET @Offset; + + SELECT COUNT(*) FROM blog_posts WHERE deleted_at IS NOT NULL;", + new { PageSize = pageSize, Offset = offset }); + + var items = (await reader.ReadAsync()).ToList(); + var total = await reader.ReadFirstAsync(); + return (items, total); + } + public async Task CreateAsync(BlogPost post, CancellationToken cancellationToken = default) { using var conn = Conn(); diff --git a/TaxBaik.Web.Client/Services/BlogBrowserClient.cs b/TaxBaik.Web.Client/Services/BlogBrowserClient.cs index 0a24f03..e7d66ad 100644 --- a/TaxBaik.Web.Client/Services/BlogBrowserClient.cs +++ b/TaxBaik.Web.Client/Services/BlogBrowserClient.cs @@ -6,10 +6,12 @@ using TaxBaik.Application.DTOs; public interface IBlogBrowserClient { Task<(IEnumerable Items, int Total)> GetAdminPagedAsync(int page = 1, int pageSize = 20, CancellationToken ct = default); + Task<(IEnumerable Items, int Total)> GetArchivedPagedAsync(int page = 1, int pageSize = 20, CancellationToken ct = default); Task GetByIdAsync(int id, CancellationToken ct = default); Task CreateAsync(CreateBlogPostDto dto, CancellationToken ct = default); Task UpdateAsync(int id, CreateBlogPostDto dto, CancellationToken ct = default); Task DeleteAsync(int id, CancellationToken ct = default); + Task RestoreAsync(int id, CancellationToken ct = default); Task TogglePublishAsync(int id, CreateBlogPostDto dto, CancellationToken ct = default); } @@ -41,6 +43,13 @@ public class BlogBrowserClient : IBlogBrowserClient return result != null ? (result.Data, result.Total) : ([], 0); } + public async Task<(IEnumerable Items, int Total)> GetArchivedPagedAsync(int page = 1, int pageSize = 20, CancellationToken ct = default) + { + EnsureAuthHeader(); + var result = await _http.GetFromJsonAsync($"blog/admin/archived?page={page}&pageSize={pageSize}", ct); + return result != null ? (result.Data, result.Total) : ([], 0); + } + public async Task GetByIdAsync(int id, CancellationToken ct = default) { EnsureAuthHeader(); @@ -74,6 +83,13 @@ public class BlogBrowserClient : IBlogBrowserClient return response.IsSuccessStatusCode; } + public async Task RestoreAsync(int id, CancellationToken ct = default) + { + EnsureAuthHeader(); + var response = await _http.PostAsync($"blog/{id}/restore", null, ct); + return response.IsSuccessStatusCode; + } + public async Task TogglePublishAsync(int id, CreateBlogPostDto dto, CancellationToken ct = default) { var result = await UpdateAsync(id, dto, ct); diff --git a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor index c9266a0..874d650 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Blog/BlogList.razor @@ -7,6 +7,14 @@ + + @(showArchived ? "전체 글 보기" : "숨김 글 보기") + + + 새로고침 + 새 포스트 작성 @@ -39,8 +47,16 @@ 수정하기 - 삭제 + @if (showArchived) + { + 복원 + } + else + { + 삭제 + } @@ -61,6 +77,7 @@ private int currentPage = 1; private int totalPages = 1; private int totalPosts = 0; + private bool showArchived; private const int PageSize = 20; private IEnumerable FilteredPosts => posts @@ -85,7 +102,9 @@ isLoading = true; try { - var result = await BlogClient.GetAdminPagedAsync(currentPage, PageSize); + var result = showArchived + ? await BlogClient.GetArchivedPagedAsync(currentPage, PageSize) + : await BlogClient.GetAdminPagedAsync(currentPage, PageSize); posts = result.Items.ToList(); totalPosts = result.Total; totalPages = Math.Max(1, (int)Math.Ceiling(totalPosts / (double)PageSize)); @@ -155,4 +174,26 @@ Snackbar.Add("포스트가 삭제되었습니다.", Severity.Success); await LoadPosts(); } + + private async Task RestorePost(int postId) + { + var restored = await BlogClient.RestoreAsync(postId); + if (!restored) + { + Snackbar.Add("포스트 복원에 실패했습니다.", Severity.Error); + return; + } + + Snackbar.Add("포스트가 복원되었습니다.", Severity.Success); + await LoadPosts(); + } + + private async Task ToggleArchiveView() + { + showArchived = !showArchived; + currentPage = 1; + await LoadPosts(); + } + + private async Task Reload() => await LoadPosts(); } diff --git a/TaxBaik.Web/Controllers/BlogController.cs b/TaxBaik.Web/Controllers/BlogController.cs index 60c6c15..90f58b1 100644 --- a/TaxBaik.Web/Controllers/BlogController.cs +++ b/TaxBaik.Web/Controllers/BlogController.cs @@ -58,6 +58,14 @@ public class BlogController : ControllerBase return Ok(new { data = items, total, page, pageSize }); } + [HttpGet("admin/archived")] + [Authorize] + public async Task GetArchivedPaged([FromQuery] int page = 1, [FromQuery] int pageSize = 20) + { + var (items, total) = await _blogService.GetArchivedPagedAsync(page, pageSize); + return Ok(new { data = items, total, page, pageSize }); + } + [HttpPost] [Authorize] public async Task Create([FromBody] CreateBlogPostDto dto)