Implement admin CRUD for announcements inquiries and faqs
TaxBaik CI/CD / build-and-deploy (push) Successful in 54s

This commit is contained in:
2026-07-09 14:06:14 +09:00
parent bfc4d70ebd
commit e3addc1a79
19 changed files with 727 additions and 23 deletions
@@ -0,0 +1,47 @@
@page
@model TaxBaik.Web.Pages.Admin.Faqs.CreateModel
@{ ViewData["Title"] = "FAQ 작성"; }
<section class="admin-page-shell">
<partial name="_AdminPageHeader" model='("Support", "FAQ 작성", "Tabler 표준 폼으로 FAQ를 등록합니다.", null, null)' />
<form method="post" class="card">
<div class="card-body">
<partial name="_ValidationSummary" />
<div class="row g-3">
<div class="col-lg-8">
<label asp-for="Input.Question" class="form-label"></label>
<input asp-for="Input.Question" class="form-control" />
</div>
<div class="col-lg-4">
<label asp-for="Input.Category" class="form-label"></label>
<input asp-for="Input.Category" class="form-control" list="faq-categories" />
<datalist id="faq-categories">
@foreach (var category in Model.Categories)
{
<option value="@category"></option>
}
</datalist>
</div>
<div class="col-12">
<label asp-for="Input.Answer" class="form-label"></label>
<textarea asp-for="Input.Answer" class="form-control" rows="10"></textarea>
</div>
<div class="col-md-4">
<label asp-for="Input.SortOrder" class="form-label"></label>
<input asp-for="Input.SortOrder" class="form-control" type="number" />
</div>
<div class="col-12">
<label class="form-check">
<input asp-for="Input.IsActive" class="form-check-input" type="checkbox" />
<span class="form-check-label">활성</span>
</label>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end gap-2">
<a class="btn btn-outline-secondary" href="/admin/faqs">취소</a>
<button class="btn btn-primary" type="submit">저장</button>
</div>
</form>
</section>
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Domain.Entities;
using TaxBaik.Application.Services;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Faqs;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class CreateModel(FaqService faqService) : PageModel
{
[BindProperty]
public Faq Input { get; set; } = new();
public IReadOnlyList<string> Categories => FaqService.Categories;
public IActionResult OnGet()
{
Input.IsActive = true;
return Page();
}
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
{
if (!ModelState.IsValid)
return Page();
await faqService.CreateAsync(Input, ct);
return RedirectToPage("/Admin/Faqs/Index");
}
}
@@ -0,0 +1,43 @@
@page "{id:int}"
@model TaxBaik.Web.Pages.Admin.Faqs.DetailModel
@{ ViewData["Title"] = "FAQ 상세"; }
<section class="admin-page-shell">
<partial name="_AdminPageHeader" model='("Support", "FAQ 상세", "FAQ의 질문, 답변, 상태를 확인합니다.", null, null)' />
@if (Model.Item is null)
{
<partial name="_EmptyState" model="FAQ를 찾을 수 없습니다." />
}
else
{
<div class="row g-4">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<dl class="row details-grid mb-0">
<dt class="col-sm-3">질문</dt>
<dd class="col-sm-9">@Model.Item.Question</dd>
<dt class="col-sm-3">답변</dt>
<dd class="col-sm-9"><div class="pre-wrap">@Model.Item.Answer</div></dd>
<dt class="col-sm-3">카테고리</dt>
<dd class="col-sm-9">@Model.Item.Category</dd>
<dt class="col-sm-3">활성</dt>
<dd class="col-sm-9"><partial name="_StatusBadge" model="@(Model.Item.IsActive ? "active" : "inactive")" /></dd>
<dt class="col-sm-3">정렬</dt>
<dd class="col-sm-9">@Model.Item.SortOrder</dd>
</dl>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body d-grid gap-2">
<a class="btn btn-primary" href="/admin/faqs/@Model.Item.Id/edit">수정</a>
<a class="btn btn-outline-secondary" href="/admin/faqs">목록</a>
</div>
</div>
</div>
</div>
}
</section>
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Faqs;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class DetailModel(FaqService faqService) : PageModel
{
public Faq? Item { get; private set; }
public async Task<IActionResult> OnGetAsync(int id, CancellationToken ct)
{
Item = await faqService.GetByIdAsync(id, ct);
return Page();
}
}
@@ -0,0 +1,51 @@
@page "{id:int}"
@model TaxBaik.Web.Pages.Admin.Faqs.EditModel
@{ ViewData["Title"] = "FAQ 수정"; }
<section class="admin-page-shell">
<partial name="_AdminPageHeader" model='("Support", "FAQ 수정", "기존 FAQ를 Tabler 표준 폼으로 수정합니다.", null, null)' />
<form method="post" class="card">
<div class="card-body">
<partial name="_ValidationSummary" />
<input type="hidden" asp-for="Input.Id" />
<div class="row g-3">
<div class="col-lg-8">
<label asp-for="Input.Question" class="form-label"></label>
<input asp-for="Input.Question" class="form-control" />
</div>
<div class="col-lg-4">
<label asp-for="Input.Category" class="form-label"></label>
<input asp-for="Input.Category" class="form-control" list="faq-categories" />
<datalist id="faq-categories">
@foreach (var category in Model.Categories)
{
<option value="@category"></option>
}
</datalist>
</div>
<div class="col-12">
<label asp-for="Input.Answer" class="form-label"></label>
<textarea asp-for="Input.Answer" class="form-control" rows="10"></textarea>
</div>
<div class="col-md-4">
<label asp-for="Input.SortOrder" class="form-label"></label>
<input asp-for="Input.SortOrder" class="form-control" type="number" />
</div>
<div class="col-12">
<label class="form-check">
<input asp-for="Input.IsActive" class="form-check-input" type="checkbox" />
<span class="form-check-label">활성</span>
</label>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-between gap-2">
<button class="btn btn-outline-danger" type="submit" formaction="?handler=Delete" formmethod="post" onclick="return confirm('정말 삭제하시겠습니까?');">삭제</button>
<div class="d-flex gap-2">
<a class="btn btn-outline-secondary" href="/admin/faqs">취소</a>
<button class="btn btn-primary" type="submit">저장</button>
</div>
</div>
</form>
</section>
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Domain.Entities;
using TaxBaik.Application.Services;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Faqs;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class EditModel(FaqService faqService) : PageModel
{
[BindProperty(SupportsGet = true)]
public int Id { get; set; }
[BindProperty]
public Faq Input { get; set; } = new();
public IReadOnlyList<string> Categories => FaqService.Categories;
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
{
var faq = await faqService.GetByIdAsync(Id, ct);
if (faq is null)
return NotFound();
Input = new Faq
{
Id = faq.Id,
Question = faq.Question,
Answer = faq.Answer,
Category = faq.Category,
SortOrder = faq.SortOrder,
IsActive = faq.IsActive
};
return Page();
}
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
{
if (!ModelState.IsValid)
return Page();
await faqService.UpdateAsync(Input, ct);
return RedirectToPage("/Admin/Faqs/Index");
}
public async Task<IActionResult> OnPostDeleteAsync(CancellationToken ct)
{
await faqService.DeleteAsync(Id, ct);
return RedirectToPage("/Admin/Faqs/Index");
}
}
+10 -4
View File
@@ -3,17 +3,17 @@
@{ ViewData["Title"] = "FAQ"; }
<section class="admin-page-shell">
<partial name="_AdminPageHeader" model='("Support", "FAQ 관리", "질문과 답변을 관리합니다.", null, null)' />
<partial name="_AdminPageHeader" model='("Support", "FAQ 관리", "질문과 답변을 관리합니다.", "/admin/faqs/create", "새 FAQ 작성")' />
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<table class="table table-vcenter card-table table-hover">
<thead>
<tr><th>질문</th><th>카테고리</th><th>활성</th><th>정렬</th></tr>
<tr><th>질문</th><th>카테고리</th><th>활성</th><th>정렬</th><th class="text-end">작업</th></tr>
</thead>
<tbody>
@if (Model.Items.Count == 0)
{
<tr><td colspan="4"><partial name="_EmptyState" model="FAQ가 없습니다." /></td></tr>
<tr><td colspan="5"><partial name="_EmptyState" model="FAQ가 없습니다." /></td></tr>
}
else
{
@@ -24,6 +24,12 @@
<td>@item.Category</td>
<td><partial name="_StatusBadge" model="@(item.IsActive ? "active" : "inactive")" /></td>
<td>@item.SortOrder</td>
<td class="text-end">
<div class="btn-list flex-nowrap justify-content-end">
<a class="btn btn-sm btn-outline-secondary" href="/admin/faqs/@item.Id">상세</a>
<a class="btn btn-sm btn-primary" href="/admin/faqs/@item.Id/edit">수정</a>
</div>
</td>
</tr>
}
}