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.

247 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Database, RefreshCw, BarChart3, TrendingUp, Calendar, AlertCircle } from 'lucide-react';
import { stockApi, DatabaseStats as DatabaseStatsType, formatNumber } from '@/lib/api';
const DatabaseStats: React.FC = () => {
const [stats, setStats] = useState<DatabaseStatsType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const fetchStats = async () => {
setLoading(true);
setError(null);
try {
const data = await stockApi.getDatabaseStats();
setStats(data);
setLastUpdated(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : '데이터베이스 통계를 가져오는데 실패했습니다.');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStats();
}, []);
const handleRefresh = () => {
fetchStats();
};
if (loading && !stats) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<AlertCircle className="h-5 w-5 text-red-400 mr-2" />
<p className="text-red-800">{error}</p>
</div>
<button
onClick={handleRefresh}
className="px-3 py-1 bg-red-100 text-red-800 rounded-md hover:bg-red-200 text-sm"
>
</button>
</div>
</div>
);
}
if (!stats) return null;
return (
<div className="space-y-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900 flex items-center">
<Database className="h-6 w-6 mr-2 text-blue-600" />
</h2>
<div className="flex items-center space-x-4">
{lastUpdated && (
<span className="text-sm text-gray-500">
: {lastUpdated.toLocaleString('ko-KR')}
</span>
)}
<button
onClick={handleRefresh}
disabled={loading}
className="flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{/* 요약 카드들 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* 총 회사 수 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<TrendingUp className="h-8 w-8 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500"> </p>
<p className="text-3xl font-bold text-gray-900">{formatNumber(stats.companies.total)}</p>
</div>
</div>
<div className="mt-4 text-sm text-gray-600">
<p> : {stats.companies.with_financial_data}</p>
<p> : {stats.companies.with_price_data}</p>
</div>
</div>
{/* 재무 데이터 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<BarChart3 className="h-8 w-8 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500"> </p>
<p className="text-3xl font-bold text-gray-900">{formatNumber(stats.financial_data.total_records)}</p>
</div>
</div>
<div className="mt-4 text-sm text-gray-600">
<p className="text-green-600">: {formatNumber(stats.financial_data.real_data)}</p>
<p className="text-yellow-600">: {formatNumber(stats.financial_data.estimated_data)}</p>
</div>
</div>
{/* 주가 데이터 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<TrendingUp className="h-8 w-8 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500"> </p>
<p className="text-3xl font-bold text-gray-900">{formatNumber(stats.price_data.total_records)}</p>
</div>
</div>
<div className="mt-4 text-sm text-gray-600">
<p> : {stats.price_data.tickers.length}</p>
</div>
</div>
{/* 계산된 지표 */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<Calendar className="h-8 w-8 text-orange-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500"> </p>
<p className="text-3xl font-bold text-gray-900">{formatNumber(stats.calculated_metrics.total_records)}</p>
</div>
</div>
</div>
</div>
{/* 상세 정보 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 재무 데이터 상세 */}
<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"> </h3>
<div className="space-y-4">
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-gray-600"> </span>
<span className="text-sm font-medium text-green-600">
{stats.financial_data.real_data} ({((stats.financial_data.real_data / stats.financial_data.total_records) * 100).toFixed(1)}%)
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full"
style={{ width: `${(stats.financial_data.real_data / stats.financial_data.total_records) * 100}%` }}
></div>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<span className="text-sm text-gray-600"> </span>
<span className="text-sm font-medium text-yellow-600">
{stats.financial_data.estimated_data} ({((stats.financial_data.estimated_data / stats.financial_data.total_records) * 100).toFixed(1)}%)
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-yellow-500 h-2 rounded-full"
style={{ width: `${(stats.financial_data.estimated_data / stats.financial_data.total_records) * 100}%` }}
></div>
</div>
</div>
<div className="mt-6">
<h4 className="text-sm font-medium text-gray-700 mb-2"> </h4>
<p className="text-sm text-gray-600">
{new Date(stats.financial_data.date_range.earliest).toLocaleDateString('ko-KR')} ~ {new Date(stats.financial_data.date_range.latest).toLocaleDateString('ko-KR')}
</p>
</div>
</div>
</div>
{/* 데이터 소스별 분포 */}
<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"> </h3>
<div className="space-y-3">
{Object.entries(stats.financial_data.by_source).map(([source, count]) => (
<div key={source} className="flex items-center justify-between">
<div className="flex items-center">
<div className="w-3 h-3 bg-blue-500 rounded-full mr-3"></div>
<span className="text-sm text-gray-700">{source}</span>
</div>
<div className="text-right">
<span className="text-sm font-medium text-gray-900">{formatNumber(count)}</span>
<span className="text-xs text-gray-500 ml-1">
({((count / stats.financial_data.total_records) * 100).toFixed(1)}%)
</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* 주가 데이터 종목 목록 */}
<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"> </h3>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-3">
{stats.price_data.tickers.map((ticker) => (
<div key={ticker} className="bg-gray-50 rounded-md px-3 py-2 text-center">
<span className="text-sm font-medium text-gray-900">{ticker}</span>
</div>
))}
</div>
<div className="mt-4 text-sm text-gray-600">
<p> : {new Date(stats.price_data.date_range.earliest).toLocaleDateString('ko-KR')} ~ {new Date(stats.price_data.date_range.latest).toLocaleDateString('ko-KR')}</p>
</div>
</div>
</div>
);
};
export default DatabaseStats;