revert: rollback Fluent UI and Blazor homepage to last successful state (3be3794)

This commit is contained in:
2026-06-30 20:29:42 +09:00
parent 488b8d11b7
commit 54c179b1eb
69 changed files with 3996 additions and 2904 deletions
@@ -4,123 +4,185 @@
@inject ClientService ClientService
@inject ConsultationService ConsultationService
@inject NavigationManager Navigation
@inject IJSRuntime JS
@inject ISnackbar Snackbar
<PageTitle>고객 상세</PageTitle>
<section class="admin-page-hero">
<div>
<div class="admin-eyebrow">Client Details</div>
<h1 class="admin-page-title">고객 상세</h1>
<p class="admin-page-subtitle">고객 정보와 상담 이력을 관리합니다.</p>
<MudText Typo="Typo.caption" Class="admin-eyebrow">Client Details</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">고객 상세</MudText>
<MudText Typo="Typo.body2" Class="admin-page-subtitle">고객 정보와 상담 이력을 관리합니다.</MudText>
</div>
</section>
@if (client == null)
{
<div class="admin-surface mt-4">고객을 찾을 수 없습니다.</div>
<MudText>고객을 찾을 수 없습니다.</MudText>
return;
}
else
{
<div class="admin-page-actions">
<button type="button" class="site-button secondary" @onclick='() => Navigation.NavigateTo("/taxbaik/admin/clients")'>목록으로</button>
<a class="site-button secondary" href="@($"/taxbaik/admin/clients/{ClientId}/edit")">수정</a>
</div>
<div class="admin-detail-grid">
<section class="admin-surface">
<h3 class="admin-section-title">고객 정보</h3>
<div class="admin-kv-grid">
<div><span>이름</span><strong>@client.Name</strong></div>
<div><span>상호</span><strong>@(client.CompanyName ?? "-")</strong></div>
<div><span>연락처</span><strong>@(client.Phone ?? "-")</strong></div>
<div><span>이메일</span><strong>@(client.Email ?? "-")</strong></div>
<div><span>서비스</span><strong>@(client.ServiceType ?? "-")</strong></div>
<div><span>사업자 유형</span><strong>@(client.TaxType ?? "-")</strong></div>
<div><span>유입 경로</span><strong>@(client.Source ?? "-")</strong></div>
<div><span>등록일</span><strong>@client.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd")</strong></div>
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-4" Spacing="2">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ArrowBack"
@onclick="@(() => Navigation.NavigateTo("/taxbaik/admin/clients"))">
목록으로
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Warning"
StartIcon="@Icons.Material.Filled.Edit"
Href="@($"/taxbaik/admin/clients/{ClientId}/edit")">
수정
</MudButton>
</MudStack>
<MudGrid>
<MudItem xs="12" md="5">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-3">고객 정보</MudText>
<MudGrid Spacing="2">
<MudItem xs="6">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">이름</MudText>
<MudText>@client.Name</MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">상호</MudText>
<MudText>@(client.CompanyName ?? "-")</MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">연락처</MudText>
<MudText>@(client.Phone ?? "-")</MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">이메일</MudText>
<MudText>@(client.Email ?? "-")</MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">서비스</MudText>
<MudText>@(client.ServiceType ?? "-")</MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">사업자 유형</MudText>
<MudText>@(client.TaxType ?? "-")</MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">유입 경로</MudText>
<MudText>@(client.Source ?? "-")</MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">등록일</MudText>
<MudText>@client.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd")</MudText>
</MudItem>
@if (!string.IsNullOrWhiteSpace(client.Memo))
{
<div class="span-2"><span>메모</span><strong style="white-space: pre-wrap;">@client.Memo</strong></div>
<MudItem xs="12">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">메모</MudText>
<MudText Style="white-space: pre-wrap;">@client.Memo</MudText>
</MudItem>
}
</div>
</section>
</MudGrid>
</MudPaper>
</MudItem>
<section class="admin-surface">
<div class="admin-section-header compact">
<div>
<h3 class="admin-section-title">상담 이력</h3>
</div>
<button type="button" class="site-button primary" @onclick="OpenAddConsultation">+ 상담 추가</button>
</div>
<MudItem xs="12" md="7">
<MudPaper Class="pa-4" Elevation="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
<MudText Typo="Typo.h6">상담 이력</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Size="Size.Small"
OnClick="OpenAddConsultation">
+ 상담 추가
</MudButton>
</MudStack>
@if (showAddForm)
{
<form class="admin-dialog-card mb-4" @onsubmit="AddConsultation" @onsubmit:preventDefault="true">
<label>상담일 <input class="admin-input" type="text" placeholder="2026-06-29" @bind="ConsultationDateText" /></label>
<label>서비스 분야
<select class="admin-input" @bind="newServiceType">
<option value="">선택하세요</option>
@foreach (var t in ClientService.ServiceTypes)
{
<option value="@t">@t</option>
}
</select>
</label>
<label>상담 내용 * <textarea class="admin-input" rows="3" @bind="newSummary"></textarea></label>
<label>결과
<select class="admin-input" @bind="newResult">
<option value="">-</option>
@foreach (var r in ConsultationService.Results)
{
<option value="@r">@r</option>
}
</select>
</label>
<label>수임료 (원) <input class="admin-input" type="text" placeholder="100000" @bind="FeeText" /></label>
<div class="admin-dialog-actions">
<button type="submit" class="site-button primary">저장</button>
<button type="button" class="site-button secondary" @onclick='() => showAddForm = false'>취소</button>
</div>
</form>
<MudPaper Class="pa-3 mb-3" Outlined="true">
<MudGrid Spacing="2">
<MudItem xs="12" sm="6">
<MudDatePicker @bind-Date="newDate" Label="상담일" DateFormat="yyyy-MM-dd" />
</MudItem>
<MudItem xs="12" sm="6">
<MudSelect T="string" @bind-Value="newServiceType" Label="서비스 분야">
@foreach (var t in ClientService.ServiceTypes)
{
<MudSelectItem Value="@t">@t</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudTextField T="string" @bind-Value="newSummary" Label="상담 내용 *"
Lines="3" Variant="Variant.Outlined" Required="true" />
</MudItem>
<MudItem xs="12" sm="6">
<MudSelect T="string" @bind-Value="newResult" Label="결과">
<MudSelectItem Value="@("")">-</MudSelectItem>
@foreach (var r in ConsultationService.Results)
{
<MudSelectItem Value="@r">@r</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField T="decimal?" @bind-Value="newFee" Label="수임료 (원)"
Format="N0" />
</MudItem>
</MudGrid>
<MudStack Row="true" Class="mt-2" Spacing="2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="AddConsultation">저장</MudButton>
<MudButton Variant="Variant.Outlined" OnClick="@(() => showAddForm = false)">취소</MudButton>
</MudStack>
</MudPaper>
}
@if (consultations.Count == 0)
{
<p class="muted">상담 이력이 없습니다.</p>
<MudText Color="Color.Secondary">상담 이력이 없습니다.</MudText>
}
else
{
<div class="admin-activity-list">
<MudList T="string" Dense="true">
@foreach (var c in consultations)
{
<article class="admin-activity-card">
<div class="admin-activity-head">
<div>
<span class="muted">@c.ConsultationDate.ToString("yyyy-MM-dd") @(string.IsNullOrEmpty(c.ServiceType) ? "" : $"· {c.ServiceType}")</span>
</div>
<button type="button" class="admin-icon-button danger" @onclick="@(() => DeleteConsultation(c.Id))">✕</button>
</div>
<p style="white-space: pre-wrap;">@c.Summary</p>
@if (!string.IsNullOrEmpty(c.Result))
{
<span class="status-pill info">@c.Result</span>
}
@if (c.Fee.HasValue)
{
<div class="muted">수임료: @c.Fee.Value.ToString("N0")원</div>
}
</article>
<MudListItem>
<MudPaper Class="pa-3" Outlined="true" Style="width:100%">
<MudStack Row="true" AlignItems="AlignItems.Start" Justify="Justify.SpaceBetween">
<div>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@c.ConsultationDate.ToString("yyyy-MM-dd")
@if (!string.IsNullOrEmpty(c.ServiceType)) { <text> · @c.ServiceType</text> }
</MudText>
<MudText Style="white-space: pre-wrap;" Class="mt-1">@c.Summary</MudText>
@if (!string.IsNullOrEmpty(c.Result))
{
<MudChip T="string" Size="Size.Small" Color="Color.Info" Class="mt-1">@c.Result</MudChip>
}
@if (c.Fee.HasValue)
{
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-1">
수임료: @c.Fee.Value.ToString("N0")원
</MudText>
}
</div>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small" Color="Color.Error"
OnClick="@(() => DeleteConsultation(c.Id))" />
</MudStack>
</MudPaper>
</MudListItem>
}
</div>
</MudList>
}
</section>
</div>
}
</MudPaper>
</MudItem>
</MudGrid>
@code {
[Parameter] public int ClientId { get; set; }
[Parameter]
public int ClientId { get; set; }
private Domain.Entities.Client? client;
private List<Domain.Entities.Consultation> consultations = [];
private bool showAddForm;
private DateTime? newDate = DateTime.Today;
private string newServiceType = "";
@@ -128,10 +190,10 @@ else
private string newResult = "";
private decimal? newFee;
private string ConsultationDateText { get => newDate?.ToString("yyyy-MM-dd") ?? ""; set => newDate = DateTime.TryParse(value, out var dt) ? dt : null; }
private string FeeText { get => newFee?.ToString() ?? ""; set => newFee = decimal.TryParse(value, out var d) ? d : null; }
protected override async Task OnInitializedAsync() => await LoadAll();
protected override async Task OnInitializedAsync()
{
await LoadAll();
}
private async Task LoadAll()
{
@@ -153,12 +215,6 @@ else
{
try
{
if (string.IsNullOrWhiteSpace(newSummary))
{
await JS.InvokeVoidAsync("alert", "상담 내용을 입력하세요.");
return;
}
var c = new Domain.Entities.Consultation
{
ClientId = ClientId,
@@ -168,23 +224,21 @@ else
Result = string.IsNullOrWhiteSpace(newResult) ? null : newResult,
Fee = newFee
};
await ConsultationService.CreateAsync(c);
showAddForm = false;
consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList();
await JS.InvokeVoidAsync("alert", "상담이 추가되었습니다.");
Snackbar.Add("상담이 추가되었습니다.", Severity.Success);
}
catch (ValidationException ex)
{
await JS.InvokeVoidAsync("alert", ex.Message);
Snackbar.Add(ex.Message, Severity.Error);
}
}
private async Task DeleteConsultation(int id)
{
if (!await JS.InvokeAsync<bool>("confirm", "이 상담을 삭제하시겠습니까?")) return;
await ConsultationService.DeleteAsync(id);
consultations = (await ConsultationService.GetByClientIdAsync(ClientId)).ToList();
await JS.InvokeVoidAsync("alert", "삭제되었습니다.");
Snackbar.Add("삭제되었습니다.", Severity.Info);
}
}
@@ -6,74 +6,117 @@
@using TaxBaik.Domain.Entities
@inject IClientBrowserClient ClientClient
@inject NavigationManager Navigation
@inject IJSRuntime JS
@inject ISnackbar Snackbar
<PageTitle>@(Id.HasValue ? "고객 수정" : "고객 등록")</PageTitle>
<section class="admin-page-hero">
<div>
<div class="admin-eyebrow">CRM</div>
<h1 class="admin-page-title">@(Id.HasValue ? "고객 수정" : "고객 등록")</h1>
<MudText Typo="Typo.caption" Class="admin-eyebrow">CRM</MudText>
<MudText Typo="Typo.h4" Class="admin-page-title">@(Id.HasValue ? "고객 수정" : "고객 등록")</MudText>
</div>
<button type="button" class="site-button secondary" @onclick='() => Navigation.NavigateTo("/taxbaik/admin/clients")'>목록으로</button>
<MudButton Variant="Variant.Outlined" Href="/taxbaik/admin/clients"
StartIcon="@Icons.Material.Filled.ArrowBack">목록으로</MudButton>
</section>
<div class="admin-surface" style="max-width:720px;">
<MudPaper Class="admin-surface" Elevation="0" Style="max-width:720px;">
@if (isLoading)
{
<Skeleton Count="5" CssClass="taxbaik-skeleton-grid" />
<MudProgressLinear Indeterminate="true" />
}
else
{
<form class="admin-dialog-card" @onsubmit="SaveAsync" @onsubmit:preventDefault="true">
<label>고객명 * <input class="admin-input" @bind="dto.Name" /></label>
<label>회사명 <input class="admin-input" @bind="dto.CompanyName" /></label>
<label>연락처 <input class="admin-input" @bind="dto.Phone" /></label>
<label>이메일 <input class="admin-input" type="email" @bind="dto.Email" /></label>
<label>서비스 유형
<select class="admin-input" @bind="dto.ServiceType">
<option value="">선택하세요</option>
@foreach (var t in ClientService.ServiceTypes)
{
<option value="@t">@t</option>
}
</select>
</label>
<label>세금 유형
<select class="admin-input" @bind="dto.TaxType">
<option value="">선택하세요</option>
@foreach (var t in ClientService.TaxTypes)
{
<option value="@t">@t</option>
}
</select>
</label>
<label>상태
<select class="admin-input" @bind="dto.Status">
<option value="active">활성</option>
<option value="inactive">비활성</option>
</select>
</label>
<label>유입 경로
<select class="admin-input" @bind="dto.Source">
<option value="">선택하세요</option>
@foreach (var s in ClientService.Sources)
{
<option value="@s">@s</option>
}
</select>
</label>
<label>메모 <textarea class="admin-input" rows="4" @bind="dto.Memo"></textarea></label>
<div class="admin-dialog-actions">
<button type="submit" class="site-button primary" disabled="@isSaving">저장</button>
<button type="button" class="site-button secondary" @onclick='() => Navigation.NavigateTo("/taxbaik/admin/clients")'>취소</button>
</div>
</form>
<MudForm @ref="form" @bind-IsValid="isValid">
<MudGrid Spacing="3">
@* 기본 정보 *@
<MudItem xs="12">
<MudText Typo="Typo.subtitle1" Class="fw-bold mb-1">기본 정보</MudText>
<MudDivider />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="dto.Name" Label="고객명 *" Required="true"
RequiredError="고객명을 입력하세요." />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="dto.CompanyName" Label="회사명 (선택)" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="dto.Phone" Label="연락처"
Placeholder="010-0000-0000" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="dto.Email" Label="이메일" InputType="InputType.Email" />
</MudItem>
@* 세무 정보 *@
<MudItem xs="12" Class="mt-2">
<MudText Typo="Typo.subtitle1" Class="fw-bold mb-1">세무 정보</MudText>
<MudDivider />
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="dto.ServiceType" Label="서비스 유형" T="string" Clearable="true">
@foreach (var t in ClientService.ServiceTypes)
{
<MudSelectItem Value="@t">@t</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="dto.TaxType" Label="세금 유형" T="string" Clearable="true">
@foreach (var t in ClientService.TaxTypes)
{
<MudSelectItem Value="@t">@t</MudSelectItem>
}
</MudSelect>
</MudItem>
@* 관리 정보 *@
<MudItem xs="12" Class="mt-2">
<MudText Typo="Typo.subtitle1" Class="fw-bold mb-1">관리 정보</MudText>
<MudDivider />
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="dto.Status" Label="상태 *" T="string" Required="true">
<MudSelectItem Value="@("active")">활성</MudSelectItem>
<MudSelectItem Value="@("inactive")">비활성</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="dto.Source" Label="유입 경로" T="string" Clearable="true">
@foreach (var s in ClientService.Sources)
{
<MudSelectItem Value="@s">@s</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudTextField @bind-Value="dto.Memo" Label="메모"
Lines="4" AutoGrow="true"
Placeholder="상담 배경, 특이사항, 중요 날짜 등 자유롭게 기록하세요" />
</MudItem>
@* 저장 버튼 *@
<MudItem xs="12" Class="d-flex gap-2 mt-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Save"
OnClick="@SaveAsync" Disabled="@isSaving">
@(isSaving ? "저장 중..." : "저장")
</MudButton>
<MudButton Variant="Variant.Outlined" Href="/taxbaik/admin/clients">
취소
</MudButton>
</MudItem>
</MudGrid>
</MudForm>
}
</div>
</MudPaper>
@code {
[Parameter] public int? Id { get; set; }
private MudForm form = null!;
private CreateClientDto dto = new() { Status = "active" };
private bool isValid;
private bool isLoading = true;
private bool isSaving;
@@ -86,7 +129,7 @@
var client = await ClientClient.GetByIdAsync(Id.Value);
if (client is null)
{
await JS.InvokeVoidAsync("alert", "고객을 찾을 수 없습니다.");
Snackbar.Add("고객을 찾을 수 없습니다.", Severity.Error);
Navigation.NavigateTo("/taxbaik/admin/clients");
return;
}
@@ -102,42 +145,46 @@
Source = client.Source,
Memo = client.Memo
};
}
catch (Exception ex)
{
await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}");
Navigation.NavigateTo("/taxbaik/admin/clients");
return;
}
}
catch (Exception ex)
{
Snackbar.Add($"오류: {ex.Message}", Severity.Error);
Navigation.NavigateTo("/taxbaik/admin/clients");
return;
}
}
isLoading = false;
}
private async Task SaveAsync()
{
await form.Validate();
if (!isValid) return;
isSaving = true;
try
{
if (string.IsNullOrWhiteSpace(dto.Name))
{
await JS.InvokeVoidAsync("alert", "고객명을 입력하세요.");
return;
}
if (Id.HasValue)
{
var result = await ClientClient.UpdateAsync(Id.Value, dto);
await JS.InvokeVoidAsync("alert", result != null ? "고객 정보가 수정되었습니다." : "수정에 실패했습니다.");
if (result != null)
Snackbar.Add("고객 정보가 수정되었습니다.", Severity.Success);
else
Snackbar.Add("수정에 실패했습니다.", Severity.Error);
}
else
{
var result = await ClientClient.CreateAsync(dto);
await JS.InvokeVoidAsync("alert", result != null ? "고객이 등록되었습니다." : "등록에 실패했습니다.");
if (result != null)
Snackbar.Add("고객이 등록되었습니다.", Severity.Success);
else
Snackbar.Add("등록에 실패했습니다.", Severity.Error);
}
Navigation.NavigateTo("/taxbaik/admin/clients");
}
catch (Exception ex)
{
await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}");
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
}
finally
{
@@ -4,94 +4,134 @@
@using TaxBaik.Domain.Entities
@inject IClientBrowserClient ClientClient
@inject NavigationManager Navigation
@inject IJSRuntime JS
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>고객 관리</PageTitle>
<section class="admin-page-hero">
<div>
<div class="admin-eyebrow">CRM</div>
<h1 class="admin-page-title">고객 관리</h1>
<p class="admin-page-subtitle">고객 카드를 등록하고 상담 이력을 관리합니다.</p>
<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>
<button type="button" class="site-button primary" @onclick='() => Navigation.NavigateTo("/taxbaik/admin/clients/create")'>고객 등록</button>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PersonAdd"
Href="/taxbaik/admin/clients/create">
고객 등록
</MudButton>
</section>
<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 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">
<MudPaper Class="admin-surface" Elevation="0">
@if (clients is null)
{
<Skeleton Count="6" CssClass="taxbaik-skeleton-grid" />
<MudProgressLinear Indeterminate="true" />
}
else if (!clients.Any())
{
<div class="muted mt-4">등록된 고객이 없습니다.</div>
<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
{
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<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>
<th>이름</th>
<th>회사명</th>
<th>연락처</th>
<th>서비스</th>
<th>세금 유형</th>
<th>상태</th>
<th>유입 경로</th>
<th>등록일</th>
<th></th>
<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>
</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>
}
</tbody>
</MudSimpleTable>
@* 페이징 *@
@if (totalPages > 1)
{
<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 class="d-flex justify-center pa-3">
<MudPagination BoundaryCount="1" MiddleCount="3"
Count="@totalPages" Selected="@currentPage"
SelectedChanged="@OnPageChanged" />
</div>
}
<div class="admin-table-footer">총 @(totalCount)명</div>
<MudText Typo="Typo.caption" Class="pa-2 text-muted">총 @(totalCount)명</MudText>
}
</div>
</MudPaper>
@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 = "";
@@ -102,13 +142,16 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && AuthStateTask != null)
if (firstRender)
{
var authState = await AuthStateTask;
if (authState.User.Identity?.IsAuthenticated == true)
if (AuthStateTask != null)
{
await LoadAsync();
StateHasChanged();
var authState = await AuthStateTask;
if (authState.User.Identity?.IsAuthenticated == true)
{
await LoadAsync();
StateHasChanged();
}
}
}
}
@@ -117,39 +160,75 @@
{
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)
{
await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}");
Snackbar.Add($"오류: {ex.Message}", Severity.Error);
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 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 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 JS.InvokeAsync<bool>("confirm", $"'{client.Name}' 고객을 삭제하시겠습니까? 관련 데이터도 함께 삭제됩니다.");
if (!confirmed) return;
var confirmed = await DialogService.ShowMessageBox(
"고객 삭제",
$"'{client.Name}' 고객을 삭제하시겠습니까? 관련 데이터도 함께 삭제됩니다.",
yesText: "삭제", cancelText: "취소");
if (confirmed != true) return;
try
{
var success = await ClientClient.DeleteAsync(client.Id);
if (success)
{
await JS.InvokeVoidAsync("alert", $"{client.Name} 고객이 삭제되었습니다.");
Snackbar.Add($"{client.Name} 고객이 삭제되었습니다.", Severity.Success);
await LoadAsync();
}
else
{
Snackbar.Add("삭제에 실패했습니다.", Severity.Error);
}
}
catch (Exception ex)
{
await JS.InvokeVoidAsync("alert", $"오류: {ex.Message}");
Snackbar.Add($"오류: {ex.Message}", Severity.Error);
}
await LoadAsync();
}
}