import { useEffect, useRef, useState } from "react"; import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; import type { DashboardStockOrderBookResponse } from "@/features/dashboard/types/dashboard.types"; import { fetchStockOrderBook } from "@/features/dashboard/apis/kis-stock.api"; import { useKisOrderbookWebSocket } from "@/features/dashboard/hooks/useKisOrderbookWebSocket"; import { toast } from "sonner"; /** * @description 선택한 종목의 초기 호가를 조회하고 실시간 호가로 갱신합니다. * @see features/dashboard/components/orderbook/OrderBook.tsx 호가창 화면에서 사용합니다. */ export function useOrderBook( symbol: string | undefined, market: "KOSPI" | "KOSDAQ" | undefined, credentials: KisRuntimeCredentials | null, isVerified: boolean, options: { enabled?: boolean } = {}, ) { const { enabled = true } = 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]); const { realtimeOrderBook, isConnected: isWsConnected, error: wsError, } = useKisOrderbookWebSocket( symbol, market, credentials, isVerified && enabled, ); // 실시간 패킷이 0값으로 들어오는 경우에는 초기 REST 호가를 유지합니다. const safeRealtimeOrderBook = isUsableOrderBook(realtimeOrderBook) ? realtimeOrderBook : null; const orderBook = isRequestEnabled ? (safeRealtimeOrderBook ?? initialData) : null; const mergedError = isRequestEnabled ? error || wsError : null; const mergedLoading = isRequestEnabled ? isLoading && !orderBook : false; return { orderBook, isLoading: mergedLoading, error: mergedError, isWsConnected, }; } /** * @description 호가 데이터가 실제 유효한지(0만 있는지) 판별합니다. * @see features/dashboard/hooks/useOrderBook.ts 실시간/초기 호가 병합 우선순위 결정에 사용합니다. */ function isUsableOrderBook(orderBook: DashboardStockOrderBookResponse | null) { if (!orderBook) return false; if (orderBook.totalAskSize > 0 || orderBook.totalBidSize > 0) return true; return orderBook.levels.some( (level) => level.askPrice > 0 || level.bidPrice > 0 || level.askSize > 0 || level.bidSize > 0, ); }