Fix blog category selection and empty state rendering
TaxBaik CI/CD / build-and-deploy (push) Successful in 53s
TaxBaik CI/CD / build-and-deploy (push) Successful in 53s
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user