feat(admin): 표준 화면 패턴으로 CRM 화면 정리

This commit is contained in:
2026-06-28 18:39:28 +09:00
parent 42e73fa694
commit d2cfcd90f0
7 changed files with 453 additions and 276 deletions
+213
View File
@@ -1093,6 +1093,219 @@ Admin 로그인 페이지만 [AllowAnonymous]:
- **메모이제이션**: `OnParametersSet` vs `OnInitializedAsync` 구분 - **메모이제이션**: `OnParametersSet` vs `OnInitializedAsync` 구분
- **API 캐싱**: 변경이 없으면 `IMemoryCache` 사용 (5분 TTL) - **API 캐싱**: 변경이 없으면 `IMemoryCache` 사용 (5분 TTL)
### 8.7 Blazor 페이지 추가 표준 가이드 ✅ (2026-06-28 갱신)
**목표**: 모든 관리자 페이지가 일관된 구조와 UX를 유지하도록 강제
#### 필수 구조 (기존 Dashboard 패턴 준수)
**Step 1: 페이지 헤더 (`<section class="admin-page-hero">`)**
```razor
@page "/admin/새페이지"
@attribute [Authorize]
@inject INewPageClient NewPageClient
@inject NavigationManager Nav
<PageTitle>페이지 제목</PageTitle>
<!-- 반드시 포함할 요소 -->
<section class="admin-page-hero">
<div>
<MudText Typo="Typo.caption" Class="admin-eyebrow">카테고리</MudText>
<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" StartIcon="@Icons.Material.Filled.Add" OnClick="OpenCreateDialog">
새 항목 추가
</MudButton>
</section>
```
**Step 2: 콘텐츠 영역**
```razor
<!-- 로딩 상태 -->
@if (items == null)
{
<MudProgressCircular Indeterminate="true" Class="mt-4" />
}
<!-- 빈 상태 -->
else if (items.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="mt-4">데이터가 없습니다.</MudAlert>
}
<!-- 데이터 그리드 -->
else
{
<MudDataGrid T="YourEntity"
Items="@items"
Dense="true"
Hover="true"
Striped="true"
Virtualize="true"
RowsPerPage="30"
Class="admin-grid mt-4">
<Columns>
<!-- 필수: 컬럼 정의 -->
</Columns>
</MudDataGrid>
}
```
**Step 3: 모달 다이얼로그 (Create/Edit)**
```razor
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent>
<MudText Typo="Typo.h6">@(isEditMode ? "항목 수정" : "새 항목 추가")</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="form">
<!-- 폼 필드 -->
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDialog">취소</MudButton>
<MudButton Color="Color.Primary" OnClick="SaveItem">저장</MudButton>
</DialogActions>
</MudDialog>
```
**Step 4: @code 섹션 구조**
```csharp
@code {
private List<YourEntity>? items;
private List<RelatedEntity> relatedItems = [];
private Dictionary<int, string> itemMap = new();
private MudForm? form;
private bool isDialogOpen;
private bool isEditMode;
private YourEntity? editingItem;
private YourItemForm itemForm = new();
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
try
{
items = await YourItemClient.GetAllAsync();
// 필요시 관련 데이터 로드
}
catch (Exception ex)
{
Snackbar.Add($"데이터 로드 실패: {ex.Message}", Severity.Error);
}
}
private void OpenCreateDialog()
{
isEditMode = false;
editingItem = null;
itemForm = new();
isDialogOpen = true;
}
private async Task OpenEditDialog(YourEntity item)
{
isEditMode = true;
editingItem = item;
itemForm = new YourItemForm { /* 초기화 */ };
isDialogOpen = true;
}
private async Task SaveItem()
{
try
{
if (isEditMode)
{
await YourItemClient.UpdateAsync(editingItem!.Id, /* params */);
Snackbar.Add("항목이 업데이트되었습니다.", Severity.Success);
}
else
{
var newId = await YourItemClient.CreateAsync(/* params */);
if (newId > 0)
{
Snackbar.Add("항목이 추가되었습니다.", Severity.Success);
}
}
CloseDialog();
await LoadData();
}
catch (Exception ex)
{
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
}
}
private async Task DeleteItem(int id)
{
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 YourItemClient.DeleteAsync(id);
Snackbar.Add("항목이 삭제되었습니다.", Severity.Success);
await LoadData();
}
catch (Exception ex)
{
Snackbar.Add($"삭제 실패: {ex.Message}", Severity.Error);
}
}
private void CloseDialog()
{
isDialogOpen = false;
isEditMode = false;
editingItem = null;
itemForm = new();
}
private class YourItemForm
{
// DTO 필드
}
}
```
#### 체크리스트 (모든 페이지)
- [ ] @page 지시문 확인
- [ ] @attribute [Authorize] 추가
- [ ] @inject로 필요한 Client 주입
- [ ] <PageTitle> 추가
- [ ] <section class="admin-page-hero"> (캡션, 제목, 부제, 추가 버튼)
- [ ] 로딩 상태 (MudProgressCircular)
- [ ] 빈 상태 (MudAlert)
- [ ] MudDataGrid (Dense=true, Virtualize=true, RowsPerPage=30, admin-grid 클래스)
- [ ] MudDialog (Create/Edit 모달)
- [ ] ConfirmDialog (Delete 확인)
- [ ] @code 섹션: OnInitializedAsync → LoadData() 패턴
- [ ] 모든 에러 처리 (try-catch, Snackbar 메시지)
- [ ] CloseDialog() 메서드로 모달 상태 초기화
#### 위반 사항
❌ **이 패턴을 따르지 않는 페이지는 실시간 코드 리뷰 대상:**
- 페이지 헤더 (admin-page-hero) 누락
- 인라인 스타일로 레이아웃 구성
- MudDialog 없이 별도 라우트로 Create/Edit 처리 (흰 화면 플래시)
- @code 섹션 구조 다름
- 모달에서 직접 onSubmit 대신 Snackbar 피드백 미제공
--- ---
## 9. Do's & Don'ts ## 9. Do's & Don'ts
@@ -8,21 +8,28 @@
<PageTitle>상담 활동 관리</PageTitle> <PageTitle>상담 활동 관리</PageTitle>
<div class="admin-container"> <section class="admin-page-hero">
<div class="admin-header"> <div>
<MudText Typo="Typo.h5" Class="font-weight-bold">상담 활동 관리</MudText> <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>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add"> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
새 활동 기록 새 활동 기록
</MudButton> </MudButton>
</div> </section>
@if (activities == null) <MudPaper Class="admin-surface" Elevation="0">
@if (activities is null)
{ {
<MudProgressCircular Indeterminate="true" Class="mt-4" /> <MudProgressLinear Indeterminate="true" />
} }
else if (activities.Count == 0) else if (activities.Count == 0)
{ {
<MudAlert Severity="Severity.Info" Class="mt-4">상담 활동이 없습니다.</MudAlert> <div class="pa-6 text-center">
<MudIcon Icon="@Icons.Material.Filled.Timeline" Style="font-size:3rem; opacity:.3;" />
<MudText Class="mt-2 text-muted">상담 활동이 없습니다.</MudText>
</div>
} }
else else
{ {
@@ -33,7 +40,7 @@
Striped="true" Striped="true"
Virtualize="true" Virtualize="true"
RowsPerPage="30" RowsPerPage="30"
Class="admin-grid mt-4"> Class="admin-grid">
<Columns> <Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" /> <PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
<TemplateColumn Title="고객"> <TemplateColumn Title="고객">
@@ -82,9 +89,8 @@
</Columns> </Columns>
</MudDataGrid> </MudDataGrid>
} }
</div> </MudPaper>
<!-- Create/Edit Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }"> <MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent> <TitleContent>
<MudText Typo="Typo.h6">@(editingActivity == null ? "새 활동 기록" : "활동 기록 수정")</MudText> <MudText Typo="Typo.h6">@(editingActivity == null ? "새 활동 기록" : "활동 기록 수정")</MudText>
@@ -201,9 +207,11 @@
private async Task DeleteActivity(int id) private async Task DeleteActivity(int id)
{ {
var parameters = new DialogParameters(); var parameters = new DialogParameters
parameters.Add("Title", "삭제 확인"); {
parameters.Add("Message", "이 활동을 삭제하시겠습니까?"); { "Title", "삭제 확인" },
{ "Message", "이 활동을 삭제하시겠습니까?" }
};
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters); var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
var result = await dialog.Result; var result = await dialog.Result;
@@ -239,20 +247,3 @@
public DateTime? NextFollowupDate { get; set; } public DateTime? NextFollowupDate { get; set; }
} }
} }
<style>
.admin-container {
padding: 20px;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.admin-grid {
font-size: 13px;
}
</style>
@@ -8,29 +8,35 @@
<PageTitle>계약 관리</PageTitle> <PageTitle>계약 관리</PageTitle>
<div class="admin-container"> <section class="admin-page-hero">
<div class="admin-header">
<div> <div>
<MudText Typo="Typo.h5" Class="font-weight-bold">계약 관리</MudText> <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>
@if (mrr > 0) @if (mrr > 0)
{ {
<MudText Typo="Typo.body2" Class="mt-2"> <MudText Typo="Typo.body2" Class="mt-2">
월 정기수익: <MudChip Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">₩@mrr.ToString("N0")</MudChip> 월 정기수익:
<MudChip Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">₩@mrr.ToString("N0")</MudChip>
</MudText> </MudText>
} }
</div> </div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add"> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
새 계약 추가 새 계약 추가
</MudButton> </MudButton>
</div> </section>
@if (contracts == null) <MudPaper Class="admin-surface" Elevation="0">
@if (contracts is null)
{ {
<MudProgressCircular Indeterminate="true" Class="mt-4" /> <MudProgressLinear Indeterminate="true" />
} }
else if (contracts.Count == 0) else if (contracts.Count == 0)
{ {
<MudAlert Severity="Severity.Info" Class="mt-4">계약이 없습니다.</MudAlert> <div class="pa-6 text-center">
<MudIcon Icon="@Icons.Material.Filled.Description" Style="font-size:3rem; opacity:.3;" />
<MudText Class="mt-2 text-muted">계약이 없습니다.</MudText>
</div>
} }
else else
{ {
@@ -41,7 +47,7 @@
Striped="true" Striped="true"
Virtualize="true" Virtualize="true"
RowsPerPage="30" RowsPerPage="30"
Class="admin-grid mt-4"> Class="admin-grid">
<Columns> <Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" /> <PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
<TemplateColumn Title="고객"> <TemplateColumn Title="고객">
@@ -92,7 +98,7 @@
</Columns> </Columns>
</MudDataGrid> </MudDataGrid>
} }
</div> </MudPaper>
<!-- Create Dialog --> <!-- Create Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }"> <MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
@@ -181,9 +187,11 @@
private async Task DeleteContract(int id) private async Task DeleteContract(int id)
{ {
var parameters = new DialogParameters(); var parameters = new DialogParameters
parameters.Add("Title", "삭제 확인"); {
parameters.Add("Message", "이 계약을 삭제하시겠습니까?"); { "Title", "삭제 확인" },
{ "Message", "이 계약을 삭제하시겠습니까?" }
};
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters); var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
var result = await dialog.Result; var result = await dialog.Result;
@@ -218,20 +226,3 @@
public decimal? MonthlyFee { get; set; } public decimal? MonthlyFee { get; set; }
} }
} }
<style>
.admin-container {
padding: 20px;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.admin-grid {
font-size: 13px;
}
</style>
@@ -8,21 +8,28 @@
<PageTitle>수익 추적 관리</PageTitle> <PageTitle>수익 추적 관리</PageTitle>
<div class="admin-container"> <section class="admin-page-hero">
<div class="admin-header"> <div>
<MudText Typo="Typo.h5" Class="font-weight-bold">수익 추적 관리</MudText> <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>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add"> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
새 청구 추가 새 청구 추가
</MudButton> </MudButton>
</div> </section>
@if (revenues == null) <MudPaper Class="admin-surface" Elevation="0">
@if (revenues is null)
{ {
<MudProgressCircular Indeterminate="true" Class="mt-4" /> <MudProgressLinear Indeterminate="true" />
} }
else if (revenues.Count == 0) else if (revenues.Count == 0)
{ {
<MudAlert Severity="Severity.Info" Class="mt-4">청구 기록이 없습니다.</MudAlert> <div class="pa-6 text-center">
<MudIcon Icon="@Icons.Material.Filled.Payments" Style="font-size:3rem; opacity:.3;" />
<MudText Class="mt-2 text-muted">청구 기록이 없습니다.</MudText>
</div>
} }
else else
{ {
@@ -33,7 +40,7 @@
Striped="true" Striped="true"
Virtualize="true" Virtualize="true"
RowsPerPage="30" RowsPerPage="30"
Class="admin-grid mt-4"> Class="admin-grid">
<Columns> <Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" /> <PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
<TemplateColumn Title="고객"> <TemplateColumn Title="고객">
@@ -77,7 +84,7 @@
</Columns> </Columns>
</MudDataGrid> </MudDataGrid>
} }
</div> </MudPaper>
<!-- Create Dialog --> <!-- Create Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }"> <MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
@@ -180,9 +187,11 @@
private async Task DeleteRevenue(int id) private async Task DeleteRevenue(int id)
{ {
var parameters = new DialogParameters(); var parameters = new DialogParameters
parameters.Add("Title", "삭제 확인"); {
parameters.Add("Message", "이 청구를 삭제하시겠습니까?"); { "Title", "삭제 확인" },
{ "Message", "이 청구를 삭제하시겠습니까?" }
};
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters); var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
var result = await dialog.Result; var result = await dialog.Result;
@@ -218,20 +227,3 @@
public DateTime? DueDate { get; set; } public DateTime? DueDate { get; set; }
} }
} }
<style>
.admin-container {
padding: 20px;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.admin-grid {
font-size: 13px;
}
</style>
@@ -6,23 +6,33 @@
@inject IDialogService DialogService @inject IDialogService DialogService
@attribute [Authorize] @attribute [Authorize]
<PageTitle>신고 일정 관리</PageTitle> <PageTitle>신고 일정</PageTitle>
<div class="admin-container"> <section class="admin-page-hero">
<div class="admin-header"> <div>
<MudText Typo="Typo.h5" Class="font-weight-bold">신고 일정 관리</MudText> <MudText Typo="Typo.caption" Class="admin-eyebrow">CRM & 세무관리</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add"> <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> </MudButton>
</div> </section>
@if (schedules == null) <MudPaper Class="admin-surface" Elevation="0">
@if (schedules is null)
{ {
<MudProgressCircular Indeterminate="true" Class="mt-4" /> <MudProgressLinear Indeterminate="true" />
} }
else if (schedules.Count == 0) else if (schedules.Count == 0)
{ {
<MudAlert Severity="Severity.Info" Class="mt-4">신고 일정이 없습니다.</MudAlert> <div class="pa-6 text-center">
<MudIcon Icon="@Icons.Material.Filled.EventBusy" Style="font-size:3rem; opacity:.3;" />
<MudText Class="mt-2 text-muted">신고 일정이 없습니다.</MudText>
</div>
} }
else else
{ {
@@ -33,7 +43,7 @@
Striped="true" Striped="true"
Virtualize="true" Virtualize="true"
RowsPerPage="30" RowsPerPage="30"
Class="admin-grid mt-4"> Class="admin-grid">
<Columns> <Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" /> <PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
<TemplateColumn Title="고객"> <TemplateColumn Title="고객">
@@ -55,8 +65,14 @@
} }
<MudChip Size="Size.Small" Color="@statusColor" Variant="Variant.Filled"> <MudChip Size="Size.Small" Color="@statusColor" Variant="Variant.Filled">
@context.Item.DueDate.ToString("yyyy-MM-dd") @context.Item.DueDate.ToString("yyyy-MM-dd")
@if (daysLeft >= 0) { <span>(D-@daysLeft)</span> } @if (daysLeft >= 0)
else { <span>(마감@(Math.Abs(daysLeft))일경과)</span> } {
<span class="ms-1">(D-@daysLeft)</span>
}
else
{
<span class="ms-1">(마감 @Math.Abs(daysLeft)일 경과)</span>
}
</MudChip> </MudChip>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
@@ -78,27 +94,36 @@
<MudButtonGroup Size="Size.Small" Variant="Variant.Outlined"> <MudButtonGroup Size="Size.Small" Variant="Variant.Outlined">
@if (context.Item.Status != "completed") @if (context.Item.Status != "completed")
{ {
<MudIconButton Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" <MudIconButton Icon="@Icons.Material.Filled.CheckCircle"
OnClick="@(async () => await CompleteSchedule(context.Item.Id))" Title="완료" /> Color="Color.Success"
OnClick="@(async () => await CompleteSchedule(context.Item.Id))"
Title="완료" />
} }
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" <MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(async () => await DeleteSchedule(context.Item.Id))" /> Color="Color.Error"
OnClick="@(async () => await DeleteSchedule(context.Item.Id))"
Title="삭제" />
</MudButtonGroup> </MudButtonGroup>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
</Columns> </Columns>
</MudDataGrid> </MudDataGrid>
} }
</div> </MudPaper>
<!-- Create/Edit Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }"> <MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent> <TitleContent>
<MudText Typo="Typo.h6">@(editingSchedule == null ? "새 신고 일정 추가" : "신고 일정 수정")</MudText> <MudText Typo="Typo.h6">새 신고 일정 추가</MudText>
</TitleContent> </TitleContent>
<DialogContent> <DialogContent>
<MudForm @ref="form"> <MudForm @ref="form">
<MudSelect T="int" @bind-Value="scheduleForm.ClientId" Label="고객" Required="true" Variant="Variant.Outlined" FullWidth="true" Class="mb-4"> <MudSelect T="int"
@bind-Value="scheduleForm.ClientId"
Label="고객"
Required="true"
Variant="Variant.Outlined"
FullWidth="true"
Class="mb-4">
@foreach (var client in clients) @foreach (var client in clients)
{ {
<MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem> <MudSelectItem Value="@client.Id">@client.CompanyName</MudSelectItem>
@@ -121,13 +146,9 @@
private Dictionary<int, string> clientMap = new(); private Dictionary<int, string> clientMap = new();
private MudForm? form; private MudForm? form;
private bool isDialogOpen; private bool isDialogOpen;
private TaxFilingSchedule? editingSchedule;
private TaxFilingScheduleForm scheduleForm = new(); private TaxFilingScheduleForm scheduleForm = new();
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync() => await LoadData();
{
await LoadData();
}
private async Task LoadData() private async Task LoadData()
{ {
@@ -146,21 +167,18 @@
private void OpenCreateDialog() private void OpenCreateDialog()
{ {
editingSchedule = null; scheduleForm = new TaxFilingScheduleForm { FilingYear = DateTime.Now.Year };
scheduleForm = new();
isDialogOpen = true; isDialogOpen = true;
} }
private async Task SaveSchedule() private async Task SaveSchedule()
{ {
try try
{
if (editingSchedule == null)
{ {
var newId = await TaxFilingClient.CreateAsync( var newId = await TaxFilingClient.CreateAsync(
scheduleForm.ClientId, scheduleForm.ClientId,
scheduleForm.FilingType, scheduleForm.FilingType,
scheduleForm.DueDate ?? DateTime.Now, scheduleForm.DueDate ?? DateTime.Today,
scheduleForm.FilingYear); scheduleForm.FilingYear);
if (newId > 0) if (newId > 0)
@@ -169,6 +187,9 @@
CloseDialog(); CloseDialog();
await LoadData(); await LoadData();
} }
else
{
Snackbar.Add("등록에 실패했습니다.", Severity.Error);
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -193,13 +214,14 @@
private async Task DeleteSchedule(int id) private async Task DeleteSchedule(int id)
{ {
var parameters = new DialogParameters(); var parameters = new DialogParameters
parameters.Add("Title", "삭제 확인"); {
parameters.Add("Message", "이 신고 일정을 삭제하시겠습니까?"); { "Title", "삭제 확인" },
{ "Message", "이 신고 일정을 삭제하시겠습니까?" }
};
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters); var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
var result = await dialog.Result; var result = await dialog.Result;
if (result?.Canceled ?? true) if (result?.Canceled ?? true)
return; return;
@@ -218,7 +240,6 @@
private void CloseDialog() private void CloseDialog()
{ {
isDialogOpen = false; isDialogOpen = false;
editingSchedule = null;
scheduleForm = new(); scheduleForm = new();
} }
@@ -230,20 +251,3 @@
public int FilingYear { get; set; } = DateTime.Now.Year; public int FilingYear { get; set; } = DateTime.Now.Year;
} }
} }
<style>
.admin-container {
padding: 20px;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.admin-grid {
font-size: 13px;
}
</style>
@@ -6,15 +6,18 @@
@inject IDialogService DialogService @inject IDialogService DialogService
@attribute [Authorize] @attribute [Authorize]
<PageTitle>세무 프로필 관리</PageTitle> <PageTitle>세무 프로필</PageTitle>
<div class="admin-container"> <section class="admin-page-hero">
<div class="admin-header"> <div>
<MudText Typo="Typo.h5" Class="font-weight-bold">세무 프로필 관리</MudText> <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>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add"> <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OpenCreateDialog" StartIcon="@Icons.Material.Filled.Add">
새 프로필 추가 새 프로필 추가
</MudButton> </MudButton>
</div> </section>
@if (profiles == null) @if (profiles == null)
{ {
@@ -73,12 +76,11 @@
</Columns> </Columns>
</MudDataGrid> </MudDataGrid>
} }
</div>
<!-- Create/Edit Dialog --> <!-- Create/Edit Dialog -->
<MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }"> <MudDialog @bind-IsVisible="isDialogOpen" Options="new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true }">
<TitleContent> <TitleContent>
<MudText Typo="Typo.h6">@(editingProfile == null ? " 프로필 추가" : "프로필 수정")</MudText> <MudText Typo="Typo.h6">@(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가")</MudText>
</TitleContent> </TitleContent>
<DialogContent> <DialogContent>
<MudForm @ref="form"> <MudForm @ref="form">
@@ -110,6 +112,7 @@
private Dictionary<int, string> clientMap = new(); private Dictionary<int, string> clientMap = new();
private MudForm? form; private MudForm? form;
private bool isDialogOpen; private bool isDialogOpen;
private bool isEditMode;
private TaxProfile? editingProfile; private TaxProfile? editingProfile;
private TaxProfileForm profileForm = new(); private TaxProfileForm profileForm = new();
@@ -135,6 +138,7 @@
private void OpenCreateDialog() private void OpenCreateDialog()
{ {
isEditMode = false;
editingProfile = null; editingProfile = null;
profileForm = new(); profileForm = new();
isDialogOpen = true; isDialogOpen = true;
@@ -142,6 +146,7 @@
private async Task OpenEditDialog(TaxProfile profile) private async Task OpenEditDialog(TaxProfile profile)
{ {
isEditMode = true;
editingProfile = profile; editingProfile = profile;
profileForm = new TaxProfileForm profileForm = new TaxProfileForm
{ {
@@ -158,33 +163,29 @@
{ {
try try
{ {
if (editingProfile == null) if (isEditMode)
{
var newId = await TaxProfileClient.CreateAsync(
profileForm.ClientId,
profileForm.BusinessType);
if (newId > 0)
{
Snackbar.Add("프로필이 생성되었습니다.", Severity.Success);
CloseDialog();
await LoadData();
}
}
else
{ {
await TaxProfileClient.UpdateAsync( await TaxProfileClient.UpdateAsync(
editingProfile.Id, editingProfile!.Id,
profileForm.BusinessType, profileForm.BusinessType,
null, null,
profileForm.NextFilingDueDate, profileForm.NextFilingDueDate,
profileForm.TaxRiskLevel); profileForm.TaxRiskLevel);
Snackbar.Add("세무 프로필이 업데이트되었습니다.", Severity.Success);
Snackbar.Add("프로필이 업데이트되었습니다.", Severity.Success); }
else
{
var newId = await TaxProfileClient.CreateAsync(
profileForm.ClientId,
profileForm.BusinessType);
if (newId > 0)
{
Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success);
}
}
CloseDialog(); CloseDialog();
await LoadData(); await LoadData();
} }
}
catch (Exception ex) catch (Exception ex)
{ {
Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error); Snackbar.Add($"저장 실패: {ex.Message}", Severity.Error);
@@ -195,7 +196,7 @@
{ {
var parameters = new DialogParameters(); var parameters = new DialogParameters();
parameters.Add("Title", "삭제 확인"); parameters.Add("Title", "삭제 확인");
parameters.Add("Message", "이 프로필을 삭제하시겠습니까?"); parameters.Add("Message", "이 세무 프로필을 삭제하시겠습니까?");
var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters); var dialog = await DialogService.ShowAsync<ConfirmDialog>("", parameters);
var result = await dialog.Result; var result = await dialog.Result;
@@ -206,7 +207,7 @@
try try
{ {
await TaxProfileClient.DeleteAsync(id); await TaxProfileClient.DeleteAsync(id);
Snackbar.Add("프로필이 삭제되었습니다.", Severity.Success); Snackbar.Add("세무 프로필이 삭제되었습니다.", Severity.Success);
await LoadData(); await LoadData();
} }
catch (Exception ex) catch (Exception ex)
@@ -218,11 +219,12 @@
private void CloseDialog() private void CloseDialog()
{ {
isDialogOpen = false; isDialogOpen = false;
isEditMode = false;
editingProfile = null; editingProfile = null;
profileForm = new(); profileForm = new();
} }
private Color GetRiskColor(string level) => level switch private Color GetRiskColor(string riskLevel) => riskLevel switch
{ {
"high" => Color.Error, "high" => Color.Error,
"normal" => Color.Warning, "normal" => Color.Warning,
@@ -239,20 +241,3 @@
public string? SpecialNotes { get; set; } public string? SpecialNotes { get; set; }
} }
} }
<style>
.admin-container {
padding: 20px;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.admin-grid {
font-size: 13px;
}
</style>
+1
View File
@@ -54,6 +54,7 @@
<p>© 2026 백원숙 세무회계. All rights reserved.</p> <p>© 2026 백원숙 세무회계. All rights reserved.</p>
<a href="/taxbaik/privacy" class="text-decoration-none text-muted me-2">개인정보처리방침</a> <a href="/taxbaik/privacy" class="text-decoration-none text-muted me-2">개인정보처리방침</a>
<a href="/taxbaik/terms" class="text-decoration-none text-muted">이용약관</a> <a href="/taxbaik/terms" class="text-decoration-none text-muted">이용약관</a>
<a href="/taxbaik/portal" class="text-decoration-none text-muted ms-2">고객 포털</a>
@if (Context.RequestServices.GetService(typeof(VersionInfo)) is VersionInfo version) @if (Context.RequestServices.GetService(typeof(VersionInfo)) is VersionInfo version)
{ {
<div class="mt-2 text-muted" style="font-size: 0.75rem; opacity: 0.6;"> <div class="mt-2 text-muted" style="font-size: 0.75rem; opacity: 0.6;">