o2o-castad-frontend/src/pages/Dashboard/UrlInputContent.tsx

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;