From 11e51583784b1ce69942f904e18ce4ba8f06ee81 Mon Sep 17 00:00:00 2001 From: zephyrdark Date: Tue, 3 Feb 2026 00:02:15 +0900 Subject: [PATCH] feat: add data management admin page Add frontend page for admin data collection management at /admin/data. The page displays available collectors (stocks, sectors, prices, valuations) with buttons to trigger collection jobs, and shows recent job history with status, timing, record counts, and error information. Co-Authored-By: Claude Opus 4.5 --- frontend/src/app/admin/data/page.tsx | 180 +++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 frontend/src/app/admin/data/page.tsx diff --git a/frontend/src/app/admin/data/page.tsx b/frontend/src/app/admin/data/page.tsx new file mode 100644 index 0000000..f65c978 --- /dev/null +++ b/frontend/src/app/admin/data/page.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Sidebar from '@/components/layout/Sidebar'; +import Header from '@/components/layout/Header'; +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; +} + +interface User { + id: number; + username: string; + email: string; +} + +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 [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [jobs, setJobs] = useState([]); + const [collecting, setCollecting] = useState(null); + + useEffect(() => { + const init = async () => { + try { + const userData = await api.getCurrentUser() as User; + setUser(userData); + await fetchJobs(); + } catch { + router.push('/login'); + } finally { + setLoading(false); + } + }; + init(); + }, [router]); + + const fetchJobs = async () => { + try { + const data = await api.get('/api/admin/collect/status'); + setJobs(data); + } catch (err) { + console.error('Failed to fetch jobs:', err); + } + }; + + const runCollector = async (key: string) => { + setCollecting(key); + try { + await api.post(`/api/admin/collect/${key}`); + await fetchJobs(); + } catch (err) { + console.error('Collection failed:', err); + } finally { + setCollecting(null); + } + }; + + const getStatusBadge = (status: string) => { + const colors: Record = { + success: 'bg-green-100 text-green-800', + failed: 'bg-red-100 text-red-800', + running: 'bg-yellow-100 text-yellow-800', + }; + return colors[status] || 'bg-gray-100 text-gray-800'; + }; + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+ +
+
+
+

데이터 수집 관리

+ +
+
+

수집 작업

+
+
+
+ {collectors.map((col) => ( +
+

{col.label}

+

{col.description}

+ +
+ ))} +
+
+
+ +
+
+

최근 작업 이력

+ +
+
+ + + + + + + + + + + + {jobs.map((job) => ( + + + + + + + + ))} + {jobs.length === 0 && ( + + + + )} + +
작업명상태시작 시간건수에러
{job.job_name} + + {job.status} + + + {new Date(job.started_at).toLocaleString('ko-KR')} + {job.records_count ?? '-'} + {job.error_msg || '-'} +
+ 아직 수집 이력이 없습니다. +
+
+
+
+
+
+ ); +}