refactor(frontend): statistics/ai-ops/parent-inference PageContainer 적용
statistics: - Statistics: icon=BarChart3, secondary 보고서 생성 Button - ReportManagement: destructive '새 보고서' + Button 그룹 + demo - ExternalService: demo ai-operations: - AIAssistant: PageContainer + h-full flex 조합 (chat 레이아웃) - AIModelManagement: 운영 모델 상태 뱃지는 actions 슬롯 유지 - MLOpsPage: demo parent-inference: - ParentReview/LabelSession/ParentExclusion: size=lg + Select + primary Button Phase B-4 완료. 총 9개 파일.
This commit is contained in:
부모
2976796652
커밋
64e24cea71
@ -2,6 +2,7 @@ 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 { MessageSquare, Send, Bot, User, BookOpen, Shield, AlertTriangle, FileText, ExternalLink } from 'lucide-react';
|
||||
import { sendChatMessage } from '@/services/chatApi';
|
||||
|
||||
@ -75,11 +76,13 @@ export function AIAssistant() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-5 h-full flex flex-col">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2"><MessageSquare className="w-5 h-5 text-green-400" />{t('assistant.title')}</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('assistant.desc')}</p>
|
||||
</div>
|
||||
<PageContainer className="h-full flex flex-col">
|
||||
<PageHeader
|
||||
icon={MessageSquare}
|
||||
iconColor="text-green-400"
|
||||
title={t('assistant.title')}
|
||||
description={t('assistant.desc')}
|
||||
/>
|
||||
<div className="flex-1 flex gap-3 min-h-0">
|
||||
{/* 대화 이력 사이드바 */}
|
||||
<Card className="w-56 shrink-0 bg-surface-raised border-border">
|
||||
@ -150,6 +153,6 @@ export function AIAssistant() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
||||
import {
|
||||
Brain, Settings, Zap, Activity, TrendingUp, BarChart3,
|
||||
@ -248,29 +249,21 @@ export function AIModelManagement() {
|
||||
const currentModel = MODELS.find((m) => m.status === '운영중')!;
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<Brain className="w-5 h-5 text-purple-400" />
|
||||
{t('modelManagement.title')}
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-yellow-500/15 border border-yellow-500/30 rounded text-[10px] text-yellow-400 font-normal">
|
||||
<span>⚠</span><span>데모 데이터 (백엔드 API 미구현)</span>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">
|
||||
{t('modelManagement.desc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Brain}
|
||||
iconColor="text-purple-400"
|
||||
title={t('modelManagement.title')}
|
||||
description={t('modelManagement.desc')}
|
||||
demo
|
||||
actions={
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
<span className="text-[10px] text-green-400 font-bold">운영 모델: {currentModel.version}</span>
|
||||
<span className="text-[10px] text-hint">Accuracy {currentModel.accuracy}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* KPI */}
|
||||
<div className="flex gap-2">
|
||||
@ -994,6 +987,6 @@ export function AIModelManagement() {
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ 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 { getModelStatusIntent, getQualityGateIntent, getExperimentIntent, MODEL_STATUSES, QUALITY_GATE_STATUSES, EXPERIMENT_STATUSES } from '@shared/constants/modelDeploymentStatuses';
|
||||
import {
|
||||
Cpu, Brain, Database, GitBranch, Activity, RefreshCw, Server, Shield,
|
||||
@ -111,18 +112,14 @@ export function MLOpsPage() {
|
||||
const [selectedLLM, setSelectedLLM] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-purple-400" />{t('mlops.title')}
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-yellow-500/15 border border-yellow-500/30 rounded text-[10px] text-yellow-400 font-normal">
|
||||
<span>⚠</span><span>데모 데이터 (백엔드 API 미구현)</span>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('mlops.desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Cpu}
|
||||
iconColor="text-purple-400"
|
||||
title={t('mlops.title')}
|
||||
description={t('mlops.desc')}
|
||||
demo
|
||||
/>
|
||||
|
||||
{/* 탭 */}
|
||||
<div className="flex gap-0 border-b border-border">
|
||||
@ -562,6 +559,6 @@ export function MLOpsPage() {
|
||||
</CardContent></Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
import { Tag, X, Loader2 } from 'lucide-react';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { Button } from '@shared/components/ui/button';
|
||||
import { Select } from '@shared/components/ui/select';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { useAuth } from '@/app/auth/AuthContext';
|
||||
import {
|
||||
fetchLabelSessions,
|
||||
@ -85,23 +88,32 @@ export function LabelSession() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-heading">학습 세션</h1>
|
||||
<p className="text-xs text-hint mt-1">정답 라벨링 → prediction 모델 학습 데이터로 활용</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select value={filter} onChange={(e) => setFilter(e.target.value)}
|
||||
className="bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs text-heading">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="CANCELLED">CANCELLED</option>
|
||||
<option value="COMPLETED">COMPLETED</option>
|
||||
</select>
|
||||
<button type="button" onClick={load} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs rounded">새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer size="lg">
|
||||
<PageHeader
|
||||
icon={Tag}
|
||||
iconColor="text-cyan-400"
|
||||
title="학습 세션"
|
||||
description="정답 라벨링 → prediction 모델 학습 데이터로 활용"
|
||||
actions={
|
||||
<>
|
||||
<Select
|
||||
size="sm"
|
||||
aria-label="상태 필터"
|
||||
title="상태 필터"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
>
|
||||
<option value="">전체 상태</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="CANCELLED">CANCELLED</option>
|
||||
<option value="COMPLETED">COMPLETED</option>
|
||||
</Select>
|
||||
<Button variant="primary" size="sm" onClick={load}>
|
||||
새로고침
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
@ -180,6 +192,6 @@ export function LabelSession() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
import { Ban, RotateCcw, Loader2, Globe, Layers } from 'lucide-react';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { Button } from '@shared/components/ui/button';
|
||||
import { Select } from '@shared/components/ui/select';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { useAuth } from '@/app/auth/AuthContext';
|
||||
import {
|
||||
fetchExclusions,
|
||||
@ -102,25 +105,31 @@ export function ParentExclusion() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-heading">모선 후보 제외</h1>
|
||||
<p className="text-xs text-hint mt-1">GROUP/GLOBAL 스코프로 잘못된 후보를 차단합니다.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value as '' | 'GROUP' | 'GLOBAL')}
|
||||
className="bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs text-heading"
|
||||
>
|
||||
<option value="">전체 스코프</option>
|
||||
<option value="GROUP">GROUP</option>
|
||||
<option value="GLOBAL">GLOBAL</option>
|
||||
</select>
|
||||
<button type="button" onClick={load} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs rounded">새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer size="lg">
|
||||
<PageHeader
|
||||
icon={Ban}
|
||||
iconColor="text-red-400"
|
||||
title="모선 후보 제외"
|
||||
description="GROUP/GLOBAL 스코프로 잘못된 후보를 차단합니다."
|
||||
actions={
|
||||
<>
|
||||
<Select
|
||||
size="sm"
|
||||
aria-label="스코프 필터"
|
||||
title="스코프 필터"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value as '' | 'GROUP' | 'GLOBAL')}
|
||||
>
|
||||
<option value="">전체 스코프</option>
|
||||
<option value="GROUP">GROUP</option>
|
||||
<option value="GLOBAL">GLOBAL</option>
|
||||
</Select>
|
||||
<Button variant="primary" size="sm" onClick={load}>
|
||||
새로고침
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 신규 등록: GROUP */}
|
||||
<Card>
|
||||
@ -226,6 +235,6 @@ export function ParentExclusion() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { CheckCircle, XCircle, RotateCcw, Loader2 } from 'lucide-react';
|
||||
import { CheckCircle, XCircle, RotateCcw, Loader2, GitMerge } from 'lucide-react';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { Button } from '@shared/components/ui/button';
|
||||
import { Select } from '@shared/components/ui/select';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { useAuth } from '@/app/auth/AuthContext';
|
||||
import {
|
||||
fetchReviewList,
|
||||
@ -97,34 +100,26 @@ export function ParentReview() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-heading">모선 확정/거부</h1>
|
||||
<p className="text-xs text-hint mt-1">
|
||||
추론된 모선 후보를 확정/거부합니다. 권한: parent-inference-workflow:parent-review (UPDATE)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="bg-surface-overlay border border-border rounded px-3 py-1.5 text-xs text-heading"
|
||||
>
|
||||
<option value="">전체 상태</option>
|
||||
<option value="UNRESOLVED">미해결</option>
|
||||
<option value="MANUAL_CONFIRMED">확정됨</option>
|
||||
<option value="REVIEW_REQUIRED">검토필요</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={load}
|
||||
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs rounded"
|
||||
>
|
||||
새로고침
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer size="lg">
|
||||
<PageHeader
|
||||
icon={GitMerge}
|
||||
iconColor="text-purple-400"
|
||||
title="모선 확정/거부"
|
||||
description="추론된 모선 후보를 확정/거부합니다. 권한: parent-inference-workflow:parent-review (UPDATE)"
|
||||
actions={
|
||||
<>
|
||||
<Select size="sm" title="상태 필터" value={filter} onChange={(e) => setFilter(e.target.value)}>
|
||||
<option value="">전체 상태</option>
|
||||
<option value="UNRESOLVED">미해결</option>
|
||||
<option value="MANUAL_CONFIRMED">확정됨</option>
|
||||
<option value="REVIEW_REQUIRED">검토필요</option>
|
||||
</Select>
|
||||
<Button variant="primary" size="sm" onClick={load}>
|
||||
새로고침
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 신규 등록 폼 (테스트용) */}
|
||||
{canUpdate && (
|
||||
@ -267,6 +262,6 @@ export function ParentReview() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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 { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
||||
import { Globe, Shield, Clock, BarChart3, ExternalLink, Lock, Unlock } from 'lucide-react';
|
||||
import { Globe } from 'lucide-react';
|
||||
|
||||
/* SFR-14: 외부 서비스(예보·경보) 제공 결과 연계 */
|
||||
|
||||
@ -31,16 +31,14 @@ const cols: DataColumn<Service>[] = [
|
||||
export function ExternalService() {
|
||||
const { t } = useTranslation('statistics');
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<Globe className="w-5 h-5 text-green-400" />{t('externalService.title')}
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-yellow-500/15 border border-yellow-500/30 rounded text-[10px] text-yellow-400 font-normal">
|
||||
<span>⚠</span><span>데모 데이터 (백엔드 API 미구현)</span>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('externalService.desc')}</p>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Globe}
|
||||
iconColor="text-green-400"
|
||||
title={t('externalService.title')}
|
||||
description={t('externalService.desc')}
|
||||
demo
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{[{ l: '운영 서비스', v: DATA.filter(d => d.status === '운영').length, c: 'text-green-400' }, { l: '테스트', v: DATA.filter(d => d.status === '테스트').length, c: 'text-blue-400' }, { l: '총 호출', v: '21,675', c: 'text-heading' }].map(k => (
|
||||
<div key={k.l} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
|
||||
@ -49,6 +47,6 @@ export function ExternalService() {
|
||||
))}
|
||||
</div>
|
||||
<DataTable data={DATA} columns={cols} pageSize={10} searchPlaceholder="서비스명, 대상기관 검색..." searchKeys={['name', 'target']} exportFilename="외부서비스연계" />
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,12 +2,14 @@ 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 { Button } from '@shared/components/ui/button';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { ExcelExport } from '@shared/components/common/ExcelExport';
|
||||
import { PrintButton } from '@shared/components/common/PrintButton';
|
||||
import { FileUpload } from '@shared/components/common/FileUpload';
|
||||
import { SaveButton } from '@shared/components/common/SaveButton';
|
||||
import { SearchInput } from '@shared/components/common/SearchInput';
|
||||
import { Plus, Download, Clock, MapPin, Upload, X } from 'lucide-react';
|
||||
import { FileText, Plus, Upload, X, Clock, MapPin, Download } from 'lucide-react';
|
||||
|
||||
interface Report {
|
||||
id: string;
|
||||
@ -37,40 +39,40 @@ export function ReportManagement() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
{t('reports.title')}
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-yellow-500/15 border border-yellow-500/30 rounded text-[10px] text-yellow-400 font-normal">
|
||||
<span>⚠</span><span>데모 데이터 (백엔드 API 미구현)</span>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-xs text-hint mt-0.5">{t('reports.desc')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SearchInput value={search} onChange={setSearch} placeholder="보고서 검색..." className="w-48" />
|
||||
<ExcelExport
|
||||
data={reports as unknown as Record<string, unknown>[]}
|
||||
columns={[
|
||||
{ key: 'id', label: '보고서번호' }, { key: 'name', label: '선박명' },
|
||||
{ key: 'type', label: '유형' }, { key: 'status', label: '상태' },
|
||||
{ key: 'date', label: '일시' }, { key: 'evidence', label: '증거수' },
|
||||
]}
|
||||
filename="보고서목록"
|
||||
/>
|
||||
<PrintButton />
|
||||
<button
|
||||
onClick={() => setShowUpload(!showUpload)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 bg-surface-overlay border border-border rounded-lg text-[10px] text-muted-foreground hover:text-heading transition-colors"
|
||||
>
|
||||
<Upload className="w-3 h-3" />증거 업로드
|
||||
</button>
|
||||
<button className="flex items-center gap-2 bg-red-600 hover:bg-red-500 text-on-vivid px-4 py-2 rounded-lg text-sm transition-colors">
|
||||
<Plus className="w-4 h-4" />새 보고서
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={FileText}
|
||||
iconColor="text-muted-foreground"
|
||||
title={t('reports.title')}
|
||||
description={t('reports.desc')}
|
||||
demo
|
||||
actions={
|
||||
<>
|
||||
<SearchInput value={search} onChange={setSearch} placeholder="보고서 검색..." className="w-48" />
|
||||
<ExcelExport
|
||||
data={reports as unknown as Record<string, unknown>[]}
|
||||
columns={[
|
||||
{ key: 'id', label: '보고서번호' }, { key: 'name', label: '선박명' },
|
||||
{ key: 'type', label: '유형' }, { key: 'status', label: '상태' },
|
||||
{ key: 'date', label: '일시' }, { key: 'evidence', label: '증거수' },
|
||||
]}
|
||||
filename="보고서목록"
|
||||
/>
|
||||
<PrintButton />
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setShowUpload(!showUpload)}
|
||||
icon={<Upload className="w-3 h-3" />}
|
||||
>
|
||||
증거 업로드
|
||||
</Button>
|
||||
<Button variant="destructive" size="md" icon={<Plus className="w-4 h-4" />}>
|
||||
새 보고서
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 증거 파일 업로드 */}
|
||||
{showUpload && (
|
||||
@ -186,6 +188,6 @@ export function ReportManagement() {
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { Button } from '@shared/components/ui/button';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
||||
import { BarChart3, Download } from 'lucide-react';
|
||||
import { BarChart, AreaChart } from '@lib/charts';
|
||||
@ -140,22 +142,18 @@ export function Statistics() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5 text-purple-400" />
|
||||
{t('statistics.title')}
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">
|
||||
{t('statistics.desc')}
|
||||
</p>
|
||||
</div>
|
||||
<button className="flex items-center gap-1 px-3 py-1.5 bg-surface-overlay border border-border rounded-lg text-[10px] text-muted-foreground hover:text-heading">
|
||||
<Download className="w-3 h-3" />
|
||||
보고서 생성
|
||||
</button>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={BarChart3}
|
||||
iconColor="text-purple-400"
|
||||
title={t('statistics.title')}
|
||||
description={t('statistics.desc')}
|
||||
actions={
|
||||
<Button variant="secondary" size="sm" icon={<Download className="w-3 h-3" />}>
|
||||
보고서 생성
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{loading && (
|
||||
<div className="text-center py-10 text-muted-foreground text-sm">
|
||||
@ -242,6 +240,6 @@ export function Statistics() {
|
||||
searchPlaceholder="지표명 검색..."
|
||||
exportFilename="성과지표"
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user