182 lines
8.7 KiB
JavaScript
182 lines
8.7 KiB
JavaScript
import { useEffect, useState } from 'react';
|
||
import {
|
||
getMusicSetup, updateMusicSetup,
|
||
getYoutubeAuthUrl, getYoutubeStatus, disconnectYoutube,
|
||
} from '../../../api';
|
||
|
||
export default function SetupTab() {
|
||
const [setup, setSetup] = useState(null);
|
||
const [yt, setYt] = useState(null);
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
useEffect(() => {
|
||
Promise.all([getMusicSetup(), getYoutubeStatus()])
|
||
.then(([s, y]) => { setSetup(s); setYt(y); })
|
||
.catch(e => setError(String(e)));
|
||
}, []);
|
||
|
||
if (!setup) return <p className="ms-loading">Loading…</p>;
|
||
|
||
const save = async (patch) => {
|
||
setSaving(true);
|
||
try {
|
||
const next = await updateMusicSetup(patch);
|
||
setSetup(next);
|
||
} catch (e) { setError(String(e)); }
|
||
finally { setSaving(false); }
|
||
};
|
||
|
||
const connectYoutube = async () => {
|
||
const { url } = await getYoutubeAuthUrl();
|
||
window.location.href = url;
|
||
};
|
||
|
||
return (
|
||
<div className="setup-container">
|
||
{error && <div className="ms-error">{error}</div>}
|
||
|
||
<section className="setup-card">
|
||
<h3>YouTube 채널 연동</h3>
|
||
{yt && yt.channel_id ? (
|
||
<div className="setup-channel">
|
||
{yt.avatar_url && <img src={yt.avatar_url} alt="" className="setup-avatar" />}
|
||
<span>{yt.channel_title}</span>
|
||
<button onClick={async () => { await disconnectYoutube(); setYt({}); }}>
|
||
연결 해제
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<button className="button primary" onClick={connectYoutube}>
|
||
Google 계정 연결
|
||
</button>
|
||
)}
|
||
</section>
|
||
|
||
<section className="setup-card">
|
||
<h3>메타데이터 템플릿</h3>
|
||
<label>제목 패턴
|
||
<input
|
||
value={setup.metadata_template.title}
|
||
onChange={e => setSetup(s => ({...s, metadata_template: {...s.metadata_template, title: e.target.value}}))}
|
||
/>
|
||
</label>
|
||
<label>설명 템플릿
|
||
<textarea
|
||
rows={6}
|
||
value={setup.metadata_template.description}
|
||
onChange={e => setSetup(s => ({...s, metadata_template: {...s.metadata_template, description: e.target.value}}))}
|
||
/>
|
||
</label>
|
||
<label>기본 태그 (쉼표 구분)
|
||
<input
|
||
value={(setup.metadata_template.tags || []).join(', ')}
|
||
onChange={e => setSetup(s => ({...s, metadata_template: {...s.metadata_template,
|
||
tags: e.target.value.split(',').map(t => t.trim()).filter(Boolean)}}))}
|
||
/>
|
||
</label>
|
||
<button onClick={() => save({ metadata_template: setup.metadata_template })}>저장</button>
|
||
</section>
|
||
|
||
<section className="setup-card">
|
||
<h3>AI 커버 prompt (장르별)</h3>
|
||
{Object.entries(setup.cover_prompts).map(([g, p]) => (
|
||
<div key={g} className="setup-prompt-row">
|
||
<span className="setup-prompt-genre">{g}</span>
|
||
<input
|
||
value={p}
|
||
onChange={e => setSetup(s => ({...s, cover_prompts: {...s.cover_prompts, [g]: e.target.value}}))}
|
||
/>
|
||
</div>
|
||
))}
|
||
<button onClick={() => save({ cover_prompts: setup.cover_prompts })}>저장</button>
|
||
</section>
|
||
|
||
<section className="setup-card">
|
||
<h3>AI 최종 검토 기준</h3>
|
||
{['meta','policy','viewer','trend'].map(k => (
|
||
<label key={k}>
|
||
{k} 가중치 ({setup.review_weights[k]})
|
||
<input type="range" min="0" max="100"
|
||
value={setup.review_weights[k]}
|
||
onChange={e => setSetup(s => ({...s, review_weights: {...s.review_weights, [k]: parseInt(e.target.value)}}))}
|
||
/>
|
||
</label>
|
||
))}
|
||
<label>임계값 ({setup.review_threshold})
|
||
<input type="range" min="0" max="100" value={setup.review_threshold}
|
||
onChange={e => setSetup(s => ({...s, review_threshold: parseInt(e.target.value)}))}
|
||
/>
|
||
</label>
|
||
<button onClick={() => save({ review_weights: setup.review_weights, review_threshold: setup.review_threshold })}>저장</button>
|
||
</section>
|
||
|
||
<section className="setup-card">
|
||
<h3>영상 비주얼 기본값</h3>
|
||
|
||
<label>해상도
|
||
<select value={setup.visual_defaults.resolution || '1920x1080'}
|
||
onChange={e => setSetup(s => ({...s, visual_defaults: {...s.visual_defaults, resolution: e.target.value}}))}>
|
||
<option value="1920x1080">1920×1080 (가로)</option>
|
||
<option value="1080x1920">1080×1920 (세로/Shorts)</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label>기본 시각 스타일
|
||
<select value={setup.visual_defaults.default_visual_style || 'essential'}
|
||
onChange={e => setSetup(s => ({...s, visual_defaults: {...s.visual_defaults, default_visual_style: e.target.value}}))}>
|
||
<option value="essential">essential (배경 + 중앙 비주얼)</option>
|
||
<option value="single">single (커버 + 가장자리 파형)</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label>기본 배경 모드
|
||
<select value={setup.visual_defaults.default_background_mode || 'static'}
|
||
onChange={e => setSetup(s => ({...s, visual_defaults: {...s.visual_defaults, default_background_mode: e.target.value}}))}>
|
||
<option value="static">정적 사진</option>
|
||
<option value="video_loop">영상 루프 (Pexels)</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label>기본 배경 키워드 (비우면 장르 기반 자동)
|
||
<input value={setup.visual_defaults.default_background_keyword || ''}
|
||
onChange={e => setSetup(s => ({...s, visual_defaults: {...s.visual_defaults, default_background_keyword: e.target.value}}))}
|
||
placeholder="lofi cafe, rainy window, mountain ..." />
|
||
</label>
|
||
|
||
<label>배경 이미지 소스 (정적 모드)
|
||
<select value={setup.visual_defaults.background_image_source || 'ai'}
|
||
onChange={e => setSetup(s => ({...s, visual_defaults: {...s.visual_defaults, background_image_source: e.target.value}}))}>
|
||
<option value="ai">AI 생성 (DALL·E)</option>
|
||
<option value="pexels">Pexels 스톡 사진</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label className="setup-checkbox">
|
||
<input type="checkbox"
|
||
checked={setup.visual_defaults.subtitle_track_titles ?? true}
|
||
onChange={e => setSetup(s => ({...s, visual_defaults: {...s.visual_defaults, subtitle_track_titles: e.target.checked}}))}/>
|
||
Mix에서 곡명 자막 표시 (트랙 시작 시 5초)
|
||
</label>
|
||
|
||
<button onClick={() => save({ visual_defaults: setup.visual_defaults })}>저장</button>
|
||
</section>
|
||
|
||
<section className="setup-card">
|
||
<h3>발행 정책</h3>
|
||
<label>privacy
|
||
<select value={setup.publish_policy.privacy}
|
||
onChange={e => setSetup(s => ({...s, publish_policy: {...s.publish_policy, privacy: e.target.value}}))}>
|
||
<option value="private">Private (비공개)</option>
|
||
<option value="unlisted">Unlisted</option>
|
||
<option value="public">Public</option>
|
||
</select>
|
||
</label>
|
||
<button onClick={() => save({ publish_policy: setup.publish_policy })}>저장</button>
|
||
</section>
|
||
|
||
{saving && <div className="setup-saving">저장 중...</div>}
|
||
</div>
|
||
);
|
||
}
|