Merge pull request 'refactor(frontend): LGCNS 3개 페이지 디자인 시스템 공통 구조 전환' (#33) from feature/lgcns-design-system-align into develop

This commit is contained in:
htlee 2026-04-13 11:49:11 +09:00
커밋 ce01c2134e
8개의 변경된 파일362개의 추가작업 그리고 98개의 파일을 삭제

파일 보기

@ -4,6 +4,14 @@
## [Unreleased] ## [Unreleased]
### 변경
- **LGCNS 3개 페이지 디자인 시스템 전환** — LGCNSMLOps/AISecurityPage/AIAgentSecurityPage 공통 구조 적용
- 커스텀 탭 → TabBar/TabButton 공통 컴포넌트 교체
- hex 색상 맵 → Tailwind 토큰, `style={{ }}` 인라인 제거
- 인라인 Badge intent 삼항 → 카탈로그 함수 교체 (getAgentPermTypeIntent 등)
- 신규 카탈로그 4종: MLOps Job 상태, AI 위협 수준, Agent 권한 유형, Agent 실행 결과
- catalogRegistry 등록 → design-system.html 쇼케이스 자동 노출
## [2026-04-13] ## [2026-04-13]
### 추가 ### 추가

파일 보기

@ -1,12 +1,15 @@
import { useState } from 'react'; import { useState } from 'react';
import { Card, CardContent } from '@shared/components/ui/card'; import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge'; import { Badge } from '@shared/components/ui/badge';
import { TabBar, TabButton } from '@shared/components/ui/tabs';
import { PageContainer, PageHeader } from '@shared/components/layout'; import { PageContainer, PageHeader } from '@shared/components/layout';
import { getStatusIntent } from '@shared/constants/statusIntent'; import { getStatusIntent } from '@shared/constants/statusIntent';
import { getAgentPermTypeIntent, getThreatLevelIntent, getAgentExecResultIntent } from '@shared/constants/aiSecurityStatuses';
import { import {
Bot, Shield, Lock, Eye, Activity, AlertTriangle, Bot, Activity, AlertTriangle,
CheckCircle, FileText, Settings, Terminal, Users, CheckCircle, FileText,
Key, Layers, Workflow, Hand, Users,
Key, Layers, Hand,
} from 'lucide-react'; } from 'lucide-react';
/* /*
@ -19,6 +22,15 @@ import {
type Tab = 'overview' | 'whitelist' | 'killswitch' | 'identity' | 'mcp' | 'audit'; 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 현황 ────────────────── // ─── Agent 현황 ──────────────────
const AGENTS = [ const AGENTS = [
{ name: '위험도 분석 Agent', type: '조회 전용', tools: 4, status: '활성', calls24h: 1240, lastCall: '04-10 09:28' }, { name: '위험도 분석 Agent', type: '조회 전용', tools: 4, status: '활성', calls24h: 1240, lastCall: '04-10 09:28' },
@ -29,11 +41,11 @@ const AGENTS = [
]; ];
const AGENT_KPI = [ const AGENT_KPI = [
{ label: '활성 Agent', value: '4', color: '#10b981' }, { label: '활성 Agent', value: '4', color: 'text-green-400', bg: 'bg-green-500/10' },
{ label: '등록 Tool', value: '26', color: '#3b82f6' }, { label: '등록 Tool', value: '26', color: 'text-blue-400', bg: 'bg-blue-500/10' },
{ label: '24h 호출', value: '2,656', color: '#8b5cf6' }, { label: '24h 호출', value: '2,656', color: 'text-purple-400', bg: 'bg-purple-500/10' },
{ label: '차단 건수', value: '3', color: '#ef4444' }, { label: '차단 건수', value: '3', color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: '승인 대기', value: '0', color: '#f59e0b' }, { 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"> <TabBar variant="underline">
{([ {TABS.map(tt => (
{ key: 'overview' as Tab, icon: Activity, label: 'Agent 현황' }, <TabButton key={tt.key} active={tab === tt.key} icon={<tt.icon className="w-3.5 h-3.5" />} onClick={() => setTab(tt.key)}>
{ key: 'whitelist' as Tab, icon: CheckCircle, label: '화이트리스트 도구' }, {tt.label}
{ key: 'killswitch' as Tab, icon: AlertTriangle, label: '자동 중단·승인' }, </TabButton>
{ 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>
))} ))}
</div> </TabBar>
{/* ── ① Agent 현황 ── */} {/* ── ① Agent 현황 ── */}
{tab === 'overview' && ( {tab === 'overview' && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex gap-2"> <div className="flex gap-2">
{AGENT_KPI.map(k => ( {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 key={k.label} className={`flex-1 px-4 py-3 rounded-xl border border-border ${k.bg}`}>
<div className="text-xl font-bold" style={{ color: k.color }}>{k.value}</div> <div className={`text-xl font-bold ${k.color}`}>{k.value}</div>
<div className="text-[9px] text-hint">{k.label}</div> <div className="text-[9px] text-hint">{k.label}</div>
</div> </div>
))} ))}
@ -143,7 +147,7 @@ export function AIAgentSecurityPage() {
<tbody>{AGENTS.map(a => ( <tbody>{AGENTS.map(a => (
<tr key={a.name} className="border-b border-border/50 hover:bg-surface-overlay transition-colors"> <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 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-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.calls24h.toLocaleString()}</td>
<td className="py-2 text-center text-muted-foreground">{a.lastCall}</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 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-hint">{t.desc}</td>
<td className="py-2 text-center text-muted-foreground">{t.agent}</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-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> </tr>
))}</tbody> ))}</tbody>
</table> </table>
@ -210,7 +214,7 @@ export function AIAgentSecurityPage() {
<tbody>{SENSITIVE_COMMANDS.map(c => ( <tbody>{SENSITIVE_COMMANDS.map(c => (
<tr key={c.command} className="border-b border-border/50 hover:bg-surface-overlay transition-colors"> <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 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-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">{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> <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-hint text-[9px]">{l.chain}</td>
<td className="py-2 text-center text-heading">{l.agent}</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-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> <td className="py-2 px-2 text-right text-muted-foreground">{l.latency}</td>
</tr> </tr>
))}</tbody> ))}</tbody>

파일 보기

@ -1,12 +1,13 @@
import { useState } from 'react'; import { useState } from 'react';
import { Card, CardContent } from '@shared/components/ui/card'; import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge'; import { Badge } from '@shared/components/ui/badge';
import { TabBar, TabButton } from '@shared/components/ui/tabs';
import { PageContainer, PageHeader } from '@shared/components/layout'; import { PageContainer, PageHeader } from '@shared/components/layout';
import { getStatusIntent } from '@shared/constants/statusIntent'; import { getStatusIntent } from '@shared/constants/statusIntent';
import { import {
Shield, Database, Brain, Lock, Eye, Activity, Shield, Database, Brain, Lock, Eye, Activity,
AlertTriangle, CheckCircle, XCircle, FileText, AlertTriangle, CheckCircle,
Server, Layers, Settings, Search, RefreshCw, Server, Search, RefreshCw,
} from 'lucide-react'; } from 'lucide-react';
/* /*
@ -19,13 +20,22 @@ import {
type Tab = 'overview' | 'data' | 'training' | 'io' | 'boundary' | 'vulnerability'; 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 ────────────────── // ─── 보안 현황 KPI ──────────────────
const SECURITY_KPI = [ const SECURITY_KPI = [
{ label: '데이터 수집 보안', value: '정상', score: 95, icon: Database, color: '#10b981' }, { label: '데이터 수집 보안', value: '정상', score: 95, icon: Database, color: 'text-green-400', bg: 'bg-green-500/10' },
{ label: 'AI 학습 보안', value: '정상', score: 92, icon: Brain, color: '#3b82f6' }, { label: 'AI 학습 보안', value: '정상', score: 92, icon: Brain, color: 'text-blue-400', bg: 'bg-blue-500/10' },
{ label: '입출력 필터링', value: '정상', score: 98, icon: Lock, color: '#8b5cf6' }, { label: '입출력 필터링', value: '정상', score: 98, icon: Lock, color: 'text-purple-400', bg: 'bg-purple-500/10' },
{ label: '경계 보안', value: '주의', score: 85, icon: Server, color: '#f59e0b' }, { label: '경계 보안', value: '주의', score: 85, icon: Server, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
{ label: '취약점 점검', value: '정상', score: 90, icon: Search, color: '#06b6d4' }, { 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"> <TabBar variant="underline">
{([ {TABS.map(tt => (
{ key: 'overview' as Tab, icon: Activity, label: '보안 현황' }, <TabButton
{ key: 'data' as Tab, icon: Database, label: '데이터 수집 보안' }, key={tt.key}
{ key: 'training' as Tab, icon: Brain, label: 'AI 학습 보안' }, active={tab === tt.key}
{ key: 'io' as Tab, icon: Lock, label: '입출력 보안' }, icon={<tt.icon className="w-3.5 h-3.5" />}
{ key: 'boundary' as Tab, icon: Server, label: '경계 보안' }, onClick={() => setTab(tt.key)}
{ key: 'vulnerability' as Tab, icon: Search, label: '취약점 점검' }, >
]).map(t => ( {tt.label}
<button type="button" key={t.key} onClick={() => setTab(t.key)} </TabButton>
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>
))} ))}
</div> </TabBar>
{/* ── ① 보안 현황 ── */} {/* ── ① 보안 현황 ── */}
{tab === 'overview' && ( {tab === 'overview' && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex gap-2"> <div className="flex gap-2">
{SECURITY_KPI.map(k => ( {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 }}> <div key={k.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
<k.icon className="w-5 h-5" style={{ color: k.color }} /> <div className={`p-1.5 rounded-lg ${k.bg}`}>
<div> <k.icon className={`w-3.5 h-3.5 ${k.color}`} />
<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> </div>
<span className={`text-base font-bold ${k.color}`}>{k.score}</span>
<span className="text-[9px] text-hint">{k.label}</span>
</div> </div>
))} ))}
</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 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 font-mono">{v.version}</td>
<td className="py-2 text-center text-muted-foreground">{v.lastScan}</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> <td className="py-2 text-center"><Badge intent={getStatusIntent(v.status)} size="sm">{v.status}</Badge></td>
</tr> </tr>
))}</tbody> ))}</tbody>

