refactor: admin ui를 fluent v5와 html 기반으로 전환
TaxBaik CI/CD / build-and-deploy (push) Failing after 1m53s

This commit is contained in:
2026-06-29 22:37:40 +09:00
parent 1a7bc9e209
commit 1b173376ee
51 changed files with 2471 additions and 3560 deletions
@@ -2,145 +2,125 @@
@using TaxBaik.Web.Services.AdminClients
@inject IRevenueTrackingBrowserClient RevenueClient
@inject IClientBrowserClient ClientClient
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject IJSRuntime JS
@attribute [Authorize]
<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" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
새 청구 추가
</MudButton>
<button type="button" class="site-button primary" @onclick="OpenCreateDialog">새 청구 추가</button>
</section>
<MudPaper Class="admin-surface" Elevation="0">
<div class="admin-surface">
@if (revenues is null)
{
<MudProgressLinear Indeterminate="true" />
<Skeleton Count="6" CssClass="taxbaik-skeleton-grid" />
}
else if (revenues.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="mt-4">
<MudIcon Icon="@Icons.Material.Filled.Payments" Class="me-2" />
청구 기록이 없습니다.
</MudAlert>
<div class="muted">청구 기록이 없습니다.</div>
}
else
{
<MudDataGrid T="RevenueTracking"
Items="@revenues"
Dense="true"
Hover="true"
Striped="true"
Virtualize="true"
RowsPerPage="30"
Class="admin-grid">
<Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
<TemplateColumn Title="고객">
<CellTemplate>
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
{
<MudLink Href="@($"/taxbaik/admin/clients/{context.Item.ClientId}")" Color="Color.Primary">
@clientName
</MudLink>
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.InvoiceNumber" Title="청구번호" />
<PropertyColumn Property="x => x.InvoiceDate" Title="청구일" Format="yyyy-MM-dd" />
<PropertyColumn Property="x => x.Amount" Title="청구액" Format="C" />
<TemplateColumn Title="납부여부">
<CellTemplate>
@if (context.Item.PaymentStatus == "paid")
{
<MudChip Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">납부</MudChip>
}
else
{
<MudChip Size="Size.Small" Color="Color.Warning" Variant="Variant.Filled">미납</MudChip>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="작업" Sortable="false">
<CellTemplate>
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
@if (context.Item.PaymentStatus != "paid")
{
<MudIconButton Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success"
OnClick="@(async () => await MarkPaid(context.Item.Id))" Title="납부 처리" />
}
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="@(async () => await DeleteRevenue(context.Item.Id))" />
</MudButtonGroup>
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>고객</th>
<th>청구번호</th>
<th>청구일</th>
<th>청구액</th>
<th>납부여부</th>
<th>작업</th>
</tr>
</thead>
<tbody>
@foreach (var item in revenues)
{
<tr>
<td>@item.Id</td>
<td>@clientMap.GetValueOrDefault(item.ClientId, $"Client #{item.ClientId}")</td>
<td>@item.InvoiceNumber</td>
<td>@item.InvoiceDate.ToString("yyyy-MM-dd")</td>
<td>@item.Amount.ToString("C")</td>
<td><span class="status-pill @(item.PaymentStatus == "paid" ? "success" : "warning")">@(item.PaymentStatus == "paid" ? "납부" : "미납")</span></td>
<td>
<div class="admin-row-actions">
@if (item.PaymentStatus != "paid")
{
<button type="button" class="site-button secondary" @onclick="@(async () => await MarkPaid(item.Id))">완료</button>
}
<button type="button" class="admin-icon-button danger" @onclick="@(async () => await DeleteRevenue(item.Id))">✕</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</MudPaper>
</div>
<!-- Create Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent>
<MudText Typo="Typo.h6">새 청구 추가</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="form">
<MudSelect T="int" @bind-Value="revenueForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
<dialog class="admin-dialog" open="@isDialogOpen">
<form class="admin-dialog-card" @onsubmit="SaveRevenue" @onsubmit:preventDefault="true">
<h3>새 청구 추가</h3>
<label>고객
<select class="admin-input" @bind="ClientIdText">
<option value="">선택하세요</option>
@foreach (var client in clients)
{
<MudSelectItem Value="@client.Id">@GetClientDisplayName(client)</MudSelectItem>
<option value="@client.Id.ToString()">@GetClientDisplayName(client)</option>
}
</MudSelect>
<MudTextField T="string" @bind-Value="revenueForm.InvoiceNumber" Label="청구번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
<MudDatePicker @bind-Date="revenueForm.InvoiceDate" Label="청구일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
<MudNumericField T="decimal" @bind-Value="revenueForm.Amount" Label="청구액" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
<MudSelect T="string" @bind-Value="revenueForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
<MudSelectItem Value="@("기장 수수료")">기장 수수료</MudSelectItem>
<MudSelectItem Value="@("세무조정료")">세무조정료</MudSelectItem>
<MudSelectItem Value="@("세무상담료")">세무상담료</MudSelectItem>
<MudSelectItem Value="@("신고 대행료")">신고 대행료</MudSelectItem>
<MudSelectItem Value="@("자문 수수료")">자문 수수료</MudSelectItem>
</MudSelect>
<MudDatePicker @bind-Date="revenueForm.DueDate" Label="납부예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDialog">취소</MudButton>
<MudButton Color="Color.Primary" OnClick="SaveRevenue">저장</MudButton>
</DialogActions>
</MudDialog>
</select>
</label>
<label>청구번호 <input class="admin-input" @bind="revenueForm.InvoiceNumber" /></label>
<label>청구일 <input class="admin-input" type="text" placeholder="2026-06-29" @bind="InvoiceDateText" /></label>
<label>청구액 <input class="admin-input" type="text" placeholder="100000" @bind="AmountText" /></label>
<label>서비스 유형
<select class="admin-input" @bind="revenueForm.ServiceType">
<option value="">선택하세요</option>
<option value="기장 수수료">기장 수수료</option>
<option value="세무조정료">세무조정료</option>
<option value="세무상담료">세무상담료</option>
<option value="신고 대행료">신고 대행료</option>
<option value="자문 수수료">자문 수수료</option>
</select>
</label>
<label>납부예정일 <input class="admin-input" type="text" placeholder="2026-07-13" @bind="DueDateText" /></label>
<div class="admin-dialog-actions">
<button type="button" class="site-button secondary" @onclick="CloseDialog">취소</button>
<button type="submit" class="site-button primary">저장</button>
</div>
</form>
</dialog>
@code {
[CascadingParameter]
private Task<AuthenticationState>? AuthStateTask { get; set; }
[CascadingParameter] private Task<AuthenticationState>? AuthStateTask { get; set; }
private List<RevenueTracking>? revenues;
private List<Client> clients = [];
private Dictionary<int, string> clientMap = new();
private MudForm? form;
private bool isDialogOpen;
private RevenueForm revenueForm = new();
private string ClientIdText { get => revenueForm.ClientId > 0 ? revenueForm.ClientId.ToString() : ""; set => revenueForm.ClientId = int.TryParse(value, out var id) ? id : 0; }
private string InvoiceDateText { get => revenueForm.InvoiceDate?.ToString("yyyy-MM-dd") ?? ""; set => revenueForm.InvoiceDate = DateTime.TryParse(value, out var dt) ? dt : null; }
private string AmountText { get => revenueForm.Amount?.ToString() ?? ""; set => revenueForm.Amount = decimal.TryParse(value, out var amt) ? amt : null; }
private string DueDateText { get => revenueForm.DueDate?.ToString("yyyy-MM-dd") ?? ""; set => revenueForm.DueDate = DateTime.TryParse(value, out var dt) ? dt : null; }
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 LoadData();
StateHasChanged();
}
await LoadData();
StateHasChanged();
}
}
}
@@ -156,53 +136,36 @@
}
catch (Exception ex)
{
Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
await JS.InvokeVoidAsync("alert", $"데이터 로드 실패: {ex.Message}");
}
}
private void OpenCreateDialog()
{
revenueForm = new RevenueForm
{
ClientId = clients.FirstOrDefault()?.Id ?? 0,
InvoiceDate = DateTime.Today,
DueDate = DateTime.Today.AddDays(14)
};
revenueForm = new RevenueForm { ClientId = clients.FirstOrDefault()?.Id ?? 0, InvoiceDate = DateTime.Today, DueDate = DateTime.Today.AddDays(14) };
isDialogOpen = true;
}
private async Task SaveRevenue()
{
if (form != null)
if (revenueForm.ClientId <= 0 || string.IsNullOrWhiteSpace(revenueForm.InvoiceNumber) || revenueForm.Amount is null)
{
await form.Validate();
if (!form.IsValid)
{
Snackbar.Add("필수 항목을 입력해주세요.", Severity.Warning);
return;
}
await JS.InvokeVoidAsync("alert", "필수 항목을 입력해주세요.");
return;
}
try
{
var newId = await RevenueClient.CreateAsync(
revenueForm.ClientId,
revenueForm.InvoiceNumber,
revenueForm.InvoiceDate ?? DateTime.Now,
revenueForm.Amount,
revenueForm.ServiceType,
revenueForm.DueDate);
var newId = await RevenueClient.CreateAsync(revenueForm.ClientId, revenueForm.InvoiceNumber, revenueForm.InvoiceDate ?? DateTime.Today, revenueForm.Amount.Value, revenueForm.ServiceType, revenueForm.DueDate);
if (newId > 0)
{
Snackbar.Add("청구가 추가되었습니다.", Severity.Success);
await JS.InvokeVoidAsync("alert", "청구가 추가되었습니다.");
CloseDialog();
await LoadData();
}
}
catch (Exception ex)
{
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}");
}
}
@@ -211,60 +174,31 @@
try
{
await RevenueClient.MarkPaidAsync(id, DateTime.Now);
Snackbar.Add("납부가 처리되었습니다.", Severity.Success);
await JS.InvokeVoidAsync("alert", "납부가 처리되었습니다.");
await LoadData();
}
catch (Exception ex)
{
Snackbar.Add($"처리 실패: {ex.Message}", Severity.Error);
await JS.InvokeVoidAsync("alert", $"처리 실패: {ex.Message}");
}
}
private async Task DeleteRevenue(int id)
{
var parameters = new DialogParameters
{
{ "Title", "삭제 확인" },
{ "Message", "이 청구를 삭제하시겠습니까?" }
};
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
var result = await dialog.Result;
if (result?.Canceled ?? true)
return;
if (!await JS.InvokeAsync<bool>("confirm", "이 청구를 삭제하시겠습니까?")) return;
try
{
await RevenueClient.DeleteAsync(id);
Snackbar.Add("청구가 삭제되었습니다.", Severity.Success);
await JS.InvokeVoidAsync("alert", "청구가 삭제되었습니다.");
await LoadData();
}
catch (Exception ex)
{
Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
await JS.InvokeVoidAsync("alert", $"삭제 실패: {ex.Message}");
}
}
private void CloseDialog()
{
isDialogOpen = false;
revenueForm = new();
}
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class RevenueForm
{
public int ClientId { get; set; }
public string InvoiceNumber { get; set; } = "";
public DateTime? InvoiceDate { get; set; }
public decimal Amount { get; set; }
public string? ServiceType { get; set; }
public DateTime? DueDate { get; set; }
}
private void CloseDialog() { isDialogOpen = false; revenueForm = new(); }
private static string GetClientDisplayName(Client client) => !string.IsNullOrWhiteSpace(client.CompanyName) ? client.CompanyName : !string.IsNullOrWhiteSpace(client.Name) ? client.Name : $"Client #{client.Id}";
private sealed class RevenueForm { public int ClientId { get; set; } public string InvoiceNumber { get; set; } = ""; public DateTime? InvoiceDate { get; set; } public decimal? Amount { get; set; } public string? ServiceType { get; set; } public DateTime? DueDate { get; set; } }
}