diff --git a/src/pages/music/components/RemixTab.jsx b/src/pages/music/components/RemixTab.jsx
new file mode 100644
index 0000000..f86222d
--- /dev/null
+++ b/src/pages/music/components/RemixTab.jsx
@@ -0,0 +1,193 @@
+import React, { useState } from 'react';
+import { uploadAndCover, uploadAndExtend, addVocals, addInstrumental } from '../../../api';
+
+const REMIX_ACTIONS = [
+ { id: 'cover', label: 'AI Cover', icon: '🎨', desc: '외부 음원을 Suno AI 스타일로 리메이크' },
+ { id: 'extend', label: 'Extend', icon: '⏩', desc: '외부 음원을 이어서 확장' },
+ { id: 'add-vocals', label: 'Add Vocals', icon: '🎤', desc: '인스트루멘탈에 AI 보컬 입히기' },
+ { id: 'add-instrumental', label: 'Add Instrumental', icon: '🎹', desc: '보컬에 AI 반주 입히기' },
+];
+
+const RemixTab = ({ onTaskStarted, model, isGenerating }) => {
+ const [uploadUrl, setUploadUrl] = useState('');
+ const [activeAction, setActiveAction] = useState(null);
+
+ // 각 액션별 파라미터
+ const [title, setTitle] = useState('');
+ const [style, setStyle] = useState('');
+ const [prompt, setPrompt] = useState('');
+ const [tags, setTags] = useState('');
+ const [negativeTags, setNegativeTags] = useState('');
+ const [vocalGender, setVocalGender] = useState(null);
+ const [continueAt, setContinueAt] = useState(0);
+ const [instrumental, setInstrumental] = useState(false);
+
+ const handleSubmit = async () => {
+ if (!uploadUrl || !activeAction || isGenerating) return;
+
+ let apiCall;
+ let payload = {};
+
+ switch (activeAction) {
+ case 'cover':
+ apiCall = uploadAndCover;
+ payload = {
+ upload_url: uploadUrl, model, custom_mode: true,
+ instrumental, prompt, style, title,
+ vocal_gender: vocalGender || undefined,
+ negative_tags: negativeTags || undefined,
+ };
+ break;
+ case 'extend':
+ apiCall = uploadAndExtend;
+ payload = {
+ upload_url: uploadUrl, model,
+ default_param_flag: !!prompt,
+ continue_at: continueAt || undefined,
+ prompt, style, title, instrumental,
+ vocal_gender: vocalGender || undefined,
+ negative_tags: negativeTags || undefined,
+ };
+ break;
+ case 'add-vocals':
+ apiCall = addVocals;
+ payload = {
+ upload_url: uploadUrl, prompt, title, style,
+ negative_tags: negativeTags,
+ vocal_gender: vocalGender || undefined,
+ model: 'V4_5PLUS',
+ };
+ break;
+ case 'add-instrumental':
+ apiCall = addInstrumental;
+ payload = {
+ upload_url: uploadUrl, title, tags,
+ negative_tags: negativeTags,
+ vocal_gender: vocalGender || undefined,
+ model: 'V4_5PLUS',
+ };
+ break;
+ default:
+ return;
+ }
+
+ try {
+ const res = await apiCall(payload);
+ if (res?.task_id) {
+ onTaskStarted(res.task_id, `Remix: ${REMIX_ACTIONS.find(a => a.id === activeAction)?.label}`);
+ }
+ } catch (e) {
+ // 에러는 부모 컴포넌트에서 처리
+ }
+ };
+
+ return (
+
+
+
Remix Studio
+
외부 음원을 AI로 리메이크, 확장, 보컬/반주 추가
+
+
+
+
+ setUploadUrl(e.target.value)}
+ style={{ width: '100%' }}
+ />
+
+
+
+ {REMIX_ACTIONS.map((action) => (
+
+ ))}
+
+
+ {activeAction && (
+
+ {/* 공통 파라미터 */}
+
+
+ setTitle(e.target.value)} placeholder="곡 제목" style={{ width: '100%' }} />
+
+
+ {(activeAction === 'cover' || activeAction === 'extend' || activeAction === 'add-vocals') && (
+
+
+
+ )}
+
+ {(activeAction === 'cover' || activeAction === 'extend' || activeAction === 'add-vocals') && (
+
+
+ setStyle(e.target.value)} placeholder="예: Pop, Energetic, Piano" style={{ width: '100%' }} />
+
+ )}
+
+ {activeAction === 'add-instrumental' && (
+
+
+ setTags(e.target.value)} placeholder="예: acoustic, warm, dreamy" style={{ width: '100%' }} />
+
+ )}
+
+ {activeAction === 'extend' && (
+
+
+ setContinueAt(Number(e.target.value))} min={0} style={{ width: '120px' }} />
+
+ )}
+
+
+
+ setNegativeTags(e.target.value)} placeholder="제외할 스타일" style={{ width: '100%' }} />
+
+
+
+
+
+ {[{ value: null, label: 'Auto' }, { value: 'm', label: 'Male' }, { value: 'f', label: 'Female' }].map((opt) => (
+
+ ))}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default RemixTab;