282 lines
9.9 KiB
TypeScript
282 lines
9.9 KiB
TypeScript
|
|
import React, { useState, useRef, useCallback } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { searchAccommodation, AccommodationSearchItem, AutocompleteRequest } from '../../utils/api';
|
|
import { CrawlingResponse } from '../../types/api';
|
|
|
|
type SearchType = 'url' | 'name';
|
|
|
|
// 환경변수에서 테스트 모드 확인
|
|
const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
|
|
|
interface UrlInputContentProps {
|
|
onAnalyze: (value: string, type?: SearchType) => void;
|
|
onAutocomplete?: (data: AutocompleteRequest) => void;
|
|
onTestData?: (data: CrawlingResponse) => void;
|
|
error: string | null;
|
|
}
|
|
|
|
const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomplete, onTestData, error }) => {
|
|
const { t, i18n } = useTranslation();
|
|
const [inputValue, setInputValue] = useState('');
|
|
const [searchType, setSearchType] = useState<SearchType>('url');
|
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
const [autocompleteResults, setAutocompleteResults] = useState<AccommodationSearchItem[]>([]);
|
|
const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false);
|
|
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
|
const [selectedItem, setSelectedItem] = useState<AccommodationSearchItem | null>(null);
|
|
const [isLoadingTest, setIsLoadingTest] = useState(false);
|
|
|
|
// 테스트 데이터 로드 핸들러
|
|
const handleTestData = async () => {
|
|
if (!onTestData) return;
|
|
setIsLoadingTest(true);
|
|
try {
|
|
const jsonFile = i18n.language === 'en' ? '/example_analysis_en.json' : '/example_analysis.json';
|
|
const response = await fetch(jsonFile);
|
|
const data: CrawlingResponse = await response.json();
|
|
onTestData(data);
|
|
} catch (err) {
|
|
console.error('테스트 데이터 로드 실패:', err);
|
|
} finally {
|
|
setIsLoadingTest(false);
|
|
}
|
|
};
|
|
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
|
|
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
|
const autocompleteRef = useRef<HTMLDivElement>(null);
|
|
|
|
const searchTypeOptions = [
|
|
{ value: 'url' as SearchType, label: 'URL' },
|
|
{ value: 'name' as SearchType, label: t('urlInput.searchTypeBusinessName') },
|
|
];
|
|
|
|
const getPlaceholder = () => {
|
|
return searchType === 'url'
|
|
? 'https://www.castad.com'
|
|
: t('urlInput.placeholderBusinessName');
|
|
};
|
|
|
|
const getGuideText = () => {
|
|
return searchType === 'url'
|
|
? t('urlInput.guideUrl')
|
|
: t('urlInput.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 (error) {
|
|
console.error('자동완성 검색 오류:', error);
|
|
setAutocompleteResults([]);
|
|
setShowAutocomplete(false);
|
|
} finally {
|
|
setIsAutocompleteLoading(false);
|
|
}
|
|
}, [searchType]);
|
|
|
|
// 자동완성 항목 선택 - 업체 정보 저장
|
|
const handleSelectAutocomplete = (item: AccommodationSearchItem) => {
|
|
setInputValue(item.title.replace(/<[^>]*>/g, '')); // HTML 태그 제거
|
|
setSelectedItem(item); // 선택된 업체 정보 저장
|
|
setShowAutocomplete(false);
|
|
setAutocompleteResults([]);
|
|
setHighlightedIndex(-1);
|
|
};
|
|
|
|
// 키보드 네비게이션 핸들러
|
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
if (!showAutocomplete || autocompleteResults.length === 0) return;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
setHighlightedIndex(prev =>
|
|
prev < autocompleteResults.length - 1 ? prev + 1 : 0
|
|
);
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
setHighlightedIndex(prev =>
|
|
prev > 0 ? prev - 1 : autocompleteResults.length - 1
|
|
);
|
|
break;
|
|
case 'Enter':
|
|
if (highlightedIndex >= 0 && highlightedIndex < autocompleteResults.length) {
|
|
e.preventDefault();
|
|
handleSelectAutocomplete(autocompleteResults[highlightedIndex]);
|
|
}
|
|
break;
|
|
case 'Escape':
|
|
setShowAutocomplete(false);
|
|
setHighlightedIndex(-1);
|
|
break;
|
|
}
|
|
};
|
|
|
|
// 폼 제출 처리
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!inputValue.trim()) return;
|
|
|
|
if (searchType === 'name' && selectedItem && onAutocomplete) {
|
|
// 업체명 검색인 경우 autocomplete API 호출
|
|
const request: AutocompleteRequest = {
|
|
address: selectedItem.address,
|
|
roadAddress: selectedItem.roadAddress,
|
|
title: selectedItem.title,
|
|
};
|
|
onAutocomplete(request);
|
|
} else {
|
|
// URL 검색인 경우 기존 로직
|
|
onAnalyze(inputValue.trim(), searchType);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="url-input-container">
|
|
<div className="url-input-content">
|
|
{/* 로고 */}
|
|
<div className="url-input-logo">
|
|
<img src="/assets/images/ado2-logo.svg" alt="ADO2" />
|
|
</div>
|
|
|
|
{/* URL 입력 폼 */}
|
|
<form onSubmit={handleSubmit} className="url-input-form">
|
|
<div className="url-input-wrapper">
|
|
{/* 드롭다운 */}
|
|
<div className="url-input-dropdown-container">
|
|
<button
|
|
type="button"
|
|
className="url-input-dropdown-trigger"
|
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
>
|
|
<span>{searchTypeOptions.find(opt => opt.value === searchType)?.label}</span>
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
className={`url-input-dropdown-arrow ${isDropdownOpen ? 'open' : ''}`}
|
|
>
|
|
<path d="M6 9l6 6 6-6" />
|
|
</svg>
|
|
</button>
|
|
{isDropdownOpen && (
|
|
<div className="url-input-dropdown-menu">
|
|
{searchTypeOptions.map((option) => (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
className={`url-input-dropdown-item ${searchType === option.value ? 'active' : ''}`}
|
|
onClick={() => {
|
|
setSearchType(option.value);
|
|
setIsDropdownOpen(false);
|
|
}}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 입력 필드 */}
|
|
<div className="url-input-field-container" ref={autocompleteRef}>
|
|
<input
|
|
type={searchType === 'url' ? 'url' : 'text'}
|
|
value={inputValue}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
setInputValue(value);
|
|
|
|
// 업체명 검색일 때 자동완성 검색 (디바운스)
|
|
if (searchType === 'name') {
|
|
if (debounceRef.current) {
|
|
clearTimeout(debounceRef.current);
|
|
}
|
|
debounceRef.current = setTimeout(() => {
|
|
handleAutocompleteSearch(value);
|
|
}, 300);
|
|
}
|
|
}}
|
|
onFocus={() => {
|
|
if (searchType === 'name' && autocompleteResults.length > 0) {
|
|
setShowAutocomplete(true);
|
|
}
|
|
}}
|
|
placeholder={getPlaceholder()}
|
|
className="url-input-field"
|
|
/>
|
|
|
|
{/* 자동완성 결과 */}
|
|
{showAutocomplete && searchType === 'name' && (
|
|
<div className="url-input-autocomplete-dropdown">
|
|
{isAutocompleteLoading ? (
|
|
<div className="url-input-autocomplete-loading">{t('urlInput.searching')}</div>
|
|
) : (
|
|
autocompleteResults.map((item, index) => (
|
|
<button
|
|
key={index}
|
|
type="button"
|
|
className="url-input-autocomplete-item"
|
|
onMouseDown={(e) => {
|
|
e.preventDefault();
|
|
handleSelectAutocomplete(item);
|
|
}}
|
|
>
|
|
<div className="url-input-autocomplete-title" dangerouslySetInnerHTML={{ __html: item.title }} />
|
|
<div className="url-input-autocomplete-address">{item.roadAddress || item.address}</div>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 검색 버튼 */}
|
|
<button type="submit" className="url-input-button">
|
|
{t('urlInput.searchButton')}
|
|
</button>
|
|
</div>
|
|
|
|
{/* 에러 메시지 */}
|
|
{error && (
|
|
<p className="url-input-error">{error}</p>
|
|
)}
|
|
</form>
|
|
|
|
{/* 안내 텍스트 */}
|
|
<p className="url-input-guide">
|
|
{getGuideText()}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 테스트 버튼 (VITE_IS_TESTPAGE=true일 때만 표시) */}
|
|
{isTestPage && onTestData && (
|
|
<button
|
|
onClick={handleTestData}
|
|
disabled={isLoadingTest}
|
|
className="test-data-button"
|
|
>
|
|
{isLoadingTest ? t('urlInput.testDataLoading') : t('urlInput.testData')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UrlInputContent;
|