docs: add admin grid UX (Dorsum ERP level) and deployment user experience protection guidelines
TaxBaik CI/CD / build-and-deploy (push) Successful in 57s
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:
@@ -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 설정
|
||||
|
||||
Reference in New Issue
Block a user