From 4dff60c1ba73d9d4982bbc1934572e9b8f992069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Wed, 19 Aug 2026 16:31:36 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20main=20=EB=A8=B8=EC=A7=80=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=B5=EA=B5=AC=EB=90=9C=20=EB=9E=9C=EB=94=A9=EC=9D=84=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4.=20AD?= =?UTF-8?q?O2=20=EC=BD=98=ED=85=90=EC=B8=A0=EB=A5=BC=20=EC=B2=AB=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EB=90=98=EB=8F=8C?= =?UTF-8?q?=EB=A6=B0=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/App.tsx | 399 ++----------------------- src/components/Header.tsx | 64 ---- src/pages/Dashboard/GenerationFlow.tsx | 2 +- src/pages/Landing/DisplaySection.tsx | 51 ---- src/pages/Landing/HeroSection.tsx | 176 ----------- src/pages/Landing/WelcomeSection.tsx | 70 ----- src/pages/Login/LoginSection.tsx | 109 ------- 7 files changed, 18 insertions(+), 853 deletions(-) delete mode 100755 src/components/Header.tsx delete mode 100755 src/pages/Landing/DisplaySection.tsx delete mode 100755 src/pages/Landing/HeroSection.tsx delete mode 100755 src/pages/Landing/WelcomeSection.tsx delete mode 100755 src/pages/Login/LoginSection.tsx diff --git a/src/App.tsx b/src/App.tsx index 925d146..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 SsulDetailPage from './pages/Ssulbox/SsulDetailPage'; -import { crawlUrl, autocomplete, marketingAnalysis, kakaoCallback, isLoggedIn, saveTokens, AutocompleteRequest, storeUtmFromUrl, trackViewContent, trackCompleteRegistration } from './utils/api'; -import { saveSearchHistory } from './components/SearchHistory/useSearchHistory'; -import { CrawlingResponse } from './types/api'; +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 = 'castad_view_mode'; -const ANALYSIS_DATA_KEY = 'castad_analysis_data'; const SESSION_KEY = 'castad_session_active'; // 새 탭/새 창에서 접근 시 localStorage 초기화 (sessionStorage로 현재 세션 확인) @@ -31,15 +19,7 @@ const initializeOnNewSession = () => { if (!isExistingSession) { // 새 세션이면 localStorage 정리하고 세션 표시 - localStorage.removeItem(VIEW_MODE_KEY); - localStorage.removeItem(ANALYSIS_DATA_KEY); - localStorage.removeItem('castad_wizard_step'); - localStorage.removeItem('castad_active_item'); - localStorage.removeItem('castad_song_task_id'); - localStorage.removeItem('castad_image_task_id'); - localStorage.removeItem('castad_song_generation'); - localStorage.removeItem('castad_video_generation'); - localStorage.removeItem('castad_video_ratio'); + clearSessionStorage(); sessionStorage.setItem(SESSION_KEY, 'true'); } }; @@ -48,61 +28,8 @@ 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); // 카카오 로그인 콜백 처리 (URL에서 토큰 또는 code 파라미터 확인) @@ -130,8 +57,8 @@ const App: React.FC = () => { setIsProcessingCallback(true); handleKakaoCallback(code); } - // 콜백이 아닌 일반 진입이고 랜딩이 첫 화면이면 ViewContent 발화 - else if (viewMode === 'landing') { + // 콜백이 아닌 일반 진입 — 광고 유입 추적 + else { trackViewContent(); } }, []); @@ -155,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')); @@ -203,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; @@ -450,12 +146,14 @@ 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) { return ( @@ -467,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/pages/Dashboard/GenerationFlow.tsx b/src/pages/Dashboard/GenerationFlow.tsx index 4960c41..4dcb600 100755 --- a/src/pages/Dashboard/GenerationFlow.tsx +++ b/src/pages/Dashboard/GenerationFlow.tsx @@ -313,7 +313,7 @@ const GenerationFlow: React.FC = ({ setImageList(prev => [...newImages, ...prev]); }; - // 홈 버튼(로고) 클릭 시 프로젝트 상태를 비우고, 부모가 있으면 랜딩으로 돌아간다. + // 홈 버튼(로고) 클릭 시 프로젝트 상태를 비우고 ADO2 콘텐츠로 돌아간다. const handleHome = () => { clearProjectStorage(); localStorage.removeItem(ANALYSIS_DATA_KEY); 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) => ( -
-