diff --git a/index.css b/index.css index ca9fdd2..3d0e089 100644 --- a/index.css +++ b/index.css @@ -4113,6 +4113,27 @@ cursor: not-allowed; } +.url-input-manual-button { + margin: auto; + max-width: 400px; + width: 100%; + padding: 11px 16px; + border-radius: 12px; + border: 1px solid rgba(155, 202, 204, 0.35); + background: transparent; + color: rgba(155, 202, 204, 0.8); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-normal); +} + +.url-input-manual-button:hover { + border-color: #9BCACC; + color: #9BCACC; + background: rgba(155, 202, 204, 0.06); +} + .url-input-error { color: #F56565; font-family: 'Pretendard', sans-serif; @@ -4676,6 +4697,26 @@ } } +.hero-manual-button { + width: 100%; + padding: 11px 16px; + height: 48px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.3); + background: transparent; + color: rgba(255, 255, 255, 0.8); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-normal); +} + +.hero-manual-button:hover { + border-color: rgba(255, 255, 255, 0.6); + color: #fff; + background: rgba(255, 255, 255, 0.06); +} + .hero-button:hover { background-color: #9a5ef0; animation: none; @@ -9265,6 +9306,15 @@ font-weight: 600; } +.city-modal-item-all { + border-style: dashed; + border-color: rgba(255,255,255,0.3); +} + +.city-modal-item-all.active { + border-style: solid; +} + .ado2-contents-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); @@ -12464,6 +12514,31 @@ border-color: #9BCACC; } +.manual-modal-city-btn { + 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: rgba(255, 255, 255, 0.3); + font-size: 14px; + text-align: left; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + transition: border-color 0.15s; + box-sizing: border-box; +} + +.manual-modal-city-btn.selected { + color: #fff; +} + +.manual-modal-city-btn:hover { + border-color: #9BCACC; +} + .manual-modal-actions { display: flex; gap: 8px; diff --git a/src/components/BusinessNameInputModal.tsx b/src/components/BusinessNameInputModal.tsx index 401c937..2550383 100644 --- a/src/components/BusinessNameInputModal.tsx +++ b/src/components/BusinessNameInputModal.tsx @@ -1,5 +1,7 @@ import React, { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; +import CitySelectModal, { REGIONS } from './CitySelectModal'; interface BusinessNameInputModalProps { onClose: () => void; @@ -9,7 +11,9 @@ interface BusinessNameInputModalProps { const BusinessNameInputModal: React.FC = ({ onClose, onSubmit }) => { const { t } = useTranslation(); const [businessName, setBusinessName] = useState(''); - const [address, setAddress] = useState(''); + const [selectedCity, setSelectedCity] = useState(''); + const [detailAddress, setDetailAddress] = useState(''); + const [isCityModalOpen, setIsCityModalOpen] = useState(false); useEffect(() => { document.body.style.overflow = 'hidden'; @@ -25,11 +29,19 @@ const BusinessNameInputModal: React.FC = ({ onClose }; }, [onClose]); - const isValid = businessName.trim().length > 0 && address.trim().length > 0; + const handleCitySelect = (city: string) => { + const region = REGIONS.find(r => r.cities.includes(city)); + // 특별시/광역시는 도 이름 없이 도시명만 사용 + const prefix = region && region.label !== '특별시 / 광역시' ? `${region.label} ` : ''; + setSelectedCity(`${prefix}${city}`); + }; + + const isValid = businessName.trim().length > 0 && selectedCity.length > 0 && detailAddress.trim().length > 0; const handleSubmit = () => { if (!isValid) return; - onSubmit(businessName.trim(), address.trim()); + const fullAddress = `${selectedCity} ${detailAddress.trim()}`; + onSubmit(businessName.trim(), fullAddress); onClose(); }; @@ -37,58 +49,86 @@ const BusinessNameInputModal: React.FC = ({ onClose if (e.key === 'Enter' && isValid) handleSubmit(); }; - return ( -
-
e.stopPropagation()}> -
- {t('landing.hero.manualModalTitle')} - -
+ // CitySelectModal의 selected prop용 — 도시명만 추출 + const cityOnly = selectedCity.split(' ').pop() ?? ''; -
-
- - setBusinessName(e.target.value)} - onKeyDown={handleKeyDown} - maxLength={50} - autoFocus - /> + return createPortal( + <> +
+
e.stopPropagation()}> +
+ {t('landing.hero.manualModalTitle')} +
-
- - setAddress(e.target.value)} - onKeyDown={handleKeyDown} - maxLength={100} - /> -
+
+
+ + setBusinessName(e.target.value)} + onKeyDown={handleKeyDown} + maxLength={50} + autoFocus + /> +
-
- - +
+ + +
+ +
+ + setDetailAddress(e.target.value)} + onKeyDown={handleKeyDown} + maxLength={100} + /> +
+ +
+ + +
-
+ + {isCityModalOpen && ( + { handleCitySelect(city); setIsCityModalOpen(false); }} + onClose={() => setIsCityModalOpen(false)} + /> + )} + , + document.body ); }; diff --git a/src/components/CitySelectModal.tsx b/src/components/CitySelectModal.tsx index de42fba..2592264 100644 --- a/src/components/CitySelectModal.tsx +++ b/src/components/CitySelectModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; -const REGIONS: { label: string; cities: string[] }[] = [ +export const REGIONS: { label: string; cities: string[] }[] = [ { label: '특별시 / 광역시', cities: ['서울시', '부산시', '대구시', '인천시', '광주시', '대전시', '울산시', '세종시'], @@ -88,10 +88,11 @@ const CitySelectModal: React.FC = ({ selected, onSelect, o return () => { document.body.style.overflow = ''; }; }, []); - const cities = REGIONS.find(r => r.label === activeRegion)?.cities ?? []; + const activeEntry = REGIONS.find(r => r.label === activeRegion); + const cities = activeEntry?.cities ?? []; - const handleCityClick = (city: string) => { - onSelect(city === selected ? '' : city); + const handleSelect = (value: string) => { + onSelect(value === selected ? '' : value); onClose(); }; @@ -112,25 +113,38 @@ const CitySelectModal: React.FC = ({ selected, onSelect, o {activeRegion === null ? (
- {REGIONS.map(r => ( - - ))} + {REGIONS.map(r => { + const isRegionSelected = selected === r.label; + const hasCitySelected = r.cities.includes(selected); + return ( + + ); + })}
) : (
+ {activeRegion !== '특별시 / 광역시' && ( + + )} {cities.map(city => ( diff --git a/src/components/Tutorial/TutorialOverlay.tsx b/src/components/Tutorial/TutorialOverlay.tsx index 7f76c93..1c7d5a3 100644 --- a/src/components/Tutorial/TutorialOverlay.tsx +++ b/src/components/Tutorial/TutorialOverlay.tsx @@ -143,7 +143,11 @@ const TutorialOverlay: React.FC = ({ }; const tryBind = (): boolean => { - const el = document.querySelector(hint.targetSelector) as HTMLElement | null; + const els = Array.from(document.querySelectorAll(hint.targetSelector)); + const el = (els.find(e => { + const r = (e as HTMLElement).getBoundingClientRect(); + return r.width > 0 && r.height > 0; + }) ?? els[0]) as HTMLElement | null; if (!el) return false; bindToTarget(el); return true; diff --git a/src/components/Tutorial/tutorialSteps.ts b/src/components/Tutorial/tutorialSteps.ts index 87f9d4a..0633fd7 100644 --- a/src/components/Tutorial/tutorialSteps.ts +++ b/src/components/Tutorial/tutorialSteps.ts @@ -64,6 +64,15 @@ export const tutorialSteps: TutorialStepDef[] = [ noSpotlight: true, variant: 'bubble', }, + { + targetSelector: '.hero-manual-button', + titleKey: 'tutorial.landing.manual.title', + descriptionKey: 'tutorial.landing.manual.desc', + position: 'bottom', + clickToAdvance: false, + noSpotlight: true, + variant: 'bubble', + }, { targetSelector: '.hero-button', titleKey: 'tutorial.landing.button.title', diff --git a/src/locales/en.json b/src/locales/en.json index 6676ca5..4162675 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -30,6 +30,7 @@ "intro": { "title": "Welcome to ADO2 Tutorial", "desc": "We'll guide you through ADO2 step by step." }, "dropdown": { "title": "Choose Search Type", "desc": "Select URL or business name from the dropdown." }, "field": { "title": "Enter Search Term", "desc": "For URL, paste a Naver Maps share URL.\nFor business name, type the name and select from the list." }, + "manual": { "title": "Direct Input", "desc": "You can also enter the business name and address manually to start analysis." }, "button": { "title": "Start Brand Analysis", "desc": "Click the button to let AI start analyzing your brand." } }, "asset": { @@ -174,8 +175,12 @@ "manualModalTitle": "Enter Business Info", "manualLabelName": "Business Name", "manualLabelAddress": "Address", + "manualLabelRegion": "Region", + "manualLabelDetail": "Detail Address", "manualPlaceholderName": "Enter the business name", - "manualPlaceholderAddress": "Enter the address" + "manualPlaceholderAddress": "Enter the address", + "manualPlaceholderRegion": "Select a region", + "manualPlaceholderDetail": "Enter detail address (e.g. Gangnam-gu Teheran-ro 123)" }, "welcome": { "title": "Welcome to ADO2.AI", @@ -211,8 +216,12 @@ "manualModalTitle": "Enter Business Info", "manualLabelName": "Business Name", "manualLabelAddress": "Address", + "manualLabelRegion": "Region", + "manualLabelDetail": "Detail Address", "manualPlaceholderName": "Enter the business name", - "manualPlaceholderAddress": "Enter the address" + "manualPlaceholderAddress": "Enter the address", + "manualPlaceholderRegion": "Select a region", + "manualPlaceholderDetail": "Enter detail address (e.g. Gangnam-gu Teheran-ro 123)" }, "assetManagement": { "title": "Brand Assets", diff --git a/src/locales/ko.json b/src/locales/ko.json index 1f20d08..1fd080f 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -30,6 +30,7 @@ "intro": { "title": "ADO2 튜토리얼 시작", "desc": "ADO2 사용 방법을 단계별로 안내해 드릴게요." }, "dropdown": { "title": "검색 방식 선택", "desc": "드롭다운에서 URL 또는 업체명 중 원하는 방식을 선택하세요." }, "field": { "title": "입력하기", "desc": "URL 방식이라면 네이버 지도 공유 URL,\n업체명 방식이라면 업체명을 입력하고 목록에서 선택하세요." }, + "manual": { "title": "직접 입력", "desc": "업체명과 주소를 직접 입력해서 분석을 시작할 수도 있어요." }, "button": { "title": "브랜드 분석 시작", "desc": "버튼을 누르면 AI가 브랜드를 분석하기 시작해요." } }, "asset": { @@ -174,8 +175,12 @@ "manualModalTitle": "업체 정보 입력", "manualLabelName": "업체명", "manualLabelAddress": "주소", + "manualLabelRegion": "지역", + "manualLabelDetail": "상세 주소", "manualPlaceholderName": "업체명을 입력하세요.", - "manualPlaceholderAddress": "주소를 입력하세요." + "manualPlaceholderAddress": "주소를 입력하세요.", + "manualPlaceholderRegion": "지역을 선택하세요.", + "manualPlaceholderDetail": "상세 주소를 입력하세요. (예: 강남구 테헤란로 123)" }, "welcome": { "title": "ADO2.AI에 오신 것을 환영합니다.", @@ -211,8 +216,12 @@ "manualModalTitle": "업체 정보 입력", "manualLabelName": "업체명", "manualLabelAddress": "주소", + "manualLabelRegion": "지역", + "manualLabelDetail": "상세 주소", "manualPlaceholderName": "업체명을 입력하세요.", - "manualPlaceholderAddress": "주소를 입력하세요." + "manualPlaceholderAddress": "주소를 입력하세요.", + "manualPlaceholderRegion": "지역을 선택하세요.", + "manualPlaceholderDetail": "상세 주소를 입력하세요. (예: 강남구 테헤란로 123)" }, "assetManagement": { "title": "브랜드 에셋", diff --git a/src/pages/Dashboard/UrlInputContent.tsx b/src/pages/Dashboard/UrlInputContent.tsx index d9b4855..50ca81e 100644 --- a/src/pages/Dashboard/UrlInputContent.tsx +++ b/src/pages/Dashboard/UrlInputContent.tsx @@ -325,6 +325,10 @@ const UrlInputContent: React.FC = ({ onAnalyze, onAutocomp + +
diff --git a/src/pages/Landing/HeroSection.tsx b/src/pages/Landing/HeroSection.tsx index d2ff016..0dcea04 100755 --- a/src/pages/Landing/HeroSection.tsx +++ b/src/pages/Landing/HeroSection.tsx @@ -521,6 +521,11 @@ const HeroSection: React.FC = ({ onAnalyze, onAutocomplete, on + + {/* 직접 입력 버튼 */} +
@@ -554,7 +559,7 @@ const HeroSection: React.FC = ({ onAnalyze, onAutocomplete, on )} - {tutorial.isActive && ( + {tutorial.isActive && !isManualModalOpen && ( = ({ onAnalyze, onAutocomplete, on {isManualModalOpen && ( setIsManualModalOpen(false)} - onSubmit={(businessName, address) => { setIsManualModalOpen(false); onManualInput?.(businessName, address); }} + onSubmit={(businessName, address) => { + if (tutorial.isActive) tutorial.nextHint(); + setIsManualModalOpen(false); + onManualInput?.(businessName, address); + }} /> )}