업체 직접입력 기능 추가
parent
0ea14da748
commit
1d855fd14c
134
index.css
134
index.css
|
|
@ -12318,3 +12318,137 @@
|
||||||
background: rgba(255, 255, 255, 0.12);
|
background: rgba(255, 255, 255, 0.12);
|
||||||
color: rgba(255, 255, 255, 0.4);
|
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 ADO2ContentsPage from './pages/Dashboard/ADO2ContentsPage';
|
||||||
import VideoDetailPage from './components/VideoDetailPage';
|
import VideoDetailPage from './components/VideoDetailPage';
|
||||||
import LoginPromptModal from './components/LoginPromptModal';
|
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 { saveSearchHistory } from './components/SearchHistory/useSearchHistory';
|
||||||
import { CrawlingResponse } from './types/api';
|
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 handleTestData = (data: CrawlingResponse) => {
|
||||||
const tagged = { ...data, _isTestData: true };
|
const tagged = { ...data, _isTestData: true };
|
||||||
|
|
@ -462,6 +486,7 @@ const App: React.FC = () => {
|
||||||
<HeroSection
|
<HeroSection
|
||||||
onAnalyze={handleStartAnalysis}
|
onAnalyze={handleStartAnalysis}
|
||||||
onAutocomplete={handleAutocomplete}
|
onAutocomplete={handleAutocomplete}
|
||||||
|
onManualInput={handleManualInput}
|
||||||
onTestData={handleTestData}
|
onTestData={handleTestData}
|
||||||
onNext={() => scrollToSection(1)}
|
onNext={() => scrollToSection(1)}
|
||||||
error={error}
|
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": {
|
"footer": {
|
||||||
"company":"O2O Inc.",
|
"company":"O2O Inc.",
|
||||||
"businessNumber": "Business Registration No. : 620-87-00810 | CEO : Ahn Sungmin",
|
"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",
|
"headquarters": "HQ : 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",
|
"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",
|
"phone": "Tel : 070-4260-8310 | 010-2755-6463",
|
||||||
"email": "Email : o2oteam@o2o.kr",
|
"email": "Email : o2oteam@o2o.kr",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Privacy Policy",
|
||||||
|
|
@ -169,7 +169,13 @@
|
||||||
"testDataLoading": "Loading...",
|
"testDataLoading": "Loading...",
|
||||||
"testData": "Test Data",
|
"testData": "Test Data",
|
||||||
"testDataLoadFailed": "Failed to load 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": {
|
"welcome": {
|
||||||
"title": "Welcome to ADO2.AI",
|
"title": "Welcome to ADO2.AI",
|
||||||
|
|
@ -200,7 +206,13 @@
|
||||||
"searchButton": "Search",
|
"searchButton": "Search",
|
||||||
"searching": "Searching...",
|
"searching": "Searching...",
|
||||||
"testDataLoading": "Loading...",
|
"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": {
|
"assetManagement": {
|
||||||
"title": "Brand Assets",
|
"title": "Brand Assets",
|
||||||
|
|
|
||||||
|
|
@ -95,8 +95,8 @@
|
||||||
"footer": {
|
"footer": {
|
||||||
"company":"㈜에이아이오투오",
|
"company":"㈜에이아이오투오",
|
||||||
"businessNumber": "사업자 등록번호 : 620-87-00810 | 대표 : 안성민",
|
"businessNumber": "사업자 등록번호 : 620-87-00810 | 대표 : 안성민",
|
||||||
"headquarters": "본사 : 41593 대구광역시 북구 옥산로 111, 5층 유니콘랩 대구 A05호",
|
"headquarters": "본사 : 대구광역시 북구 옥산로 111, 5층 유니콘랩 대구 A05호",
|
||||||
"researchCenter": "연구소 : 13453 경기 성남시 수정구 금토로 32 (금토동) (주)KT 판교빌딩 504호~505호 (East)",
|
"researchCenter": "연구소 : 경기 성남시 수정구 금토로 32 (금토동) (주)KT 판교빌딩 504호~505호 (East)",
|
||||||
"phone": "전화 : 070-4260-8310 | 010-2755-6463",
|
"phone": "전화 : 070-4260-8310 | 010-2755-6463",
|
||||||
"email": "이메일 : o2oteam@o2o.kr",
|
"email": "이메일 : o2oteam@o2o.kr",
|
||||||
"privacyPolicy": "개인정보처리방침",
|
"privacyPolicy": "개인정보처리방침",
|
||||||
|
|
@ -169,7 +169,13 @@
|
||||||
"testDataLoading": "로딩 중...",
|
"testDataLoading": "로딩 중...",
|
||||||
"testData": "테스트 데이터",
|
"testData": "테스트 데이터",
|
||||||
"testDataLoadFailed": "테스트 데이터를 불러오는데 실패했습니다.",
|
"testDataLoadFailed": "테스트 데이터를 불러오는데 실패했습니다.",
|
||||||
"searching": "검색 중..."
|
"searching": "검색 중...",
|
||||||
|
"searchTypeManual": "직접 입력",
|
||||||
|
"manualModalTitle": "업체 정보 직접 입력",
|
||||||
|
"manualLabelName": "업체명",
|
||||||
|
"manualLabelAddress": "주소",
|
||||||
|
"manualPlaceholderName": "업체명을 입력하세요.",
|
||||||
|
"manualPlaceholderAddress": "주소를 입력하세요."
|
||||||
},
|
},
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"title": "ADO2.AI에 오신 것을 환영합니다.",
|
"title": "ADO2.AI에 오신 것을 환영합니다.",
|
||||||
|
|
@ -200,7 +206,13 @@
|
||||||
"searchButton": "검색하기",
|
"searchButton": "검색하기",
|
||||||
"searching": "검색 중...",
|
"searching": "검색 중...",
|
||||||
"testDataLoading": "로딩 중...",
|
"testDataLoading": "로딩 중...",
|
||||||
"testData": "테스트 데이터"
|
"testData": "테스트 데이터",
|
||||||
|
"searchTypeManual": "직접 입력",
|
||||||
|
"manualModalTitle": "업체 정보 직접 입력",
|
||||||
|
"manualLabelName": "업체명",
|
||||||
|
"manualLabelAddress": "주소",
|
||||||
|
"manualPlaceholderName": "업체명을 입력하세요.",
|
||||||
|
"manualPlaceholderAddress": "주소를 입력하세요."
|
||||||
},
|
},
|
||||||
"assetManagement": {
|
"assetManagement": {
|
||||||
"title": "브랜드 에셋",
|
"title": "브랜드 에셋",
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import ContentCalendarContent from './ContentCalendarContent';
|
||||||
import LoadingSection from '../Analysis/LoadingSection';
|
import LoadingSection from '../Analysis/LoadingSection';
|
||||||
import AnalysisResultSection from '../Analysis/AnalysisResultSection';
|
import AnalysisResultSection from '../Analysis/AnalysisResultSection';
|
||||||
import { ImageItem, CrawlingResponse, UserMeResponse } from '../../types/api';
|
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 { useTutorial } from '../../components/Tutorial/useTutorial';
|
||||||
import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
|
import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
|
||||||
import TutorialOverlay, { TutorialRestartPopup } from '../../components/Tutorial/TutorialOverlay';
|
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)
|
// 테스트용 랜덤 m_id 생성 (99 ~ 300)
|
||||||
const generateRandomMId = (): number => {
|
const generateRandomMId = (): number => {
|
||||||
return Math.floor(Math.random() * (300 - 99 + 1)) + 99;
|
return Math.floor(Math.random() * (300 - 99 + 1)) + 99;
|
||||||
|
|
@ -411,6 +438,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
<UrlInputContent
|
<UrlInputContent
|
||||||
onAnalyze={handleStartAnalysis}
|
onAnalyze={handleStartAnalysis}
|
||||||
onAutocomplete={handleAutocomplete}
|
onAutocomplete={handleAutocomplete}
|
||||||
|
onManualInput={handleManualInput}
|
||||||
onTestData={handleTestData}
|
onTestData={handleTestData}
|
||||||
error={analysisError}
|
error={analysisError}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ import { searchAccommodation, AccommodationSearchItem, AutocompleteRequest } fro
|
||||||
import { CrawlingResponse } from '../../types/api';
|
import { CrawlingResponse } from '../../types/api';
|
||||||
import { useSearchHistory } from '../../components/SearchHistory/useSearchHistory';
|
import { useSearchHistory } from '../../components/SearchHistory/useSearchHistory';
|
||||||
import SearchHistoryDropdown from '../../components/SearchHistory/SearchHistoryDropdown';
|
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';
|
const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
||||||
|
|
@ -13,11 +14,12 @@ const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
||||||
interface UrlInputContentProps {
|
interface UrlInputContentProps {
|
||||||
onAnalyze: (value: string, type?: SearchType) => void;
|
onAnalyze: (value: string, type?: SearchType) => void;
|
||||||
onAutocomplete?: (data: AutocompleteRequest) => void;
|
onAutocomplete?: (data: AutocompleteRequest) => void;
|
||||||
|
onManualInput?: (businessName: string, address: string) => void;
|
||||||
onTestData?: (data: CrawlingResponse) => void;
|
onTestData?: (data: CrawlingResponse) => void;
|
||||||
error: string | null;
|
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 { t, i18n } = useTranslation();
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [searchType, setSearchType] = useState<SearchType>('name');
|
const [searchType, setSearchType] = useState<SearchType>('name');
|
||||||
|
|
@ -27,7 +29,8 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
||||||
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
||||||
const [selectedItem, setSelectedItem] = useState<AccommodationSearchItem | null>(null);
|
const [selectedItem, setSelectedItem] = useState<AccommodationSearchItem | null>(null);
|
||||||
const [isLoadingTest, setIsLoadingTest] = useState(false);
|
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 }) => {
|
const handleSelectHistory = (item: { type: 'url' | 'name'; value: string; address?: string; roadAddress?: string }) => {
|
||||||
closeHistory();
|
closeHistory();
|
||||||
|
|
@ -67,6 +70,7 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
||||||
const searchTypeOptions = [
|
const searchTypeOptions = [
|
||||||
{ value: 'url' as SearchType, label: 'URL' },
|
{ value: 'url' as SearchType, label: 'URL' },
|
||||||
{ value: 'name' as SearchType, label: t('urlInput.searchTypeBusinessName') },
|
{ value: 'name' as SearchType, label: t('urlInput.searchTypeBusinessName') },
|
||||||
|
{ value: 'manual' as SearchType, label: t('urlInput.searchTypeManual') },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getPlaceholder = () => {
|
const getPlaceholder = () => {
|
||||||
|
|
@ -215,8 +219,13 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
||||||
type="button"
|
type="button"
|
||||||
className={`url-input-dropdown-item ${searchType === option.value ? 'active' : ''}`}
|
className={`url-input-dropdown-item ${searchType === option.value ? 'active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchType(option.value);
|
if (option.value === 'manual') {
|
||||||
setIsDropdownOpen(false);
|
setIsManualModalOpen(true);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
} else {
|
||||||
|
setSearchType(option.value);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|
@ -329,6 +338,13 @@ const UrlInputContent: React.FC<UrlInputContentProps> = ({ onAnalyze, onAutocomp
|
||||||
{isLoadingTest ? t('urlInput.testDataLoading') : t('urlInput.testData')}
|
{isLoadingTest ? t('urlInput.testDataLoading') : t('urlInput.testData')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isManualModalOpen && (
|
||||||
|
<BusinessNameInputModal
|
||||||
|
onClose={() => setIsManualModalOpen(false)}
|
||||||
|
onSubmit={(businessName, address) => { setIsManualModalOpen(false); onManualInput?.(businessName, address); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
|
||||||
import TutorialOverlay from '../../components/Tutorial/TutorialOverlay';
|
import TutorialOverlay from '../../components/Tutorial/TutorialOverlay';
|
||||||
import { useSearchHistory } from '../../components/SearchHistory/useSearchHistory';
|
import { useSearchHistory } from '../../components/SearchHistory/useSearchHistory';
|
||||||
import SearchHistoryDropdown from '../../components/SearchHistory/SearchHistoryDropdown';
|
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';
|
const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
||||||
|
|
@ -17,6 +18,7 @@ const isTestPage = import.meta.env.VITE_IS_TESTPAGE === 'true';
|
||||||
interface HeroSectionProps {
|
interface HeroSectionProps {
|
||||||
onAnalyze?: (value: string, type?: SearchType) => void;
|
onAnalyze?: (value: string, type?: SearchType) => void;
|
||||||
onAutocomplete?: (data: AutocompleteRequest) => void;
|
onAutocomplete?: (data: AutocompleteRequest) => void;
|
||||||
|
onManualInput?: (businessName: string, address: string) => void;
|
||||||
onTestData?: (data: CrawlingResponse) => void;
|
onTestData?: (data: CrawlingResponse) => void;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
error?: string | null;
|
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 },
|
{ 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 { t, i18n } = useTranslation();
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [searchType, setSearchType] = useState<SearchType>('name');
|
const [searchType, setSearchType] = useState<SearchType>('name');
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
const [isManualModalOpen, setIsManualModalOpen] = useState(false);
|
||||||
const [localError, setLocalError] = useState('');
|
const [localError, setLocalError] = useState('');
|
||||||
const [isLoadingTest, setIsLoadingTest] = useState(false);
|
const [isLoadingTest, setIsLoadingTest] = useState(false);
|
||||||
|
|
||||||
|
|
@ -86,7 +89,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
||||||
setIsLoadingTest(false);
|
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 [isFocused, setIsFocused] = useState(false);
|
||||||
const [autocompleteResults, setAutocompleteResults] = useState<AccommodationSearchItem[]>([]);
|
const [autocompleteResults, setAutocompleteResults] = useState<AccommodationSearchItem[]>([]);
|
||||||
const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false);
|
const [isAutocompleteLoading, setIsAutocompleteLoading] = useState(false);
|
||||||
|
|
@ -113,6 +116,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
||||||
const searchTypeOptions = [
|
const searchTypeOptions = [
|
||||||
{ value: 'url' as SearchType, label: 'URL' },
|
{ value: 'url' as SearchType, label: 'URL' },
|
||||||
{ value: 'name' as SearchType, label: t('landing.hero.searchTypeBusinessName') },
|
{ 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"
|
type="button"
|
||||||
className={`hero-dropdown-item ${searchType === option.value ? 'active' : ''}`}
|
className={`hero-dropdown-item ${searchType === option.value ? 'active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchType(option.value);
|
if (option.value === 'manual') {
|
||||||
setIsDropdownOpen(false);
|
setIsManualModalOpen(true);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
} else {
|
||||||
|
setSearchType(option.value);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
|
@ -555,6 +564,13 @@ const HeroSection: React.FC<HeroSectionProps> = ({ onAnalyze, onAutocomplete, on
|
||||||
groupProgress={tutorial.groupProgress}
|
groupProgress={tutorial.groupProgress}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isManualModalOpen && (
|
||||||
|
<BusinessNameInputModal
|
||||||
|
onClose={() => setIsManualModalOpen(false)}
|
||||||
|
onSubmit={(businessName, address) => { setIsManualModalOpen(false); onManualInput?.(businessName, address); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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 처리
|
// Social OAuth TOKEN_EXPIRED 처리
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue