Merge pull request 'feat: LGCNS MLOps + AI 보안(SER-10) + AI Agent 보안(SER-11) 메뉴 추가' (#30) from feature/lgcns-mlops-ai-security-menu into develop

This commit is contained in:
htlee 2026-04-13 11:13:02 +09:00
커밋 90c270ac53
13개의 변경된 파일1188개의 추가작업 그리고 3개의 파일을 삭제

파일 보기

@ -0,0 +1,76 @@
-- ============================================================
-- V025: LGCNS MLOps + AI 보안(SER-10) + AI Agent 보안(SER-11) 메뉴 추가
-- 시스템관리 > AI 플랫폼 / 감사·보안 서브그룹
-- ============================================================
-- ──────────────────────────────────────────────────────────────
-- 1. LGCNS MLOps (시스템관리 > AI 플랫폼, MLOps와 LLM 사이)
-- ──────────────────────────────────────────────────────────────
INSERT INTO kcg.auth_perm_tree(rsrc_cd, parent_cd, rsrc_nm, rsrc_level, sort_ord)
VALUES ('admin:lgcns-mlops', 'admin', 'LGCNS MLOps', 1, 36)
ON CONFLICT (rsrc_cd) DO NOTHING;
UPDATE kcg.auth_perm_tree
SET url_path = '/lgcns-mlops',
label_key = 'nav.lgcnsMlops',
component_key = 'features/ai-operations/LGCNSMLOpsPage',
nav_group = 'admin',
nav_sub_group = 'AI 플랫폼',
nav_sort = 350,
labels = '{"ko":"LGCNS MLOps","en":"LGCNS MLOps"}'
WHERE rsrc_cd = 'admin:lgcns-mlops';
INSERT INTO kcg.auth_perm(role_sn, rsrc_cd, oper_cd, grant_yn)
SELECT r.role_sn, 'admin:lgcns-mlops', op.oper_cd, 'Y'
FROM kcg.auth_role r
CROSS JOIN (VALUES ('READ'), ('CREATE'), ('UPDATE'), ('DELETE'), ('EXPORT')) AS op(oper_cd)
WHERE r.role_cd = 'ADMIN'
ON CONFLICT (role_sn, rsrc_cd, oper_cd) DO NOTHING;
-- ──────────────────────────────────────────────────────────────
-- 2. AI 보안 (SER-10) (시스템관리 > 감사·보안, 로그인 이력 뒤)
-- ──────────────────────────────────────────────────────────────
INSERT INTO kcg.auth_perm_tree(rsrc_cd, parent_cd, rsrc_nm, rsrc_level, sort_ord)
VALUES ('admin:ai-security', 'admin', 'AI 보안', 1, 55)
ON CONFLICT (rsrc_cd) DO NOTHING;
UPDATE kcg.auth_perm_tree
SET url_path = '/admin/ai-security',
label_key = 'nav.aiSecurity',
component_key = 'features/admin/AISecurityPage',
nav_group = 'admin',
nav_sub_group = '감사·보안',
nav_sort = 1800,
labels = '{"ko":"AI 보안","en":"AI Security"}'
WHERE rsrc_cd = 'admin:ai-security';
INSERT INTO kcg.auth_perm(role_sn, rsrc_cd, oper_cd, grant_yn)
SELECT r.role_sn, 'admin:ai-security', op.oper_cd, 'Y'
FROM kcg.auth_role r
CROSS JOIN (VALUES ('READ'), ('CREATE'), ('UPDATE'), ('DELETE'), ('EXPORT')) AS op(oper_cd)
WHERE r.role_cd = 'ADMIN'
ON CONFLICT (role_sn, rsrc_cd, oper_cd) DO NOTHING;
-- ──────────────────────────────────────────────────────────────
-- 3. AI Agent 보안 (SER-11) (시스템관리 > 감사·보안, AI 보안 뒤)
-- ──────────────────────────────────────────────────────────────
INSERT INTO kcg.auth_perm_tree(rsrc_cd, parent_cd, rsrc_nm, rsrc_level, sort_ord)
VALUES ('admin:ai-agent-security', 'admin', 'AI Agent 보안', 1, 56)
ON CONFLICT (rsrc_cd) DO NOTHING;
UPDATE kcg.auth_perm_tree
SET url_path = '/admin/ai-agent-security',
label_key = 'nav.aiAgentSecurity',
component_key = 'features/admin/AIAgentSecurityPage',
nav_group = 'admin',
nav_sub_group = '감사·보안',
nav_sort = 1900,
labels = '{"ko":"AI Agent 보안","en":"AI Agent Security"}'
WHERE rsrc_cd = 'admin:ai-agent-security';
INSERT INTO kcg.auth_perm(role_sn, rsrc_cd, oper_cd, grant_yn)
SELECT r.role_sn, 'admin:ai-agent-security', op.oper_cd, 'Y'
FROM kcg.auth_role r
CROSS JOIN (VALUES ('READ'), ('CREATE'), ('UPDATE'), ('DELETE'), ('EXPORT')) AS op(oper_cd)
WHERE r.role_cd = 'ADMIN'
ON CONFLICT (role_sn, rsrc_cd, oper_cd) DO NOTHING;

파일 보기

@ -4,6 +4,12 @@
## [Unreleased]
### 추가
- **LGCNS MLOps 메뉴** — 시스템관리 > AI 플랫폼 하위, 모델 레지스트리/학습 파이프라인/서빙 현황/모델 모니터링 탭 구성
- **AI 보안(SER-10) 메뉴** — 시스템관리 > 감사·보안 하위, AI 모델 보안 감사/Adversarial 공격 탐지/데이터 무결성 검증/보안 이벤트 타임라인
- **AI Agent 보안(SER-11) 메뉴** — 시스템관리 > 감사·보안 하위, 에이전트 실행 로그/정책 위반 탐지/자원 사용 모니터링/신뢰도 대시보드
- **V025 마이그레이션** — auth_perm_tree에 admin:lgcns-mlops, admin:ai-security, admin:ai-agent-security 노드 + ADMIN 역할 CRUD 권한 시드
## [2026-04-09.2]
### 추가

파일 보기

@ -1,5 +1,5 @@
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
import { fetchMe, loginApi, logoutApi, LoginError, type BackendUser } from '@/services/authApi';
import { fetchMe, loginApi, logoutApi, LoginError, type BackendUser, type MenuConfigItem } from '@/services/authApi';
import { useMenuStore } from '@stores/menuStore';
/*

파일 보기

@ -80,6 +80,9 @@ export const COMPONENT_REGISTRY: Record<string, LazyComponent> = {
'features/ai-operations/MLOpsPage': lazy(() =>
import('@features/ai-operations').then((m) => ({ default: m.MLOpsPage })),
),
'features/ai-operations/LGCNSMLOpsPage': lazy(() =>
import('@features/ai-operations').then((m) => ({ default: m.LGCNSMLOpsPage })),
),
'features/ai-operations/LLMOpsPage': lazy(() =>
import('@features/ai-operations').then((m) => ({ default: m.LLMOpsPage })),
),
@ -113,6 +116,12 @@ export const COMPONENT_REGISTRY: Record<string, LazyComponent> = {
default: m.LoginHistoryView,
})),
),
'features/admin/AISecurityPage': lazy(() =>
import('@features/admin').then((m) => ({ default: m.AISecurityPage })),
),
'features/admin/AIAgentSecurityPage': lazy(() =>
import('@features/admin').then((m) => ({ default: m.AIAgentSecurityPage })),
),
// ── 모선 워크플로우 ──
'features/parent-inference/ParentReview': lazy(() =>
import('@features/parent-inference/ParentReview').then((m) => ({

파일 보기

@ -0,0 +1,295 @@
import { useState } from 'react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { getStatusIntent } from '@shared/constants/statusIntent';
import {
Bot, Shield, Lock, Eye, Activity, AlertTriangle,
CheckCircle, FileText, Settings, Terminal, Users,
Key, Layers, Workflow, Hand,
} from 'lucide-react';
/*
* SER-11: AI Agent
*
* AI Agent :
* Agent ·
* · MCP Tool
*/
type Tab = 'overview' | 'whitelist' | 'killswitch' | 'identity' | 'mcp' | 'audit';
// ─── Agent 현황 ──────────────────
const AGENTS = [
{ name: '위험도 분석 Agent', type: '조회 전용', tools: 4, status: '활성', calls24h: 1240, lastCall: '04-10 09:28' },
{ name: '법령 Q&A Agent', type: '조회 전용', tools: 3, status: '활성', calls24h: 856, lastCall: '04-10 09:25' },
{ name: '단속 이력 Agent', type: '조회 전용', tools: 5, status: '활성', calls24h: 432, lastCall: '04-10 09:20' },
{ name: '모선 추론 Agent', type: '조회+쓰기', tools: 6, status: '활성', calls24h: 128, lastCall: '04-10 09:15' },
{ name: '데이터 관리 Agent', type: '관리자', tools: 8, status: '대기', calls24h: 0, lastCall: '04-09 16:00' },
];
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' },
];
// ─── 화이트리스트 도구 ──────────────────
const WHITELIST_TOOLS = [
{ tool: 'db_read_vessel', agent: '위험도 분석', permission: 'READ', mcp: 'kcg-db-server', status: '허용', desc: '선박 정보 조회' },
{ tool: 'db_read_analysis', agent: '위험도 분석', permission: 'READ', mcp: 'kcg-db-server', status: '허용', desc: '분석 결과 조회' },
{ tool: 'search_law', agent: '법령 Q&A', permission: 'READ', mcp: 'kcg-rag-server', status: '허용', desc: '법령·판례 검색' },
{ tool: 'search_cases', agent: '법령 Q&A', permission: 'READ', mcp: 'kcg-rag-server', status: '허용', desc: '유사 사례 검색' },
{ tool: 'read_enforcement', agent: '단속 이력', permission: 'READ', mcp: 'kcg-db-server', status: '허용', desc: '단속 이력 조회' },
{ tool: 'write_parent_result', agent: '모선 추론', permission: 'CREATE', mcp: 'kcg-db-server', status: '허용', desc: '모선 추론 결과 저장' },
{ tool: 'update_parent_status', agent: '모선 추론', permission: 'UPDATE', mcp: 'kcg-db-server', status: '허용', desc: '모선 상태 갱신' },
{ tool: 'db_delete_any', agent: '-', permission: 'DELETE', mcp: '-', status: '차단', desc: 'DB 삭제 (금지 도구)' },
{ tool: 'system_exec', agent: '-', permission: 'ADMIN', mcp: '-', status: '차단', desc: '시스템 명령 실행 (금지)' },
];
// ─── 자동 중단 설정 ──────────────────
const KILL_SWITCH_RULES = [
{ rule: '유해·금지행위 탐지', desc: '유해·금지행위, 잘못된 목표 설정 시 Agent 자동 중단', threshold: '즉시', status: '활성' },
{ rule: '자원 소비 임계값 초과', desc: 'GPU/메모리/API 호출 임계값 초과 시 자동 중단', threshold: 'GPU 90% / 메모리 85%', status: '활성' },
{ rule: '이상 호출 패턴', desc: '비정상적으로 빈번한 Tool 호출 탐지', threshold: '100회/분', status: '활성' },
{ rule: '응답 시간 초과', desc: 'Agent 응답 타임아웃', threshold: '30초', status: '활성' },
];
// ─── 민감명령 승인 ──────────────────
const SENSITIVE_COMMANDS = [
{ command: 'DB 데이터 수정/삭제', level: '높음', approval: '담당자 승인 필수', hitl: true, status: '적용' },
{ command: '모델 배포/롤백', level: '높음', approval: '담당자 승인 필수', hitl: true, status: '적용' },
{ command: '사용자 권한 변경', level: '높음', approval: '관리자 승인 필수', hitl: true, status: '적용' },
{ command: '외부 시스템 연계 호출', level: '중간', approval: '자동 승인 (로그)', hitl: false, status: '적용' },
{ command: '분석 결과 조회', level: '낮음', approval: '자동 승인', hitl: false, status: '적용' },
];
// ─── 에이전트 신원 확인 ──────────────────
const IDENTITY_POLICIES = [
{ policy: '미승인 권한 위임 차단', desc: '명시적으로 승인되지 않은 AI 에이전트로 권한 위임 제한', status: '적용' },
{ policy: 'Agent 간 신원 확인', desc: '협업할 AI 에이전트가 적합한 인증 혹은 신원 보유 중인지 상호 검증', status: '적용' },
{ policy: 'Agent 인증 방식', desc: 'Agent 간 인증은 표준 방식으로 제안, 발주처 간 협의를 통해 최종 선정', status: '적용' },
{ policy: '과도한 호출 방지', desc: 'AI 에이전트의 과도한 호출로 인한 레거시 시스템 마비 방지', status: '적용' },
];
// ─── MCP Tool 권한 ──────────────────
const MCP_PERMISSIONS = [
{ server: 'kcg-db-server', tools: 8, principle: '최소 권한', detail: '조회 Agent는 DB READ만, 쓰기 Agent는 특정 테이블 C/U만', status: '적용' },
{ server: 'kcg-rag-server', tools: 5, principle: '최소 권한', detail: 'RAG 검색만 허용, 인덱스 수정 불가', status: '적용' },
{ server: 'kcg-api-server', tools: 6, principle: '최소 권한', detail: '외부 API 호출은 Rate Limiting + Caching', status: '적용' },
{ server: 'kcg-notify-server', tools: 3, principle: '최소 권한', detail: '알림 발송만 허용, 수신자 목록 수정 불가', status: '적용' },
];
// ─── 감사 로그 ──────────────────
const AUDIT_LOG_SAMPLE = [
{ time: '09:28:15', chain: 'User → LLM → MCP Client → kcg-db-server → DB', agent: '위험도 분석', tool: 'db_read_vessel', result: '성공', latency: '120ms' },
{ time: '09:25:42', chain: 'User → LLM → MCP Client → kcg-rag-server → Milvus', agent: '법령 Q&A', tool: 'search_law', result: '성공', latency: '850ms' },
{ time: '09:20:10', chain: 'User → LLM → MCP Client → kcg-db-server → DB', agent: '단속 이력', tool: 'read_enforcement', result: '성공', latency: '95ms' },
{ time: '09:15:33', chain: 'User → LLM → MCP Client → kcg-db-server → DB', agent: '모선 추론', tool: 'write_parent_result', result: '승인→성공', latency: '230ms' },
{ time: '08:42:00', chain: 'User → LLM → Kill Switch', agent: '데이터 관리', tool: 'db_delete_any', result: '차단', latency: '-' },
];
export function AIAgentSecurityPage() {
const [tab, setTab] = useState<Tab>('overview');
return (
<PageContainer>
<PageHeader
icon={Bot}
iconColor="text-orange-400"
title="AI Agent 구축·운영 보안"
description="SER-11 | AI Agent 화이트리스트·자동중단·민감명령 승인·MCP 최소권한·감사로그"
demo
/>
{/* 탭 */}
<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>
))}
</div>
{/* ── ① 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 className="text-[9px] text-hint">{k.label}</div>
</div>
))}
</div>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> Agent </div>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2">Agent명</th><th className="text-center py-2"></th>
<th className="text-center py-2"></th><th className="text-center py-2">24h </th>
<th className="text-center py-2"> </th><th className="text-center py-2"></th>
</tr></thead>
<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 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>
<td className="py-2 text-center"><Badge intent={getStatusIntent(a.status)} size="sm">{a.status}</Badge></td>
</tr>
))}</tbody>
</table>
</CardContent></Card>
</div>
)}
{/* ── ② 화이트리스트 도구 ── */}
{tab === 'whitelist' && (
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<p className="text-[10px] text-hint mb-3">AI가 · </p>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2">Tool ID</th><th className="text-left py-2"></th>
<th className="text-center py-2">Agent</th><th className="text-center py-2"></th>
<th className="text-left py-2">MCP Server</th><th className="text-center py-2"></th>
</tr></thead>
<tbody>{WHITELIST_TOOLS.map(t => (
<tr key={t.tool} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<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-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>
</tr>
))}</tbody>
</table>
</CardContent></Card>
)}
{/* ── ③ 자동 중단·민감명령 승인 ── */}
{tab === 'killswitch' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">AI (Kill Switch)</div>
<div className="space-y-2">
{KILL_SWITCH_RULES.map(r => (
<div key={r.rule} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<AlertTriangle className="w-4 h-4 text-red-400" />
<div className="flex-1">
<div className="text-[11px] text-heading font-medium">{r.rule}</div>
<div className="text-[9px] text-hint">{r.desc}</div>
</div>
<span className="text-[9px] text-muted-foreground">: {r.threshold}</span>
<Badge intent={getStatusIntent(r.status)} size="sm">{r.status}</Badge>
</div>
))}
</div>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> (Human-in-the-loop)</div>
<p className="text-[10px] text-hint mb-3">· </p>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"> </th><th className="text-center py-2"></th>
<th className="text-left py-2"> </th><th className="text-center py-2">HITL</th><th className="text-center py-2"></th>
</tr></thead>
<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-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>
</tr>
))}</tbody>
</table>
</CardContent></Card>
</div>
)}
{/* ── ④ Agent 신원·권한 ── */}
{tab === 'identity' && (
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<div className="space-y-2">
{IDENTITY_POLICIES.map(p => (
<div key={p.policy} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Key className="w-4 h-4 text-orange-400" />
<div className="flex-1">
<div className="text-[11px] text-heading font-medium">{p.policy}</div>
<div className="text-[9px] text-hint">{p.desc}</div>
</div>
<Badge intent="success" size="sm">{p.status}</Badge>
</div>
))}
</div>
</CardContent></Card>
)}
{/* ── ⑤ MCP Tool 권한 ── */}
{tab === 'mcp' && (
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">MCP Tool </div>
<p className="text-[10px] text-hint mb-3">MCP를 (Tool) (: 조회 Agent는 DB Update MCP Tool )</p>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2">MCP Server</th><th className="text-center py-2">Tool </th>
<th className="text-center py-2"></th><th className="text-left py-2"></th><th className="text-center py-2"></th>
</tr></thead>
<tbody>{MCP_PERMISSIONS.map(m => (
<tr key={m.server} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2.5 px-2 text-heading font-mono text-[9px]">{m.server}</td>
<td className="py-2.5 text-center text-heading">{m.tools}</td>
<td className="py-2.5 text-center"><Badge intent="info" size="sm">{m.principle}</Badge></td>
<td className="py-2.5 text-hint text-[9px]">{m.detail}</td>
<td className="py-2.5 text-center"><Badge intent="success" size="sm">{m.status}</Badge></td>
</tr>
))}</tbody>
</table>
<div className="mt-4 p-3 bg-surface-overlay rounded-lg">
<div className="text-[10px] text-heading font-medium mb-1">Rate Limiting &amp; Caching</div>
<div className="text-[9px] text-hint">AI , MCP Rate Limiting( ) Caching() </div>
</div>
</CardContent></Card>
)}
{/* ── ⑥ 감사 로그 ── */}
{tab === 'audit' && (
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> (Audit Log)</div>
<p className="text-[10px] text-hint mb-3"> MCP Tool (User Request LLM MCP Client MCP Server Legacy) , A2A </p>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th><th className="text-left py-2"> </th>
<th className="text-center py-2">Agent</th><th className="text-left py-2">Tool</th><th className="text-center py-2"></th><th className="text-right py-2 px-2"></th>
</tr></thead>
<tbody>{AUDIT_LOG_SAMPLE.map(l => (
<tr key={l.time + l.tool} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2 px-2 text-muted-foreground font-mono">{l.time}</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-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 px-2 text-right text-muted-foreground">{l.latency}</td>
</tr>
))}</tbody>
</table>
</CardContent></Card>
)}
</PageContainer>
);
}

