docs: add admin grid UX (Dorsum ERP level) and deployment user experience protection guidelines
TaxBaik CI/CD / build-and-deploy (push) Successful in 57s

Admin Grid UX Enhancements (Section 8.6):
- High-density data display (32px row height, 5-7 column layout)
- Responsive design: PC(6) → Tablet(4) → Mobile(2) columns
- Pad-optimized (24px cells, 36px buttons for touch)
- Advanced interactions: inline editing, multi-select, context menu
- MudDataGrid implementation pattern with virtualization
- Status-based coloring (normal/warning/danger/success)
- Performance optimization (virtualization, lazy loading, caching)

Deployment User Experience Protection (Section 11.1):
- No forced refresh during deployment 
- Users receive notifications with manual refresh option 
- SignalR-based deployment notification (not server-sent events)
- Auto-save form data to sessionStorage
- Recovery options after refresh
- Deployment status API endpoint
- Admin-only deployment notification API

Core Principles:
- 사용자 작업 중 배포 시 강제 새로고침 금지
- 알림 + 수동 새로고침 옵션 제공
- 폼 데이터 자동 보존 및 복구 기능
This commit is contained in:
2026-06-28 17:03:21 +09:00
parent 59f1509368
commit c38b97377a
+289
View File
@@ -927,6 +927,134 @@ Admin 로그인 페이지만 [AllowAnonymous]:
- 페이지 로드 시 `OnInitializedAsync`에서 데이터 가져오기
- 업데이트는 `StateHasChanged()` 호출
### 8.6 어드민 그리드 UX (Dorsum ERP 수준)
**목표**: 패드/PC에 특화된 고밀도 데이터 표시 + ERP 수준의 상호작용성
#### 그리드 기본 원칙
- **데이터 밀도**: 줄 높이 32px, 최대 주요 정보 5-7개 컬럼 (시각적 혼잡 제거)
- **반응형**: PC(1920px) 6컬럼 → 태블릿(960px) 4컬럼 → 모바일(480px) 2컬럼
- **패드 특화**: 터치 친화적 (최소 24px 셀 높이, 36px 버튼)
- **PC 최적화**: 마우스 호버 선택행, 키보드 네비게이션 (Arrow/Enter/Esc)
#### 고급 인터랙션
- **인라인 편집**: 셀 더블클릭 → 편집 모드 (취소: Esc, 저장: Enter)
- **다중 선택**: Ctrl/Cmd + Click, Shift + Click로 범위 선택
- **컨텍스트 메뉴**: 우클릭 → 행 삭제, 복사, 내보내기
- **정렬/필터**: 컬럼 헤더 클릭 정렬, 필터 아이콘 필터링
- **페이징**: 하단 "1/10" 표시 + 이전/다음 버튼 (기본 20행/페이지)
- **검색**: 우상단 검색 박스 (실시간 필터링, 하이라이트 처리)
#### MudBlazor 적용 패턴
```razor
<MudDataGrid T="YourItem"
Dense="true"
Hover="true"
Striped="true"
RowsPerPage="20"
Virtualize="true"
@ref="dataGrid"
Items="items"
Sortable="true"
Filterable="true"
ShowMenuIcon="true">
<Columns>
<PropertyColumn Property="x => x.Id" Title="ID" Sortable="true" />
<PropertyColumn Property="x => x.Name" Title="이름" Filterable="true" />
<PropertyColumn Property="x => x.Amount" Title="금액" Sortable="true"
Format="C" />
<TemplateColumn Title="작업">
<CellTemplate>
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Small"
OnClick="@(() => Edit(context.Item))" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Small"
OnClick="@(() => Delete(context.Item))" />
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
```
#### 색상 & 상태 표시
- **정상** (Normal): 회색 배경
- **주의** (Warning): 주황색 배경 (TaxRiskLevel: "warning")
- **긴급** (Danger): 빨간색 배경 (TaxRiskLevel: "danger", 미납 송장)
- **완료** (Success): 녹색 배경 (완료된 신고, 결제됨)
```razor
<MudChip Color="@(item.TaxRiskLevel == "danger" ? Color.Error :
item.TaxRiskLevel == "warning" ? Color.Warning : Color.Default)">
@item.TaxRiskLevel
</MudChip>
```
#### 페이지 구조 (예: TaxProfile 관리)
```
┌─────────────────────────────────────────────┐
│ 세무프로필 관리 [+새로 추가] │
├─────────────────────────────────────────────┤
│ 🔍 검색... │
├──────┬────────┬────────┬────────┬────────┬──┤
│ 고객 │ 상태 │ 리스크 │ 다음신고│ 담당자 │작│
├──────┼────────┼────────┼────────┼────────┼──┤
│ (선택)고객A │ 활성 │ 🔴높음 │5/30 │ A │✎│
│ │ │ │ │ │✕│
│ (선택)고객B │ 활성 │ 🟡보통 │6/15 │ B │✎│
│ │ │ │ │ │✕│
├──────┴────────┴────────┴────────┴────────┴──┤
│ ◀ 1 2 3 4 ▶ | 20행/페이지 | 전체: 150개 │
└─────────────────────────────────────────────┘
```
#### CSS 클래스 표준
```css
/* admin-grid.css */
.admin-grid {
font-size: 13px;
line-height: 1.4;
}
.admin-grid--dense {
--mud-table-row-height: 32px;
}
.admin-grid__header {
background-color: #f5f5f5;
font-weight: 600;
padding: 8px;
}
.admin-grid__cell {
padding: 8px;
vertical-align: middle;
}
.admin-grid__cell--danger {
background-color: #ffebee;
}
.admin-grid__cell--warning {
background-color: #fff3e0;
}
.admin-grid__cell--success {
background-color: #e8f5e9;
}
.admin-grid__action-button {
padding: 4px 8px;
min-width: 36px;
min-height: 36px;
}
```
#### 성능 최적화
- **가상화**: `Virtualize="true"` (10,000행 이상 대응)
- **지연 로드**: IntersectionObserver로 스크롤 시 다음 페이지 로드
- **메모이제이션**: `OnParametersSet` vs `OnInitializedAsync` 구분
- **API 캐싱**: 변경이 없으면 `IMemoryCache` 사용 (5분 TTL)
---
## 9. Do's & Don'ts
@@ -1096,6 +1224,167 @@ npx playwright test # CI에서 배포 후 자동 실행
- ✅ 폼 필드 너비 (200px 이상)
- ✅ 수평 오버플로우 없음 (모든 크기)
### 배포 중 사용자 경험 보호
**문제**: 배포 중 사용자가 관리 페이지에서 작업 중이면 강제 새로고침이 발생하여 미저장 데이터 손실
**해결 방안**:
#### 1. 배포 알림 전략 (강제 새로고침 금지)
```csharp
// Program.cs - SignalR 배포 알림
app.MapHub<NotificationHub>("/taxbaik/hub/notifications");
// NotificationHub.cs
public async Task NotifyDeploymentStart()
{
// ❌ 강제 새로고침하지 않음
// ✅ 대신 사용자에게 알림만 보냄
await Clients.Group("admins").SendAsync("DeploymentNotification", new
{
Type = "DeploymentStart",
Message = "새 버전이 배포되고 있습니다. 진행 중인 작업을 계속할 수 있습니다.",
TimeoutSeconds = 60 // 사용자가 60초 후 수동으로 새로고침 가능
});
}
```
#### 2. 프론트엔드: 배포 알림 모달 (자동 새로고침 금지)
```razor
@* Components/Admin/Shared/DeploymentNotification.razor *@
@if (showNotification)
{
<MudDialog @bind-Visible="showNotification">
<TitleContent>
<MudText Typo="Typo.h6">새 버전 배포</MudText>
</TitleContent>
<DialogContent>
<MudText>새 버전이 배포되고 있습니다. 진행 중인 작업을 계속할 수 있습니다.</MudText>
<MudText Typo="Typo.caption" Class="mt-4">
업데이트: <strong>@countdown</strong>초 후 새로고침 (또는 수동으로 새로고침)
</MudText>
<MudLinearProgressIndeterminate />
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" OnClick="RefreshNow">지금 새로고침</MudButton>
<MudButton Color="Color.Default" OnClick="DismissNotification">나중에</MudButton>
</DialogActions>
</MudDialog>
}
@code {
private bool showNotification = false;
private int countdown = 60;
private HubConnection? hubConnection;
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl("/taxbaik/hub/notifications", options =>
options.AccessTokenProvider = async () =>
await LocalStorage.GetItemAsStringAsync("authToken") ?? "")
.WithAutomaticReconnect()
.Build();
hubConnection.On<dynamic>("DeploymentNotification", async (notification) =>
{
showNotification = true;
// 사용자가 "나중에" 누르지 않으면 60초 후 자동 새로고침
await Task.Delay(TimeSpan.FromSeconds(60));
if (showNotification)
RefreshNow();
});
await hubConnection.StartAsync();
}
private void RefreshNow() => NavigationManager.NavigateTo(NavigationManager.Uri, true);
private void DismissNotification()
{
showNotification = false;
countdown = 0;
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (hubConnection is not null)
await hubConnection.DisposeAsync();
}
}
```
#### 3. CI/CD 배포 알림 (server-sent events 대신 SignalR)
```yaml
# .gitea/workflows/deploy.yml
- name: Notify deployment start
run: |
curl -X POST "http://127.0.0.1:5001/taxbaik/api/admin/deployment-start" \
-H "Authorization: Bearer ${{ env.INTERNAL_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"message":"New version deploying..."}'
```
#### 4. 사용자 상태 보호 (데이터 손실 방지)
- ✅ 폼 데이터를 `sessionStorage`에 자동 저장 (변경 감지 시)
- ✅ 페이지 이탈 시 경고 (unsaved changes)
- ✅ 강제 새로고침 후 복구 옵션 제공
```csharp
// 폼 자동 저장 (선택적)
public class AutoSaveService
{
private readonly IJSRuntime js;
public async Task SaveFormAsync<T>(string key, T data)
{
await js.InvokeVoidAsync("sessionStorage.setItem", key,
System.Text.Json.JsonSerializer.Serialize(data));
}
public async Task<T?> RestoreFormAsync<T>(string key)
{
var json = await js.InvokeAsync<string>("sessionStorage.getItem", key);
return json == null ? default :
System.Text.Json.JsonSerializer.Deserialize<T>(json);
}
}
```
#### 5. 배포 상태 확인 엔드포인트
```csharp
// Controllers/DeploymentController.cs
[ApiController]
[Route("api/admin/[controller]")]
public class DeploymentController : ControllerBase
{
[HttpPost("deployment-start")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> NotifyDeploymentStart(
[FromServices] IHubContext<NotificationHub> hubContext)
{
await hubContext.Clients.Group("admins").SendAsync(
"DeploymentNotification", new
{
Type = "DeploymentStart",
Timestamp = DateTime.UtcNow
});
return Ok(new { message = "배포 알림 전송됨" });
}
[HttpGet("status")]
public IActionResult GetDeploymentStatus() =>
Ok(new { Status = "Running", Version = "2026-06-28" });
}
```
**핵심 원칙**:
- 배포 중 강제 새로고침 절대 금지 ❌
- 사용자에게 알림만 보내고 수동 새로고침 제공 ✅
- 폼 데이터는 세션 저장소에 자동 보존 ✅
- 강제 새로고침 후 복구 옵션 제공 ✅
### CI/CD 파이프라인 최적화 (2026-06-28)
**목표**: 전체 배포 시간을 최소화하고 명확한 Timeout 설정