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

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;