/** * ChartPanel.tsx — 차트 패널 및 AI 분석 패널 컴포넌트 * * 역할: * - ChartPanel: 종목 캔들 차트 표시 (TradingChart 래퍼), 일/주/월봉 기간 전환 * - AiAnalysisPanel: 선택 종목의 AI 투자 분석 텍스트 표시 * * 최적화: * - React.memo + 커스텀 비교 함수로 livePrice 변경 시 리렌더 방지 * (livePrice는 TradingChart 내부에서 ref 기반으로 처리) * - 차트 로딩 중에도 기존 차트를 유지하여 깜빡임 방지 */ import React from 'react' import { Brain, RefreshCw } from 'lucide-react' import { TradingChart } from './TradingChart' import { formatTime } from '../utils/formatUtils' /** ChartPanel Props 정의 */ interface ChartPanelProps { /** 캔들 차트 데이터 배열 (null이면 데이터 없음 표시) */ chartData: any[] | null | undefined /** 차트 로딩 상태 (오버레이 스피너 표시 여부) */ chartLoading: boolean /** 현재 선택된 종목 코드 */ selectedStock: string /** 차트 기간 (D: 일봉, W: 주봉, M: 월봉) */ chartPeriod: 'D' | 'W' | 'M' /** 기간 변경 콜백 */ onPeriodChange: (period: 'D' | 'W' | 'M') => void /** 차트 헤더에 표시할 종목명 */ stockName: string /** 실시간 현재가 (TradingChart에서 ref 기반 처리) */ livePrice?: number } /** * ChartPanel — 종목 캔들 차트 패널 * * React.memo 커스텀 비교 함수: * - livePrice 변경은 무시 (TradingChart가 ref 기반 500ms polling으로 직접 처리) * - chartData, chartLoading, selectedStock, chartPeriod 등 변경 시에만 리렌더 */ const ChartPanel = React.memo(({ chartData, chartLoading, selectedStock, chartPeriod, onPeriodChange, stockName, livePrice, }: ChartPanelProps) => { return (
{/* Header - Bloomberg ticker style */}
{selectedStock ? ( <> {stockName} {selectedStock} ) : ( 종목을 선택하세요 )}
{(['D', 'W', 'M'] as const).map((p) => ( ))}
{/* Chart area — 로딩 중에도 기존 차트 유지 (깜빡임 방지) */}
{chartData && chartData.length > 0 ? ( ) : !chartLoading ? (
데이터 없음
) : null} {chartLoading && (
차트 로딩중...
)}
) }, (prev, next) => { // livePrice 변경은 TradingChart 내부에서 ref 기반 처리 — ChartPanel 리렌더 불필요 if (prev.chartData !== next.chartData) return false if (prev.chartLoading !== next.chartLoading) return false if (prev.selectedStock !== next.selectedStock) return false if (prev.chartPeriod !== next.chartPeriod) return false if (prev.stockName !== next.stockName) return false if (prev.onPeriodChange !== next.onPeriodChange) return false // livePrice 변경은 무시 (TradingChart가 ref로 직접 처리) return true }) ChartPanel.displayName = 'ChartPanel' /** * AiAnalysisPanel — AI 투자 분석 결과 표시 패널 * * 선택된 종목의 기술적 지표, 추천 방향, AI 분석 근거를 텍스트로 표시합니다. * 종목 미선택 시 안내 메시지를 표시합니다. */ interface AiAnalysisPanelProps { /** AI 분석 데이터 (analysis: 포맷된 텍스트, data: 원시 데이터) */ aiAnalysisData: { analysis: string; data: any } | null | undefined } export const AiAnalysisPanel = React.memo(({ aiAnalysisData }: AiAnalysisPanelProps) => { const aiAnalysisText = aiAnalysisData?.analysis || null return (
{/* Header */}

AI 투자 분석

{aiAnalysisData?.data?.analyzed_at && ( {formatTime(aiAnalysisData.data.analyzed_at)} )}
{/* 매수 추천 금액 — 실제 자동매매 사이징과 동일 기준 (총자산×종목당 비중, 신뢰도 반영) */} {aiAnalysisData?.data && (
매수 추천 {aiAnalysisData.data.suggested_quantity > 0 ? ( <> {Number(aiAnalysisData.data.suggested_amount).toLocaleString('ko-KR')}원 {aiAnalysisData.data.suggested_quantity}주 총자산 {aiAnalysisData.data.position_ratio}% ) : ( {aiAnalysisData.data.sizing_note || '매수 제안 없음'} )}
{aiAnalysisData.data.suggested_quantity > 0 && aiAnalysisData.data.sizing_note && (
{aiAnalysisData.data.sizing_note}
)} {/* 목표가 · 손절가 (CLAUDE.md §5.2: 손절은 ATR/퍼센트 중 타이트한 쪽, 목표는 +2R) */} {aiAnalysisData.data.target_price > 0 && (
목표 {Number(aiAnalysisData.data.target_price).toLocaleString('ko-KR')}원 (+{aiAnalysisData.data.target_pct}%) 손절 {Number(aiAnalysisData.data.stop_loss_price).toLocaleString('ko-KR')}원 ({aiAnalysisData.data.stop_loss_pct}%) R:R 1:{aiAnalysisData.data.risk_reward}
)} {aiAnalysisData.data.target_note && (
{aiAnalysisData.data.target_note}
)}
)} {/* Content */}
{aiAnalysisText ? (
                        {typeof aiAnalysisText === 'string' ? aiAnalysisText : JSON.stringify(aiAnalysisText, null, 2)}
                    
) : (

종목을 선택하면
AI 투자 분석을 표시합니다.

)}
) }) AiAnalysisPanel.displayName = 'AiAnalysisPanel' export default ChartPanel