From d2cfcd90f0abece9269156463e4f62706aba7108 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 28 Jun 2026 18:39:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=ED=91=9C=EC=A4=80=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=8C=A8=ED=84=B4=EC=9C=BC=EB=A1=9C=20CRM=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 213 ++++++++++++++++++ .../Admin/Pages/ConsultingActivities.razor | 55 ++--- .../Components/Admin/Pages/Contracts.razor | 69 +++--- .../Admin/Pages/RevenueTrackings.razor | 54 ++--- .../Admin/Pages/TaxFilingSchedules.razor | 142 ++++++------ .../Components/Admin/Pages/TaxProfiles.razor | 195 ++++++++-------- TaxBaik.Web/Pages/_Layout.cshtml | 1 + 7 files changed, 453 insertions(+), 276 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9086c0f..2135e78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1093,6 +1093,219 @@ Admin 로그인 페이지만 [AllowAnonymous]: - **메모이제이션**: `OnParametersSet` vs `OnInitializedAsync` 구분 - **API 캐싱**: 변경이 없으면 `IMemoryCache` 사용 (5분 TTL) +### 8.7 Blazor 페이지 추가 표준 가이드 ✅ (2026-06-28 갱신) + +**목표**: 모든 관리자 페이지가 일관된 구조와 UX를 유지하도록 강제 + +#### 필수 구조 (기존 Dashboard 패턴 준수) + +**Step 1: 페이지 헤더 (`
`)** +```razor +@page "/admin/새페이지" +@attribute [Authorize] +@inject INewPageClient NewPageClient +@inject NavigationManager Nav + +페이지 제목 + + +
+
+ 카테고리 + 페이지 제목 + 한 줄 설명 +
+ + 새 항목 추가 + +
+``` + +**Step 2: 콘텐츠 영역** +```razor + +@if (items == null) +{ + +} + +else if (items.Count == 0) +{ + 데이터가 없습니다. +} + +else +{ + + + + + +} +``` + +**Step 3: 모달 다이얼로그 (Create/Edit)** +```razor + + + @(isEditMode ? "항목 수정" : "새 항목 추가") + + + + + + + + 취소 + 저장 + + +``` + +**Step 4: @code 섹션 구조** +```csharp +@code { + private List? items; + private List relatedItems = []; + private Dictionary 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("", 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 주입 +- [ ] 추가 +- [ ]
(캡션, 제목, 부제, 추가 버튼) +- [ ] 로딩 상태 (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 diff --git a/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor b/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor index 786e962..5275c77 100644 --- a/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor +++ b/TaxBaik.Web/Components/Admin/Pages/ConsultingActivities.razor @@ -8,21 +8,28 @@ 상담 활동 관리 -
-
- 상담 활동 관리 - - 새 활동 기록 - +
+
+ CRM & 세무관리 + 상담 활동 관리 + 고객별 상담 이력과 팔로업을 추적합니다.
+ + 새 활동 기록 + +
- @if (activities == null) + + @if (activities is null) { - + } else if (activities.Count == 0) { - 상담 활동이 없습니다. +
+ + 상담 활동이 없습니다. +
} else { @@ -33,7 +40,7 @@ Striped="true" Virtualize="true" RowsPerPage="30" - Class="admin-grid mt-4"> + Class="admin-grid"> @@ -82,9 +89,8 @@ } -
+ - @(editingActivity == null ? "새 활동 기록" : "활동 기록 수정") @@ -201,9 +207,11 @@ private async Task DeleteActivity(int id) { - var parameters = new DialogParameters(); - parameters.Add("Title", "삭제 확인"); - parameters.Add("Message", "이 활동을 삭제하시겠습니까?"); + var parameters = new DialogParameters + { + { "Title", "삭제 확인" }, + { "Message", "이 활동을 삭제하시겠습니까?" } + }; var dialog = await DialogService.ShowAsync("", parameters); var result = await dialog.Result; @@ -239,20 +247,3 @@ public DateTime? NextFollowupDate { get; set; } } } - - diff --git a/TaxBaik.Web/Components/Admin/Pages/Contracts.razor b/TaxBaik.Web/Components/Admin/Pages/Contracts.razor index 7e605a0..9ae943b 100644 --- a/TaxBaik.Web/Components/Admin/Pages/Contracts.razor +++ b/TaxBaik.Web/Components/Admin/Pages/Contracts.razor @@ -8,29 +8,35 @@ 계약 관리 -
-
-
- 계약 관리 - @if (mrr > 0) - { - - 월 정기수익: ₩@mrr.ToString("N0") - - } -
- - 새 계약 추가 - +
+
+ CRM & 세무관리 + 계약 관리 + 고객 계약과 월 정기수익을 함께 관리합니다. + @if (mrr > 0) + { + + 월 정기수익: + ₩@mrr.ToString("N0") + + }
+ + 새 계약 추가 + +
- @if (contracts == null) + + @if (contracts is null) { - + } else if (contracts.Count == 0) { - 계약이 없습니다. +
+ + 계약이 없습니다. +
} else { @@ -41,7 +47,7 @@ Striped="true" Virtualize="true" RowsPerPage="30" - Class="admin-grid mt-4"> + Class="admin-grid"> @@ -92,7 +98,7 @@ } -
+ @@ -181,9 +187,11 @@ private async Task DeleteContract(int id) { - var parameters = new DialogParameters(); - parameters.Add("Title", "삭제 확인"); - parameters.Add("Message", "이 계약을 삭제하시겠습니까?"); + var parameters = new DialogParameters + { + { "Title", "삭제 확인" }, + { "Message", "이 계약을 삭제하시겠습니까?" } + }; var dialog = await DialogService.ShowAsync("", parameters); var result = await dialog.Result; @@ -218,20 +226,3 @@ public decimal? MonthlyFee { get; set; } } } - - diff --git a/TaxBaik.Web/Components/Admin/Pages/RevenueTrackings.razor b/TaxBaik.Web/Components/Admin/Pages/RevenueTrackings.razor index c1410cf..c01ec1d 100644 --- a/TaxBaik.Web/Components/Admin/Pages/RevenueTrackings.razor +++ b/TaxBaik.Web/Components/Admin/Pages/RevenueTrackings.razor @@ -8,21 +8,28 @@ 수익 추적 관리 -
-
- 수익 추적 관리 - - 새 청구 추가 - +
+
+ CRM & 세무관리 + 수익 추적 관리 + 청구, 납부, 미수금 상태를 한 화면에서 관리합니다.
+ + 새 청구 추가 + +
- @if (revenues == null) + + @if (revenues is null) { - + } else if (revenues.Count == 0) { - 청구 기록이 없습니다. +
+ + 청구 기록이 없습니다. +
} else { @@ -33,7 +40,7 @@ Striped="true" Virtualize="true" RowsPerPage="30" - Class="admin-grid mt-4"> + Class="admin-grid"> @@ -77,7 +84,7 @@ } -
+ @@ -180,9 +187,11 @@ private async Task DeleteRevenue(int id) { - var parameters = new DialogParameters(); - parameters.Add("Title", "삭제 확인"); - parameters.Add("Message", "이 청구를 삭제하시겠습니까?"); + var parameters = new DialogParameters + { + { "Title", "삭제 확인" }, + { "Message", "이 청구를 삭제하시겠습니까?" } + }; var dialog = await DialogService.ShowAsync("", parameters); var result = await dialog.Result; @@ -218,20 +227,3 @@ public DateTime? DueDate { get; set; } } } - - diff --git a/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor b/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor index 886686d..4c15f3b 100644 --- a/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor +++ b/TaxBaik.Web/Components/Admin/Pages/TaxFilingSchedules.razor @@ -6,34 +6,44 @@ @inject IDialogService DialogService @attribute [Authorize] -신고 일정 관리 +신고 일정 -
-
- 신고 일정 관리 - - 새 일정 추가 - +
+
+ CRM & 세무관리 + 신고 일정 + 고객별 마감일과 처리 상태를 한 화면에서 관리합니다.
+ + 새 일정 추가 + +
- @if (schedules == null) + + @if (schedules is null) { - + } else if (schedules.Count == 0) { - 신고 일정이 없습니다. +
+ + 신고 일정이 없습니다. +
} else { + Items="@schedules" + Dense="true" + Hover="true" + Striped="true" + Virtualize="true" + RowsPerPage="30" + Class="admin-grid"> @@ -55,8 +65,14 @@ } @context.Item.DueDate.ToString("yyyy-MM-dd") - @if (daysLeft >= 0) { (D-@daysLeft) } - else { (마감@(Math.Abs(daysLeft))일경과) } + @if (daysLeft >= 0) + { + (D-@daysLeft) + } + else + { + (마감 @Math.Abs(daysLeft)일 경과) + } @@ -78,27 +94,36 @@ @if (context.Item.Status != "completed") { - + } - + } -
+ - - @(editingSchedule == null ? "새 신고 일정 추가" : "신고 일정 수정") + 새 신고 일정 추가 - + @foreach (var client in clients) { @client.CompanyName @@ -121,13 +146,9 @@ private Dictionary clientMap = new(); private MudForm? form; private bool isDialogOpen; - private TaxFilingSchedule? editingSchedule; private TaxFilingScheduleForm scheduleForm = new(); - protected override async Task OnInitializedAsync() - { - await LoadData(); - } + protected override async Task OnInitializedAsync() => await LoadData(); private async Task LoadData() { @@ -146,8 +167,7 @@ private void OpenCreateDialog() { - editingSchedule = null; - scheduleForm = new(); + scheduleForm = new TaxFilingScheduleForm { FilingYear = DateTime.Now.Year }; isDialogOpen = true; } @@ -155,20 +175,21 @@ { try { - if (editingSchedule == null) - { - var newId = await TaxFilingClient.CreateAsync( - scheduleForm.ClientId, - scheduleForm.FilingType, - scheduleForm.DueDate ?? DateTime.Now, - scheduleForm.FilingYear); + var newId = await TaxFilingClient.CreateAsync( + scheduleForm.ClientId, + scheduleForm.FilingType, + scheduleForm.DueDate ?? DateTime.Today, + scheduleForm.FilingYear); - if (newId > 0) - { - Snackbar.Add("신고 일정이 추가되었습니다.", Severity.Success); - CloseDialog(); - await LoadData(); - } + if (newId > 0) + { + Snackbar.Add("신고 일정이 추가되었습니다.", Severity.Success); + CloseDialog(); + await LoadData(); + } + else + { + Snackbar.Add("등록에 실패했습니다.", Severity.Error); } } catch (Exception ex) @@ -193,13 +214,14 @@ private async Task DeleteSchedule(int id) { - var parameters = new DialogParameters(); - parameters.Add("Title", "삭제 확인"); - parameters.Add("Message", "이 신고 일정을 삭제하시겠습니까?"); + var parameters = new DialogParameters + { + { "Title", "삭제 확인" }, + { "Message", "이 신고 일정을 삭제하시겠습니까?" } + }; var dialog = await DialogService.ShowAsync("", parameters); var result = await dialog.Result; - if (result?.Canceled ?? true) return; @@ -218,7 +240,6 @@ private void CloseDialog() { isDialogOpen = false; - editingSchedule = null; scheduleForm = new(); } @@ -230,20 +251,3 @@ public int FilingYear { get; set; } = DateTime.Now.Year; } } - - diff --git a/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor b/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor index b85a27a..9b618c3 100644 --- a/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor +++ b/TaxBaik.Web/Components/Admin/Pages/TaxProfiles.razor @@ -6,79 +6,81 @@ @inject IDialogService DialogService @attribute [Authorize] -세무 프로필 관리 +세무 프로필 -
-
- 세무 프로필 관리 - - 새 프로필 추가 - +
+
+ CRM & 세무관리 + 세무 프로필 + 고객별 세무 프로필, 신고 일정, 위험도 추적
+ + 새 프로필 추가 + +
- @if (profiles == null) - { - - } - else if (profiles.Count == 0) - { - 세무 프로필이 없습니다. - } - else - { - - - - - - @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName)) - { - - @clientName - - } - - - - - - - @context.Item.TaxRiskLevel - - - - - - @if (context.Item.NextFilingDueDate.HasValue) - { - @context.Item.NextFilingDueDate.Value.ToString("yyyy-MM-dd") - } - - - - - - - - - - - - - } -
+@if (profiles == null) +{ + +} +else if (profiles.Count == 0) +{ + 세무 프로필이 없습니다. +} +else +{ + + + + + + @if (clientMap.TryGetValue(context.Item.ClientId, out var clientName)) + { + + @clientName + + } + + + + + + + @context.Item.TaxRiskLevel + + + + + + @if (context.Item.NextFilingDueDate.HasValue) + { + @context.Item.NextFilingDueDate.Value.ToString("yyyy-MM-dd") + } + + + + + + + + + + + + +} - @(editingProfile == null ? "새 프로필 추가" : "프로필 수정") + @(isEditMode ? "세무 프로필 수정" : "새 세무 프로필 추가") @@ -110,6 +112,7 @@ private Dictionary clientMap = new(); private MudForm? form; private bool isDialogOpen; + private bool isEditMode; private TaxProfile? editingProfile; private TaxProfileForm profileForm = new(); @@ -135,6 +138,7 @@ private void OpenCreateDialog() { + isEditMode = false; editingProfile = null; profileForm = new(); isDialogOpen = true; @@ -142,6 +146,7 @@ private async Task OpenEditDialog(TaxProfile profile) { + isEditMode = true; editingProfile = profile; profileForm = new TaxProfileForm { @@ -158,32 +163,28 @@ { try { - if (editingProfile == null) - { - var newId = await TaxProfileClient.CreateAsync( - profileForm.ClientId, - profileForm.BusinessType); - - if (newId > 0) - { - Snackbar.Add("프로필이 생성되었습니다.", Severity.Success); - CloseDialog(); - await LoadData(); - } - } - else + if (isEditMode) { await TaxProfileClient.UpdateAsync( - editingProfile.Id, + editingProfile!.Id, profileForm.BusinessType, null, profileForm.NextFilingDueDate, profileForm.TaxRiskLevel); - - Snackbar.Add("프로필이 업데이트되었습니다.", Severity.Success); - CloseDialog(); - await LoadData(); + Snackbar.Add("세무 프로필이 업데이트되었습니다.", Severity.Success); } + else + { + var newId = await TaxProfileClient.CreateAsync( + profileForm.ClientId, + profileForm.BusinessType); + if (newId > 0) + { + Snackbar.Add("세무 프로필이 추가되었습니다.", Severity.Success); + } + } + CloseDialog(); + await LoadData(); } catch (Exception ex) { @@ -195,7 +196,7 @@ { var parameters = new DialogParameters(); parameters.Add("Title", "삭제 확인"); - parameters.Add("Message", "이 프로필을 삭제하시겠습니까?"); + parameters.Add("Message", "이 세무 프로필을 삭제하시겠습니까?"); var dialog = await DialogService.ShowAsync("", parameters); var result = await dialog.Result; @@ -206,7 +207,7 @@ try { await TaxProfileClient.DeleteAsync(id); - Snackbar.Add("프로필이 삭제되었습니다.", Severity.Success); + Snackbar.Add("세무 프로필이 삭제되었습니다.", Severity.Success); await LoadData(); } catch (Exception ex) @@ -218,11 +219,12 @@ private void CloseDialog() { isDialogOpen = false; + isEditMode = false; editingProfile = null; profileForm = new(); } - private Color GetRiskColor(string level) => level switch + private Color GetRiskColor(string riskLevel) => riskLevel switch { "high" => Color.Error, "normal" => Color.Warning, @@ -239,20 +241,3 @@ public string? SpecialNotes { get; set; } } } - - diff --git a/TaxBaik.Web/Pages/_Layout.cshtml b/TaxBaik.Web/Pages/_Layout.cshtml index 72562ca..021f4d4 100644 --- a/TaxBaik.Web/Pages/_Layout.cshtml +++ b/TaxBaik.Web/Pages/_Layout.cshtml @@ -54,6 +54,7 @@

© 2026 백원숙 세무회계. All rights reserved.

개인정보처리방침 이용약관 + 고객 포털 @if (Context.RequestServices.GetService(typeof(VersionInfo)) is VersionInfo version) {