diff --git a/src/App.tsx b/src/App.tsx index 5cf1aa7..17d2168 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,28 +1,16 @@ -import React, { useRef, useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import Header from './components/Header'; -import HeroSection from './pages/Landing/HeroSection'; -import WelcomeSection from './pages/Landing/WelcomeSection'; -import DisplaySection from './pages/Landing/DisplaySection'; -import LoadingSection from './pages/Analysis/LoadingSection'; -import AnalysisResultSection from './pages/Analysis/AnalysisResultSection'; -import LoginSection from './pages/Login/LoginSection'; import GenerationFlow from './pages/Dashboard/GenerationFlow'; import SocialConnectSuccess from './pages/Social/SocialConnectSuccess'; import SocialConnectError from './pages/Social/SocialConnectError'; import YouTubeOAuthCallback from './pages/Social/YouTubeOAuthCallback'; -import ADO2ContentsPage from './pages/Dashboard/ADO2ContentsPage'; import VideoDetailPage from './components/VideoDetailPage'; -import { crawlUrl, autocomplete, marketingAnalysis, kakaoCallback, isLoggedIn, saveTokens, getVideosList, AutocompleteRequest, storeUtmFromUrl, trackViewContent, trackCompleteRegistration } from './utils/api'; -import { saveSearchHistory } from './components/SearchHistory/useSearchHistory'; -import { CrawlingResponse } from './types/api'; -import { K, clearSessionStorage } from './utils/storageKeys'; +import SsulDetailPage from './pages/Ssulbox/SsulDetailPage'; +import { kakaoCallback, saveTokens, storeUtmFromUrl, trackViewContent, trackCompleteRegistration } from './utils/api'; +import { clearSessionStorage } from './utils/storageKeys'; +import { NAV } from './components/navItems'; -type ViewMode = 'landing' | 'loading' | 'analysis' | 'login' | 'generation_flow'; - -const VIEW_MODE_KEY = K.VIEW_MODE; -const ANALYSIS_DATA_KEY = K.ANALYSIS_DATA; const SESSION_KEY = 'castad_session_active'; // 새 탭/새 창에서 접근 시 localStorage 초기화 (sessionStorage로 현재 세션 확인) @@ -40,79 +28,9 @@ const initializeOnNewSession = () => { initializeOnNewSession(); const App: React.FC = () => { - const { t, i18n } = useTranslation(); - const containerRef = useRef(null); - - // localStorage에서 저장된 상태 복원 (새 세션이면 이미 초기화됨) - const savedViewMode = localStorage.getItem(VIEW_MODE_KEY) as ViewMode | null; - const savedAnalysisData = localStorage.getItem(ANALYSIS_DATA_KEY); - - // 저장된 분석 데이터 파싱 및 유효성 검사 - const parseSavedAnalysisData = (): CrawlingResponse | null => { - if (!savedAnalysisData) return null; - try { - const data = JSON.parse(savedAnalysisData) as CrawlingResponse; - // 기본값 보장 - if (data.marketing_analysis) { - data.marketing_analysis.brand_identity = data.marketing_analysis.brand_identity || { - location_feature_analysis: '', - concept_scalability: '' - }; - data.marketing_analysis.market_positioning = data.marketing_analysis.market_positioning || { - category_definition: '', - core_value: '' - }; - data.marketing_analysis.target_persona = data.marketing_analysis.target_persona || []; - data.marketing_analysis.selling_points = data.marketing_analysis.selling_points || []; - data.marketing_analysis.target_keywords = data.marketing_analysis.target_keywords || []; - } - 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 || []; - return data; - } catch { - localStorage.removeItem(ANALYSIS_DATA_KEY); - return null; - } - }; - - // 초기 viewMode 결정: 로그인 상태면 바로 generation_flow로 - const getInitialViewMode = (): ViewMode => { - if (savedViewMode === 'generation_flow') return 'generation_flow'; - if (isLoggedIn()) return 'generation_flow'; - return 'landing'; - }; - - const [viewMode, setViewMode] = useState(getInitialViewMode()); - const [initialTab, setInitialTab] = useState('새 프로젝트 만들기'); - const [analysisData, setAnalysisData] = useState( - parseSavedAnalysisData() - ); - const [error, setError] = useState(null); - const [isAnalysisComplete, setIsAnalysisComplete] = useState(false); - const [afterLoadTarget, setAfterLoadTarget] = useState('analysis'); - const [scrollProgress, setScrollProgress] = useState(0); + const { t } = useTranslation(); + const [initialTab, setInitialTab] = useState(NAV.HOME); const [isProcessingCallback, setIsProcessingCallback] = useState(false); - const tutorialVideoCheckedRef = useRef(false); - - // generation_flow 진입 시 영상 보유 여부 확인 → 있으면 튜토리얼 자동 off - useEffect(() => { - if (viewMode !== 'generation_flow') return; - if (tutorialVideoCheckedRef.current) return; - tutorialVideoCheckedRef.current = true; - const ENABLED_KEY = 'ado2_tutorial_enabled'; - if (localStorage.getItem(ENABLED_KEY) !== null) return; - getVideosList(1, 1).then(response => { - const hasVideos = response.items.some(v => v.result_movie_url?.trim()); - if (hasVideos) { - localStorage.setItem(ENABLED_KEY, 'false'); - window.dispatchEvent(new Event('ado2-tutorial-auto-disable')); - } - }).catch(() => {}); - }, [viewMode]); // 카카오 로그인 콜백 처리 (URL에서 토큰 또는 code 파라미터 확인) useEffect(() => { @@ -139,8 +57,8 @@ const App: React.FC = () => { setIsProcessingCallback(true); handleKakaoCallback(code); } - // 콜백이 아닌 일반 진입이고 랜딩이 첫 화면이면 ViewContent 발화 - else if (viewMode === 'landing') { + // 콜백이 아닌 일반 진입 — 광고 유입 추적 + else { trackViewContent(); } }, []); @@ -164,21 +82,15 @@ const App: React.FC = () => { await trackCompleteRegistration(); } - const savedData = localStorage.getItem(ANALYSIS_DATA_KEY); - if (savedData) { - // 분석 데이터가 있으면 에셋 관리(step 1)부터 시작 - // 이전에 저장된 wizard step이 URL 입력(-2) 등으로 남아있을 수 있으므로 초기화 - localStorage.removeItem('castad_wizard_step'); - localStorage.removeItem('castad_active_item'); - } + // 로그인 전 특정 화면(예: /video/{id})에서 유도된 로그인이면 그 경로로 복귀 const redirectPath = sessionStorage.getItem('castad_login_redirect'); sessionStorage.removeItem('castad_login_redirect'); if (redirectPath && redirectPath !== '/') { window.location.href = redirectPath; return; } - setInitialTab('새 프로젝트 만들기'); - setViewMode('generation_flow'); + // 그 외(사이드바 게이트 등)는 GenerationFlow 가 castad_login_next_item 으로 자체 복귀 처리 + setInitialTab(NAV.HOME); } catch (err) { console.error('Token callback failed:', err); alert(t('app.loginFailed')); @@ -212,231 +124,6 @@ const App: React.FC = () => { } }; - // viewMode 변경 시 localStorage에 저장 - useEffect(() => { - localStorage.setItem(VIEW_MODE_KEY, viewMode); - }, [viewMode]); - - // 스크롤 이벤트 핸들러 - 첫 번째 섹션에서 두 번째 섹션으로 넘어갈 때 0~1 값 계산 - useEffect(() => { - const container = containerRef.current; - if (!container || viewMode !== 'landing') return; - - const handleScroll = () => { - const scrollTop = container.scrollTop; - const sectionHeight = container.clientHeight; - // 첫 번째 섹션 스크롤 진행률 (0 ~ 1) - const progress = Math.min(1, Math.max(0, scrollTop / sectionHeight)); - setScrollProgress(progress); - }; - - container.addEventListener('scroll', handleScroll); - return () => container.removeEventListener('scroll', handleScroll); - }, [viewMode]); - - const scrollToSection = (index: number) => { - if (containerRef.current) { - const h = containerRef.current.clientHeight; - containerRef.current.scrollTo({ - top: h * index, - behavior: 'smooth' - }); - } - }; - - // 크롤링 응답 유효성 검사 - const validateCrawlingResponse = (data: CrawlingResponse): boolean => { - // 필수 필드 존재 여부 확인 - if (!data) return false; - if (!data.processed_info) return false; - if (!data.marketing_analysis) return false; - - // marketing_analysis 내부 필드 기본값 보장 - if (!data.marketing_analysis.brand_identity) { - data.marketing_analysis.brand_identity = { - location_feature_analysis: '', - concept_scalability: '' - }; - } - if (!data.marketing_analysis.market_positioning) { - data.marketing_analysis.market_positioning = { - category_definition: '', - core_value: '' - }; - } - if (!data.marketing_analysis.target_persona) { - data.marketing_analysis.target_persona = []; - } - if (!data.marketing_analysis.selling_points) { - data.marketing_analysis.selling_points = []; - } - if (!data.marketing_analysis.target_keywords) { - data.marketing_analysis.target_keywords = []; - } - - // processed_info 내부 필드 기본값 보장 - if (!data.processed_info.customer_name) { - data.processed_info.customer_name = '알 수 없음'; - } - if (!data.processed_info.region) { - data.processed_info.region = ''; - } - if (!data.processed_info.detail_region_info) { - data.processed_info.detail_region_info = ''; - } - - // image_list 기본값 보장 - if (!data.image_list) { - data.image_list = []; - } - - return true; - }; - - const handleStartAnalysis = async (url: string) => { - if (!url.trim()) return; - - setAfterLoadTarget('analysis'); - setViewMode('loading'); - setIsAnalysisComplete(false); - setError(null); - - try { - const data = await crawlUrl(url); - - // 응답 유효성 검사 - if (!validateCrawlingResponse(data)) { - throw new Error(t('app.invalidUrl')); - } - - setAnalysisData(data); - localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(data)); - saveSearchHistory({ type: 'url', value: url }); - setIsAnalysisComplete(true); - } catch (err) { - console.error('Crawling failed:', err); - setError(t('app.analysisError')); - setViewMode('landing'); - } - }; - - // 업체명 자동완성으로 분석 시작 - const handleAutocomplete = async (request: AutocompleteRequest) => { - setAfterLoadTarget('analysis'); - setViewMode('loading'); - setIsAnalysisComplete(false); - setError(null); - - try { - const data = await autocomplete(request); - - // 응답 유효성 검사 - if (!validateCrawlingResponse(data)) { - throw new Error(t('app.autocompleteError')); - } - - setAnalysisData(data); - localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(data)); - saveSearchHistory({ type: 'name', value: request.title.replace(/<[^>]*>/g, ''), address: request.address, roadAddress: request.roadAddress }); - setIsAnalysisComplete(true); - } catch (err) { - console.error('Autocomplete failed:', err); - setError(t('app.autocompleteError')); - setViewMode('landing'); - } - }; - - // 업체명·주소 수동 입력으로 마케팅 분석 API 호출 - const handleManualInput = async (businessName: string, address: string, category: string) => { - setAfterLoadTarget('generation_flow'); - setViewMode('loading'); - setIsAnalysisComplete(false); - setError(null); - - try { - const data = await marketingAnalysis(businessName, address, category); - - 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 }; - setAnalysisData(tagged); - localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(tagged)); - setViewMode('analysis'); - }; - - // 언어 변경 시 테스트 데이터 다시 로드 - useEffect(() => { - const saved = localStorage.getItem(ANALYSIS_DATA_KEY); - if (!saved) return; - try { - const parsed = JSON.parse(saved); - if (!parsed._isTestData) return; - const jsonFile = i18n.language === 'en' ? '/example_analysis_en.json' : '/example_analysis.json'; - fetch(jsonFile) - .then(res => res.json()) - .then((data: CrawlingResponse) => { - const tagged = { ...data, _isTestData: true }; - setAnalysisData(tagged); - localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(tagged)); - }) - .catch(err => console.error('Failed to reload test data:', err)); - } catch { /* ignore */ } - }, [i18n.language]); - - const handleToLogin = async () => { - // 이미 로그인된 상태면 바로 generation_flow로 이동 - if (isLoggedIn()) { - // 분석 데이터가 있으면 이전 wizard step 초기화 (에셋 관리부터 시작하도록) - const savedData = localStorage.getItem(ANALYSIS_DATA_KEY); - if (savedData) { - localStorage.removeItem('castad_wizard_step'); - localStorage.removeItem('castad_active_item'); - } - setInitialTab('새 프로젝트 만들기'); - setViewMode('generation_flow'); - return; - } - - // 로그인 안 된 상태면 카카오 로그인 페이지로 리다이렉션 - try { - const { getKakaoLoginUrl } = await import('./utils/api'); - const response = await getKakaoLoginUrl(); - window.location.href = response.auth_url; - } catch (err) { - console.error('Failed to get Kakao login URL:', err); - alert(t('app.loginUrlFailed')); - } - }; - - const handleLoginSuccess = () => { - setInitialTab('새 프로젝트 만들기'); - setViewMode('generation_flow'); - }; - - const handleGoBack = () => { - // localStorage 정리 - localStorage.removeItem(VIEW_MODE_KEY); - localStorage.removeItem(ANALYSIS_DATA_KEY); - localStorage.removeItem('castad_wizard_step'); - localStorage.removeItem('castad_active_item'); - setViewMode('landing'); - }; - // Social OAuth 콜백 페이지 처리 const pathname = window.location.pathname; @@ -459,6 +146,13 @@ const App: React.FC = () => { return ; } + // 썰박스 공유 페이지 (/ssul/{id}) — 위와 동일한 패턴, 비로그인 열람 가능. + // /video/{id} 와 경로를 나눈 이유: video.id 와 ssul_content.id 가 독립 시퀀스라 + // 값이 겹치므로, 한 경로로 합치면 다른 종류의 콘텐츠가 열린다. + const ssulDetailMatch = pathname.match(/^\/ssul\/(\d+)$/); + if (ssulDetailMatch) { + return ; + } // 카카오 콜백 처리 중 로딩 화면 표시 if (isProcessingCallback) { @@ -471,71 +165,8 @@ const App: React.FC = () => { ); } - if (viewMode === 'loading') { - return ( - setViewMode(afterLoadTarget)} - /> - ); - } - - if (viewMode === 'analysis' && analysisData) { - return ( - - ); - } - - if (viewMode === 'login') { - return setViewMode('analysis')} onLogin={handleLoginSuccess} />; - } - - if (viewMode === 'generation_flow') { - return ( - - ); - } - - // 로그인된 상태에서 "시작하기" 버튼 클릭 - const handleHeaderStart = () => { - setInitialTab('새 프로젝트 만들기'); - setViewMode('generation_flow'); - }; - - return ( -
-
-
- scrollToSection(1)} - error={error} - scrollProgress={scrollProgress} - /> -
-
- scrollToSection(0)} - onNext={() => scrollToSection(0)} - /> -
-
- scrollToSection(0)} /> -
-
- ); + // 그 외 전 경로는 ADO2 콘텐츠 갤러리를 첫 화면으로 하는 GenerationFlow 로 수렴 + return ; }; export default App; diff --git a/src/components/Header.tsx b/src/components/Header.tsx deleted file mode 100755 index f5ee1ef..0000000 --- a/src/components/Header.tsx +++ /dev/null @@ -1,64 +0,0 @@ - -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { getKakaoLoginUrl, isLoggedIn } from '../utils/api'; -import LanguageSwitch from './LanguageSwitch'; - -interface HeaderProps { - onStartClick?: () => void; -} - -const Header: React.FC = ({ onStartClick }) => { - const { t } = useTranslation(); - const [isLoading, setIsLoading] = useState(false); - const loggedIn = isLoggedIn(); - - const handleLogin = async () => { - if (isLoading) return; - - setIsLoading(true); - try { - const response = await getKakaoLoginUrl(); - window.location.href = response.auth_url; - } catch (err) { - console.error('Failed to get Kakao login URL:', err); - alert(t('header.loginFailedAlert')); - setIsLoading(false); - } - }; - - const handleStart = () => { - if (onStartClick) { - onStartClick(); - } - }; - - return ( -
-
- ADO2 -
-
- - {loggedIn ? ( - - ) : ( - - )} -
-
- ); -}; - -export default Header; diff --git a/src/components/Tutorial/TutorialOverlay.tsx b/src/components/Tutorial/TutorialOverlay.tsx deleted file mode 100644 index 1c7d5a3..0000000 --- a/src/components/Tutorial/TutorialOverlay.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { TutorialHint } from './tutorialSteps'; - -interface Rect { - top: number; - left: number; - width: number; - height: number; - bottom: number; -} - -interface TooltipPos { - top: number; - left: number; -} - -interface TutorialOverlayProps { - hints: TutorialHint[]; - currentIndex: number; - onNext: () => void; - onPrev: () => void; - onSkip: () => void; - groupProgress?: { groupTotal: number; groupOffset: number; isLastKeyInGroup: boolean } | null; -} - -const PADDING = 8; - -function getTargetRect(selector: string): Rect | null { - const els = Array.from(document.querySelectorAll(selector)); - const el = els.find(e => { - const r = (e as HTMLElement).getBoundingClientRect(); - return r.width > 0 && r.height > 0; - }) ?? els[0]; - if (!el) return null; - const r = el.getBoundingClientRect(); - return { top: r.top, left: r.left, width: r.width, height: r.height, bottom: r.bottom }; -} - -function getSpotlightRect( - rect: Rect, - padding: number, - override?: { top?: number; right?: number; bottom?: number; left?: number } -): Rect { - const pTop = override?.top ?? padding; - const pRight = override?.right ?? padding; - const pBottom = override?.bottom ?? padding; - const pLeft = override?.left ?? padding; - const left = Math.max(0, Math.floor(rect.left - pLeft)); - const top = Math.max(0, Math.floor(rect.top - pTop)); - const right = Math.min(window.innerWidth, Math.ceil(rect.left + rect.width + pRight)); - const bottom = Math.min(window.innerHeight, Math.ceil(rect.top + rect.height + pBottom)); - - return { - top, - bottom, - left, - width: Math.max(0, right - left), - height: Math.max(0, bottom - top), - }; -} - -function calcTooltipPos(rect: Rect, position: TutorialHint['position'], tooltipW = 300, tooltipH = 160): TooltipPos { - - switch (position) { - case 'bottom': - return { - top: Math.min(rect.top + rect.height + PADDING, window.innerHeight - tooltipH - 8), - left: Math.min(Math.max(rect.left + rect.width / 2 - tooltipW / 2, 8), window.innerWidth - tooltipW - 8), - }; - case 'top': - return { - top: Math.max(rect.top - tooltipH - PADDING, 8), - left: Math.min(Math.max(rect.left + rect.width / 2 - tooltipW / 2, 8), window.innerWidth - tooltipW - 8), - }; - case 'right': - return { - top: Math.min(Math.max(rect.top + rect.height / 2 - tooltipH / 2, 8), window.innerHeight - tooltipH - 8), - left: Math.min(rect.left + rect.width + PADDING, window.innerWidth - tooltipW - 8), - }; - case 'left': - return { - top: Math.min(Math.max(rect.top + rect.height / 2 - tooltipH / 2, 8), window.innerHeight - tooltipH - 8), - left: Math.max(rect.left - tooltipW - PADDING, 8), - }; - } -} - -const TutorialOverlay: React.FC = ({ - hints, - currentIndex, - onNext, - onPrev, - onSkip, - groupProgress, -}) => { - const { t } = useTranslation(); - const [targetRect, setTargetRect] = useState(null); - const tooltipRef = React.useRef(null); - const [tooltipSize, setTooltipSize] = useState({ w: 300, h: 160 }); - - const hint = hints[currentIndex]; - const isLast = currentIndex === hints.length - 1; - // 그룹이 있으면 그룹의 마지막 키 + 마지막 힌트일 때만 완료, 그룹 없으면 기존대로 - const isFinish = isLast && (groupProgress ? groupProgress.isLastKeyInGroup : true); - - const updateRect = useCallback(() => { - if (!hint) return; - setTargetRect(getTargetRect(hint.targetSelector)); - }, [hint]); - - useEffect(() => { - updateRect(); - window.addEventListener('resize', updateRect); - window.addEventListener('scroll', updateRect, true); - return () => { - window.removeEventListener('resize', updateRect); - window.removeEventListener('scroll', updateRect, true); - }; - }, [updateRect]); - - useEffect(() => { - if (!hint) return; - let retryTimer: number | undefined; - let rectTimer: number | undefined; - let cleanupTarget: (() => void) | undefined; - - const bindToTarget = (el: HTMLElement) => { - el.scrollIntoView({ behavior: 'smooth', block: 'center' }); - rectTimer = window.setTimeout(updateRect, 200); - - const shouldClickAdvance = hint.clickToAdvance !== false; - if (shouldClickAdvance) { - el.style.cursor = 'pointer'; - el.addEventListener('click', onNext); - cleanupTarget = () => { - el.style.cursor = ''; - el.removeEventListener('click', onNext); - }; - } else { - cleanupTarget = () => {}; - } - }; - - const tryBind = (): boolean => { - 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; - }; - - if (!tryBind()) { - retryTimer = window.setInterval(() => { - if (tryBind() && retryTimer) { - window.clearInterval(retryTimer); - retryTimer = undefined; - } - }, 80); - } - - return () => { - if (retryTimer) window.clearInterval(retryTimer); - if (rectTimer) window.clearTimeout(rectTimer); - cleanupTarget?.(); - }; - }, [hint, updateRect, onNext]); - - // 툴팁 DOM 크기 측정 — 힌트가 바뀔 때만 재측정 - useEffect(() => { - const el = tooltipRef.current; - if (!el) return; - const { offsetWidth, offsetHeight } = el; - if (offsetWidth && offsetHeight) { - setTooltipSize(prev => - prev.w === offsetWidth && prev.h === offsetHeight - ? prev - : { w: offsetWidth, h: offsetHeight } - ); - } - }, [currentIndex, hints]); - - if (!hint) return null; - - const isTargetVisible = targetRect - ? targetRect.top + 60 < window.innerHeight && targetRect.bottom > 60 - : true; - - if (!isTargetVisible) return null; - - const spotlightPadding = hint.spotlightPadding ?? PADDING; - const spotlightRect = (targetRect && !hint.noSpotlight) ? getSpotlightRect(targetRect, spotlightPadding, hint.spotlightPaddingOverride) : null; - - // 툴팁은 spotlightRect 기준으로 배치해야 스포트라이트와 겹치지 않음 - const tooltipPos: TooltipPos = (spotlightRect ?? targetRect) - ? calcTooltipPos((spotlightRect ?? targetRect)!, hint.position, tooltipSize.w, tooltipSize.h) - : { - top: window.innerHeight / 2 - tooltipSize.h / 2, - left: window.innerWidth / 2 - tooltipSize.w / 2, - }; - - return ( -
- {spotlightRect ? ( - <> -
-
-
-
-
- - ) : null} - -
e.stopPropagation()} - > -

