const listEl = document.getElementById("tts-list"); const textInput = document.getElementById("text-input"); const saveBtn = document.getElementById("save-btn"); const voiceSelect = document.getElementById("voice-select"); const editBtn = document.getElementById("edit-btn"); const deleteBtn = document.getElementById("delete-btn"); const cancelBtn = document.getElementById("cancel-btn"); const downloadLink = document.getElementById("download-link"); const progressWrap = document.getElementById("save-progress"); const progressBar = document.getElementById("save-progress-bar"); let items = []; let editMode = false; const selectedIds = new Set(); let progressTimer = null; let selectedItemId = null; let selectedDownloadUrl = null; function startProgress() { let value = 0; progressWrap.classList.remove("hidden"); progressBar.style.width = "0%"; progressBar.setAttribute("aria-valuenow", "0"); if (progressTimer) { clearInterval(progressTimer); } progressTimer = setInterval(() => { value = Math.min(value + Math.random() * 8 + 2, 90); progressBar.style.width = `${value}%`; progressBar.setAttribute("aria-valuenow", `${Math.round(value)}`); }, 300); } function finishProgress(success = true) { if (progressTimer) { clearInterval(progressTimer); progressTimer = null; } progressBar.style.width = "100%"; progressBar.setAttribute("aria-valuenow", "100"); const delay = success ? 400 : 1200; setTimeout(() => { progressBar.style.width = "0%"; progressBar.setAttribute("aria-valuenow", "0"); progressWrap.classList.add("hidden"); }, delay); } function setEditMode(isEdit) { editMode = isEdit; selectedIds.clear(); selectedItemId = null; selectedDownloadUrl = null; editBtn.classList.toggle("hidden", editMode); deleteBtn.classList.toggle("hidden", !editMode); cancelBtn.classList.toggle("hidden", !editMode); downloadLink.classList.add("hidden"); downloadLink.href = "#"; renderList(); } function renderList() { listEl.innerHTML = ""; items.forEach((item) => { const li = document.createElement("li"); li.className = "tts-item"; if (!editMode && selectedItemId === item.id) { li.classList.add("selected"); } if (editMode) { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = selectedIds.has(item.id); checkbox.addEventListener("click", (event) => { event.stopPropagation(); if (checkbox.checked) { selectedIds.add(item.id); } else { selectedIds.delete(item.id); } }); li.appendChild(checkbox); } else { const bullet = document.createElement("span"); bullet.textContent = "•"; bullet.className = "bullet"; li.appendChild(bullet); } const label = document.createElement("span"); label.textContent = item.size_display ? `${item.display_time} (${item.size_display})` : item.display_time; label.className = "item-label"; li.appendChild(label); li.addEventListener("click", () => handleItemClick(item)); listEl.appendChild(li); }); } async function loadList() { const res = await fetch("/api/tts"); items = await res.json(); renderList(); } async function handleSave() { const text = (textInput.value || "").trim(); const voice = (voiceSelect?.value || "male").trim(); if (text.length < 11) { alert("10개 글자 이상이어야 합니다"); return; } startProgress(); const res = await fetch("/api/tts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, voice }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); alert(err.detail || "저장에 실패했습니다."); finishProgress(false); return; } const created = await res.json(); items.unshift(created); renderList(); finishProgress(true); } async function handleItemClick(item) { if (editMode) { if (selectedIds.has(item.id)) { selectedIds.delete(item.id); } else { selectedIds.add(item.id); } renderList(); return; } if (selectedItemId === item.id) { selectedItemId = null; selectedDownloadUrl = null; downloadLink.href = "#"; downloadLink.classList.add("hidden"); renderList(); return; } const res = await fetch(`/api/tts/${item.id}`); if (!res.ok) { alert("항목을 불러오지 못했습니다."); return; } const data = await res.json(); textInput.value = data.text || ""; selectedItemId = item.id; selectedDownloadUrl = data.download_url; downloadLink.href = selectedDownloadUrl; downloadLink.classList.remove("hidden"); renderList(); } async function handleDelete() { const ids = Array.from(selectedIds); if (ids.length === 0) { alert("삭제할 항목을 선택하세요."); return; } const res = await fetch("/api/tts", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids }), }); if (!res.ok) { alert("삭제에 실패했습니다."); return; } const data = await res.json(); const deletedSet = new Set(data.deleted || []); items = items.filter((item) => !deletedSet.has(item.id)); textInput.value = ""; selectedItemId = null; selectedDownloadUrl = null; downloadLink.href = "#"; downloadLink.classList.add("hidden"); setEditMode(false); } saveBtn.addEventListener("click", handleSave); editBtn.addEventListener("click", () => setEditMode(true)); cancelBtn.addEventListener("click", () => setEditMode(false)); deleteBtn.addEventListener("click", handleDelete); downloadLink.addEventListener("click", (event) => { if (!selectedDownloadUrl) { event.preventDefault(); alert("다운로드할 항목을 선택하세요."); } }); loadList();