import { useRef, useEffect } from "react"; import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardStockOrderBookResponse } from "@/features/trade/types/trade.types"; import { useKisWebSocketStore } from "@/features/kis-realtime/stores/kisWebSocketStore"; import { extractKisRealtimeTrId, hasMeaningfulOrderBookPayload, parseKisRealtimeOrderbook, resolveOrderBookTrIds, shouldAcceptRealtimeMessageByPriority, } from "@/features/trade/utils/kisRealtimeUtils"; import type { DomesticKisSession } from "@/lib/kis/domestic-market-session"; interface UseOrderbookSubscriptionParams { symbol: string | undefined; // orderBookSymbol market: "KOSPI" | "KOSDAQ" | undefined; isVerified: boolean; credentials: KisRuntimeCredentials | null; marketSession: DomesticKisSession; onOrderBookMessage?: (data: DashboardStockOrderBookResponse) => void; } const STABLE_SOURCE_STALE_MS = Number.POSITIVE_INFINITY; /** * @description 실시간 호가(Orderbook) 구독 로직을 담당하는 훅입니다. * - 호가 데이터는 빈도가 매우 높으므로 별도의 상태(state)에 저장하지 않고, * - 콜백 함수(onOrderBookMessage)를 통해 상위 컴포넌트로 데이터를 직접 전달합니다. * - 이를 통해 불필요한 리렌더링을 방지합니다. */ export function useOrderbookSubscription({ symbol, market, isVerified, credentials, marketSession, onOrderBookMessage, }: UseOrderbookSubscriptionParams) { const { subscribe, connect } = useKisWebSocketStore(); const onOrderBookMessageRef = useRef(onOrderBookMessage); const activeOrderBookTrIdRef = useRef(null); const activeOrderBookTrUpdatedAtRef = useRef(0); useEffect(() => { onOrderBookMessageRef.current = onOrderBookMessage; }, [onOrderBookMessage]); useEffect(() => { if (!symbol || !isVerified || !credentials) return; connect(); const trIds = resolveOrderBookTrIds( credentials.tradingEnv, marketSession, market, ); const unsubscribers: Array<() => void> = []; const handleOrderBookMessage = (data: string) => { const incomingTrId = extractKisRealtimeTrId(data); if (!incomingTrId) return; // UI 흐름: 소켓 수신 -> TR 우선순위 고정(시장별 상위 소스 우선) -> 파싱 -> 호가 상태 반영 const shouldAccept = shouldAcceptRealtimeMessageByPriority({ incomingTrId, preferredTrIds: trIds, activeTrId: activeOrderBookTrIdRef.current, activeTrUpdatedAtMs: activeOrderBookTrUpdatedAtRef.current, // 정규장/동시호가에서는 한 번 잡은 상위 소스를 유지해 소스 간 왕복 반영을 막습니다. staleAfterMs: STABLE_SOURCE_STALE_MS, }); if (!shouldAccept) return; const ob = parseKisRealtimeOrderbook(data, symbol); if (ob) { if (hasMeaningfulOrderBookPayload(ob)) { activeOrderBookTrIdRef.current = incomingTrId; activeOrderBookTrUpdatedAtRef.current = Date.now(); } ob.tradingEnv = credentials.tradingEnv; onOrderBookMessageRef.current?.(ob); } }; for (const trId of trIds) { unsubscribers.push(subscribe(trId, symbol, handleOrderBookMessage)); } return () => { unsubscribers.forEach((unsub) => unsub()); activeOrderBookTrIdRef.current = null; activeOrderBookTrUpdatedAtRef.current = 0; }; }, [symbol, market, isVerified, credentials, marketSession, connect, subscribe]); }