{t(hint.titleKey)}

-

{t(hint.descriptionKey)}

- {hint.noteKey && ( -

{t(hint.noteKey)}

- )} - -
- - {groupProgress ? groupProgress.groupOffset + 1 : currentIndex + 1} / {groupProgress ? groupProgress.groupTotal : hints.length} - -
- - {currentIndex > 0 && ( - - )} - {(hint.clickToAdvance !== true || isFinish) && ( - - )} -
-
-
-
- ); -}; - -export default TutorialOverlay; - -interface TutorialRestartPopupProps { - onConfirm: () => void; - onCancel: () => void; -} - -export const TutorialRestartPopup: React.FC = ({ onConfirm, onCancel }) => { - const { t } = useTranslation(); - return ( -
-
e.stopPropagation()}> -

{t('tutorial.restart.title')}

-

{t('tutorial.restart.desc')}

-
- - -
-
-
- ); -}; diff --git a/src/components/Tutorial/tutorialSteps.ts b/src/components/Tutorial/tutorialSteps.ts deleted file mode 100644 index 1d25bad..0000000 --- a/src/components/Tutorial/tutorialSteps.ts +++ /dev/null @@ -1,377 +0,0 @@ -export interface TutorialHint { - targetSelector: string; - titleKey: string; - descriptionKey: string; - position: 'top' | 'bottom' | 'left' | 'right'; - noteKey?: string; - variant?: 'bubble'; - clickToAdvance?: boolean; - advanceSelector?: string; - noSpotlight?: boolean; - spotlightPadding?: number; - spotlightPaddingOverride?: { top?: number; right?: number; bottom?: number; left?: number }; -} - -export interface TutorialStepDef { - key: string; - hints: TutorialHint[]; -} - -export const TUTORIAL_KEYS = { - LANDING: 'landing', - ANALYSIS: 'analysis', - ASSET: 'asset', - SOUND: 'sound', - SOUND_LYRICS: 'soundLyrics', - SOUND_AUDIO: 'soundAudio', - GENERATING:'generating', - COMPLETION: 'completion', - MY_INFO: 'myInfo', - ADO2_CONTENTS: 'ado2Contents', - UPLOAD_MODAL: 'uploadModal', - UPLOAD_FORM: 'uploadForm', - DASHBOARD: 'dashboard', - CONTENT_CALENDAR: 'contentCalendar', - FEEDBACK: 'feedback', -} as const; - -// 같은 페이지에 속하는 튜토리얼 키 그룹 — 진행 카운터를 합산해서 표시 -export const TUTORIAL_PAGE_GROUPS: string[][] = [ - [TUTORIAL_KEYS.SOUND, TUTORIAL_KEYS.SOUND_LYRICS, TUTORIAL_KEYS.SOUND_AUDIO], - [TUTORIAL_KEYS.GENERATING, TUTORIAL_KEYS.COMPLETION], - [TUTORIAL_KEYS.UPLOAD_MODAL, TUTORIAL_KEYS.UPLOAD_FORM], -]; - -export const tutorialSteps: TutorialStepDef[] = [ - { - key: TUTORIAL_KEYS.LANDING, - hints: [ - { - targetSelector: '.hero-input-wrapper', - titleKey: 'tutorial.landing.field.title', - descriptionKey: 'tutorial.landing.field.desc', - position: 'top', - clickToAdvance: false, - noSpotlight: true, - variant: 'bubble', - }, - { - targetSelector: '.hero-manual-card-title', - titleKey: 'tutorial.landing.manual.title', - descriptionKey: 'tutorial.landing.manual.desc', - position: 'top', - clickToAdvance: false, - noSpotlight: true, - variant: 'bubble', - }, - { - targetSelector: '.hero-button', - titleKey: 'tutorial.landing.button.title', - descriptionKey: 'tutorial.landing.button.desc', - position: 'bottom', - clickToAdvance: true, - noSpotlight: true, - variant: 'bubble', - }, - ], - }, - { - key: TUTORIAL_KEYS.ASSET, - hints: [ - { - targetSelector: '.asset-column.asset-column-left', - titleKey: 'tutorial.asset.image.title', - descriptionKey: 'tutorial.asset.image.desc', - position: 'left', - clickToAdvance: false, - }, - { - targetSelector: '.asset-upload-zone, .asset-mobile-upload-btn', - titleKey: 'tutorial.asset.upload.title', - descriptionKey: 'tutorial.asset.upload.desc', - position: 'bottom', - clickToAdvance: false, - }, - { - targetSelector: '.asset-ratio-section', - titleKey: 'tutorial.asset.ratio.title', - descriptionKey: 'tutorial.asset.ratio.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.asset-next-button', - titleKey: 'tutorial.asset.next.title', - descriptionKey: 'tutorial.asset.next.desc', - position: 'top', - clickToAdvance: true, - }, - ], - }, - { - key: TUTORIAL_KEYS.SOUND, - hints: [ - { - targetSelector: '.genre-grid', - titleKey: 'tutorial.sound.genre.title', - descriptionKey: 'tutorial.sound.genre.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.language-grid', - titleKey: 'tutorial.sound.language.title', - descriptionKey: 'tutorial.sound.language.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.btn-generate-sound', - titleKey: 'tutorial.sound.generate.title', - descriptionKey: 'tutorial.sound.generate.desc', - position: 'top', - clickToAdvance: true, - }, - ], - }, - { - key: TUTORIAL_KEYS.SOUND_LYRICS, - hints: [ - { - targetSelector: '.lyrics-display', - titleKey: 'tutorial.sound.lyrics.title', - descriptionKey: 'tutorial.sound.lyrics.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.status-message-new', - titleKey: 'tutorial.sound.lyricsWait.title', - descriptionKey: 'tutorial.sound.lyricsWait.desc', - position: 'top', - clickToAdvance: false, - }, - ], - }, - { - key: TUTORIAL_KEYS.SOUND_AUDIO, - hints: [ - { - targetSelector: '.audio-player', - titleKey: 'tutorial.sound.audioPlayer.title', - descriptionKey: 'tutorial.sound.audioPlayer.desc', - position: 'bottom', - clickToAdvance: false, - }, - { - targetSelector: '.btn-video-generate', - titleKey: 'tutorial.sound.video.title', - descriptionKey: 'tutorial.sound.video.desc', - position: 'top', - clickToAdvance: true, - }, - ], - }, - { - key: TUTORIAL_KEYS.MY_INFO, - hints: [ - { - targetSelector: '.youtube-connect-section', - titleKey: 'tutorial.myInfo.myInfo.title', - descriptionKey: 'tutorial.myInfo.myInfo.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.myinfo-social-btn', - titleKey: 'tutorial.myInfo.connect.title', - descriptionKey: 'tutorial.myInfo.connect.desc', - noteKey: 'tutorial.myInfo.connect.note', - position: 'top', - clickToAdvance: true, - }, - { - targetSelector: '.myinfo-connected-accounts', - titleKey: 'tutorial.myInfo.connected.title', - descriptionKey: 'tutorial.myInfo.connected.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '#sidebar-ado2-contents', - titleKey: 'tutorial.myInfo.ado2.title', - descriptionKey: 'tutorial.myInfo.ado2.desc', - position: 'right', - clickToAdvance: true, - }, - ], - }, - { - key: TUTORIAL_KEYS.ADO2_CONTENTS, - hints: [ - { - targetSelector: '.ado2-content-card', - titleKey: 'tutorial.ado2.list.title', - descriptionKey: 'tutorial.ado2.list.desc', - position: 'right', - clickToAdvance: false, - }, - { - targetSelector: '.content-upload-btn', - titleKey: 'tutorial.ado2.download.title', - descriptionKey: 'tutorial.ado2.download.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.content-delete-btn', - titleKey: 'tutorial.ado2.delete.title', - descriptionKey: 'tutorial.ado2.delete.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.content-download-btn', - titleKey: 'tutorial.ado2.upload.title', - descriptionKey: 'tutorial.ado2.upload.desc', - position: 'top', - clickToAdvance: true, - }, - ], - }, - { - key: TUTORIAL_KEYS.GENERATING, - hints:[ - { - targetSelector: '.comp2-info-section', - titleKey: 'tutorial.completion.contentInfo.title', - descriptionKey: 'tutorial.completion.contentInfo.desc', - position: 'left', - }, - { - targetSelector: '.comp2-video-section', - titleKey: 'tutorial.completion.generating.title', - descriptionKey: 'tutorial.completion.generating.desc', - position: 'top', - } - ] - }, - { - key: TUTORIAL_KEYS.COMPLETION, - hints: [ - { - targetSelector: '.comp2-video-section', - titleKey: 'tutorial.completion.completion.title', - descriptionKey: 'tutorial.completion.completion.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '#sidebar-my-info', - titleKey: 'tutorial.completion.myInfo.title', - descriptionKey: 'tutorial.completion.myInfo.desc', - position: 'right', - clickToAdvance: true, - }, - ], - }, - { - key: TUTORIAL_KEYS.UPLOAD_MODAL, - hints: [ - { - targetSelector: '.social-posting-content', - titleKey: 'tutorial.upload.seo.title', - descriptionKey: 'tutorial.upload.seo.desc', - position: 'right', - clickToAdvance: false, - }, - ], - }, - { - key: TUTORIAL_KEYS.UPLOAD_FORM, - hints: [ - { - targetSelector: '.social-posting-form', - titleKey: 'tutorial.upload.required.title', - descriptionKey: 'tutorial.upload.required.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.social-posting-radio-group', - titleKey: 'tutorial.upload.schedule.title', - descriptionKey: 'tutorial.upload.schedule.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.social-posting-btn:not(.cancel)', - titleKey: 'tutorial.upload.submit.title', - descriptionKey: 'tutorial.upload.submit.desc', - position: 'top', - clickToAdvance: true, - }, - ], - }, - { - key: TUTORIAL_KEYS.DASHBOARD, - hints: [ - { - targetSelector: '.stats-grid-8', - titleKey: 'tutorial.dashboard.metrics.title', - descriptionKey: 'tutorial.dashboard.metrics.desc', - position: 'bottom', - }, - { - targetSelector: '.yoy-chart-card', - titleKey: 'tutorial.dashboard.chart.title', - descriptionKey: 'tutorial.dashboard.chart.desc', - position: 'top', - }, - { - targetSelector: '.tutorial-center-anchor', - titleKey: 'tutorial.dashboard.more.title', - descriptionKey: 'tutorial.dashboard.more.desc', - position: 'bottom', - clickToAdvance: false, - }, - ], - }, - { - key: TUTORIAL_KEYS.CONTENT_CALENDAR, - hints: [ - { - targetSelector: '.calendar-grid-area', - titleKey: 'tutorial.contentCalendar.grid.title', - descriptionKey: 'tutorial.contentCalendar.grid.desc', - position: 'top', - clickToAdvance: false, - }, - { - targetSelector: '.calendar-side-panel', - titleKey: 'tutorial.contentCalendar.panel.title', - descriptionKey: 'tutorial.contentCalendar.panel.desc', - position: 'left', - clickToAdvance: false, - }, - ], - }, - { - key: TUTORIAL_KEYS.FEEDBACK, - hints: [ - { - targetSelector: '.tutorial-center-anchor', - titleKey: 'tutorial.feedback.complete.title', - descriptionKey: 'tutorial.feedback.complete.desc', - position: 'bottom', - clickToAdvance: false, - }, - { - targetSelector: '.sidebar-inquiry-btn', - titleKey: 'tutorial.feedback.title', - descriptionKey: 'tutorial.feedback.desc', - position: 'right', - }, - ], - }, -]; diff --git a/src/components/Tutorial/useTutorial.ts b/src/components/Tutorial/useTutorial.ts deleted file mode 100644 index 0b2fdb5..0000000 --- a/src/components/Tutorial/useTutorial.ts +++ /dev/null @@ -1,220 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { tutorialSteps, TutorialHint, TUTORIAL_PAGE_GROUPS } from './tutorialSteps'; - -// 현재 키가 속한 그룹의 전체 힌트 수와 현재까지의 offset 반환 -function getGroupProgress(key: string, currentIndex: number): { groupTotal: number; groupOffset: number; isLastKeyInGroup: boolean } | null { - const group = TUTORIAL_PAGE_GROUPS.find(g => g.includes(key)); - if (!group) return null; - let offset = 0; - let total = 0; - for (const k of group) { - const step = tutorialSteps.find(s => s.key === k); - const count = step?.hints.length ?? 0; - if (k === key) offset = total; - total += count; - } - const isLastKeyInGroup = group[group.length - 1] === key; - return { groupTotal: total, groupOffset: offset + currentIndex, isLastKeyInGroup }; -} - -const SEEN_KEY = 'ado2_tutorial_seen'; -const PROGRESS_KEY = 'ado2_tutorial_progress'; -const ENABLED_KEY = 'ado2_tutorial_enabled'; - -// 전역 단일 활성 튜토리얼 관리 — 새 튜토리얼 시작 시 이전 것을 skip 처리 -let globalSkip: (() => void) | null = null; - -function getSeenKeys(): string[] { - try { - return JSON.parse(localStorage.getItem(SEEN_KEY) || '[]'); - } catch { - return []; - } -} - -function markSeen(key: string) { - const seen = getSeenKeys(); - if (!seen.includes(key)) { - localStorage.setItem(SEEN_KEY, JSON.stringify([...seen, key])); - } - clearProgress(key); -} - -function saveProgress(key: string, index: number) { - try { - const progress = JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}'); - progress[key] = index; - localStorage.setItem(PROGRESS_KEY, JSON.stringify(progress)); - } catch {} -} - -function loadProgress(key: string): number { - try { - const progress = JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}'); - return progress[key] ?? 0; - } catch { - return 0; - } -} - -function clearProgress(key: string) { - try { - const progress = JSON.parse(localStorage.getItem(PROGRESS_KEY) || '{}'); - delete progress[key]; - localStorage.setItem(PROGRESS_KEY, JSON.stringify(progress)); - } catch {} -} - -interface UseTutorialReturn { - isActive: boolean; - isEnabled: boolean; - isRestartPopupVisible: boolean; - currentHintIndex: number; - hints: TutorialHint[]; - tutorialKey: string | null; - groupProgress: { groupTotal: number; groupOffset: number; isLastKeyInGroup: boolean } | null; - startTutorial: (key: string, onComplete?: () => void, forceFromStart?: boolean) => void; - nextHint: () => void; - prevHint: () => void; - skipTutorial: () => void; - toggleTutorial: (currentKey: string | null) => void; - showRestartPopup: (key: string) => void; - confirmRestart: () => void; - cancelRestart: () => void; - hasSeen: (key: string) => boolean; -} - -export function useTutorial(): UseTutorialReturn { - const [isActive, setIsActive] = useState(false); - const [isEnabled, setIsEnabled] = useState(() => localStorage.getItem(ENABLED_KEY) !== 'false'); - const [isRestartPopupVisible, setIsRestartPopupVisible] = useState(false); - const [pendingRestartKey, setPendingRestartKey] = useState(null); - const [currentHintIndex, setCurrentHintIndex] = useState(0); - const [hints, setHints] = useState([]); - const [tutorialKey, setTutorialKey] = useState(null); - const onCompleteRef = React.useRef<(() => void) | undefined>(undefined); - - const startTutorial = useCallback((key: string, onComplete?: () => void, forceFromStart?: boolean) => { - if (localStorage.getItem(ENABLED_KEY) === 'false') return; - const step = tutorialSteps.find(s => s.key === key); - if (!step || step.hints.length === 0) return; - // 다른 인스턴스에서 활성화된 튜토리얼이 있으면 skip 처리 - globalSkip?.(); - const savedIndex = forceFromStart ? 0 : loadProgress(key); - const resumeIndex = savedIndex < step.hints.length ? savedIndex : 0; - onCompleteRef.current = onComplete; - setHints(step.hints); - setTutorialKey(key); - setCurrentHintIndex(resumeIndex); - setIsActive(true); - // 이 인스턴스의 skip을 전역에 등록 - globalSkip = () => { - if (key) saveProgress(key, resumeIndex); - setIsActive(false); - setCurrentHintIndex(0); - globalSkip = null; - }; - }, []); - - const nextHint = useCallback(() => { - setCurrentHintIndex(prev => { - if (prev < hints.length - 1) { - const next = prev + 1; - if (tutorialKey) saveProgress(tutorialKey, next); - return next; - } - // 마지막 힌트 완료 → seen 기록 + 진행 상태 삭제 - setIsActive(false); - if (tutorialKey) markSeen(tutorialKey); - globalSkip = null; // 완료된 튜토리얼은 globalSkip 해제 - onCompleteRef.current?.(); - onCompleteRef.current = undefined; - return 0; - }); - }, [hints.length, tutorialKey]); - - const prevHint = useCallback(() => { - setCurrentHintIndex(prev => Math.max(0, prev - 1)); - }, []); - - // 건너뛰기: 현재 진행 인덱스 저장 후 오버레이 닫기 → 다음 방문 시 이어서 표시 - const skipTutorial = useCallback(() => { - if (tutorialKey) saveProgress(tutorialKey, currentHintIndex); - setIsActive(false); - setCurrentHintIndex(0); - }, [tutorialKey, currentHintIndex]); - - const toggleTutorial = useCallback((currentKey: string | null) => { - if (isEnabled) { - // off: 튜토리얼 중단 + 비활성화 - setIsActive(false); - setIsEnabled(false); - localStorage.setItem(ENABLED_KEY, 'false'); - } else { - // on: seen/progress 초기화 + 현재 화면 튜토리얼 시작 - localStorage.removeItem(SEEN_KEY); - localStorage.removeItem(PROGRESS_KEY); - localStorage.setItem(ENABLED_KEY, 'true'); - setIsEnabled(true); - if (currentKey) { - startTutorial(currentKey, undefined, true); - } - } - }, [isEnabled, startTutorial]); - - // 튜토리얼 다시 보기: 팝업 표시만 - const showRestartPopup = useCallback((key: string) => { - setPendingRestartKey(key); - setIsRestartPopupVisible(true); - }, []); - - // 팝업에서 확인 → seen/progress 초기화 후 튜토리얼 시작 - const confirmRestart = useCallback(() => { - setIsRestartPopupVisible(false); - if (pendingRestartKey) { - localStorage.removeItem(SEEN_KEY); - localStorage.removeItem(PROGRESS_KEY); - startTutorial(pendingRestartKey, undefined, true); - } - setPendingRestartKey(null); - }, [pendingRestartKey, startTutorial]); - - // 팝업에서 취소 - const cancelRestart = useCallback(() => { - setIsRestartPopupVisible(false); - setPendingRestartKey(null); - }, []); - - const hasSeen = useCallback((key: string) => { - return getSeenKeys().includes(key); - }, []); - - // 영상 보유 사용자 자동 off 이벤트 수신 - useEffect(() => { - const handler = () => { - setIsActive(false); - setIsEnabled(false); - }; - window.addEventListener('ado2-tutorial-auto-disable', handler); - return () => window.removeEventListener('ado2-tutorial-auto-disable', handler); - }, []); - - return { - isActive, - isEnabled, - isRestartPopupVisible, - currentHintIndex, - hints, - tutorialKey, - groupProgress: tutorialKey ? getGroupProgress(tutorialKey, currentHintIndex) : null, - startTutorial, - nextHint, - prevHint, - skipTutorial, - toggleTutorial, - showRestartPopup, - confirmRestart, - cancelRestart, - hasSeen, - }; -} diff --git a/src/locales/en.json b/src/locales/en.json index 7a218f1..a6023af 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -21,83 +21,10 @@ "credits": "Credits left: {{count}}", "loggingOut": "Logging out...", "logout": "Log Out", - "tutorialRestart": "Restart Tutorial", - "tutorial": "Tutorial", - "tutorialOn": "Enable Tutorial", - "tutorialOff": "Disable Tutorial", + "login": "Log In", + "settings": "Settings", "inquiry": "Feedback" }, - "tutorial": { - "skip": "Skip", - "next": "Next", - "prev": "Back", - "finish": "Done", - "landing": { - "intro": { "title": "Welcome to ADO2 Tutorial", "desc": "We'll guide you through ADO2 step by step." }, - "field": { "title": "Enter Search Term", "desc": "Paste a Naver Maps share URL,\nor type a business 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": { - "image": { "title": "Image List", "desc": "Photos from Naver Place. Tap 'Show more' to see the rest, or X to remove any." }, - "upload": { "title": "Add Images", "desc": "You can freely add more images." }, - "ratio": { "title": "Select Video Ratio", "desc": "Choose the ratio for the video to be generated." }, - "next": { "title": "Next Step", "desc": "Proceed to the next step when ready." } - }, - "sound": { - "genre": { "title": "Select Genre", "desc": "Pick a music genre that fits your brand.", "note": "Background music is coming soon." }, - "language": { "title": "Select Language", "desc": "You can choose the language for the sound.\nWant to continue with Korean?" }, - "generate": { "title": "Generate Sound", "desc": "Click the button and AI will generate lyrics and music." }, - "lyrics": { "title": "Lyrics Complete", "desc": "AI wrote lyrics in your selected language.\nCheck the generated lyrics." }, - "lyricsWait": { "title": "Generating Music", "desc": "AI is composing music based on the lyrics.\nPlease wait a moment." }, - "audioPlayer": { "title": "Preview the Music", "desc": "Music generation is complete.\nPress play to listen to the generated music." }, - "video": { "title": "Generate Video", "desc": "Click the button to start generating your video." } - }, - "completion": { - "contentInfo": { "title": "Content Info", "desc": "Check the file name, genre, resolution, and lyrics of the generated content." }, - "generating": { "title": "Generating Video", "desc": "AI is creating your video.\nPlease wait a moment." }, - "completion": { "title": "Video Complete!", "desc": "Your video is ready. Want to take a look?" }, - "myInfo": { "title": "Connect Social Account", "desc": "To upload your video to YouTube, connect your social account in My Info. Click to go there." } - }, - "myInfo": { - "myInfo": { "title": "My Info", "desc": "In My Info, you can manage your social connections and view connected accounts." }, - "connect": { "title": "Connect Now", "desc": "Click the YouTube connect button to go to the connection page.", "note": "Instagram connection is coming soon." }, - "connected": { "title": "Connected Accounts", "desc": "Your linked social accounts appear here.\nCheck after connecting." }, - "ado2": { "title": "ADO2 Contents", "desc": "You can now upload the generated video.\nClick to navigate." } - }, - "ado2": { - "list": { "title": "Generated Videos", "desc": "View all AI-created videos here." }, - "download": { "title": "Download", "desc": "Download the video to your device." }, - "delete": { "title": "Delete", "desc": "Remove videos you no longer need." }, - "upload": { "title": "Upload to Social Media", "desc": "Select a video and upload it to social media." } - }, - "upload": { - "seo": { "title": "Title & Description", "desc": "AI is generating the title and description for your video. Please wait a moment." }, - "required": { "title": "Required Fields", "desc": "Fields marked with * are required.\nPlease check them before uploading." }, - "schedule": { "title": "Schedule Upload", "desc": "Post now or schedule for a specific time." }, - "submit": { "title": "Start Upload", "desc": "Click the Post button to start uploading." } - }, - "dashboard": { - "metrics": { "title": "Key Metrics", "desc": "Check views, subscribers, and other stats for content uploaded via ADO2." }, - "chart": { "title": "Growth Chart", "desc": "Track your channel's growth over time." }, - "more": { "title": "More Analytics", "desc": "Even more statistics are available at a glance on the dashboard." } - }, - "contentCalendar": { - "grid": { "title": "Content Calendar", "desc": "View your content schedule by date.\nWhy not select today?" }, - "panel": { "title": "Content List", "desc": "Check the detailed content schedule here." } - }, - "feedback": { - "complete": { "title": "Tutorial Complete 🎉", "desc": "You've completed the full flow from brand analysis to YouTube upload.\nTo replay the tutorial, click the button in the top right." }, - "title": "Customer Feedback", - "desc": "Share any issues or suggestions to help us improve." - }, - "restart": { - "title": "Restart Tutorial?", - "desc": "The tutorial will restart from the current screen.", - "confirm": "Start", - "cancel": "Cancel" - } - }, "footer": { "company": "O2O Inc.", "businessNumber": "Business Registration No. : 620-87-00810 | CEO : Ahn Sungmin", @@ -497,10 +424,10 @@ }, "myInfo": { "title": "My Info", - "tabBasic": "Basic Info", - "tabPayment": "Payment Info", - "tabBusiness": "Social Channel Management", - "basicPlaceholder": "Basic information settings are coming soon.", + "tabContents": "My Contents", + "tabCalendar": "Calendar", + "tabPayment": "Payment", + "tabBusiness": "SNS Management", "paymentPlaceholder": "Payment information settings are coming soon.", "myBusiness": "My Business", "noBusinessTitle": "No registered business yet", diff --git a/src/locales/ko.json b/src/locales/ko.json index d33f577..6e4d536 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -21,83 +21,10 @@ "credits": "보유 크레딧: {{count}}", "loggingOut": "로그아웃 중...", "logout": "로그아웃", - "tutorialRestart": "튜토리얼 다시 보기", - "tutorial": "튜토리얼", - "tutorialOn": "튜토리얼 켜기", - "tutorialOff": "튜토리얼 끄기", + "login": "로그인", + "settings": "설정", "inquiry": "고객의견" }, - "tutorial": { - "skip": "건너뛰기", - "next": "다음", - "prev": "이전", - "finish": "완료", - "landing": { - "intro": { "title": "ADO2 튜토리얼 시작", "desc": "ADO2 사용 방법을 단계별로 안내해 드릴게요." }, - "field": { "title": "입력하기", "desc": "네이버 지도 공유 URL을 붙여넣거나,\n업체명을 입력하면 자동으로 목록에서 선택할 수 있어요." }, - "manual": { "title": "직접 입력", "desc": "업체명과 주소를 직접 입력해서 분석을 시작할 수도 있어요." }, - "button": { "title": "브랜드 분석 시작", "desc": "버튼을 누르면 AI가 브랜드를 분석하기 시작해요." } - }, - "asset": { - "image": { "title": "이미지 목록", "desc": "네이버 Place에서 가져 온 사진이에요. \n더보기를 누르면 나머지 사진도 볼 수 있고 X를 눌러 삭제 할 수 있어요." }, - "upload": { "title": "이미지 추가", "desc": "이미지를 자유롭게 추가 할 수 있어요." }, - "ratio": { "title": "영상 비율 선택", "desc": "생성 할 영상의 비율을 선택하세요." }, - "next": { "title": "다음 단계로", "desc": "설정이 완료되면 다음 단계로 진행하세요." } - }, - "sound": { - "genre": { "title": "장르 선택", "desc": "영상에 어울리는 음악 장르를 선택하세요.", "note": "배경음악은 이후 오픈 예정입니다." }, - "language": { "title": "언어 선택", "desc": "음악의 언어를 선택할 수 있어요. \n이미 선택된 한국어로 진행해볼까요?" }, - "generate": { "title": "사운드 생성", "desc": "버튼을 클릭하면 AI가 가사와 음악을 생성해요."}, - "lyrics": { "title": "가사 생성 완료", "desc": "AI가 선택한 언어로 가사를 만들었어요.\n생성된 가사를 확인하세요." }, - "lyricsWait": { "title": "음악 생성 중", "desc": "가사를 바탕으로 AI가 음악을 만들고 있어요.\n잠시만 기다려 주세요." }, - "audioPlayer": { "title": "음악 미리 듣기", "desc": "음악 생성이 완료되었어요.\n재생 버튼을 눌러 생성된 음악을 들어보세요." }, - "video": { "title": "영상 생성", "desc": "버튼을 클릭해서 영상 생성을 시작하세요." } - }, - "completion": { - "contentInfo": { "title": "콘텐츠 정보", "desc": "콘텐츠의 파일명, 장르, 규격, 가사를 확인하세요." }, - "generating": { "title": "영상 제작 중", "desc": "AI가 영상을 만들고 있어요. \n잠시만 기다려 주세요." }, - "completion": { "title": "영상 완성!", "desc": "영상 제작이 완료되었어요. \n영상을 확인해 볼까요?" }, - "myInfo": { "title": "소셜 계정 연동", "desc": "영상을 유튜브에 업로드하려면 내 정보에서 소셜 계정을 연동해야 해요. \n클릭해서 이동하세요." } - }, - "myInfo": { - "myInfo": { "title": "내 정보", "desc": "내 정보에서는 소셜 연결과 연결된 계정을 확인 할 수 있어요." }, - "connect": { "title": "연결하기", "desc": "YouTube 연결 버튼을 누르면 연결 페이지로 이동합니다.", "note": "Instagram 연결은 오픈 예정입니다." }, - "connected": { "title": "연결 계정", "desc": "연결된 소셜 계정 목록이에요. \n연결 후 여기서 확인할 수 있어요." }, - "ado2": { "title": "ADO2 콘텐츠", "desc": "이제 생성된 영상을 업로드할 수 있어요. \n클릭해서 이동하세요." } - }, - "ado2": { - "list": { "title": "생성된 영상 목록", "desc": "ADO2에서 만든 영상들을 확인할 수 있어요." }, - "download": { "title": "다운로드", "desc": "영상을 다운로드 할 수 있어요." }, - "delete": { "title": "삭제", "desc": "필요없는 영상을 삭제할 수 있어요." }, - "upload": { "title": "소셜 업로드", "desc": "선택해서 소셜미디어에 업로드하세요." } - }, - "upload": { - "seo": { "title": "제목 및 설명", "desc": "영상의 제목과 설명을 AI가 만들고 있어요. 잠시만 기다려 주세요." }, - "required": { "title": "필수 항목", "desc": "영상을 업로드 하기 전 *는 필수 항목으로 \n반드시 확인해 주세요." }, - "schedule": { "title": "업로드 예약", "desc": "지금 게시하거나 원하는 시간에 예약할 수 있어요." }, - "submit": { "title": "업로드 시작", "desc": "게시 버튼을 눌러 업로드를 시작하세요." } - }, - "dashboard": { - "metrics": { "title": "핵심 지표", "desc": "조회수, 구독자 등 ADO2로 업로드한 콘텐츠의 주요 통계를 확인하세요." }, - "chart": { "title": "성장 추이 차트", "desc": "기간별 성장 추이를 그래프로 확인할 수 있어요." }, - "more": { "title": "더 많은 통계", "desc": "그 외에도 다양한 통계를 대시보드에서 한눈에 확인할 수 있어요." } - }, - "contentCalendar": { - "grid": { "title": "콘텐츠 캘린더", "desc": "날짜별로 콘텐츠 스케줄을 확인할 수 있어요. \n오늘 날짜를 선택해 볼까요?" }, - "panel": { "title": "콘텐츠 목록", "desc": "자세한 콘텐츠 스케줄을 확인 할수 있어요." } - }, - "feedback": { - "complete": { "title": "튜토리얼 완료 🎉", "desc": "유튜브 업로드까지 모든 과정을 완료했어요. \n튜토리얼을 다시보고 싶다면 우측 상단의 버튼을 눌러주세요." }, - "title": "고객의견", - "desc": "서비스 이용 중 불편한 점이나 개선 의견을 보내주세요." - }, - "restart": { - "title": "튜토리얼을 다시 시작할까요?", - "desc": "현재 화면부터 튜토리얼이 다시 시작됩니다.", - "confirm": "시작하기", - "cancel": "취소" - } - }, "footer": { "company":"㈜에이아이오투오", "businessNumber": "사업자 등록번호 : 620-87-00810 | 대표 : 안성민", @@ -244,7 +171,7 @@ "create": { "step1": "STEP 1 · 시나리오 선택", "step2": "STEP 2 · 업장 검색", - "searchPlaceholder": "가게 이름 (예: 골목냉면) 또는 네이버 지도 링크", + "searchPlaceholder": "업체명 또는 URL을 입력하세요.", "searchButton": "검색", "searching": "검색 중", "placeLoading": "가게를 찾는 중… (몇 초 걸려요)", @@ -274,7 +201,9 @@ "title": "썰박스가 완성됐어요", "download": "다운로드", "downloading": "다운로드 중...", - "loadFailed": "영상을 불러오지 못했어요. 내 콘텐츠에서 확인해주세요." + "loadFailed": "영상을 불러오지 못했어요. 내 콘텐츠에서 확인해주세요.", + "scenarioLabel": "시나리오", + "createdLabel": "생성일시" }, "viewer": { "untitled": "이름 없는 콘텐츠" @@ -543,10 +472,10 @@ }, "myInfo": { "title": "내 정보", - "tabBasic": "기본 정보", + "tabContents": "내 콘텐츠", + "tabCalendar": "콘텐츠 캘린더", "tabPayment": "결제 정보", "tabBusiness": "소셜 채널 관리", - "basicPlaceholder": "기본 정보 설정 기능이 준비 중입니다.", "paymentPlaceholder": "결제 정보 설정 기능이 준비 중입니다.", "myBusiness": "내 비즈니스", "noBusinessTitle": "아직 등록된 비즈니스가 없어요", diff --git a/src/pages/Analysis/AnalysisResultSection.tsx b/src/pages/Analysis/AnalysisResultSection.tsx index 7c82373..0da480d 100755 --- a/src/pages/Analysis/AnalysisResultSection.tsx +++ b/src/pages/Analysis/AnalysisResultSection.tsx @@ -1,10 +1,7 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { CrawlingResponse, TargetPersona } from '../../types/api'; import { GeometricChart } from './GeometricChart'; -import { useTutorial } from '../../components/Tutorial/useTutorial'; -import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps'; -import TutorialOverlay from '../../components/Tutorial/TutorialOverlay'; interface AnalysisResultSectionProps { onBack: () => void; @@ -14,19 +11,8 @@ interface AnalysisResultSectionProps { const AnalysisResultSection: React.FC = ({ onBack, onGenerate, data }) => { const { t } = useTranslation(); - const tutorial = useTutorial(); const { processed_info, marketing_analysis } = data; - useEffect(() => { - const timer = setTimeout(() => { - if (!tutorial.hasSeen(TUTORIAL_KEYS.ANALYSIS)) { - tutorial.startTutorial(TUTORIAL_KEYS.ANALYSIS); - } - }, 600); - return () => clearTimeout(timer); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const brandIdentity = marketing_analysis?.brand_identity; const marketPositioning = marketing_analysis?.market_positioning; const targetPersonas = marketing_analysis?.target_persona || []; @@ -37,7 +23,6 @@ const AnalysisResultSection: React.FC = ({ onBack, o const sortedSellingPoints = [...sellingPoints].sort((a, b) => b.score - a.score); return ( - <>
{/* Header */}
@@ -189,18 +174,6 @@ const AnalysisResultSection: React.FC = ({ onBack, o
- - {tutorial.isActive && ( - - )} - ); }; diff --git a/src/pages/Dashboard/GenerationFlow.tsx b/src/pages/Dashboard/GenerationFlow.tsx index 2e374bd..22cb359 100755 --- a/src/pages/Dashboard/GenerationFlow.tsx +++ b/src/pages/Dashboard/GenerationFlow.tsx @@ -2,24 +2,22 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Sidebar from '../../components/Sidebar'; +import BottomNav from '../../components/BottomNav'; +import LoginPromptModal from '../../components/LoginPromptModal'; import AssetManagementContent from './AssetManagementContent'; import SoundStudioContent from './SoundStudioContent'; import CompletionContent from './CompletionContent'; import DashboardContent from './DashboardContent'; -import BusinessSettingsContent from './BusinessSettingsContent'; import UrlInputContent from './UrlInputContent'; import ADO2ContentsPage from './ADO2ContentsPage'; -import MyContentsPage from './MyContentsPage'; import MyInfoContent from './MyInfoContent'; -import ContentCalendarContent from './ContentCalendarContent'; import LoadingSection from '../Analysis/LoadingSection'; import AnalysisResultSection from '../Analysis/AnalysisResultSection'; import { ImageItem, type ImageListItem, CrawlingResponse, UserMeResponse } from '../../types/api'; -import { crawlUrl, autocomplete, marketingAnalysis, AutocompleteRequest, getUserMe, getUserCredits, clearTokens, getActiveSsulTask, waitForSsulComplete } from '../../utils/api'; -import { useTutorial } from '../../components/Tutorial/useTutorial'; -import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps'; -import TutorialOverlay, { TutorialRestartPopup } from '../../components/Tutorial/TutorialOverlay'; +import { crawlUrl, autocomplete, marketingAnalysis, AutocompleteRequest, getUserMe, getUserCredits, clearTokens, getActiveSsulTask, waitForSsulComplete, isLoggedIn } from '../../utils/api'; import WizardStepper from '../../components/WizardStepper'; +import { NAV, GATED_ITEMS, sanitizeActiveItem, MyInfoTab } from '../../components/navItems'; +import { useIsMobile } from '../../utils/useIsMobile'; import { saveSearchHistory } from '../../components/SearchHistory/useSearchHistory'; import { Pipeline } from '../../components/PipelineTabs'; @@ -45,6 +43,9 @@ const ANALYSIS_DATA_KEY = K.ANALYSIS_DATA; */ const PENDING_PIPELINE_KEY = 'castad_pending_pipeline'; +/** 비로그인 게이트 클릭 시 "로그인 후 열어줄 메뉴"를 남기는 1회성 프리셋. 카카오 왕복을 견뎌야 해서 sessionStorage */ +const LOGIN_NEXT_ITEM_KEY = 'castad_login_next_item'; + /** 썰박스 진행 단계. ADO2 의 wizardStep(-2~3) 과 별도 축이다 */ type SsulStep = 'entry' | 'making' | 'result'; @@ -68,7 +69,6 @@ interface BusinessInfo { } interface GenerationFlowProps { - onHome: () => void; initialActiveItem?: string; initialImageList?: ImageListItem[]; businessInfo?: BusinessInfo; @@ -84,8 +84,7 @@ interface GenerationFlowProps { // 3: 완료 (Completion) const GenerationFlow: React.FC = ({ - onHome, - initialActiveItem = '대시보드', + initialActiveItem = NAV.HOME, initialImageList = [], businessInfo, initialAnalysisData @@ -109,7 +108,20 @@ const GenerationFlow: React.FC = ({ } }; - const [activeItem, setActiveItem] = useState(savedActiveItem || initialActiveItem); + const isGuest = !isLoggedIn(); + + /** activeItem 초기화: 로그인 복귀 메뉴(최우선) → 저장된 값 → 초기값, 마지막에 구 이름 마이그레이션+게스트 강등 */ + const getInitialNav = () => { + const loginNext = sessionStorage.getItem(LOGIN_NEXT_ITEM_KEY); + if (loginNext && isLoggedIn()) { + sessionStorage.removeItem(LOGIN_NEXT_ITEM_KEY); + return sanitizeActiveItem(loginNext, isGuest); + } + return sanitizeActiveItem(savedActiveItem || initialActiveItem, isGuest); + }; + const initialNav = getInitialNav(); + + const [activeItem, setActiveItem] = useState(initialNav.item); // 초기 위저드 단계 결정 const getInitialWizardStep = (): number => { @@ -174,10 +186,10 @@ const GenerationFlow: React.FC = ({ const [isAnalysisComplete, setIsAnalysisComplete] = useState(false); const [userInfo, setUserInfo] = useState(null); const [credits, setCredits] = useState(null); - const [myInfoInitialTab, setMyInfoInitialTab] = useState<'basic' | 'payment' | 'business' | undefined>(undefined); - const tutorial = useTutorial(); + const [myInfoInitialTab, setMyInfoInitialTab] = useState(initialNav.myInfoTab); const refreshCredits = useCallback(async () => { + if (!isLoggedIn()) return; try { const { credits } = await getUserCredits(); setCredits(credits); @@ -193,8 +205,9 @@ const GenerationFlow: React.FC = ({ } }, [wizardStep]); - // 로그인 직후 사용자 정보 + 크레딧 조회 + // 로그인 직후 사용자 정보 + 크레딧 조회 (비로그인이면 요청 자체를 하지 않는다 — 401 → 강제 리다이렉트 방지) useEffect(() => { + if (!isLoggedIn()) return; const fetchUserInfo = async () => { try { const [data, creditsData] = await Promise.all([getUserMe(), getUserCredits()]); @@ -263,7 +276,7 @@ const GenerationFlow: React.FC = ({ setImageList(prev => [...newImages, ...prev]); }; - // 홈 버튼(로고) 클릭 시 모든 상태 초기화 후 홈으로 이동 + // 홈 버튼(로고) 클릭 시 모든 상태 초기화 후 ADO2 콘텐츠 갤러리로 이동 const handleHome = () => { clearProjectStorage(); localStorage.removeItem(ANALYSIS_DATA_KEY); @@ -272,7 +285,7 @@ const GenerationFlow: React.FC = ({ setImageTaskId(null); setAnalysisData(null); setImageList([]); - onHome(); + setActiveItem(NAV.HOME); }; // 위저드 단계 이동 @@ -413,39 +426,22 @@ const GenerationFlow: React.FC = ({ localStorage.setItem(ACTIVE_ITEM_KEY, activeItem); }, [activeItem]); - // wizardStep 변경 시 튜토리얼 트리거 (사이드바 메뉴 화면에서는 제외) - const SIDEBAR_ITEMS = ['대시보드', 'ADO2 콘텐츠', '내 정보', '콘텐츠 캘린더']; - useEffect(() => { - if (tutorial.isActive) tutorial.skipTutorial(); - if (SIDEBAR_ITEMS.includes(activeItem)) return; - const timer = setTimeout(() => { - if (wizardStep === 1 && !tutorial.hasSeen(TUTORIAL_KEYS.ASSET)) { - tutorial.startTutorial(TUTORIAL_KEYS.ASSET); - } else if (wizardStep === 2 && !tutorial.hasSeen(TUTORIAL_KEYS.SOUND)) { - tutorial.startTutorial(TUTORIAL_KEYS.SOUND); - } - }, 600); - return () => clearTimeout(timer); - }, [wizardStep, activeItem]); + const [loginGateOpen, setLoginGateOpen] = useState(false); - // activeItem 변경 시 튜토리얼 트리거 - useEffect(() => { - if (tutorial.isActive) tutorial.skipTutorial(); - if (activeItem === '내 정보' && !tutorial.hasSeen(TUTORIAL_KEYS.MY_INFO)) { - tutorial.startTutorial(TUTORIAL_KEYS.MY_INFO); - } else if (activeItem === '내 콘텐츠' && !tutorial.hasSeen(TUTORIAL_KEYS.ADO2_CONTENTS)) { - tutorial.startTutorial(TUTORIAL_KEYS.ADO2_CONTENTS); - } else if (activeItem === '대시보드' && !tutorial.hasSeen(TUTORIAL_KEYS.DASHBOARD)) { - tutorial.startTutorial(TUTORIAL_KEYS.DASHBOARD); - } else if (activeItem === '콘텐츠 캘린더' && !tutorial.hasSeen(TUTORIAL_KEYS.CONTENT_CALENDAR)) { - tutorial.startTutorial(TUTORIAL_KEYS.CONTENT_CALENDAR); + /** 내 정보의 특정 탭으로 딥링크 이동. 비로그인이면 로그인 관문을 띄운다 */ + const goToMyInfoTab = (tab: MyInfoTab) => { + if (!isLoggedIn()) { + sessionStorage.setItem(LOGIN_NEXT_ITEM_KEY, NAV.MY); + setLoginGateOpen(true); + return; } - }, [activeItem]); + setMyInfoInitialTab(tab); + setActiveItem(NAV.MY); + }; // 결제 탭으로 바로 이동하는 핸들러 (크레딧 충전하기 버튼용) const handleGoToPayment = () => { - setMyInfoInitialTab('payment'); - setActiveItem('내 정보'); + goToMyInfoTab('payment'); }; /** @@ -520,6 +516,7 @@ const GenerationFlow: React.FC = ({ * initializeOnNewSession 이 새 탭에서 그것을 지우기 때문에 서버를 물어야 한다. */ useEffect(() => { + if (!isLoggedIn()) return; let alive = true; getActiveSsulTask().then((task) => { if (!alive || !task) return; @@ -543,8 +540,13 @@ const GenerationFlow: React.FC = ({ // 네비게이션 핸들러 - "새 프로젝트 만들기" 클릭 시 기존 프로젝트 데이터 초기화 const handleNavigate = (item: string) => { + if (!isLoggedIn() && GATED_ITEMS.includes(item)) { + sessionStorage.setItem(LOGIN_NEXT_ITEM_KEY, item); + setLoginGateOpen(true); + return; + } setMyInfoInitialTab(undefined); - if (item === '새 프로젝트 만들기') { + if (item === NAV.NEW) { // 기존 프로젝트 데이터 초기화 clearProjectStorage(); localStorage.removeItem(ANALYSIS_DATA_KEY); @@ -660,13 +662,6 @@ const GenerationFlow: React.FC = ({ videoGenerationStatus={videoGenerationStatus} videoGenerationProgress={videoGenerationProgress} onGoToPayment={handleGoToPayment} - onStatusChange={(status: string) => { - if (status === 'generating_song' && !tutorial.hasSeen(TUTORIAL_KEYS.SOUND_LYRICS)) { - setTimeout(() => tutorial.startTutorial(TUTORIAL_KEYS.SOUND_LYRICS), 400); - } else if (status === 'complete' && !tutorial.hasSeen(TUTORIAL_KEYS.SOUND_AUDIO)) { - setTimeout(() => tutorial.startTutorial(TUTORIAL_KEYS.SOUND_AUDIO), 400); - } - }} /> ); case 3: @@ -684,7 +679,7 @@ const GenerationFlow: React.FC = ({ songTaskId={songTaskId} onVideoStatusChange={setVideoGenerationStatus} onVideoProgressChange={setVideoGenerationProgress} - onGoToCalendar={() => handleNavigate('콘텐츠 캘린더')} + onGoToCalendar={() => goToMyInfoTab('calendar')} /> ); default: @@ -694,24 +689,17 @@ const GenerationFlow: React.FC = ({ const renderContent = () => { switch (activeItem) { - case '대시보드': - return ; - case '비즈니스 설정': - return ; - case 'ADO2 콘텐츠': - return ; - case '내 콘텐츠': + case NAV.DASHBOARD: return ( - setActiveItem('새 프로젝트 만들기')} - onNavigate={handleNavigate} + (id === NAV.MY ? goToMyInfoTab('business') : handleNavigate(id))} /> ); - case '콘텐츠 캘린더': - return ; - case '내 정보': - return ; - case '새 프로젝트 만들기': + case NAV.HOME: + return ; + case NAV.MY: + return ; + case NAV.NEW: // 진입 화면(탭)과 ADO2 로딩(-1)은 스텝퍼 없이 전체 화면으로 표시 if (isEntry || (pipeline === 'ado2' && wizardStep === -1)) { return renderWizardContent(); @@ -752,88 +740,75 @@ const GenerationFlow: React.FC = ({ } }; - // 로딩 화면에서만 Sidebar 숨김 + // 로딩 화면에서만 Sidebar/BottomNav 숨김 const showSidebar = wizardStep !== -1; // 브랜드 분석(0)일 때는 전체 페이지 스크롤 - const isBrandAnalysis = activeItem === '새 프로젝트 만들기' && wizardStep === 0; - // 스크롤이 필요한 페이지: 대시보드, 비즈니스 설정, 브랜드 분석(0), ADO2 콘텐츠, 내 정보 - const needsScroll = activeItem === '대시보드' || activeItem === '비즈니스 설정' || activeItem === 'ADO2 콘텐츠' || activeItem === '내 콘텐츠' || activeItem === '내 정보' || activeItem === '콘텐츠 캘린더' || isBrandAnalysis; + const isBrandAnalysis = activeItem === NAV.NEW && wizardStep === 0; + // 위저드 진행 중에도 탭바를 노출한다. 각 단계의 고정 CTA 는 모바일에서 + // 탭바 높이만큼 띄우도록 CSS(@media max-width:767px)에서 처리한다. + const showBottomNav = showSidebar; + // 스크롤이 필요한 페이지: 새 프로젝트 만들기(위저드) 외 전부 + 브랜드 분석(0) + const needsScroll = activeItem !== NAV.NEW || isBrandAnalysis; - // 현재 화면에 맞는 튜토리얼 키 반환 (없으면 null) - const getCurrentTutorialKey = (): string | null => { - if (activeItem === '내 정보') return TUTORIAL_KEYS.MY_INFO; - if (activeItem === '내 콘텐츠') return TUTORIAL_KEYS.ADO2_CONTENTS; - if (activeItem === '대시보드') return TUTORIAL_KEYS.DASHBOARD; - if (activeItem === '콘텐츠 캘린더') return TUTORIAL_KEYS.CONTENT_CALENDAR; - if (activeItem === '새 프로젝트 만들기') { - if (wizardStep === 1) return TUTORIAL_KEYS.ASSET; - if (wizardStep === 2) return TUTORIAL_KEYS.SOUND; - } - return null; - }; + const isMobile = useIsMobile(); - const tutorialUI = ( - <> - {getCurrentTutorialKey() && ( - - )} - {tutorial.isActive && ( - setLoginGateOpen(true)} /> - )} - {tutorial.isRestartPopupVisible && ( - setLoginGateOpen(true)} /> - )} - + )); + + const loginGateModal = loginGateOpen && ( + { + sessionStorage.removeItem(LOGIN_NEXT_ITEM_KEY); + setLoginGateOpen(false); + }} + /> ); // 브랜드 분석일 때는 전체 화면 스크롤 if (isBrandAnalysis) { return (
- {showSidebar && ( - tutorial.toggleTutorial(getCurrentTutorialKey())} /> - )} -
+ {!isMobile && nav} +
{renderContent()}
- {tutorialUI} + {isMobile && nav} + {loginGateModal}
); } return (
- {showSidebar && ( - tutorial.toggleTutorial(getCurrentTutorialKey())} /> - )} - {tutorialUI} -
+ {!isMobile && nav} +
{renderContent()}
+ {isMobile && nav} + {loginGateModal}
); }; diff --git a/src/pages/Landing/DisplaySection.tsx b/src/pages/Landing/DisplaySection.tsx deleted file mode 100755 index a265952..0000000 --- a/src/pages/Landing/DisplaySection.tsx +++ /dev/null @@ -1,51 +0,0 @@ - -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import Footer from '../../components/Footer'; - -interface DisplaySectionProps { - onStartClick?: () => void; -} - -const DisplaySection: React.FC = ({ onStartClick }) => { - const { t } = useTranslation(); - // YouTube Shorts 영상 ID들 - const videos = [ - { id: 1, videoId: 'M3iuPZ59X1I' }, - { id: 2, videoId: 'JxWQxELDHSs' }, - { id: 3, videoId: 'c2ZdwhaB7S4' }, - ]; - - return ( -
-
- {/* Main visual frames container */} -
- {videos.map((video, index) => ( -
-