feat(music-lab): Phase 3 UI — RemixTab + 뮤직비디오 생성

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 09:14:18 +09:00
parent 0849c70644
commit 1f00866694
4 changed files with 304 additions and 2 deletions

View File

@@ -14,6 +14,7 @@ import {
splitStems,
getTimestampedLyrics,
generateStyleBoost,
generateVideo,
} from '../../api';
import './MusicStudio.css';
import AudioPlayer from './components/AudioPlayer';
@@ -23,6 +24,7 @@ import CoverArtModal from './components/CoverArtModal';
import LyricsTab from './components/LyricsTab';
import StemModal from './components/StemModal';
import SyncedLyricsPlayer from './components/SyncedLyricsPlayer';
import RemixTab from './components/RemixTab';
/* ─────────────────────────────────────────────
데이터 상수
@@ -333,7 +335,7 @@ const TrackResult = ({ track, onDownload, onNew }) => {
/* ─────────────────────────────────────────────
Library Card
───────────────────────────────────────────── */
const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, isGenerating }) => {
const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, isGenerating }) => {
const [menuOpen, setMenuOpen] = useState(false);
const genre = GENRES.find((g) => g.id === track.genre);
const totalSec = track.duration_sec ?? null;
@@ -419,6 +421,8 @@ const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemo
disabled={isGenerating}>🎛 12 Stems (50cr)</button>
<button type="button" onClick={() => { onSyncedLyrics(track); setMenuOpen(false); }}
disabled={isGenerating || !track.lyrics}>📝 Synced Lyrics</button>
<button type="button" onClick={() => { onVideoGenerate(track); setMenuOpen(false); }}
disabled={isGenerating}>🎬 Music Video</button>
</div>
)}
</div>
@@ -441,7 +445,7 @@ const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemo
/* ─────────────────────────────────────────────
Library Section
───────────────────────────────────────────── */
const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, isGenerating, loading }) => {
const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, isGenerating, loading }) => {
const [playingId, setPlayingId] = useState(null);
const handlePlay = (track) => {
@@ -494,6 +498,7 @@ const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCove
onWavConvert={onWavConvert}
onStemSplit={onStemSplit}
onSyncedLyrics={onSyncedLyrics}
onVideoGenerate={onVideoGenerate}
isGenerating={isGenerating}
/>
))}
@@ -1026,6 +1031,31 @@ export default function MusicStudio() {
finally { setStyleBoostLoading(false); }
};
/* ── 뮤직비디오 핸들러 ── */
const handleVideoGenerate = async (track) => {
if (!track.task_id || !track.suno_id || isGenerating) return;
setTab('create');
setIsGenerating(true);
setTrack(null);
setGenProgress(0);
setGenStep('뮤직비디오 생성 요청 중…');
setGenError(null);
try {
const res = await generateVideo({
suno_task_id: track.task_id,
suno_id: track.suno_id,
track_id: track.id,
});
if (res?.task_id) {
taskIdRef.current = res.task_id;
startPolling(res.task_id, `${track.title} (Video)`);
}
} catch {
setIsGenerating(false);
setGenError('뮤직비디오 생성에 실패했습니다');
}
};
const handleNewTrack = () => {
setTrack(null);
setGenProgress(0);
@@ -1082,6 +1112,13 @@ export default function MusicStudio() {
<span className="ms-tab__badge">{library.length}</span>
)}
</button>
<button
type="button"
className={`ms-tab ${tab === 'remix' ? 'is-active' : ''}`}
onClick={() => setTab('remix')}
>
<span className="ms-tab__icon">🔄</span> Remix
</button>
</nav>
{/* ═══ LIBRARY TAB ═══ */}
@@ -1097,6 +1134,7 @@ export default function MusicStudio() {
onWavConvert={handleWavConvert}
onStemSplit={handleStemSplit}
onSyncedLyrics={handleSyncedLyrics}
onVideoGenerate={handleVideoGenerate}
isGenerating={isGenerating}
/>
)}
@@ -1106,6 +1144,24 @@ export default function MusicStudio() {
<LyricsTab onUseInCreate={(text) => { setLyrics(text); setInstrumental(false); setProvider('suno'); setTab('create'); }} />
)}
{/* ═══ REMIX TAB ═══ */}
{tab === 'remix' && (
<RemixTab
onTaskStarted={(taskId, title) => {
setTab('create');
setIsGenerating(true);
setTrack(null);
setGenProgress(0);
setGenStep(`${title} 처리 중…`);
setGenError(null);
taskIdRef.current = taskId;
startPolling(taskId, title);
}}
model={model}
isGenerating={isGenerating}
/>
)}
{/* ═══ CREATE TAB ═══ */}
{tab === 'create' && (
<div className="ms-layout">