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
@@ -2,128 +2,145 @@
@using TaxBaik.Web.Services.AdminClients
@inject ITaxProfileBrowserClient TaxProfileClient
@inject IClientBrowserClient ClientClient
@inject IJSRuntime JS
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@attribute [Authorize]
<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="OpenCreateDialog">새 프로필 추가</button>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
새 프로필 추가
</MudButton>
</section>
<div class="admin-surface">
@if (profiles is null)
{
<Skeleton Count="5" CssClass="taxbaik-skeleton-grid" />
}
else if (profiles.Count == 0)
{
<div class="muted">세무 프로필이 없습니다.</div>
}
else
{
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>고객</th>
<th>사업 유형</th>
<th>위험도</th>
<th>다음 신고</th>
<th>작업</th>
</tr>
</thead>
<tbody>
@foreach (var item in profiles)
@if (profiles == null)
{
<MudProgressCircular Indeterminate="true" Class="mt-4" />
}
else if (profiles.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="mt-4">세무 프로필이 없습니다.</MudAlert>
}
else
{
<MudDataGrid T="TaxProfile"
Items="@profiles"
Dense="true"
Hover="true"
Striped="true"
Virtualize="true"
RowsPerPage="30"
Class="admin-grid mt-4">
<Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
<TemplateColumn Title="고객">
<CellTemplate>
@if (clientMap.TryGetValue(context.Item.ClientId, out var clientName))
{
<tr>
<td>@item.Id</td>
<td>@(clientMap.GetValueOrDefault(item.ClientId, $"Client #{item.ClientId}"))</td>
<td>@item.BusinessType</td>
<td><span class="status-pill @(item.TaxRiskLevel == "high" ? "danger" : item.TaxRiskLevel == "normal" ? "warning" : "success")">@item.TaxRiskLevel</span></td>
<td>@(item.NextFilingDueDate?.ToString("yyyy-MM-dd") ?? "—")</td>
<td>
<div class="admin-row-actions">
<button type="button" class="admin-icon-button" @onclick="@(async () => await OpenEditDialog(item))">✎</button>
<button type="button" class="admin-icon-button danger" @onclick="@(async () => await DeleteProfile(item.Id))">✕</button>
</div>
</td>
</tr>
<MudLink Href="@($"/taxbaik/admin/clients/{context.Item.ClientId}")" Color="Color.Primary">
@clientName
</MudLink>
}
</tbody>
</table>
</div>
}
</div>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.BusinessType" Title="사업 유형" />
<TemplateColumn Title="위험도">
<CellTemplate>
<MudChip Size="Size.Small" Color="@GetRiskColor(context.Item.TaxRiskLevel)" Variant="Variant.Filled">
@context.Item.TaxRiskLevel
</MudChip>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="다음 신고">
<CellTemplate>
@if (context.Item.NextFilingDueDate.HasValue)
{
@context.Item.NextFilingDueDate.Value.ToString("yyyy-MM-dd")
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="작업" Sortable="false">
<CellTemplate>
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
<MudIconButton Icon="@Icons.Material.Filled.Edit" OnClick="@(async () => await OpenEditDialog(context.Item))" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@(async () => await DeleteProfile(context.Item.Id))" />
</MudButtonGroup>
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
}
<dialog class="admin-dialog" open="@isDialogOpen">
<form class="admin-dialog-card" @onsubmit="SaveProfile" @onsubmit:preventDefault="true">
<h3>@(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가")</h3>
<label>고객
<select class="admin-input" @bind="ClientIdText">
<option value="">선택하세요</option>
<!-- Create/Edit Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent>
<MudText Typo="Typo.h6">@(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가")</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="form">
<MudSelect T="int?" @bind-Value="profileForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
@foreach (var client in clients)
{
<option value="@client.Id.ToString()">@GetClientDisplayName(client)</option>
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
}
</select>
</label>
<label>사업 유형
<select class="admin-input" @bind="profileForm.BusinessType">
<option value="">선택하세요</option>
<option value="일반제조업">일반제조업</option>
<option value="도소매업">도소매업</option>
<option value="서비스업">서비스업</option>
<option value="정보통신업">정보통신업</option>
<option value="부동산업">부동산업</option>
<option value="건설업">건설업</option>
<option value="음식점업">음식점업</option>
<option value="프리랜서">프리랜서</option>
<option value="기타">기타</option>
</select>
</label>
<label>위험도
<select class="admin-input" @bind="profileForm.TaxRiskLevel">
<option value="low">낮음</option>
<option value="normal">보통</option>
<option value="high">높음</option>
</select>
</label>
<label>다음 신고 예정일 <input class="admin-input" type="text" placeholder="2026-07-01" @bind="NextFilingText" /></label>
<label>특수 사항 <textarea class="admin-input" rows="3" @bind="profileForm.SpecialNotes"></textarea></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>
</MudSelect>
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
<MudSelectItem Value="@("일반제조업")">일반제조업</MudSelectItem>
<MudSelectItem Value="@("도소매업")">도소매업</MudSelectItem>
<MudSelectItem Value="@("서비스업")">서비스업</MudSelectItem>
<MudSelectItem Value="@("정보통신업")">정보통신업</MudSelectItem>
<MudSelectItem Value="@("부동산업")">부동산업</MudSelectItem>
<MudSelectItem Value="@("건설업")">건설업</MudSelectItem>
<MudSelectItem Value="@("음식점업")">음식점업</MudSelectItem>
<MudSelectItem Value="@("프리랜서")">프리랜서</MudSelectItem>
<MudSelectItem Value="@("기타")">기타</MudSelectItem>
</MudSelect>
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
<MudSelectItem Value="@("low")">낮음</MudSelectItem>
<MudSelectItem Value="@("normal")">보통</MudSelectItem>
<MudSelectItem Value="@("high")">높음</MudSelectItem>
</MudSelect>
<MudDatePicker @bind-Date="profileForm.NextFilingDueDate" Label="다음 신고 예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
<MudTextField T="string" @bind-Value="profileForm.SpecialNotes" Label="특수 사항" Variant="Variant.Outlined" FullWidth="true" Lines="2" />
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDialog">취소</MudButton>
<MudButton Color="Color.Primary" OnClick="SaveProfile">저장</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private Task<AuthenticationState>? AuthStateTask { get; set; }
[CascadingParameter]
private Task<AuthenticationState>? AuthStateTask { get; set; }
private List<TaxProfile>? profiles;
private List<Client> clients = [];
private Dictionary<int, string> clientMap = new();
private MudForm? form;
private bool isDialogOpen;
private bool isEditMode;
private TaxProfile? editingProfile;
private TaxProfileForm profileForm = new();
private string ClientIdText { get => profileForm.ClientId > 0 ? profileForm.ClientId.ToString() : ""; set => profileForm.ClientId = int.TryParse(value, out var id) ? id : 0; }
private string NextFilingText { get => profileForm.NextFilingDueDate?.ToString("yyyy-MM-dd") ?? ""; set => profileForm.NextFilingDueDate = DateTime.TryParse(value, out var dt) ? dt : null; }
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 LoadData();
StateHasChanged();
var authState = await AuthStateTask;
if (authState.User.Identity?.IsAuthenticated == true)
{
await LoadData();
StateHasChanged();
}
}
}
}
@@ -139,7 +156,7 @@
}
catch (Exception ex)
{
await JS.InvokeVoidAsync("alert", $"데이터 로드 실패: {ex.Message}");
Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
}
}
@@ -147,7 +164,12 @@
{
isEditMode = false;
editingProfile = null;
profileForm = new TaxProfileForm { ClientId = clients.FirstOrDefault()?.Id ?? 0, TaxRiskLevel = "normal", NextFilingDueDate = DateTime.Today.AddMonths(1) };
profileForm = new TaxProfileForm
{
ClientId = clients.FirstOrDefault()?.Id,
TaxRiskLevel = "normal",
NextFilingDueDate = DateTime.Today.AddMonths(1)
};
isDialogOpen = true;
}
@@ -164,30 +186,48 @@
SpecialNotes = profile.SpecialNotes
};
isDialogOpen = true;
await Task.CompletedTask;
}
private async Task SaveProfile()
{
if (profileForm.ClientId <= 0 || string.IsNullOrWhiteSpace(profileForm.BusinessType))
if (form != null)
{
await JS.InvokeVoidAsync("alert", "고객과 사업 유형을 입력하세요.");
return;
await form.Validate();
if (!form.IsValid)
{
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
return;
}
}
try
{
if (isEditMode && editingProfile != null)
{
await TaxProfileClient.UpdateAsync(editingProfile.Id, profileForm.BusinessType, null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel);
await JS.InvokeVoidAsync("alert", "세무 프로필이 수정되었습니다.");
await TaxProfileClient.UpdateAsync(editingProfile.Id, profileForm.BusinessType,
null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel);
Snackbar.Add("세무 프로필이 수정되었습니다.", Severity.Success);
}
else
{
var newId = await TaxProfileClient.CreateAsync(profileForm.ClientId, profileForm.BusinessType);
if (!profileForm.ClientId.HasValue)
{
Snackbar.Add("고객을 선택하세요.", Severity.Warning);
return;
}
var newId = await TaxProfileClient.CreateAsync(
profileForm.ClientId.Value,
profileForm.BusinessType);
if (newId > 0)
{
await TaxProfileClient.UpdateAsync(newId, profileForm.BusinessType, null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel);
await JS.InvokeVoidAsync("alert", "세무 프로필이 추가되었습니다.");
// 생성 후 상태 업데이트 처리
await TaxProfileClient.UpdateAsync(
newId,
profileForm.BusinessType,
null,
profileForm.NextFilingDueDate,
profileForm.TaxRiskLevel);
Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success);
}
}
CloseDialog();
@@ -195,26 +235,62 @@
}
catch (Exception ex)
{
await JS.InvokeVoidAsync("alert", $"저장 실패: {ex.Message}");
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
}
}
private async Task DeleteProfile(int id)
{
if (!await JS.InvokeAsync<bool>("confirm", "이 세무 프로필을 삭제하시겠습니까?")) return;
var parameters = new DialogParameters();
parameters.Add("Title", "삭제 확인");
parameters.Add("Message", "이 세무 프로필을 삭제하시겠습니까?");
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
var result = await dialog.Result;
if (result?.Canceled ?? true)
return;
try
{
await TaxProfileClient.DeleteAsync(id);
await JS.InvokeVoidAsync("alert", "세무 프로필이 삭제되었습니다.");
Snackbar.Add("세무 프로필이 삭제되었습니다.", Severity.Success);
await LoadData();
}
catch (Exception ex)
{
await JS.InvokeVoidAsync("alert", $"삭제 실패: {ex.Message}");
Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
}
}
private void CloseDialog() { isDialogOpen = false; isEditMode = false; editingProfile = null; profileForm = 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 TaxProfileForm { public int ClientId { get; set; } public string BusinessType { get; set; } = ""; public string TaxRiskLevel { get; set; } = "normal"; public DateTime? NextFilingDueDate { get; set; } public string? SpecialNotes { get; set; } }
private void CloseDialog()
{
isDialogOpen = false;
isEditMode = false;
editingProfile = null;
profileForm = new();
}
private Color GetRiskColor(string riskLevel) => riskLevel switch
{
"high" => Color.Error,
"normal" => Color.Warning,
"low" => Color.Success,
_ => Color.Default
};
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class TaxProfileForm
{
public int? ClientId { get; set; }
public string BusinessType { get; set; } = "";
public string TaxRiskLevel { get; set; } = "normal";
public DateTime? NextFilingDueDate { get; set; }
public string? SpecialNotes { get; set; }
}
}