자동완성 기능 추가 .
parent
7ee911f0aa
commit
6d89a28982
|
|
@ -292,11 +292,11 @@ const LayoutGridIcon = () => (
|
|||
|
||||
const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, onGenerate, data }) => {
|
||||
const { processed_info, marketing_analysis } = data;
|
||||
const tags = marketing_analysis.tags || [];
|
||||
const facilities = marketing_analysis.facilities || [];
|
||||
const tags = marketing_analysis?.tags || [];
|
||||
const facilities = marketing_analysis?.facilities || [];
|
||||
const reportSections = useMemo(
|
||||
() => splitMarkdownSections(marketing_analysis.report || ''),
|
||||
[marketing_analysis.report]
|
||||
() => splitMarkdownSections(marketing_analysis?.report || ''),
|
||||
[marketing_analysis?.report]
|
||||
);
|
||||
const locationAnalysis =
|
||||
pickSectionContent(reportSections, ['지역', '입지']) || reportSections[0]?.content || '';
|
||||
|
|
@ -332,7 +332,7 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
|
|||
</div>
|
||||
<h1 className="text-4xl font-bold mb-3 tracking-tight text-white">브랜드 인텔리전스</h1>
|
||||
<p className="text-brand-muted text-lg max-w-xl mx-auto">
|
||||
<span className="text-brand-accent font-semibold">AI 데이터 분석</span>을 통해 도출된 {processed_info.customer_name}의 핵심 전략입니다.
|
||||
<span className="text-brand-accent font-semibold">AI 데이터 분석</span>을 통해 도출된 {processed_info?.customer_name || '브랜드'}의 핵심 전략입니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -346,12 +346,12 @@ const AnalysisResultSection: React.FC<AnalysisResultSectionProps> = ({ onBack, o
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl font-bold mb-2 text-white tracking-tight">{processed_info.customer_name}</h2>
|
||||
<h2 className="text-3xl font-bold mb-2 text-white tracking-tight">{processed_info?.customer_name || '브랜드명'}</h2>
|
||||
<div className="flex items-start gap-2 text-brand-muted text-sm mb-6">
|
||||
<MapPinIcon />
|
||||
<div>
|
||||
<p>{processed_info.detail_region_info || '주소 정보 없음'}</p>
|
||||
<p className="opacity-70">{processed_info.region}</p>
|
||||
<p>{processed_info?.detail_region_info || '주소 정보 없음'}</p>
|
||||
<p className="opacity-70">{processed_info?.region || ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import ADO2ContentsPage from './ADO2ContentsPage';
|
|||
import LoadingSection from '../Analysis/LoadingSection';
|
||||
import AnalysisResultSection from '../Analysis/AnalysisResultSection';
|
||||
import { ImageItem, CrawlingResponse } from '../../types/api';
|
||||
import { crawlUrl } from '../../utils/api';
|
||||
import { crawlUrl, autocomplete, AutocompleteRequest } from '../../utils/api';
|
||||
|
||||
const WIZARD_STEP_KEY = 'castad_wizard_step';
|
||||
const ACTIVE_ITEM_KEY = 'castad_active_item';
|
||||
|
|
@ -159,6 +159,37 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
localStorage.setItem(WIZARD_STEP_KEY, step.toString());
|
||||
};
|
||||
|
||||
// 업체명 자동완성으로 분석 시작
|
||||
const handleAutocomplete = async (request: AutocompleteRequest) => {
|
||||
goToWizardStep(-1); // 로딩 상태로
|
||||
setAnalysisError(null);
|
||||
|
||||
try {
|
||||
const data = await autocomplete(request);
|
||||
|
||||
// 기본값 보장
|
||||
if (data.marketing_analysis) {
|
||||
data.marketing_analysis.tags = data.marketing_analysis.tags || [];
|
||||
data.marketing_analysis.facilities = data.marketing_analysis.facilities || [];
|
||||
data.marketing_analysis.report = data.marketing_analysis.report || '';
|
||||
}
|
||||
if (data.processed_info) {
|
||||
data.processed_info.customer_name = data.processed_info.customer_name || '알 수 없음';
|
||||
data.processed_info.region = data.processed_info.region || '';
|
||||
data.processed_info.detail_region_info = data.processed_info.detail_region_info || '';
|
||||
}
|
||||
data.image_list = data.image_list || [];
|
||||
|
||||
setAnalysisData(data);
|
||||
localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(data));
|
||||
goToWizardStep(0); // 브랜드 분석 결과로
|
||||
} catch (err) {
|
||||
console.error('Autocomplete error:', err);
|
||||
setAnalysisError(err instanceof Error ? err.message : '업체 정보 조회에 실패했습니다.');
|
||||
goToWizardStep(-2); // URL 입력으로 돌아가기
|
||||
}
|
||||
};
|
||||
|
||||
// URL 분석 시작
|
||||
const handleStartAnalysis = async (url: string) => {
|
||||
if (!url.trim()) return;
|
||||
|
|
@ -234,6 +265,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
return (
|
||||
<UrlInputContent
|
||||
onAnalyze={handleStartAnalysis}
|
||||
onAutocomplete={handleAutocomplete}
|
||||
error={analysisError}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { searchNaverLocal, NaverLocalSearchItem, AutocompleteRequest } from '../../utils/api';
|
||||
import { searchAccommodation, AccommodationSearchItem, AutocompleteRequest } from '../../utils/api';
|
||||
|
||||
type SearchType = 'url' | 'name';
|
||||
|
||||
|
|
@ -14,19 +14,13 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
const [inputValue, setInputValue] = useState('');
|
||||
const [searchType, setSearchType] = useState<SearchType>('url');
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [autocompleteResults, setAutocompleteResults] = useState<NaverLocalSearchItem[]>([]);
|
||||
const [autocompleteResults, setAutocompleteResults] = useState<AccommodationSearchItem[]>([]);
|
||||
const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false);
|
||||
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<AccommodationSearchItem | null>(null);
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const autocompleteRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (inputValue.trim()) {
|
||||
onAnalyze(inputValue.trim(), searchType);
|
||||
}
|
||||
};
|
||||
|
||||
const searchTypeOptions = [
|
||||
{ value: 'url' as SearchType, label: 'URL' },
|
||||
{ value: 'name' as SearchType, label: '업체명' },
|
||||
|
|
@ -54,7 +48,7 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
|
||||
setIsAutocompleteLoading(true);
|
||||
try {
|
||||
const response = await searchNaverLocal(query);
|
||||
const response = await searchAccommodation(query);
|
||||
setAutocompleteResults(response.items || []);
|
||||
setShowAutocomplete(response.items && response.items.length > 0);
|
||||
} catch (error) {
|
||||
|
|
@ -66,20 +60,30 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
}
|
||||
}, [searchType]);
|
||||
|
||||
// 자동완성 항목 선택
|
||||
const handleSelectAutocomplete = (item: NaverLocalSearchItem) => {
|
||||
const request: AutocompleteRequest = {
|
||||
address: item.address,
|
||||
roadAddress: item.roadAddress,
|
||||
title: item.title,
|
||||
};
|
||||
|
||||
// 자동완성 항목 선택 - 업체 정보 저장
|
||||
const handleSelectAutocomplete = (item: AccommodationSearchItem) => {
|
||||
setInputValue(item.title.replace(/<[^>]*>/g, '')); // HTML 태그 제거
|
||||
setSelectedItem(item); // 선택된 업체 정보 저장
|
||||
setShowAutocomplete(false);
|
||||
setAutocompleteResults([]);
|
||||
};
|
||||
|
||||
if (onAutocomplete) {
|
||||
// 폼 제출 처리
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -185,6 +189,11 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 검색 버튼 */}
|
||||
<button type="submit" className="url-input-button">
|
||||
검색하기
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 에러 메시지 */}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { searchNaverLocal, NaverLocalSearchItem, AutocompleteRequest } from '../../utils/api';
|
||||
import { searchAccommodation, AccommodationSearchItem, AutocompleteRequest } from '../../utils/api';
|
||||
|
||||
type SearchType = 'url' | 'name';
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [autocompleteResults, setAutocompleteResults] = useState<NaverLocalSearchItem[]>([]);
|
||||
const [autocompleteResults, setAutocompleteResults] = useState<AccommodationSearchItem[]>([]);
|
||||
const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false);
|
||||
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
||||
const orbRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
|
@ -95,7 +95,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
|
||||
setIsAutocompleteLoading(true);
|
||||
try {
|
||||
const response = await searchNaverLocal(query);
|
||||
const response = await searchAccommodation(query);
|
||||
setAutocompleteResults(response.items || []);
|
||||
setShowAutocomplete(response.items && response.items.length > 0);
|
||||
} catch (error) {
|
||||
|
|
@ -108,7 +108,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
}, [searchType]);
|
||||
|
||||
// 자동완성 항목 선택
|
||||
const handleSelectAutocomplete = async (item: NaverLocalSearchItem) => {
|
||||
const handleSelectAutocomplete = async (item: AccommodationSearchItem) => {
|
||||
const request: AutocompleteRequest = {
|
||||
address: item.address,
|
||||
roadAddress: item.roadAddress,
|
||||
|
|
|
|||
|
|
@ -446,7 +446,6 @@ export function clearTokens() {
|
|||
// 인증 헤더 생성
|
||||
function getAuthHeader(): HeadersInit {
|
||||
const token = getAccessToken();
|
||||
console.log('[Auth] Token exists:', !!token, token ? `${token.substring(0, 20)}...` : 'null');
|
||||
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
|
|
@ -489,14 +488,10 @@ export async function kakaoCallback(code: string): Promise<KakaoCallbackResponse
|
|||
}
|
||||
|
||||
const data: KakaoCallbackResponse = await verifyResponse.json();
|
||||
console.log('[Auth] Kakao verify response:', {
|
||||
hasAccessToken: !!data.access_token,
|
||||
hasRefreshToken: !!data.refresh_token
|
||||
});
|
||||
|
||||
|
||||
// 토큰 저장
|
||||
saveTokens(data.access_token, data.refresh_token);
|
||||
console.log('[Auth] Tokens saved to localStorage');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -592,27 +587,19 @@ export function isLoggedIn(): boolean {
|
|||
}
|
||||
|
||||
// ============================================
|
||||
// 네이버 지역 검색 & 자동완성 API
|
||||
// 숙소 검색 & 자동완성 API
|
||||
// ============================================
|
||||
|
||||
export interface NaverLocalSearchItem {
|
||||
title: string;
|
||||
link: string;
|
||||
category: string;
|
||||
description: string;
|
||||
telephone: string;
|
||||
export interface AccommodationSearchItem {
|
||||
address: string;
|
||||
roadAddress: string;
|
||||
mapx: string;
|
||||
mapy: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface NaverLocalSearchResponse {
|
||||
lastBuildDate: string;
|
||||
total: number;
|
||||
start: number;
|
||||
display: number;
|
||||
items: NaverLocalSearchItem[];
|
||||
export interface AccommodationSearchResponse {
|
||||
count: number;
|
||||
items: AccommodationSearchItem[];
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface AutocompleteRequest {
|
||||
|
|
@ -621,9 +608,9 @@ export interface AutocompleteRequest {
|
|||
title: string;
|
||||
}
|
||||
|
||||
// 네이버 지역 검색 API (백엔드 프록시 경유)
|
||||
export async function searchNaverLocal(query: string): Promise<NaverLocalSearchResponse> {
|
||||
const response = await fetch(`${API_URL}/naver/local/search?query=${encodeURIComponent(query)}`, {
|
||||
// 숙소 검색 API (업체명 자동완성용)
|
||||
export async function searchAccommodation(query: string): Promise<AccommodationSearchResponse> {
|
||||
const response = await fetch(`${API_URL}/search/accommodation?query=${encodeURIComponent(query)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...getAuthHeader(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue