diff --git a/src/api.js b/src/api.js index 9de8f3f..6bedae5 100644 --- a/src/api.js +++ b/src/api.js @@ -334,6 +334,62 @@ export function deleteLyrics(id) { return apiDelete(`/api/music/lyrics/library/${id}`); } +// ── Phase 1: 커버 이미지 ──────────────────────────────────────────────────── + +// POST /api/music/cover-image body: { suno_task_id, track_id } +export function generateCoverImage(payload) { + return apiPost('/api/music/cover-image', payload); +} + +// ── Phase 2 API ───────────────────────────────────────────────────────────── + +// POST /api/music/wav body: { suno_task_id, suno_id, track_id } +export function convertToWav(payload) { + return apiPost('/api/music/wav', payload); +} + +// POST /api/music/stem-split body: { suno_task_id, suno_id, track_id } +export function splitStems(payload) { + return apiPost('/api/music/stem-split', payload); +} + +// GET /api/music/timestamped-lyrics?task_id=...&suno_id=... +export function getTimestampedLyrics(taskId, sunoId) { + return apiGet(`/api/music/timestamped-lyrics?task_id=${encodeURIComponent(taskId)}&suno_id=${encodeURIComponent(sunoId)}`); +} + +// POST /api/music/style-boost body: { content } +export function generateStyleBoost(content) { + return apiPost('/api/music/style-boost', { content }); +} + +// ── Phase 3 API ───────────────────────────────────────────────────────────── + +// POST /api/music/upload-cover +export function uploadAndCover(payload) { + return apiPost('/api/music/upload-cover', payload); +} + +// POST /api/music/upload-extend +export function uploadAndExtend(payload) { + return apiPost('/api/music/upload-extend', payload); +} + +// POST /api/music/add-vocals +export function addVocals(payload) { + return apiPost('/api/music/add-vocals', payload); +} + +// POST /api/music/add-instrumental +export function addInstrumental(payload) { + return apiPost('/api/music/add-instrumental', payload); +} + +// POST /api/music/video +export function generateVideo(payload) { + return apiPost('/api/music/video', payload); +} + // ── 로또 고도화 API ──────────────────────────────────────────────────────────── // GET /api/lotto/stats/performance diff --git a/src/pages/music/MusicStudio.css b/src/pages/music/MusicStudio.css index 89df6ed..1f2f59a 100644 --- a/src/pages/music/MusicStudio.css +++ b/src/pages/music/MusicStudio.css @@ -2426,3 +2426,173 @@ animation: none !important; } } + +/* ── Phase 1: Credits Badge ─────────────────────────────── */ +.ms-credits-badge { + display: inline-flex; align-items: center; gap: 6px; + padding: 6px 14px; border-radius: 20px; + background: rgba(245, 166, 35, 0.1); + border: 1px solid rgba(245, 166, 35, 0.25); + font-family: 'Courier Prime', monospace; + font-size: 0.85rem; color: var(--ms-accent); +} +.ms-credits-badge__icon { font-size: 1rem; } +.ms-credits-badge__value { font-weight: 700; font-size: 1.1rem; } +.ms-credits-badge__label { color: var(--ms-muted); font-size: 0.75rem; text-transform: uppercase; } +.ms-credits-badge.is-low { + background: rgba(231, 76, 60, 0.15); + border-color: rgba(231, 76, 60, 0.4); + color: #e74c3c; + animation: pulse-badge 1.5s ease-in-out infinite; +} +@keyframes pulse-badge { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +/* ── Phase 1: Vocal Gender Toggle ───────────────────────── */ +.ms-gender-toggle { display: flex; gap: 6px; } +.ms-gender-btn { + flex: 1; padding: 8px 12px; border-radius: 8px; + background: var(--ms-surface); border: 1px solid var(--ms-line); + color: var(--ms-muted); font-family: 'Syne', sans-serif; + font-size: 0.82rem; cursor: pointer; transition: all 0.2s; + display: flex; align-items: center; gap: 6px; justify-content: center; +} +.ms-gender-btn:hover { border-color: var(--ms-accent); color: var(--ms-text); } +.ms-gender-btn.is-active { background: rgba(245, 166, 35, 0.12); border-color: var(--ms-accent); color: var(--ms-text); } +.ms-gender-btn.is-active.is-male { background: rgba(74, 158, 255, 0.12); border-color: #4a9eff; color: #4a9eff; } +.ms-gender-btn.is-active.is-female { background: rgba(255, 107, 157, 0.12); border-color: #ff6b9d; color: #ff6b9d; } +.ms-gender-btn__icon { font-size: 1.1rem; } + +/* ── Phase 1: Negative Tags ─────────────────────────────── */ +.ms-negative-tags { display: flex; flex-direction: column; gap: 8px; } +.ms-negative-tags__presets { display: flex; flex-wrap: wrap; gap: 6px; } +.ms-neg-chip { + padding: 4px 12px; border-radius: 14px; + background: var(--ms-surface); border: 1px solid var(--ms-line); + color: var(--ms-muted); font-size: 0.78rem; cursor: pointer; + font-family: 'Syne', sans-serif; transition: all 0.2s; +} +.ms-neg-chip:hover { border-color: #e74c3c; color: var(--ms-text); } +.ms-neg-chip.is-active { + background: rgba(231, 76, 60, 0.12); border-color: #e74c3c; color: #e74c3c; + text-decoration: line-through; +} +.ms-negative-tags__input { + padding: 8px 12px; border-radius: 8px; + background: var(--ms-surface); border: 1px solid var(--ms-line); + color: var(--ms-text); font-family: 'Syne', sans-serif; font-size: 0.82rem; +} +.ms-negative-tags__input::placeholder { color: var(--ms-dim); } +.ms-param-hint--inline { + font-size: 0.72rem; color: var(--ms-dim); margin: 0 0 4px; + font-family: 'Courier Prime', monospace; +} + +/* ── More Menu ──────────────────────────────────────────── */ +.ms-more-menu { position: relative; } +.ms-more-menu__dropdown { + position: absolute; bottom: 100%; right: 0; + background: var(--ms-surface2); border: 1px solid var(--ms-line); + border-radius: 8px; padding: 4px; min-width: 160px; z-index: 20; + box-shadow: 0 8px 24px rgba(0,0,0,0.4); +} +.ms-more-menu__dropdown button { + display: block; width: 100%; padding: 8px 12px; border: none; + background: none; color: var(--ms-text); font-size: 0.82rem; + font-family: 'Syne', sans-serif; cursor: pointer; text-align: left; + border-radius: 6px; +} +.ms-more-menu__dropdown button:hover { background: rgba(245,166,35,0.1); } +.ms-more-menu__dropdown button:disabled { opacity: 0.4; cursor: not-allowed; } + +/* ── Modal ──────────────────────────────────────────────── */ +.ms-modal-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.7); + display: flex; align-items: center; justify-content: center; z-index: 100; +} +.ms-modal { + background: var(--ms-surface); border: 1px solid var(--ms-line); + border-radius: 16px; padding: 24px; max-width: 520px; width: 90%; + max-height: 90vh; overflow-y: auto; +} +.ms-modal__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } +.ms-modal__title { font-family: 'Bebas Neue', sans-serif; font-size: 1.3rem; color: var(--ms-text); } +.ms-modal__close { background: none; border: none; color: var(--ms-muted); font-size: 1.2rem; cursor: pointer; } +.ms-modal__actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; } + +/* ── Cover Art Grid ─────────────────────────────────────── */ +.ms-cover-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.ms-cover-option { + border: 2px solid var(--ms-line); border-radius: 12px; overflow: hidden; + cursor: pointer; background: none; padding: 0; transition: border-color 0.2s; +} +.ms-cover-option:hover { border-color: var(--ms-accent); } +.ms-cover-option.is-selected { border-color: var(--ms-accent); box-shadow: 0 0 12px rgba(245,166,35,0.3); } +.ms-cover-option__img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; } +.ms-cover-option__label { + display: block; padding: 8px; text-align: center; + font-family: 'Courier Prime', monospace; font-size: 0.78rem; color: var(--ms-muted); +} + +/* ── Stem Modal ─────────────────────────────────────────── */ +.ms-modal--wide { max-width: 680px; } +.ms-modal__subtitle { font-size: 0.78rem; color: var(--ms-muted); font-family: 'Courier Prime', monospace; } +.ms-stem-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } +.ms-stem-card { + display: flex; flex-direction: column; align-items: center; gap: 6px; + padding: 12px 8px; border-radius: 10px; + background: var(--ms-surface2); border: 1px solid var(--ms-line); + transition: border-color 0.2s; +} +.ms-stem-card.is-playing { border-color: var(--ms-accent); background: rgba(245,166,35,0.08); } +.ms-stem-card__icon { font-size: 1.4rem; } +.ms-stem-card__name { + font-family: 'Courier Prime', monospace; font-size: 0.72rem; + color: var(--ms-muted); text-transform: capitalize; +} +.ms-stem-card__actions { display: flex; gap: 6px; } + +/* ── Synced Lyrics Player ───────────────────────────────── */ +.ms-synced-player { + background: var(--ms-surface); border: 1px solid var(--ms-line); + border-radius: 12px; padding: 16px; margin-top: 12px; +} +.ms-synced-player__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } +.ms-synced-player__title { font-family: 'Bebas Neue', sans-serif; font-size: 1.1rem; color: var(--ms-text); } +.ms-synced-player__audio { width: 100%; margin-bottom: 12px; } +.ms-synced-player__lyrics { line-height: 1.8; font-family: 'Syne', sans-serif; font-size: 0.95rem; } +.ms-synced-word { color: var(--ms-dim); transition: color 0.15s; } +.ms-synced-word.is-active { color: var(--synced-accent, var(--ms-accent)); font-weight: 600; } +.ms-synced-word.is-past { color: var(--ms-muted); } + +/* ── Style Boost Button ─────────────────────────────────── */ +.ms-style-boost-btn { margin-left: auto; } +.ms-style-boost-btn.is-loading { opacity: 0.6; } + +/* ── Remix Tab ──────────────────────────────────────────── */ +.ms-remix-tab { display: flex; flex-direction: column; gap: 20px; } +.ms-remix-tab__header { } +.ms-remix-tab__title { font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; color: var(--ms-text); } +.ms-remix-tab__desc { font-size: 0.85rem; color: var(--ms-muted); } + +.ms-remix-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.ms-remix-card { + display: flex; flex-direction: column; align-items: center; gap: 6px; + padding: 20px 12px; border-radius: 12px; cursor: pointer; + background: var(--ms-surface); border: 1px solid var(--ms-line); + transition: all 0.2s; text-align: center; +} +.ms-remix-card:hover { border-color: var(--ms-accent); background: var(--ms-surface2); } +.ms-remix-card.is-active { border-color: var(--ms-accent); background: rgba(245,166,35,0.08); } +.ms-remix-card__icon { font-size: 2rem; } +.ms-remix-card__label { font-family: 'Bebas Neue', sans-serif; font-size: 1.1rem; color: var(--ms-text); } +.ms-remix-card__desc { font-size: 0.72rem; color: var(--ms-muted); font-family: 'Courier Prime', monospace; } + +.ms-remix-params { + display: flex; flex-direction: column; gap: 12px; + padding: 16px; border-radius: 12px; + background: var(--ms-surface); border: 1px solid var(--ms-line); +} +.ms-remix-submit { align-self: flex-start; margin-top: 8px; } diff --git a/src/pages/music/MusicStudio.jsx b/src/pages/music/MusicStudio.jsx index fb0d61d..8521672 100644 --- a/src/pages/music/MusicStudio.jsx +++ b/src/pages/music/MusicStudio.jsx @@ -7,15 +7,24 @@ import { getMusicProviders, getMusicStatus, getMusicModels, - getMusicCredits, extendMusicTrack, removeVocals, - getSavedLyrics, - saveLyrics, - updateLyrics, - deleteLyrics, + generateCoverImage, + convertToWav, + splitStems, + getTimestampedLyrics, + generateStyleBoost, + generateVideo, } from '../../api'; import './MusicStudio.css'; +import AudioPlayer from './components/AudioPlayer'; +import { fmtTime } from './components/AudioPlayer'; +import CreditsBadge from './components/CreditsBadge'; +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'; /* ───────────────────────────────────────────── 데이터 상수 @@ -83,12 +92,6 @@ const SIM_STEPS = [ { msg: 'Track ready!', pct: 100 }, ]; -/* ───────────────────────────────────────────── - 유틸 -───────────────────────────────────────────── */ -const pad = (n) => String(Math.floor(n)).padStart(2, '0'); -const fmtTime = (s) => `${pad(s / 60)}:${pad(s % 60)}`; - /* ───────────────────────────────────────────── Loading Skeleton ───────────────────────────────────────────── */ @@ -261,125 +264,6 @@ const GenerationProgress = ({ progress, stepMsg }) => ( ); -/* ───────────────────────────────────────────── - Audio Player (실제