feat(admin): 표준 화면 패턴으로 CRM 화면 정리
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user