임시커밋
This commit is contained in:
191
features/trade/hooks/useStockSearch.ts
Normal file
191
features/trade/hooks/useStockSearch.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { fetchStockSearch } from "@/features/trade/apis/kis-stock.api";
|
||||
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
|
||||
import type {
|
||||
DashboardStockSearchHistoryItem,
|
||||
DashboardStockSearchItem,
|
||||
} from "@/features/trade/types/trade.types";
|
||||
|
||||
const SEARCH_HISTORY_STORAGE_KEY = "jurini:stock-search-history:v1";
|
||||
const SEARCH_HISTORY_LIMIT = 12;
|
||||
|
||||
interface StoredSearchHistory {
|
||||
version: 1;
|
||||
items: DashboardStockSearchHistoryItem[];
|
||||
}
|
||||
|
||||
function readSearchHistory(): DashboardStockSearchHistoryItem[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(SEARCH_HISTORY_STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
|
||||
const parsed = JSON.parse(raw) as StoredSearchHistory;
|
||||
if (parsed?.version !== 1 || !Array.isArray(parsed.items)) return [];
|
||||
|
||||
return parsed.items
|
||||
.filter((item) => item?.symbol && item?.name && item?.market)
|
||||
.slice(0, SEARCH_HISTORY_LIMIT);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeSearchHistory(items: DashboardStockSearchHistoryItem[]) {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const payload: StoredSearchHistory = {
|
||||
version: 1,
|
||||
items,
|
||||
};
|
||||
window.localStorage.setItem(SEARCH_HISTORY_STORAGE_KEY, JSON.stringify(payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 종목 검색 상태(키워드/결과/에러)와 검색 히스토리(localStorage)를 함께 관리합니다.
|
||||
* @see features/trade/components/TradeContainer.tsx 검색 제출/자동검색/히스토리 클릭 이벤트에서 호출합니다.
|
||||
* @see features/trade/components/search/StockSearchHistory.tsx 히스토리 목록 렌더링 데이터로 사용합니다.
|
||||
*/
|
||||
export function useStockSearch() {
|
||||
// ========== SEARCH STATE ==========
|
||||
const [keyword, setKeyword] = useState("삼성전자");
|
||||
const [searchResults, setSearchResults] = useState<DashboardStockSearchItem[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
// ========== SEARCH HISTORY STATE ==========
|
||||
const [searchHistory, setSearchHistory] = useState<DashboardStockSearchHistoryItem[]>(
|
||||
() => readSearchHistory(),
|
||||
);
|
||||
|
||||
// 동일 시점 중복 요청과 경합 응답을 막기 위한 취소 컨트롤러
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const loadSearch = useCallback(async (query: string) => {
|
||||
const controller = new AbortController();
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = controller;
|
||||
|
||||
setIsSearching(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await fetchStockSearch(query, controller.signal);
|
||||
setSearchResults(data.items);
|
||||
return data.items;
|
||||
} catch (err) {
|
||||
if (controller.signal.aborted) return [];
|
||||
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "종목 검색 중 오류가 발생했습니다.",
|
||||
);
|
||||
return [];
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description 검색어를 받아 종목 검색 API를 호출합니다.
|
||||
* @see features/trade/components/TradeContainer.tsx handleSearchSubmit 자동/수동 검색에 사용합니다.
|
||||
*/
|
||||
const search = useCallback(
|
||||
(query: string, credentials: KisRuntimeCredentials | null) => {
|
||||
if (!credentials) {
|
||||
setError("API 키 검증이 필요합니다.");
|
||||
setSearchResults([]);
|
||||
setIsSearching(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) {
|
||||
abortRef.current?.abort();
|
||||
setSearchResults([]);
|
||||
setError(null);
|
||||
setIsSearching(false);
|
||||
return;
|
||||
}
|
||||
|
||||
void loadSearch(trimmed);
|
||||
},
|
||||
[loadSearch],
|
||||
);
|
||||
|
||||
/**
|
||||
* @description 검색 결과를 지우고 진행 중인 검색 요청을 중단합니다.
|
||||
* @see features/trade/components/TradeContainer.tsx 종목 선택 직후 검색 패널 정리에 사용합니다.
|
||||
*/
|
||||
const clearSearch = useCallback(() => {
|
||||
abortRef.current?.abort();
|
||||
setSearchResults([]);
|
||||
setError(null);
|
||||
setIsSearching(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description API 검증 전 같은 상황에서 공통 에러 메시지를 표시하기 위한 setter입니다.
|
||||
* @see features/trade/components/TradeContainer.tsx ensureSearchReady 인증 가드에서 사용합니다.
|
||||
*/
|
||||
const setSearchError = useCallback((message: string | null) => {
|
||||
setError(message);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description 선택한 종목을 검색 히스토리 맨 위에 추가(중복 제거)합니다.
|
||||
* @see features/trade/components/TradeContainer.tsx handleSelectStock 종목 선택 이벤트에서 호출합니다.
|
||||
*/
|
||||
const appendSearchHistory = useCallback((item: DashboardStockSearchItem) => {
|
||||
setSearchHistory((prev) => {
|
||||
const deduped = prev.filter((historyItem) => historyItem.symbol !== item.symbol);
|
||||
const nextItems: DashboardStockSearchHistoryItem[] = [
|
||||
{ ...item, savedAt: Date.now() },
|
||||
...deduped,
|
||||
].slice(0, SEARCH_HISTORY_LIMIT);
|
||||
|
||||
writeSearchHistory(nextItems);
|
||||
return nextItems;
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description 종목코드 기준으로 히스토리 항목을 삭제합니다.
|
||||
* @see features/trade/components/search/StockSearchHistory.tsx 삭제 버튼 클릭 이벤트에서 호출합니다.
|
||||
*/
|
||||
const removeSearchHistory = useCallback((symbol: string) => {
|
||||
setSearchHistory((prev) => {
|
||||
const nextItems = prev.filter((item) => item.symbol !== symbol);
|
||||
writeSearchHistory(nextItems);
|
||||
return nextItems;
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description 저장된 검색 히스토리를 전체 삭제합니다.
|
||||
* @see features/trade/components/search/StockSearchHistory.tsx 전체 삭제 버튼 이벤트에서 호출합니다.
|
||||
*/
|
||||
const clearSearchHistory = useCallback(() => {
|
||||
setSearchHistory([]);
|
||||
writeSearchHistory([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
keyword,
|
||||
setKeyword,
|
||||
searchResults,
|
||||
error,
|
||||
isSearching,
|
||||
search,
|
||||
clearSearch,
|
||||
setSearchError,
|
||||
searchHistory,
|
||||
appendSearchHistory,
|
||||
removeSearchHistory,
|
||||
clearSearchHistory,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user