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:
55
src/pages/music/components/StemModal.jsx
Normal file
55
src/pages/music/components/StemModal.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const STEM_ICONS = {
|
||||
vocal: '🎤', backing_vocals: '🎶', drums: '🥁', bass: '🎸',
|
||||
guitar: '🎸', keyboard: '🎹', strings: '🎻', brass: '🎺',
|
||||
woodwinds: '🪈', percussion: '🪘', synth: '🎛', fx: '✨',
|
||||
};
|
||||
|
||||
const StemModal = ({ stems, onClose }) => {
|
||||
const [playingStem, setPlayingStem] = useState(null);
|
||||
|
||||
if (!stems || Object.keys(stems).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="ms-modal-overlay" onClick={onClose}>
|
||||
<div className="ms-modal ms-modal--wide" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="ms-modal__header">
|
||||
<h3 className="ms-modal__title">12 Stems</h3>
|
||||
<span className="ms-modal__subtitle">각 스템을 개별 재생 및 다운로드할 수 있습니다</span>
|
||||
<button type="button" className="ms-modal__close" onClick={onClose}>✕</button>
|
||||
</div>
|
||||
<div className="ms-stem-grid">
|
||||
{Object.entries(stems).map(([name, url]) => {
|
||||
if (!url) return null;
|
||||
const isPlaying = playingStem === name;
|
||||
return (
|
||||
<div key={name} className={`ms-stem-card ${isPlaying ? 'is-playing' : ''}`}>
|
||||
<span className="ms-stem-card__icon">{STEM_ICONS[name] || '🎵'}</span>
|
||||
<span className="ms-stem-card__name">{name.replace(/_/g, ' ')}</span>
|
||||
<div className="ms-stem-card__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ms-btn--icon"
|
||||
onClick={() => setPlayingStem(isPlaying ? null : name)}
|
||||
>
|
||||
{isPlaying ? '■' : '▶'}
|
||||
</button>
|
||||
<a href={url} download className="ms-btn--icon" aria-label="다운로드">↓</a>
|
||||
</div>
|
||||
{isPlaying && (
|
||||
<audio src={url} autoPlay onEnded={() => setPlayingStem(null)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="ms-modal__actions">
|
||||
<button type="button" className="ms-btn ms-btn--ghost" onClick={onClose}>닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StemModal;
|
||||
51
src/pages/music/components/SyncedLyricsPlayer.jsx
Normal file
51
src/pages/music/components/SyncedLyricsPlayer.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user