diff --git a/src/api.js b/src/api.js index 9de8f3f..4ec3b55 100644 --- a/src/api.js +++ b/src/api.js @@ -334,6 +334,13 @@ 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); +} + // ── 로또 고도화 API ──────────────────────────────────────────────────────────── // GET /api/lotto/stats/performance diff --git a/src/pages/music/MusicStudio.css b/src/pages/music/MusicStudio.css index 3c7adff..beb6f6d 100644 --- a/src/pages/music/MusicStudio.css +++ b/src/pages/music/MusicStudio.css @@ -2449,3 +2449,89 @@ 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); +} diff --git a/src/pages/music/MusicStudio.jsx b/src/pages/music/MusicStudio.jsx index 57ef7eb..0037ecf 100644 --- a/src/pages/music/MusicStudio.jsx +++ b/src/pages/music/MusicStudio.jsx @@ -9,11 +9,13 @@ import { getMusicModels, extendMusicTrack, removeVocals, + generateCoverImage, } 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'; /* ───────────────────────────────────────────── @@ -325,7 +327,8 @@ const TrackResult = ({ track, onDownload, onNew }) => { /* ───────────────────────────────────────────── Library Card ───────────────────────────────────────────── */ -const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, isGenerating }) => { +const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, isGenerating }) => { + const [menuOpen, setMenuOpen] = useState(false); const genre = GENRES.find((g) => g.id === track.genre); const totalSec = track.duration_sec ?? null; const filename = track.audio_url ? track.audio_url.split('/').pop() : ''; @@ -386,29 +389,27 @@ const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemo {hasSunoId && (
Prompt ↔ Style 밸런스
+ setStyleWeight(Number(e.target.value))} + className="ms-bpm-slider" + aria-label="Style Weight" + /> +Original ↔ AI 밸런스
+ setAudioWeight(Number(e.target.value))} + className="ms-bpm-slider" + aria-label="Audio Weight" + /> +