Merge pull request 'refactor(frontend): LGCNS 3개 페이지 디자인 시스템 공통 구조 전환' (#33) from feature/lgcns-design-system-align into develop
This commit is contained in:
커밋
ce01c2134e
@ -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>
|
||||||
|
|||||||
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';
|
} 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로 특정 카탈로그 조회 */
|
||||||
|
|||||||
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',
|
||||||
'배포': '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',
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user