Files
auto-trade/features/trade/hooks/useStockSearch.ts

192 lines
6.1 KiB
TypeScript
Raw Normal View History

2026-02-10 11:16:39 +09:00
import { useCallback, useRef, useState } from "react";
2026-02-11 16:31:28 +09:00
import { fetchStockSearch } from "@/features/trade/apis/kis-stock.api";
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
2026-02-11 14:06:06 +09:00
import type {
DashboardStockSearchHistoryItem,
DashboardStockSearchItem,
2026-02-11 16:31:28 +09:00
} from "@/features/trade/types/trade.types";
2026-02-11 14:06:06 +09:00
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 [];
2026-02-10 11:16:39 +09:00
2026-02-11 14:06:06 +09:00
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) .
2026-02-11 16:31:28 +09:00
* @see features/trade/components/TradeContainer.tsx // .
* @see features/trade/components/search/StockSearchHistory.tsx .
2026-02-11 14:06:06 +09:00
*/
2026-02-10 11:16:39 +09:00
export function useStockSearch() {
2026-02-11 14:06:06 +09:00
// ========== SEARCH STATE ==========
2026-02-12 10:24:03 +09:00
const [keyword, setKeyword] = useState("");
2026-02-11 14:06:06 +09:00
const [searchResults, setSearchResults] = useState<DashboardStockSearchItem[]>([]);
2026-02-10 11:16:39 +09:00
const [error, setError] = useState<string | null>(null);
const [isSearching, setIsSearching] = useState(false);
2026-02-11 14:06:06 +09:00
// ========== SEARCH HISTORY STATE ==========
const [searchHistory, setSearchHistory] = useState<DashboardStockSearchHistoryItem[]>(
() => readSearchHistory(),
);
// 동일 시점 중복 요청과 경합 응답을 막기 위한 취소 컨트롤러
2026-02-10 11:16:39 +09:00
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);
2026-02-11 14:06:06 +09:00
setSearchResults(data.items);
2026-02-10 11:16:39 +09:00
return data.items;
} catch (err) {
2026-02-11 14:06:06 +09:00
if (controller.signal.aborted) return [];
setError(
err instanceof Error
? err.message
: "종목 검색 중 오류가 발생했습니다.",
);
2026-02-10 11:16:39 +09:00
return [];
} finally {
2026-02-11 14:06:06 +09:00
if (!controller.signal.aborted) {
2026-02-10 11:16:39 +09:00
setIsSearching(false);
}
}
}, []);
2026-02-11 14:06:06 +09:00
/**
* @description API를 .
2026-02-11 16:31:28 +09:00
* @see features/trade/components/TradeContainer.tsx handleSearchSubmit / .
2026-02-11 14:06:06 +09:00
*/
2026-02-10 11:16:39 +09:00
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],
);
2026-02-11 14:06:06 +09:00
/**
* @description .
2026-02-11 16:31:28 +09:00
* @see features/trade/components/TradeContainer.tsx .
2026-02-11 14:06:06 +09:00
*/
2026-02-10 11:16:39 +09:00
const clearSearch = useCallback(() => {
abortRef.current?.abort();
setSearchResults([]);
setError(null);
setIsSearching(false);
}, []);
2026-02-11 14:06:06 +09:00
/**
* @description API setter입니다.
2026-02-11 16:31:28 +09:00
* @see features/trade/components/TradeContainer.tsx ensureSearchReady .
2026-02-11 14:06:06 +09:00
*/
const setSearchError = useCallback((message: string | null) => {
setError(message);
}, []);
/**
* @description ( ).
2026-02-11 16:31:28 +09:00
* @see features/trade/components/TradeContainer.tsx handleSelectStock .
2026-02-11 14:06:06 +09:00
*/
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 .
2026-02-11 16:31:28 +09:00
* @see features/trade/components/search/StockSearchHistory.tsx .
2026-02-11 14:06:06 +09:00
*/
const removeSearchHistory = useCallback((symbol: string) => {
setSearchHistory((prev) => {
const nextItems = prev.filter((item) => item.symbol !== symbol);
writeSearchHistory(nextItems);
return nextItems;
});
}, []);
/**
* @description .
2026-02-11 16:31:28 +09:00
* @see features/trade/components/search/StockSearchHistory.tsx .
2026-02-11 14:06:06 +09:00
*/
const clearSearchHistory = useCallback(() => {
setSearchHistory([]);
writeSearchHistory([]);
}, []);
2026-02-10 11:16:39 +09:00
return {
keyword,
setKeyword,
searchResults,
error,
isSearching,
search,
clearSearch,
2026-02-11 14:06:06 +09:00
setSearchError,
searchHistory,
appendSearchHistory,
removeSearchHistory,
clearSearchHistory,
2026-02-10 11:16:39 +09:00
};
}