You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import dynamic from 'next/dynamic';
|
|
import { stockApi } from '@/lib/api';
|
|
import { List, Database as DBIcon, Layers, Calendar } from 'lucide-react';
|
|
import Layout from '@/components/Layout';
|
|
|
|
const DatabaseStats = dynamic(() => import('@/components/DatabaseStats'), {
|
|
ssr: false,
|
|
loading: () => (
|
|
<div className="animate-pulse space-y-4">
|
|
<div className="h-6 bg-gray-200 rounded w-1/3"></div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="h-32 bg-gray-200 rounded"></div>
|
|
<div className="h-32 bg-gray-200 rounded"></div>
|
|
<div className="h-32 bg-gray-200 rounded"></div>
|
|
</div>
|
|
</div>
|
|
),
|
|
});
|
|
|
|
const DatabasePage: React.FC = () => {
|
|
const [tab, setTab] = useState<'stats' | 'etf' | 'financial'>('stats');
|
|
const [etf, setEtf] = useState<{loading:boolean; items:any[]; total:number}>({loading:false, items:[], total:0});
|
|
const [fin, setFin] = useState<{loading:boolean; items:any[]; total:number}>({loading:false, items:[], total:0});
|
|
|
|
const loadEtf = async () => {
|
|
setEtf(s => ({...s, loading:true}));
|
|
try {
|
|
const res = await stockApi.getEtfSnapshots({ limit: 25 });
|
|
setEtf({loading:false, items:res.results, total:res.count});
|
|
} catch {
|
|
setEtf({loading:false, items:[], total:0});
|
|
}
|
|
};
|
|
|
|
const loadFin = async () => {
|
|
setFin(s => ({...s, loading:true}));
|
|
try {
|
|
const res = await stockApi.getFinancialRecords({ limit: 50 });
|
|
setFin({loading:false, items:res.results, total:res.count});
|
|
} catch {
|
|
setFin({loading:false, items:[], total:0});
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (tab === 'etf') loadEtf();
|
|
if (tab === 'financial') loadFin();
|
|
}, [tab]);
|
|
|
|
return (
|
|
<Layout title="Database - Stock Oracle">
|
|
<div className="mb-6 flex items-center space-x-2">
|
|
<button onClick={() => setTab('stats')} className={`px-3 py-2 rounded-md text-sm ${tab==='stats'?'bg-blue-600 text-white':'bg-gray-100 text-gray-700'}`}>
|
|
개요
|
|
</button>
|
|
<button onClick={() => setTab('etf')} className={`px-3 py-2 rounded-md text-sm ${tab==='etf'?'bg-blue-600 text-white':'bg-gray-100 text-gray-700'}`}>
|
|
ETF 스냅샷
|
|
</button>
|
|
<button onClick={() => setTab('financial')} className={`px-3 py-2 rounded-md text-sm ${tab==='financial'?'bg-blue-600 text-white':'bg-gray-100 text-gray-700'}`}>
|
|
재무 레코드
|
|
</button>
|
|
</div>
|
|
|
|
{tab==='stats' && <DatabaseStats />}
|
|
|
|
{tab==='etf' && (
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center"><Layers className="h-5 w-5 mr-2"/>저장된 ETF 스냅샷</h3>
|
|
{etf.loading ? (
|
|
<p className="text-gray-500">로딩 중...</p>
|
|
) : etf.items.length===0 ? (
|
|
<p className="text-gray-500">저장된 스냅샷이 없습니다.</p>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full text-sm text-gray-900">
|
|
<thead>
|
|
<tr className="text-left text-gray-800 border-b">
|
|
<th className="py-2 pr-4">Snapshot ID</th>
|
|
<th className="py-2 pr-4">Ticker</th>
|
|
<th className="py-2 pr-4">Date</th>
|
|
<th className="py-2 pr-4">Source</th>
|
|
<th className="py-2 pr-4">Holdings</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{etf.items.map((s:any) => (
|
|
<tr key={s.id} className="border-b hover:bg-gray-50 text-gray-900">
|
|
<td className="py-2 pr-4 font-mono text-xs break-all">{s.id}</td>
|
|
<td className="py-2 pr-4">{s.ticker}</td>
|
|
<td className="py-2 pr-4 flex items-center"><Calendar className="h-4 w-4 mr-1"/>{new Date(s.snapshot_date).toLocaleDateString('ko-KR')}</td>
|
|
<td className="py-2 pr-4">{s.source || '-'}</td>
|
|
<td className="py-2 pr-4">{s.holdings_count}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{tab==='financial' && (
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center"><DBIcon className="h-5 w-5 mr-2"/>저장된 재무 레코드</h3>
|
|
{fin.loading ? (
|
|
<p className="text-gray-500">로딩 중...</p>
|
|
) : fin.items.length===0 ? (
|
|
<p className="text-gray-500">레코드가 없습니다.</p>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full text-sm text-gray-900">
|
|
<thead>
|
|
<tr className="text-left text-gray-800 border-b">
|
|
<th className="py-2 pr-4">Ticker</th>
|
|
<th className="py-2 pr-4">Period</th>
|
|
<th className="py-2 pr-4">Type</th>
|
|
<th className="py-2 pr-4">Revenue</th>
|
|
<th className="py-2 pr-4">Net Income</th>
|
|
<th className="py-2 pr-4">Source</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{fin.items.map((r:any) => (
|
|
<tr key={`${r.ticker}-${r.period_date}-${r.period_type}`} className="border-b hover:bg-gray-50 text-gray-900">
|
|
<td className="py-2 pr-4">{r.ticker}</td>
|
|
<td className="py-2 pr-4">{new Date(r.period_date).toLocaleDateString('ko-KR')}</td>
|
|
<td className="py-2 pr-4">{r.period_type}</td>
|
|
<td className="py-2 pr-4">{r.revenue?.toLocaleString() ?? '-'}</td>
|
|
<td className="py-2 pr-4">{r.net_income?.toLocaleString() ?? '-'}</td>
|
|
<td className="py-2 pr-4">{r.data_source}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default DatabasePage; |