import React, { useRef, 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 { crawlUrl, autocomplete, kakaoCallback, isLoggedIn, saveTokens, AutocompleteRequest } from './utils/api'; import { CrawlingResponse } from './types/api'; 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로 현재 세션 확인) const initializeOnNewSession = () => { const isExistingSession = sessionStorage.getItem(SESSION_KEY); 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'); sessionStorage.setItem(SESSION_KEY, 'true'); } }; // 앱 시작 시 세션 체크 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 [scrollProgress, setScrollProgress] = useState(0); const [isProcessingCallback, setIsProcessingCallback] = useState(false); // 카카오 로그인 콜백 처리 (URL에서 토큰 또는 code 파라미터 확인) useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const accessToken = urlParams.get('access_token'); const refreshToken = urlParams.get('refresh_token'); const code = urlParams.get('code'); const currentPath = window.location.pathname; // Social OAuth 콜백 경로는 여기서 처리하지 않음 (백엔드에서 처리) const isSocialOAuthCallback = currentPath.includes('/social/oauth/') || currentPath.includes('/social/connect/'); // 백엔드에서 토큰을 URL 파라미터로 전달한 경우 if (accessToken && refreshToken && !isProcessingCallback) { setIsProcessingCallback(true); handleTokenCallback(accessToken, refreshToken); } // 기존 code 방식 (Redirect URI가 프론트엔드인 경우) - Social OAuth는 제외 else if (code && !isProcessingCallback && !isSocialOAuthCallback) { setIsProcessingCallback(true); handleKakaoCallback(code); } }, []); // 백엔드에서 토큰을 URL 파라미터로 전달받은 경우 처리 const handleTokenCallback = (accessToken: string, refreshToken: string) => { try { // 토큰 저장 saveTokens(accessToken, refreshToken); // URL에서 토큰 파라미터 제거 const url = new URL(window.location.href); url.searchParams.delete('access_token'); url.searchParams.delete('refresh_token'); window.history.replaceState({}, document.title, url.pathname); 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'); } setInitialTab('새 프로젝트 만들기'); setViewMode('generation_flow'); } catch (err) { console.error('Token callback failed:', err); alert(t('app.loginFailed')); } finally { setIsProcessingCallback(false); } }; const handleKakaoCallback = async (code: string) => { try { const response = await kakaoCallback(code); // 로그인 성공 - 서버에서 받은 redirect_url로 이동 window.location.href = response.redirect_url; } catch (err) { console.error('Kakao callback failed:', err); alert(t('app.kakaoLoginFailed')); // URL에서 code 파라미터 제거 const url = new URL(window.location.href); url.searchParams.delete('code'); window.history.replaceState({}, document.title, url.pathname); } finally { setIsProcessingCallback(false); } }; // 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; setViewMode('loading'); 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)); setViewMode('analysis'); } catch (err) { console.error('Crawling failed:', err); const errorMessage = err instanceof Error ? err.message : t('app.analysisError'); setError(errorMessage); setViewMode('landing'); } }; // 업체명 자동완성으로 분석 시작 const handleAutocomplete = async (request: AutocompleteRequest) => { setViewMode('loading'); 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)); setViewMode('analysis'); } catch (err) { console.error('Autocomplete failed:', err); const errorMessage = err instanceof Error ? err.message : t('app.autocompleteGeneralError'); setError(errorMessage); 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; // YouTube OAuth 콜백 처리 (Google에서 리다이렉트) if (pathname === '/social/oauth/youtube/callback') { return ; } if (pathname === '/social/connect/success') { return ; } if (pathname === '/social/connect/error') { return ; } // 카카오 콜백 처리 중 로딩 화면 표시 if (isProcessingCallback) { return (

{t('app.loginProcessing')}

); } if (viewMode === 'loading') { return ; } 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)} />
); }; export default App;