0e98e68532
DB: - V006__CreateClients.sql: clients 테이블 (name, company_name, phone, email, service_type, tax_type, status, source, memo) Domain: - Client 엔티티 - IClientRepository (GetPagedAsync 이름/연락처/회사명 검색 + 상태 필터) Infrastructure: - ClientRepository: ILIKE 검색, 페이징, CRUD Application: - ClientService: ServiceTypes/TaxTypes/Sources 상수 정의 - CreateClientDto Admin UI: - ClientList.razor: 검색바 + 상태 필터 + 페이징 테이블 - ClientEdit.razor: 기본정보/세무정보/관리정보 섹션 폼 - MainLayout: 고객 관리 NavGroup 추가, 홈페이지 메뉴 그룹화 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
193 lines
7.0 KiB
Plaintext
193 lines
7.0 KiB
Plaintext
@page "/admin/clients"
|
|
@attribute [Authorize]
|
|
@using TaxBaik.Application.Services
|
|
@using TaxBaik.Domain.Entities
|
|
@inject ClientService ClientService
|
|
@inject NavigationManager Navigation
|
|
@inject IDialogService DialogService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<PageTitle>고객 관리</PageTitle>
|
|
|
|
<section class="admin-page-hero">
|
|
<div>
|
|
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM</MudText>
|
|
<MudText Typo="Typo.h4" Class="admin-page-title">고객 관리</MudText>
|
|
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객 카드를 등록하고 상담 이력을 관리합니다.</MudText>
|
|
</div>
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.PersonAdd"
|
|
Href="/taxbaik/admin/clients/create">
|
|
고객 등록
|
|
</MudButton>
|
|
</section>
|
|
|
|
@* 검색/필터 바 *@
|
|
<MudPaper Class="admin-surface mb-3 pa-3" Elevation="0">
|
|
<MudGrid>
|
|
<MudItem xs="12" md="5">
|
|
<MudTextField @bind-Value="searchText" Label="검색 (이름·연락처·회사명)"
|
|
Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Search"
|
|
Immediate="false" OnKeyUp="@OnSearchKeyUp" />
|
|
</MudItem>
|
|
<MudItem xs="12" md="3">
|
|
<MudSelect @bind-Value="statusFilter" Label="상태" T="string">
|
|
<MudSelectItem Value="@("")">전체</MudSelectItem>
|
|
<MudSelectItem Value="@("active")">활성</MudSelectItem>
|
|
<MudSelectItem Value="@("inactive")">비활성</MudSelectItem>
|
|
</MudSelect>
|
|
</MudItem>
|
|
<MudItem xs="12" md="2" Class="d-flex align-center">
|
|
<MudButton Variant="Variant.Outlined" OnClick="@SearchAsync" FullWidth="true">검색</MudButton>
|
|
</MudItem>
|
|
<MudItem xs="12" md="2" Class="d-flex align-center">
|
|
<MudButton Variant="Variant.Text" OnClick="@ResetAsync" FullWidth="true">초기화</MudButton>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudPaper>
|
|
|
|
<MudPaper Class="admin-surface" Elevation="0">
|
|
@if (clients is null)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" />
|
|
}
|
|
else if (!clients.Any())
|
|
{
|
|
<div class="pa-6 text-center">
|
|
<MudIcon Icon="@Icons.Material.Filled.PeopleAlt" Style="font-size:3rem; opacity:.3;" />
|
|
<MudText Class="mt-2 text-muted">등록된 고객이 없습니다.</MudText>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<MudSimpleTable Striped="true" Dense="true" Class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>이름</th>
|
|
<th>회사명</th>
|
|
<th>연락처</th>
|
|
<th>서비스</th>
|
|
<th>세금 유형</th>
|
|
<th>상태</th>
|
|
<th>유입 경로</th>
|
|
<th>등록일</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var c in clients)
|
|
{
|
|
<tr>
|
|
<td><strong>@c.Name</strong></td>
|
|
<td>@(c.CompanyName ?? "—")</td>
|
|
<td>@(c.Phone ?? "—")</td>
|
|
<td>
|
|
@if (!string.IsNullOrEmpty(c.ServiceType))
|
|
{
|
|
<MudChip Size="Size.Small" Color="Color.Primary">@c.ServiceType</MudChip>
|
|
}
|
|
</td>
|
|
<td>@(c.TaxType ?? "—")</td>
|
|
<td>
|
|
@if (c.Status == "active")
|
|
{
|
|
<MudChip Size="Size.Small" Color="Color.Success">활성</MudChip>
|
|
}
|
|
else
|
|
{
|
|
<MudChip Size="Size.Small" Color="Color.Default">비활성</MudChip>
|
|
}
|
|
</td>
|
|
<td>@(c.Source ?? "—")</td>
|
|
<td class="small">@c.CreatedAt.ToLocalTime().ToString("yy.MM.dd")</td>
|
|
<td>
|
|
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
|
|
<MudButton @onclick="@(() => Navigation.NavigateTo($"/taxbaik/admin/clients/{c.Id}/edit"))">
|
|
수정
|
|
</MudButton>
|
|
<MudButton Color="Color.Error" @onclick="@(() => DeleteAsync(c))">
|
|
삭제
|
|
</MudButton>
|
|
</MudButtonGroup>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</MudSimpleTable>
|
|
|
|
@* 페이징 *@
|
|
@if (totalPages > 1)
|
|
{
|
|
<div class="d-flex justify-center pa-3">
|
|
<MudPagination BoundaryCount="1" MiddleCount="3"
|
|
Count="@totalPages" Selected="@currentPage"
|
|
SelectedChanged="@OnPageChanged" />
|
|
</div>
|
|
}
|
|
<MudText Typo="Typo.caption" Class="pa-2 text-muted">총 @(totalCount)명</MudText>
|
|
}
|
|
</MudPaper>
|
|
|
|
@code {
|
|
private List<Client>? clients;
|
|
private string searchText = "";
|
|
private string statusFilter = "";
|
|
private int currentPage = 1;
|
|
private int totalCount;
|
|
private int totalPages;
|
|
private const int PageSize = 20;
|
|
|
|
protected override async Task OnInitializedAsync() => await LoadAsync();
|
|
|
|
private async Task LoadAsync()
|
|
{
|
|
var (items, total) = await ClientService.GetPagedAsync(
|
|
currentPage, PageSize,
|
|
string.IsNullOrEmpty(statusFilter) ? null : statusFilter,
|
|
string.IsNullOrEmpty(searchText) ? null : searchText);
|
|
|
|
clients = items.ToList();
|
|
totalCount = total;
|
|
totalPages = (int)Math.Ceiling((double)total / PageSize);
|
|
}
|
|
|
|
private async Task SearchAsync()
|
|
{
|
|
currentPage = 1;
|
|
await LoadAsync();
|
|
}
|
|
|
|
private async Task ResetAsync()
|
|
{
|
|
searchText = "";
|
|
statusFilter = "";
|
|
currentPage = 1;
|
|
await LoadAsync();
|
|
}
|
|
|
|
private async Task OnPageChanged(int page)
|
|
{
|
|
currentPage = page;
|
|
await LoadAsync();
|
|
}
|
|
|
|
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter") await SearchAsync();
|
|
}
|
|
|
|
private async Task DeleteAsync(Client client)
|
|
{
|
|
var confirmed = await DialogService.ShowMessageBox(
|
|
"고객 삭제",
|
|
$"'{client.Name}' 고객을 삭제하시겠습니까? 관련 데이터도 함께 삭제됩니다.",
|
|
yesText: "삭제", cancelText: "취소");
|
|
|
|
if (confirmed != true) return;
|
|
|
|
await ClientService.DeleteAsync(client.Id);
|
|
Snackbar.Add($"{client.Name} 고객이 삭제되었습니다.", Severity.Success);
|
|
await LoadAsync();
|
|
}
|
|
}
|