feat: implement ERP-style split pane master-detail layout for tax profiles, schedules, and contracts backoffice pages
TaxBaik CI/CD / build-and-deploy (push) Successful in 1m31s

This commit is contained in:
2026-06-30 22:44:32 +09:00
parent 80c97fba96
commit b2477d977b
4 changed files with 447 additions and 361 deletions
@@ -21,19 +21,23 @@
</MudText>
}
</div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-contract">
새 계약 추가
</MudButton>
</section>
<MudPaper Class="admin-surface" Elevation="0">
@if (contracts is null)
{
@if (contracts is null)
{
<MudProgressLinear Indeterminate="true" />
}
else if (contracts.Count == 0)
}
else
{
<MudGrid Spacing="2" Class="mt-2">
<!-- Left: Dense Grid List -->
<MudItem XS="12" MD="8">
@if (contracts.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="mt-4">
<MudAlert Severity="Severity.Info">
<MudIcon Icon="@Icons.Material.Filled.Description" Class="me-2" />
계약이 없습니다.
</MudAlert>
@@ -47,6 +51,8 @@
Striped="true"
Virtualize="true"
RowsPerPage="30"
SelectedItem="@selectedContract"
SelectedItemChanged="OnRowSelected"
Class="admin-grid">
<Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
@@ -54,9 +60,7 @@
<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>
@@ -89,32 +93,36 @@
</TemplateColumn>
<TemplateColumn Title="작업" Sortable="false">
<CellTemplate>
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
OnClick="@(async () => await DeleteContract(context.Item.Id))" />
</MudButtonGroup>
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
}
</MudPaper>
</MudItem>
<!-- Create Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent>
<MudText Typo="Typo.h6">새 계약 추가</MudText>
</TitleContent>
<DialogContent>
<!-- Right: Detail Form Panel (Inline Editor) -->
<MudItem XS="12" MD="4">
<MudPaper Class="pa-4 admin-surface admin-editor-panel" Elevation="0" Style="border: 1px solid var(--border-color); min-height: 400px;">
<div class="d-flex align-center justify-space-between mb-4">
<MudText Typo="Typo.h6" Class="font-weight-bold">@(isEditMode ? "계약 상세 정보" : "새 계약 추가")</MudText>
@if (isEditMode)
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="PrepareCreate">
새로 작성
</MudButton>
}
</div>
<MudForm @ref="form">
<MudSelect T="int?" @bind-Value="contractForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
<MudSelect T="int?" @bind-Value="contractForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" RequiredError="고객을 선택하세요." Disabled="@isEditMode">
@foreach (var client in clients)
{
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
<MudSelect T="string" @bind-Value="contractForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
<MudTextField T="string" @bind-Value="contractForm.ContractNumber" Label="계약번호" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" Required="true" />
<MudSelect T="string" @bind-Value="contractForm.ServiceType" Label="서비스 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" Required="true">
<MudSelectItem Value="@("개인 기장대리")">개인 기장대리</MudSelectItem>
<MudSelectItem Value="@("법인 기장대리")">법인 기장대리</MudSelectItem>
<MudSelectItem Value="@("세무조정 대행")">세무조정 대행</MudSelectItem>
@@ -122,15 +130,24 @@
<MudSelectItem Value="@("상속·증여 자문")">상속·증여 자문</MudSelectItem>
<MudSelectItem Value="@("세무조사 대응")">세무조사 대응</MudSelectItem>
</MudSelect>
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
<MudDatePicker @bind-Date="contractForm.StartDate" Label="계약 시작일" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" Required="true" />
<MudNumericField T="decimal?" @bind-Value="contractForm.MonthlyFee" Label="월 수수료" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" />
<div class="d-flex justify-end gap-2">
@if (isEditMode)
{
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="@(async () => await DeleteContract(selectedContract?.Id ?? 0))">삭제</MudButton>
}
else
{
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveContract" id="btn-save-contract">저장</MudButton>
}
</div>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDialog">취소</MudButton>
<MudButton Color="Color.Primary" OnClick="SaveContract">저장</MudButton>
</DialogActions>
</MudDialog>
</MudPaper>
</MudItem>
</MudGrid>
}
@code {
[CascadingParameter]
@@ -141,7 +158,8 @@
private Dictionary<int, string> clientMap = new();
private decimal mrr = 0;
private MudForm? form;
private bool isDialogOpen;
private bool isEditMode;
private Contract? selectedContract;
private ContractForm contractForm = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -154,6 +172,7 @@
if (authState.User.Identity?.IsAuthenticated == true)
{
await LoadData();
PrepareCreate();
StateHasChanged();
}
}
@@ -176,14 +195,30 @@
}
}
private void OpenCreateDialog()
private void PrepareCreate()
{
selectedContract = null;
isEditMode = false;
contractForm = new ContractForm
{
ClientId = clients.FirstOrDefault()?.Id,
StartDate = DateTime.Today
};
isDialogOpen = true;
}
private void OnRowSelected(Contract contract)
{
if (contract == null) return;
selectedContract = contract;
isEditMode = true;
contractForm = new ContractForm
{
ClientId = contract.ClientId,
ContractNumber = contract.ContractNumber,
ServiceType = contract.ServiceType,
StartDate = contract.StartDate,
MonthlyFee = contract.MonthlyFee
};
}
private async Task SaveContract()
@@ -211,7 +246,7 @@
if (newId > 0)
{
Snackbar.Add("계약이 추가되었습니다.", Severity.Success);
CloseDialog();
PrepareCreate();
await LoadData();
}
}
@@ -239,6 +274,10 @@
{
await ContractClient.DeleteAsync(id);
Snackbar.Add("계약이 삭제되었습니다.", Severity.Success);
if (selectedContract?.Id == id)
{
PrepareCreate();
}
await LoadData();
}
catch (Exception ex)
@@ -247,18 +286,13 @@
}
}
private void CloseDialog()
{
isDialogOpen = false;
contractForm = new();
}
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class ContractForm
{
public int? ClientId { get; set; }
@@ -14,22 +14,23 @@
<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"
OnClick="OpenCreateDialog"
StartIcon="@Icons.Material.Filled.Add">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-schedule">
새 일정 추가
</MudButton>
</section>
<MudPaper Class="admin-surface" Elevation="0">
@if (schedules is null)
{
@if (schedules is null)
{
<MudProgressLinear Indeterminate="true" />
}
else if (schedules.Count == 0)
}
else
{
<MudGrid Spacing="2" Class="mt-2">
<!-- Left: Dense Grid List -->
<MudItem XS="12" MD="8">
@if (schedules.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="mt-4">
<MudAlert Severity="Severity.Info">
<MudIcon Icon="@Icons.Material.Filled.EventBusy" Class="me-2" />
신고 일정이 없습니다.
</MudAlert>
@@ -43,6 +44,8 @@
Striped="true"
Virtualize="true"
RowsPerPage="30"
SelectedItem="@selectedSchedule"
SelectedItemChanged="OnRowSelected"
Class="admin-grid">
<Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
@@ -50,9 +53,7 @@
<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>
@@ -109,13 +110,20 @@
</Columns>
</MudDataGrid>
}
</MudPaper>
</MudItem>
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent>
<MudText Typo="Typo.h6">새 신고 일정 추가</MudText>
</TitleContent>
<DialogContent>
<!-- Right: Detail Form Panel (Inline Editor) -->
<MudItem XS="12" MD="4">
<MudPaper Class="pa-4 admin-surface admin-editor-panel" Elevation="0" Style="border: 1px solid var(--border-color); min-height: 400px;">
<div class="d-flex align-center justify-space-between mb-4">
<MudText Typo="Typo.h6" Class="font-weight-bold">@(isEditMode ? "신고 일정 상세" : "새 신고 일정 추가")</MudText>
@if (isEditMode)
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="PrepareCreate">
새로 작성
</MudButton>
}
</div>
<MudForm @ref="form">
<MudSelect T="int?"
@bind-Value="scheduleForm.ClientId"
@@ -123,14 +131,15 @@
Required="true"
Variant="Variant.Outlined"
FullWidth="true"
Class="mb-4"
RequiredError="고객을 선택하세요.">
Class="mb-3"
RequiredError="고객을 선택하세요."
Disabled="@isEditMode">
@foreach (var client in clients)
{
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" @bind-Value="scheduleForm.FilingType" Label="신고 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
<MudSelect T="string" @bind-Value="scheduleForm.FilingType" Label="신고 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" Required="true">
<MudSelectItem Value="@("종합소득세")">종합소득세</MudSelectItem>
<MudSelectItem Value="@("부가가치세")">부가가치세</MudSelectItem>
<MudSelectItem Value="@("법인세")">법인세</MudSelectItem>
@@ -140,15 +149,28 @@
<MudSelectItem Value="@("상속·증여세")">상속·증여세</MudSelectItem>
<MudSelectItem Value="@("세무조정")">세무조정</MudSelectItem>
</MudSelect>
<MudDatePicker @bind-Date="scheduleForm.DueDate" Label="마감일" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
<MudDatePicker @bind-Date="scheduleForm.DueDate" Label="마감일" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" Required="true" />
<MudNumericField T="int" @bind-Value="scheduleForm.FilingYear" Label="신고연도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true" />
<div class="d-flex justify-end gap-2">
@if (isEditMode && selectedSchedule?.Status != "completed")
{
<MudButton Variant="Variant.Outlined" Color="Color.Success" OnClick="@(async () => await CompleteSchedule(selectedSchedule?.Id ?? 0))">완료 처리</MudButton>
}
@if (isEditMode)
{
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="@(async () => await DeleteSchedule(selectedSchedule?.Id ?? 0))">삭제</MudButton>
}
else
{
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSchedule" id="btn-save-schedule">저장</MudButton>
}
</div>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDialog">취소</MudButton>
<MudButton Color="Color.Primary" OnClick="SaveSchedule">저장</MudButton>
</DialogActions>
</MudDialog>
</MudPaper>
</MudItem>
</MudGrid>
}
@code {
[CascadingParameter]
@@ -158,7 +180,8 @@
private List<Client> clients = [];
private Dictionary<int, string> clientMap = new();
private MudForm? form;
private bool isDialogOpen;
private bool isEditMode;
private TaxFilingSchedule? selectedSchedule;
private TaxFilingScheduleForm scheduleForm = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -171,6 +194,7 @@
if (authState.User.Identity?.IsAuthenticated == true)
{
await LoadData();
PrepareCreate();
StateHasChanged();
}
}
@@ -192,15 +216,30 @@
}
}
private void OpenCreateDialog()
private void PrepareCreate()
{
selectedSchedule = null;
isEditMode = false;
scheduleForm = new TaxFilingScheduleForm
{
FilingYear = DateTime.Now.Year,
DueDate = DateTime.Today,
ClientId = clients.FirstOrDefault()?.Id
};
isDialogOpen = true;
}
private void OnRowSelected(TaxFilingSchedule schedule)
{
if (schedule == null) return;
selectedSchedule = schedule;
isEditMode = true;
scheduleForm = new TaxFilingScheduleForm
{
ClientId = schedule.ClientId,
FilingType = schedule.FilingType,
DueDate = schedule.DueDate,
FilingYear = schedule.FilingYear
};
}
private async Task SaveSchedule()
@@ -227,7 +266,7 @@
if (newId > 0)
{
Snackbar.Add("신고 일정이 추가되었습니다.", Severity.Success);
CloseDialog();
PrepareCreate();
await LoadData();
}
else
@@ -247,6 +286,10 @@
{
await TaxFilingClient.MarkCompletedAsync(id);
Snackbar.Add("신고 일정이 완료 처리되었습니다.", Severity.Success);
if (selectedSchedule?.Id == id)
{
PrepareCreate();
}
await LoadData();
}
catch (Exception ex)
@@ -272,6 +315,10 @@
{
await TaxFilingClient.DeleteAsync(id);
Snackbar.Add("신고 일정이 삭제되었습니다.", Severity.Success);
if (selectedSchedule?.Id == id)
{
PrepareCreate();
}
await LoadData();
}
catch (Exception ex)
@@ -280,18 +327,13 @@
}
}
private void CloseDialog()
{
isDialogOpen = false;
scheduleForm = new();
}
private static string GetClientDisplayName(Client client)
=> !string.IsNullOrWhiteSpace(client.CompanyName)
? client.CompanyName
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class TaxFilingScheduleForm
{
public int? ClientId { get; set; }
@@ -15,7 +15,7 @@
<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" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="PrepareCreate" StartIcon="@Icons.Material.Filled.Add" id="btn-add-profile">
새 프로필 추가
</MudButton>
</section>
@@ -24,12 +24,17 @@
{
<MudProgressCircular Indeterminate="true" Class="mt-4" />
}
else if (profiles.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="mt-4">세무 프로필이 없습니다.</MudAlert>
}
else
{
<MudGrid Spacing="2" Class="mt-2">
<!-- Left: Dense Grid List -->
<MudItem XS="12" MD="8">
@if (profiles.Count == 0)
{
<MudAlert Severity="Severity.Info">세무 프로필이 없습니다.</MudAlert>
}
else
{
<MudDataGrid T="TaxProfile"
Items="@profiles"
Dense="true"
@@ -37,16 +42,16 @@ else
Striped="true"
Virtualize="true"
RowsPerPage="30"
Class="admin-grid mt-4">
SelectedItem="@selectedProfile"
SelectedItemChanged="OnRowSelected"
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>
@@ -68,50 +73,60 @@ else
</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>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="@(async () => await DeleteProfile(context.Item.Id))" />
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
}
}
</MudItem>
<!-- Create/Edit Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent>
<MudText Typo="Typo.h6">@(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가")</MudText>
</TitleContent>
<DialogContent>
<!-- Right: Detail Form Panel (Inline Editor) -->
<MudItem XS="12" MD="4">
<MudPaper Class="pa-4 admin-surface admin-editor-panel" Elevation="0" Style="border: 1px solid var(--border-color); min-height: 400px;">
<div class="d-flex align-center justify-space-between mb-4">
<MudText Typo="Typo.h6" Class="font-weight-bold">@(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가")</MudText>
@if (isEditMode)
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="PrepareCreate">
새로 작성
</MudButton>
}
</div>
<MudForm @ref="form">
<MudSelect T="int?" @bind-Value="profileForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" RequiredError="고객을 선택하세요.">
<MudSelect T="int?" @bind-Value="profileForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" RequiredError="고객을 선택하세요." Disabled="@isEditMode">
@foreach (var client in clients)
{
<MudSelectItem Value="@((int?)client.Id)">@GetClientDisplayName(client)</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-4" Required="true">
<MudSelect T="string" @bind-Value="profileForm.BusinessType" Label="사업 유형" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" Required="true">
@foreach (var type in businessTypes)
{
<MudSelectItem Value="@type.CodeValue">@type.CodeName</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-4">
<MudSelect T="string" @bind-Value="profileForm.TaxRiskLevel" Label="위험도" Variant="Variant.Outlined" FullWidth="true" Class="mb-3">
@foreach (var level in riskLevels)
{
<MudSelectItem Value="@level.CodeValue">@level.CodeName</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" />
<MudDatePicker @bind-Date="profileForm.NextFilingDueDate" Label="다음 신고 예정일" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
<MudTextField T="string" @bind-Value="profileForm.SpecialNotes" Label="특수 사항" Variant="Variant.Outlined" FullWidth="true" Lines="3" Class="mb-4" />
<div class="d-flex justify-end gap-2">
@if (isEditMode)
{
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="@(async () => await DeleteProfile(selectedProfile?.Id ?? 0))">삭제</MudButton>
}
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveProfile" id="btn-save-profile">저장</MudButton>
</div>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDialog">취소</MudButton>
<MudButton Color="Color.Primary" OnClick="SaveProfile">저장</MudButton>
</DialogActions>
</MudDialog>
</MudPaper>
</MudItem>
</MudGrid>
}
@code {
[CascadingParameter]
@@ -123,9 +138,8 @@ else
private List<CommonCode> businessTypes = [];
private List<CommonCode> riskLevels = [];
private MudForm? form;
private bool isDialogOpen;
private bool isEditMode;
private TaxProfile? editingProfile;
private TaxProfile? selectedProfile;
private TaxProfileForm profileForm = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -138,6 +152,7 @@ else
if (authState.User.Identity?.IsAuthenticated == true)
{
await LoadData();
PrepareCreate();
StateHasChanged();
}
}
@@ -185,23 +200,23 @@ else
}
}
private void OpenCreateDialog()
private void PrepareCreate()
{
selectedProfile = null;
isEditMode = false;
editingProfile = null;
profileForm = new TaxProfileForm
{
ClientId = clients.FirstOrDefault()?.Id,
TaxRiskLevel = "normal",
NextFilingDueDate = DateTime.Today.AddMonths(1)
};
isDialogOpen = true;
}
private async Task OpenEditDialog(TaxProfile profile)
private void OnRowSelected(TaxProfile profile)
{
if (profile == null) return;
selectedProfile = profile;
isEditMode = true;
editingProfile = profile;
profileForm = new TaxProfileForm
{
ClientId = profile.ClientId,
@@ -210,7 +225,6 @@ else
NextFilingDueDate = profile.NextFilingDueDate,
SpecialNotes = profile.SpecialNotes
};
isDialogOpen = true;
}
private async Task SaveProfile()
@@ -227,9 +241,9 @@ else
try
{
if (isEditMode && editingProfile != null)
if (isEditMode && selectedProfile != null)
{
await TaxProfileClient.UpdateAsync(editingProfile.Id, profileForm.BusinessType,
await TaxProfileClient.UpdateAsync(selectedProfile.Id, profileForm.BusinessType,
null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel);
Snackbar.Add("세무 프로필이 수정되었습니다.", Severity.Success);
}
@@ -245,7 +259,6 @@ else
profileForm.BusinessType);
if (newId > 0)
{
// 생성 후 상태 업데이트 처리
await TaxProfileClient.UpdateAsync(
newId,
profileForm.BusinessType,
@@ -255,7 +268,7 @@ else
Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success);
}
}
CloseDialog();
PrepareCreate();
await LoadData();
}
catch (Exception ex)
@@ -280,6 +293,10 @@ else
{
await TaxProfileClient.DeleteAsync(id);
Snackbar.Add("세무 프로필이 삭제되었습니다.", Severity.Success);
if (selectedProfile?.Id == id)
{
PrepareCreate();
}
await LoadData();
}
catch (Exception ex)
@@ -288,14 +305,6 @@ else
}
}
private void CloseDialog()
{
isDialogOpen = false;
isEditMode = false;
editingProfile = null;
profileForm = new();
}
private Color GetRiskColor(string riskLevel) => riskLevel switch
{
"high" => Color.Error,
@@ -310,6 +319,7 @@ else
: !string.IsNullOrWhiteSpace(client.Name)
? client.Name
: $"Client #{client.Id}";
private class TaxProfileForm
{
public int? ClientId { get; set; }
+11 -11
View File
@@ -90,14 +90,13 @@ test.describe('admin CRM pages', () => {
}
});
test('TaxProfiles modal dialog opens on add button click', async ({ page }) => {
test('TaxProfiles editor panel is visible on add button click', async ({ page }) => {
await navigateInBlazor(page, `${baseUrl}/admin/tax-profiles`);
const addButton = page.getByRole('button', { name: /새 프로필 추가/ });
await expect(addButton).toBeVisible();
await addButton.click();
await expect(page).toHaveURL(/\/taxbaik\/admin\/tax-profiles$/);
await expect(addButton).toBeVisible();
await expect(page.locator('.admin-editor-panel')).toBeVisible({ timeout: 5000 });
});
test('No console errors on CRM page navigation', async ({ page }) => {
@@ -131,11 +130,11 @@ test.describe('admin CRM pages', () => {
const addButton = page.getByRole('button', { name: /새 프로필 추가/ });
await addButton.click();
// 대화상자(MudDialog) 자체의 노출 대기
await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 });
// 분할 편집기(admin-editor-panel) 노출 대기
await expect(page.locator('.admin-editor-panel')).toBeVisible({ timeout: 5000 });
// mud-select 내의 input 클릭 (이벤트 핸들러 격발 유도)
const select = page.locator('.mud-dialog .mud-select').filter({ hasText: '사업 유형' }).first();
const select = page.locator('.admin-editor-panel .mud-select').filter({ hasText: '사업 유형' }).first();
await page.waitForTimeout(500);
await select.locator('input').click();
@@ -153,9 +152,9 @@ test.describe('admin CRM pages', () => {
const addButton = page.getByRole('button', { name: /새 일정 추가/ });
await addButton.click();
await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 });
await expect(page.locator('.admin-editor-panel')).toBeVisible({ timeout: 5000 });
const select = page.locator('.mud-dialog .mud-select').filter({ hasText: '신고 유형' }).first();
const select = page.locator('.admin-editor-panel .mud-select').filter({ hasText: '신고 유형' }).first();
await page.waitForTimeout(500);
await select.locator('input').click();
@@ -171,9 +170,9 @@ test.describe('admin CRM pages', () => {
const addButton = page.getByRole('button', { name: /새 계약 추가/ });
await addButton.click();
await expect(page.locator('.mud-dialog')).toBeVisible({ timeout: 5000 });
await expect(page.locator('.admin-editor-panel')).toBeVisible({ timeout: 5000 });
const select = page.locator('.mud-dialog .mud-select').filter({ hasText: '서비스 유형' }).first();
const select = page.locator('.admin-editor-panel .mud-select').filter({ hasText: '서비스 유형' }).first();
await page.waitForTimeout(500);
await select.locator('input').click();
@@ -181,4 +180,5 @@ test.describe('admin CRM pages', () => {
await expect(popover.getByText('개인 기장대리')).toBeVisible({ timeout: 5000 });
await expect(popover.getByText('법인 기장대리')).toBeVisible({ timeout: 5000 });
});
});
}
);