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