import { useMemo } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ScrollArea } from "@/components/ui/scroll-area"; import type { DashboardRealtimeTradeTick, DashboardStockOrderBookResponse, } from "@/features/trade/types/trade.types"; import type { BookRow } from "./orderbook-utils"; import { buildBookRows, buildFallbackLevelsFromTick, hasOrderBookLevelData, resolveReferencePrice, } from "./orderbook-utils"; import { BookHeader, BookSideRows, CumulativeRows, CurrentPriceBar, OrderBookSkeleton, SummaryPanel, TradeTape, } from "./orderbook-sections"; interface OrderBookProps { symbol?: string; referencePrice?: number; latestTick: DashboardRealtimeTradeTick | null; recentTicks: DashboardRealtimeTradeTick[]; orderBook: DashboardStockOrderBookResponse | null; isLoading?: boolean; } /** * @description 호가창 — 실시간 매도·매수 10호가와 체결 목록을 표시합니다. * @see features/trade/components/orderbook/orderbook-utils.ts 호가 계산/포맷 유틸 * @see features/trade/components/orderbook/orderbook-sections.tsx 호가/체결/요약 UI 섹션 */ export function OrderBook({ symbol, referencePrice, latestTick, recentTicks, orderBook, isLoading, }: OrderBookProps) { const realtimeLevels = useMemo(() => orderBook?.levels ?? [], [orderBook]); const fallbackLevelsFromTick = useMemo( () => buildFallbackLevelsFromTick(latestTick), [latestTick], ); const hasRealtimeLevelData = useMemo( () => hasOrderBookLevelData(realtimeLevels), [realtimeLevels], ); const levels = useMemo(() => { if (hasRealtimeLevelData) return realtimeLevels; return fallbackLevelsFromTick; }, [fallbackLevelsFromTick, hasRealtimeLevelData, realtimeLevels]); const isTickFallbackActive = !hasRealtimeLevelData && fallbackLevelsFromTick.length > 0; const latestPrice = latestTick?.price && latestTick.price > 0 ? latestTick.price : 0; const basePrice = resolveReferencePrice({ referencePrice, latestTick }); const askRows: BookRow[] = useMemo( () => buildBookRows({ levels, side: "ask", basePrice, latestPrice, }), [levels, basePrice, latestPrice], ); const bidRows: BookRow[] = useMemo( () => buildBookRows({ levels, side: "bid", basePrice, latestPrice, }), [levels, basePrice, latestPrice], ); const askMax = useMemo(() => Math.max(1, ...askRows.map((r) => r.size)), [askRows]); const bidMax = useMemo(() => Math.max(1, ...bidRows.map((r) => r.size)), [bidRows]); const mobileAskRows = useMemo(() => askRows.slice(0, 6), [askRows]); const mobileBidRows = useMemo(() => bidRows.slice(0, 6), [bidRows]); const { bestAsk, spread, totalAsk, totalBid, imbalance } = useMemo(() => { const resolvedBestAsk = levels.find((level) => level.askPrice > 0)?.askPrice ?? 0; const resolvedBestBid = levels.find((level) => level.bidPrice > 0)?.bidPrice ?? 0; const resolvedSpread = resolvedBestAsk > 0 && resolvedBestBid > 0 ? resolvedBestAsk - resolvedBestBid : 0; const resolvedTotalAsk = orderBook?.totalAskSize && orderBook.totalAskSize > 0 ? orderBook.totalAskSize : (latestTick?.totalAskSize ?? 0); const resolvedTotalBid = orderBook?.totalBidSize && orderBook.totalBidSize > 0 ? orderBook.totalBidSize : (latestTick?.totalBidSize ?? 0); const resolvedImbalance = resolvedTotalAsk + resolvedTotalBid > 0 ? ((resolvedTotalBid - resolvedTotalAsk) / (resolvedTotalAsk + resolvedTotalBid)) * 100 : 0; return { bestAsk: resolvedBestAsk, spread: resolvedSpread, totalAsk: resolvedTotalAsk, totalBid: resolvedTotalBid, imbalance: resolvedImbalance, }; }, [latestTick?.totalAskSize, latestTick?.totalBidSize, levels, orderBook]); if (!symbol) { return (