2026-02-13 12:17:35 +09:00
|
|
|
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 {
|
2026-02-23 15:37:22 +09:00
|
|
|
extractKisRealtimeTrId,
|
|
|
|
|
hasMeaningfulOrderBookPayload,
|
2026-02-13 12:17:35 +09:00
|
|
|
parseKisRealtimeOrderbook,
|
|
|
|
|
resolveOrderBookTrIds,
|
2026-02-23 15:37:22 +09:00
|
|
|
shouldAcceptRealtimeMessageByPriority,
|
2026-02-13 12:17:35 +09:00
|
|
|
} from "@/features/trade/utils/kisRealtimeUtils";
|
|
|
|
|
import type { DomesticKisSession } from "@/lib/kis/domestic-market-session";
|
|
|
|
|
|
|
|
|
|
interface UseOrderbookSubscriptionParams {
|
|
|
|
|
symbol: string | undefined; // orderBookSymbol
|
2026-02-23 15:37:22 +09:00
|
|
|
market: "KOSPI" | "KOSDAQ" | undefined;
|
2026-02-13 12:17:35 +09:00
|
|
|
isVerified: boolean;
|
|
|
|
|
credentials: KisRuntimeCredentials | null;
|
|
|
|
|
marketSession: DomesticKisSession;
|
|
|
|
|
onOrderBookMessage?: (data: DashboardStockOrderBookResponse) => void;
|
|
|
|
|
}
|
2026-02-23 15:37:22 +09:00
|
|
|
const STABLE_SOURCE_STALE_MS = Number.POSITIVE_INFINITY;
|
2026-02-13 12:17:35 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description 실시간 호가(Orderbook) 구독 로직을 담당하는 훅입니다.
|
|
|
|
|
* - 호가 데이터는 빈도가 매우 높으므로 별도의 상태(state)에 저장하지 않고,
|
|
|
|
|
* - 콜백 함수(onOrderBookMessage)를 통해 상위 컴포넌트로 데이터를 직접 전달합니다.
|
|
|
|
|
* - 이를 통해 불필요한 리렌더링을 방지합니다.
|
|
|
|
|
*/
|
|
|
|
|
export function useOrderbookSubscription({
|
|
|
|
|
symbol,
|
2026-02-23 15:37:22 +09:00
|
|
|
market,
|
2026-02-13 12:17:35 +09:00
|
|
|
isVerified,
|
|
|
|
|
credentials,
|
|
|
|
|
marketSession,
|
|
|
|
|
onOrderBookMessage,
|
|
|
|
|
}: UseOrderbookSubscriptionParams) {
|
2026-02-24 15:43:56 +09:00
|
|
|
const subscribeRef = useRef(useKisWebSocketStore.getState().subscribe);
|
|
|
|
|
const connectRef = useRef(useKisWebSocketStore.getState().connect);
|
2026-02-13 12:17:35 +09:00
|
|
|
const onOrderBookMessageRef = useRef(onOrderBookMessage);
|
2026-02-23 15:37:22 +09:00
|
|
|
const activeOrderBookTrIdRef = useRef<string | null>(null);
|
|
|
|
|
const activeOrderBookTrUpdatedAtRef = useRef(0);
|
2026-02-13 12:17:35 +09:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
onOrderBookMessageRef.current = onOrderBookMessage;
|
|
|
|
|
}, [onOrderBookMessage]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!symbol || !isVerified || !credentials) return;
|
|
|
|
|
|
2026-02-24 15:43:56 +09:00
|
|
|
connectRef.current();
|
2026-02-13 12:17:35 +09:00
|
|
|
|
2026-02-23 15:37:22 +09:00
|
|
|
const trIds = resolveOrderBookTrIds(
|
|
|
|
|
credentials.tradingEnv,
|
|
|
|
|
marketSession,
|
|
|
|
|
market,
|
|
|
|
|
);
|
2026-02-13 12:17:35 +09:00
|
|
|
const unsubscribers: Array<() => void> = [];
|
|
|
|
|
|
|
|
|
|
const handleOrderBookMessage = (data: string) => {
|
2026-02-23 15:37:22 +09:00
|
|
|
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;
|
|
|
|
|
|
2026-02-13 12:17:35 +09:00
|
|
|
const ob = parseKisRealtimeOrderbook(data, symbol);
|
|
|
|
|
if (ob) {
|
2026-02-23 15:37:22 +09:00
|
|
|
if (hasMeaningfulOrderBookPayload(ob)) {
|
|
|
|
|
activeOrderBookTrIdRef.current = incomingTrId;
|
|
|
|
|
activeOrderBookTrUpdatedAtRef.current = Date.now();
|
|
|
|
|
}
|
2026-02-13 12:17:35 +09:00
|
|
|
ob.tradingEnv = credentials.tradingEnv;
|
|
|
|
|
onOrderBookMessageRef.current?.(ob);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const trId of trIds) {
|
2026-02-24 15:43:56 +09:00
|
|
|
unsubscribers.push(
|
|
|
|
|
subscribeRef.current(trId, symbol, handleOrderBookMessage),
|
|
|
|
|
);
|
2026-02-13 12:17:35 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
unsubscribers.forEach((unsub) => unsub());
|
2026-02-23 15:37:22 +09:00
|
|
|
activeOrderBookTrIdRef.current = null;
|
|
|
|
|
activeOrderBookTrUpdatedAtRef.current = 0;
|
2026-02-13 12:17:35 +09:00
|
|
|
};
|
2026-02-24 15:43:56 +09:00
|
|
|
}, [symbol, market, isVerified, credentials, marketSession]);
|
2026-02-13 12:17:35 +09:00
|
|
|
}
|