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 Title { get; set; }
|
||||||
public required string Content { 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? Tags { get; set; }
|
||||||
public string? SeoTitle { get; set; }
|
public string? SeoTitle { get; set; }
|
||||||
public string? SeoDescription { get; set; }
|
public string? SeoDescription { get; set; }
|
||||||
@@ -18,7 +18,7 @@ public class BlogPostResponseDto
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
public string Content { 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? Tags { get; set; }
|
||||||
public string? SeoTitle { get; set; }
|
public string? SeoTitle { get; set; }
|
||||||
public string? SeoDescription { 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.Title).NotEmpty().MaximumLength(ValidationRules.TitleMaxLength);
|
||||||
RuleFor(x => x.Content).NotEmpty().MinimumLength(ValidationRules.MessageMinLength).MaximumLength(ValidationRules.ContentMaxLength);
|
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.Tags).MaximumLength(ValidationRules.TagsMaxLength);
|
||||||
RuleFor(x => x.SeoTitle).MaximumLength(ValidationRules.SeoTitleMaxLength);
|
RuleFor(x => x.SeoTitle).MaximumLength(ValidationRules.SeoTitleMaxLength);
|
||||||
RuleFor(x => x.SeoDescription).MaximumLength(ValidationRules.SeoDescriptionMaxLength);
|
RuleFor(x => x.SeoDescription).MaximumLength(ValidationRules.SeoDescriptionMaxLength);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
@model TaxBaik.Web.Pages.Admin.Blog.CreateModel
|
@model TaxBaik.Web.Pages.Admin.Blog.CreateModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "블로그 등록";
|
ViewData["Title"] = "블로그 등록";
|
||||||
|
ViewData["BlogCategories"] = Model.Categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
@await Html.PartialAsync("_BlogEditor", new TaxBaik.Web.Pages.Shared.BlogEditorModel(
|
@await Html.PartialAsync("_BlogEditor", new TaxBaik.Web.Pages.Shared.BlogEditorModel(
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using TaxBaik.Application.DTOs;
|
using TaxBaik.Application.DTOs;
|
||||||
using TaxBaik.Application.Services;
|
using TaxBaik.Application.Services;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
using TaxBaik.Web.Services;
|
using TaxBaik.Web.Services;
|
||||||
|
|
||||||
namespace TaxBaik.Web.Pages.Admin.Blog;
|
namespace TaxBaik.Web.Pages.Admin.Blog;
|
||||||
|
|
||||||
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
|
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
|
||||||
public class CreateModel(BlogService blogService) : PageModel
|
public class CreateModel(BlogService blogService, CategoryService categoryService) : PageModel
|
||||||
{
|
{
|
||||||
[BindProperty]
|
[BindProperty]
|
||||||
public CreateBlogPostDto Input { get; set; } = new()
|
public CreateBlogPostDto Input { get; set; } = new()
|
||||||
@@ -17,14 +18,47 @@ public class CreateModel(BlogService blogService) : PageModel
|
|||||||
Title = string.Empty
|
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)
|
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
Categories = (await EnsureCategoriesAsync(ct)).ToList();
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
return Page();
|
return Page();
|
||||||
|
|
||||||
await blogService.CreateAsync(Input, ct);
|
await blogService.CreateAsync(Input, ct);
|
||||||
return RedirectToPage("/Admin/Blog/Index");
|
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["Title"] = "블로그 수정";
|
||||||
ViewData["BlogEditorId"] = Model.Id;
|
ViewData["BlogEditorId"] = Model.Id;
|
||||||
|
ViewData["BlogCategories"] = Model.Categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
@await Html.PartialAsync("_BlogEditor", new TaxBaik.Web.Pages.Shared.BlogEditorModel(
|
@await Html.PartialAsync("_BlogEditor", new TaxBaik.Web.Pages.Shared.BlogEditorModel(
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using TaxBaik.Application.DTOs;
|
using TaxBaik.Application.DTOs;
|
||||||
using TaxBaik.Application.Services;
|
using TaxBaik.Application.Services;
|
||||||
|
using TaxBaik.Domain.Entities;
|
||||||
using TaxBaik.Web.Services;
|
using TaxBaik.Web.Services;
|
||||||
|
|
||||||
namespace TaxBaik.Web.Pages.Admin.Blog;
|
namespace TaxBaik.Web.Pages.Admin.Blog;
|
||||||
|
|
||||||
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
|
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
|
||||||
public class EditModel(BlogService blogService) : PageModel
|
public class EditModel(BlogService blogService, CategoryService categoryService) : PageModel
|
||||||
{
|
{
|
||||||
[BindProperty(SupportsGet = true)]
|
[BindProperty(SupportsGet = true)]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
@@ -20,17 +21,20 @@ public class EditModel(BlogService blogService) : PageModel
|
|||||||
Title = string.Empty
|
Title = string.Empty
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public IReadOnlyList<Category> Categories { get; private set; } = [];
|
||||||
|
|
||||||
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
|
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var post = await blogService.GetByIdAsync(Id, ct);
|
var post = await blogService.GetByIdAsync(Id, ct);
|
||||||
if (post is null)
|
if (post is null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
|
Categories = (await EnsureCategoriesAsync(ct)).ToList();
|
||||||
Input = new CreateBlogPostDto
|
Input = new CreateBlogPostDto
|
||||||
{
|
{
|
||||||
Title = post.Title,
|
Title = post.Title,
|
||||||
Content = post.Content,
|
Content = post.Content,
|
||||||
CategoryId = post.CategoryId,
|
CategoryId = post.CategoryId ?? 0,
|
||||||
Tags = post.Tags,
|
Tags = post.Tags,
|
||||||
SeoTitle = post.SeoTitle,
|
SeoTitle = post.SeoTitle,
|
||||||
SeoDescription = post.SeoDescription,
|
SeoDescription = post.SeoDescription,
|
||||||
@@ -44,6 +48,7 @@ public class EditModel(BlogService blogService) : PageModel
|
|||||||
|
|
||||||
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
|
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
Categories = (await EnsureCategoriesAsync(ct)).ToList();
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
return Page();
|
return Page();
|
||||||
|
|
||||||
@@ -59,4 +64,27 @@ public class EditModel(BlogService blogService) : PageModel
|
|||||||
await blogService.DeleteAsync(Id, ct);
|
await blogService.DeleteAsync(Id, ct);
|
||||||
return RedirectToPage("/Admin/Blog/Index");
|
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(
|
public sealed record BlogEditorModel(
|
||||||
string Title,
|
string Title,
|
||||||
string Content,
|
string Content,
|
||||||
int? CategoryId,
|
int CategoryId,
|
||||||
bool IsPublished,
|
bool IsPublished,
|
||||||
string? ThumbnailUrl,
|
string? ThumbnailUrl,
|
||||||
string? SeoTitle,
|
string? SeoTitle,
|
||||||
|
|||||||
@@ -43,7 +43,20 @@
|
|||||||
<div class="row g-3 blog-meta-grid">
|
<div class="row g-3 blog-meta-grid">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label" for="Input_CategoryId">카테고리</label>
|
<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>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label d-block" for="Input_IsPublished">발행 여부</label>
|
<label class="form-label d-block" for="Input_IsPublished">발행 여부</label>
|
||||||
@@ -85,7 +98,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="text-muted small text-uppercase mb-1">불러온 원본</div>
|
<div class="text-muted small text-uppercase mb-1">불러온 원본</div>
|
||||||
<div class="fw-semibold">@Model.Title</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 class="text-muted small">발행: @(Model.IsPublished ? "예" : "아니오")</div>
|
||||||
</div>
|
</div>
|
||||||
<article class="content-prose blog-preview-pane" data-blog-preview>@Html.Raw(Model.Content)</article>
|
<article class="content-prose blog-preview-pane" data-blog-preview>@Html.Raw(Model.Content)</article>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
@model string
|
@model string
|
||||||
|
@{
|
||||||
|
var emptyMessage = string.IsNullOrWhiteSpace(Model) ? "데이터가 없습니다." : Model;
|
||||||
|
}
|
||||||
<div class="empty py-5">
|
<div class="empty py-5">
|
||||||
<div class="empty-img">
|
<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">
|
<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" />
|
<path d="M3 3l18 18" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="empty-title">@string.IsNullOrWhiteSpace(Model) ? "데이터가 없습니다." : Model</p>
|
<p class="empty-title">@emptyMessage</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user