Merge pull request 'refactor(admin): 3개 신규 페이지 디자인 시스템 준수 + RBAC skeleton (Phase 1-A)' (#53) from feature/admin-ds-rbac-refactor into develop

This commit is contained in:
htlee 2026-04-16 08:43:52 +09:00
커밋 03d3d428e5
6개의 변경된 파일164개의 추가작업 그리고 66개의 파일을 삭제

파일 보기

@ -4,6 +4,11 @@
## [Unreleased]
### 변경
- **Admin 3개 페이지 디자인 시스템 준수 리팩토링 (Phase 1-A)** — PerformanceMonitoring/DataRetentionPolicy/DataModelVerification 자체 탭 네비 → `TabBar/TabButton` 공통 컴포넌트, 원시 `<button>``TabButton`, PerformanceMonitoring 정적 hex 9건 → `performanceStatus` 카탈로그 경유
- **신규 카탈로그** `shared/constants/performanceStatus.ts` — PerformanceStatus(good/warning/critical/running/passed/failed/active/scheduled/archived) → {intent, hex, label} + utilizationStatus(ratio) 헬퍼
- **RBAC skeleton** — 3개 페이지 최상단 `useAuth().hasPermission('admin:{resource}', 'OP')` 호출 배치 (Phase 3 액션 버튼 추가 시 가드로 연결)
## [2026-04-16.2]
### 추가

파일 보기

@ -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, CheckCircle, AlertTriangle, FileText, Users,
Layers, Table2, Search, ChevronRight, GitBranch,
@ -105,6 +107,18 @@ const VERIFICATION_HISTORY = [
export function DataModelVerification() {
const [tab, setTab] = useState<Tab>('overview');
const { hasPermission } = useAuth();
// 향후 Phase 3 에서 검증 승인·이력 등록 버튼 추가 시 가드로 연결
void hasPermission('admin:data-model-verification', 'CREATE');
void hasPermission('admin:data-model-verification', 'UPDATE');
const TABS: Array<{ key: Tab; icon: typeof Eye; label: string }> = [
{ key: 'overview', icon: Eye, label: '검증 현황' },
{ key: 'logical', icon: GitBranch, label: '논리 모델 검증' },
{ key: 'physical', icon: Database, label: '물리 모델 검증' },
{ key: 'duplication', icon: Search, label: '중복·정합성 점검' },
{ key: 'history', icon: FileText, label: '검증 결과 이력' },
];
return (
<PageContainer>
@ -116,21 +130,14 @@ export function DataModelVerification() {
demo
/>
{/* 탭 */}
<div className="flex gap-0 border-b border-border">
{([
{ key: 'overview' as Tab, icon: Eye, label: '검증 현황' },
{ key: 'logical' as Tab, icon: GitBranch, label: '논리 모델 검증' },
{ key: 'physical' as Tab, icon: Database, label: '물리 모델 검증' },
{ key: 'duplication' as Tab, icon: Search, label: '중복·정합성 점검' },
{ key: 'history' as Tab, icon: FileText, label: '검증 결과 이력' },
]).map(t => (
<button type="button" key={t.key} onClick={() => setTab(t.key)}
className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-green-400 border-green-400' : 'text-hint border-transparent hover:text-label'}`}>
<t.icon className="w-3.5 h-3.5" />{t.label}
</button>
<TabBar variant="underline">
{TABS.map(t => (
<TabButton key={t.key} variant="underline" active={tab === t.key}
icon={<t.icon className="w-3.5 h-3.5" />} onClick={() => setTab(t.key)}>
{t.label}
</TabButton>
))}
</div>
</TabBar>
{/* ── ① 검증 현황 ── */}
{tab === 'overview' && (

파일 보기

@ -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<Tab>('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 (
<PageContainer>
@ -100,21 +114,14 @@ export function DataRetentionPolicy() {
demo
/>
{/* 탭 */}
<div className="flex gap-0 border-b border-border">
{([
{ 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 => (
<button type="button" key={t.key} onClick={() => setTab(t.key)}
className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-blue-400 border-blue-400' : 'text-hint border-transparent hover:text-label'}`}>
<t.icon className="w-3.5 h-3.5" />{t.label}
</button>
<TabBar variant="underline">
{TABS.map(t => (
<TabButton key={t.key} variant="underline" active={tab === t.key}
icon={<t.icon className="w-3.5 h-3.5" />} onClick={() => setTab(t.key)}>
{t.label}
</TabButton>
))}
</div>
</TabBar>
{/* ── ① 보관 현황 ── */}
{tab === 'overview' && (

파일 보기

@ -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<Tab>('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 (
<PageContainer>
@ -141,38 +159,34 @@ export function PerformanceMonitoring() {
demo
/>
{/* 탭 */}
<div className="flex gap-0 border-b border-border">
{([
{ 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 => (
<button type="button" key={t.key} onClick={() => setTab(t.key)}
className={`flex items-center gap-1.5 px-4 py-2.5 text-[11px] font-medium border-b-2 transition-colors ${tab === t.key ? 'text-cyan-400 border-cyan-400' : 'text-hint border-transparent hover:text-label'}`}>
<t.icon className="w-3.5 h-3.5" />{t.label}
</button>
<TabBar variant="underline">
{TABS.map(t => (
<TabButton key={t.key} variant="underline" active={tab === t.key}
icon={<t.icon className="w-3.5 h-3.5" />} onClick={() => setTab(t.key)}>
{t.label}
</TabButton>
))}
</div>
</TabBar>
{/* ── ① 성능 현황 ── */}
{tab === 'overview' && (
<div className="space-y-3">
{/* KPI */}
<div className="grid grid-cols-6 gap-2">
{PERF_KPI.map(k => (
<div key={k.label} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card" style={{ borderLeftColor: k.color, borderLeftWidth: 3 }}>
<k.icon className="w-5 h-5" style={{ color: k.color }} />
<div>
<div className="text-lg font-bold" style={{ color: k.color }}>
{k.value}<span className="text-[10px] ml-1 text-hint">{k.unit}</span>
{PERF_KPI.map(k => {
const hex = getPerformanceStatusHex(k.status);
return (
<div key={k.label} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card" style={{ borderLeftColor: hex, borderLeftWidth: 3 }}>
<k.icon className="w-5 h-5" style={{ color: hex }} />
<div>
<div className="text-lg font-bold" style={{ color: hex }}>
{k.value}<span className="text-[10px] ml-1 text-hint">{k.unit}</span>
</div>
<div className="text-[9px] text-hint">{k.label}</div>
</div>
<div className="text-[9px] text-hint">{k.label}</div>
</div>
</div>
))}
);
})}
</div>
{/* 사용자 그룹별 SLO */}

파일 보기

@ -30,3 +30,4 @@ export * from './connectionStatuses';
export * from './trainingZoneTypes';
export * from './kpiUiMap';
export * from './statusIntent';
export * from './performanceStatus';

파일 보기

@ -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<PerformanceStatus, PerformanceStatusMeta> = {
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;
}