feat(web-ui): PipelineStartModal Mix 입력 라디오 + 고급 옵션
This commit is contained in:
@@ -3330,3 +3330,14 @@
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
.cmp-btn-video:hover { background: rgba(56, 189, 248, 0.25); }
|
.cmp-btn-video:hover { background: rgba(56, 189, 248, 0.25); }
|
||||||
|
|
||||||
|
.psm-input-radio { border: 1px solid var(--ms-line, #2a2a3a); padding: 8px 12px;
|
||||||
|
border-radius: 8px; margin-bottom: 12px; }
|
||||||
|
.psm-input-radio legend { padding: 0 6px; font-size: 11px; color: var(--ms-muted, #a0a0b0); }
|
||||||
|
.psm-input-radio label { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; }
|
||||||
|
.psm-advanced { margin-top: 12px; padding: 8px 0; }
|
||||||
|
.psm-advanced summary { cursor: pointer; font-size: 12px; color: var(--ms-muted, #a0a0b0); user-select: none; }
|
||||||
|
.psm-advanced label { display: block; margin: 8px 0; font-size: 12px; }
|
||||||
|
.psm-advanced input, .psm-advanced select { width: 100%; padding: 6px 8px;
|
||||||
|
background: rgba(255,255,255,.04); border: 1px solid var(--ms-line, #2a2a3a);
|
||||||
|
border-radius: 6px; color: var(--ms-text, #f0f0f5); font-size: 12px; }
|
||||||
|
|||||||
@@ -1,31 +1,134 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { createPipeline, startPipeline } from '../../../api';
|
import { createPipeline, startPipeline, getCompileJobs } from '../../../api';
|
||||||
|
|
||||||
|
const fmtDur = (s) => {
|
||||||
|
if (!s) return '0:00';
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
const sec = Math.round(s % 60);
|
||||||
|
return `${m}:${String(sec).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
export default function PipelineStartModal({ library, initialTrackId, onClose, onCreated }) {
|
export default function PipelineStartModal({ library, initialTrackId, onClose, onCreated }) {
|
||||||
|
const [inputType, setInputType] = useState('track'); // 'track' | 'compile'
|
||||||
const [tid, setTid] = useState(initialTrackId || library?.[0]?.id || '');
|
const [tid, setTid] = useState(initialTrackId || library?.[0]?.id || '');
|
||||||
|
const [cid, setCid] = useState('');
|
||||||
|
const [compileJobs, setCompileJobs] = useState([]);
|
||||||
|
const [advanced, setAdvanced] = useState(false);
|
||||||
|
const [visualStyle, setVisualStyle] = useState('');
|
||||||
|
const [bgMode, setBgMode] = useState('');
|
||||||
|
const [bgKeyword, setBgKeyword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputType === 'compile') {
|
||||||
|
getCompileJobs()
|
||||||
|
.then(r => {
|
||||||
|
const list = (r.jobs || r || []);
|
||||||
|
const completed = list.filter(j => j.status === 'done' || j.status === 'succeeded');
|
||||||
|
setCompileJobs(completed);
|
||||||
|
if (completed.length && !cid) setCid(completed[0].id);
|
||||||
|
})
|
||||||
|
.catch(e => setError(String(e)));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [inputType]);
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
const p = await createPipeline(parseInt(tid));
|
const payload = {};
|
||||||
|
if (inputType === 'track') {
|
||||||
|
payload.track_id = parseInt(tid);
|
||||||
|
} else {
|
||||||
|
if (!cid) {
|
||||||
|
setError('완료된 Mix를 선택해주세요');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.compile_job_id = parseInt(cid);
|
||||||
|
}
|
||||||
|
if (visualStyle) payload.visual_style = visualStyle;
|
||||||
|
if (bgMode) payload.background_mode = bgMode;
|
||||||
|
if (bgKeyword) payload.background_keyword = bgKeyword;
|
||||||
|
|
||||||
|
const p = await createPipeline(payload);
|
||||||
await startPipeline(p.id);
|
await startPipeline(p.id);
|
||||||
onCreated(p);
|
onCreated(p);
|
||||||
} catch (e) { setError(String(e)); }
|
} catch (e) {
|
||||||
|
setError(e.message || String(e));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-overlay" onClick={onClose}>
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
<div className="modal-body" onClick={e => e.stopPropagation()}>
|
<div className="modal-body" onClick={e => e.stopPropagation()}>
|
||||||
<h3>새 파이프라인 시작</h3>
|
<h3>새 파이프라인 시작</h3>
|
||||||
|
|
||||||
|
<fieldset className="psm-input-radio">
|
||||||
|
<legend>입력</legend>
|
||||||
|
<label>
|
||||||
|
<input type="radio" checked={inputType === 'track'}
|
||||||
|
onChange={() => setInputType('track')} />
|
||||||
|
{' '}단일 트랙
|
||||||
|
</label>
|
||||||
|
<label style={{ marginLeft: 12 }}>
|
||||||
|
<input type="radio" checked={inputType === 'compile'}
|
||||||
|
onChange={() => setInputType('compile')} />
|
||||||
|
{' '}Mix (컴파일 결과)
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{inputType === 'track' ? (
|
||||||
<select value={tid} onChange={e => setTid(e.target.value)}>
|
<select value={tid} onChange={e => setTid(e.target.value)}>
|
||||||
{(library || []).map(t => (
|
{(library || []).map(t => (
|
||||||
<option key={t.id} value={t.id}>{t.title} ({t.genre})</option>
|
<option key={t.id} value={t.id}>{t.title} ({t.genre})</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
) : (
|
||||||
|
<select value={cid} onChange={e => setCid(e.target.value)}>
|
||||||
|
{compileJobs.length === 0 && <option value="">완료된 Mix가 없습니다</option>}
|
||||||
|
{compileJobs.map(j => (
|
||||||
|
<option key={j.id} value={j.id}>
|
||||||
|
{j.title || `Mix #${j.id}`}
|
||||||
|
{' '}({fmtDur(j.duration_sec || 0)},{' '}
|
||||||
|
{j.tracks_count || (j.track_ids && j.track_ids.length) || '?'}곡)
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<details className="psm-advanced" open={advanced}>
|
||||||
|
<summary onClick={(e) => { e.preventDefault(); setAdvanced(!advanced); }}>
|
||||||
|
고급 옵션
|
||||||
|
</summary>
|
||||||
|
<label>
|
||||||
|
시각 스타일
|
||||||
|
<select value={visualStyle} onChange={e => setVisualStyle(e.target.value)}>
|
||||||
|
<option value="">기본 (구성 탭 default)</option>
|
||||||
|
<option value="essential">essential (배경 + 중앙 비주얼)</option>
|
||||||
|
<option value="single">single (커버 + 가장자리 파형)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
배경 모드
|
||||||
|
<select value={bgMode} onChange={e => setBgMode(e.target.value)}>
|
||||||
|
<option value="">기본 (구성 탭 default)</option>
|
||||||
|
<option value="static">정적 사진</option>
|
||||||
|
<option value="video_loop">영상 루프 (Pexels)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
배경 키워드 (Pexels 검색용)
|
||||||
|
<input value={bgKeyword} onChange={e => setBgKeyword(e.target.value)}
|
||||||
|
placeholder="rainy window, lofi cafe ..." />
|
||||||
|
</label>
|
||||||
|
</details>
|
||||||
|
|
||||||
{error && <div className="ms-error">{error}</div>}
|
{error && <div className="ms-error">{error}</div>}
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button onClick={onClose}>취소</button>
|
<button onClick={onClose}>취소</button>
|
||||||
<button className="button primary" onClick={submit}>시작</button>
|
<button className="button primary" onClick={submit}
|
||||||
|
disabled={inputType === 'compile' && !cid}>
|
||||||
|
시작
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user