diff --git a/src/api.js b/src/api.js index d3f857d..3159fea 100644 --- a/src/api.js +++ b/src/api.js @@ -247,20 +247,33 @@ export function deleteSellHistory(id) { } // ── AI 음악 생성 API ────────────────────────────────────────────────────────── -// POST /api/music/generate body: { genre, moods, instruments, duration_sec, bpm, key, scale, prompt } -// → { task_id: string } + +// GET /api/music/providers → { providers: [{ id, name, description, features }] } +export function getMusicProviders() { + return apiGet('/api/music/providers'); +} + +// POST /api/music/generate +// body: { provider, genre, moods, instruments, duration_sec, bpm, key, scale, prompt, lyrics, instrumental } +// → { task_id: string, provider: string } export function generateMusic(payload) { return apiPost('/api/music/generate', payload); } // GET /api/music/status/:task_id -// → { status: "queued"|"processing"|"succeeded"|"failed", progress: 0~100, message, audio_url?, error? } +// → { status, progress, message, audio_url?, error?, provider?, track? } export function getMusicStatus(taskId) { return apiGet(`/api/music/status/${encodeURIComponent(taskId)}`); } +// POST /api/music/lyrics body: { prompt } +// → { id, status, text } (Suno 가사 생성) +export function generateMusicLyrics(prompt) { + return apiPost('/api/music/lyrics', { prompt }); +} + // GET /api/music/library -// → { tracks: [{ id, title, genre, moods, instruments, duration_id, bpm, key, scale, audio_url, created_at }] } +// → { tracks: [{ id, title, genre, ..., provider, lyrics, image_url, suno_id }] } export function getMusicLibrary() { return apiGet('/api/music/library'); } diff --git a/src/pages/music/MusicStudio.css b/src/pages/music/MusicStudio.css index 2f2a036..781eab9 100644 --- a/src/pages/music/MusicStudio.css +++ b/src/pages/music/MusicStudio.css @@ -1701,6 +1701,184 @@ } } +/* ═══════════════════════════════════════════════════ + PROVIDER BAR +═══════════════════════════════════════════════════ */ +.ms-provider-bar { + display: flex; + gap: 8px; + margin-bottom: 20px; +} + +.ms-provider-btn { + flex: 1; + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s; + text-align: left; +} + +.ms-provider-btn:hover { + background: rgba(255,255,255,0.06); + border-color: rgba(255,255,255,0.15); +} + +.ms-provider-btn.is-active { + background: rgba(var(--ms-accent-rgb, 245,166,35), 0.12); + border-color: var(--ms-accent, #f5a623); + box-shadow: 0 0 12px rgba(var(--ms-accent-rgb, 245,166,35), 0.15); +} + +.ms-provider-btn__icon { + font-size: 20px; + flex-shrink: 0; +} + +.ms-provider-btn__name { + font-family: 'Bebas Neue', sans-serif; + font-size: 15px; + letter-spacing: 0.04em; + color: #f0f0f0; +} + +.ms-provider-btn__desc { + font-size: 10px; + color: rgba(255,255,255,0.4); + margin-left: auto; +} + +/* ═══════════════════════════════════════════════════ + VOCALS & LYRICS (SUNO) +═══════════════════════════════════════════════════ */ +.ms-vocal-toggle { + display: flex; + gap: 6px; + margin-bottom: 16px; +} + +.ms-vocal-btn { + flex: 1; + padding: 10px 12px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 8px; + color: rgba(255,255,255,0.6); + cursor: pointer; + font-size: 13px; + transition: all 0.2s; +} + +.ms-vocal-btn:hover { + background: rgba(255,255,255,0.06); +} + +.ms-vocal-btn.is-active { + background: rgba(var(--ms-accent-rgb, 245,166,35), 0.12); + border-color: var(--ms-accent, #f5a623); + color: #f0f0f0; +} + +.ms-lyrics-wrap { + margin-top: 4px; +} + +.ms-lyrics-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.ms-lyrics { + width: 100%; + min-height: 160px; + padding: 12px; + background: rgba(0,0,0,0.3); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 8px; + color: #e0e0e0; + font-family: 'Courier Prime', monospace; + font-size: 13px; + line-height: 1.6; + resize: vertical; +} + +.ms-lyrics:focus { + outline: none; + border-color: var(--ms-accent, #f5a623); +} + +.ms-lyrics-hint { + margin-top: 6px; + font-size: 11px; + color: rgba(255,255,255,0.35); +} + +.ms-btn--sm { + font-size: 11px; + padding: 4px 10px; +} + +/* ═══════════════════════════════════════════════════ + PROVIDER TAG +═══════════════════════════════════════════════════ */ +.ms-result__tag--provider { + font-weight: 600; + letter-spacing: 0.02em; +} + +.ms-result__tag--provider.is-suno { + background: rgba(168, 85, 247, 0.15); + border-color: rgba(168, 85, 247, 0.3); + color: #c084fc; +} + +.ms-result__tag--provider.is-local { + background: rgba(96, 165, 250, 0.15); + border-color: rgba(96, 165, 250, 0.3); + color: #60a5fa; +} + +/* ═══════════════════════════════════════════════════ + LYRICS IN RESULT +═══════════════════════════════════════════════════ */ +.ms-result__lyrics { + margin-top: 12px; + border: 1px solid rgba(255,255,255,0.06); + border-radius: 8px; + overflow: hidden; +} + +.ms-result__lyrics summary { + padding: 8px 12px; + cursor: pointer; + font-size: 13px; + color: rgba(255,255,255,0.6); + background: rgba(255,255,255,0.02); +} + +.ms-result__lyrics summary:hover { + color: #f0f0f0; +} + +.ms-result__lyrics-text { + padding: 12px; + margin: 0; + font-family: 'Courier Prime', monospace; + font-size: 12px; + line-height: 1.7; + color: rgba(255,255,255,0.7); + white-space: pre-wrap; + max-height: 300px; + overflow-y: auto; +} + /* ═══════════════════════════════════════════════════ REDUCED MOTION ═══════════════════════════════════════════════════ */ diff --git a/src/pages/music/MusicStudio.jsx b/src/pages/music/MusicStudio.jsx index e15952b..893be60 100644 --- a/src/pages/music/MusicStudio.jsx +++ b/src/pages/music/MusicStudio.jsx @@ -2,7 +2,9 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { deleteMusicTrack, generateMusic, + generateMusicLyrics, getMusicLibrary, + getMusicProviders, getMusicStatus, } from '../../api'; import './MusicStudio.css'; @@ -380,6 +382,11 @@ const TrackResult = ({ track, onDownload, onNew }) => { />
+ {track.provider && ( + + {track.provider === 'suno' ? '🎙️ Suno' : '🤖 MusicGen'} + + )} {(track.instruments ?? []).slice(0, 4).map((inst) => ( {inst} ))} @@ -388,6 +395,13 @@ const TrackResult = ({ track, onDownload, onNew }) => { ))}
+ {track.lyrics && ( +
+ 🎤 가사 보기 +
{track.lyrics}
+
+ )} +
+ ))} +
+ )} + {/* Step 1: Genre */}
@@ -1031,6 +1109,62 @@ export default function MusicStudio() { {prompt.length}/500
+ + {/* Step 6: Vocals & Lyrics (Suno only) */} + {provider === 'suno' && ( +
+
+ 06 +

Vocals & Lyrics

+ Suno 전용 +
+ +
+ + +
+ + {!instrumental && ( + <> +
+
+ + +
+