refactor(frontend): LGCNS 3개 페이지 디자인 시스템 공통 구조 전환
- 커스텀 탭 → TabBar/TabButton 공통 컴포넌트 교체 (3개 파일)
- hex 색상 맵 → Tailwind 클래스 토큰 전환, style={{ }} 인라인 제거
- 인라인 Badge intent 삼항 → 카탈로그 함수 교체 (getAgentPermTypeIntent 등)
- 신규 카탈로그: mlopsJobStatuses (4종), aiSecurityStatuses (위협3+권한5+결과3)
- catalogRegistry에 4건 등록 → design-system.html 쇼케이스 자동 노출
- statusIntent.ts에 '허용', '위험', '관리자', '중지', '실행' 매핑 추가
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
f304f778ca
커밋
99d72e3622
@ -1,12 +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 { getStatusIntent } from '@shared/constants/statusIntent';
|
||||
import { getAgentPermTypeIntent, getThreatLevelIntent, getAgentExecResultIntent } from '@shared/constants/aiSecurityStatuses';
|
||||
import {
|
||||
Bot, Shield, Lock, Eye, Activity, AlertTriangle,
|
||||
CheckCircle, FileText, Settings, Terminal, Users,
|
||||
Key, Layers, Workflow, Hand,
|
||||
Bot, Activity, AlertTriangle,
|
||||
CheckCircle, FileText,
|
||||
Users,
|
||||
Key, Layers, Hand,
|
||||
} from 'lucide-react';
|
||||
|
||||
/*
|
||||
@ -19,6 +22,15 @@ import {
|
||||
|
||||
type Tab = 'overview' | 'whitelist' | 'killswitch' | 'identity' | 'mcp' | 'audit';
|
||||
|
||||
const TABS: { key: Tab; icon: React.ElementType; label: string }[] = [
|
||||
{ key: 'overview', icon: Activity, label: 'Agent 현황' },
|
||||
{ key: 'whitelist', icon: CheckCircle, label: '화이트리스트 도구' },
|
||||
{ key: 'killswitch', icon: AlertTriangle, label: '자동 중단·승인' },
|
||||
{ key: 'identity', icon: Users, label: 'Agent 신원·권한' },
|
||||
{ key: 'mcp', icon: Layers, label: 'MCP Tool 권한' },
|
||||
{ key: 'audit', icon: FileText, label: '감사 로그' },
|
||||
];
|
||||
|
||||
// ─── Agent 현황 ──────────────────
|
||||
const AGENTS = [
|
||||
{ name: '위험도 분석 Agent', type: '조회 전용', tools: 4, status: '활성', calls24h: 1240, lastCall: '04-10 09:28' },
|
||||
@ -29,11 +41,11 @@ const AGENTS = [
|
||||
];
|
||||
|
||||
const AGENT_KPI = [
|
||||
{ label: '활성 Agent', value: '4', color: '#10b981' },
|
||||
{ label: '등록 Tool', value: '26', color: '#3b82f6' },
|
||||
{ label: '24h 호출', value: '2,656', color: '#8b5cf6' },
|
||||
{ label: '차단 건수', value: '3', color: '#ef4444' },
|
||||
{ label: '승인 대기', value: '0', color: '#f59e0b' },
|
||||
{ label: '활성 Agent', value: '4', color: 'text-green-400', bg: 'bg-green-500/10' },
|
||||
{ label: '등록 Tool', value: '26', color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||||
{ label: '24h 호출', value: '2,656', color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||||
{ label: '차단 건수', value: '3', color: 'text-red-400', bg: 'bg-red-500/10' },
|
||||
{ label: '승인 대기', value: '0', color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
|
||||
];
|
||||
|
||||
// ─── 화이트리스트 도구 ──────────────────
|
||||
@ -105,29 +117,21 @@ export function AIAgentSecurityPage() {
|
||||
/>
|
||||
|
||||
{/* 탭 */}
|
||||
<div className="flex gap-0 border-b border-border">
|
||||
{([
|
||||
{ key: 'overview' as Tab, icon: Activity, label: 'Agent 현황' },
|
||||
{ key: 'whitelist' as Tab, icon: CheckCircle, label: '화이트리스트 도구' },
|
||||
{ key: 'killswitch' as Tab, icon: AlertTriangle, label: '자동 중단·승인' },
|
||||
{ key: 'identity' as Tab, icon: Users, label: 'Agent 신원·권한' },
|
||||
{ key: 'mcp' as Tab, icon: Layers, label: 'MCP Tool 권한' },
|
||||
{ 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-orange-400 border-orange-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(tt => (
|
||||
<TabButton key={tt.key} active={tab === tt.key} icon={<tt.icon className="w-3.5 h-3.5" />} onClick={() => setTab(tt.key)}>
|
||||
{tt.label}
|
||||
</TabButton>
|
||||
))}
|
||||
</div>
|
||||
</TabBar>
|
||||
|
||||
{/* ── ① Agent 현황 ── */}
|
||||
{tab === 'overview' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
{AGENT_KPI.map(k => (
|
||||
<div key={k.label} className="flex-1 px-4 py-3 rounded-xl border border-border bg-card" style={{ borderLeftColor: k.color, borderLeftWidth: 3 }}>
|
||||
<div className="text-xl font-bold" style={{ color: k.color }}>{k.value}</div>
|
||||
<div key={k.label} className={`flex-1 px-4 py-3 rounded-xl border border-border ${k.bg}`}>
|
||||
<div className={`text-xl font-bold ${k.color}`}>{k.value}</div>
|
||||
<div className="text-[9px] text-hint">{k.label}</div>
|
||||
</div>
|
||||
))}
|
||||
@ -143,7 +147,7 @@ export function AIAgentSecurityPage() {
|
||||
<tbody>{AGENTS.map(a => (
|
||||
<tr key={a.name} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
|
||||
<td className="py-2 px-2 text-heading font-medium">{a.name}</td>
|
||||
<td className="py-2 text-center"><Badge intent={a.type === '관리자' ? 'high' : a.type.includes('쓰기') ? 'warning' : 'info'} size="sm">{a.type}</Badge></td>
|
||||
<td className="py-2 text-center"><Badge intent={getAgentPermTypeIntent(a.type)} size="sm">{a.type}</Badge></td>
|
||||
<td className="py-2 text-center text-heading">{a.tools}</td>
|
||||
<td className="py-2 text-center text-muted-foreground">{a.calls24h.toLocaleString()}</td>
|
||||
<td className="py-2 text-center text-muted-foreground">{a.lastCall}</td>
|
||||
@ -171,9 +175,9 @@ export function AIAgentSecurityPage() {
|
||||
<td className="py-2 px-2 text-heading font-mono text-[9px]">{t.tool}</td>
|
||||
<td className="py-2 text-hint">{t.desc}</td>
|
||||
<td className="py-2 text-center text-muted-foreground">{t.agent}</td>
|
||||
<td className="py-2 text-center"><Badge intent={t.permission === 'DELETE' || t.permission === 'ADMIN' ? 'critical' : t.permission === 'READ' ? 'info' : 'warning'} size="sm">{t.permission}</Badge></td>
|
||||
<td className="py-2 text-center"><Badge intent={getAgentPermTypeIntent(t.permission)} size="sm">{t.permission}</Badge></td>
|
||||
<td className="py-2 text-muted-foreground font-mono text-[9px]">{t.mcp}</td>
|
||||
<td className="py-2 text-center"><Badge intent={t.status === '허용' ? 'success' : 'critical'} size="sm">{t.status}</Badge></td>
|
||||
<td className="py-2 text-center"><Badge intent={getStatusIntent(t.status)} size="sm">{t.status}</Badge></td>
|
||||
</tr>
|
||||
))}</tbody>
|
||||
</table>
|
||||
@ -210,7 +214,7 @@ export function AIAgentSecurityPage() {
|
||||
<tbody>{SENSITIVE_COMMANDS.map(c => (
|
||||
<tr key={c.command} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
|
||||
<td className="py-2 px-2 text-heading font-medium">{c.command}</td>
|
||||
<td className="py-2 text-center"><Badge intent={c.level === '높음' ? 'critical' : c.level === '중간' ? 'warning' : 'info'} size="sm">{c.level}</Badge></td>
|
||||
<td className="py-2 text-center"><Badge intent={getThreatLevelIntent(c.level)} size="sm">{c.level}</Badge></td>
|
||||
<td className="py-2 text-muted-foreground">{c.approval}</td>
|
||||
<td className="py-2 text-center">{c.hitl ? <Hand className="w-3.5 h-3.5 text-orange-400 mx-auto" /> : <span className="text-hint">-</span>}</td>
|
||||
<td className="py-2 text-center"><Badge intent="success" size="sm">{c.status}</Badge></td>
|
||||
@ -283,7 +287,7 @@ export function AIAgentSecurityPage() {
|
||||
<td className="py-2 text-hint text-[9px]">{l.chain}</td>
|
||||
<td className="py-2 text-center text-heading">{l.agent}</td>
|
||||
<td className="py-2 text-heading font-mono text-[9px]">{l.tool}</td>
|
||||
<td className="py-2 text-center"><Badge intent={l.result === '차단' ? 'critical' : l.result.includes('승인') ? 'warning' : 'success'} size="sm">{l.result}</Badge></td>
|
||||
<td className="py-2 text-center"><Badge intent={getAgentExecResultIntent(l.result)} size="sm">{l.result}</Badge></td>
|
||||
<td className="py-2 px-2 text-right text-muted-foreground">{l.latency}</td>
|
||||
</tr>
|
||||
))}</tbody>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
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 {
|
||||
Shield, Database, Brain, Lock, Eye, Activity,
|
||||
AlertTriangle, CheckCircle, XCircle, FileText,
|
||||
Server, Layers, Settings, Search, RefreshCw,
|
||||
AlertTriangle, CheckCircle,
|
||||
Server, Search, RefreshCw,
|
||||
} from 'lucide-react';
|
||||
|
||||
/*
|
||||
@ -19,13 +20,22 @@ import {
|
||||
|
||||
type Tab = 'overview' | 'data' | 'training' | 'io' | 'boundary' | 'vulnerability';
|
||||
|
||||
const TABS: { key: Tab; icon: React.ComponentType<{ className?: string }>; label: string }[] = [
|
||||
{ key: 'overview', icon: Activity, label: '보안 현황' },
|
||||
{ key: 'data', icon: Database, label: '데이터 수집 보안' },
|
||||
{ key: 'training', icon: Brain, label: 'AI 학습 보안' },
|
||||
{ key: 'io', icon: Lock, label: '입출력 보안' },
|
||||
{ key: 'boundary', icon: Server, label: '경계 보안' },
|
||||
{ key: 'vulnerability', icon: Search, label: '취약점 점검' },
|
||||
];
|
||||
|
||||
// ─── 보안 현황 KPI ──────────────────
|
||||
const SECURITY_KPI = [
|
||||
{ label: '데이터 수집 보안', value: '정상', score: 95, icon: Database, color: '#10b981' },
|
||||
{ label: 'AI 학습 보안', value: '정상', score: 92, icon: Brain, color: '#3b82f6' },
|
||||
{ label: '입출력 필터링', value: '정상', score: 98, icon: Lock, color: '#8b5cf6' },
|
||||
{ label: '경계 보안', value: '주의', score: 85, icon: Server, color: '#f59e0b' },
|
||||
{ label: '취약점 점검', value: '정상', score: 90, icon: Search, color: '#06b6d4' },
|
||||
{ label: '데이터 수집 보안', value: '정상', score: 95, icon: Database, color: 'text-green-400', bg: 'bg-green-500/10' },
|
||||
{ label: 'AI 학습 보안', value: '정상', score: 92, icon: Brain, color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||||
{ label: '입출력 필터링', value: '정상', score: 98, icon: Lock, color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||||
{ label: '경계 보안', value: '주의', score: 85, icon: Server, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
|
||||
{ label: '취약점 점검', value: '정상', score: 90, icon: Search, color: 'text-cyan-400', bg: 'bg-cyan-500/10' },
|
||||
];
|
||||
|
||||
// ─── 데이터 수집 보안 ──────────────────
|
||||
@ -108,36 +118,30 @@ export function AISecurityPage() {
|
||||
/>
|
||||
|
||||
{/* 탭 */}
|
||||
<div className="flex gap-0 border-b border-border">
|
||||
{([
|
||||
{ key: 'overview' as Tab, icon: Activity, label: '보안 현황' },
|
||||
{ key: 'data' as Tab, icon: Database, label: '데이터 수집 보안' },
|
||||
{ key: 'training' as Tab, icon: Brain, label: 'AI 학습 보안' },
|
||||
{ key: 'io' as Tab, icon: Lock, label: '입출력 보안' },
|
||||
{ key: 'boundary' as Tab, icon: Server, label: '경계 보안' },
|
||||
{ key: 'vulnerability' as Tab, icon: Search, 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-red-400 border-red-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(tt => (
|
||||
<TabButton
|
||||
key={tt.key}
|
||||
active={tab === tt.key}
|
||||
icon={<tt.icon className="w-3.5 h-3.5" />}
|
||||
onClick={() => setTab(tt.key)}
|
||||
>
|
||||
{tt.label}
|
||||
</TabButton>
|
||||
))}
|
||||
</div>
|
||||
</TabBar>
|
||||
|
||||
{/* ── ① 보안 현황 ── */}
|
||||
{tab === 'overview' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
{SECURITY_KPI.map(k => (
|
||||
<div key={k.label} className="flex-1 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="flex items-center gap-2">
|
||||
<span className="text-xl font-bold" style={{ color: k.color }}>{k.score}</span>
|
||||
<span className="text-[9px] text-hint">/ 100</span>
|
||||
</div>
|
||||
<div className="text-[9px] text-hint">{k.label}</div>
|
||||
<div key={k.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
|
||||
<div className={`p-1.5 rounded-lg ${k.bg}`}>
|
||||
<k.icon className={`w-3.5 h-3.5 ${k.color}`} />
|
||||
</div>
|
||||
<span className={`text-base font-bold ${k.color}`}>{k.score}</span>
|
||||
<span className="text-[9px] text-hint">{k.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -322,7 +326,9 @@ export function AISecurityPage() {
|
||||
<td className="py-2 px-2 text-heading font-medium">{v.target}</td>
|
||||
<td className="py-2 text-center text-muted-foreground font-mono">{v.version}</td>
|
||||
<td className="py-2 text-center text-muted-foreground">{v.lastScan}</td>
|
||||
<td className="py-2 text-center">{v.vulns > 0 ? <span className="text-yellow-400 font-bold">{v.vulns}건</span> : <span className="text-green-400">0건</span>}</td>
|
||||
<td className="py-2 text-center">
|
||||
<Badge intent={v.vulns > 0 ? 'warning' : 'success'} size="xs">{v.vulns}건</Badge>
|
||||
</td>
|
||||
<td className="py-2 text-center"><Badge intent={getStatusIntent(v.status)} size="sm">{v.status}</Badge></td>
|
||||
</tr>
|
||||
))}</tbody>
|
||||
|
||||
@ -2,8 +2,11 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { getMlopsJobStatusIntent, getMlopsJobStatusLabel } from '@shared/constants/mlopsJobStatuses';
|
||||
import { getModelStatusIntent, getModelStatusLabel } from '@shared/constants/modelDeploymentStatuses';
|
||||
import {
|
||||
Brain, Database, GitBranch, Activity, Server, Shield,
|
||||
Settings, Layers, BarChart3, Code, Play,
|
||||
@ -21,6 +24,23 @@ import {
|
||||
|
||||
type Tab = 'project' | 'environment' | 'model' | 'job' | 'common' | 'monitoring' | 'repository';
|
||||
|
||||
const TABS: { key: Tab; icon: React.ElementType; label: string }[] = [
|
||||
{ key: 'project', icon: Layers, label: '프로젝트 관리' },
|
||||
{ key: 'environment', icon: Terminal, label: '분석환경 관리' },
|
||||
{ key: 'model', icon: Brain, label: '모델 관리' },
|
||||
{ key: 'job', icon: Play, label: 'Job 실행 관리' },
|
||||
{ key: 'common', icon: Settings, label: '공통서비스' },
|
||||
{ key: 'monitoring', icon: Activity, label: '모니터링' },
|
||||
{ key: 'repository', icon: Database, label: 'Repository' },
|
||||
];
|
||||
|
||||
const JOB_PROGRESS_COLOR: Record<string, string> = {
|
||||
running: 'bg-blue-500',
|
||||
done: 'bg-green-500',
|
||||
fail: 'bg-red-500',
|
||||
pending: 'bg-muted',
|
||||
};
|
||||
|
||||
// ─── 프로젝트 관리 ──────────────────
|
||||
const PROJECTS = [
|
||||
{ id: 'PRJ-001', name: '불법조업 위험도 예측', owner: '분석팀', status: '활성', models: 5, experiments: 12, updated: '2026-04-10' },
|
||||
@ -96,15 +116,23 @@ const GPU_RESOURCES = [
|
||||
];
|
||||
|
||||
const SYSTEM_METRICS = [
|
||||
{ label: '실행중 Job', value: '3', color: '#3b82f6' },
|
||||
{ label: '대기중 Job', value: '2', color: '#f59e0b' },
|
||||
{ label: 'GPU 사용률', value: '65%', color: '#10b981' },
|
||||
{ label: '배포 모델', value: '2', color: '#8b5cf6' },
|
||||
{ label: 'API 호출/s', value: '221', color: '#06b6d4' },
|
||||
{ label: '실행중 Job', value: '3', icon: Play, color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||||
{ label: '대기중 Job', value: '2', icon: RefreshCw, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
|
||||
{ label: 'GPU 사용률', value: '65%', icon: Activity, color: 'text-green-400', bg: 'bg-green-500/10' },
|
||||
{ label: '배포 모델', value: '2', icon: Brain, color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||||
{ label: 'API 호출/s', value: '221', icon: Zap, color: 'text-cyan-400', bg: 'bg-cyan-500/10' },
|
||||
];
|
||||
|
||||
const PROJECT_STATS = [
|
||||
{ label: '활성 프로젝트', value: 3, icon: Layers, color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||||
{ label: '총 모델', value: 11, icon: Brain, color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||||
{ label: '총 실험', value: 29, icon: FlaskConical, color: 'text-green-400', bg: 'bg-green-500/10' },
|
||||
{ label: '참여 인원', value: 8, icon: Server, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
|
||||
];
|
||||
|
||||
export function LGCNSMLOpsPage() {
|
||||
const { t } = useTranslation('ai');
|
||||
const { t, i18n } = useTranslation('ai');
|
||||
const lang = i18n.language as 'ko' | 'en';
|
||||
const [tab, setTab] = useState<Tab>('project');
|
||||
|
||||
return (
|
||||
@ -118,22 +146,13 @@ export function LGCNSMLOpsPage() {
|
||||
/>
|
||||
|
||||
{/* 탭 */}
|
||||
<div className="flex gap-0 border-b border-border">
|
||||
{([
|
||||
{ key: 'project' as Tab, icon: Layers, label: '프로젝트 관리' },
|
||||
{ key: 'environment' as Tab, icon: Terminal, label: '분석환경 관리' },
|
||||
{ key: 'model' as Tab, icon: Brain, label: '모델 관리' },
|
||||
{ key: 'job' as Tab, icon: Play, label: 'Job 실행 관리' },
|
||||
{ key: 'common' as Tab, icon: Settings, label: '공통서비스' },
|
||||
{ key: 'monitoring' as Tab, icon: Activity, label: '모니터링' },
|
||||
{ key: 'repository' as Tab, icon: Database, label: 'Repository' },
|
||||
]).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(tt => (
|
||||
<TabButton key={tt.key} active={tab === tt.key} icon={<tt.icon className="w-3.5 h-3.5" />} onClick={() => setTab(tt.key)}>
|
||||
{tt.label}
|
||||
</TabButton>
|
||||
))}
|
||||
</div>
|
||||
</TabBar>
|
||||
|
||||
{/* ── ① 프로젝트 관리 ── */}
|
||||
{tab === 'project' && (
|
||||
@ -159,15 +178,13 @@ export function LGCNSMLOpsPage() {
|
||||
</table>
|
||||
</CardContent></Card>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ label: '활성 프로젝트', value: 3, icon: Layers, color: '#3b82f6' },
|
||||
{ label: '총 모델', value: 11, icon: Brain, color: '#8b5cf6' },
|
||||
{ label: '총 실험', value: 29, icon: FlaskConical, color: '#10b981' },
|
||||
{ label: '참여 인원', value: 8, icon: Server, color: '#f59e0b' },
|
||||
].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-xl font-bold" style={{ color: k.color }}>{k.value}</div><div className="text-[9px] text-hint">{k.label}</div></div>
|
||||
{PROJECT_STATS.map(k => (
|
||||
<div key={k.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
|
||||
<div className={`p-1.5 rounded-lg ${k.bg}`}>
|
||||
<k.icon className={`w-3.5 h-3.5 ${k.color}`} />
|
||||
</div>
|
||||
<span className={`text-base font-bold ${k.color}`}>{k.value}</span>
|
||||
<span className="text-[9px] text-hint">{k.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -201,11 +218,11 @@ export function LGCNSMLOpsPage() {
|
||||
<span className="text-[11px] text-heading font-medium flex-1">{w.name}</span>
|
||||
<span className="text-[10px] text-muted-foreground">Steps: {w.steps}</span>
|
||||
<div className="w-20 h-1.5 bg-switch-background rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full ${w.status === 'running' ? 'bg-blue-500' : w.status === 'done' ? 'bg-green-500' : 'bg-red-500'}`} style={{ width: `${w.progress}%` }} />
|
||||
<div className={`h-full rounded-full ${JOB_PROGRESS_COLOR[w.status] ?? 'bg-muted'}`} style={{ width: `${w.progress}%` }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground">{w.progress}%</span>
|
||||
<Badge intent={getStatusIntent(w.status === 'running' ? '실행중' : w.status === 'done' ? '완료' : '실패')} size="sm">
|
||||
{w.status === 'running' ? '실행중' : w.status === 'done' ? '완료' : '실패'}
|
||||
<Badge intent={getMlopsJobStatusIntent(w.status)} size="sm">
|
||||
{getMlopsJobStatusLabel(w.status, t, lang)}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
@ -228,7 +245,11 @@ export function LGCNSMLOpsPage() {
|
||||
<tr key={m.name} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
|
||||
<td className="py-2 px-2 text-heading font-medium">{m.name}</td>
|
||||
<td className="py-2 text-muted-foreground">{m.framework}</td>
|
||||
<td className="py-2 text-center"><Badge intent={getStatusIntent(m.status === 'DEPLOYED' ? '활성' : m.status === 'APPROVED' ? '승인' : m.status === 'TESTING' ? '테스트' : '대기')} size="sm">{m.status}</Badge></td>
|
||||
<td className="py-2 text-center">
|
||||
<Badge intent={getModelStatusIntent(m.status)} size="sm">
|
||||
{getModelStatusLabel(m.status, t, lang)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-2 text-center text-heading font-bold">{m.accuracy}%</td>
|
||||
<td className="py-2 text-center text-green-400">{m.kpi}</td>
|
||||
<td className="py-2 text-muted-foreground">{m.version}</td>
|
||||
@ -294,14 +315,14 @@ export function LGCNSMLOpsPage() {
|
||||
<td className="py-2">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<div className="w-16 h-1.5 bg-switch-background rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full ${j.status === 'running' ? 'bg-blue-500' : j.status === 'done' ? 'bg-green-500' : 'bg-red-500'}`} style={{ width: `${j.progress}%` }} />
|
||||
<div className={`h-full rounded-full ${JOB_PROGRESS_COLOR[j.status] ?? 'bg-muted'}`} style={{ width: `${j.progress}%` }} />
|
||||
</div>
|
||||
<span className="text-heading">{j.progress}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 text-center">
|
||||
<Badge intent={getStatusIntent(j.status === 'running' ? '실행중' : j.status === 'done' ? '완료' : '실패')} size="sm">
|
||||
{j.status === 'running' ? '실행중' : j.status === 'done' ? '완료' : '실패'}
|
||||
<Badge intent={getMlopsJobStatusIntent(j.status)} size="sm">
|
||||
{getMlopsJobStatusLabel(j.status, t, lang)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-2 px-2 text-right text-muted-foreground">{j.elapsed}</td>
|
||||
@ -345,8 +366,12 @@ export function LGCNSMLOpsPage() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
{SYSTEM_METRICS.map(k => (
|
||||
<div key={k.label} className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card" style={{ borderLeftColor: k.color, borderLeftWidth: 3 }}>
|
||||
<div><div className="text-xl font-bold" style={{ color: k.color }}>{k.value}</div><div className="text-[9px] text-hint">{k.label}</div></div>
|
||||
<div key={k.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
|
||||
<div className={`p-1.5 rounded-lg ${k.bg}`}>
|
||||
<k.icon className={`w-3.5 h-3.5 ${k.color}`} />
|
||||
</div>
|
||||
<span className={`text-base font-bold ${k.color}`}>{k.value}</span>
|
||||
<span className="text-[9px] text-hint">{k.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
132
frontend/src/shared/constants/aiSecurityStatuses.ts
Normal file
132
frontend/src/shared/constants/aiSecurityStatuses.ts
Normal file
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* AI 보안 / Agent 보안 도메인 상태 카탈로그
|
||||
*
|
||||
* 사용처: AISecurityPage, AIAgentSecurityPage
|
||||
*/
|
||||
|
||||
import type { BadgeIntent } from '@lib/theme/variants';
|
||||
|
||||
// ─── 위협 수준 ──────────────
|
||||
export type ThreatLevel = 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
|
||||
export const THREAT_LEVELS: Record<ThreatLevel, { i18nKey: string; fallback: { ko: string; en: string }; intent: BadgeIntent }> = {
|
||||
HIGH: {
|
||||
i18nKey: 'threatLevel.HIGH',
|
||||
fallback: { ko: '높음', en: 'High' },
|
||||
intent: 'critical',
|
||||
},
|
||||
MEDIUM: {
|
||||
i18nKey: 'threatLevel.MEDIUM',
|
||||
fallback: { ko: '중간', en: 'Medium' },
|
||||
intent: 'warning',
|
||||
},
|
||||
LOW: {
|
||||
i18nKey: 'threatLevel.LOW',
|
||||
fallback: { ko: '낮음', en: 'Low' },
|
||||
intent: 'info',
|
||||
},
|
||||
};
|
||||
|
||||
export function getThreatLevelIntent(s: string): BadgeIntent {
|
||||
const normalized = s === '높음' ? 'HIGH' : s === '중간' ? 'MEDIUM' : s === '낮음' ? 'LOW' : s;
|
||||
return THREAT_LEVELS[normalized as ThreatLevel]?.intent ?? 'muted';
|
||||
}
|
||||
|
||||
export function getThreatLevelLabel(
|
||||
s: string,
|
||||
t: (k: string, opts?: { defaultValue?: string }) => string,
|
||||
lang: 'ko' | 'en' = 'ko',
|
||||
): string {
|
||||
const normalized = s === '높음' ? 'HIGH' : s === '중간' ? 'MEDIUM' : s === '낮음' ? 'LOW' : s;
|
||||
const meta = THREAT_LEVELS[normalized as ThreatLevel];
|
||||
if (!meta) return s;
|
||||
return t(meta.i18nKey, { defaultValue: meta.fallback[lang] });
|
||||
}
|
||||
|
||||
// ─── Agent 권한 유형 ──────────────
|
||||
export type AgentPermType = 'ADMIN' | 'WRITE' | 'READ' | 'EXECUTE' | 'DELETE';
|
||||
|
||||
export const AGENT_PERM_TYPES: Record<AgentPermType, { i18nKey: string; fallback: { ko: string; en: string }; intent: BadgeIntent }> = {
|
||||
ADMIN: {
|
||||
i18nKey: 'agentPerm.ADMIN',
|
||||
fallback: { ko: '관리자', en: 'Admin' },
|
||||
intent: 'high',
|
||||
},
|
||||
DELETE: {
|
||||
i18nKey: 'agentPerm.DELETE',
|
||||
fallback: { ko: '삭제', en: 'Delete' },
|
||||
intent: 'critical',
|
||||
},
|
||||
WRITE: {
|
||||
i18nKey: 'agentPerm.WRITE',
|
||||
fallback: { ko: '쓰기', en: 'Write' },
|
||||
intent: 'warning',
|
||||
},
|
||||
READ: {
|
||||
i18nKey: 'agentPerm.READ',
|
||||
fallback: { ko: '읽기', en: 'Read' },
|
||||
intent: 'info',
|
||||
},
|
||||
EXECUTE: {
|
||||
i18nKey: 'agentPerm.EXECUTE',
|
||||
fallback: { ko: '실행', en: 'Execute' },
|
||||
intent: 'purple',
|
||||
},
|
||||
};
|
||||
|
||||
export function getAgentPermTypeIntent(s: string): BadgeIntent {
|
||||
const map: Record<string, AgentPermType> = { '관리자': 'ADMIN', '삭제': 'DELETE', '쓰기': 'WRITE', '조회+쓰기': 'WRITE', '읽기': 'READ', '실행': 'EXECUTE' };
|
||||
const key = map[s] ?? s;
|
||||
return AGENT_PERM_TYPES[key as AgentPermType]?.intent ?? 'muted';
|
||||
}
|
||||
|
||||
export function getAgentPermTypeLabel(
|
||||
s: string,
|
||||
t: (k: string, opts?: { defaultValue?: string }) => string,
|
||||
lang: 'ko' | 'en' = 'ko',
|
||||
): string {
|
||||
const map: Record<string, AgentPermType> = { '관리자': 'ADMIN', '삭제': 'DELETE', '쓰기': 'WRITE', '조회+쓰기': 'WRITE', '읽기': 'READ', '실행': 'EXECUTE' };
|
||||
const key = map[s] ?? s;
|
||||
const meta = AGENT_PERM_TYPES[key as AgentPermType];
|
||||
if (!meta) return s;
|
||||
return t(meta.i18nKey, { defaultValue: meta.fallback[lang] });
|
||||
}
|
||||
|
||||
// ─── Agent 실행 결과 ──────────────
|
||||
export type AgentExecResult = 'BLOCKED' | 'CONDITIONAL' | 'ALLOWED';
|
||||
|
||||
export const AGENT_EXEC_RESULTS: Record<AgentExecResult, { i18nKey: string; fallback: { ko: string; en: string }; intent: BadgeIntent }> = {
|
||||
BLOCKED: {
|
||||
i18nKey: 'agentExec.BLOCKED',
|
||||
fallback: { ko: '차단', en: 'Blocked' },
|
||||
intent: 'critical',
|
||||
},
|
||||
CONDITIONAL: {
|
||||
i18nKey: 'agentExec.CONDITIONAL',
|
||||
fallback: { ko: '조건부 승인', en: 'Conditional' },
|
||||
intent: 'warning',
|
||||
},
|
||||
ALLOWED: {
|
||||
i18nKey: 'agentExec.ALLOWED',
|
||||
fallback: { ko: '정상', en: 'Allowed' },
|
||||
intent: 'success',
|
||||
},
|
||||
};
|
||||
|
||||
export function getAgentExecResultIntent(s: string): BadgeIntent {
|
||||
const map: Record<string, AgentExecResult> = { '차단': 'BLOCKED', '조건부 승인': 'CONDITIONAL', '승인→성공': 'ALLOWED', '정상': 'ALLOWED' };
|
||||
const key = map[s] ?? s;
|
||||
return AGENT_EXEC_RESULTS[key as AgentExecResult]?.intent ?? 'muted';
|
||||
}
|
||||
|
||||
export function getAgentExecResultLabel(
|
||||
s: string,
|
||||
t: (k: string, opts?: { defaultValue?: string }) => string,
|
||||
lang: 'ko' | 'en' = 'ko',
|
||||
): string {
|
||||
const map: Record<string, AgentExecResult> = { '차단': 'BLOCKED', '조건부 승인': 'CONDITIONAL', '승인→성공': 'ALLOWED', '정상': 'ALLOWED' };
|
||||
const key = map[s] ?? s;
|
||||
const meta = AGENT_EXEC_RESULTS[key as AgentExecResult];
|
||||
if (!meta) return s;
|
||||
return t(meta.i18nKey, { defaultValue: meta.fallback[lang] });
|
||||
}
|
||||
@ -39,6 +39,8 @@ import {
|
||||
} from './vesselAnalysisStatuses';
|
||||
import { CONNECTION_STATUSES } from './connectionStatuses';
|
||||
import { TRAINING_ZONE_TYPES } from './trainingZoneTypes';
|
||||
import { MLOPS_JOB_STATUSES } from './mlopsJobStatuses';
|
||||
import { THREAT_LEVELS, AGENT_PERM_TYPES, AGENT_EXEC_RESULTS } from './aiSecurityStatuses';
|
||||
|
||||
/**
|
||||
* 카탈로그 공통 메타 — 쇼케이스 렌더와 UI 일관성을 위한 최소 스키마
|
||||
@ -259,6 +261,42 @@ export const CATALOG_REGISTRY: CatalogEntry[] = [
|
||||
description: 'NAVY / AIRFORCE / ARMY / ADD / KCG',
|
||||
items: TRAINING_ZONE_TYPES,
|
||||
},
|
||||
{
|
||||
id: 'mlops-job-status',
|
||||
showcaseId: 'TRK-CAT-mlops-job-status',
|
||||
titleKo: 'MLOps Job 상태',
|
||||
titleEn: 'MLOps Job Status',
|
||||
description: 'running / done / fail / pending',
|
||||
source: 'LGCNS MLOps 파이프라인',
|
||||
items: MLOPS_JOB_STATUSES,
|
||||
},
|
||||
{
|
||||
id: 'threat-level',
|
||||
showcaseId: 'TRK-CAT-threat-level',
|
||||
titleKo: 'AI 위협 수준',
|
||||
titleEn: 'AI Threat Level',
|
||||
description: 'HIGH / MEDIUM / LOW',
|
||||
source: 'AI 보안 (SER-10)',
|
||||
items: THREAT_LEVELS,
|
||||
},
|
||||
{
|
||||
id: 'agent-perm-type',
|
||||
showcaseId: 'TRK-CAT-agent-perm-type',
|
||||
titleKo: 'Agent 권한 유형',
|
||||
titleEn: 'Agent Permission Type',
|
||||
description: 'ADMIN / DELETE / WRITE / READ / EXECUTE',
|
||||
source: 'AI Agent 보안 (SER-11)',
|
||||
items: AGENT_PERM_TYPES,
|
||||
},
|
||||
{
|
||||
id: 'agent-exec-result',
|
||||
showcaseId: 'TRK-CAT-agent-exec-result',
|
||||
titleKo: 'Agent 실행 결과',
|
||||
titleEn: 'Agent Execution Result',
|
||||
description: 'BLOCKED / CONDITIONAL / ALLOWED',
|
||||
source: 'AI Agent 보안 (SER-11)',
|
||||
items: AGENT_EXEC_RESULTS,
|
||||
},
|
||||
];
|
||||
|
||||
/** ID로 특정 카탈로그 조회 */
|
||||
|
||||
46
frontend/src/shared/constants/mlopsJobStatuses.ts
Normal file
46
frontend/src/shared/constants/mlopsJobStatuses.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* MLOps Job / 워크플로우 / 파이프라인 상태 카탈로그
|
||||
*
|
||||
* 사용처: LGCNSMLOpsPage
|
||||
*/
|
||||
|
||||
import type { BadgeIntent } from '@lib/theme/variants';
|
||||
|
||||
export type MlopsJobStatus = 'running' | 'done' | 'fail' | 'pending';
|
||||
|
||||
export const MLOPS_JOB_STATUSES: Record<MlopsJobStatus, { i18nKey: string; fallback: { ko: string; en: string }; intent: BadgeIntent }> = {
|
||||
running: {
|
||||
i18nKey: 'mlopsJob.running',
|
||||
fallback: { ko: '실행중', en: 'Running' },
|
||||
intent: 'info',
|
||||
},
|
||||
done: {
|
||||
i18nKey: 'mlopsJob.done',
|
||||
fallback: { ko: '완료', en: 'Done' },
|
||||
intent: 'success',
|
||||
},
|
||||
fail: {
|
||||
i18nKey: 'mlopsJob.fail',
|
||||
fallback: { ko: '실패', en: 'Failed' },
|
||||
intent: 'critical',
|
||||
},
|
||||
pending: {
|
||||
i18nKey: 'mlopsJob.pending',
|
||||
fallback: { ko: '대기', en: 'Pending' },
|
||||
intent: 'muted',
|
||||
},
|
||||
};
|
||||
|
||||
export function getMlopsJobStatusIntent(s: string): BadgeIntent {
|
||||
return MLOPS_JOB_STATUSES[s as MlopsJobStatus]?.intent ?? 'muted';
|
||||
}
|
||||
|
||||
export function getMlopsJobStatusLabel(
|
||||
s: string,
|
||||
t: (k: string, opts?: { defaultValue?: string }) => string,
|
||||
lang: 'ko' | 'en' = 'ko',
|
||||
): string {
|
||||
const meta = MLOPS_JOB_STATUSES[s as MlopsJobStatus];
|
||||
if (!meta) return s;
|
||||
return t(meta.i18nKey, { defaultValue: meta.fallback[lang] });
|
||||
}
|
||||
@ -27,6 +27,7 @@ const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
|
||||
'성공': 'success',
|
||||
'통과': 'success',
|
||||
'배포': 'success',
|
||||
'허용': 'success',
|
||||
active: 'success',
|
||||
running: 'info',
|
||||
online: 'success',
|
||||
@ -73,6 +74,7 @@ const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
|
||||
'수정': 'warning',
|
||||
'변경': 'warning',
|
||||
'점검': 'warning',
|
||||
'중지': 'warning',
|
||||
warning: 'warning',
|
||||
WARNING: 'warning',
|
||||
review: 'warning',
|
||||
@ -88,6 +90,7 @@ const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
|
||||
'정지': 'critical',
|
||||
'거부': 'critical',
|
||||
'폐기': 'critical',
|
||||
'위험': 'critical',
|
||||
'만료': 'critical',
|
||||
critical: 'critical',
|
||||
CRITICAL: 'critical',
|
||||
@ -104,6 +107,7 @@ const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
|
||||
// 높음
|
||||
'높음': 'high',
|
||||
'높은': 'high',
|
||||
'관리자': 'high',
|
||||
high: 'high',
|
||||
HIGH: 'high',
|
||||
|
||||
@ -111,6 +115,7 @@ const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
|
||||
'분석': 'purple',
|
||||
'학습': 'purple',
|
||||
'추론': 'purple',
|
||||
'실행': 'purple',
|
||||
'배포중': 'purple',
|
||||
analyzing: 'purple',
|
||||
training: 'purple',
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user