"use client"; import { useEffect, useRef } from "react"; import { useShallow } from "zustand/react/shallow"; import { cn } from "@/lib/utils"; import { useKisRuntimeStore } from "@/features/dashboard/store/use-kis-runtime-store"; import { KisAuthForm } from "@/features/dashboard/components/auth/KisAuthForm"; import { StockSearchForm } from "@/features/dashboard/components/search/StockSearchForm"; import { StockSearchResults } from "@/features/dashboard/components/search/StockSearchResults"; import { useStockSearch } from "@/features/dashboard/hooks/useStockSearch"; import { useOrderBook } from "@/features/dashboard/hooks/useOrderBook"; // Added import import { useKisTradeWebSocket } from "@/features/dashboard/hooks/useKisTradeWebSocket"; import { useStockOverview } from "@/features/dashboard/hooks/useStockOverview"; import { DashboardLayout } from "@/features/dashboard/components/layout/DashboardLayout"; import { StockHeader } from "@/features/dashboard/components/header/StockHeader"; import { OrderBook } from "@/features/dashboard/components/orderbook/OrderBook"; import { OrderForm } from "@/features/dashboard/components/order/OrderForm"; import { StockLineChart } from "@/features/dashboard/components/chart/StockLineChart"; import type { DashboardStockItem, DashboardStockSearchItem, } from "@/features/dashboard/types/dashboard.types"; /** * @description 대시보드 메인 컨테이너 * @see app/(main)/dashboard/page.tsx 로그인 완료 후 이 컴포넌트를 렌더링합니다. * @see features/dashboard/hooks/useStockSearch.ts 검색 입력/요청 상태를 관리합니다. * @see features/dashboard/hooks/useStockOverview.ts 선택 종목 상세 상태를 관리합니다. */ export function DashboardContainer() { const skipNextAutoSearchRef = useRef(false); const { verifiedCredentials, isKisVerified } = useKisRuntimeStore( useShallow((state) => ({ verifiedCredentials: state.verifiedCredentials, isKisVerified: state.isKisVerified, })), ); const { keyword, setKeyword, searchResults, setSearchResults, setError: setSearchError, isSearching, search, clearSearch, } = useStockSearch(); const { selectedStock, loadOverview, updateRealtimeTradeTick } = useStockOverview(); // 1. Trade WebSocket (Execution) const { latestTick, realtimeCandles, recentTradeTicks } = useKisTradeWebSocket( selectedStock?.symbol, verifiedCredentials, isKisVerified, updateRealtimeTradeTick, ); // 2. OrderBook WebSocket (Bid/Ask) - Lifted from OrderBook component // User requested to use OrderBook data if Trade data is missing const { orderBook, isLoading: isOrderBookLoading } = useOrderBook( selectedStock?.symbol, selectedStock?.market, verifiedCredentials, isKisVerified, { enabled: !!selectedStock && !!verifiedCredentials && isKisVerified }, ); useEffect(() => { if (skipNextAutoSearchRef.current) { skipNextAutoSearchRef.current = false; return; } if (!isKisVerified || !verifiedCredentials) { clearSearch(); return; } const trimmed = keyword.trim(); if (!trimmed) { clearSearch(); return; } const timer = window.setTimeout(() => { search(trimmed, verifiedCredentials); }, 220); return () => window.clearTimeout(timer); }, [keyword, isKisVerified, verifiedCredentials, search, clearSearch]); // Price Calculation Logic // Prioritize latestTick (Real Exec) > OrderBook Ask1 (Proxy) > REST Data let currentPrice = selectedStock?.currentPrice; let change = selectedStock?.change; let changeRate = selectedStock?.changeRate; if (latestTick) { currentPrice = latestTick.price; change = latestTick.change; changeRate = latestTick.changeRate; } else if (orderBook?.levels[0]?.askPrice) { // Fallback: Use Best Ask Price as proxy for current price const askPrice = orderBook.levels[0].askPrice; if (askPrice > 0) { currentPrice = askPrice; // Recalculate change/rate based on prevClose if (selectedStock && selectedStock.prevClose > 0) { change = currentPrice - selectedStock.prevClose; changeRate = (change / selectedStock.prevClose) * 100; } } } function handleSearchSubmit(e: React.FormEvent) { e.preventDefault(); if (!isKisVerified || !verifiedCredentials) { setSearchError("API 키 검증을 먼저 완료해 주세요."); return; } search(keyword, verifiedCredentials); } function handleSelectStock(item: DashboardStockSearchItem) { if (!isKisVerified || !verifiedCredentials) { setSearchError("API 키 검증을 먼저 완료해 주세요."); return; } // 이미 선택된 종목을 다시 누른 경우 불필요한 개요 API 재호출을 막습니다. if (selectedStock?.symbol === item.symbol) { setSearchResults([]); return; } // 카드 선택으로 keyword가 바뀔 때 자동 검색이 다시 돌지 않도록 1회 건너뜁니다. skipNextAutoSearchRef.current = true; setKeyword(item.name); setSearchResults([]); loadOverview(item.symbol, verifiedCredentials, item.market); } return (