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