diff --git a/.env b/.env index 0ce2296..d741a33 100644 --- a/.env +++ b/.env @@ -4,4 +4,4 @@ DB_NAME=tts DB_USER=ncue DB_PASSWORD=ncue5004! TTS_ENGINE=mms -MMS_MODEL=facebook/mms-tts-kor +MMS_MODEL=facebook/mms-tts-kor \ No newline at end of file diff --git a/client/static/app.js b/client/static/app.js index 0878ce9..0c5343a 100644 --- a/client/static/app.js +++ b/client/static/app.js @@ -5,10 +5,47 @@ 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; + +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; @@ -69,6 +106,7 @@ async function handleSave() { return; } + startProgress(); const res = await fetch("/api/tts", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -78,12 +116,14 @@ async function handleSave() { 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) { diff --git a/client/static/styles.css b/client/static/styles.css index 1081ed8..da0ffe6 100644 --- a/client/static/styles.css +++ b/client/static/styles.css @@ -65,6 +65,21 @@ button.danger { color: #ffffff; } +.progress-wrap { + width: 100%; + height: 10px; + background: #f1d9a6; + border-radius: 6px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + width: 0%; + background: #f5a623; + transition: width 0.2s ease; +} + .tts-list { list-style: none; padding: 0; diff --git a/client/templates/index.html b/client/templates/index.html index 373f31a..7edcbed 100644 --- a/client/templates/index.html +++ b/client/templates/index.html @@ -12,6 +12,16 @@