From 6d89a289826847afbe31eb7fdbe26980cc803dd2 Mon Sep 17 00:00:00 2001 From: hbyang Date: Thu, 29 Jan 2026 16:06:35 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=99=84=EC=84=B1=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Analysis/AnalysisResultSection.tsx | 16 +++---- src/pages/Dashboard/GenerationFlow.tsx | 34 +++++++++++++- src/pages/Dashboard/UrlInputContent.tsx | 47 ++++++++++++-------- src/pages/Landing/HeroSection.tsx | 8 ++-- src/utils/api.ts | 35 +++++---------- 5 files changed, 84 insertions(+), 56 deletions(-) diff --git a/src/pages/Analysis/AnalysisResultSection.tsx b/src/pages/Analysis/AnalysisResultSection.tsx index 50de724..3444647 100755 --- a/src/pages/Analysis/AnalysisResultSection.tsx +++ b/src/pages/Analysis/AnalysisResultSection.tsx @@ -292,11 +292,11 @@ const LayoutGridIcon = () => ( const AnalysisResultSection: React.FC = ({ 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 = ({ onBack, o

브랜드 인텔리전스

- AI 데이터 분석을 통해 도출된 {processed_info.customer_name}의 핵심 전략입니다. + AI 데이터 분석을 통해 도출된 {processed_info?.customer_name || '브랜드'}의 핵심 전략입니다.

@@ -346,12 +346,12 @@ const AnalysisResultSection: React.FC = ({ onBack, o -

{processed_info.customer_name}

+

{processed_info?.customer_name || '브랜드명'}

-

{processed_info.detail_region_info || '주소 정보 없음'}

-

{processed_info.region}

+

{processed_info?.detail_region_info || '주소 정보 없음'}

+

{processed_info?.region || ''}

diff --git a/src/pages/Dashboard/GenerationFlow.tsx b/src/pages/Dashboard/GenerationFlow.tsx index 878f6ac..d7be301 100755 --- a/src/pages/Dashboard/GenerationFlow.tsx +++ b/src/pages/Dashboard/GenerationFlow.tsx @@ -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 = ({ 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 = ({ return ( ); diff --git a/src/pages/Dashboard/UrlInputContent.tsx b/src/pages/Dashboard/UrlInputContent.tsx index 47d6c5b..d75dbf3 100644 --- a/src/pages/Dashboard/UrlInputContent.tsx +++ b/src/pages/Dashboard/UrlInputContent.tsx @@ -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 = ({ onAnalyze, onAutocomp const [inputValue, setInputValue] = useState(''); const [searchType, setSearchType] = useState('url'); const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [autocompleteResults, setAutocompleteResults] = useState([]); + const [autocompleteResults, setAutocompleteResults] = useState([]); const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false); const [showAutocomplete, setShowAutocomplete] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); const debounceRef = useRef(null); const autocompleteRef = useRef(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 = ({ 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 = ({ 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 = ({ onAnalyze, onAutocomp )} + + {/* 검색 버튼 */} + {/* 에러 메시지 */} diff --git a/src/pages/Landing/HeroSection.tsx b/src/pages/Landing/HeroSection.tsx index 31c32b9..1a44d99 100755 --- a/src/pages/Landing/HeroSection.tsx +++ b/src/pages/Landing/HeroSection.tsx @@ -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 = ({ onAnalyze, onAutocomplete, on const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [localError, setLocalError] = useState(''); const [isFocused, setIsFocused] = useState(false); - const [autocompleteResults, setAutocompleteResults] = useState([]); + const [autocompleteResults, setAutocompleteResults] = useState([]); const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false); const [showAutocomplete, setShowAutocomplete] = useState(false); const orbRefs = useRef<(HTMLDivElement | null)[]>([]); @@ -95,7 +95,7 @@ const HeroSection: React.FC = ({ 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 = ({ onAnalyze, onAutocomplete, on }, [searchType]); // 자동완성 항목 선택 - const handleSelectAutocomplete = async (item: NaverLocalSearchItem) => { + const handleSelectAutocomplete = async (item: AccommodationSearchItem) => { const request: AutocompleteRequest = { address: item.address, roadAddress: item.roadAddress, diff --git a/src/utils/api.ts b/src/utils/api.ts index 2b571bf..c6242c7 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -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 { - const response = await fetch(`${API_URL}/naver/local/search?query=${encodeURIComponent(query)}`, { +// 숙소 검색 API (업체명 자동완성용) +export async function searchAccommodation(query: string): Promise { + const response = await fetch(`${API_URL}/search/accommodation?query=${encodeURIComponent(query)}`, { method: 'GET', headers: { ...getAuthHeader(),