119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
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<HTMLDivElement | null>(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<HTMLDivElement>) => {
|
|
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<HTMLDivElement>) => {
|
|
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,
|
|
};
|
|
}
|