import React, { useState, useRef, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { searchAccommodation, AccommodationSearchItem, AutocompleteRequest } from '../utils/api'; import { useSearchHistory } from './SearchHistory/useSearchHistory'; import SearchHistoryDropdown from './SearchHistory/SearchHistoryDropdown'; import BusinessNameInputModal from './BusinessNameInputModal'; type SearchType = 'url' | 'name' | 'manual'; export type { SearchType }; const isValidUrl = (string: string): boolean => { try { const url = new URL(string); return url.protocol === 'http:' || url.protocol === 'https:'; } catch { return false; } }; interface SearchInputFormProps { onAnalyze?: (value: string, type: SearchType) => void; onAutocomplete?: (data: AutocompleteRequest) => void; onManualInput?: (businessName: string, address: string) => void; /** 직접입력 버튼 클릭 시 기본 동작(모달 열기)을 대체합니다. 제공 시 모달을 직접 관리해야 합니다. */ onManualButtonClick?: () => void; onShortform?: () => void; error?: string | null; } const SearchInputForm: React.FC = ({ onAnalyze, onAutocomplete, onManualInput, onManualButtonClick, onShortform, error: externalError, }) => { const { t } = useTranslation(); const [inputValue, setInputValue] = useState(''); const [searchType, setSearchType] = useState('name'); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isFocused, setIsFocused] = useState(false); const [isManualModalOpen, setIsManualModalOpen] = useState(false); const [localError, setLocalError] = useState(''); const [autocompleteResults, setAutocompleteResults] = useState([]); const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false); const [showAutocomplete, setShowAutocomplete] = useState(false); const [selectedItem, setSelectedItem] = useState(null); const [highlightedIndex, setHighlightedIndex] = useState(-1); const { filteredHistory, showHistory, openHistory, closeHistory, hideOnInput, deleteItem } = useSearchHistory(searchType === 'manual' ? 'name' : searchType); const debounceRef = useRef(null); const dropdownRef = useRef(null); const autocompleteRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsDropdownOpen(false); } if (autocompleteRef.current && !autocompleteRef.current.contains(event.target as Node)) { setShowAutocomplete(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const searchTypeOptions = [ { value: 'url' as SearchType, label: 'URL' }, { value: 'name' as SearchType, label: t('landing.hero.searchTypeBusinessName') }, { value: 'manual' as SearchType, label: t('landing.hero.searchTypeManual') }, ]; const getPlaceholder = () => searchType === 'url' ? 'https://naver.me/abcdef' : t('landing.hero.placeholderBusinessName'); const getGuideText = () => searchType === 'url' ? t('landing.hero.guideUrl') : t('landing.hero.guideBusinessName'); const handleAutocompleteSearch = useCallback(async (query: string) => { if (!query.trim() || searchType !== 'name') { setAutocompleteResults([]); setShowAutocomplete(false); return; } setIsAutocompleteLoading(true); try { const response = await searchAccommodation(query); setAutocompleteResults(response.items || []); setShowAutocomplete(response.items && response.items.length > 0); } catch { setAutocompleteResults([]); setShowAutocomplete(false); } finally { setIsAutocompleteLoading(false); } }, [searchType]); const handleSelectAutocomplete = (item: AccommodationSearchItem) => { setInputValue(item.title.replace(/<[^>]*>/g, '')); setSelectedItem(item); setShowAutocomplete(false); setAutocompleteResults([]); setHighlightedIndex(-1); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (showAutocomplete && autocompleteResults.length > 0) { switch (e.key) { case 'ArrowDown': e.preventDefault(); setHighlightedIndex(prev => prev < autocompleteResults.length - 1 ? prev + 1 : 0); return; case 'ArrowUp': e.preventDefault(); setHighlightedIndex(prev => prev > 0 ? prev - 1 : autocompleteResults.length - 1); return; case 'Enter': e.preventDefault(); if (highlightedIndex >= 0 && highlightedIndex < autocompleteResults.length) { handleSelectAutocomplete(autocompleteResults[highlightedIndex]); } return; case 'Escape': e.preventDefault(); setShowAutocomplete(false); setHighlightedIndex(-1); return; } } if (e.key === 'Enter') { e.preventDefault(); } }; const handleStart = () => { if (!inputValue.trim()) { setLocalError(searchType === 'url' ? t('landing.hero.errorUrlRequired') : t('landing.hero.errorNameRequired')); return; } if (searchType === 'url' && !isValidUrl(inputValue.trim())) { setLocalError(t('landing.hero.errorInvalidUrl')); return; } setLocalError(''); if (searchType === 'name' && selectedItem && onAutocomplete) { onAutocomplete({ address: selectedItem.address, roadAddress: selectedItem.roadAddress, title: selectedItem.title, }); } else if (onAnalyze) { onAnalyze(inputValue, searchType); } }; const handleManualButtonClick = () => { if (onManualButtonClick) { onManualButtonClick(); } else { setIsManualModalOpen(true); } }; const error = externalError || localError; return (
{/* 드롭다운 */}
{isDropdownOpen && (
{searchTypeOptions.map((option) => ( ))}
)}
{/* 입력 필드 */}
{ let value = e.target.value; if (searchType === 'url') { const urlMatch = value.match(/https?:\/\/\S+/); if (urlMatch && urlMatch[0] !== value) value = urlMatch[0]; } setInputValue(value); setSelectedItem(null); setHighlightedIndex(-1); hideOnInput(value); if (localError) setLocalError(''); if (searchType === 'name') { if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => handleAutocompleteSearch(value), 300); } }} onPaste={(e: React.ClipboardEvent) => { if (searchType !== 'url') return; const pasted = e.clipboardData.getData('text'); const urlMatch = pasted.match(/https?:\/\/[^\s]+/); if (urlMatch) { e.preventDefault(); setInputValue(urlMatch[0]); if (localError) setLocalError(''); } }} onFocus={() => { setIsFocused(true); if (searchType === 'name' && autocompleteResults.length > 0) setShowAutocomplete(true); openHistory(inputValue); }} onBlur={() => { setIsFocused(false); setTimeout(() => closeHistory(), 150); }} onKeyDown={handleKeyDown} placeholder={getPlaceholder()} className={`hero-input ${inputValue ? 'has-value' : ''}`} /> {/* 최근 검색 히스토리 */} {showHistory && !showAutocomplete && ( { closeHistory(); if (item.type === 'url') { setSearchType('url'); setInputValue(item.value); } else { setSearchType('name'); setInputValue(item.value); setSelectedItem({ title: item.value, address: item.address || '', roadAddress: item.roadAddress || '' }); } }} onDelete={(e, value) => { e.preventDefault(); e.stopPropagation(); deleteItem(value); }} className="hero-autocomplete-dropdown" itemClassName="hero-autocomplete-item" titleClassName="hero-autocomplete-title" addressClassName="hero-autocomplete-address" /> )} {/* 자동완성 결과 */} {showAutocomplete && searchType === 'name' && (
{isAutocompleteLoading ? (
{t('landing.hero.searching')}
) : ( autocompleteResults.map((item, index) => ( )) )}
)}
{/* 입력 clear 버튼 */} {inputValue && ( )}
{getGuideText()} {error &&

{error}

} {onShortform && ( )} {/* onManualButtonClick 미제공 시 모달을 내부에서 관리 */} {!onManualButtonClick && isManualModalOpen && ( setIsManualModalOpen(false)} onSubmit={(businessName, address) => { setIsManualModalOpen(false); onManualInput?.(businessName, address); }} /> )}
); }; export default SearchInputForm;