import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Sidebar from '../../components/Sidebar'; 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 MyInfoContent from './MyInfoContent'; import LoadingSection from '../Analysis/LoadingSection'; import AnalysisResultSection from '../Analysis/AnalysisResultSection'; import { ImageItem, CrawlingResponse, UserMeResponse } from '../../types/api'; import { crawlUrl, autocomplete, AutocompleteRequest, getUserMe, clearTokens } from '../../utils/api'; const WIZARD_STEP_KEY = 'castad_wizard_step'; const ACTIVE_ITEM_KEY = 'castad_active_item'; const SONG_TASK_ID_KEY = 'castad_song_task_id'; const IMAGE_TASK_ID_KEY = 'castad_image_task_id'; const ANALYSIS_DATA_KEY = 'castad_analysis_data'; // 다른 컴포넌트에서 사용하는 storage key들 (초기화용) const SONG_GENERATION_KEY = 'castad_song_generation'; const VIDEO_GENERATION_KEY = 'castad_video_generation'; const VIDEO_COMPLETE_KEY = 'castad_video_complete'; // 완료된 영상 정보 // 모든 프로젝트 관련 localStorage 초기화 const clearAllProjectStorage = () => { localStorage.removeItem(WIZARD_STEP_KEY); localStorage.removeItem(SONG_TASK_ID_KEY); localStorage.removeItem(IMAGE_TASK_ID_KEY); localStorage.removeItem(SONG_GENERATION_KEY); localStorage.removeItem(VIDEO_GENERATION_KEY); localStorage.removeItem(VIDEO_COMPLETE_KEY); }; interface BusinessInfo { customer_name: string; region: string; detail_region_info: string; } interface GenerationFlowProps { onHome: () => void; initialActiveItem?: string; initialImageList?: string[]; businessInfo?: BusinessInfo; initialAnalysisData?: CrawlingResponse | null; } // 위저드 단계: // -2: URL 입력 // -1: 로딩 (분석 중) // 0: 브랜드 분석 결과 // 1: 에셋 관리 (Asset Management) // 2: 사운드 스튜디오 (Sound Studio) // 3: 완료 (Completion) const GenerationFlow: React.FC = ({ onHome, initialActiveItem = '대시보드', initialImageList = [], businessInfo, initialAnalysisData }) => { const { t, i18n } = useTranslation(); // localStorage에서 저장된 상태 복원 const savedActiveItem = localStorage.getItem(ACTIVE_ITEM_KEY); const savedWizardStep = localStorage.getItem(WIZARD_STEP_KEY); const savedSongTaskId = localStorage.getItem(SONG_TASK_ID_KEY); const savedImageTaskId = localStorage.getItem(IMAGE_TASK_ID_KEY); const savedAnalysisData = localStorage.getItem(ANALYSIS_DATA_KEY); // 분석 데이터 파싱 const parseAnalysisData = (): CrawlingResponse | null => { if (initialAnalysisData) return initialAnalysisData; if (!savedAnalysisData) return null; try { return JSON.parse(savedAnalysisData) as CrawlingResponse; } catch { return null; } }; const [activeItem, setActiveItem] = useState(savedActiveItem || initialActiveItem); // 초기 위저드 단계 결정 const getInitialWizardStep = (): number => { // 저장된 단계가 있으면 사용 if (savedWizardStep !== null) { return parseInt(savedWizardStep, 10); } // 분석 데이터가 있으면 에셋 관리(1)부터, 없으면 URL 입력(-2)부터 const hasAnalysisData = initialAnalysisData || savedAnalysisData; return hasAnalysisData ? 1 : -2; }; const [wizardStep, setWizardStep] = useState(getInitialWizardStep()); const [songTaskId, setSongTaskId] = useState(savedSongTaskId); const [imageTaskId, setImageTaskId] = useState(savedImageTaskId); const [videoGenerationStatus, setVideoGenerationStatus] = useState<'idle' | 'generating' | 'complete' | 'error'>('idle'); const [videoGenerationProgress, setVideoGenerationProgress] = useState(0); const [analysisData, setAnalysisData] = useState(parseAnalysisData()); const [analysisError, setAnalysisError] = useState(null); const [userInfo, setUserInfo] = useState(null); // 로그인 직후 사용자 정보 조회 useEffect(() => { const fetchUserInfo = async () => { try { const data = await getUserMe(); setUserInfo(data); } catch (error) { console.error('Failed to fetch user info:', error); } }; fetchUserInfo(); }, []); // 로그아웃 핸들러 const handleLogout = () => { clearTokens(); localStorage.removeItem('castad_view_mode'); localStorage.removeItem('castad_analysis_data'); localStorage.removeItem(WIZARD_STEP_KEY); localStorage.removeItem(ACTIVE_ITEM_KEY); window.location.href = '/'; }; // 현재 비즈니스 정보 (분석 데이터에서 가져오거나 prop에서 가져옴) const currentBusinessInfo = analysisData?.processed_info || businessInfo; // URL 이미지를 ImageItem 형태로 변환하여 초기화 const getInitialImageList = (): ImageItem[] => { if (analysisData?.image_list && analysisData.image_list.length > 0) { return analysisData.image_list.map(url => ({ type: 'url', url })); } return initialImageList.map(url => ({ type: 'url', url })); }; const [imageList, setImageList] = useState(getInitialImageList()); // analysisData 변경 시 imageList 업데이트 useEffect(() => { if (analysisData?.image_list && analysisData.image_list.length > 0) { setImageList(analysisData.image_list.map(url => ({ type: 'url', url }))); } }, [analysisData]); const handleRemoveImage = (index: number) => { setImageList(prev => { const item = prev[index]; // 파일 이미지인 경우 메모리 해제 if (item.type === 'file') { URL.revokeObjectURL(item.preview); } return prev.filter((_, i) => i !== index); }); }; const handleAddImages = (files: File[]) => { const newImages: ImageItem[] = files.map(file => ({ type: 'file', file, preview: URL.createObjectURL(file), })); // 새로 업로드된 이미지를 배열 앞에 추가 (최신 이미지가 상단에 표시) setImageList(prev => [...newImages, ...prev]); }; // 홈 버튼(로고) 클릭 시 모든 상태 초기화 후 홈으로 이동 const handleHome = () => { clearAllProjectStorage(); localStorage.removeItem(ANALYSIS_DATA_KEY); setWizardStep(-2); setSongTaskId(null); setImageTaskId(null); setAnalysisData(null); setImageList([]); onHome(); }; // 위저드 단계 이동 const goToWizardStep = (step: number) => { setWizardStep(step); localStorage.setItem(WIZARD_STEP_KEY, step.toString()); }; // 업체명 자동완성으로 분석 시작 const handleAutocomplete = async (request: AutocompleteRequest) => { goToWizardStep(-1); // 로딩 상태로 setAnalysisError(null); try { const data = await autocomplete(request); // 기본값 보장 if (data.marketing_analysis) { data.marketing_analysis.tags = data.marketing_analysis.tags || []; data.marketing_analysis.facilities = data.marketing_analysis.facilities || []; data.marketing_analysis.report = data.marketing_analysis.report || ''; } 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 || []; setAnalysisData(data); localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(data)); goToWizardStep(0); // 브랜드 분석 결과로 } catch (err) { console.error('Autocomplete error:', err); setAnalysisError(err instanceof Error ? err.message : t('app.autocompleteError')); goToWizardStep(-2); // URL 입력으로 돌아가기 } }; // 테스트 데이터로 브랜드 분석 페이지 이동 const handleTestData = (data: CrawlingResponse) => { const tagged = { ...data, _isTestData: true }; setAnalysisData(tagged); localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(tagged)); goToWizardStep(0); // 브랜드 분석 결과로 }; // 언어 변경 시 테스트 데이터 다시 로드 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]); // URL 분석 시작 const handleStartAnalysis = async (url: string) => { if (!url.trim()) return; goToWizardStep(-1); // 로딩 상태로 setAnalysisError(null); try { const data = await crawlUrl(url); // 기본값 보장 if (data.marketing_analysis) { data.marketing_analysis.tags = data.marketing_analysis.tags || []; data.marketing_analysis.facilities = data.marketing_analysis.facilities || []; data.marketing_analysis.report = data.marketing_analysis.report || ''; } 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 || []; setAnalysisData(data); localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(data)); goToWizardStep(0); // 브랜드 분석 결과로 } catch (err) { console.error('Crawling failed:', err); const errorMessage = err instanceof Error ? err.message : t('app.analysisError'); setAnalysisError(errorMessage); goToWizardStep(-2); // URL 입력으로 돌아가기 } }; // 브랜드 분석에서 콘텐츠 생성 클릭 const handleAnalysisGenerate = () => { goToWizardStep(1); // 에셋 관리로 }; // 브랜드 분석에서 뒤로가기 const handleAnalysisBack = () => { goToWizardStep(-2); // URL 입력으로 }; // activeItem 변경 시 localStorage에 저장 useEffect(() => { localStorage.setItem(ACTIVE_ITEM_KEY, activeItem); }, [activeItem]); // 네비게이션 핸들러 - "새 프로젝트 만들기" 클릭 시 기존 프로젝트 데이터 초기화 const handleNavigate = (item: string) => { if (item === '새 프로젝트 만들기') { // 기존 프로젝트 데이터 초기화 clearAllProjectStorage(); localStorage.removeItem(ANALYSIS_DATA_KEY); setWizardStep(-2); setSongTaskId(null); setImageTaskId(null); setAnalysisData(null); setImageList([]); setVideoGenerationStatus('idle'); setVideoGenerationProgress(0); setAnalysisError(null); } setActiveItem(item); }; // 새 프로젝트 만들기 - 단계별 컨텐츠 렌더링 const renderWizardContent = () => { switch (wizardStep) { case -2: // URL 입력 단계 return ( ); case -1: // 로딩 단계 return ; case 0: // 브랜드 분석 결과 단계 if (!analysisData) { goToWizardStep(-2); return null; } return ( ); case 1: return ( { // Clear video generation state to start fresh localStorage.removeItem(VIDEO_GENERATION_KEY); localStorage.removeItem(VIDEO_COMPLETE_KEY); setVideoGenerationStatus('idle'); setVideoGenerationProgress(0); setImageTaskId(taskId); localStorage.setItem(IMAGE_TASK_ID_KEY, taskId); goToWizardStep(2); }} imageList={imageList} onRemoveImage={handleRemoveImage} onAddImages={handleAddImages} /> ); case 2: return ( goToWizardStep(1)} onNext={(taskId: string) => { setSongTaskId(taskId); localStorage.setItem(SONG_TASK_ID_KEY, taskId); goToWizardStep(3); }} businessInfo={currentBusinessInfo} imageTaskId={imageTaskId} videoGenerationStatus={videoGenerationStatus} videoGenerationProgress={videoGenerationProgress} /> ); case 3: return ( { // 뒤로가기 시 비디오 생성 상태 초기화 // 새 노래 생성 후 다시 영상 생성할 수 있도록 localStorage.removeItem(VIDEO_GENERATION_KEY); localStorage.removeItem(VIDEO_COMPLETE_KEY); setVideoGenerationStatus('idle'); setVideoGenerationProgress(0); goToWizardStep(2); }} songTaskId={songTaskId} onVideoStatusChange={setVideoGenerationStatus} onVideoProgressChange={setVideoGenerationProgress} /> ); default: return null; } }; const renderContent = () => { switch (activeItem) { case '대시보드': return ; case '비즈니스 설정': return ; case 'ADO2 콘텐츠': return ( setActiveItem('새 프로젝트 만들기')} /> ); case '내 정보': return ; case '새 프로젝트 만들기': // 브랜드 분석(0)과 로딩(-1)은 전체 화면으로 표시 if (wizardStep === 0 || wizardStep === -1) { return renderWizardContent(); } return (
{renderWizardContent()}
); default: return (
{t('app.pageComingSoon', { page: activeItem })}
); } }; // 로딩 화면에서만 Sidebar 숨김 const showSidebar = wizardStep !== -1; // 브랜드 분석(0)일 때는 전체 페이지 스크롤 const isBrandAnalysis = activeItem === '새 프로젝트 만들기' && wizardStep === 0; // 스크롤이 필요한 페이지: 대시보드, 비즈니스 설정, 브랜드 분석(0), ADO2 콘텐츠, 내 정보 const needsScroll = activeItem === '대시보드' || activeItem === '비즈니스 설정' || activeItem === 'ADO2 콘텐츠' || activeItem === '내 정보' || isBrandAnalysis; // 브랜드 분석일 때는 전체 화면 스크롤 if (isBrandAnalysis) { return (
{showSidebar && ( )}
{renderContent()}
); } return (
{showSidebar && ( )}
{renderContent()}
); }; export default GenerationFlow;