import React, { useState, useEffect } from 'react'; import Layout from '@/components/Layout'; import NewsSocialDisplay from '@/components/NewsSocialDisplay'; import { Search, TrendingUp, BarChart3, Building, Calendar, DollarSign, Percent, Users, AlertCircle, RefreshCw, ChevronDown, ChevronUp, TrendingDown, Activity, PieChart, LineChart, Eye, BarChart, Target, Layers, Shield, Award, Briefcase, Calculator } from 'lucide-react'; import { stockApi, Company, FinancialData, PriceData, formatCurrency, formatNumber, formatPercentage, formatDate } from '@/lib/api'; import dynamic from 'next/dynamic'; const Plot = dynamic(() => import('react-plotly.js'), { ssr: false }); interface StockDetailPageProps {} type ChartPeriod = '1D' | '5D' | '1M' | '3M' | '6M' | '1Y' | '2Y' | '5Y' | '10Y'; const StockDetailPage: React.FC = () => { const [ticker, setTicker] = useState(''); const [searchTicker, setSearchTicker] = useState(''); const [company, setCompany] = useState(null); const [financialData, setFinancialData] = useState([]); const [priceData, setPriceData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [showAllFinancialData, setShowAllFinancialData] = useState(false); const [periodType, setPeriodType] = useState<'quarterly' | 'annual' | 'all'>('all'); const [chartPeriod, setChartPeriod] = useState('6M'); const [chartType, setChartType] = useState<'line' | 'candlestick'>('line'); const searchStock = async () => { if (!searchTicker.trim()) { setError('종목 코드를 입력해주세요'); return; } setLoading(true); setError(''); setTicker(searchTicker.toUpperCase()); try { // Get date range for price data based on chart selection const getDateRangeForChart = (period: ChartPeriod): { start_date: string; end_date: string } => { const today = new Date(); const endDate = today.toISOString().split('T')[0]; let startDate: Date; switch (period) { case '1D': startDate = new Date(today.getTime() - 1 * 24 * 60 * 60 * 1000); break; case '5D': startDate = new Date(today.getTime() - 5 * 24 * 60 * 60 * 1000); break; case '1M': startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); break; case '3M': startDate = new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000); break; case '6M': startDate = new Date(today.getTime() - 180 * 24 * 60 * 60 * 1000); break; case '1Y': startDate = new Date(today.getTime() - 365 * 24 * 60 * 60 * 1000); break; case '2Y': startDate = new Date(today.getTime() - 2 * 365 * 24 * 60 * 60 * 1000); break; case '5Y': startDate = new Date(today.getTime() - 5 * 365 * 24 * 60 * 60 * 1000); break; case '10Y': startDate = new Date(today.getTime() - 10 * 365 * 24 * 60 * 60 * 1000); break; default: startDate = new Date(today.getTime() - 180 * 24 * 60 * 60 * 1000); // Default to 6M } return { start_date: startDate.toISOString().split('T')[0], end_date: endDate }; }; const { start_date, end_date } = getDateRangeForChart(chartPeriod); // 재무 데이터와 주가 데이터를 동시에 가져오기 const [financialResponse, priceResponse] = await Promise.allSettled([ stockApi.getFinancialData({ ticker: searchTicker.toUpperCase(), start_date: '2020-01-01', end_date: new Date().toISOString().split('T')[0], period_type: periodType, include_metrics: true }), stockApi.getPriceData({ ticker: searchTicker.toUpperCase(), start_date, end_date, interval: '1d' }) ]); // 재무 데이터 처리 if (financialResponse.status === 'fulfilled') { console.log('🔍 Debug: Financial data loaded', { company: financialResponse.value.company, dataCount: financialResponse.value.financial_data?.length, firstRecord: financialResponse.value.financial_data?.[0], lastRecord: financialResponse.value.financial_data?.[financialResponse.value.financial_data.length - 1] }); setCompany(financialResponse.value.company); setFinancialData(financialResponse.value.financial_data); } // 주가 데이터 처리 if (priceResponse.status === 'fulfilled') { console.log('🔍 Debug: Price data loaded', { ticker: priceResponse.value.ticker, dataCount: priceResponse.value.data?.length, firstRecord: priceResponse.value.data?.[0], lastRecord: priceResponse.value.data?.[priceResponse.value.data?.length - 1] }); setPriceData(priceResponse.value.data || []); } // 둘 다 실패한 경우에만 에러 표시 if (financialResponse.status === 'rejected' && priceResponse.status === 'rejected') { setError(`${searchTicker} 종목에 대한 데이터를 찾을 수 없습니다.`); } } catch (err: any) { setError(err.message || '데이터를 불러오는 중 오류가 발생했습니다.'); } finally { setLoading(false); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { searchStock(); } }; // 차트 기간이 변경될 때 데이터 다시 로드 // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only re-fetch when chartPeriod changes useEffect(() => { if (ticker) { searchStock(); } }, [chartPeriod]); // Helper function to format chart data const formatChartData = () => { if (!priceData || priceData.length === 0) return []; // Sort by date ascending (oldest to newest) for proper chronological display const sortedData = [...priceData].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); return sortedData.map(item => ({ date: formatDate(item.date, 'MM/dd'), price: item.close, volume: item.volume || 0, high: item.high, low: item.low, open: item.open, })); }; // Calculate price change from previous to latest price const getPriceChange = () => { if (!priceData || priceData.length < 2) return { change: 0, changePercent: 0 }; // API returns data in ascending order, so latest is last, previous is second to last const latest = priceData[priceData.length - 1]; const previous = priceData[priceData.length - 2]; const change = latest.close - previous.close; const changePercent = (change / previous.close) * 100; return { change, changePercent }; }; // Calculate 52-week range const get52WeekRange = () => { if (!priceData || priceData.length === 0) return { low: 0, high: 0 }; const prices = priceData.map(p => p.close); return { low: Math.min(...prices), high: Math.max(...prices) }; }; // Display financial data with most recent first const displayedFinancialData = showAllFinancialData ? [...financialData].reverse() : [...(financialData || [])].reverse().slice(0, 8); const getLatestPrice = () => { if (!priceData || priceData.length === 0) { console.log('🔍 Debug: No price data available', { priceData }); return null; } // API returns data in ascending order (oldest first), so get the last element for most recent const latest = priceData[priceData.length - 1]; console.log('🔍 Debug: Latest price data', { latest, totalRecords: priceData.length }); return latest; }; const getLatestFinancials = () => { if (!financialData || financialData.length === 0) { console.log('🔍 Debug: No financial data available', { financialData }); return null; } // Get the most recent financial data (last element if sorted ascending) const latest = financialData[financialData.length - 1]; console.log('🔍 Debug: Latest financial data', { latest, totalRecords: financialData.length }); return latest; }; const latestPrice = getLatestPrice(); const latestFinancials = getLatestFinancials(); const chartData = formatChartData(); const priceChange = getPriceChange(); const weekRange = get52WeekRange(); const chartPeriods: ChartPeriod[] = ['1D', '5D', '1M', '3M', '6M', '1Y', '2Y', '5Y', '10Y']; return (
{/* 검색 섹션 */}

종목 상세 조회

setSearchTicker(e.target.value.toUpperCase())} onKeyPress={handleKeyPress} placeholder="종목 코드를 입력하세요" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
{error && (
{error}
)}
{/* Professional Stock Analysis Dashboard */} {company && (
{/* Premium Company Header */}

{company.name}

{company.ticker} {company.sector && ( • {company.sector} )}
{/* Live Price Display */} {latestPrice && (
{formatCurrency(latestPrice.close)}
= 0 ? 'bg-green-500/20 text-green-300' : 'bg-red-500/20 text-red-300' }`}> {priceChange.change >= 0 ? : } {priceChange.change >= 0 ? '+' : ''}{priceChange.change.toFixed(2)} ({priceChange.changePercent >= 0 ? '+' : ''}{priceChange.changePercent.toFixed(2)}%)
)}
{/* Market Status */}
Live Data
{latestPrice && (
Last Updated: {formatDate(latestPrice.date)}
)}
{/* Key Performance Indicators Grid */} {(latestPrice || latestFinancials) && (
{latestPrice && ( <>
52W High
{formatCurrency(weekRange.high)}
52W Low
{formatCurrency(weekRange.low)}
Volume
{formatNumber(latestPrice.volume)}
)} {latestFinancials && ( <>
P/E Ratio
{latestFinancials.eps && latestPrice ? (latestPrice.close / latestFinancials.eps).toFixed(2) : 'N/A' }
ROE
{formatPercentage(latestFinancials.roe)}
Margin
{formatPercentage(latestFinancials.net_margin)}
)}
)} {/* Main Dashboard Grid */}
{/* Chart Section - Takes 2/3 width */}
{/* Stock Chart */}

주가 차트

{/* Period Selector */}
{chartPeriods.map((period) => ( ))}
{/* Chart Display */}
d.date), y: chartData.map((d:any) => d.price), type: 'scatter', mode: 'lines', name: '주가', line: { color: '#10b981', width: 3 }, yaxis: 'y1', }, { x: chartData.map((d:any) => d.date), y: chartData.map((d:any) => d.volume), type: 'bar', name: '거래량', marker: { color: 'rgba(59,130,246,0.4)' }, yaxis: 'y2', }, ]} layout={{ autosize: true, height: 400, margin: { t: 20, r: 20, b: 40, l: 40 }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', xaxis: { title: '날짜', tickfont: { size: 12 } }, yaxis: { title: '주가', titlefont: { size: 12 }, tickprefix: '$' }, yaxis2: { title: '거래량', overlaying: 'y', side: 'right', showgrid: false, }, legend: { orientation: 'h', y: -0.2 }, }} style={{ width: '100%', height: '100%' }} config={{ displayModeBar: false, responsive: true }} />
{/* Financial Trend Chart */} {financialData.length > 1 && (

재무 트렌드

{ const sorted = [...financialData] .slice(0, 8) .sort((a, b) => new Date(a.period_date).getTime() - new Date(b.period_date).getTime()); const x = sorted.map((d) => formatDate(d.period_date, 'MM/yy')); return [ { x, y: sorted.map((d) => d.revenue ?? null), type: 'scatter', mode: 'lines+markers', name: '매출액', line: { color: '#3b82f6', width: 3 }, marker: { color: '#3b82f6' }, }, { x, y: sorted.map((d) => d.net_income ?? null), type: 'scatter', mode: 'lines+markers', name: '순이익', line: { color: '#10b981', width: 3 }, marker: { color: '#10b981' }, }, ]; })()} layout={{ autosize: true, height: 300, margin: { t: 10, r: 20, b: 40, l: 50 }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', xaxis: { title: '기간', tickfont: { size: 12 } }, yaxis: { title: '금액', tickprefix: '$' }, legend: { orientation: 'h', y: -0.2 }, }} style={{ width: '100%', height: '100%' }} config={{ displayModeBar: false, responsive: true }} />
)}
{/* Sidebar - Takes 1/3 width */}
{/* Company Overview */} {company.business_description && (

회사 개요

{company.business_description}

)} {/* Key Financial Metrics */} {latestFinancials && (

주요 재무 지표

매출액 {formatCurrency(latestFinancials.revenue)}
순이익 {formatCurrency(latestFinancials.net_income)}
총 자산 {formatCurrency(latestFinancials.total_assets)}
EPS {latestFinancials.eps ? `$${latestFinancials.eps.toFixed(2)}` : 'N/A'}
)} {/* Price Statistics */} {latestPrice && (

주가 정보

시가 {formatCurrency(latestPrice.open)}
고가 {formatCurrency(latestPrice.high)}
저가 {formatCurrency(latestPrice.low)}
거래량 {formatNumber(latestPrice.volume)}
)} {/* Financial Ratios */} {latestFinancials && (

재무 비율

ROA {formatPercentage(latestFinancials.roa)}
순이익률 {formatPercentage(latestFinancials.net_margin)}
부채비율 {latestFinancials.total_debt && latestFinancials.total_assets ? formatPercentage((latestFinancials.total_debt / latestFinancials.total_assets) * 100) : 'N/A' }
)}
{/* Financial Data Table */} {financialData && financialData.length > 0 && (

재무 데이터 히스토리

{financialData.length}개 기록
{financialData.length > 8 && ( )}
{displayedFinancialData.map((data, index) => ( ))}
날짜 유형 매출액 순이익 EPS ROE 순이익률
{formatDate(data.period_date)} {data.period_type === 'quarterly' ? '분기' : '연간'} {formatCurrency(data.revenue)} {formatCurrency(data.net_income)} {data.eps ? `$${data.eps.toFixed(2)}` : 'N/A'} {formatPercentage(data.roe)} {formatPercentage(data.net_margin)}
)}
)} {/* 뉴스 및 소셜 미디어 섹션 */} {ticker && ( )} {/* 데이터 없음 메시지 */} {ticker && !loading && !company && (!financialData || financialData.length === 0) && (!priceData || priceData.length === 0) && (

데이터를 찾을 수 없습니다

"{ticker}" 종목에 대한 저장된 데이터가 없습니다. 다른 종목 코드를 시도해보세요.

)}
); }; export default StockDetailPage;