'use client'; // D6 §5.2 — Ctrl+K 커맨드 팔레트 (shadcn Command / cmdk) // 종목 자동완성(/api/stocks/suggest) + 글 검색(Forum) 합성 (d0 §3-D 확정). // 종목 선택 → /stock/{code} 즉시 이동, 글 검색 → /latest?search=subject&keyword=... 이동. // 헤더 인라인 combobox(HeaderSearch)와 별개로, 전역 Ctrl+K/⌘K 로 열리는 팔레트. import { useCallback, useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import { fetchApi } from '@/lib/utils/client'; import { StockSuggestItem, StockSuggestResponse } from '@/types/stock'; import { CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator, } from '@/components/ui/command'; const DEBOUNCE_MS = 250; export default function SearchCommand() { const router = useRouter(); const [open, setOpen] = useState(false); const [keyword, setKeyword] = useState(''); const [items, setItems] = useState([]); const debounceRef = useRef|null>(null); const requestSeq = useRef(0); // 전역 단축키: Ctrl+K / ⌘K → 팔레트 토글 useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setOpen(prev => !prev); } }; window.addEventListener('keydown', handler); return () => { window.removeEventListener('keydown', handler); }; }, []); // 닫힐 때 입력/결과 초기화 useEffect(() => { if (!open) { setKeyword(''); setItems([]); requestSeq.current++; } }, [open]); useEffect(() => { return () => { if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, []); const requestSuggest = useCallback(async (q: string) => { const seq = ++requestSeq.current; const res = await fetchApi(`/api/stocks/suggest?q=${encodeURIComponent(q)}`, { silent: true }); // out-of-order 응답 무시 if (seq !== requestSeq.current) { return; } setItems(res.success ? (res.data?.list ?? []) : []); }, []); const handleValueChange = useCallback((value: string) => { setKeyword(value); if (debounceRef.current) { clearTimeout(debounceRef.current); } const trimmed = value.trim(); if (!trimmed) { requestSeq.current++; setItems([]); return; } debounceRef.current = setTimeout(() => { requestSuggest(trimmed); }, DEBOUNCE_MS); }, [requestSuggest]); const goStock = useCallback((code: string) => { setOpen(false); router.push(`/stock/${code}`); }, [router]); const goStockSearch = useCallback((q: string) => { setOpen(false); router.push(`/stock?q=${encodeURIComponent(q)}`); }, [router]); const goPostSearch = useCallback((q: string) => { setOpen(false); router.push(`/latest?search=subject&keyword=${encodeURIComponent(q)}`); }, [router]); const trimmed = keyword.trim(); return ( {!trimmed && ( 검색어를 입력하세요. )} {trimmed && items.length === 0 && ( 일치하는 종목이 없습니다. )} {items.length > 0 && ( {items.map(item => ( goStock(item.code)} > {item.name} {item.code} {item.market} ))} )} {trimmed && ( <> goStockSearch(trimmed)}> 종목 목록에서 ‘{trimmed}’ 검색 goPostSearch(trimmed)}> 게시글에서 ‘{trimmed}’ 검색 )} ); }