Theowise / static /script-editor.js
hoon1018's picture
Upload 4 files
77a41a5 verified
Raw
History Blame Contribute Delete
12 kB
// πŸ’‘ [μˆ˜μ •] ν”„λ‘œκ·Έλž¨ μ‹€ν–‰ μ‹œ 기본으둜 1단계가 μ‘΄μž¬ν•˜λ„λ‘ μ΄ˆκΈ°κ°’ μ„€μ •
let currentScript = [{
credit: "좜처 μž…λ ₯\n@username",
idiom: "βœ“ 핡심 ν‘œν˜„",
meaning: "; λœ»μ„ μž…λ ₯ν•˜μ„Έμš”",
en: "",
ko: "",
pron: "",
prompt_top: "stand-up comedian, highly detailed",
prompt_bottom: "audience reacting, cinematic lighting"
}];
let assetStates = {};
let currentPreviewTurn = 0;
// 🎨 숏폼 μžλ§‰ νƒ€μ΄ν¬κ·Έλž˜ν”Ό κΈ€λ‘œλ²Œ μ„€μ • (λ³„ν‘œ 효과 포함)
let styleConfig = {
english: {
font: 'SUIT', size: 80, baseColor: '#FFFFFF', bgColor: '#000000', bgOpacity: 0.85,
highlights: {
1: { color: '#FFF200', desc: '주제(λ…Έλž‘)' },
2: { color: '#00E5FF', desc: 'νŒ¨ν„΄(μ‹œμ•ˆ)' },
3: { color: '#FF3B30', desc: '주의(λΉ¨κ°•)' },
4: { color: '#4CD964', desc: 'μ–΄νœ˜(초둝)' },
5: { color: '#FF2D55', desc: 'λ‰˜μ•™μŠ€(핑크)' }
}
},
korean: { font: 'SeoulNamsanM', size: 55, color: '#FFFFFF', outlineColor: '#000000', outlineWidth: 3 },
pronunciation: { font: 'SchoolSafeDictation', size: 65, color: '#FF3B30', outlineColor: '#000000', outlineWidth: 2 }
};
// ✨ [μ—”μ§„] λ³„ν‘œ 개수 기반 λ©€ν‹° μŠ€νƒ€μΌ μžλ§‰ λ³€ν™˜ ν•¨μˆ˜
function parseAsteriskToHTML(text) {
if (!text) return "";
let html = text;
html = html.replace(/\*{5}(.*?)\*{5}/g, `<span class="font-bold" style="color: ${styleConfig.english.highlights[5].color};">$1</span>`);
html = html.replace(/\*{4}(.*?)\*{4}/g, `<span class="font-bold" style="color: ${styleConfig.english.highlights[4].color};">$1</span>`);
html = html.replace(/\*{3}(.*?)\*{3}/g, `<span class="font-bold underline" style="color: ${styleConfig.english.highlights[3].color};">$1</span>`);
html = html.replace(/\*{2}(.*?)\*{2}/g, `<span class="font-bold" style="color: ${styleConfig.english.highlights[2].color};">$1</span>`);
html = html.replace(/\*(.*?)\*/g, `<span class="font-bold" style="color: ${styleConfig.english.highlights[1].color};">$1</span>`);
return html;
}
// [νŒ¨λ„ 1] AI λŒ€λ³Έ 생성 μ—”μ§„
async function generateScript() {
const topic = document.getElementById('topicInput').value;
if(!topic) { alert("주제λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”."); return; }
const btn = event.currentTarget;
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> 생성 쀑...';
btn.disabled = true;
try {
const response = await fetch('/api/studio/generate-script', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic: topic })
});
if (!response.ok) throw new Error(`μ„œλ²„ 였λ₯˜: ${response.status}`);
const data = await response.json();
currentScript = data.dialogue.map((turn) => ({
credit: turn.credit || "μ§€λ―Έ μΉ΄\n@jimmycarr",
idiom: turn.idiom || "βœ“ 핡심 ν‘œν˜„",
meaning: turn.meaning || "; λœ»μ„ μž…λ ₯ν•˜μ„Έμš”",
en: turn.en || "",
ko: turn.ko || "",
pron: turn.ko_pron || turn.pron || "",
prompt_top: turn.image_prompt_top || turn.image_prompt || "stand-up comedian, highly detailed",
prompt_bottom: turn.image_prompt_bottom || "audience reacting, cinematic lighting"
}));
renderScriptPanel();
renderAssetPanel();
} catch (error) {
console.error("λŒ€λ³Έ 생성 μ—λŸ¬:", error);
// πŸ’‘ [μˆ˜μ •] AI 생성 μ‹€νŒ¨ μ‹œ(Mock 데이터) κΈ°λ³Έ 4단계 κΈ°νšμ— 맞좰 좜λ ₯
currentScript = [
{
credit: "μ§€λ―Έ μΉ΄\n@jimmycarr", idiom: "βœ“ test the water(s)", meaning: "; λΆ„μœ„κΈ°λ₯Ό μ‚΄ν”Όλ‹€ / λ°˜μ‘μ„ 떠보닀",
en: "I'll just, I'll *test the water* first.", ko: "κ·Έλƒ₯ λΆ„μœ„κΈ° ν•œ 번 볼게", pron: "μ•„μ„μ €μŠ€, μ•„μ„ν…ŒμŠ€λ” μ›ŒλŸ΄ νΌμŠ€λ”",
prompt_top: "stand-up comedian holding a microphone", prompt_bottom: "crowd of people smiling"
},
{
credit: "μ§€λ―Έ μΉ΄\n@jimmycarr", idiom: "βœ“ make a move", meaning: "; 행동을 μ·¨ν•˜λ‹€ / λ‹€κ°€κ°€λ‹€",
en: "Before I actually **make a move**, you know?", ko: "본격적으둜 ν–‰λ™ν•˜κΈ° 전에 말이야.", pron: "비포 아이 앑츄얼리 메이컀 무브, 유 λ…Έ?",
prompt_top: "man looking thoughtful, cinematic lighting", prompt_bottom: "close up of a person's eyes"
},
{
credit: "μ§€λ―Έ μΉ΄\n@jimmycarr", idiom: "βœ“ play it safe", meaning: "; μ•ˆμ „ν•˜κ²Œ κ°€λ‹€ / λ¬΄λ¦¬ν•˜μ§€ μ•Šλ‹€",
en: "It's always better to ***play it safe***.", ko: "μ–Έμ œλ‚˜ μ•ˆμ „ν•˜κ²Œ κ°€λŠ” 게 λ‚«μ§€.", pron: "이츠 μ˜¬μ›¨μ΄μ¦ˆ 베러 투 ν”Œλ ˆμ΄ μž‡ 세이프.",
prompt_top: "a cautious person, dark background", prompt_bottom: "audience nodding in agreement"
},
{
credit: "μ§€λ―Έ μΉ΄\n@jimmycarr", idiom: "βœ“ go all in", meaning: "; μ˜¬μΈν•˜λ‹€ / λ‹€ κ±Έλ‹€",
en: "Then I'll just ****go all in****.", ko: "그러고 λ‚˜μ„œ μ‹Ή λ‹€ 걸어버릴 κ±°μ•Ό.", pron: "덴 μ•„μ„μ €μŠ€ κ³  올 인.",
prompt_top: "casino chips, aggressive lighting", prompt_bottom: "people cheering loudly"
}
];
renderScriptPanel();
renderAssetPanel();
} finally {
btn.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> AI 생성';
btn.disabled = false;
}
}
// [νŒ¨λ„ 1] λŒ€λ³Έ 에디터 UI λ Œλ”λ§
function renderScriptPanel() {
const container = document.getElementById('scriptContainer');
container.innerHTML = "";
currentScript.forEach((turn, idx) => {
// πŸ’‘ [μˆ˜μ •] μΉ΄λ“œ 우츑 상단에 X(μ‚­μ œ) λ²„νŠΌ μΆ”κ°€
const html = `
<div class="bg-neutral-800 rounded-xl p-4 border border-neutral-700 shadow-md relative space-y-3">
<div class="absolute -left-2 -top-2 bg-neutral-700 text-white text-xs w-6 h-6 flex items-center justify-center rounded-full font-mono font-bold shadow-md">${idx+1}</div>
<button onclick="deleteScriptTurn(${idx})" class="absolute -right-2 -top-2 bg-neutral-900 border border-neutral-700 text-neutral-400 hover:bg-red-600 hover:text-white hover:border-red-500 w-6 h-6 flex items-center justify-center rounded-full transition-colors shadow-md text-[10px]" title="이 단계 μ‚­μ œ">
<i class="fa-solid fa-xmark"></i>
</button>
<div class="grid grid-cols-2 gap-2 pt-1">
<div>
<label class="block text-[10px] text-gray-500 font-bold mb-1">🏷️ 상단 좜처 ν‘œκΈ° (Credit)</label>
<textarea onchange="updateScriptData(${idx}, 'credit', this.value)" class="w-full bg-neutral-950 border border-neutral-700 rounded p-1.5 text-xs text-gray-300 font-mono h-12 leading-tight resize-none">${turn.credit}</textarea>
</div>
<div class="space-y-1.5">
<div>
<label class="block text-[10px] text-yellow-500 font-bold">⭐ 핡심 ν‘œν˜„ (Idiom)</label>
<input onchange="updateScriptData(${idx}, 'idiom', this.value)" type="text" class="w-full bg-neutral-950 border border-neutral-700 rounded px-2 py-1 text-xs text-white font-bold" value="${turn.idiom}">
</div>
<div>
<label class="block text-[10px] text-neutral-400 font-bold">πŸ’¬ ν‘œν˜„ 뜻 (Meaning)</label>
<input onchange="updateScriptData(${idx}, 'meaning', this.value)" type="text" class="w-full bg-neutral-950 border border-neutral-700 rounded px-2 py-1 text-xs text-neutral-400" value="${turn.meaning}">
</div>
</div>
</div>
<div class="space-y-2 border-t border-neutral-700/60 pt-2">
<div>
<label class="block text-[10px] text-blue-400 font-bold mb-0.5">πŸ‡ΊπŸ‡Έ μ˜μ–΄ λ¬Έμž₯ (Sentence)</label>
<textarea id="textarea_en_${idx}" onchange="updateScriptData(${idx}, 'en', this.value)" class="w-full bg-neutral-950 border border-neutral-700 rounded p-2 text-xs text-white resize-none font-medium" rows="2">${turn.en}</textarea>
</div>
<div>
<label class="block text-[10px] text-emerald-400 font-bold mb-0.5">πŸ‡°πŸ‡· ν•œκ΅­μ–΄ 해석 (Translation)</label>
<input id="textarea_ko_${idx}" onchange="updateScriptData(${idx}, 'ko', this.value)" type="text" class="w-full bg-neutral-950 border border-neutral-700 rounded px-2 py-1.5 text-xs text-gray-300" value="${turn.ko}">
</div>
<div>
<label class="block text-[10px] text-purple-400 font-bold mb-0.5">πŸ—£οΈ ν•œκΈ€ 발음 ν‘œκΈ° (Pronunciation)</label>
<input id="textarea_pron_${idx}" onchange="updateScriptData(${idx}, 'pron', this.value)" type="text" class="w-full bg-neutral-950 border border-neutral-700 rounded px-2 py-1.5 text-xs text-purple-300 font-mono" value="${turn.pron}">
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
});
}
// πŸ—‘οΈ [μ‹ κ·œ μΆ”κ°€] μˆ˜λ™μœΌλ‘œ νŠΉμ • 단계(Turn) μ‚­μ œ ν•¨μˆ˜
function deleteScriptTurn(idx) {
if (currentScript.length <= 1) {
alert("μ΅œμ†Œ 1개의 λ‹¨κ³„λŠ” μœ μ§€ν•΄μ•Ό ν•©λ‹ˆλ‹€.");
return;
}
if (confirm(`μ •λ§λ‘œ ${idx + 1}단계λ₯Ό μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?`)) {
// ν•΄λ‹Ή 인덱슀의 λŒ€λ³Έ 및 에셋 데이터 μ‚­μ œ
currentScript.splice(idx, 1);
// 에셋 데이터도 λ™κΈ°ν™”ν•˜μ—¬ μ‚­μ œν•˜κ³  인덱슀 μž¬μ •λ ¬
const newAssetStates = {};
currentScript.forEach((_, newIdx) => {
newAssetStates[newIdx] = assetStates[newIdx >= idx ? newIdx + 1 : newIdx];
});
assetStates = newAssetStates;
// 프리뷰 μΈλ±μŠ€κ°€ λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ μ‘°μ •
if (currentPreviewTurn >= currentScript.length) {
currentPreviewTurn = Math.max(0, currentScript.length - 1);
}
localStorage.setItem('studio_current_script', JSON.stringify(currentScript));
renderScriptPanel();
renderAssetPanel();
updatePreviewScreen();
}
}
function updateScriptData(idx, field, value) {
currentScript[idx][field] = value;
localStorage.setItem('studio_current_script', JSON.stringify(currentScript));
updatePreviewScreen();
}
// πŸ’Š [νŒ¨λ„ 1] μˆ˜λ™μœΌλ‘œ μƒˆλ‘œμš΄ λŒ€λ³Έ 단계(Turn) μΆ”κ°€ ν•¨μˆ˜
function addScriptTurn() {
// 1. 빈 데이터 배열에 λ°€μ–΄λ„£κΈ°
currentScript.push({
credit: "좜처 μž…λ ₯\n@username",
idiom: "βœ“ 핡심 ν‘œν˜„",
meaning: "; λœ»μ„ μž…λ ₯ν•˜μ„Έμš”",
en: "",
ko: "",
pron: "",
prompt_top: "stand-up comedian, highly detailed",
prompt_bottom: "audience reacting, cinematic lighting"
});
// 2. νŒ¨λ„ λ‹€μ‹œ 그리기 (1번, 2번 νŒ¨λ„ λ™μ‹œ 동기화)
renderScriptPanel();
renderAssetPanel();
// 3. μ‚¬μš©μžκ°€ 방금 μΆ”κ°€ν•œ 맨 μ•„λž˜μͺ½μœΌλ‘œ 슀크둀 λΆ€λ“œλŸ½κ²Œ 이동
setTimeout(() => {
const container = document.getElementById('scriptContainer');
if (container) {
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
}
}, 50);
}
// 🎨 ν…Œλ§ˆ 프리셋 적용 ν•¨μˆ˜
function applyThemePreset(theme) {
if (theme === 'variety') {
// 예λŠ₯ν˜•: κ°•λ ¬ν•œ 흰색 + λ‘κΊΌμš΄ 검은 ν…Œλ‘λ¦¬, λ°œμŒμ€ 빨간색
styleConfig.korean.color = '#FFFFFF';
styleConfig.korean.outlineColor = '#000000';
styleConfig.pronunciation.color = '#FF3B30';
styleConfig.pronunciation.outlineColor = '#000000';
} else if (theme === 'docu') {
// λ‹€νν˜•: μ°¨λΆ„ν•œ μ—°νšŒμƒ‰ + ν…Œλ‘λ¦¬ μ—†μŒ, λ°œμŒμ€ μ–΄λ‘μš΄ νšŒμƒ‰
styleConfig.korean.color = '#E5E5E5';
styleConfig.korean.outlineColor = 'transparent';
styleConfig.pronunciation.color = '#A3A3A3';
styleConfig.pronunciation.outlineColor = 'transparent';
} else if (theme === 'trendy') {
// νŠΈλ Œλ””νŒ: λ„€μ˜¨ λŠλ‚Œμ˜ μ‹œμ•ˆμƒ‰ + μ–΄λ‘μš΄ 보라색 ν…Œλ‘λ¦¬
styleConfig.korean.color = '#00E5FF';
styleConfig.korean.outlineColor = '#1A0B2E';
styleConfig.pronunciation.color = '#FF2D55';
styleConfig.pronunciation.outlineColor = '#1A0B2E';
}
// λ³€κ²½λœ 섀정을 미리보기 화면에 μ¦‰μ‹œ λ Œλ”λ§
updatePreviewScreen();
}