import React, { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { generateLyric, waitForLyricComplete, 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: (songTaskId: string) => void; businessInfo?: BusinessInfo; imageTaskId: string | null; mId: number; onStatusChange?: (status: string) => void; videoGenerationStatus?: 'idle' | 'generating' | 'complete' | 'error'; videoGenerationProgress?: number; } type GenerationStatus = 'idle' | 'generating_lyric' | 'generating_song' | 'polling' | 'complete' | 'error'; const MAX_RETRY_COUNT = 3; const LANGUAGE_FLAGS: Record = { 'ํ•œ๊ตญ์–ด': '๐Ÿ‡ฐ๐Ÿ‡ท', 'English': '๐Ÿ‡บ๐Ÿ‡ธ', 'ไธญๆ–‡': '๐Ÿ‡จ๐Ÿ‡ณ', 'ๆ—ฅๆœฌ่ชž': '๐Ÿ‡ฏ๐Ÿ‡ต', 'เน„เธ—เธข': '๐Ÿ‡น๐Ÿ‡ญ', 'Tiแบฟng Viแป‡t': '๐Ÿ‡ป๐Ÿ‡ณ', }; const SoundStudioContent: React.FC = ({ onBack, onNext, businessInfo, imageTaskId, mId, onStatusChange, videoGenerationStatus = 'idle', videoGenerationProgress = 0 }) => { const { t } = useTranslation(); const [selectedType, setSelectedType] = useState('๋ณด์ปฌ'); const [selectedLang, setSelectedLang] = useState('ํ•œ๊ตญ์–ด'); const [selectedGenre, setSelectedGenre] = useState('์ž๋™ ์„ ํƒ'); const [progress, setProgress] = useState(0); const [isDragging, setIsDragging] = useState(false); const [status, setStatus] = useState('idle'); 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 [songTaskId, setSongTaskId] = useState(null); const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false); const progressBarRef = useRef(null); const audioRef = useRef(null); const languageDropdownRef = useRef(null); // ์™„๋ฃŒ ๋ฐ์ดํ„ฐ๋ฅผ localStorage์— ์ €์žฅ const saveSongCompletionData = () => { const completionData = { businessName: businessInfo?.customer_name || '์•Œ ์ˆ˜ ์—†์Œ', genre: selectedGenre === '์ž๋™ ์„ ํƒ' ? 'K-POP' : selectedGenre, lyrics: lyrics, timestamp: Date.now(), }; localStorage.setItem('castad_song_completion', JSON.stringify(completionData)); }; // Close language dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target as Node)) { setIsLanguageDropdownOpen(false); } }; if (isLanguageDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isLanguageDropdownOpen]); // Auto-navigate to next page when video generation is complete useEffect(() => { if (videoGenerationStatus === 'complete' && songTaskId) { // Save completion data before navigating saveSongCompletionData(); // Wait a brief moment to show 100% completion before navigating const timer = setTimeout(() => { onNext(songTaskId); }, 500); return () => clearTimeout(timer); } }, [videoGenerationStatus, songTaskId, onNext]); const resumePolling = async (taskId: string, songId: string, currentLyrics: string, currentRetryCount: number = 0) => { try { const statusResponse = await waitForSongComplete( songId, (pollStatus: string) => { if (pollStatus === 'streaming') { setStatusMessage(t('soundStudio.generatingSong')); } else if (pollStatus === 'queued') { setStatusMessage(t('soundStudio.songQueued')); } } ); if (!statusResponse.success) { throw new Error(statusResponse.error_message || t('soundStudio.musicGenerationFailed')); } // song_result_url์„ ์‚ฌ์šฉํ•˜์—ฌ ์žฌ์ƒ setSongTaskId(taskId); setAudioUrl(statusResponse.song_result_url); setStatus('complete'); setStatusMessage(''); setRetryCount(0); } 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(t('soundStudio.retryMessage', { count: newRetryCount, max: MAX_RETRY_COUNT })); await regenerateSongOnly(currentLyrics, newRetryCount); } else { setStatus('error'); setErrorMessage(t('soundStudio.multipleRetryFailed')); setRetryCount(0); } } else { setStatus('error'); setErrorMessage(error instanceof Error ? error.message : t('soundStudio.musicGenerationError')); setRetryCount(0); } } }; const regenerateSongOnly = async (currentLyrics: string, currentRetryCount: number) => { if (!businessInfo || !imageTaskId) return; try { const language = LANGUAGE_MAP[selectedLang] || 'Korean'; const genreMap: Record = { '์ž๋™ ์„ ํƒ': 'pop', 'K-POP': 'kpop', '๋ฐœ๋ผ๋“œ': 'ballad', 'Hip-Hop': 'hip-hop', 'R&B': 'rnb', 'EDM': 'edm', 'JAZZ': 'jazz', 'ROCK': 'rock', }; const songResponse = await generateSong(imageTaskId, { genre: genreMap[selectedGenre] || 'pop', language, lyrics: currentLyrics, }); if (!songResponse.success) { throw new Error(songResponse.error_message || t('soundStudio.songGenerationFailed')); } await resumePolling(songResponse.task_id, songResponse.song_id, currentLyrics, currentRetryCount); } catch (error) { console.error('Song regeneration failed:', error); setStatus('error'); setErrorMessage(error instanceof Error ? error.message : t('soundStudio.songRegenerationError')); setRetryCount(0); } }; 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(t('soundStudio.noBusinessInfo')); return; } if (!imageTaskId) { setErrorMessage(t('soundStudio.noImageUploadInfo')); return; } setStatus('generating_lyric'); setErrorMessage(null); setStatusMessage(t('soundStudio.generatingLyrics')); const savedRatio = localStorage.getItem('castad_video_ratio'); const orientation = (savedRatio === 'horizontal' || savedRatio === 'vertical') ? savedRatio : 'vertical'; try { const language = LANGUAGE_MAP[selectedLang] || 'Korean'; // 1. ๊ฐ€์‚ฌ ์ƒ์„ฑ ์š”์ฒญ console.log('[SoundStudio] Sending m_id:', mId); const lyricResponse = await generateLyric({ customer_name: businessInfo.customer_name, detail_region_info: businessInfo.detail_region_info, language, m_id: mId, region: businessInfo.region, task_id: imageTaskId, orientation, }); if (!lyricResponse.success || !lyricResponse.task_id) { throw new Error(lyricResponse.error_message || t('soundStudio.lyricGenerationFailed')); } // 2. ๊ฐ€์‚ฌ ์ƒ์„ฑ ์ƒํƒœ ํด๋ง โ†’ ์™„๋ฃŒ ์‹œ ์ƒ์„ธ ์กฐํšŒ setStatusMessage(t('soundStudio.generatingLyrics')); const lyricDetailResponse = await waitForLyricComplete( lyricResponse.task_id, (status: string) => { if (status === 'processing') { setStatusMessage(t('soundStudio.generatingLyrics')); } } ); if (!lyricDetailResponse.lyric_result) { throw new Error(t('soundStudio.lyricNotReceived')); } // "I'm sorry" ์ฒดํฌ if (lyricDetailResponse.lyric_result.includes("I'm sorry")) { throw new Error(t('soundStudio.lyricGenerationError')); } setLyrics(lyricDetailResponse.lyric_result); setStatus('generating_song'); setStatusMessage(t('soundStudio.generatingSong')); const genreMap: Record = { '์ž๋™ ์„ ํƒ': 'pop', 'K-POP': 'kpop', '๋ฐœ๋ผ๋“œ': 'ballad', 'Hip-Hop': 'hip-hop', 'R&B': 'rnb', 'EDM': 'edm', 'JAZZ': 'jazz', 'ROCK': 'rock', }; const songResponse = await generateSong(imageTaskId, { genre: genreMap[selectedGenre] || 'pop', language, lyrics: lyricDetailResponse.lyric_result, }); if (!songResponse.success) { throw new Error(songResponse.error_message || t('soundStudio.songGenerationFailed')); } // ๋””๋ฒ„๊น…: songResponse ํ™•์ธ console.log('songResponse:', songResponse); console.log('song_id:', songResponse.song_id); console.log('task_id:', songResponse.task_id); if (!songResponse.song_id) { throw new Error(t('soundStudio.songIdMissing')); } setStatus('polling'); setStatusMessage(t('soundStudio.generatingSong')); await resumePolling(songResponse.task_id, songResponse.song_id, lyricDetailResponse.lyric_result, 0); } catch (error) { console.error('Music generation failed:', error); setStatus('error'); setErrorMessage(error instanceof Error ? error.message : t('soundStudio.musicGenerationError')); setRetryCount(0); } }; const handleRegenerate = async () => { setAudioUrl(null); setLyrics(''); setProgress(0); setCurrentTime(0); setDuration(0); setIsPlaying(false); setRetryCount(0); await handleGenerateMusic(); }; const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling'; // status ๋ณ€๊ฒฝ ์‹œ ๋ถ€๋ชจ์— ์•Œ๋ฆผ useEffect(() => { onStatusChange?.(status); }, [status]); return ( <>
{audioUrl && (