파일 보기

@ -0,0 +1,351 @@
import { useState } from 'react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
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,
} from 'lucide-react';
/*
* SER-10: AI
*
* AI :
* AI
*
*/
type Tab = 'overview' | 'data' | 'training' | 'io' | 'boundary' | 'vulnerability';
// ─── 보안 현황 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' },
];
// ─── 데이터 수집 보안 ──────────────────
const DATA_SOURCES = [
{ name: 'AIS 원본 (SNPDB)', provider: '해경', trust: '인증', encryption: 'TLS 1.3', lastAudit: '2026-04-10', status: '정상' },
{ name: 'V-PASS 위치정보', provider: '해수부', trust: '인증', encryption: 'TLS 1.3', lastAudit: '2026-04-09', status: '정상' },
{ name: '기상 데이터', provider: '기상청', trust: '인증', encryption: 'TLS 1.2', lastAudit: '2026-04-08', status: '정상' },
{ name: '위성영상', provider: '해양조사원', trust: '인증', encryption: 'TLS 1.3', lastAudit: '2026-04-07', status: '정상' },
{ name: '법령·판례', provider: '법무부', trust: '인증', encryption: 'TLS 1.2', lastAudit: '2026-04-05', status: '정상' },
{ name: '단속 이력', provider: '해경', trust: '내부', encryption: 'AES-256', lastAudit: '2026-04-10', status: '정상' },
];
const CONTAMINATION_CHECKS = [
{ check: '오염데이터 탐지', desc: 'AI 학습·재학습 시 오염데이터 검사·관리', status: '활성', lastRun: '04-10 09:15' },
{ check: 'RAG 오염 차단', desc: '신규데이터 참조 시 오염데이터 유입 차단·방지', status: '활성', lastRun: '04-10 09:00' },
{ check: '출처 검증', desc: '공신력 있는 출처·배포 정보 구분 제출', status: '활성', lastRun: '04-10 08:30' },
{ check: '악성코드 사전검사', desc: '신뢰 출처 데이터라도 오염 가능, 사전 검사 필요', status: '활성', lastRun: '04-10 08:00' },
];
// ─── AI 학습 보안 ──────────────────
const TRAINING_POLICIES = [
{ policy: '보안등급별 데이터 분류', desc: 'AI시스템 활용목적 및 등급분류에 맞게 기밀·민감·공개등급 데이터 활용', status: '적용' },
{ policy: '사용자별 접근통제', desc: 'AI시스템이 사용자·부서별 권한에 맞는 학습데이터만 사용하도록 세분화', status: '적용' },
{ policy: '비인가자 접근 차단', desc: 'AI가 비인가자에게 기밀·민감등급 데이터를 제공하지 않도록 통제', status: '적용' },
{ policy: '저장소·DB 접근통제', desc: '보관된 학습데이터에 대한 사용자 접근통제', status: '적용' },
{ policy: '최소 접근권한 설계', desc: '사용자, 그룹, 데이터별로 최소 접근권한만 부여하도록 설계', status: '적용' },
{ policy: '다중 보안 인증', desc: '학습데이터 관리자 권한에 대해서는 다중 보안 인증 등 활용 방안 적용', status: '적용' },
{ policy: '오픈소스 모델 신뢰성', desc: '공신력 있는 출처·배포자가 제공하는 AI 모델·라이브러리 사용', status: '적용' },
];
// ─── 입출력 보안 ──────────────────
const IO_FILTERS = [
{ name: '입력 필터링', desc: '민감정보·적대적 공격 문구 포함여부 확인 및 차단', status: '활성', blocked: 23, total: 15420 },
{ name: '출력 필터링', desc: '응답 내 민감정보 노출 차단', status: '활성', blocked: 8, total: 15420 },
{ name: '입력 길이·형식 제한', desc: '공격용 프롬프트 과도 입력 방지', status: '활성', blocked: 5, total: 15420 },
{ name: '요청 속도 제한', desc: '호출 횟수, 동시 처리 요청수, 출력 용량 등 제한', status: '활성', blocked: 12, total: 15420 },
];
// ─── 경계 보안 ──────────────────
const BOUNDARY_ITEMS = [
{ item: 'DMZ·중계서버', desc: 'AI시스템에 접근하는 사용자·시스템 식별 및 통제', status: '적용' },
{ item: '인가 시스템 제한', desc: 'AI시스템이 인가된 내·외부 시스템·데이터만 활용하도록 제한', status: '적용' },
{ item: '권한 부여 제한', desc: 'AI시스템에 과도한 권한 부여 제한', status: '적용' },
{ item: '민감작업 승인절차', desc: '데이터 수정·시스템 제어 등 민감한 작업 수행 시 담당자 검토·승인', status: '적용' },
];
const EXPLAINABILITY = [
{ item: '추론 시각화', desc: '데이터 수정·시스템 제어 시 추론 과정·결과를 설명하거나 판단 근거 시각화', status: '구현' },
{ item: 'Feature Importance', desc: '모델 결정에 영향을 미친 주요 피처 표시', status: '구현' },
{ item: '판단 근거 제공', desc: '위험도 산출 시 기여 요인 설명', status: '구현' },
];
// ─── 취약점 점검 ──────────────────
const VULN_CHECKS = [
{ target: 'AI 모델 서빙 (PyTorch)', version: '2.4.1', lastScan: '2026-04-10', vulns: 0, status: '안전' },
{ target: 'FastAPI 서버', version: '0.115.0', lastScan: '2026-04-10', vulns: 0, status: '안전' },
{ target: 'LangChain (RAG)', version: '0.3.2', lastScan: '2026-04-09', vulns: 1, status: '주의' },
{ target: 'Milvus 벡터DB', version: '2.4.0', lastScan: '2026-04-09', vulns: 0, status: '안전' },
{ target: 'Spring Boot 백엔드', version: '3.4.1', lastScan: '2026-04-10', vulns: 0, status: '안전' },
{ target: 'Node.js (Vite)', version: '22.x', lastScan: '2026-04-10', vulns: 0, status: '안전' },
];
const RECOVERY_PLANS = [
{ plan: '모델 백업 저장소', desc: '이상행위 탐지 시 정상 모델·학습데이터 등으로 복원', status: '활성', detail: 'S3 버전별 백업 24개' },
{ plan: '버전정보 관리', desc: '모델·데이터·설정 버전 이력 추적', status: '활성', detail: 'Git + DVC 연동' },
{ plan: '자동 롤백', desc: '성능 저하 감지 시 이전 안정 버전으로 자동 복구', status: '활성', detail: '임계치 기반 트리거' },
];
export function AISecurityPage() {
const [tab, setTab] = useState<Tab>('overview');
return (
<PageContainer>
<PageHeader
icon={Shield}
iconColor="text-red-400"
title="AI 보안 관리"
description="SER-10 | AI 데이터 수집·학습·입출력·경계 보안 및 취약점 관리"
demo
/>
{/* 탭 */}
<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>
))}
</div>
{/* ── ① 보안 현황 ── */}
{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>
</div>
))}
</div>
<div className="grid grid-cols-2 gap-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<div className="space-y-1.5 text-[10px]">
{[
['데이터 출처 인증', '6/6 소스 인증 완료', '완료'],
['오염데이터 검사', '4/4 검사 활성화', '완료'],
['학습데이터 접근통제', '7/7 정책 적용', '완료'],
['입출력 필터링', '4/4 필터 활성', '완료'],
['경계 보안 설정', '4/4 항목 적용', '완료'],
['취약점 점검', '5/6 안전 (1건 주의)', '주의'],
['복구 계획', '3/3 활성', '완료'],
].map(([k, v, s]) => (
<div key={k} className="flex items-center gap-2 px-2 py-1.5 bg-surface-overlay rounded">
{s === '완료' ? <CheckCircle className="w-3 h-3 text-green-500" /> : <AlertTriangle className="w-3 h-3 text-yellow-500" />}
<span className="text-heading flex-1">{k}</span>
<span className="text-hint">{v}</span>
</div>
))}
</div>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<div className="space-y-1.5 text-[10px]">
{[
['04-10 09:15', '오염데이터 검사 완료', '정상', '0건 탐지'],
['04-10 08:42', '입력 필터링 차단', '경고', '공격 패턴 1건 차단'],
['04-09 14:30', '취약점 스캔 완료', '주의', 'LangChain CVE-2026-1234'],
['04-09 10:00', '학습데이터 접근 감사', '정상', '비정상 접근 0건'],
['04-08 16:00', '모델 백업 완료', '정상', 'v2.1.0 → S3'],
['04-08 09:00', 'RAG 오염 차단 검사', '정상', '0건 탐지'],
].map(([time, event, level, detail]) => (
<div key={time + event} className="flex items-center gap-2 px-2 py-1.5 bg-surface-overlay rounded">
<span className="text-muted-foreground w-24">{time}</span>
<span className="text-heading flex-1">{event}</span>
<Badge intent={getStatusIntent(level)} size="sm">{level}</Badge>
<span className="text-hint text-[9px]">{detail}</span>
</div>
))}
</div>
</CardContent></Card>
</div>
</div>
)}
{/* ── ② 데이터 수집 보안 ── */}
{tab === 'data' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"> </th><th className="text-left py-2"></th>
<th className="text-center py-2"></th><th className="text-center py-2"></th>
<th className="text-center py-2"> </th><th className="text-center py-2"></th>
</tr></thead>
<tbody>{DATA_SOURCES.map(d => (
<tr key={d.name} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2 px-2 text-heading font-medium">{d.name}</td>
<td className="py-2 text-muted-foreground">{d.provider}</td>
<td className="py-2 text-center"><Badge intent="success" size="sm">{d.trust}</Badge></td>
<td className="py-2 text-center text-muted-foreground">{d.encryption}</td>
<td className="py-2 text-center text-muted-foreground">{d.lastAudit}</td>
<td className="py-2 text-center"><Badge intent={getStatusIntent(d.status)} size="sm">{d.status}</Badge></td>
</tr>
))}</tbody>
</table>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<div className="space-y-2">
{CONTAMINATION_CHECKS.map(c => (
<div key={c.check} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<CheckCircle className="w-4 h-4 text-green-500" />
<div className="flex-1">
<div className="text-[11px] text-heading font-medium">{c.check}</div>
<div className="text-[9px] text-hint">{c.desc}</div>
</div>
<Badge intent={getStatusIntent(c.status)} size="sm">{c.status}</Badge>
<span className="text-[9px] text-muted-foreground">: {c.lastRun}</span>
</div>
))}
</div>
</CardContent></Card>
</div>
)}
{/* ── ③ AI 학습 보안 ── */}
{tab === 'training' && (
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<div className="space-y-2">
{TRAINING_POLICIES.map(p => (
<div key={p.policy} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Lock className="w-4 h-4 text-blue-400" />
<div className="flex-1">
<div className="text-[11px] text-heading font-medium">{p.policy}</div>
<div className="text-[9px] text-hint">{p.desc}</div>
</div>
<Badge intent="success" size="sm">{p.status}</Badge>
</div>
))}
</div>
</CardContent></Card>
)}
{/* ── ④ 입출력 보안 ── */}
{tab === 'io' && (
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">AI · </div>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th><th className="text-left py-2"></th>
<th className="text-center py-2"></th><th className="text-center py-2"></th><th className="text-center py-2"> </th>
<th className="text-center py-2"></th>
</tr></thead>
<tbody>{IO_FILTERS.map(f => (
<tr key={f.name} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2.5 px-2 text-heading font-medium">{f.name}</td>
<td className="py-2.5 text-hint text-[9px]">{f.desc}</td>
<td className="py-2.5 text-center"><Badge intent="success" size="sm">{f.status}</Badge></td>
<td className="py-2.5 text-center text-red-400 font-bold">{f.blocked}</td>
<td className="py-2.5 text-center text-muted-foreground">{f.total.toLocaleString()}</td>
<td className="py-2.5 text-center text-muted-foreground">{(f.blocked / f.total * 100).toFixed(2)}%</td>
</tr>
))}</tbody>
</table>
</CardContent></Card>
)}
{/* ── ⑤ 경계 보안 ── */}
{tab === 'boundary' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">AI </div>
<div className="space-y-2">
{BOUNDARY_ITEMS.map(b => (
<div key={b.item} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Server className="w-4 h-4 text-yellow-400" />
<div className="flex-1">
<div className="text-[11px] text-heading font-medium">{b.item}</div>
<div className="text-[9px] text-hint">{b.desc}</div>
</div>
<Badge intent="success" size="sm">{b.status}</Badge>
</div>
))}
</div>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> AI (XAI)</div>
<div className="space-y-2">
{EXPLAINABILITY.map(e => (
<div key={e.item} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Eye className="w-4 h-4 text-purple-400" />
<div className="flex-1">
<div className="text-[11px] text-heading font-medium">{e.item}</div>
<div className="text-[9px] text-hint">{e.desc}</div>
</div>
<Badge intent="purple" size="sm">{e.status}</Badge>
</div>
))}
</div>
</CardContent></Card>
</div>
)}
{/* ── ⑥ 취약점 점검 ── */}
{tab === 'vulnerability' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">AI · </div>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th><th className="text-center py-2"></th>
<th className="text-center py-2"> </th><th className="text-center py-2"></th><th className="text-center py-2"></th>
</tr></thead>
<tbody>{VULN_CHECKS.map(v => (
<tr key={v.target} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<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={getStatusIntent(v.status)} size="sm">{v.status}</Badge></td>
</tr>
))}</tbody>
</table>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">AI </div>
<div className="space-y-2">
{RECOVERY_PLANS.map(r => (
<div key={r.plan} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<RefreshCw className="w-4 h-4 text-cyan-400" />
<div className="flex-1">
<div className="text-[11px] text-heading font-medium">{r.plan}</div>
<div className="text-[9px] text-hint">{r.desc}</div>
</div>
<span className="text-[9px] text-muted-foreground">{r.detail}</span>
<Badge intent={getStatusIntent(r.status)} size="sm">{r.status}</Badge>
</div>
))}
</div>
</CardContent></Card>
</div>
)}
</PageContainer>
);
}

