Files
web-page/src/pages/music/components/SetupTab.jsx

182 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}