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` 구분
- **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