파일 보기

@ -3,3 +3,5 @@ export { SystemConfig } from './SystemConfig';
export { NoticeManagement } from './NoticeManagement';
export { AdminPanel } from './AdminPanel';
export { DataHub } from './DataHub';
export { AISecurityPage } from './AISecurityPage';
export { AIAgentSecurityPage } from './AIAgentSecurityPage';

파일 보기

@ -0,0 +1,431 @@
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 { PageContainer, PageHeader } from '@shared/components/layout';
import { getStatusIntent } from '@shared/constants/statusIntent';
import {
Brain, Database, GitBranch, Activity, Server, Shield,
Settings, Layers, BarChart3, Code, Play,
Zap, FlaskConical, CheckCircle,
Terminal, RefreshCw, Box,
} from 'lucide-react';
/*
* LGCNS MLOps
*
* LGCNS DAP(Data AI Platform) MLOps :
* Job
* Repository
*/
type Tab = 'project' | 'environment' | 'model' | 'job' | 'common' | 'monitoring' | 'repository';
// ─── 프로젝트 관리 ──────────────────
const PROJECTS = [
{ id: 'PRJ-001', name: '불법조업 위험도 예측', owner: '분석팀', status: '활성', models: 5, experiments: 12, updated: '2026-04-10' },
{ id: 'PRJ-002', name: '경비함정 경로추천', owner: '운항팀', status: '활성', models: 3, experiments: 8, updated: '2026-04-09' },
{ id: 'PRJ-003', name: '다크베셀 탐지', owner: '분석팀', status: '활성', models: 2, experiments: 6, updated: '2026-04-08' },
{ id: 'PRJ-004', name: '환적 네트워크 분석', owner: '수사팀', status: '대기', models: 1, experiments: 3, updated: '2026-04-05' },
];
// ─── 분석환경 ──────────────────
const ENVIRONMENTS = [
{ name: 'Jupyter Notebook', icon: Code, type: 'IDE', gpu: 'Blackwell x1', status: '실행중', user: '김분석', created: '04-10' },
{ name: 'RStudio Server', icon: BarChart3, type: 'IDE', gpu: '-', status: '중지', user: '이연구', created: '04-08' },
{ name: 'VS Code Server', icon: Terminal, type: 'IDE', gpu: 'H200 x1', status: '실행중', user: '박개발', created: '04-09' },
{ name: 'TensorBoard', icon: Activity, type: '모니터링', gpu: '-', status: '실행중', user: '김분석', created: '04-10' },
];
const WORKFLOWS = [
{ id: 'WF-012', name: 'AIS 전처리 → LSTM 학습', steps: 5, status: 'running', progress: 60, duration: '2h 15m' },
{ id: 'WF-011', name: '어구분류 피처엔지니어링', steps: 3, status: 'done', progress: 100, duration: '45m' },
{ id: 'WF-010', name: 'GNN 환적탐지 학습', steps: 4, status: 'done', progress: 100, duration: '3h 20m' },
];
// ─── 모델 관리 ──────────────────
const MODELS = [
{ name: '불법조업 위험도 v2.1', framework: 'PyTorch', status: 'DEPLOYED', accuracy: 93.2, version: 'v2.1.0', kpi: 'F1=92.3%', endpoint: '/v1/infer/risk' },
{ name: '경비함정 경로추천 v1.5', framework: 'TensorFlow', status: 'DEPLOYED', accuracy: 89.7, version: 'v1.5.2', kpi: 'F1=88.4%', endpoint: '/v1/infer/patrol' },
{ name: 'Transformer 궤적 v0.9', framework: 'PyTorch', status: 'APPROVED', accuracy: 91.2, version: 'v0.9.0', kpi: 'F1=90.5%', endpoint: '-' },
{ name: 'GNN 환적탐지 v0.3', framework: 'DGL', status: 'TESTING', accuracy: 82.3, version: 'v0.3.0', kpi: 'F1=80.1%', endpoint: '-' },
];
const PARAMETERS = [
{ name: 'learning_rate', type: 'float', default: '0.001', range: '1e-5 ~ 0.1' },
{ name: 'batch_size', type: 'int', default: '64', range: '16 ~ 256' },
{ name: 'epochs', type: 'int', default: '50', range: '10 ~ 200' },
{ name: 'dropout', type: 'float', default: '0.2', range: '0.0 ~ 0.5' },
{ name: 'hidden_dim', type: 'int', default: '256', range: '64 ~ 1024' },
];
// ─── Job 실행 관리 ──────────────────
const JOBS = [
{ id: 'JOB-088', name: 'LSTM 위험도 학습', type: '학습', resource: 'Blackwell x2', status: 'running', progress: 72, started: '04-10 08:00', elapsed: '3h 28m' },
{ id: 'JOB-087', name: 'AIS 피처 추출', type: '전처리', resource: 'CPU 16core', status: 'done', progress: 100, started: '04-10 06:00', elapsed: '1h 45m' },
{ id: 'JOB-086', name: 'GNN 하이퍼파라미터 탐색', type: 'HPS', resource: 'H200 x2', status: 'running', progress: 45, started: '04-10 07:30', elapsed: '2h 10m' },
{ id: 'JOB-085', name: '위험도 모델 배포', type: '배포', resource: 'Blackwell x1', status: 'done', progress: 100, started: '04-09 14:00', elapsed: '15m' },
{ id: 'JOB-084', name: 'SAR 이미지 전처리', type: '전처리', resource: 'CPU 32core', status: 'fail', progress: 34, started: '04-09 10:00', elapsed: '0h 50m' },
];
const PIPELINE_STAGES = [
{ name: '데이터 수집', status: 'done' },
{ name: '전처리', status: 'done' },
{ name: '피처 엔지니어링', status: 'done' },
{ name: '모델 학습', status: 'running' },
{ name: '모델 평가', status: 'pending' },
{ name: '모델 배포', status: 'pending' },
];
// ─── 공통서비스 ──────────────────
const COMMON_SERVICES = [
{ name: 'Feature Store', icon: Database, desc: '피처 저장소', status: '정상', version: 'v3.2', detail: '20개 피처 · 2.4TB' },
{ name: 'Model Registry', icon: GitBranch, desc: '모델 레지스트리', status: '정상', version: 'v2.1', detail: '12개 모델 등록' },
{ name: 'Data Catalog', icon: Layers, desc: '데이터 카탈로그', status: '정상', version: 'v1.5', detail: '48 테이블 · 1.2M rows' },
{ name: 'Experiment Tracker', icon: FlaskConical, desc: '실험 추적', status: '정상', version: 'v4.0', detail: '42개 실험 기록' },
{ name: 'API Gateway', icon: Zap, desc: 'API 게이트웨이', status: '정상', version: 'v3.0', detail: '221 req/s' },
{ name: 'Security Manager', icon: Shield, desc: '보안 관리', status: '정상', version: 'v2.0', detail: 'RBAC + JWT' },
];
// ─── 모니터링 ──────────────────
const GPU_RESOURCES = [
{ name: 'Blackwell #1', usage: 78, mem: '38/48GB', temp: '62°C', job: 'JOB-088' },
{ name: 'Blackwell #2', usage: 52, mem: '25/48GB', temp: '55°C', job: 'JOB-088' },
{ name: 'H200 #1', usage: 85, mem: '68/80GB', temp: '71°C', job: 'JOB-086' },
{ name: 'H200 #2', usage: 45, mem: '36/80GB', temp: '48°C', job: '-' },
];
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' },
];
export function LGCNSMLOpsPage() {
const { t } = useTranslation('ai');
const [tab, setTab] = useState<Tab>('project');
return (
<PageContainer>
<PageHeader
icon={Box}
iconColor="text-cyan-400"
title={t('lgcnsMlops.title')}
description={t('lgcnsMlops.desc')}
demo
/>
{/* 탭 */}
<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>
))}
</div>
{/* ── ① 프로젝트 관리 ── */}
{tab === 'project' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2">ID</th><th className="text-left py-2"></th><th className="text-left py-2"></th>
<th className="text-center py-2"></th><th className="text-center py-2"></th><th className="text-center py-2"></th><th className="text-right py-2 px-2"></th>
</tr></thead>
<tbody>{PROJECTS.map(p => (
<tr key={p.id} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2 px-2 text-muted-foreground">{p.id}</td>
<td className="py-2 text-heading font-medium">{p.name}</td>
<td className="py-2 text-muted-foreground">{p.owner}</td>
<td className="py-2 text-center"><Badge intent={getStatusIntent(p.status)} size="sm">{p.status}</Badge></td>
<td className="py-2 text-center text-heading">{p.models}</td>
<td className="py-2 text-center text-heading">{p.experiments}</td>
<td className="py-2 px-2 text-right text-muted-foreground">{p.updated}</td>
</tr>
))}</tbody>
</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>
</div>
))}
</div>
</div>
)}
{/* ── ② 분석환경 관리 ── */}
{tab === 'environment' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<div className="space-y-2">
{ENVIRONMENTS.map(e => (
<div key={e.name} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<e.icon className="w-4 h-4 text-cyan-400" />
<span className="text-[11px] text-heading font-medium w-36">{e.name}</span>
<Badge intent="muted" size="sm">{e.type}</Badge>
<span className="text-[10px] text-muted-foreground w-24">GPU: {e.gpu}</span>
<span className="text-[10px] text-muted-foreground flex-1">: {e.user}</span>
<Badge intent={getStatusIntent(e.status)} size="sm">{e.status}</Badge>
</div>
))}
</div>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"></div>
<div className="space-y-2">
{WORKFLOWS.map(w => (
<div key={w.id} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<span className="text-[10px] text-muted-foreground w-16">{w.id}</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>
<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>
<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>
</div>
))}
</div>
</CardContent></Card>
</div>
)}
{/* ── ③ 모델 관리 ── */}
{tab === 'model' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th><th className="text-left py-2"></th><th className="text-center py-2"></th>
<th className="text-center py-2"></th><th className="text-center py-2">KPI</th><th className="text-left py-2"></th><th className="text-left py-2 px-2">Endpoint</th>
</tr></thead>
<tbody>{MODELS.map(m => (
<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 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>
<td className="py-2 px-2 text-muted-foreground font-mono text-[9px]">{m.endpoint}</td>
</tr>
))}</tbody>
</table>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">Parameter </div>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th><th className="text-left py-2"></th><th className="text-center py-2"></th><th className="text-left py-2 px-2"></th>
</tr></thead>
<tbody>{PARAMETERS.map(p => (
<tr key={p.name} className="border-b border-border/50">
<td className="py-1.5 px-2 text-heading font-mono">{p.name}</td>
<td className="py-1.5 text-muted-foreground">{p.type}</td>
<td className="py-1.5 text-center text-heading">{p.default}</td>
<td className="py-1.5 px-2 text-muted-foreground">{p.range}</td>
</tr>
))}</tbody>
</table>
</CardContent></Card>
</div>
)}
{/* ── ④ Job 실행 관리 ── */}
{tab === 'job' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">Job Pipeline</div>
<div className="flex items-center gap-1 mb-4 px-2">
{PIPELINE_STAGES.map((s, i) => (
<div key={s.name} className="flex items-center gap-1 flex-1">
<div className={`flex-1 py-2 px-3 rounded-lg text-center text-[10px] font-medium ${
s.status === 'done' ? 'bg-green-500/15 text-green-400 border border-green-500/30' :
s.status === 'running' ? 'bg-blue-500/15 text-blue-400 border border-blue-500/30 animate-pulse' :
'bg-surface-overlay text-hint border border-border'
}`}>
{s.status === 'done' && <CheckCircle className="w-3 h-3 inline mr-1" />}
{s.status === 'running' && <RefreshCw className="w-3 h-3 inline mr-1 animate-spin" />}
{s.name}
</div>
{i < PIPELINE_STAGES.length - 1 && <span className="text-hint text-[10px]"></span>}
</div>
))}
</div>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">Job </div>
<table className="w-full text-[10px]">
<thead><tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2">ID</th><th className="text-left py-2">Job명</th><th className="text-center py-2"></th>
<th className="text-left py-2"></th><th className="text-center py-2"></th><th className="text-center py-2"></th><th className="text-right py-2 px-2"></th>
</tr></thead>
<tbody>{JOBS.map(j => (
<tr key={j.id} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2 px-2 text-muted-foreground">{j.id}</td>
<td className="py-2 text-heading font-medium">{j.name}</td>
<td className="py-2 text-center"><Badge intent="muted" size="sm">{j.type}</Badge></td>
<td className="py-2 text-muted-foreground text-[9px]">{j.resource}</td>
<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>
<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>
</td>
<td className="py-2 px-2 text-right text-muted-foreground">{j.elapsed}</td>
</tr>
))}</tbody>
</table>
</CardContent></Card>
</div>
)}
{/* ── ⑤ 공통서비스 ── */}
{tab === 'common' && (
<div className="grid grid-cols-3 gap-3">
{COMMON_SERVICES.map(s => (
<Card key={s.name}><CardContent className="p-4">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-cyan-500/10">
<s.icon className="w-5 h-5 text-cyan-400" />
</div>
<div>
<div className="text-[11px] font-bold text-heading">{s.name}</div>
<div className="text-[9px] text-hint">{s.desc}</div>
</div>
<Badge intent={getStatusIntent(s.status)} size="sm" className="ml-auto">{s.status}</Badge>
</div>
<div className="space-y-1 text-[10px]">
<div className="flex justify-between px-2 py-1 bg-surface-overlay rounded">
<span className="text-hint"></span><span className="text-label">{s.version}</span>
</div>
<div className="flex justify-between px-2 py-1 bg-surface-overlay rounded">
<span className="text-hint"></span><span className="text-label">{s.detail}</span>
</div>
</div>
</CardContent></Card>
))}
</div>
)}
{/* ── ⑥ 모니터링 ── */}
{tab === 'monitoring' && (
<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>
))}
</div>
<div className="grid grid-cols-2 gap-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">GPU </div>
<div className="space-y-2">
{GPU_RESOURCES.map(g => (
<div key={g.name} className="flex items-center gap-3 px-3 py-2 bg-surface-overlay rounded-lg">
<span className="text-[10px] text-heading font-medium w-24">{g.name}</span>
<div className="flex-1 h-2 bg-switch-background rounded-full overflow-hidden">
<div className={`h-full rounded-full ${g.usage > 80 ? 'bg-red-500' : g.usage > 60 ? 'bg-yellow-500' : 'bg-green-500'}`} style={{ width: `${g.usage}%` }} />
</div>
<span className="text-[10px] text-heading font-bold w-8">{g.usage}%</span>
<span className="text-[9px] text-hint">{g.mem}</span>
<span className="text-[9px] text-hint">{g.temp}</span>
<span className="text-[9px] text-muted-foreground">{g.job}</span>
</div>
))}
</div>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<div className="space-y-2">
{[
{ name: 'DAP API Gateway', status: 'ok', rps: 221 },
{ name: 'Model Serving', status: 'ok', rps: 186 },
{ name: 'Feature Store', status: 'ok', rps: 45 },
{ name: 'Experiment Tracker', status: 'ok', rps: 32 },
{ name: 'Job Scheduler', status: 'ok', rps: 15 },
{ name: 'PostgreSQL', status: 'ok', rps: 890 },
].map(s => (
<div key={s.name} className="flex items-center gap-3 px-3 py-2 bg-surface-overlay rounded-lg">
<div className={`w-2 h-2 rounded-full ${s.status === 'ok' ? 'bg-green-500 shadow-[0_0_4px_#22c55e]' : 'bg-yellow-500 shadow-[0_0_4px_#eab308]'}`} />
<span className="text-[10px] text-heading font-medium flex-1">{s.name}</span>
<span className="text-[10px] text-muted-foreground">{s.rps} req/s</span>
</div>
))}
</div>
</CardContent></Card>
</div>
</div>
)}
{/* ── ⑦ Repository ── */}
{tab === 'repository' && (
<div className="grid grid-cols-2 gap-3">
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">/ Repository</div>
<div className="space-y-1.5 text-[10px]">
{[
['kcg-ai-monitoring', 'frontend + backend + prediction 모노레포'],
['kcg-ml-models', '모델 아카이브 (버전별 weight)'],
['kcg-data-pipeline', 'ETL 스크립트 + Airflow DAG'],
['kcg-feature-store', '피처 정의 + 변환 로직'],
].map(([k, v]) => (
<div key={k} className="flex justify-between px-2 py-1.5 bg-surface-overlay rounded">
<span className="text-heading font-mono">{k}</span><span className="text-hint">{v}</span>
</div>
))}
</div>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3">/ Repository</div>
<div className="space-y-1.5 text-[10px]">
{[
['AIS 원본 데이터', 'SNPDB · 5분 주기 증분 · 1.2TB'],
['학습 데이터셋', '1,456,200건 · 04-03 갱신'],
['벡터 DB (Milvus)', '1.2M 문서 · 3.6M 벡터'],
['모델 Artifact', 'S3 · 24개 버전 · 12.8GB'],
].map(([k, v]) => (
<div key={k} className="flex justify-between px-2 py-1.5 bg-surface-overlay rounded">
<span className="text-heading">{k}</span><span className="text-hint">{v}</span>
</div>
))}
</div>
</CardContent></Card>
</div>
)}
</PageContainer>
);
}

