import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { generateVideo, waitForVideoComplete } from '../../utils/api'; import SocialPostingModal from '../../components/SocialPostingModal'; import { useTutorial } from '../../components/Tutorial/useTutorial'; import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps'; import TutorialOverlay from '../../components/Tutorial/TutorialOverlay'; interface CompletionContentProps { onBack: () => void; songTaskId: string | null; onVideoStatusChange?: (status: 'idle' | 'generating' | 'complete' | 'error') => void; onVideoProgressChange?: (progress: number) => void; } type VideoStatus = 'idle' | 'generating' | 'polling' | 'complete' | 'error'; const VIDEO_STORAGE_KEY = 'castad_video_generation'; const VIDEO_COMPLETE_KEY = 'castad_video_complete'; const VIDEO_STORAGE_EXPIRY = 30 * 60 * 1000; interface SavedVideoState { videoTaskId: string; songTaskId: string; status: VideoStatus; videoUrl: string | null; videoDbId?: number; timestamp: number; } const CompletionContent: React.FC = ({ onBack, songTaskId, onVideoStatusChange, onVideoProgressChange }) => { const { t } = useTranslation(); const [videoStatus, setVideoStatus] = useState('idle'); const [videoUrl, setVideoUrl] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [statusMessage, setStatusMessage] = useState(''); const [renderProgress, setRenderProgress] = useState(0); const hasStartedGeneration = useRef(false); const tutorial = useTutorial(); // 영상 생성 중 튜토리얼 트리거 (생성 상태 안내 -> 콘텐츠 정보 -> 내 정보 이동) useEffect(() => { const isComplete = videoStatus === 'complete'; const isProcessing = videoStatus === 'generating' || videoStatus === 'polling'; if (isProcessing && !tutorial.isActive && !tutorial.hasSeen(TUTORIAL_KEYS.GENERATING)) { tutorial.startTutorial(TUTORIAL_KEYS.GENERATING); } else if (isComplete && !tutorial.isActive && !tutorial.hasSeen(TUTORIAL_KEYS.COMPLETION)) { tutorial.startTutorial(TUTORIAL_KEYS.COMPLETION); } }, [videoStatus, tutorial]); // 소셜 미디어 포스팅 모달 const [showSocialModal, setShowSocialModal] = useState(false); const [videoDbId, setVideoDbId] = useState(null); // 저장된 완료 데이터 const [songCompletionData, setSongCompletionData] = useState<{ businessName: string; genre: string; lyrics: string; } | null>(null); // 비디오 비율 const [videoRatio, setVideoRatio] = useState<'vertical' | 'horizontal'>('vertical'); useEffect(() => { if (onVideoStatusChange) { const mappedStatus = videoStatus === 'polling' ? 'generating' : videoStatus; onVideoStatusChange(mappedStatus); } }, [videoStatus, onVideoStatusChange]); useEffect(() => { if (onVideoProgressChange) { onVideoProgressChange(renderProgress); } }, [renderProgress, onVideoProgressChange]); const saveToStorage = (videoTaskId: string, currentSongTaskId: string, status: VideoStatus, url: string | null, dbId?: number) => { const data: SavedVideoState = { videoTaskId, songTaskId: currentSongTaskId, status, videoUrl: url, videoDbId: dbId, timestamp: Date.now(), }; localStorage.setItem(VIDEO_STORAGE_KEY, JSON.stringify(data)); if (status === 'complete' && url) { const completeData = { songTaskId: currentSongTaskId, videoUrl: url, videoDbId: dbId, completedAt: Date.now(), }; localStorage.setItem(VIDEO_COMPLETE_KEY, JSON.stringify(completeData)); } }; const clearStorage = () => { localStorage.removeItem(VIDEO_STORAGE_KEY); }; const loadCompleteVideo = (): { songTaskId: string; videoUrl: string; videoDbId?: number } | null => { try { const saved = localStorage.getItem(VIDEO_COMPLETE_KEY); if (!saved) return null; return JSON.parse(saved); } catch { return null; } }; const loadFromStorage = (): SavedVideoState | null => { try { const saved = localStorage.getItem(VIDEO_STORAGE_KEY); if (!saved) return null; const data: SavedVideoState = JSON.parse(saved); if (Date.now() - data.timestamp > VIDEO_STORAGE_EXPIRY) { clearStorage(); return null; } return data; } catch { clearStorage(); return null; } }; const startVideoGeneration = async () => { if (!songTaskId || hasStartedGeneration.current) return; hasStartedGeneration.current = true; setVideoStatus('generating'); setStatusMessage(t('completion.requestingGeneration')); setErrorMessage(null); try { const savedRatio = localStorage.getItem('castad_video_ratio'); const orientation = (savedRatio === 'horizontal' || savedRatio === 'vertical') ? savedRatio : 'vertical'; const videoResponse = await generateVideo(songTaskId, orientation); if (!videoResponse.success) { throw new Error(videoResponse.error_message || t('completion.generationFailed')); } setVideoStatus('polling'); setStatusMessage(t('completion.generatingVideo')); saveToStorage(videoResponse.creatomate_render_id, songTaskId, 'polling', null); await pollVideoStatus(videoResponse.creatomate_render_id, songTaskId); } catch (error) { console.error('Video generation failed:', error); setVideoStatus('error'); setErrorMessage(error instanceof Error ? error.message : t('completion.generationError')); hasStartedGeneration.current = false; clearStorage(); } }; const getStatusMessage = (status: string): string => { switch (status) { case 'planned': return t('completion.statusPlanned'); case 'waiting': return t('completion.statusWaiting'); case 'transcribing': return t('completion.statusTranscribing'); case 'rendering': return t('completion.statusRendering'); case 'succeeded': return t('completion.statusSucceeded'); default: return t('completion.statusDefault'); } }; const getProgressForStatus = (status: string): number => { switch (status) { case 'planned': return 20; case 'waiting': return 40; case 'transcribing': return 60; case 'rendering': return 80; case 'succeeded': return 100; default: return 0; } }; const pollVideoStatus = async (videoTaskId: string, currentSongTaskId: string) => { try { const statusResponse = await waitForVideoComplete( videoTaskId, (status: string) => { setStatusMessage(getStatusMessage(status)); setRenderProgress(getProgressForStatus(status)); } ); const videoUrlFromResponse = statusResponse.render_data?.url; if (videoUrlFromResponse) { const videoId = statusResponse.render_data?.video_id; setVideoUrl(videoUrlFromResponse); if (videoId) { setVideoDbId(videoId); } setVideoStatus('complete'); setStatusMessage(''); saveToStorage(videoTaskId, currentSongTaskId, 'complete', videoUrlFromResponse, videoId); } else { throw new Error(t('completion.videoUrlMissing')); } } catch (error) { console.error('Video polling failed:', error); if (error instanceof Error && error.message === 'TIMEOUT') { setVideoStatus('error'); setErrorMessage(t('completion.generationTimeout')); } else { setVideoStatus('error'); setErrorMessage(error instanceof Error ? error.message : t('completion.generationError')); } hasStartedGeneration.current = false; clearStorage(); } }; useEffect(() => { if (!songTaskId) return; const completeVideo = loadCompleteVideo(); if (completeVideo && completeVideo.songTaskId === songTaskId && completeVideo.videoUrl) { setVideoUrl(completeVideo.videoUrl); if (completeVideo.videoDbId) setVideoDbId(completeVideo.videoDbId); setVideoStatus('complete'); hasStartedGeneration.current = true; return; } const savedState = loadFromStorage(); if (savedState && savedState.songTaskId === songTaskId) { if (savedState.status === 'complete' && savedState.videoUrl) { setVideoUrl(savedState.videoUrl); if (savedState.videoDbId) setVideoDbId(savedState.videoDbId); setVideoStatus('complete'); hasStartedGeneration.current = true; } else if (savedState.status === 'polling') { setVideoStatus('polling'); setStatusMessage(t('completion.processingAfterRefresh')); hasStartedGeneration.current = true; pollVideoStatus(savedState.videoTaskId, savedState.songTaskId); } } else if (!hasStartedGeneration.current) { startVideoGeneration(); } }, [songTaskId]); // 완료 데이터 로드 useEffect(() => { try { const saved = localStorage.getItem('castad_song_completion'); if (saved) { const data = JSON.parse(saved); setSongCompletionData(data); } } catch (error) { console.error('Failed to load song completion data:', error); } // 비디오 비율 로드 const savedRatio = localStorage.getItem('castad_video_ratio'); if (savedRatio === 'horizontal' || savedRatio === 'vertical') { setVideoRatio(savedRatio); } }, []); const handleDownload = () => { if (videoUrl) { const link = document.createElement('a'); link.href = videoUrl; link.download = 'castad_video.mp4'; link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }; const handleOpenSocialConnect = () => { setShowSocialModal(true); }; const handleCloseSocialConnect = () => { setShowSocialModal(false); }; const handleRetry = () => { hasStartedGeneration.current = false; setVideoStatus('idle'); setVideoUrl(null); setErrorMessage(null); clearStorage(); startVideoGeneration(); }; const isLoading = videoStatus === 'generating' || videoStatus === 'polling'; // 비디오 해상도 계산 const getVideoResolution = () => { const savedRatio = localStorage.getItem('castad_video_ratio'); return savedRatio === 'horizontal' ? '1280×720' : '720×1280'; }; // 파일명 생성 const getFileName = () => { const businessName = songCompletionData?.businessName || '콘텐츠'; return `${businessName}.mp4`; }; return (

{t('completion.contentComplete', { defaultValue: '콘텐츠 제작 완료' })}

{t('completion.contentCompleteDesc', { defaultValue: 'AI 분석 및 편집을 통해 최적화된 콘텐츠가 완성되었습니다' })}

{/* 왼쪽: 영상 */}
{isLoading ? (

{statusMessage}

) : videoStatus === 'error' ? (

{errorMessage}

) : videoUrl ? (
{/* 오른쪽: 콘텐츠 정보 */}
{t('completion.contentInfo', { defaultValue: '콘텐츠 정보' })}

{getFileName()}

{/*

19.6MB

*/}
{t('completion.genre', { defaultValue: '장르' })} : {songCompletionData?.genre || 'K-POP'}
{t('completion.resolution', { defaultValue: '규격' })} : {getVideoResolution()}
{t('completion.lyrics', { defaultValue: '가사' })}

{songCompletionData?.lyrics || t('completion.sampleLyrics', { defaultValue: '가사를 불러오는 중...' })}

{/* 하단 버튼 */}
{/* 소셜 미디어 포스팅 모달 (기존 SocialPostingModal 컴포넌트 사용) */} {tutorial.isActive && ( )}
); }; export default CompletionContent;