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,11 +1,36 @@
|
||||
/**
|
||||
* useWebSocket.ts — WebSocket 연결 관리 커스텀 훅
|
||||
*
|
||||
* 역할:
|
||||
* - 백엔드 WebSocket 서버와의 지속 연결 관리
|
||||
* - 자동 재연결 (지수 백오프: 3초 ~ 최대 30초, 무제한 재시도)
|
||||
* - 30초 간격 ping/pong 헬스 체크 (Nginx 프록시 타임아웃 방지)
|
||||
* - 메시지 타입별 분기 처리 (market_update, chart_data, 기타)
|
||||
*
|
||||
* 메시지 라우팅:
|
||||
* - market_update: 콜백(onMarketUpdate)으로만 전달 (배열 누적 방지)
|
||||
* - chart_data: CustomEvent로 전달 (React 상태 우회 — StrictMode 안전)
|
||||
* - 기타: messages state 배열에 추가 (최대 100개 유지)
|
||||
*
|
||||
* StrictMode 안전:
|
||||
* - socketRef로 활성 WS 추적 → stale onclose/onopen 이벤트 무시
|
||||
* - mountedRef로 언마운트 후 재연결 방지
|
||||
*
|
||||
* @param clientId - 고유 클라이언트 ID (탭별 구분, 서버 덮어쓰기 방지)
|
||||
* @param options - 메시지 타입별 콜백 (onMarketUpdate, onChartData)
|
||||
* @returns { messages, connected, sendMessage }
|
||||
*/
|
||||
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { config } from '../config'
|
||||
|
||||
/** WebSocket 메시지 기본 구조 */
|
||||
interface WebSocketMessage {
|
||||
type: string
|
||||
data: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** useWebSocket 옵션 인터페이스 */
|
||||
interface UseWebSocketOptions {
|
||||
/** market_update 메시지 전용 콜백 (배열 누적 방지 — 실시간 틱 전용) */
|
||||
onMarketUpdate?: (data: unknown) => void
|
||||
@@ -25,23 +50,43 @@ export const useWebSocket = (clientId: string, options?: UseWebSocketOptions) =>
|
||||
// 콜백을 ref로 유지 — connect 클로저가 재생성되지 않아도 최신 콜백 참조
|
||||
const onMarketUpdateRef = useRef(options?.onMarketUpdate)
|
||||
const onChartDataRef = useRef(options?.onChartData)
|
||||
// 렌더 중 동기적으로 ref 업데이트 (useEffect 대기 없이 즉시 반영)
|
||||
onMarketUpdateRef.current = options?.onMarketUpdate
|
||||
onChartDataRef.current = options?.onChartData
|
||||
/** 재연결 기본 딜레이 (3초, 지수 백오프 적용) */
|
||||
const baseDelay = 3000
|
||||
/** 마지막 pong 수신 시각 (타임아웃 판단용) */
|
||||
const lastPongReceivedRef = useRef<number>(Date.now())
|
||||
const pongTimeoutMs = 10000 // 10초 이내에 pong을 받지 못하면 연결 끊김으로 간주
|
||||
|
||||
// options 변경 시 ref 동기화
|
||||
useEffect(() => {
|
||||
onMarketUpdateRef.current = options?.onMarketUpdate
|
||||
onChartDataRef.current = options?.onChartData
|
||||
})
|
||||
/** pong 타임아웃 (30초 이내 미수신 시 연결 끊김 간주) */
|
||||
const pongTimeoutMs = 30000
|
||||
|
||||
/**
|
||||
* WebSocket 연결 생성 함수
|
||||
*
|
||||
* 연결 흐름:
|
||||
* 1. 기존 연결 정리 (이중 연결 방지)
|
||||
* 2. API 키 포함 WS URL로 연결 시도
|
||||
* 3. onopen: 연결 성공, ping 인터벌 시작
|
||||
* 4. onmessage: 메시지 타입별 라우팅
|
||||
* 5. onclose: 재연결 스케줄링 (지수 백오프)
|
||||
*/
|
||||
const connect = useCallback(() => {
|
||||
if (!mountedRef.current) return
|
||||
|
||||
// 기존 연결이 남아있으면 정리 (이중 연결 방지)
|
||||
if (socketRef.current) {
|
||||
try { socketRef.current.close() } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(`${config.wsBaseUrl}/ws/${clientId}`)
|
||||
const apiKey = localStorage.getItem('api_key') || ''
|
||||
const ws = new WebSocket(`${config.wsBaseUrl}/ws/${clientId}?token=${encodeURIComponent(apiKey)}`)
|
||||
// 즉시 socketRef에 할당 — onclose 가드가 null 상태를 보지 않도록
|
||||
socketRef.current = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
// StrictMode guard: 이미 다른 WS가 활성화되었으면 이 이벤트 무시
|
||||
if (socketRef.current !== ws) { ws.close(); return }
|
||||
console.log('🔗 WebSocket connected')
|
||||
setConnected(true)
|
||||
reconnectAttemptsRef.current = 0
|
||||
@@ -52,7 +97,7 @@ export const useWebSocket = (clientId: string, options?: UseWebSocketOptions) =>
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'ping' }))
|
||||
|
||||
// Pong 타임아웃 설정 (10초 이내에 pong 응답이 없으면 연결 끊김으로 간주)
|
||||
// Pong 타임아웃 설정 (30초 이내에 pong 응답이 없으면 연결 끊김으로 간주)
|
||||
if (pongTimeoutRef.current) {
|
||||
clearTimeout(pongTimeoutRef.current)
|
||||
}
|
||||
@@ -87,13 +132,14 @@ export const useWebSocket = (clientId: string, options?: UseWebSocketOptions) =>
|
||||
return
|
||||
}
|
||||
|
||||
// chart_data: 콜백으로만 전달 (배열 누적 방지 — 차트 로드 전용)
|
||||
// chart_data: CustomEvent로 직접 전달 (React 상태 우회 — StrictMode 타이밍 이슈 방지)
|
||||
if (message.type === 'chart_data') {
|
||||
onChartDataRef.current?.(message)
|
||||
console.log('📬 chart_data → CustomEvent', (message as any).stock_code)
|
||||
window.dispatchEvent(new CustomEvent('ws-chart-data', { detail: message }))
|
||||
return
|
||||
}
|
||||
|
||||
setMessages((prev: WebSocketMessage[]) => [...prev, message])
|
||||
setMessages((prev: WebSocketMessage[]) => [...prev.slice(-99), message])
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error)
|
||||
}
|
||||
@@ -104,6 +150,10 @@ export const useWebSocket = (clientId: string, options?: UseWebSocketOptions) =>
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
// StrictMode guard: 이미 다른 WS가 활성화되었으면 stale 이벤트 무시
|
||||
// (cleanup에서 닫힌 WS의 onclose가 새 WS에 영향 주는 것을 방지)
|
||||
if (socketRef.current !== null && socketRef.current !== ws) return
|
||||
|
||||
console.log('🔌 WebSocket disconnected')
|
||||
setConnected(false)
|
||||
socketRef.current = null
|
||||
@@ -130,7 +180,6 @@ export const useWebSocket = (clientId: string, options?: UseWebSocketOptions) =>
|
||||
}
|
||||
}
|
||||
|
||||
socketRef.current = ws
|
||||
} catch (error) {
|
||||
console.error('Failed to create WebSocket:', error)
|
||||
}
|
||||
@@ -157,6 +206,10 @@ export const useWebSocket = (clientId: string, options?: UseWebSocketOptions) =>
|
||||
}
|
||||
}, [connect])
|
||||
|
||||
/**
|
||||
* WebSocket 메시지 전송 함수
|
||||
* 연결이 OPEN 상태일 때만 전송 (그 외 상태에서는 무시)
|
||||
*/
|
||||
const sendMessage = useCallback((message: string) => {
|
||||
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
|
||||
socketRef.current.send(message)
|
||||
|
||||
Reference in New Issue
Block a user