zephyrdark eb3ce0e6e7 feat(frontend): apply DashboardLayout to all pages
- 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>
2026-02-05 22:54:22 +09:00

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