업체 직접입력 기능 추가
parent
0ea14da748
commit
1d855fd14c
134
index.css
134
index.css
|
|
@ -12318,3 +12318,137 @@
|
|||
background: rgba(255, 255, 255, 0.12);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* BusinessNameInputModal */
|
||||
.manual-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.manual-modal {
|
||||
background: #1a2a2b;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 16px;
|
||||
width: 375px;
|
||||
max-width: 92vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.manual-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.manual-modal-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.manual-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.manual-modal-close:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.manual-modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px 20px 24px;
|
||||
}
|
||||
|
||||
.manual-modal-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.manual-modal-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.manual-modal-input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(155, 202, 204, 0.25);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.manual-modal-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.manual-modal-input:focus {
|
||||
border-color: #9BCACC;
|
||||
}
|
||||
|
||||
.manual-modal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.manual-modal-cancel {
|
||||
flex: 1;
|
||||
padding: 11px 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(155, 202, 204, 0.3);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.manual-modal-cancel:hover {
|
||||
background: rgba(155, 202, 204, 0.08);
|
||||
}
|
||||
|
||||
.manual-modal-submit {
|
||||
flex: 2;
|
||||
padding: 11px 0;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: #9BCACC;
|
||||
color: #1a2a2b;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, opacity 0.15s;
|
||||
}
|
||||
|
||||
.manual-modal-submit:hover:not(:disabled) {
|
||||
background: #b0d8da;
|
||||
}
|
||||
|
||||
.manual-modal-submit:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
|
|||
27
src/App.tsx
27
src/App.tsx
|
|
@ -15,7 +15,7 @@ import YouTubeOAuthCallback from './pages/Social/YouTubeOAuthCallback';
|
|||
import ADO2ContentsPage from './pages/Dashboard/ADO2ContentsPage';
|
||||
import VideoDetailPage from './components/VideoDetailPage';
|
||||
import LoginPromptModal from './components/LoginPromptModal';
|
||||
import { crawlUrl, autocomplete, kakaoCallback, isLoggedIn, saveTokens, AutocompleteRequest } from './utils/api';
|
||||
import { crawlUrl, autocomplete, marketingAnalysis, kakaoCallback, isLoggedIn, saveTokens, AutocompleteRequest } from './utils/api';
|
||||
import { saveSearchHistory } from './components/SearchHistory/useSearchHistory';
|
||||
import { CrawlingResponse } from './types/api';
|
||||
|
||||
|
|
@ -314,6 +314,30 @@ const App: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// 업체명·주소 수동 입력으로 마케팅 분석 API 호출
|
||||
const handleManualInput = async (businessName: string, address: string) => {
|
||||
setViewMode('loading');
|
||||
setIsAnalysisComplete(false);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await marketingAnalysis(businessName, address);
|
||||
|
||||
if (!validateCrawlingResponse(data)) {
|
||||
throw new Error(t('app.autocompleteError'));
|
||||
}
|
||||
|
||||
setAnalysisData(data);
|
||||
localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(data));
|
||||
saveSearchHistory({ type: 'name', value: businessName, address, roadAddress: address });
|
||||
setIsAnalysisComplete(true);
|
||||
} catch (err) {
|
||||
console.error('Marketing analysis failed:', err);
|
||||
setError(t('app.autocompleteError'));
|
||||
setViewMode('landing');
|
||||
}
|
||||
};
|
||||
|
||||
// 테스트 데이터로 브랜드 분석 페이지 이동
|
||||
const handleTestData = (data: CrawlingResponse) => {
|
||||
const tagged = { ...data, _isTestData: true };
|
||||
|
|
@ -462,6 +486,7 @@ const App: React.FC = () => {
|
|||
<HeroSection
|
||||
onAnalyze={handleStartAnalysis}
|
||||
onAutocomplete={handleAutocomplete}
|
||||
onManualInput={handleManualInput}
|
||||
onTestData={handleTestData}
|
||||
onNext={() => scrollToSection(1)}
|
||||
error={error}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface BusinessNameInputModalProps {
|
||||
onClose: () => void;
|
||||
onSubmit: (businessName: string, address: string) => void;
|
||||
}
|
||||
|
||||
const BusinessNameInputModal: React.FC<BusinessNameInputModalProps> = ({ onClose, onSubmit }) => {
|
||||
const { t } = useTranslation();
|
||||
const [businessName, setBusinessName] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const isValid = businessName.trim().length > 0 && address.trim().length > 0;
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!isValid) return;
|
||||
onSubmit(businessName.trim(), address.trim());
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && isValid) handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="manual-modal-backdrop" onClick={onClose}>
|
||||
<div className="manual-modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="manual-modal-header">
|
||||
<span className="manual-modal-title">{t('landing.hero.manualModalTitle')}</span>
|
||||
<button className="manual-modal-close" onClick={onClose}>✕</button>
|
||||
</div>
|
||||
|
||||
<div className="manual-modal-body">
|
||||
<div className="manual-modal-field">
|
||||
<label className="manual-modal-label">{t('landing.hero.manualLabelName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="manual-modal-input"
|
||||
placeholder={t('landing.hero.manualPlaceholderName')}
|
||||
value={businessName}
|
||||
onChange={e => setBusinessName(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength={50}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="manual-modal-field">
|
||||
<label className="manual-modal-label">{t('landing.hero.manualLabelAddress')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="manual-modal-input"
|
||||
placeholder={t('landing.hero.manualPlaceholderAddress')}
|
||||
value={address}
|
||||
onChange={e => setAddress(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="manual-modal-actions">
|
||||
<button type="button" className="manual-modal-cancel" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="manual-modal-submit"
|
||||
onClick={handleSubmit}
|
||||
disabled={!isValid}
|
||||
>
|
||||
{t('landing.hero.analyzeButton')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessNameInputModal;
|
||||
|
|
@ -95,8 +95,8 @@
|
|||
"footer": {
|
||||
"company":"O2O Inc.",
|
||||
"businessNumber": "Business Registration No. : 620-87-00810 | CEO : Ahn Sungmin",
|
||||
"headquarters": "HQ : 41593 Unicorn Lab Daegu A05, 5F, 111 Oksan-ro, Buk-gu, Daegu, Korea",
|
||||
"researchCenter": "R&D : 13453 Rooms 504-505 (East), KT Pangyo Bldg, 32 Geumto-ro, Sujeong-gu, Seongnam-si, Gyeonggi-do, Korea",
|
||||
"headquarters": "HQ : Unicorn Lab Daegu A05, 5F, 111 Oksan-ro, Buk-gu, Daegu, Korea",
|
||||
"researchCenter": "R&D : Rooms 504-505 (East), KT Pangyo Bldg, 32 Geumto-ro, Sujeong-gu, Seongnam-si, Gyeonggi-do, Korea",
|
||||
"phone": "Tel : 070-4260-8310 | 010-2755-6463",
|
||||
"email": "Email : o2oteam@o2o.kr",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
|
|
@ -169,7 +169,13 @@
|
|||
"testDataLoading": "Loading...",
|
||||
"testData": "Test Data",
|
||||
"testDataLoadFailed": "Failed to load test data.",
|
||||
"searching": "Searching..."
|
||||
"searching": "Searching...",
|
||||
"searchTypeManual": "Manual Input",
|
||||
"manualModalTitle": "Enter Business Info",
|
||||
"manualLabelName": "Business Name",
|
||||
"manualLabelAddress": "Address",
|
||||
"manualPlaceholderName": "Enter the business name",
|
||||
"manualPlaceholderAddress": "Enter the address"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Welcome to ADO2.AI",
|
||||
|
|
@ -200,7 +206,13 @@
|
|||
"searchButton": "Search",
|
||||
"searching": "Searching...",
|
||||
"testDataLoading": "Loading...",
|
||||
"testData": "Test Data"
|
||||
"testData": "Test Data",
|
||||
"searchTypeManual": "Manual Input",
|
||||
"manualModalTitle": "Enter Business Info",
|
||||
"manualLabelName": "Business Name",
|
||||
"manualLabelAddress": "Address",
|
||||
"manualPlaceholderName": "Enter the business name",
|
||||
"manualPlaceholderAddress": "Enter the address"
|
||||
},
|
||||
"assetManagement": {
|
||||
"title": "Brand Assets",
|
||||
|
|
|
|||
|
|
@ -95,8 +95,8 @@
|
|||
"footer": {
|
||||
"company":"㈜에이아이오투오",
|
||||
"businessNumber": "사업자 등록번호 : 620-87-00810 | 대표 : 안성민",
|
||||
"headquarters": "본사 : 41593 대구광역시 북구 옥산로 111, 5층 유니콘랩 대구 A05호",
|
||||
"researchCenter": "연구소 : 13453 경기 성남시 수정구 금토로 32 (금토동) (주)KT 판교빌딩 504호~505호 (East)",
|
||||
"headquarters": "본사 : 대구광역시 북구 옥산로 111, 5층 유니콘랩 대구 A05호",
|
||||
"researchCenter": "연구소 : 경기 성남시 수정구 금토로 32 (금토동) (주)KT 판교빌딩 504호~505호 (East)",
|
||||
"phone": "전화 : 070-4260-8310 | 010-2755-6463",
|
||||
"email": "이메일 : o2oteam@o2o.kr",
|
||||
"privacyPolicy": "개인정보처리방침",
|
||||
|
|
@ -169,7 +169,13 @@
|
|||
"testDataLoading": "로딩 중...",
|
||||
"testData": "테스트 데이터",
|
||||
"testDataLoadFailed": "테스트 데이터를 불러오는데 실패했습니다.",
|
||||
"searching": "검색 중..."
|
||||
"searching": "검색 중...",
|
||||
"searchTypeManual": "직접 입력",
|
||||
"manualModalTitle": "업체 정보 직접 입력",
|
||||
"manualLabelName": "업체명",
|
||||
"manualLabelAddress": "주소",
|
||||
"manualPlaceholderName": "업체명을 입력하세요.",
|
||||
"manualPlaceholderAddress": "주소를 입력하세요."
|
||||
},
|
||||
"welcome": {
|
||||
"title": "ADO2.AI에 오신 것을 환영합니다.",
|
||||
|
|
@ -200,7 +206,13 @@
|
|||
"searchButton": "검색하기",
|
||||
"searching": "검색 중...",
|
||||
"testDataLoading": "로딩 중...",
|
||||
"testData": "테스트 데이터"
|
||||
"testData": "테스트 데이터",
|
||||
"searchTypeManual": "직접 입력",
|
||||
"manualModalTitle": "업체 정보 직접 입력",
|
||||
"manualLabelName": "업체명",
|
||||
"manualLabelAddress": "주소",
|
||||
"manualPlaceholderName": "업체명을 입력하세요.",
|
||||
"manualPlaceholderAddress": "주소를 입력하세요."
|
||||
},
|
||||
"assetManagement": {
|
||||
"title": "브랜드 에셋",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import ContentCalendarContent from './ContentCalendarContent';
|
|||
import LoadingSection from '../Analysis/LoadingSection';
|
||||
import AnalysisResultSection from '../Analysis/AnalysisResultSection';
|
||||
import { ImageItem, CrawlingResponse, UserMeResponse } from '../../types/api';
|
||||
import { crawlUrl, autocomplete, AutocompleteRequest, getUserMe, getUserCredits, clearTokens } from '../../utils/api';
|
||||
import { crawlUrl, autocomplete, marketingAnalysis, AutocompleteRequest, getUserMe, getUserCredits, clearTokens } from '../../utils/api';
|
||||
import { useTutorial } from '../../components/Tutorial/useTutorial';
|
||||
import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
|
||||
import TutorialOverlay, { TutorialRestartPopup } from '../../components/Tutorial/TutorialOverlay';
|
||||
|
|
@ -261,6 +261,33 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// 업체명·주소 수동 입력으로 마케팅 분석 API 호출
|
||||
const handleManualInput = async (businessName: string, address: string) => {
|
||||
goToWizardStep(-1);
|
||||
setIsAnalysisComplete(false);
|
||||
setAnalysisError(null);
|
||||
|
||||
try {
|
||||
const data = await marketingAnalysis(businessName, address);
|
||||
|
||||
if (data.processed_info) {
|
||||
data.processed_info.customer_name = data.processed_info.customer_name || businessName;
|
||||
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));
|
||||
saveSearchHistory({ type: 'name', value: businessName, address, roadAddress: address });
|
||||
setIsAnalysisComplete(true);
|
||||
} catch (err) {
|
||||
console.error('Marketing analysis error:', err);
|
||||
setAnalysisError(t('app.autocompleteError'));
|
||||
goToWizardStep(-2);
|
||||
}
|
||||
};
|
||||
|
||||
// 테스트용 랜덤 m_id 생성 (99 ~ 300)
|
||||
const generateRandomMId = (): number => {
|
||||
return Math.floor(Math.random() * (300 - 99 + 1)) + 99;
|
||||
|
|
@ -411,6 +438,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|||
<UrlInputContent
|
||||
onAnalyze={handleStartAnalysis}
|
||||
onAutocomplete={handleAutocomplete}
|
||||
onManualInput={handleManualInput}
|
||||
onTestData={handleTestData}
|
||||
error={analysisError}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import { searchAccommodation, AccommodationSearchItem, AutocompleteRequest } fro
|
|||
import { CrawlingResponse } from '../../types/api';
|
||||
import { useSearchHistory } from '../../components/SearchHistory/useSearchHistory';
|
||||
import SearchHistoryDropdown from '../../components/SearchHistory/SearchHistoryDropdown';
|
||||
import BusinessNameInputModal from '../../components/BusinessNameInputModal';
|
||||
|
||||
type SearchType = 'url' | 'name';
|
||||
type SearchType = 'url' | 'name' | 'manual';
|
||||
|
||||
// 환경변수에서 테스트 모드 확인
|
||||
const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
||||
|
|
@ -13,11 +14,12 @@ const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
|||
interface UrlInputContentProps {
|
||||
onAnalyze: (value: string, type?: SearchType) => void;
|
||||
onAutocomplete?: (data: AutocompleteRequest) => void;
|
||||
onManualInput?: (businessName: string, address: string) => void;
|
||||
onTestData?: (data: CrawlingResponse) => void;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomplete, onTestData, error }) => {
|
||||
const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomplete, onManualInput, onTestData, error }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [searchType, setSearchType] = useState<SearchType>('name');
|
||||
|
|
@ -27,7 +29,8 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<AccommodationSearchItem | null>(null);
|
||||
const [isLoadingTest, setIsLoadingTest] = useState(false);
|
||||
const { filteredHistory, showHistory, openHistory, closeHistory, hideOnInput, deleteItem } = useSearchHistory(searchType);
|
||||
const [isManualModalOpen, setIsManualModalOpen] = useState(false);
|
||||
const { filteredHistory, showHistory, openHistory, closeHistory, hideOnInput, deleteItem } = useSearchHistory(searchType === 'manual' ? 'name' : searchType);
|
||||
|
||||
const handleSelectHistory = (item: { type: 'url' | 'name'; value: string; address?: string; roadAddress?: string }) => {
|
||||
closeHistory();
|
||||
|
|
@ -67,6 +70,7 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
const searchTypeOptions = [
|
||||
{ value: 'url' as SearchType, label: 'URL' },
|
||||
{ value: 'name' as SearchType, label: t('urlInput.searchTypeBusinessName') },
|
||||
{ value: 'manual' as SearchType, label: t('urlInput.searchTypeManual') },
|
||||
];
|
||||
|
||||
const getPlaceholder = () => {
|
||||
|
|
@ -215,8 +219,13 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
type="button"
|
||||
className={`url-input-dropdown-item ${searchType === option.value ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (option.value === 'manual') {
|
||||
setIsManualModalOpen(true);
|
||||
setIsDropdownOpen(false);
|
||||
} else {
|
||||
setSearchType(option.value);
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
|
|
@ -329,6 +338,13 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
|||
{isLoadingTest ? t('urlInput.testDataLoading') : t('urlInput.testData')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isManualModalOpen && (
|
||||
<BusinessNameInputModal
|
||||
onClose={() => setIsManualModalOpen(false)}
|
||||
onSubmit={(businessName, address) => { setIsManualModalOpen(false); onManualInput?.(businessName, address); }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
|
|||
import TutorialOverlay from '../../components/Tutorial/TutorialOverlay';
|
||||
import { useSearchHistory } from '../../components/SearchHistory/useSearchHistory';
|
||||
import SearchHistoryDropdown from '../../components/SearchHistory/SearchHistoryDropdown';
|
||||
import BusinessNameInputModal from '../../components/BusinessNameInputModal';
|
||||
|
||||
type SearchType = 'url' | 'name';
|
||||
type SearchType = 'url' | 'name' | 'manual';
|
||||
|
||||
// 환경변수에서 테스트 모드 확인
|
||||
const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
||||
|
|
@ -17,6 +18,7 @@ const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
|||
interface HeroSectionProps {
|
||||
onAnalyze?: (value: string, type?: SearchType) => void;
|
||||
onAutocomplete?: (data: AutocompleteRequest) => void;
|
||||
onManualInput?: (businessName: string, address: string) => void;
|
||||
onTestData?: (data: CrawlingResponse) => void;
|
||||
onNext?: () => void;
|
||||
error?: string | null;
|
||||
|
|
@ -62,11 +64,12 @@ const orbConfigs: OrbConfig[] = [
|
|||
{ id: 'orb-6', size: 450, initialX: 65, initialY: 70, color: 'radial-gradient(circle, rgba(180, 255, 235, 0.95) 15%, rgba(200, 160, 255, 0.8) 50%, rgba(94, 235, 195, 0.45) 100%)', minX: 45, maxX: 110, minY: 55, maxY: 110 },
|
||||
];
|
||||
|
||||
const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, onTestData, onNext, error: externalError, scrollProgress = 0 }) => {
|
||||
const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, onManualInput, onTestData, onNext, error: externalError, scrollProgress = 0 }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [searchType, setSearchType] = useState<SearchType>('name');
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isManualModalOpen, setIsManualModalOpen] = useState(false);
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [isLoadingTest, setIsLoadingTest] = useState(false);
|
||||
|
||||
|
|
@ -86,7 +89,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
setIsLoadingTest(false);
|
||||
}
|
||||
};
|
||||
const { filteredHistory, showHistory, openHistory, closeHistory, hideOnInput, deleteItem } = useSearchHistory(searchType);
|
||||
const { filteredHistory, showHistory, openHistory, closeHistory, hideOnInput, deleteItem } = useSearchHistory(searchType === 'manual' ? 'name' : searchType);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [autocompleteResults, setAutocompleteResults] = useState<AccommodationSearchItem[]>([]);
|
||||
const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false);
|
||||
|
|
@ -113,6 +116,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
const searchTypeOptions = [
|
||||
{ value: 'url' as SearchType, label: 'URL' },
|
||||
{ value: 'name' as SearchType, label: t('landing.hero.searchTypeBusinessName') },
|
||||
{ value: 'manual' as SearchType, label: t('landing.hero.searchTypeManual') },
|
||||
];
|
||||
|
||||
// 드롭다운 외부 클릭 감지
|
||||
|
|
@ -377,8 +381,13 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
type="button"
|
||||
className={`hero-dropdown-item ${searchType === option.value ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (option.value === 'manual') {
|
||||
setIsManualModalOpen(true);
|
||||
setIsDropdownOpen(false);
|
||||
} else {
|
||||
setSearchType(option.value);
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
|
|
@ -555,6 +564,13 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
|||
groupProgress={tutorial.groupProgress}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isManualModalOpen && (
|
||||
<BusinessNameInputModal
|
||||
onClose={() => setIsManualModalOpen(false)}
|
||||
onSubmit={(businessName, address) => { setIsManualModalOpen(false); onManualInput?.(businessName, address); }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -900,6 +900,37 @@ export async function autocomplete(request: AutocompleteRequest): Promise<Crawli
|
|||
}
|
||||
}
|
||||
|
||||
// 업체명·주소 직접 입력으로 마케팅 분석
|
||||
export async function marketingAnalysis(storeName: string, address: string): Promise<CrawlingResponse> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), CRAWL_TIMEOUT);
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(`${API_URL}/marketing`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ store_name: storeName, address }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error('요청 시간이 초과되었습니다. 다시 시도해주세요.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Social OAuth TOKEN_EXPIRED 처리
|
||||
// ============================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue