import { useQuery } from '@tanstack/react-query' import api from '../lib/api' import { useWebSocket } from '../hooks/useWebSocket' import { useEffect, useMemo, useState } from 'react' import HtsPage from '../components/HtsPage' import CandlestickChart from '../components/CandlestickChart' import './Dashboard.css' const Dashboard = () => { console.log('๐Ÿš€ Dashboard component mounted') const { connected: wsConnected } = useWebSocket('dashboard') const [selectedStock, setSelectedStock] = useState('') const [chartPeriod, setChartPeriod] = useState<'D' | 'W' | 'M'>('D') const [showIndicators, setShowIndicators] = useState(false) const [indicators, setIndicators] = useState({ use_macd: false, use_rsi: true, use_bollinger: false, use_moving_average: true, use_stochastic: false, use_adx: false, use_obv: false, }) console.log('๐Ÿ”ง Setting up queries...') const { data: portfolioSummary, error: portfolioSummaryError } = useQuery({ queryKey: ['portfolio-summary'], queryFn: async () => { console.log('๐Ÿ”ต Fetching portfolio summary...') const response = await api.get('/portfolio/summary') console.log('๐ŸŸข Portfolio summary received:', response.data) return response.data }, refetchInterval: 5000, }) const { data: portfolio, error: portfolioError } = useQuery({ queryKey: ['portfolio'], queryFn: async () => { console.log('๐Ÿ”ต Fetching portfolio...') try { const response = await api.get('/portfolio') console.log('๐ŸŸข Portfolio received:', response.data) return response.data } catch (error) { console.error('โŒ Portfolio fetch error:', error) throw error } }, refetchInterval: 5000, retry: 1, }) // ์—๋Ÿฌ ๋กœ๊น… useEffect(() => { if (portfolioSummaryError) { console.error('โŒ Portfolio Summary Error:', portfolioSummaryError) } if (portfolioError) { console.error('โŒ Portfolio Error:', portfolioError) } }, [portfolioSummaryError, portfolioError]) const { data: recentTrades } = useQuery({ queryKey: ['recent-trades'], queryFn: async () => { const response = await api.get('/trades', { params: { limit: 20 } }) return response.data }, refetchInterval: 5000, }) const { data: mcpStatus } = useQuery({ queryKey: ['mcp-status'], queryFn: async () => { const response = await api.get('/mcp/status') return response.data }, refetchInterval: 5000, }) const { data: news } = useQuery({ queryKey: ['market-news'], queryFn: async () => { const response = await api.get('/market-data/news', { params: { limit: 5 } }) return response.data }, refetchInterval: 30000, }) // ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ž๋™ ์ถ”์ฒœ (์ž๋™ ๋กœ๋“œ) const { data: ruleBasedRecommendations } = useQuery({ queryKey: ['rule-based-recommendations'], queryFn: async () => { console.log('๐Ÿ”ต Fetching rule-based recommendations...') try { const response = await api.get('/recommendations/rule-based') console.log('๐ŸŸข Rule-based recommendations received:', response.data) return response.data } catch (error) { console.error('๐Ÿ”ด Rule-based recommendations error:', error) throw error } }, refetchInterval: 300000, // 5๋ถ„๋งˆ๋‹ค ์ž๋™ ๊ฐฑ์‹  }) // ์ˆ˜๋™ ์ง€ํ‘œ ๋ถ„์„ (AI ๋ถ„์„ ๋ฒ„ํŠผ์šฉ) const { data: aiRecommendations, refetch: refetchRecommendations, isLoading: recommendationsLoading, error: recommendationsError } = useQuery({ queryKey: ['ai-recommendations', indicators], queryFn: async () => { console.log('๐Ÿ”ต Fetching recommendations with indicators:', indicators) try { const response = await api.get('/recommendations', { params: indicators }) console.log('๐ŸŸข Recommendations received:', response.data) return response.data } catch (error) { console.error('๐Ÿ”ด Recommendations fetch error:', error) throw error } }, enabled: false, // ์ˆ˜๋™์œผ๋กœ๋งŒ ํ˜ธ์ถœ }) // ์ถ”์ฒœ ์ข…๋ชฉ ์—๋Ÿฌ ๋กœ๊น… if (recommendationsError) { console.error('โŒ Recommendations query error:', recommendationsError) } // ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ ๋กœ๊น… if (ruleBasedRecommendations) { console.log('โœ… Rule-based Recommendations:', { total: ruleBasedRecommendations.total, activeRules: ruleBasedRecommendations.active_rules, found: ruleBasedRecommendations.recommendations?.length || 0 }) } // ์ˆ˜๋™ ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋กœ๊น… if (aiRecommendations) { console.log('โœ… AI Recommendations:', { total: aiRecommendations.total, analyzed: aiRecommendations.analyzed_stocks, found: aiRecommendations.recommendations?.length || 0 }) } const { data: chartData, isLoading: chartLoading, error: chartError } = useQuery({ queryKey: ['chart-data', selectedStock, chartPeriod], queryFn: async () => { if (!selectedStock) { console.log('โš ๏ธ No stock selected, skipping chart fetch') return null } console.log('๐Ÿ”ต Fetching chart data for:', selectedStock, 'period:', chartPeriod) try { const response = await api.get(`/market-data/chart/${selectedStock}`, { params: { period: chartPeriod }, }) console.log('๐ŸŸข Chart data received:', { stock_code: response.data.stock_code, selectedStock: selectedStock, dataLength: response.data.data?.length, firstPrice: response.data.data?.[0]?.close, match: response.data.stock_code === selectedStock }) return response.data } catch (error) { console.error('๐Ÿ”ด Chart data fetch error:', error) throw error } }, enabled: Boolean(selectedStock), refetchInterval: 60000, }) // ์ฐจํŠธ ์—๋Ÿฌ ๋กœ๊น… if (chartError) { console.error('โŒ Chart query error:', chartError) } // AI ์ข…ํ•ฉ ๋ถ„์„ (์„ ํƒ๋œ ์ข…๋ชฉ์— ๋Œ€ํ•ด) const { data: aiAnalysis, isLoading: aiAnalysisLoading } = useQuery({ queryKey: ['ai-analysis', selectedStock, chartData], queryFn: async () => { if (!selectedStock || !chartData?.data?.[0]) return null console.log('๐Ÿ”ต Fetching AI analysis for:', selectedStock) try { const response = await fetch('http://localhost:8001/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ analysis_type: 'MARKET_ANALYSIS', data: { stock_code: selectedStock, market_data: { stock_name: chartData?.stock_name || selectedStock, current_price: Number(chartData.data[0].close), change_rate: Number(chartData.data[0].change_rate || 0), volume: Number(chartData.data[0].volume || 0), high: Number(chartData.data[0].high), low: Number(chartData.data[0].low) }, news: [] } }) }) const result = await response.json() console.log('๐ŸŸข AI analysis received:', result) return result.result || result.analysis } catch (error) { console.error('๐Ÿ”ด AI analysis error:', error) return 'โš ๏ธ AI ๋ถ„์„ ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' } }, enabled: Boolean(selectedStock && chartData?.data?.[0]), refetchInterval: 300000, // 5๋ถ„๋งˆ๋‹ค }) // ๋ฆฌ์Šคํฌ ์ฒดํฌ const { data: riskCheck, isLoading: riskCheckLoading } = useQuery({ queryKey: ['risk-check', selectedStock, portfolio, chartData], queryFn: async () => { if (!selectedStock || !chartData?.data?.[0]) return null console.log('๐Ÿ”ต Fetching risk check for:', selectedStock) try { const response = await fetch('http://localhost:8001/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ analysis_type: 'RISK_ASSESSMENT', data: { portfolio: { total_value: portfolioSummary?.total_value || 0, total_stocks: portfolioSummary?.total_stocks || 0, holdings: portfolio || [] }, trade_plan: { stock_code: selectedStock, stock_name: chartData?.stock_name || selectedStock, current_price: Number(chartData.data[0].close), action: 'BUY', quantity: 1 } } }) }) const result = await response.json() console.log('๐ŸŸข Risk check received:', result) return result.result || result.analysis } catch (error) { console.error('๐Ÿ”ด Risk check error:', error) return 'โš ๏ธ ๋ฆฌ์Šคํฌ ๋ถ„์„ ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' } }, enabled: Boolean(selectedStock && chartData?.data?.[0]), refetchInterval: 300000, // 5๋ถ„๋งˆ๋‹ค }) // ์ฐจํŠธ ์ƒ๋‹จ์— ํ‘œ์‹œํ•  ์ข…๋ชฉ ๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ const displayStocks = useMemo(() => { // 1. ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ if (ruleBasedRecommendations?.recommendations_by_rule?.length > 0) { const stocks: any[] = [] ruleBasedRecommendations.recommendations_by_rule.forEach((ruleGroup: any) => { if (ruleGroup.stocks && stocks.length < 4) { ruleGroup.stocks.slice(0, 4 - stocks.length).forEach((stock: any) => { stocks.push(stock) }) } }) return stocks.slice(0, 4) } // 2. ํฌํŠธํด๋ฆฌ์˜ค๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ if (portfolio && portfolio.length > 0) { return [...portfolio] .sort((a: any, b: any) => (b.profit_loss_rate ?? 0) - (a.profit_loss_rate ?? 0)) .slice(0, 4) } // 3. ๋‘˜ ๋‹ค ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด (ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ) return [] }, [ruleBasedRecommendations, portfolio]) // ํ‘œ์‹œํ•  ์ข…๋ชฉ๋“ค์˜ ์ตœ์‹  ๊ฐ€๊ฒฉ ๊ฐ€์ ธ์˜ค๊ธฐ const stockCodesToFetch = displayStocks.map((item: any) => item.stock_code) const { data: stockPrices = {} } = useQuery>({ queryKey: ['all-stock-prices', stockCodesToFetch.join(',')], queryFn: async () => { if (stockCodesToFetch.length === 0) { return {} } const promises = stockCodesToFetch.map(async (code: string) => { try { const response = await api.get(`/market-data/chart/${code}`, { params: { period: 'D' }, }) // KIS API๋Š” ์ตœ์‹  ๋ฐ์ดํ„ฐ๊ฐ€ ์ฒซ๋ฒˆ์งธ (index 0) const latestPrice = response.data?.data?.[0]?.close || 0 return { code, price: Number(latestPrice) } } catch (error) { console.error(`Failed to fetch price for ${code}:`, error) return { code, price: 0 } } }) const results = await Promise.all(promises) return results.reduce((acc: Record, item) => { acc[item.code] = item.price return acc }, {}) }, enabled: stockCodesToFetch.length > 0, refetchInterval: 60000, }) const recommendedStocks = useMemo(() => { console.log('๐Ÿ’ก Building recommendedStocks:', { displayStocksCount: displayStocks.length, stockPricesCount: Object.keys(stockPrices).length, stockPrices }) return displayStocks.map((item: any) => { const priceFromQuery = stockPrices[item.stock_code] const finalPrice = priceFromQuery || item.current_price || 0 console.log(` - ${item.stock_code}: query=${priceFromQuery}, original=${item.current_price}, final=${finalPrice}`) return { ...item, current_price: finalPrice, } }) }, [displayStocks, stockPrices]) const currentStockPrice = useMemo(() => { if (!chartData?.data || chartData.data.length === 0) { console.log('โŒ currentStockPrice: No data') return null } // KIS API๋Š” ์ตœ์‹  ๋ฐ์ดํ„ฐ๊ฐ€ ์ฒซ๋ฒˆ์งธ (index 0) const price = Number(chartData.data[0].close) console.log('โœ… currentStockPrice:', price, 'from', chartData.data[0].date) return price }, [chartData]) // ์ดˆ๊ธฐ ์ข…๋ชฉ ์„ ํƒ (ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰) useEffect(() => { console.log('๐Ÿ“Š Initial stock selection check') // ์ด๋ฏธ ์ข…๋ชฉ์ด ์„ ํƒ๋˜์–ด ์žˆ์œผ๋ฉด ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ if (selectedStock && selectedStock !== '') { console.log('โœ… Stock already selected:', selectedStock) return } // ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ์ด ์žˆ์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์ข…๋ชฉ ์„ ํƒ if (ruleBasedRecommendations?.recommendations_by_rule?.length > 0) { const firstRule = ruleBasedRecommendations.recommendations_by_rule[0] if (firstRule.stocks?.length > 0) { const firstStock = firstRule.stocks[0] console.log('๐ŸŽฏ Auto-selecting first rule-based recommendation:', firstStock.stock_code) setSelectedStock(firstStock.stock_code) return } } // ํฌํŠธํด๋ฆฌ์˜ค๊ฐ€ ์žˆ์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์ข…๋ชฉ ์„ ํƒ if (portfolio && portfolio.length > 0) { console.log('๐ŸŽฏ Auto-selecting first portfolio stock:', portfolio[0].stock_code) setSelectedStock(portfolio[0].stock_code) return } // ์•„๋ฌด๊ฒƒ๋„ ์—†์œผ๋ฉด ์ข…๋ชฉ ์„ ํƒ ์—†์Œ (ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ) console.log('โš ๏ธ No stocks available') }, [ruleBasedRecommendations, portfolio]) // selectedStock์€ ์˜์กด์„ฑ์—์„œ ์ œ๊ฑฐ const chartSeries = useMemo(() => { if (!selectedStock) { console.log('โš ๏ธ chartSeries: No stock selected') return [] } if (!chartData?.data || chartData.data.length === 0) { console.log('โŒ chartSeries: No data', chartData) return [] } // ์ฐจํŠธ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋œ ์ข…๋ชฉ๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ if (chartData.stock_code !== selectedStock) { console.error('๐Ÿ”ด CRITICAL: Chart data mismatch!', { chartStock: chartData.stock_code, selectedStock: selectedStock, chartName: chartData.stock_name, dataLength: chartData.data.length }) // ๋ฐ์ดํ„ฐ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜ (์ƒˆ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ๋Œ€๊ธฐ) return [] } console.log('โœ… Chart data matches selected stock:', selectedStock) console.log('โœ… Raw chartData:', { stock: chartData.stock_code, dataLength: chartData.data.length, first: chartData.data[0], last: chartData.data[chartData.data.length - 1] }) // KIS API๋Š” ์ตœ์‹  ๋ฐ์ดํ„ฐ๊ฐ€ ์ฒซ๋ฒˆ์งธ (20251104, 20251103, ...) // ์ฐจํŠธ๋Š” ์˜ค๋ž˜๋œ ๊ฒƒ๋ถ€ํ„ฐ ์ตœ์‹  ์ˆœ์„œ๋กœ ํ‘œ์‹œํ•ด์•ผ ํ•˜๋ฏ€๋กœ reverse ํ•„์š” const sortedData = [...chartData.data] .reverse() // ์˜ค๋ž˜๋œ ๋‚ ์งœ๋ถ€ํ„ฐ ์ •๋ ฌ // KIS API๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ (slice ์ œ๊ฑฐ) .filter((item: any) => { // ์œ ํšจํ•œ ๋ฐ์ดํ„ฐ๋งŒ ํ•„ํ„ฐ๋ง (0์ด ์•„๋‹Œ ๊ฐ€๊ฒฉ) return item.open > 0 && item.high > 0 && item.low > 0 && item.close > 0 }) .map((item: any) => ({ date: item.date, dateLabel: item.date ? `${item.date.slice(4, 6)}/${item.date.slice(6, 8)}` : '', open: Number(item.open), high: Number(item.high), low: Number(item.low), close: Number(item.close), volume: Number(item.volume), })) console.log('โœ… Chart series processed:', { length: sortedData.length, first: sortedData[0], last: sortedData[sortedData.length - 1] }) if (sortedData.length === 0) { console.log('โŒ All data filtered out (invalid prices)') } return sortedData }, [chartData, selectedStock]) const formatCurrency = (value: number) => { if (!value) { return '0์›' } return new Intl.NumberFormat('ko-KR').format(value) + '์›' } const formatPercent = (value: number) => { if (!value) { return '+0.00%' } return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%` } const formatDate = (dateStr: string) => { if (!dateStr || dateStr.length !== 8) return '' // YYYYMMDD โ†’ MM/DD return `${dateStr.slice(4, 6)}/${dateStr.slice(6, 8)}` } const getTradeStatusClass = (status: string) => { switch (status) { case 'EXECUTED': return 'executed' case 'PENDING': return 'pending' case 'FAILED': return 'failed' default: return '' } } const handleStockSelect = (stockCode: string) => { setSelectedStock(stockCode) } return (
๊ณ„์ขŒ ์ •๋ณด
์˜ˆ์ˆ˜๊ธˆ 10,000,000์›
์ด ์ž์‚ฐ {formatCurrency((portfolioSummary?.total_value || 0) + 10000000)}
์ด ์†์ต = 0 ? 'up' : 'down'}> {formatCurrency(portfolioSummary?.total_profit_loss || 0)}
์ˆ˜์ต๋ฅ  = 0 ? 'up' : 'down'}> {formatPercent(portfolioSummary?.total_profit_loss_rate || 0)}
๋ณด์œ  ์ข…๋ชฉ {portfolioSummary?.total_stocks || 0}๊ฐœ
๋ณด์œ  ์ข…๋ชฉ
{portfolio && portfolio.length > 0 ? ( portfolio.map((item: any) => (
handleStockSelect(item.stock_code)} style={{ cursor: 'pointer' }} >
{item.stock_name} ({item.stock_code})
{item.quantity}์ฃผ {formatCurrency(item.current_price)} = 0 ? 'up' : 'down'}> {formatPercent(item.profit_loss_rate || 0)}
)) ) : (
๋ณด์œ  ์ข…๋ชฉ ์—†์Œ
)}
๋Œ€ํ‘œ ์ข…๋ชฉ ์ฐจํŠธ
{recommendedStocks.length > 0 ? ( recommendedStocks.map((item: any) => { const isSelected = selectedStock === item.stock_code // stockPrices์—์„œ ์ตœ์‹  ๊ฐ€๊ฒฉ ๊ฐ€์ ธ์˜ค๊ธฐ (์ฐจํŠธ ์ƒ๋‹จ๊ณผ ์ถ”์ฒœ ์ข…๋ชฉ ๋™์ผํ•œ ๊ฐ€๊ฒฉ) const displayPrice = stockPrices[item.stock_code] || item.current_price || 0 return ( ) }) ) : (
์ถ”์ฒœ ์ข…๋ชฉ ๋กœ๋”ฉ ์ค‘...
)}
{/* MCP ์ƒํƒœ UI ์ œ๊ฑฐ๋จ */} {/* ์‹ค์‹œ๊ฐ„ ์—ฐ๊ฒฐ UI ์ œ๊ฑฐ๋จ */}
{(['D', 'W', 'M'] as const).map((period) => ( ))}
{chartLoading &&
์ฐจํŠธ ๋กœ๋”ฉ ์ค‘...
} {!chartLoading && chartSeries.length === 0 && (
์ฐจํŠธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
)} {!chartLoading && chartSeries.length > 0 && (
)}
์ตœ๊ทผ ๊ฑฐ๋ž˜ ๋‚ด์—ญ
{recentTrades && recentTrades.length > 0 ? ( recentTrades.map((trade: any) => ( )) ) : ( )}
์‹œ๊ฐ„ ์ข…๋ชฉ ๊ตฌ๋ถ„ ์ˆ˜๋Ÿ‰ ๊ฐ€๊ฒฉ ์ƒํƒœ
{new Date(trade.created_at).toLocaleTimeString('ko-KR')} {trade.stock_name} {trade.order_type} {trade.order_quantity} {formatCurrency(trade.order_price || 0)} {trade.status}
๊ฑฐ๋ž˜ ๋‚ด์—ญ ์—†์Œ
์ถ”์ฒœ ์ข…๋ชฉ
{(() => { console.log('๐ŸŽฏ Rendering recommend-list:', { hasRule: !!ruleBasedRecommendations, ruleLength: ruleBasedRecommendations?.recommendations?.length || 0, hasAI: !!aiRecommendations, aiLength: aiRecommendations?.recommendations?.length || 0, portfolioLength: recommendedStocks.length }) return null })()} {/* 1. ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ (๊ทœ์น™๋ณ„ ๊ทธ๋ฃนํ•‘) */} {ruleBasedRecommendations && ruleBasedRecommendations.recommendations_by_rule?.length > 0 ? ( ruleBasedRecommendations.recommendations_by_rule.map((ruleGroup: any) => (
{/* ๊ทœ์น™ ํ—ค๋” */}
๊ทœ์น™ {ruleGroup.priority} {ruleGroup.rule_name} ({ruleGroup.matched_count}๊ฐœ ์ข…๋ชฉ ๋งค์นญ)
{/* ์ข…๋ชฉ ๋ฆฌ์ŠคํŠธ */} {ruleGroup.stocks.map((item: any) => { // stockPrices์—์„œ ์ตœ์‹  ๊ฐ€๊ฒฉ ๊ฐ€์ ธ์˜ค๊ธฐ (์ˆซ์ž ๋ณ€ํ™˜) const latestPrice = Number(stockPrices[item.stock_code] || item.current_price || 0) return ( ) })}
)) ) : aiRecommendations && aiRecommendations.recommendations?.length > 0 ? ( /* 2. ์ˆ˜๋™ ์ง€ํ‘œ ๋ถ„์„ ์ถ”์ฒœ */ aiRecommendations.recommendations.map((item: any) => { // stockPrices์—์„œ ์ตœ์‹  ๊ฐ€๊ฒฉ ๊ฐ€์ ธ์˜ค๊ธฐ (์ˆซ์ž ๋ณ€ํ™˜) const latestPrice = Number(stockPrices[item.stock_code] || item.current_price || 0) return ( ) }) ) : recommendedStocks.length > 0 ? ( recommendedStocks.map((item: any) => { const displayPrice = Number(selectedStock === item.stock_code && currentStockPrice ? currentStockPrice : item.current_price) return ( ) }) ) : (
๋ณด์œ  ์ข…๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค
)}
{/* AI ๋ถ„์„ ์„น์…˜ */}
๐Ÿค– AI ์ข…ํ•ฉ ๋ถ„์„
{aiAnalysisLoading ? (
๋ถ„์„ ์ค‘...
) : aiAnalysis ? ( typeof aiAnalysis === 'string' ? (
{aiAnalysis}
) : typeof aiAnalysis === 'object' && aiAnalysis !== null ? (
{aiAnalysis.analysis || aiAnalysis.recommendation || JSON.stringify(aiAnalysis, null, 2)}
) : (
๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค
) ) : (
์ข…๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”
)}
{/* ๋ฆฌ์Šคํฌ ์ฒดํฌ ์„น์…˜ */}
โš ๏ธ ๋ฆฌ์Šคํฌ ์ฒดํฌ
{riskCheckLoading ? (
๋ถ„์„ ์ค‘...
) : riskCheck ? ( typeof riskCheck === 'string' ? (
{riskCheck}
) : typeof riskCheck === 'object' && riskCheck !== null ? (
{riskCheck.analysis || riskCheck.recommendation || JSON.stringify(riskCheck, null, 2)}
) : (
๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค
) ) : (
์ข…๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”
)}
์‹œ์žฅ ๋‰ด์Šค
{news && news.length > 0 ? ( // ์„ ํƒ๋œ ์ข…๋ชฉ์— ํ•ด๋‹นํ•˜๋Š” ๋‰ด์Šค๋งŒ ํ•„ํ„ฐ๋ง news.filter((item: any) => !selectedStock || item.stock_code === selectedStock) .slice(0, 5) .map((item: any) => (
window.open(item.url, '_blank')}>
{item.title}
{new Date(item.published_at).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', })}
)) ) : (
๋‰ด์Šค ์—†์Œ
)}
) } export default Dashboard