Standardize admin pages and blog editor
TaxBaik CI/CD / build-and-deploy (push) Failing after 2m39s

This commit is contained in:
2026-07-09 11:33:17 +09:00
parent 9cf6675dc9
commit 55933649f6
27 changed files with 825 additions and 68 deletions
@@ -1,4 +1,34 @@
@page
@model TaxBaik.Web.Pages.Admin.Announcements.IndexModel
@{ ViewData["Title"] = "공지사항"; }
<section class="container py-5"><h1 class="h2 fw-bold">공지사항 관리</h1></section>
<section class="container-fluid py-4">
<partial name="_AdminPageHeader" model='("Content", "공지사항 관리", "공지 목록을 서버 렌더링으로 표시합니다.", null, null)' />
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr><th>제목</th><th>유형</th><th>활성</th><th>정렬</th></tr>
</thead>
<tbody>
@if (Model.Items.Count == 0)
{
<tr><td colspan="4"><partial name="_EmptyState" model="공지사항이 없습니다." /></td></tr>
}
else
{
foreach (var item in Model.Items)
{
<tr>
<td>@item.Title</td>
<td>@item.DisplayType</td>
<td><partial name="_StatusBadge" model="@(item.IsActive ? "active" : "inactive")" /></td>
<td>@item.SortOrder</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</section>
@@ -1,7 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Announcements;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Announcements;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel(AnnouncementService announcementService) : PageModel
{
public List<Announcement> Items { get; private set; } = [];
public async Task OnGetAsync(CancellationToken ct = default)
=> Items = (await announcementService.GetAllAsync(ct)).ToList();
}
+81 -2
View File
@@ -1,4 +1,83 @@
@page
@model TaxBaik.Web.Pages.Admin.Blog.CreateModel
@{ ViewData["Title"] = "블로그 등록"; }
<section class="container py-5"><h1 class="h2 fw-bold">블로그 등록</h1></section>
@{
ViewData["Title"] = "블로그 등록";
}
<section class="container-fluid py-4" data-blog-editor-root>
<div class="row g-3">
<div class="col-12">
<div class="page-header">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">Content</div>
<h2 class="page-title">블로그 등록</h2>
</div>
</div>
</div>
</div>
<div class="col-lg-8">
<form method="post" class="card">
<div class="card-body">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<input type="hidden" asp-for="Input.Content" data-blog-editor-value />
<div class="mb-3">
<label asp-for="Input.Title" class="form-label"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title" class="text-danger small"></span>
</div>
<div class="mb-3">
<label class="form-label">본문</label>
<div class="d-flex flex-wrap gap-2 mb-2">
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="bold">Bold</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="italic">Italic</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="insertUnorderedList">UL</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="formatBlock" data-blog-value="h2">H2</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="formatBlock" data-blog-value="blockquote">Quote</button>
</div>
<div class="form-control" data-blog-editor contenteditable="true" style="min-height: 320px; overflow:auto;"></div>
<div class="form-hint text-muted small mt-2">HTML 태그는 그대로 저장되고 홈페이지에서 렌더링됩니다.</div>
</div>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="Input.CategoryId" class="form-label"></label>
<input asp-for="Input.CategoryId" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Input.IsPublished" class="form-label d-block"></label>
<input asp-for="Input.IsPublished" class="form-check-input" type="checkbox" />
</div>
<div class="col-md-4">
<label asp-for="Input.ThumbnailUrl" class="form-label"></label>
<input asp-for="Input.ThumbnailUrl" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Input.SeoTitle" class="form-label"></label>
<input asp-for="Input.SeoTitle" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Input.SeoDescription" class="form-label"></label>
<input asp-for="Input.SeoDescription" class="form-control" />
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-between">
<a class="btn btn-outline-secondary" href="/admin/blog">취소</a>
<button class="btn btn-primary" type="submit">저장</button>
</div>
</form>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header"><h3 class="card-title">미리보기</h3></div>
<div class="card-body">
<article class="markdown-body" data-blog-preview></article>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<script src="~/js/admin-blog-editor.js" asp-append-version="true"></script>
}
@@ -1,7 +1,30 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.DTOs;
using TaxBaik.Application.Services;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class CreateModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class CreateModel(BlogService blogService) : PageModel
{
[BindProperty]
public CreateBlogPostDto Input { get; set; } = new()
{
Content = string.Empty,
Title = string.Empty
};
public IActionResult OnGet() => Page();
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
{
if (!ModelState.IsValid)
return Page();
await blogService.CreateAsync(Input, ct);
return RedirectToPage("/Admin/Blog/Index");
}
}
+83 -2
View File
@@ -1,4 +1,85 @@
@page "{id:int}"
@model TaxBaik.Web.Pages.Admin.Blog.EditModel
@{ ViewData["Title"] = "블로그 수정"; }
<section class="container py-5"><h1 class="h2 fw-bold">블로그 수정</h1></section>
@{
ViewData["Title"] = "블로그 수정";
}
<section class="container-fluid py-4" data-blog-editor-root>
<div class="row g-3">
<div class="col-12">
<div class="page-header">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">Content</div>
<h2 class="page-title">블로그 수정</h2>
</div>
</div>
</div>
</div>
<div class="col-lg-8">
<form method="post" class="card">
<div class="card-body">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="Input.Content" data-blog-editor-value />
<div class="mb-3">
<label asp-for="Input.Title" class="form-label"></label>
<input asp-for="Input.Title" class="form-control" />
</div>
<div class="mb-3">
<label class="form-label">본문</label>
<div class="d-flex flex-wrap gap-2 mb-2">
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="bold">Bold</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="italic">Italic</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="insertUnorderedList">UL</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="formatBlock" data-blog-value="h2">H2</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-blog-command="formatBlock" data-blog-value="blockquote">Quote</button>
</div>
<div class="form-control" data-blog-editor contenteditable="true" style="min-height: 320px; overflow:auto;">@Html.Raw(Model.Input.Content)</div>
</div>
<div class="row g-3">
<div class="col-md-4">
<label asp-for="Input.CategoryId" class="form-label"></label>
<input asp-for="Input.CategoryId" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Input.IsPublished" class="form-label d-block"></label>
<input asp-for="Input.IsPublished" class="form-check-input" type="checkbox" />
</div>
<div class="col-md-4">
<label asp-for="Input.ThumbnailUrl" class="form-label"></label>
<input asp-for="Input.ThumbnailUrl" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Input.SeoTitle" class="form-label"></label>
<input asp-for="Input.SeoTitle" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Input.SeoDescription" class="form-label"></label>
<input asp-for="Input.SeoDescription" class="form-control" />
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-between">
<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/blog">취소</a>
<button class="btn btn-primary" type="submit">저장</button>
</div>
</div>
</form>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header"><h3 class="card-title">미리보기</h3></div>
<div class="card-body">
<article class="markdown-body" data-blog-preview>@Html.Raw(Model.Input.Content)</article>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<script src="~/js/admin-blog-editor.js" asp-append-version="true"></script>
}
@@ -1,7 +1,62 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.DTOs;
using TaxBaik.Application.Services;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class EditModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class EditModel(BlogService blogService) : PageModel
{
[BindProperty(SupportsGet = true)]
public int Id { get; set; }
[BindProperty]
public CreateBlogPostDto Input { get; set; } = new()
{
Content = string.Empty,
Title = string.Empty
};
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
{
var post = await blogService.GetByIdAsync(Id, ct);
if (post is null)
return NotFound();
Input = new CreateBlogPostDto
{
Title = post.Title,
Content = post.Content,
CategoryId = post.CategoryId,
Tags = post.Tags,
SeoTitle = post.SeoTitle,
SeoDescription = post.SeoDescription,
ThumbnailUrl = post.ThumbnailUrl,
IsPublished = post.IsPublished,
AuthorId = post.AuthorId
};
return Page();
}
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
{
if (!ModelState.IsValid)
return Page();
var updated = await blogService.UpdateAsync(Id, Input, ct);
if (updated is null)
return NotFound();
return RedirectToPage("/Admin/Blog/Index");
}
public async Task<IActionResult> OnPostDeleteAsync(CancellationToken ct)
{
await blogService.DeleteAsync(Id, ct);
return RedirectToPage("/Admin/Blog/Index");
}
}
+74 -2
View File
@@ -1,4 +1,76 @@
@page
@model TaxBaik.Web.Pages.Admin.Blog.IndexModel
@{ ViewData["Title"] = "블로그"; }
<section class="container py-5"><h1 class="h2 fw-bold">블로그 관리</h1></section>
@{
ViewData["Title"] = "블로그";
}
<section class="container-fluid py-4">
<div class="page-header d-print-none mb-4">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">Content</div>
<h2 class="page-title">블로그 관리</h2>
</div>
<div class="col-auto ms-auto">
<a class="btn btn-primary" href="/admin/blog/create">새 글 작성</a>
</div>
</div>
</div>
<form method="get" class="card mb-3">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-6">
<label class="form-label" for="SearchQuery">검색</label>
<input class="form-control" id="SearchQuery" name="searchQuery" value="@Model.SearchQuery" placeholder="제목 또는 본문 검색" />
</div>
<div class="col-md-6 d-flex gap-2">
<button class="btn btn-primary" type="submit">검색</button>
<a class="btn btn-outline-secondary" href="/admin/blog">초기화</a>
<a class="btn btn-outline-secondary" href="/admin/blog?showArchived=true">내린 글 보기</a>
</div>
</div>
</div>
</form>
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<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="5"><partial name="_EmptyState" model="블로그 글이 없습니다." /></td></tr>
}
else
{
foreach (var post in Model.Items)
{
<tr>
<td>
<div class="fw-semibold">@post.Title</div>
<div class="text-muted small">@post.Slug</div>
</td>
<td><partial name="_StatusBadge" model="@(post.IsPublished ? "active" : "inactive")" /></td>
<td>@post.ViewCount</td>
<td>@post.CreatedAt.ToString("yyyy-MM-dd")</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-primary" href="/admin/blog/@post.Id/edit">수정</a>
<a class="btn btn-sm btn-outline-secondary" href="/blog/@post.Slug" target="_blank">보기</a>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</section>
@@ -1,7 +1,25 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Blog;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel : PageModel { }
public class IndexModel(BlogService blogService) : PageModel
{
public List<BlogPost> Items { get; private set; } = [];
public string? SearchQuery { get; set; }
public async Task OnGetAsync(string? searchQuery, bool showArchived = false, CancellationToken ct = default)
{
SearchQuery = searchQuery;
var posts = showArchived
? await blogService.GetArchivedPagedAsync(1, 100, ct)
: await blogService.GetAdminPagedAsync(1, 100, ct);
Items = posts.Item1
.Where(p => string.IsNullOrWhiteSpace(searchQuery) ||
p.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
(p.Content?.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ?? false))
.ToList();
}
}
@@ -1,4 +1,91 @@
@page
@model TaxBaik.Web.Pages.Admin.Clients.IndexModel
@{ ViewData["Title"] = "클라이언트"; }
<section class="container py-5"><h1 class="h2 fw-bold">클라이언트 목록</h1><p class="text-muted">Blazor 관리자 화면을 Razor Pages로 전환하는 자리입니다.</p></section>
@{
ViewData["Title"] = "고객 관리";
}
<section class="container-fluid py-4">
<div class="page-header d-print-none mb-4">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">CRM</div>
<h2 class="page-title">고객 관리</h2>
</div>
<div class="col-auto ms-auto">
<a class="btn btn-primary" href="/admin/customers/create">고객 등록</a>
</div>
</div>
</div>
<form method="get" class="card mb-3">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-5">
<label class="form-label" for="Search">검색</label>
<input class="form-control" id="Search" name="search" value="@Model.Search" placeholder="이름, 전화, 회사명" />
</div>
<div class="col-md-3">
<label class="form-label" for="Status">상태</label>
<select class="form-select" id="Status" name="status">
<option value="" selected="@(string.IsNullOrWhiteSpace(Model.Status))">전체</option>
<option value="active" selected="@(Model.Status == "active")">active</option>
<option value="inactive" selected="@(Model.Status == "inactive")">inactive</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="PageSize">페이지 크기</label>
<select class="form-select" id="PageSize" name="pageSize">
<option value="10" selected="@(Model.PageSize == 10)">10</option>
<option value="20" selected="@(Model.PageSize == 20)">20</option>
<option value="50" selected="@(Model.PageSize == 50)">50</option>
</select>
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-primary" type="submit">검색</button>
</div>
</div>
</div>
</form>
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>고객명</th>
<th>회사명</th>
<th>전화</th>
<th>상태</th>
<th>등록일</th>
<th class="text-end">작업</th>
</tr>
</thead>
<tbody>
@if (Model.Items.Count == 0)
{
<tr><td colspan="6"><partial name="_EmptyState" model="검색 결과가 없습니다." /></td></tr>
}
else
{
foreach (var item in Model.Items)
{
<tr>
<td class="fw-semibold">@item.Name</td>
<td>@item.CompanyName</td>
<td>@item.Phone</td>
<td><partial name="_StatusBadge" model="@(item.Status)" /></td>
<td>@item.CreatedAt.ToString("yyyy-MM-dd")</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-primary" href="/admin/customers/@item.Id">상세</a>
<a class="btn btn-sm btn-outline-secondary" href="/admin/customers/@item.Id/edit">수정</a>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
<partial name="_Pagination" model="new TaxBaik.Web.Pages.Shared.PaginationModel(Model.CurrentPage, Model.TotalPages, Model.BuildPageUrl)" />
</section>
@@ -1,7 +1,41 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.Services;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Clients;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Clients;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel(ClientService clientService) : PageModel
{
public List<TaxBaik.Domain.Entities.Client> Items { get; private set; } = [];
public int CurrentPage { get; private set; } = 1;
public int TotalPages { get; private set; } = 1;
public int TotalCount { get; private set; }
public string? Search { get; set; }
public string? Status { get; set; }
public int PageSize { get; set; } = 10;
public async Task OnGetAsync(string? search, string? status, int page = 1, int pageSize = 10, CancellationToken ct = default)
{
Search = search;
Status = status;
PageSize = pageSize;
CurrentPage = Math.Max(1, page);
var (items, total) = await clientService.GetPagedAsync(CurrentPage, PageSize, status, search, ct);
Items = items.ToList();
TotalCount = total;
TotalPages = Math.Max(1, (int)Math.Ceiling(total / (double)PageSize));
}
public string BuildPageUrl(int page) =>
$"/admin/clients?search={Uri.EscapeDataString(Search ?? string.Empty)}&status={Uri.EscapeDataString(Status ?? string.Empty)}&page={page}&pageSize={PageSize}";
public async Task<IActionResult> OnPostDeleteAsync(int id, CancellationToken ct)
{
await clientService.DeleteAsync(id, ct);
return RedirectToPage();
}
}
@@ -1,4 +1,34 @@
@page
@model TaxBaik.Web.Pages.Admin.Companies.IndexModel
@{ ViewData["Title"] = "회사"; }
<section class="container py-5"><h1 class="h2 fw-bold">회사 관리</h1></section>
<section class="container-fluid py-4">
<partial name="_AdminPageHeader" model='("CRM", "회사 관리", "고객사 회사를 관리합니다.", null, null)' />
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr><th>코드</th><th>회사명</th><th>담당자</th><th>활성</th></tr>
</thead>
<tbody>
@if (Model.Items.Count == 0)
{
<tr><td colspan="4"><partial name="_EmptyState" model="회사가 없습니다." /></td></tr>
}
else
{
foreach (var item in Model.Items)
{
<tr>
<td>@item.CompanyCode</td>
<td>@item.CompanyName</td>
<td>@item.ContactPerson</td>
<td><partial name="_StatusBadge" model="@(item.IsActive ? "active" : "inactive")" /></td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</section>
@@ -1,7 +1,19 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Companies;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Companies;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel(CompanyService companyService) : PageModel
{
public List<Company> Items { get; private set; } = [];
public async Task OnGetAsync(CancellationToken ct = default)
{
var (items, _) = await companyService.GetPagedAsync(1, 100, ct);
Items = items.ToList();
}
}
+31 -1
View File
@@ -1,4 +1,34 @@
@page
@model TaxBaik.Web.Pages.Admin.Faqs.IndexModel
@{ ViewData["Title"] = "FAQ"; }
<section class="container py-5"><h1 class="h2 fw-bold">FAQ 관리</h1></section>
<section class="container-fluid py-4">
<partial name="_AdminPageHeader" model='("Support", "FAQ 관리", "질문과 답변을 관리합니다.", null, null)' />
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr><th>질문</th><th>카테고리</th><th>활성</th><th>정렬</th></tr>
</thead>
<tbody>
@if (Model.Items.Count == 0)
{
<tr><td colspan="4"><partial name="_EmptyState" model="FAQ가 없습니다." /></td></tr>
}
else
{
foreach (var item in Model.Items)
{
<tr>
<td>@item.Question</td>
<td>@item.Category</td>
<td><partial name="_StatusBadge" model="@(item.IsActive ? "active" : "inactive")" /></td>
<td>@item.SortOrder</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</section>
@@ -1,7 +1,16 @@
using Microsoft.AspNetCore.Authorization;
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 IndexModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Faqs;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel(FaqService faqService) : PageModel
{
public List<Faq> Items { get; private set; } = [];
public async Task OnGetAsync(CancellationToken ct = default)
=> Items = (await faqService.GetAllAsync(ct)).ToList();
}
@@ -1,4 +1,54 @@
@page
@model TaxBaik.Web.Pages.Admin.Inquiries.IndexModel
@{ ViewData["Title"] = "문의"; }
<section class="container py-5"><h1 class="h2 fw-bold">문의 관리</h1></section>
<section class="container-fluid py-4">
<partial name="_AdminPageHeader" model='("Customer Requests", "문의 관리", "상담 요청을 상태별로 확인합니다.", null, null)' />
<form method="get" class="card mb-3">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label class="form-label" for="Status">상태</label>
<input class="form-control" id="Status" name="status" value="@Model.Status" placeholder="new / consulting / contracted ..." />
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-primary" type="submit">필터</button>
</div>
</div>
</div>
</form>
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>이름</th>
<th>전화</th>
<th>서비스</th>
<th>상태</th>
<th>등록일</th>
</tr>
</thead>
<tbody>
@if (Model.Items.Count == 0)
{
<tr><td colspan="5"><partial name="_EmptyState" model="문의 목록이 없습니다." /></td></tr>
}
else
{
foreach (var item in Model.Items)
{
<tr>
<td>@item.Name</td>
<td>@item.Phone</td>
<td>@item.ServiceType</td>
<td><partial name="_StatusBadge" model="@(item.Status)" /></td>
<td>@item.CreatedAt.ToString("yyyy-MM-dd")</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</section>
@@ -1,7 +1,21 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.Inquiries;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Inquiries;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel(InquiryService inquiryService) : PageModel
{
public List<Inquiry> Items { get; private set; } = [];
public string? Status { get; set; }
public async Task OnGetAsync(string? status = null, CancellationToken ct = default)
{
Status = status;
var (items, _) = await inquiryService.GetPagedAsync(1, 100, status, ct);
Items = items.ToList();
}
}
@@ -1,4 +1,34 @@
@page
@model TaxBaik.Web.Pages.Admin.TaxProfiles.IndexModel
@{ ViewData["Title"] = "세무 프로필"; }
<section class="container py-5"><h1 class="h2 fw-bold">세무 프로필 관리</h1></section>
<section class="container-fluid py-4">
<partial name="_AdminPageHeader" model='("Tax", "세무 프로필 관리", "고객별 세무 속성을 관리합니다.", null, null)' />
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr><th>고객ID</th><th>사업유형</th><th>세무위험</th><th>다음신고일</th></tr>
</thead>
<tbody>
@if (Model.Items.Count == 0)
{
<tr><td colspan="4"><partial name="_EmptyState" model="세무 프로필이 없습니다." /></td></tr>
}
else
{
foreach (var item in Model.Items)
{
<tr>
<td>@item.ClientId</td>
<td>@item.BusinessType</td>
<td><partial name="_StatusBadge" model="@(item.TaxRiskLevel ?? "normal")" /></td>
<td>@item.NextFilingDueDate?.ToString("yyyy-MM-dd")</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</section>
@@ -1,7 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TaxBaik.Application.Services;
using TaxBaik.Domain.Entities;
using TaxBaik.Web.Services;
namespace TaxBaik.Web.Pages.Admin.TaxProfiles;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.TaxProfiles;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel(TaxProfileService taxProfileService) : PageModel
{
public List<TaxProfile> Items { get; private set; } = [];
public async Task OnGetAsync(CancellationToken ct = default)
=> Items = (await taxProfileService.GetAllAsync(ct)).ToList();
}
+24 -12
View File
@@ -6,23 +6,35 @@
<title>@(ViewData["Title"] ?? "TaxBaik Admin")</title>
<meta name="theme-color" content="#111827" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/@@tabler/core@1.0.0/dist/css/tabler.min.css" rel="stylesheet" />
<link rel="stylesheet" href="~/css/admin.css" asp-append-version="true" />
</head>
<body class="admin-app">
<partial name="_AdminTopbar" />
<div class="admin-shell container-fluid">
<div class="row g-0">
<aside class="col-lg-2 admin-sidebar">
<partial name="_AdminSidebar" />
</aside>
<main class="col-lg-10 admin-content">
@RenderBody()
</main>
<div class="page">
<header class="navbar navbar-expand-md d-print-none">
<div class="container-xl">
<div class="navbar-brand navbar-brand-autodark">TaxBaik Admin</div>
<div class="navbar-nav flex-row order-md-last">
<div class="nav-item">
<a class="btn btn-outline-secondary btn-sm" href="/admin/logout">로그아웃</a>
</div>
</div>
</div>
</header>
<div class="page-wrapper">
<div class="container-xl">
<div class="row g-3">
<aside class="col-12 col-lg-2">
<partial name="_AdminSidebar" />
</aside>
<main class="col-12 col-lg-10">
@RenderBody()
</main>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/@@tabler/core@1.0.0/dist/js/tabler.min.js" defer></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>