From 69b97d33f68cb5237864431d5b4dede590f70ee6 Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 16 Apr 2026 08:39:34 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor(admin):=203=EA=B0=9C=20=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=A4=80=EC=88=98=20?= =?UTF-8?q?+=20RBAC=20skeleton=20(Phase=201-A)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - performanceStatus.ts 카탈로그 신설 (status→intent/hex/label) - 자체 탭 네비 3건 → TabBar/TabButton (underline variant) - raw + + {TABS.map(t => ( + } onClick={() => setTab(t.key)}> + {t.label} + ))} - + {/* ── ① 검증 현황 ── */} {tab === 'overview' && ( diff --git a/frontend/src/features/admin/DataRetentionPolicy.tsx b/frontend/src/features/admin/DataRetentionPolicy.tsx index 3858b00..e0df961 100644 --- a/frontend/src/features/admin/DataRetentionPolicy.tsx +++ b/frontend/src/features/admin/DataRetentionPolicy.tsx @@ -1,8 +1,10 @@ import { useState } from 'react'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; +import { TabBar, TabButton } from '@shared/components/ui/tabs'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { getStatusIntent } from '@shared/constants/statusIntent'; +import { useAuth } from '@/app/auth/AuthContext'; import { Database, Clock, Trash2, ShieldCheck, FileText, AlertTriangle, CheckCircle, Archive, CalendarClock, UserCheck, Search, @@ -89,6 +91,18 @@ const STORAGE_ARCHITECTURE = [ export function DataRetentionPolicy() { const [tab, setTab] = useState('overview'); + const { hasPermission } = useAuth(); + // 향후 Phase 3 에서 파기 승인/예외 등록 시 disabled 가드로 활용 + void hasPermission('admin:data-retention', 'UPDATE'); + void hasPermission('admin:data-retention', 'DELETE'); + + const TABS: Array<{ key: Tab; icon: typeof Eye; label: string }> = [ + { key: 'overview', icon: Eye, label: '보관 현황' }, + { key: 'retention', icon: CalendarClock, label: '유형별 보관기간' }, + { key: 'disposal', icon: Trash2, label: '파기 절차' }, + { key: 'exception', icon: ShieldCheck, label: '예외·연장' }, + { key: 'audit', icon: FileText, label: '파기 감사 대장' }, + ]; return ( @@ -100,21 +114,14 @@ export function DataRetentionPolicy() { demo /> - {/* 탭 */} -
- {([ - { key: 'overview' as Tab, icon: Eye, label: '보관 현황' }, - { key: 'retention' as Tab, icon: CalendarClock, label: '유형별 보관기간' }, - { key: 'disposal' as Tab, icon: Trash2, label: '파기 절차' }, - { key: 'exception' as Tab, icon: ShieldCheck, label: '예외·연장' }, - { key: 'audit' as Tab, icon: FileText, label: '파기 감사 대장' }, - ]).map(t => ( - + + {TABS.map(t => ( + } onClick={() => setTab(t.key)}> + {t.label} + ))} -
+ {/* ── ① 보관 현황 ── */} {tab === 'overview' && ( diff --git a/frontend/src/features/admin/PerformanceMonitoring.tsx b/frontend/src/features/admin/PerformanceMonitoring.tsx index cd59ed3..02517e2 100644 --- a/frontend/src/features/admin/PerformanceMonitoring.tsx +++ b/frontend/src/features/admin/PerformanceMonitoring.tsx @@ -1,7 +1,15 @@ import { useState } from 'react'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; +import { TabBar, TabButton } from '@shared/components/ui/tabs'; import { PageContainer, PageHeader } from '@shared/components/layout'; +import { + getPerformanceStatusHex, + getPerformanceStatusIntent, + utilizationStatus, + type PerformanceStatus, +} from '@shared/constants/performanceStatus'; +import { useAuth } from '@/app/auth/AuthContext'; import { Activity, Gauge, Users, Database, Brain, Server, CheckCircle, AlertTriangle, TrendingUp, Clock, @@ -21,13 +29,13 @@ import { type Tab = 'overview' | 'response' | 'capacity' | 'aiModel' | 'availability'; // ─── 성능 KPI ────────────────── -const PERF_KPI = [ - { label: '현재 동시접속', value: '342', unit: '명', icon: Users, color: '#3b82f6', status: 'normal' }, - { label: '대시보드 p95', value: '1.8', unit: '초', icon: Gauge, color: '#10b981', status: 'good' }, - { label: '시스템 가동률', value: '99.87', unit: '%', icon: Shield, color: '#06b6d4', status: 'good' }, - { label: 'AI 추론 p95', value: '1.4', unit: '초', icon: Brain, color: '#8b5cf6', status: 'good' }, - { label: '배치 SLA 준수', value: '100', unit: '%', icon: CheckCircle, color: '#10b981', status: 'good' }, - { label: '이벤트 경보', value: '0', unit: '건', icon: AlertTriangle, color: '#f59e0b', status: 'normal' }, +const PERF_KPI: Array<{ label: string; value: string; unit: string; icon: typeof Users; status: PerformanceStatus }> = [ + { label: '현재 동시접속', value: '342', unit: '명', icon: Users, status: 'normal' }, + { label: '대시보드 p95', value: '1.8', unit: '초', icon: Gauge, status: 'good' }, + { label: '시스템 가동률', value: '99.87', unit: '%', icon: Shield, status: 'good' }, + { label: 'AI 추론 p95', value: '1.4', unit: '초', icon: Brain, status: 'good' }, + { label: '배치 SLA 준수', value: '100', unit: '%', icon: CheckCircle, status: 'good' }, + { label: '이벤트 경보', value: '0', unit: '건', icon: AlertTriangle, status: 'normal' }, ]; // ─── SLO 적용 그룹 ────────────────── @@ -116,20 +124,30 @@ const IMPACT_REDUCTION = [ { strategy: 'HPA 자동 확장', target: 'CPU/메모리 70% 임계', effect: '피크 자동 대응', per: 'PER-02·06' }, ]; -const statusIntent = (s: 'good' | 'warn' | 'critical' | 'success'): 'success' | 'warning' | 'critical' => { - if (s === 'good' || s === 'success') return 'success'; +// 로컬 status 문자열을 카탈로그 PerformanceStatus로 매핑 +const toStatus = (s: 'good' | 'warn' | 'critical' | 'success'): PerformanceStatus => { + if (s === 'good' || s === 'success') return 'good'; if (s === 'warn') return 'warning'; return 'critical'; }; - -const barColor = (ratio: number): string => { - if (ratio < 0.6) return '#10b981'; - if (ratio < 0.8) return '#f59e0b'; - return '#ef4444'; -}; +const statusIntent = (s: 'good' | 'warn' | 'critical' | 'success') => + getPerformanceStatusIntent(toStatus(s)); +const barColor = (ratio: number): string => + getPerformanceStatusHex(utilizationStatus(ratio)); export function PerformanceMonitoring() { const [tab, setTab] = useState('overview'); + const { hasPermission } = useAuth(); + // 향후 Phase 3 에서 EXPORT 버튼 추가 시 disabled={!canExport} 로 연결 + void hasPermission('admin:performance-monitoring', 'EXPORT'); + + const TABS: Array<{ key: Tab; icon: typeof BarChart3; label: string }> = [ + { key: 'overview', icon: BarChart3, label: '성능 현황' }, + { key: 'response', icon: Gauge, label: '응답성 (PER-01)' }, + { key: 'capacity', icon: Users, label: '처리용량 (PER-02·03)' }, + { key: 'aiModel', icon: Brain, label: 'AI 모델 (PER-04)' }, + { key: 'availability', icon: Shield, label: '가용성·확장성 (PER-05·06)' }, + ]; return ( @@ -141,38 +159,34 @@ export function PerformanceMonitoring() { demo /> - {/* 탭 */} -
- {([ - { key: 'overview' as Tab, icon: BarChart3, label: '성능 현황' }, - { key: 'response' as Tab, icon: Gauge, label: '응답성 (PER-01)' }, - { key: 'capacity' as Tab, icon: Users, label: '처리용량 (PER-02·03)' }, - { key: 'aiModel' as Tab, icon: Brain, label: 'AI 모델 (PER-04)' }, - { key: 'availability' as Tab, icon: Shield, label: '가용성·확장성 (PER-05·06)' }, - ]).map(t => ( - + + {TABS.map(t => ( + } onClick={() => setTab(t.key)}> + {t.label} + ))} -
+ {/* ── ① 성능 현황 ── */} {tab === 'overview' && (
{/* KPI */}
- {PERF_KPI.map(k => ( -
- -
-
- {k.value}{k.unit} + {PERF_KPI.map(k => { + const hex = getPerformanceStatusHex(k.status); + return ( +
+ +
+
+ {k.value}{k.unit} +
+
{k.label}
-
{k.label}
-
- ))} + ); + })}
{/* 사용자 그룹별 SLO */} diff --git a/frontend/src/shared/constants/index.ts b/frontend/src/shared/constants/index.ts index 112a84f..bc45e94 100644 --- a/frontend/src/shared/constants/index.ts +++ b/frontend/src/shared/constants/index.ts @@ -30,3 +30,4 @@ export * from './connectionStatuses'; export * from './trainingZoneTypes'; export * from './kpiUiMap'; export * from './statusIntent'; +export * from './performanceStatus'; diff --git a/frontend/src/shared/constants/performanceStatus.ts b/frontend/src/shared/constants/performanceStatus.ts new file mode 100644 index 0000000..f259a12 --- /dev/null +++ b/frontend/src/shared/constants/performanceStatus.ts @@ -0,0 +1,64 @@ +/** + * 성능/시스템 상태 공통 카탈로그 (admin 성능·데이터 보관·모델 검증 페이지 공유) + * + * status 문자열 → BadgeIntent + 아이콘 hex. + * 인라인 hex/하드코딩 Tailwind 색상을 이 카탈로그로 치환한다. + * + * 사용처: + * - PerformanceMonitoring (KPI 카드, SLO 대시보드) + * - DataRetentionPolicy (정책 활성/보관 상태) + * - DataModelVerification (검증 passed/failed/running) + */ + +import type { BadgeIntent } from '@lib/theme/variants'; + +export type PerformanceStatus = + | 'good' // 정상/양호 + | 'normal' // 일반 (기본) + | 'warning' // 주의 + | 'critical' // 심각/경보 + | 'running' // 실행 중 + | 'passed' // 통과 + | 'failed' // 실패 + | 'active' // 활성 + | 'scheduled' // 예약 + | 'archived'; // 보관 + +export interface PerformanceStatusMeta { + intent: BadgeIntent; + /** 아이콘·바 차트용 hex (동적 데이터 기반 예외 허용) */ + hex: string; + label: { ko: string; en: string }; +} + +export const PERFORMANCE_STATUS_META: Record = { + good: { intent: 'success', hex: '#10b981', label: { ko: '양호', en: 'Good' } }, + normal: { intent: 'info', hex: '#3b82f6', label: { ko: '정상', en: 'Normal' } }, + warning: { intent: 'warning', hex: '#f59e0b', label: { ko: '주의', en: 'Warning' } }, + critical: { intent: 'critical', hex: '#ef4444', label: { ko: '심각', en: 'Critical' } }, + running: { intent: 'info', hex: '#06b6d4', label: { ko: '실행 중', en: 'Running' } }, + passed: { intent: 'success', hex: '#10b981', label: { ko: '통과', en: 'Passed' } }, + failed: { intent: 'critical', hex: '#ef4444', label: { ko: '실패', en: 'Failed' } }, + active: { intent: 'success', hex: '#10b981', label: { ko: '활성', en: 'Active' } }, + scheduled: { intent: 'info', hex: '#8b5cf6', label: { ko: '예약', en: 'Scheduled' } }, + archived: { intent: 'muted', hex: '#6b7280', label: { ko: '보관', en: 'Archived' } }, +}; + +/** 사용률(0~1) → 상태 분류 (KPI 게이지 바 등) */ +export function utilizationStatus(ratio: number): PerformanceStatus { + if (ratio < 0.6) return 'good'; + if (ratio < 0.8) return 'warning'; + return 'critical'; +} + +export function getPerformanceStatusMeta(status: PerformanceStatus): PerformanceStatusMeta { + return PERFORMANCE_STATUS_META[status] ?? PERFORMANCE_STATUS_META.normal; +} + +export function getPerformanceStatusIntent(status: PerformanceStatus): BadgeIntent { + return getPerformanceStatusMeta(status).intent; +} + +export function getPerformanceStatusHex(status: PerformanceStatus): string { + return getPerformanceStatusMeta(status).hex; +} From a68945bd0759e69bc9082fe1bedcf3a0417c7ff7 Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 16 Apr 2026 08:40:07 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 1beeeec..8e25f5e 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,11 @@ ## [Unreleased] +### 변경 +- **Admin 3개 페이지 디자인 시스템 준수 리팩토링 (Phase 1-A)** — PerformanceMonitoring/DataRetentionPolicy/DataModelVerification 자체 탭 네비 → `TabBar/TabButton` 공통 컴포넌트, 원시 `