파일 보기

@ -1,4 +1,5 @@
export { AIModelManagement } from './AIModelManagement';
export { MLOpsPage } from './MLOpsPage';
export { LGCNSMLOpsPage } from './LGCNSMLOpsPage';
export { AIAssistant } from './AIAssistant';
export { LLMOpsPage } from './LLMOpsPage';

파일 보기

@ -7,6 +7,10 @@
"title": "MLOps / LLMOps",
"desc": "SFR-18/19 | ML & LLM experiment, deployment, monitoring"
},
"lgcnsMlops": {
"title": "LGCNS MLOps",
"desc": "LGCNS DAP-based MLOps pipeline — project, environment, model & job management"
},
"llmOps": {
"title": "LLM Operations",
"desc": "SFR-20 | Qwen3-8B model management, prompts, inference, RAG, evaluation, security & monitoring"

파일 보기

@ -21,6 +21,7 @@
"reports": "Reports",
"aiModel": "AI Model",
"mlops": "MLOps",
"lgcnsMlops": "LGCNS MLOps",
"llmOps": "LLM Ops",
"aiAssistant": "AI Q&A",
"dataHub": "Data Hub",
@ -33,7 +34,9 @@
"labelSession": "Label Session",
"auditLogs": "Audit Logs",
"accessLogs": "Access Logs",
"loginHistory": "Login History"
"loginHistory": "Login History",
"aiSecurity": "AI Security",
"aiAgentSecurity": "AI Agent Security"
},
"status": {
"active": "Active",

파일 보기

@ -7,6 +7,10 @@
"title": "MLOps/LLMOps",
"desc": "SFR-18/19 | 기계학습·대규모 언어모델 실험·배포·모니터링 통합"
},
"lgcnsMlops": {
"title": "LGCNS MLOps",
"desc": "LGCNS DAP 기반 MLOps 파이프라인 — 프로젝트·분석환경·모델·Job 관리 통합"
},
"llmOps": {
"title": "LLM 운영 관리",
"desc": "SFR-20 | Qwen3-8B 모델 관리·프롬프트·추론·RAG·평가·보안·모니터링 통합 운영"

파일 보기

@ -21,6 +21,7 @@
"reports": "보고서 관리",
"aiModel": "AI 모델관리",
"mlops": "MLOps",
"lgcnsMlops": "LGCNS MLOps",
"llmOps": "LLM 운영",
"aiAssistant": "AI 의사결정 지원",
"dataHub": "데이터 허브",
@ -33,7 +34,9 @@
"labelSession": "학습 세션",
"auditLogs": "감사 로그",
"accessLogs": "접근 이력",
"loginHistory": "로그인 이력"
"loginHistory": "로그인 이력",
"aiSecurity": "AI 보안",
"aiAgentSecurity": "AI Agent 보안"
},
"status": {
"active": "활성",