import { type FocusEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState, } from "react"; import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; interface UseTradeSearchPanelParams { canSearch: boolean; keyword: string; verifiedCredentials: KisRuntimeCredentials | null; search: (query: string, credentials: KisRuntimeCredentials | null) => void; clearSearch: () => void; } /** * @description 트레이드 검색 패널(열림/닫힘/자동검색/포커스 이탈) UI 상태를 관리합니다. * @see features/trade/components/TradeContainer.tsx TradeContainer에서 검색 관련 상태 조합을 단순화하기 위해 사용합니다. * @see features/trade/components/search/TradeSearchSection.tsx 검색 UI 이벤트 핸들러를 전달합니다. */ export function useTradeSearchPanel({ canSearch, keyword, verifiedCredentials, search, clearSearch, }: UseTradeSearchPanelParams) { // [Ref] 종목 선택 직후 자동 검색을 1회 건너뛰기 위한 플래그 const skipNextAutoSearchRef = useRef(false); // [Ref] 검색 패널 루트 (포커스 아웃 감지 범위) const searchShellRef = useRef(null); // [State] 검색 패널 열림 상태 const [isSearchPanelOpen, setIsSearchPanelOpen] = useState(false); /** * @description 다음 자동 검색 사이클 1회를 건너뛰도록 표시합니다. * @see features/trade/components/TradeContainer.tsx handleSelectStock 종목 선택 직후 중복 검색 방지에 사용합니다. */ const markSkipNextAutoSearch = useCallback(() => { skipNextAutoSearchRef.current = true; }, []); const closeSearchPanel = useCallback(() => { setIsSearchPanelOpen(false); }, []); const openSearchPanel = useCallback(() => { if (!canSearch) return; setIsSearchPanelOpen(true); }, [canSearch]); /** * @description 검색 박스에서 포커스가 완전히 벗어나면 드롭다운을 닫습니다. * @see features/trade/components/search/TradeSearchSection.tsx onBlurCapture 이벤트로 연결됩니다. */ const handleSearchShellBlur = useCallback( (event: FocusEvent) => { const nextTarget = event.relatedTarget as Node | null; if (nextTarget && searchShellRef.current?.contains(nextTarget)) return; closeSearchPanel(); }, [closeSearchPanel], ); /** * @description ESC 키 입력 시 검색 드롭다운을 닫고 포커스를 해제합니다. * @see features/trade/components/search/TradeSearchSection.tsx onKeyDownCapture 이벤트로 연결됩니다. */ const handleSearchShellKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key !== "Escape") return; closeSearchPanel(); (event.target as HTMLElement | null)?.blur?.(); }, [closeSearchPanel], ); useEffect(() => { // [Step 1] 종목 선택 직후 1회 자동 검색 스킵 처리 if (skipNextAutoSearchRef.current) { skipNextAutoSearchRef.current = false; return; } // [Step 2] 인증 불가 상태면 검색 결과를 즉시 정리 if (!canSearch) { clearSearch(); return; } const trimmed = keyword.trim(); // [Step 3] 입력값이 비어 있으면 검색 상태 초기화 if (!trimmed) { clearSearch(); return; } // [Step 4] 입력 디바운스 후 검색 실행 const timer = window.setTimeout(() => { search(trimmed, verifiedCredentials); }, 220); return () => window.clearTimeout(timer); }, [canSearch, keyword, verifiedCredentials, search, clearSearch]); return { searchShellRef, isSearchPanelOpen: canSearch && isSearchPanelOpen, markSkipNextAutoSearch, openSearchPanel, closeSearchPanel, handleSearchShellBlur, handleSearchShellKeyDown, }; }