kcg-ai-monitoring/frontend/src/features/ai-operations/LGCNSMLOpsPage.tsx
Nan Kyung Lee 1244f07de6 feat: LGCNS MLOps + AI 보안(SER-10) + AI Agent 보안(SER-11) 메뉴 추가
- V025 마이그레이션: admin 그룹 하위 3개 메뉴 등록
  - LGCNS MLOps (AI 플랫폼, nav_sort=350)
  - AI 보안 (감사·보안, nav_sort=1800)
  - AI Agent 보안 (감사·보안, nav_sort=1900)
- 페이지 컴포넌트 3개 신규 생성
- componentRegistry, i18n(ko/en) 반영

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:51:05 +09:00

432 lines
25 KiB
TypeScript

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>
);
}