Fix blog category selection and empty state rendering
TaxBaik CI/CD / build-and-deploy (push) Successful in 53s

This commit is contained in:
2026-07-09 15:32:13 +09:00
parent c5e6e4ba57
commit 2f72834317
9 changed files with 91 additions and 10 deletions
@@ -4,7 +4,7 @@ public class CreateBlogPostDto
{
public required string Title { get; set; }
public required string Content { get; set; }
public int? CategoryId { get; set; }
public int CategoryId { get; set; }
public string? Tags { get; set; }
public string? SeoTitle { get; set; }
public string? SeoDescription { get; set; }
@@ -18,7 +18,7 @@ public class BlogPostResponseDto
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public int? CategoryId { get; set; }
public int CategoryId { get; set; }
public string? Tags { get; set; }
public string? SeoTitle { get; set; }
public string? SeoDescription { get; set; }
@@ -8,6 +8,7 @@ public sealed class CreateBlogPostDtoValidator : AbstractValidator<CreateBlogPos
{
RuleFor(x => x.Title).NotEmpty().MaximumLength(ValidationRules.TitleMaxLength);
RuleFor(x => x.Content).NotEmpty().MinimumLength(ValidationRules.MessageMinLength).MaximumLength(ValidationRules.ContentMaxLength);
RuleFor(x => x.CategoryId).GreaterThan(0);
RuleFor(x => x.Tags).MaximumLength(ValidationRules.TagsMaxLength);
RuleFor(x => x.SeoTitle).MaximumLength(ValidationRules.SeoTitleMaxLength);
RuleFor(x => x.SeoDescription).MaximumLength(ValidationRules.SeoDescriptionMaxLength);
@@ -2,6 +2,7 @@
@model TaxBaik.Web.Pages.Admin.Blog.CreateModel
@{
ViewData["Title"] = "블로그 등록";
ViewData["BlogCategories"] = Model.Categories;
}
@await Html.PartialAsync("_BlogEditor", new TaxBaik.Web.Pages.Shared.BlogEditorModel(
@@ -3,12 +3,13 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.DTOs;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class CreateModel(BlogService blogService) : PageModel
public class CreateModel(BlogService blogService, CategoryService categoryService) : PageModel
{
[BindProperty]
public CreateBlogPostDto Input { get; set; } = new()
@@ -17,14 +18,47 @@ public class CreateModel(BlogService blogService) : PageModel
Title = string.Empty
};
public IActionResult OnGet() => Page();
public IReadOnlyList<Category> Categories { get; private set; } = [];
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
{
Categories = (await EnsureCategoriesAsync(ct)).ToList();
if (Categories.Count > 0)
Input.CategoryId = Categories[0].Id;
return Page();
}
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
{
Categories = (await EnsureCategoriesAsync(ct)).ToList();
if (!ModelState.IsValid)
return Page();
await blogService.CreateAsync(Input, ct);
return RedirectToPage("/Admin/Blog/Index");
}
private async Task<IEnumerable<Category>> EnsureCategoriesAsync(CancellationToken ct)
{
var categories = (await categoryService.GetAllAsync(ct)).ToList();
if (categories.Count > 0)
return categories;
var defaults = new[]
{
("사업자 세무", "business-tax", 1),
("부동산 세금", "real-estate-tax", 2),
("종합소득세", "income-tax", 3),
("부가가치세", "vat", 4),
("가족자산·증여", "family-asset", 5)
};
foreach (var (name, slug, sortOrder) in defaults)
{
await categoryService.CreateAsync(name, null, ct);
}
return await categoryService.GetAllAsync(ct);
}
}
@@ -3,6 +3,7 @@
@{
ViewData["Title"] = "블로그 수정";
ViewData["BlogEditorId"] = Model.Id;
ViewData["BlogCategories"] = Model.Categories;
}
@await Html.PartialAsync("_BlogEditor", new TaxBaik.Web.Pages.Shared.BlogEditorModel(
@@ -3,12 +3,13 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.DTOs;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class EditModel(BlogService blogService) : PageModel
public class EditModel(BlogService blogService, CategoryService categoryService) : PageModel
{
[BindProperty(SupportsGet = true)]
public int Id { get; set; }
@@ -20,17 +21,20 @@ public class EditModel(BlogService blogService) : PageModel
Title = string.Empty
};
public IReadOnlyList<Category> Categories { get; private set; } = [];
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
{
var post = await blogService.GetByIdAsync(Id, ct);
if (post is null)
return NotFound();
Categories = (await EnsureCategoriesAsync(ct)).ToList();
Input = new CreateBlogPostDto
{
Title = post.Title,
Content = post.Content,
CategoryId = post.CategoryId,
CategoryId = post.CategoryId ?? 0,
Tags = post.Tags,
SeoTitle = post.SeoTitle,
SeoDescription = post.SeoDescription,
@@ -44,6 +48,7 @@ public class EditModel(BlogService blogService) : PageModel
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
{
Categories = (await EnsureCategoriesAsync(ct)).ToList();
if (!ModelState.IsValid)
return Page();
@@ -59,4 +64,27 @@ public class EditModel(BlogService blogService) : PageModel
await blogService.DeleteAsync(Id, ct);
return RedirectToPage("/Admin/Blog/Index");
}
private async Task<IEnumerable<Category>> EnsureCategoriesAsync(CancellationToken ct)
{
var categories = (await categoryService.GetAllAsync(ct)).ToList();
if (categories.Count > 0)
return categories;
var defaults = new[]
{
("사업자 세무", "business-tax", 1),
("부동산 세금", "real-estate-tax", 2),
("종합소득세", "income-tax", 3),
("부가가치세", "vat", 4),
("가족자산·증여", "family-asset", 5)
};
foreach (var (name, slug, sortOrder) in defaults)
{
await categoryService.CreateAsync(name, null, ct);
}
return await categoryService.GetAllAsync(ct);
}
}
@@ -3,7 +3,7 @@ namespace TaxBaik.Web.Pages.Shared;
public sealed record BlogEditorModel(
string Title,
string Content,
int? CategoryId,
int CategoryId,
bool IsPublished,
string? ThumbnailUrl,
string? SeoTitle,
@@ -43,7 +43,20 @@
<div class="row g-3 blog-meta-grid">
<div class="col-md-4">
<label class="form-label" for="Input_CategoryId">카테고리</label>
<input class="form-control" id="Input_CategoryId" name="Input.CategoryId" value="@Model.CategoryId" />
<select class="form-select" id="Input_CategoryId" name="Input.CategoryId" required>
@foreach (var category in (ViewData["BlogCategories"] as IEnumerable<TaxBaik.Domain.Entities.Category>) ?? [])
{
if (category.Id == Model.CategoryId)
{
<option value="@category.Id" selected>@category.Name</option>
}
else
{
<option value="@category.Id">@category.Name</option>
}
}
</select>
<span class="text-danger small" data-valmsg-for="Input.CategoryId" data-valmsg-replace="true"></span>
</div>
<div class="col-md-4">
<label class="form-label d-block" for="Input_IsPublished">발행 여부</label>
@@ -85,7 +98,7 @@
<div>
<div class="text-muted small text-uppercase mb-1">불러온 원본</div>
<div class="fw-semibold">@Model.Title</div>
<div class="text-muted small">카테고리: @(Model.CategoryId?.ToString() ?? "미지정")</div>
<div class="text-muted small">카테고리: @Model.CategoryId</div>
<div class="text-muted small">발행: @(Model.IsPublished ? "예" : "아니오")</div>
</div>
<article class="content-prose blog-preview-pane" data-blog-preview>@Html.Raw(Model.Content)</article>
@@ -1,4 +1,7 @@
@model string
@{
var emptyMessage = string.IsNullOrWhiteSpace(Model) ? "데이터가 없습니다." : Model;
}
<div class="empty py-5">
<div class="empty-img">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database-off" width="48" height="48" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
@@ -8,5 +11,5 @@
<path d="M3 3l18 18" />
</svg>
</div>
<p class="empty-title">@string.IsNullOrWhiteSpace(Model) ? "데이터가 없습니다." : Model</p>
<p class="empty-title">@emptyMessage</p>
</div>