Standardize admin clients pages
TaxBaik CI/CD / build-and-deploy (push) Failing after 2m23s

This commit is contained in:
2026-07-09 11:41:40 +09:00
parent 55933649f6
commit 6ec4ef4c9a
25 changed files with 341 additions and 411 deletions
@@ -0,0 +1,58 @@
@page
@model TaxBaik.Web.Pages.Admin.Clients.CreateModel
@{
ViewData["Title"] = "고객 등록";
}
<section class="container py-4 py-lg-5">
<partial name="_AdminPageHeader" model='("고객 관리", "고객 등록", "FluentValidation과 Tabler 표준 폼으로 신규 고객을 등록합니다.", null, null)' />
<form method="post" class="card shadow-sm border-0">
<div class="card-body">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Input.Name" class="form-label"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="Input.CompanyName" class="form-label"></label>
<input asp-for="Input.CompanyName" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Input.Phone" class="form-label"></label>
<input asp-for="Input.Phone" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Input.Email" class="form-label"></label>
<input asp-for="Input.Email" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Input.Status" class="form-label"></label>
<input asp-for="Input.Status" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Input.ServiceType" class="form-label"></label>
<input asp-for="Input.ServiceType" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Input.TaxType" class="form-label"></label>
<input asp-for="Input.TaxType" class="form-control" />
</div>
<div class="col-12">
<label asp-for="Input.Source" class="form-label"></label>
<input asp-for="Input.Source" class="form-control" />
</div>
<div class="col-12">
<label asp-for="Input.Memo" class="form-label"></label>
<textarea asp-for="Input.Memo" class="form-control" rows="5"></textarea>
</div>
</div>
</div>
<div class="card-footer bg-white d-flex justify-content-end gap-2">
<a class="btn btn-outline-secondary" href="/admin/clients">취소</a>
<button class="btn btn-primary" type="submit">저장</button>
</div>
</form>
</section>
@@ -0,0 +1,26 @@
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.Clients;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class CreateModel(ClientService clientService) : PageModel
{
[BindProperty]
public CreateClientDto Input { get; set; } = new();
public IActionResult OnGet() => Page();
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
{
if (!ModelState.IsValid)
return Page();
var id = await clientService.CreateAsync(Input, ct);
return RedirectToPage("/Admin/Clients/Detail", new { id });
}
}
@@ -1,4 +1,57 @@
@page "{id:int}"
@model TaxBaik.Web.Pages.Admin.Clients.DetailModel
@{ ViewData["Title"] = "클라이언트 상세"; }
<section class="container py-5"><h1 class="h2 fw-bold">클라이언트 상세</h1></section>
@{
ViewData["Title"] = "고객 상세";
}
<section class="container py-4 py-lg-5">
<partial name="_AdminPageHeader" model='("고객 관리", "고객 상세", "고객의 기본 정보와 수정 경로를 확인합니다.", null, null)' />
@if (Model.Client is null)
{
<partial name="_EmptyState" model="조회한 고객을 찾을 수 없습니다." />
}
else
{
<div class="row g-4">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-3">고객명</dt>
<dd class="col-sm-9">@Model.Client.Name</dd>
<dt class="col-sm-3">회사명</dt>
<dd class="col-sm-9">@Model.Client.CompanyName</dd>
<dt class="col-sm-3">전화</dt>
<dd class="col-sm-9">@Model.Client.Phone</dd>
<dt class="col-sm-3">이메일</dt>
<dd class="col-sm-9">@Model.Client.Email</dd>
<dt class="col-sm-3">상태</dt>
<dd class="col-sm-9"><partial name="_StatusBadge" model="Model.Client.Status" /></dd>
<dt class="col-sm-3">서비스 유형</dt>
<dd class="col-sm-9">@Model.Client.ServiceType</dd>
<dt class="col-sm-3">세금 유형</dt>
<dd class="col-sm-9">@Model.Client.TaxType</dd>
<dt class="col-sm-3">유입 경로</dt>
<dd class="col-sm-9">@Model.Client.Source</dd>
<dt class="col-sm-3">등록일</dt>
<dd class="col-sm-9">@Model.Client.CreatedAt.ToString("yyyy-MM-dd HH:mm")</dd>
<dt class="col-sm-3">수정일</dt>
<dd class="col-sm-9">@Model.Client.UpdatedAt.ToString("yyyy-MM-dd HH:mm")</dd>
<dt class="col-sm-3">메모</dt>
<dd class="col-sm-9">@Model.Client.Memo</dd>
</dl>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0">
<div class="card-body d-grid gap-2">
<a class="btn btn-primary" href="/admin/clients/@Model.Client.Id/edit">수정</a>
<a class="btn btn-outline-secondary" href="/admin/clients">목록</a>
</div>
</div>
</div>
</div>
}
</section>
@@ -1,7 +1,17 @@
using Microsoft.AspNetCore.Authorization;
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 DetailModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Clients;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class DetailModel(ClientService clientService) : PageModel
{
public TaxBaik.Domain.Entities.Client? Client { get; private set; }
public async Task OnGetAsync(int id, CancellationToken ct)
{
Client = await clientService.GetByIdAsync(id, ct);
}
}
@@ -1,4 +1,62 @@
@page "{id:int}"
@model TaxBaik.Web.Pages.Admin.Clients.EditModel
@{ ViewData["Title"] = "클라이언트 수정"; }
<section class="container py-5"><h1 class="h2 fw-bold">클라이언트 수정</h1></section>
@{
ViewData["Title"] = "고객 수정";
}
<section class="container py-4 py-lg-5">
<partial name="_AdminPageHeader" model='("고객 관리", "고객 수정", "기존 고객 정보를 불러와 Tabler 표준 폼으로 수정합니다.", null, null)' />
<form method="post" class="card shadow-sm border-0">
<div class="card-body">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Input.Name" class="form-label"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="Input.CompanyName" class="form-label"></label>
<input asp-for="Input.CompanyName" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Input.Phone" class="form-label"></label>
<input asp-for="Input.Phone" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Input.Email" class="form-label"></label>
<input asp-for="Input.Email" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Input.Status" class="form-label"></label>
<input asp-for="Input.Status" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Input.ServiceType" class="form-label"></label>
<input asp-for="Input.ServiceType" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Input.TaxType" class="form-label"></label>
<input asp-for="Input.TaxType" class="form-control" />
</div>
<div class="col-12">
<label asp-for="Input.Source" class="form-label"></label>
<input asp-for="Input.Source" class="form-control" />
</div>
<div class="col-12">
<label asp-for="Input.Memo" class="form-label"></label>
<textarea asp-for="Input.Memo" class="form-control" rows="5"></textarea>
</div>
</div>
</div>
<div class="card-footer bg-white d-flex justify-content-end gap-2">
<a class="btn btn-outline-secondary" href="/admin/clients/@Model.Id">취소</a>
<button class="btn btn-primary" type="submit">저장</button>
</div>
</form>
</section>
<form method="post" asp-page-handler="Delete" class="mt-3" onsubmit="return confirm('정말 삭제하시겠습니까?');">
<button class="btn btn-outline-danger" type="submit">삭제</button>
</form>
@@ -1,7 +1,55 @@
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.Clients;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class EditModel : PageModel { }
namespace TaxBaik.Web.Pages.Admin.Clients;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class EditModel(ClientService clientService) : PageModel
{
[BindProperty(SupportsGet = true)]
public int Id { get; set; }
[BindProperty]
public CreateClientDto Input { get; set; } = new();
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
{
var client = await clientService.GetByIdAsync(Id, ct);
if (client is null)
return NotFound();
Input = new CreateClientDto
{
Name = client.Name,
CompanyName = client.CompanyName,
Phone = client.Phone,
Email = client.Email,
ServiceType = client.ServiceType,
TaxType = client.TaxType,
Status = client.Status,
Source = client.Source,
Memo = client.Memo
};
return Page();
}
public async Task<IActionResult> OnPostAsync(CancellationToken ct)
{
if (!ModelState.IsValid)
return Page();
await clientService.UpdateAsync(Id, Input, ct);
return RedirectToPage("/Admin/Clients/Detail", new { id = Id });
}
public async Task<IActionResult> OnPostDeleteAsync(CancellationToken ct)
{
await clientService.DeleteAsync(Id, ct);
return RedirectToPage("/Admin/Clients/Index");
}
}
@@ -4,23 +4,13 @@
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>
<section class="container py-4 py-lg-5">
<partial name="_AdminPageHeader" model='("고객 관리", "고객 목록", "검색, 상태 필터, 페이지네이션 기준의 고객 목록입니다.", null, null)' />
<form method="get" class="card mb-3">
<form method="get" class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-5">
<div class="col-md-4">
<label class="form-label" for="Search">검색</label>
<input class="form-control" id="Search" name="search" value="@Model.Search" placeholder="이름, 전화, 회사명" />
</div>
@@ -40,17 +30,18 @@
<option value="50" selected="@(Model.PageSize == 50)">50</option>
</select>
</div>
<div class="col-md-2 d-grid">
<div class="col-md-3 d-grid d-sm-flex gap-2">
<button class="btn btn-primary" type="submit">검색</button>
<a class="btn btn-outline-secondary" href="/admin/clients">초기화</a>
</div>
</div>
</div>
</form>
<div class="card">
<div class="card shadow-sm border-0">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>고객명</th>
<th>회사명</th>
@@ -61,27 +52,31 @@
</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)
@if (Model.Items.Count == 0)
{
<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 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-secondary" href="/admin/clients/@item.Id">상세</a>
<a class="btn btn-sm btn-outline-primary" href="/admin/clients/@item.Id/edit">수정</a>
</td>
</tr>
}
}
</tbody>
</table>
</div>