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.
262 lines
11 KiB
TypeScript
262 lines
11 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { Search, Calendar, TrendingUp, DollarSign, BarChart3, AlertCircle } from 'lucide-react';
|
|
import { stockApi, FinancialDataRequest, FinancialDataResponse, formatCurrency, formatPercentage, formatDate } from '@/lib/api';
|
|
|
|
const StockQueryFixed: React.FC = () => {
|
|
// Use individual state for each field to avoid controlled component issues
|
|
const [ticker, setTicker] = useState<string>('');
|
|
const [startDate, setStartDate] = useState<string>('');
|
|
const [endDate, setEndDate] = useState<string>('');
|
|
const [periodType, setPeriodType] = useState<'quarterly' | 'annual' | 'all'>('quarterly');
|
|
const [includeMetrics, setIncludeMetrics] = useState<boolean>(true);
|
|
const [forceRefresh, setForceRefresh] = useState<boolean>(false);
|
|
|
|
const [data, setData] = useState<FinancialDataResponse | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Initialize default values
|
|
useEffect(() => {
|
|
const today = new Date();
|
|
const oneYearAgo = new Date();
|
|
oneYearAgo.setFullYear(today.getFullYear() - 1);
|
|
|
|
setTicker('AAPL');
|
|
setStartDate(oneYearAgo.toISOString().split('T')[0]);
|
|
setEndDate(today.toISOString().split('T')[0]);
|
|
}, []);
|
|
|
|
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const query: FinancialDataRequest = {
|
|
ticker: ticker.trim().toUpperCase(),
|
|
start_date: startDate,
|
|
end_date: endDate,
|
|
period_type: periodType,
|
|
include_metrics: includeMetrics,
|
|
force_refresh: forceRefresh,
|
|
};
|
|
|
|
console.log('Submitting query:', query);
|
|
|
|
try {
|
|
const result = await stockApi.getFinancialData(query);
|
|
setData(result);
|
|
console.log('API Response:', result);
|
|
} catch (err) {
|
|
console.error('API Error:', err);
|
|
setError(err instanceof Error ? err.message : '데이터를 가져오는데 실패했습니다.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [ticker, startDate, endDate, periodType, includeMetrics, forceRefresh]);
|
|
|
|
return (
|
|
<div className="space-y-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 flex items-center">
|
|
<Search className="h-5 w-5 mr-2 text-blue-600" />
|
|
주식 데이터 조회
|
|
</h3>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{/* 종목 코드 */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
종목 코드
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={ticker}
|
|
onChange={(e) => setTicker(e.target.value.toUpperCase())}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-gray-900 bg-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder="예: AAPL"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* 시작 날짜 */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
시작 날짜
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={startDate}
|
|
onChange={(e) => setStartDate(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* 종료 날짜 */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
종료 날짜
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={endDate}
|
|
onChange={(e) => setEndDate(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* 기간 타입 */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
기간 타입
|
|
</label>
|
|
<select
|
|
value={periodType}
|
|
onChange={(e) => setPeriodType(e.target.value as 'quarterly' | 'annual' | 'all')}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="quarterly">분기별</option>
|
|
<option value="annual">연간</option>
|
|
<option value="all">전체</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* 옵션들 */}
|
|
<div className="flex items-center space-x-4 col-span-full">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={includeMetrics}
|
|
onChange={(e) => setIncludeMetrics(e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
<span className="ml-2 text-sm text-gray-700">지표 포함</span>
|
|
</label>
|
|
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={forceRefresh}
|
|
onChange={(e) => setForceRefresh(e.target.checked)}
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
<span className="ml-2 text-sm text-gray-700">강제 새로고침</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 검색 버튼 */}
|
|
<div className="flex justify-end">
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<div className="animate-spin -ml-1 mr-3 h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
|
|
검색 중...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Search className="h-4 w-4 mr-2" />
|
|
검색
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{/* 에러 메시지 */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div className="flex items-center">
|
|
<AlertCircle className="h-5 w-5 text-red-400 mr-2" />
|
|
<p className="text-red-800">{error}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 결과 표시 */}
|
|
{data && (
|
|
<div className="space-y-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 flex items-center">
|
|
<TrendingUp className="h-5 w-5 mr-2 text-blue-600" />
|
|
회사 정보
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-sm text-gray-600">회사명</p>
|
|
<p className="font-medium">{data.company.name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-600">종목 코드</p>
|
|
<p className="font-medium">{data.company.ticker}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-600">섹터</p>
|
|
<p className="font-medium">{data.company.sector || 'N/A'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-600">산업</p>
|
|
<p className="font-medium">{data.company.industry || 'N/A'}</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 flex items-center">
|
|
<BarChart3 className="h-5 w-5 mr-2 text-green-600" />
|
|
재무 데이터 ({data.financial_data.length}개 기간)
|
|
</h3>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">기간</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">매출</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">순이익</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">총자산</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">P/E 비율</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{data.financial_data.map((item, index) => (
|
|
<tr key={index} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatDate(item.period_date)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatCurrency(item.revenue)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatCurrency(item.net_income)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatCurrency(item.total_assets)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{item.pe_ratio?.toFixed(2) || 'N/A'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StockQueryFixed; |