파일 보기

@ -2,8 +2,11 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Card, CardContent } from '@shared/components/ui/card'; import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge'; import { Badge } from '@shared/components/ui/badge';
import { TabBar, TabButton } from '@shared/components/ui/tabs';
import { PageContainer, PageHeader } from '@shared/components/layout'; import { PageContainer, PageHeader } from '@shared/components/layout';
import { getStatusIntent } from '@shared/constants/statusIntent'; import { getStatusIntent } from '@shared/constants/statusIntent';
import { getMlopsJobStatusIntent, getMlopsJobStatusLabel } from '@shared/constants/mlopsJobStatuses';
import { getModelStatusIntent, getModelStatusLabel } from '@shared/constants/modelDeploymentStatuses';
import { import {
Brain, Database, GitBranch, Activity, Server, Shield, Brain, Database, GitBranch, Activity, Server, Shield,
Settings, Layers, BarChart3, Code, Play, Settings, Layers, BarChart3, Code, Play,
@ -21,6 +24,23 @@ import {
type Tab = 'project' | 'environment' | 'model' | 'job' | 'common' | 'monitoring' | 'repository'; 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 = [ const PROJECTS = [
{ id: 'PRJ-001', name: '불법조업 위험도 예측', owner: '분석팀', status: '활성', models: 5, experiments: 12, updated: '2026-04-10' }, { id: 'PRJ-001', name: '불법조업 위험도 예측', owner: '분석팀', status: '활성', models: 5, experiments: 12, updated: '2026-04-10' },
@ -96,15 +116,23 @@ const GPU_RESOURCES = [
]; ];
const SYSTEM_METRICS = [ const SYSTEM_METRICS = [
{ label: '실행중 Job', value: '3', color: '#3b82f6' }, { label: '실행중 Job', value: '3', icon: Play, color: 'text-blue-400', bg: 'bg-blue-500/10' },
{ label: '대기중 Job', value: '2', color: '#f59e0b' }, { label: '대기중 Job', value: '2', icon: RefreshCw, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
{ label: 'GPU 사용률', value: '65%', color: '#10b981' }, { label: 'GPU 사용률', value: '65%', icon: Activity, color: 'text-green-400', bg: 'bg-green-500/10' },
{ label: '배포 모델', value: '2', color: '#8b5cf6' }, { label: '배포 모델', value: '2', icon: Brain, color: 'text-purple-400', bg: 'bg-purple-500/10' },
{ label: 'API 호출/s', value: '221', color: '#06b6d4' }, { 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() { 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'); const [tab, setTab] = useState<Tab>('project');
return ( return (
@ -118,22 +146,13 @@ export function LGCNSMLOpsPage() {
/> />
{/* 탭 */} {/* 탭 */}
<div className="flex gap-0 border-b border-border"> <TabBar variant="underline">
{([ {TABS.map(tt => (
{ key: 'project' as Tab, icon: Layers, label: '프로젝트 관리' }, <TabButton key={tt.key} active={tab === tt.key} icon={<tt.icon className="w-3.5 h-3.5" />} onClick={() => setTab(tt.key)}>
{ key: 'environment' as Tab, icon: Terminal, label: '분석환경 관리' }, {tt.label}
{ key: 'model' as Tab, icon: Brain, label: '모델 관리' }, </TabButton>
{ 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>
))} ))}
</div> </TabBar>
{/* ── ① 프로젝트 관리 ── */} {/* ── ① 프로젝트 관리 ── */}
{tab === 'project' && ( {tab === 'project' && (
@ -159,15 +178,13 @@ export function LGCNSMLOpsPage() {
</table> </table>
</CardContent></Card> </CardContent></Card>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
{[ {PROJECT_STATS.map(k => (
{ label: '활성 프로젝트', value: 3, icon: Layers, color: '#3b82f6' }, <div key={k.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
{ label: '총 모델', value: 11, icon: Brain, color: '#8b5cf6' }, <div className={`p-1.5 rounded-lg ${k.bg}`}>
{ label: '총 실험', value: 29, icon: FlaskConical, color: '#10b981' }, <k.icon className={`w-3.5 h-3.5 ${k.color}`} />
{ label: '참여 인원', value: 8, icon: Server, color: '#f59e0b' }, </div>
].map(k => ( <span className={`text-base font-bold ${k.color}`}>{k.value}</span>
<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 }}> <span className="text-[9px] text-hint">{k.label}</span>
<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>
</div> </div>
))} ))}
</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-[11px] text-heading font-medium flex-1">{w.name}</span>
<span className="text-[10px] text-muted-foreground">Steps: {w.steps}</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="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> </div>
<span className="text-[10px] text-muted-foreground">{w.progress}%</span> <span className="text-[10px] text-muted-foreground">{w.progress}%</span>
<Badge intent={getStatusIntent(w.status === 'running' ? '실행중' : w.status === 'done' ? '완료' : '실패')} size="sm"> <Badge intent={getMlopsJobStatusIntent(w.status)} size="sm">
{w.status === 'running' ? '실행중' : w.status === 'done' ? '완료' : '실패'} {getMlopsJobStatusLabel(w.status, t, lang)}
</Badge> </Badge>
</div> </div>
))} ))}
@ -228,7 +245,11 @@ export function LGCNSMLOpsPage() {
<tr key={m.name} className="border-b border-border/50 hover:bg-surface-overlay transition-colors"> <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 px-2 text-heading font-medium">{m.name}</td>
<td className="py-2 text-muted-foreground">{m.framework}</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-heading font-bold">{m.accuracy}%</td>
<td className="py-2 text-center text-green-400">{m.kpi}</td> <td className="py-2 text-center text-green-400">{m.kpi}</td>
<td className="py-2 text-muted-foreground">{m.version}</td> <td className="py-2 text-muted-foreground">{m.version}</td>
@ -294,14 +315,14 @@ export function LGCNSMLOpsPage() {
<td className="py-2"> <td className="py-2">
<div className="flex items-center gap-2 justify-center"> <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="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> </div>
<span className="text-heading">{j.progress}%</span> <span className="text-heading">{j.progress}%</span>
</div> </div>
</td> </td>
<td className="py-2 text-center"> <td className="py-2 text-center">
<Badge intent={getStatusIntent(j.status === 'running' ? '실행중' : j.status === 'done' ? '완료' : '실패')} size="sm"> <Badge intent={getMlopsJobStatusIntent(j.status)} size="sm">
{j.status === 'running' ? '실행중' : j.status === 'done' ? '완료' : '실패'} {getMlopsJobStatusLabel(j.status, t, lang)}
</Badge> </Badge>
</td> </td>
<td className="py-2 px-2 text-right text-muted-foreground">{j.elapsed}</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="space-y-3">
<div className="flex gap-2"> <div className="flex gap-2">
{SYSTEM_METRICS.map(k => ( {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 key={k.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
<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 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>
))} ))}
</div> </div>

파일 보기

@ -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'; } from './vesselAnalysisStatuses';
import { CONNECTION_STATUSES } from './connectionStatuses'; import { CONNECTION_STATUSES } from './connectionStatuses';
import { TRAINING_ZONE_TYPES } from './trainingZoneTypes'; import { TRAINING_ZONE_TYPES } from './trainingZoneTypes';
import { MLOPS_JOB_STATUSES } from './mlopsJobStatuses';
import { THREAT_LEVELS, AGENT_PERM_TYPES, AGENT_EXEC_RESULTS } from './aiSecurityStatuses';
/** /**
* UI * UI
@ -259,6 +261,42 @@ export const CATALOG_REGISTRY: CatalogEntry[] = [
description: 'NAVY / AIRFORCE / ARMY / ADD / KCG', description: 'NAVY / AIRFORCE / ARMY / ADD / KCG',
items: TRAINING_ZONE_TYPES, 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로 특정 카탈로그 조회 */ /** ID로 특정 카탈로그 조회 */

파일 보기

@ -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',
'배포': 'success', '배포': 'success',
'허용': 'success',
active: 'success', active: 'success',
running: 'info', running: 'info',
online: 'success', online: 'success',
@ -73,6 +74,7 @@ const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
'수정': 'warning', '수정': 'warning',
'변경': 'warning', '변경': 'warning',
'점검': 'warning', '점검': 'warning',
'중지': 'warning',
warning: 'warning', warning: 'warning',
WARNING: 'warning', WARNING: 'warning',
review: 'warning', review: 'warning',
@ -88,6 +90,7 @@ const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
'정지': 'critical', '정지': 'critical',
'거부': 'critical', '거부': 'critical',
'폐기': 'critical', '폐기': 'critical',
'위험': 'critical',
'만료': 'critical', '만료': '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', high: 'high',
HIGH: 'high', HIGH: 'high',
@ -111,6 +115,7 @@ const STATUS_INTENT_MAP: Record<string, BadgeIntent> = {
'분석': 'purple', '분석': 'purple',
'학습': 'purple', '학습': 'purple',
'추론': 'purple', '추론': 'purple',
'실행': 'purple',
'배포중': 'purple', '배포중': 'purple',
analyzing: 'purple', analyzing: 'purple',
training: 'purple', training: 'purple',