import React, { useState, useRef, useEffect } from 'react'; import { generateLyric, generateSong, waitForSongComplete } from '../../utils/api'; import { LANGUAGE_MAP } from '../../types/api'; interface BusinessInfo { customer_name: string; region: string; detail_region_info: string; } interface SoundStudioContentProps { onBack: () => void; onNext: () => void; businessInfo?: BusinessInfo; } type GenerationStatus = 'idle' | 'generating_lyric' | 'generating_song' | 'polling' | 'complete' | 'error'; // localStorage에 저장할 데이터 구조 interface SavedGenerationState { taskId: string; lyrics: string; status: GenerationStatus; timestamp: number; } const STORAGE_KEY = 'castad_song_generation'; const STORAGE_EXPIRY = 30 * 60 * 1000; // 30분 const MAX_RETRY_COUNT = 3; // 최대 재시도 횟수 const SoundStudioContent: React.FC = ({ onBack, onNext, businessInfo }) => { const [selectedType, setSelectedType] = useState('보컬'); const [selectedLang, setSelectedLang] = useState('한국어'); const [selectedGenre, setSelectedGenre] = useState('AI 추천'); const [progress, setProgress] = useState(0); const [isDragging, setIsDragging] = useState(false); const [status, setStatus] = useState('idle'); const [showLyrics, setShowLyrics] = useState(false); const [lyrics, setLyrics] = useState(''); const [audioUrl, setAudioUrl] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [errorMessage, setErrorMessage] = useState(null); const [statusMessage, setStatusMessage] = useState(''); const [retryCount, setRetryCount] = useState(0); const progressBarRef = useRef(null); const audioRef = useRef(null); // localStorage에 상태 저장 const saveToStorage = (taskId: string, currentLyrics: string, currentStatus: GenerationStatus) => { const data: SavedGenerationState = { taskId, lyrics: currentLyrics, status: currentStatus, timestamp: Date.now(), }; localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); }; // localStorage에서 상태 제거 const clearStorage = () => { localStorage.removeItem(STORAGE_KEY); }; // localStorage에서 상태 복구 const loadFromStorage = (): SavedGenerationState | null => { try { const saved = localStorage.getItem(STORAGE_KEY); if (!saved) return null; const data: SavedGenerationState = JSON.parse(saved); // 만료된 데이터인지 확인 (30분) if (Date.now() - data.timestamp > STORAGE_EXPIRY) { clearStorage(); return null; } return data; } catch { clearStorage(); return null; } }; // 컴포넌트 마운트 시 저장된 상태 복구 useEffect(() => { const savedState = loadFromStorage(); if (savedState && (savedState.status === 'polling' || savedState.status === 'generating_song')) { // 저장된 가사가 있으면 표시 if (savedState.lyrics) { setLyrics(savedState.lyrics); setShowLyrics(true); } // 폴링 상태 복구 setStatus('polling'); setStatusMessage('음악을 처리하고 있습니다... (새로고침 후 복구됨)'); // 폴링 재개 (저장된 가사와 함께) resumePolling(savedState.taskId, savedState.lyrics, 0); } }, []); // 폴링 재개 함수 (타임아웃 시 재생성) const resumePolling = async (taskId: string, currentLyrics: string, currentRetryCount: number = 0) => { try { const downloadResponse = await waitForSongComplete( taskId, (pollStatus) => { if (pollStatus === 'pending') { setStatusMessage('대기 중...'); } else if (pollStatus === 'processing') { setStatusMessage('음악을 처리하고 있습니다...'); } } ); if (!downloadResponse.success) { throw new Error(downloadResponse.error_message || '음악 다운로드에 실패했습니다.'); } setAudioUrl(downloadResponse.file_url); setStatus('complete'); setStatusMessage(''); setRetryCount(0); clearStorage(); } catch (error) { console.error('Polling failed:', error); // 타임아웃인 경우 재생성 시도 if (error instanceof Error && error.message === 'TIMEOUT') { if (currentRetryCount < MAX_RETRY_COUNT) { const newRetryCount = currentRetryCount + 1; setRetryCount(newRetryCount); setStatusMessage(`시간 초과로 재생성 중... (${newRetryCount}/${MAX_RETRY_COUNT})`); // 새로운 노래 생성 요청 await regenerateSongOnly(currentLyrics, newRetryCount); } else { setStatus('error'); setErrorMessage('여러 번 시도했지만 음악 생성에 실패했습니다. 다시 시도해주세요.'); setRetryCount(0); clearStorage(); } } else { setStatus('error'); setErrorMessage(error instanceof Error ? error.message : '음악 생성 중 오류가 발생했습니다.'); setRetryCount(0); clearStorage(); } } }; // 노래만 재생성 (가사는 유지) const regenerateSongOnly = async (currentLyrics: string, currentRetryCount: number) => { if (!businessInfo) return; try { const language = LANGUAGE_MAP[selectedLang] || 'Korean'; const genreMap: Record = { 'AI 추천': 'pop', '로파이': 'lofi', '힙합': 'hip-hop', '어쿠스틱': 'acoustic', }; const songResponse = await generateSong({ genre: genreMap[selectedGenre] || 'pop', language, lyrics: currentLyrics, }); if (!songResponse.success) { throw new Error(songResponse.error_message || '음악 생성 요청에 실패했습니다.'); } // 새 task_id로 저장 saveToStorage(songResponse.task_id, currentLyrics, 'polling'); // 폴링 재개 await resumePolling(songResponse.task_id, currentLyrics, currentRetryCount); } catch (error) { console.error('Song regeneration failed:', error); setStatus('error'); setErrorMessage(error instanceof Error ? error.message : '음악 재생성 중 오류가 발생했습니다.'); setRetryCount(0); clearStorage(); } }; const handleMove = (clientX: number) => { if (!progressBarRef.current || !audioRef.current) return; const rect = progressBarRef.current.getBoundingClientRect(); const newProgress = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100)); const newTime = (newProgress / 100) * duration; audioRef.current.currentTime = newTime; setProgress(newProgress); setCurrentTime(newTime); }; const onMouseDown = (e: React.MouseEvent) => { if (!audioUrl) return; setIsDragging(true); handleMove(e.clientX); }; useEffect(() => { const onMouseMove = (e: MouseEvent) => { if (isDragging) handleMove(e.clientX); }; const onMouseUp = () => { setIsDragging(false); }; if (isDragging) { window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); } return () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; }, [isDragging]); useEffect(() => { const audio = audioRef.current; if (!audio) return; const handleTimeUpdate = () => { setCurrentTime(audio.currentTime); if (audio.duration > 0) { setProgress((audio.currentTime / audio.duration) * 100); } }; const handleLoadedMetadata = () => { setDuration(audio.duration); }; const handleEnded = () => { setIsPlaying(false); setProgress(0); setCurrentTime(0); }; audio.addEventListener('timeupdate', handleTimeUpdate); audio.addEventListener('loadedmetadata', handleLoadedMetadata); audio.addEventListener('ended', handleEnded); return () => { audio.removeEventListener('timeupdate', handleTimeUpdate); audio.removeEventListener('loadedmetadata', handleLoadedMetadata); audio.removeEventListener('ended', handleEnded); }; }, [audioUrl]); const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; const togglePlayPause = () => { if (!audioRef.current) return; if (isPlaying) { audioRef.current.pause(); } else { audioRef.current.play(); } setIsPlaying(!isPlaying); }; const handleGenerateMusic = async () => { if (!businessInfo) { setErrorMessage('비즈니스 정보가 없습니다. 다시 시도해주세요.'); return; } setStatus('generating_lyric'); setErrorMessage(null); setStatusMessage('가사를 생성하고 있습니다...'); try { // Step 1: Generate lyrics const language = LANGUAGE_MAP[selectedLang] || 'Korean'; let lyricResponse = await generateLyric({ customer_name: businessInfo.customer_name, detail_region_info: businessInfo.detail_region_info, language, region: businessInfo.region, }); // Retry if the response contains error message let retryCount = 0; while (lyricResponse.lyric.includes("I'm sorry") && retryCount < 3) { retryCount++; setStatusMessage(`가사 재생성 중... (${retryCount}/3)`); lyricResponse = await generateLyric({ customer_name: businessInfo.customer_name, detail_region_info: businessInfo.detail_region_info, language, region: businessInfo.region, }); } if (!lyricResponse.success) { throw new Error(lyricResponse.error_message || '가사 생성에 실패했습니다.'); } // 3번 재시도 후에도 여전히 에러 메시지가 포함되어 있으면 가사 카드를 표시하지 않고 에러 처리 if (lyricResponse.lyric.includes("I'm sorry")) { throw new Error('가사 생성에 실패했습니다. 다시 시도해주세요.'); } setLyrics(lyricResponse.lyric); setShowLyrics(true); // Step 2: Generate song setStatus('generating_song'); setStatusMessage('음악을 생성하고 있습니다...'); const genreMap: Record = { 'AI 추천': 'pop', '로파이': 'lofi', '힙합': 'hip-hop', '어쿠스틱': 'acoustic', }; const songResponse = await generateSong({ genre: genreMap[selectedGenre] || 'pop', language, lyrics: lyricResponse.lyric, }); if (!songResponse.success) { throw new Error(songResponse.error_message || '음악 생성 요청에 실패했습니다.'); } // Step 3: Poll for completion - 상태 저장 setStatus('polling'); setStatusMessage('음악을 처리하고 있습니다...'); saveToStorage(songResponse.task_id, lyricResponse.lyric, 'polling'); // 폴링 시작 (타임아웃 시 자동 재생성) await resumePolling(songResponse.task_id, lyricResponse.lyric, 0); } catch (error) { console.error('Music generation failed:', error); setStatus('error'); setErrorMessage(error instanceof Error ? error.message : '음악 생성 중 오류가 발생했습니다.'); setRetryCount(0); clearStorage(); } }; const handleRegenerate = async () => { setShowLyrics(false); setAudioUrl(null); setLyrics(''); setProgress(0); setCurrentTime(0); setDuration(0); setIsPlaying(false); setRetryCount(0); clearStorage(); await handleGenerateMusic(); }; const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling'; return (
{/* Hidden audio element */} {audioUrl && (