o2o-castad-frontend/src/pages/Dashboard/GenerationFlow.tsx

469 lines
16 KiB
TypeScript
Executable File

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<GenerationFlowProps> = ({
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<string | null>(savedSongTaskId);
const [imageTaskId, setImageTaskId] = useState<string | null>(savedImageTaskId);
const [videoGenerationStatus, setVideoGenerationStatus] = useState<'idle' | 'generating' | 'complete' | 'error'>('idle');
const [videoGenerationProgress, setVideoGenerationProgress] = useState(0);
const [analysisData, setAnalysisData] = useState<CrawlingResponse | null>(parseAnalysisData());
const [analysisError, setAnalysisError] = useState<string | null>(null);
const [userInfo, setUserInfo] = useState<UserMeResponse | null>(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<ImageItem[]>(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 (
<UrlInputContent
onAnalyze={handleStartAnalysis}
onAutocomplete={handleAutocomplete}
onTestData={handleTestData}
error={analysisError}
/>
);
case -1:
// 로딩 단계
return <LoadingSection />;
case 0:
// 브랜드 분석 결과 단계
if (!analysisData) {
goToWizardStep(-2);
return null;
}
return (
<AnalysisResultSection
onBack={handleAnalysisBack}
onGenerate={handleAnalysisGenerate}
data={analysisData}
/>
);
case 1:
return (
<AssetManagementContent
onNext={(taskId: string) => {
// 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 (
<SoundStudioContent
onBack={() => 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 (
<CompletionContent
onBack={() => {
// 뒤로가기 시 비디오 생성 상태 초기화
// 새 노래 생성 후 다시 영상 생성할 수 있도록
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 <DashboardContent />;
case '비즈니스 설정':
return <BusinessSettingsContent />;
case 'ADO2 콘텐츠':
return (
<ADO2ContentsPage
onBack={() => setActiveItem('새 프로젝트 만들기')}
/>
);
case '내 정보':
return <MyInfoContent />;
case '새 프로젝트 만들기':
// 브랜드 분석(0)과 로딩(-1)은 전체 화면으로 표시
if (wizardStep === 0 || wizardStep === -1) {
return renderWizardContent();
}
return (
<div className="wizard-page-container">
{renderWizardContent()}
</div>
);
default:
return (
<div className="flex-1 flex items-center justify-center text-gray-500 font-light">
{t('app.pageComingSoon', { page: activeItem })}
</div>
);
}
};
// 로딩 화면에서만 Sidebar 숨김
const showSidebar = wizardStep !== -1;
// 브랜드 분석(0)일 때는 전체 페이지 스크롤
const isBrandAnalysis = activeItem === '새 프로젝트 만들기' && wizardStep === 0;
// 스크롤이 필요한 페이지: 대시보드, 비즈니스 설정, 브랜드 분석(0), ADO2 콘텐츠, 내 정보
const needsScroll = activeItem === '대시보드' || activeItem === '비즈니스 설정' || activeItem === 'ADO2 콘텐츠' || activeItem === '내 정보' || isBrandAnalysis;
// 브랜드 분석일 때는 전체 화면 스크롤
if (isBrandAnalysis) {
return (
<div className="analysis-page-wrapper">
{showSidebar && (
<Sidebar activeItem={activeItem} onNavigate={handleNavigate} onHome={handleHome} userInfo={userInfo} onLogout={handleLogout} />
)}
<main className="analysis-page-main">
{renderContent()}
</main>
</div>
);
}
return (
<div className={`flex w-full bg-[#002224] text-white ${needsScroll ? 'min-h-[100dvh]' : 'h-[100dvh] overflow-hidden'}`}>
{showSidebar && (
<Sidebar activeItem={activeItem} onNavigate={handleNavigate} onHome={handleHome} userInfo={userInfo} onLogout={handleLogout} />
)}
<div className={`flex-1 relative pl-0 md:pl-0 ${needsScroll ? 'overflow-y-auto' : 'h-full overflow-hidden'}`}>
{renderContent()}
</div>
</div>
);
};
export default GenerationFlow;