refactor: admin ui를 fluent v5와 html 기반으로 전환
TaxBaik CI/CD / build-and-deploy (push) Failing after 1m53s
TaxBaik CI/CD / build-and-deploy (push) Failing after 1m53s
This commit is contained in:
@@ -4,134 +4,94 @@
|
||||
@using TaxBaik.Domain.Entities
|
||||
@inject IClientBrowserClient ClientClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<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 class="admin-eyebrow">CRM</div>
|
||||
<h1 class="admin-page-title">고객 관리</h1>
|
||||
<p class="admin-page-subtitle">고객 카드를 등록하고 상담 이력을 관리합니다.</p>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.PersonAdd"
|
||||
Href="/taxbaik/admin/clients/create">
|
||||
고객 등록
|
||||
</MudButton>
|
||||
<button type="button" class="site-button primary" @onclick='() => Navigation.NavigateTo("/taxbaik/admin/clients/create")'>고객 등록</button>
|
||||
</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>
|
||||
<div class="admin-surface mb-3 pa-3">
|
||||
<div class="admin-filter-grid">
|
||||
<input class="admin-input" placeholder="검색 (이름·연락처·회사명)" @bind="searchText" @onkeyup="OnSearchKeyUp" />
|
||||
<select class="admin-input" @bind="statusFilter">
|
||||
<option value="">전체</option>
|
||||
<option value="active">활성</option>
|
||||
<option value="inactive">비활성</option>
|
||||
</select>
|
||||
<button type="button" class="site-button secondary" @onclick="SearchAsync">검색</button>
|
||||
<button type="button" class="site-button secondary" @onclick="ResetAsync">초기화</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MudPaper Class="admin-surface" Elevation="0">
|
||||
<div class="admin-surface">
|
||||
@if (clients is null)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
<Skeleton Count="6" CssClass="taxbaik-skeleton-grid" />
|
||||
}
|
||||
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>
|
||||
<div class="muted mt-4">등록된 고객이 없습니다.</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)
|
||||
{
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<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>
|
||||
<th>이름</th>
|
||||
<th>회사명</th>
|
||||
<th>연락처</th>
|
||||
<th>서비스</th>
|
||||
<th>세금 유형</th>
|
||||
<th>상태</th>
|
||||
<th>유입 경로</th>
|
||||
<th>등록일</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
|
||||
@* 페이징 *@
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var c in clients)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@c.Name</strong></td>
|
||||
<td>@(c.CompanyName ?? "—")</td>
|
||||
<td>@(c.Phone ?? "—")</td>
|
||||
<td>@(c.ServiceType ?? "—")</td>
|
||||
<td>@(c.TaxType ?? "—")</td>
|
||||
<td>@(c.Status == "active" ? "활성" : "비활성")</td>
|
||||
<td>@(c.Source ?? "—")</td>
|
||||
<td class="small">@c.CreatedAt.ToLocalTime().ToString("yy.MM.dd")</td>
|
||||
<td>
|
||||
<div class="admin-row-actions">
|
||||
<button type="button" class="admin-icon-button" @onclick="@(() => Navigation.NavigateTo($"/taxbaik/admin/clients/{c.Id}/edit"))">✎</button>
|
||||
<button type="button" class="admin-icon-button danger" @onclick="@(() => DeleteAsync(c))">✕</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (totalPages > 1)
|
||||
{
|
||||
<div class="d-flex justify-center pa-3">
|
||||
<MudPagination BoundaryCount="1" MiddleCount="3"
|
||||
Count="@totalPages" Selected="@currentPage"
|
||||
SelectedChanged="@OnPageChanged" />
|
||||
<div class="admin-pagination">
|
||||
<button type="button" class="site-button secondary" disabled="@(currentPage <= 1)" @onclick="PreviousPage">이전</button>
|
||||
<span>@currentPage / @totalPages</span>
|
||||
<button type="button" class="site-button secondary" disabled="@(currentPage >= totalPages)" @onclick="NextPage">다음</button>
|
||||
</div>
|
||||
}
|
||||
<MudText Typo="Typo.caption" Class="pa-2 text-muted">총 @(totalCount)명</MudText>
|
||||
<div class="admin-table-footer">총 @(totalCount)명</div>
|
||||
}
|
||||
</MudPaper>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||
|
||||
[CascadingParameter] private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||
private List<Client>? clients;
|
||||
private string searchText = "";
|
||||
private string statusFilter = "";
|
||||
@@ -142,16 +102,13 @@
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
if (firstRender && AuthStateTask != null)
|
||||
{
|
||||
if (AuthStateTask != null)
|
||||
var authState = await AuthStateTask;
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
var authState = await AuthStateTask;
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
await LoadAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
await LoadAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,75 +117,39 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
var (items, total) = await ClientClient.GetPagedAsync(
|
||||
currentPage, PageSize,
|
||||
string.IsNullOrEmpty(statusFilter) ? null : statusFilter,
|
||||
string.IsNullOrEmpty(searchText) ? null : searchText);
|
||||
|
||||
var (items, total) = await ClientClient.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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"오류: {ex.Message}", Severity.Error);
|
||||
await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}");
|
||||
clients = [];
|
||||
totalCount = 0;
|
||||
totalPages = 0;
|
||||
}
|
||||
}
|
||||
|
||||
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 SearchAsync() { currentPage = 1; await LoadAsync(); }
|
||||
private async Task ResetAsync() { searchText = ""; statusFilter = ""; currentPage = 1; await LoadAsync(); }
|
||||
private async Task PreviousPage() { if (currentPage > 1) { currentPage--; await LoadAsync(); } }
|
||||
private async Task NextPage() { if (currentPage < totalPages) { currentPage++; 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;
|
||||
|
||||
var confirmed = await JS.InvokeAsync<bool>("confirm", $"'{client.Name}' 고객을 삭제하시겠습니까? 관련 데이터도 함께 삭제됩니다.");
|
||||
if (!confirmed) return;
|
||||
try
|
||||
{
|
||||
var success = await ClientClient.DeleteAsync(client.Id);
|
||||
if (success)
|
||||
{
|
||||
Snackbar.Add($"{client.Name} 고객이 삭제되었습니다.", Severity.Success);
|
||||
await JS.InvokeVoidAsync("alert", $"{client.Name} 고객이 삭제되었습니다.");
|
||||
await LoadAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add("삭제에 실패했습니다.", Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"오류: {ex.Message}", Severity.Error);
|
||||
await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}");
|
||||
}
|
||||
await LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user