feat(music-lab): Phase 2 UI — StemModal, SyncedLyricsPlayer, Style Boost, WAV 변환

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 09:05:07 +09:00
parent 7a591bb0f1
commit 0849c70644
5 changed files with 349 additions and 2 deletions

View File

@@ -0,0 +1,51 @@
import React, { useEffect, useRef, useState } from 'react';
const SyncedLyricsPlayer = ({ audioUrl, alignedWords, onClose, accentColor }) => {
const audioRef = useRef(null);
const [playing, setPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
useEffect(() => {
const el = audioRef.current;
if (!el) return;
const handler = () => setCurrentTime(el.currentTime);
el.addEventListener('timeupdate', handler);
return () => el.removeEventListener('timeupdate', handler);
}, []);
if (!alignedWords || alignedWords.length === 0) return null;
return (
<div className="ms-synced-player" style={{ '--synced-accent': accentColor }}>
<div className="ms-synced-player__header">
<h4 className="ms-synced-player__title">Synced Lyrics</h4>
<button type="button" className="ms-modal__close" onClick={onClose}></button>
</div>
<audio
ref={audioRef}
src={audioUrl}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onEnded={() => setPlaying(false)}
controls
className="ms-synced-player__audio"
/>
<div className="ms-synced-player__lyrics">
{alignedWords.map((word, idx) => {
const isActive = currentTime >= word.startS && currentTime < word.endS;
const isPast = currentTime >= word.endS;
return (
<span
key={idx}
className={`ms-synced-word ${isActive ? 'is-active' : ''} ${isPast ? 'is-past' : ''}`}
>
{word.word}{' '}
</span>
);
})}
</div>
</div>
);
};
export default SyncedLyricsPlayer;