정리
This commit is contained in:
118
features/trade/hooks/useTradeSearchPanel.ts
Normal file
118
features/trade/hooks/useTradeSearchPanel.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user