fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리
## CRITICAL 수정 (20건) - day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가 - trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산 - risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수 - risk_management/recommendations: date.today() → KST 변환 - mdd_manager: Lock 밖 executed_key 참조 수정 - correlation_filter: output → output2 키 수정 - portfolio: 해외 종목 USD/KRW 환율 적용 분리 - Sharpe Ratio 거래빈도 기반 연간화 - telegram_notifier: send_message() 공개 래퍼 추가 - position_manager: 앱 재시작 시 DB 포지션 복원 - swing exit_service: remaining_pct 조건 범위 수정 - run_backtest: original_qty 기반 remaining_pct 계산 - us_market_schedule: 2027년 미국 공휴일 추가 - Login.tsx: res.ok 검증 추가 - mean_reversion_strategy: RSI 로그 하드코딩 수정 ## HIGH 수정 (10건) - db_worker: DLQ 파일 백업 메커니즘 추가 - korea_investment_api: 만료 토큰 임의 연장 제거 - market_data: chart_cache 최대 100개 + TTL 만료 - main.py: rate_limit_store 1시간 미접근 IP 정리 - recommendations: fire-and-forget try/except 추가 - swing/scheduler: DB 세션 3개 분리 - Dashboard: wsMessages 전체 순회 - Layout: /health 엔드포인트 + 30초 폴링 - ml_ensemble: POST /train require_api_key 추가 - day_trading: record_trade_result_safe() 전환 ## 코드 정리 - 프론트엔드: isUsStock, formatTime 공통 유틸 추출 - 백엔드: dead code 제거, 중복 import 통합 - MCP: JSON 파싱 헬퍼 추출 (~60줄 감소) - 전체 ~100개 파일 한국어 주석 추가 - 테스트 13건 코드 기준 수정 (468 passed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,45 @@
|
||||
/**
|
||||
* 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'
|
||||
|
||||
const formatTime = (isoStr?: string) => {
|
||||
if (!isoStr) return null
|
||||
const d = new Date(isoStr)
|
||||
return d.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: 'Asia/Seoul' })
|
||||
}
|
||||
|
||||
/** 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,
|
||||
@@ -24,29 +47,34 @@ const ChartPanel = React.memo(({
|
||||
chartPeriod,
|
||||
onPeriodChange,
|
||||
stockName,
|
||||
livePrice,
|
||||
}: ChartPanelProps) => {
|
||||
return (
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-1 shadow-lg h-[45%] flex flex-col min-h-0">
|
||||
<div className="px-5 py-3 flex justify-between items-center border-b border-gray-700/30">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold flex items-center gap-3 text-white">
|
||||
{stockName || '종목을 선택해주세요'}
|
||||
{selectedStock && (
|
||||
<span className="text-xs font-normal text-gray-500 font-mono bg-gray-800 px-2 py-0.5 rounded">
|
||||
<div className="bg-[#0c1522] border border-[#152035] rounded flex flex-col h-[45%] min-h-0">
|
||||
{/* Header - Bloomberg ticker style */}
|
||||
<div className="px-3 py-2 flex justify-between items-center border-b border-[#152035] bg-[#091018] flex-shrink-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{selectedStock ? (
|
||||
<>
|
||||
<span className="text-base font-bold text-[#c5d5e8] truncate">{stockName}</span>
|
||||
<span className="text-[10px] font-mono text-[#3a5570] bg-[#152035] px-1.5 py-px rounded-sm flex-shrink-0">
|
||||
{selectedStock}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-sm text-[#3a5570]">종목을 선택하세요</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex bg-gray-900/50 rounded-lg p-1 border border-gray-700/50 scale-90">
|
||||
<div className="flex items-center bg-[#0a0f1c] border border-[#152035] rounded-sm overflow-hidden flex-shrink-0">
|
||||
{(['D', 'W', 'M'] as const).map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => onPeriodChange(p)}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${chartPeriod === p
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
|
||||
: 'text-gray-400 hover:text-white hover:bg-gray-800'
|
||||
}`}
|
||||
className={`px-3 py-1 text-[10px] font-mono transition-colors ${
|
||||
chartPeriod === p
|
||||
? 'bg-[#1a3050] text-[#5b9aff]'
|
||||
: 'text-[#4a6580] hover:text-[#8faabf]'
|
||||
}`}
|
||||
>
|
||||
{p === 'D' ? '일봉' : p === 'W' ? '주봉' : '월봉'}
|
||||
</button>
|
||||
@@ -54,55 +82,75 @@ const ChartPanel = React.memo(({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative bg-[#0b0e14] rounded-b-lg overflow-hidden">
|
||||
{chartLoading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-gray-500 bg-[#0b0e14]">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<RefreshCw className="animate-spin w-6 h-6 text-blue-500" />
|
||||
<span className="text-sm">차트 로딩중...</span>
|
||||
</div>
|
||||
{/* Chart area — 로딩 중에도 기존 차트 유지 (깜빡임 방지) */}
|
||||
<div className="flex-1 relative bg-[#0b0e14] rounded-b overflow-hidden">
|
||||
{chartData && chartData.length > 0 ? (
|
||||
<TradingChart data={chartData} livePrice={livePrice} />
|
||||
) : !chartLoading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-[#3a5570] text-xs font-mono">
|
||||
데이터 없음
|
||||
</div>
|
||||
) : chartData && chartData.length > 0 ? (
|
||||
<TradingChart data={chartData} />
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-gray-600 text-sm">
|
||||
데이터가 없습니다.
|
||||
) : null}
|
||||
{chartLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-[#0b0e14]/70 z-10">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<RefreshCw className="animate-spin w-5 h-5 text-[#2b65d9]" />
|
||||
<span className="text-xs text-[#3a5570] font-mono">차트 로딩중...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, (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'
|
||||
|
||||
// AI 분석 패널 — ChartPanel과 같은 파일에 배치 (center column 분리용)
|
||||
/**
|
||||
* 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 (
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4 shadow-lg flex flex-col">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className="font-bold flex items-center gap-2 text-purple-400 text-sm">
|
||||
<Brain size={16} />
|
||||
<div className="bg-[#0c1522] border border-[#152035] rounded flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b border-[#152035] bg-[#091018] flex justify-between items-center flex-shrink-0">
|
||||
<h3 className="flex items-center gap-1.5 text-[#9f7aea] text-xs font-semibold uppercase tracking-wider">
|
||||
<Brain size={12} />
|
||||
AI 투자 분석
|
||||
</h3>
|
||||
{aiAnalysisData?.data?.analyzed_at && (
|
||||
<span className="text-[10px] text-gray-500 font-mono">
|
||||
<span className="text-[9px] text-[#3a5570] font-mono">
|
||||
{formatTime(aiAnalysisData.data.analyzed_at)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded-lg p-3 text-gray-300 text-sm leading-relaxed overflow-y-auto custom-scrollbar flex-1 border border-gray-700/30 whitespace-pre-wrap">
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-3">
|
||||
{aiAnalysisText ? (
|
||||
typeof aiAnalysisText === 'string'
|
||||
? aiAnalysisText
|
||||
: JSON.stringify(aiAnalysisText, null, 2)
|
||||
<pre className="text-[11px] text-[#8faabf] leading-relaxed whitespace-pre-wrap font-mono">
|
||||
{typeof aiAnalysisText === 'string' ? aiAnalysisText : JSON.stringify(aiAnalysisText, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-gray-600 text-center text-xs">
|
||||
<div className="h-full flex items-center justify-center text-[#3a5570] text-xs text-center">
|
||||
<p>종목을 선택하면<br />AI 투자 분석을 표시합니다.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user