import { useEffect, useRef, useState } from "react"; import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardStockOrderBookResponse } from "@/features/trade/types/trade.types"; import { fetchStockOrderBook } from "@/features/trade/apis/kis-stock.api"; import { toast } from "sonner"; /** * @description 초기 REST 호가를 한 번 조회하고, 이후에는 웹소켓 호가를 우선 사용합니다. * 웹소켓 호가 데이터는 TradeContainer에서 useKisTradeWebSocket을 통해 * 단일 WebSocket으로 수신되어 externalRealtimeOrderBook으로 주입됩니다. * @see features/trade/components/TradeContainer.tsx 호가 데이터 흐름 * @see features/trade/components/orderbook/OrderBook.tsx 호가창 렌더링 데이터 공급 */ export function useOrderBook( symbol: string | undefined, market: "KOSPI" | "KOSDAQ" | undefined, credentials: KisRuntimeCredentials | null, isVerified: boolean, options: { enabled?: boolean; /** 체결 WS에서 받은 실시간 호가 데이터 (단일 WS 통합) */ externalRealtimeOrderBook?: DashboardStockOrderBookResponse | null; } = {}, ) { const { enabled = true, externalRealtimeOrderBook = null } = options; const isRequestEnabled = enabled && !!symbol && !!credentials; const requestSeqRef = useRef(0); const lastErrorToastRef = useRef(""); const [initialData, setInitialData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!isRequestEnabled || !symbol || !credentials) { return; } const requestSeq = ++requestSeqRef.current; let isDisposed = false; const loadInitialOrderBook = async () => { setInitialData(null); setIsLoading(true); setError(null); try { const data = await fetchStockOrderBook(symbol, credentials); if (isDisposed || requestSeq !== requestSeqRef.current) return; setInitialData(data); } catch (err) { if (isDisposed || requestSeq !== requestSeqRef.current) return; console.error("Failed to fetch initial orderbook:", err); const message = err instanceof Error ? err.message : "호가 정보를 불러오지 못했습니다. 잠시 후 다시 시도해주세요."; setError(message); if (lastErrorToastRef.current !== message) { lastErrorToastRef.current = message; toast.error(message); } } finally { if (isDisposed || requestSeq !== requestSeqRef.current) return; setIsLoading(false); } }; void loadInitialOrderBook(); return () => { isDisposed = true; }; }, [isRequestEnabled, symbol, credentials]); // 외부 실시간 호가 → 초기 데이터 → null 순 우선 const orderBook = isRequestEnabled ? (externalRealtimeOrderBook ?? initialData) : null; const mergedError = isRequestEnabled ? error : null; const mergedLoading = isRequestEnabled ? isLoading && !orderBook : false; return { orderBook, isLoading: mergedLoading, error: mergedError, isWsConnected: !!externalRealtimeOrderBook, }; }