- Portfolio pages updated with DashboardLayout and shadcn/ui Card components - Strategy pages updated (multi-factor, quality, value-momentum) - Backtest pages updated with consistent styling - Admin data management page updated - Login page improved with shadcn/ui Card, Input, Button, Label - All pages now support dark mode via CSS variables - Removed old Sidebar/Header imports, using unified DashboardLayout - Added shadcn/ui input and label components Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
191 lines
6.7 KiB
TypeScript
191 lines
6.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { DashboardLayout } from '@/components/layout/dashboard-layout';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { api } from '@/lib/api';
|
|
|
|
interface JobLog {
|
|
id: number;
|
|
job_name: string;
|
|
status: string;
|
|
started_at: string;
|
|
finished_at: string | null;
|
|
records_count: number | null;
|
|
error_msg: string | null;
|
|
}
|
|
|
|
const collectors = [
|
|
{ key: 'stocks', label: '종목 마스터', description: 'KRX에서 종목 정보 수집' },
|
|
{ key: 'sectors', label: '섹터 정보', description: 'WISEindex에서 섹터 분류 수집' },
|
|
{ key: 'prices', label: '가격 데이터', description: 'pykrx로 OHLCV 데이터 수집' },
|
|
{ key: 'valuations', label: '밸류 지표', description: 'KRX에서 PER/PBR 등 수집' },
|
|
];
|
|
|
|
export default function DataManagementPage() {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(true);
|
|
const [jobs, setJobs] = useState<JobLog[]>([]);
|
|
const [collecting, setCollecting] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const init = async () => {
|
|
try {
|
|
await api.getCurrentUser();
|
|
await fetchJobs();
|
|
} catch {
|
|
router.push('/login');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
init();
|
|
}, [router]);
|
|
|
|
const fetchJobs = async () => {
|
|
try {
|
|
setError(null);
|
|
const data = await api.get<JobLog[]>('/api/admin/collect/status');
|
|
setJobs(data);
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : 'Failed to fetch jobs';
|
|
setError(message);
|
|
console.error('Failed to fetch jobs:', err);
|
|
}
|
|
};
|
|
|
|
const runCollector = async (key: string) => {
|
|
setCollecting(key);
|
|
setError(null);
|
|
try {
|
|
await api.post(`/api/admin/collect/${key}`);
|
|
await fetchJobs();
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : 'Collection failed';
|
|
setError(message);
|
|
console.error('Collection failed:', err);
|
|
} finally {
|
|
setCollecting(null);
|
|
}
|
|
};
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true);
|
|
try {
|
|
await fetchJobs();
|
|
} finally {
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const colors: Record<string, string> = {
|
|
success: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
|
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
|
running: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
|
};
|
|
return colors[status] || 'bg-muted text-muted-foreground';
|
|
};
|
|
|
|
if (loading) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<h1 className="text-2xl font-bold text-foreground mb-6">데이터 수집 관리</h1>
|
|
|
|
{error && (
|
|
<div className="bg-destructive/10 border border-destructive text-destructive px-4 py-3 rounded mb-4">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<Card className="mb-6">
|
|
<CardHeader>
|
|
<CardTitle>수집 작업</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{collectors.map((col) => (
|
|
<div
|
|
key={col.key}
|
|
className="border border-border rounded-lg p-4 flex flex-col"
|
|
>
|
|
<h3 className="font-medium text-foreground">{col.label}</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">{col.description}</p>
|
|
<Button
|
|
onClick={() => runCollector(col.key)}
|
|
disabled={collecting !== null}
|
|
aria-label={`${col.label} 수집 실행`}
|
|
className="mt-auto"
|
|
>
|
|
{collecting === col.key ? '수집 중...' : '실행'}
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>최근 작업 이력</CardTitle>
|
|
<button
|
|
onClick={handleRefresh}
|
|
disabled={refreshing}
|
|
className="text-sm text-primary hover:text-primary/80 disabled:text-muted-foreground"
|
|
>
|
|
{refreshing ? '새로고침 중...' : '새로고침'}
|
|
</button>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-muted">
|
|
<tr>
|
|
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">작업명</th>
|
|
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">상태</th>
|
|
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">시작 시간</th>
|
|
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">건수</th>
|
|
<th scope="col" className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">에러</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border">
|
|
{jobs.map((job) => (
|
|
<tr key={job.id}>
|
|
<td className="px-4 py-3 text-sm">{job.job_name}</td>
|
|
<td className="px-4 py-3">
|
|
<span className={`px-2 py-1 rounded text-xs ${getStatusBadge(job.status)}`}>
|
|
{job.status}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-muted-foreground">
|
|
{new Date(job.started_at).toLocaleString('ko-KR')}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm">{job.records_count ?? '-'}</td>
|
|
<td className="px-4 py-3 text-sm text-destructive truncate max-w-xs">
|
|
{job.error_msg || '-'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{jobs.length === 0 && (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-muted-foreground">
|
|
아직 수집 이력이 없습니다.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</DashboardLayout>
|
|
);
|
|
}
|