const statusBar = document.getElementById("statusBar"); const statusLabel = document.getElementById("statusLabel"); const searchForm = document.getElementById("searchForm"); const searchQuery = document.getElementById("searchQuery"); const searchResults = document.getElementById("searchResults"); const searchWarning = document.getElementById("searchWarning"); const dropzone = document.getElementById("dropzone"); const fileInput = document.getElementById("fileInput"); const uploadResult = document.getElementById("uploadResult"); const downloadForm = document.getElementById("downloadForm"); const downloadUrl = document.getElementById("downloadUrl"); const startTime = document.getElementById("startTime"); const endTime = document.getElementById("endTime"); const downloadResult = document.getElementById("downloadResult"); const cardTemplate = document.getElementById("searchCardTemplate"); function setStatus(label, progress) { statusLabel.textContent = label; statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`; } function connectWS() { const protocol = window.location.protocol === "https:" ? "wss" : "ws"; const socket = new WebSocket(`${protocol}://${window.location.host}/ws`); socket.addEventListener("message", (event) => { const payload = JSON.parse(event.data); if (payload.event !== "progress") { return; } const data = payload.data; setStatus(`${data.type || "task"}: ${data.status}`, Number(data.progress ?? 0)); if (data.type === "upload" && data.status === "completed") { uploadResult.textContent = `${data.filename} saved successfully`; } if (data.type === "download" && data.status === "completed") { downloadResult.textContent = data.output || "download completed"; } if (data.status === "error") { downloadResult.textContent = data.message || "task failed"; } }); socket.addEventListener("close", () => { setTimeout(connectWS, 1000); }); } async function api(path, options = {}) { const response = await fetch(path, options); const data = await response.json().catch(() => ({})); if (!response.ok) { const error = new Error(data.error || "request failed"); error.status = response.status; error.data = data; throw error; } return data; } function renderResults(results) { searchResults.innerHTML = ""; for (const item of results) { const node = cardTemplate.content.firstElementChild.cloneNode(true); node.href = item.link; node.querySelector("img").src = item.thumbnailUrl; node.querySelector("img").alt = item.title; node.querySelector("h3").textContent = item.title; node.querySelector("p").textContent = item.reason; node.querySelector(".source-badge").textContent = item.source; searchResults.appendChild(node); } } searchForm.addEventListener("submit", async (event) => { event.preventDefault(); setStatus("searching", 20); searchWarning.classList.add("hidden"); try { const data = await api("/api/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: searchQuery.value }), }); renderResults(data.results || []); if (data.warning) { searchWarning.textContent = data.warning; searchWarning.classList.remove("hidden"); } setStatus("search complete", 100); } catch (error) { searchWarning.textContent = error.message; searchWarning.classList.remove("hidden"); setStatus("search failed", 100); } }); async function uploadFile(file) { const formData = new FormData(); formData.append("file", file); uploadResult.textContent = "uploading..."; await api("/api/upload", { method: "POST", body: formData }); } dropzone.addEventListener("dragover", (event) => { event.preventDefault(); dropzone.classList.add("border-white/60", "bg-white/[0.08]"); }); dropzone.addEventListener("dragleave", () => { dropzone.classList.remove("border-white/60", "bg-white/[0.08]"); }); dropzone.addEventListener("drop", async (event) => { event.preventDefault(); dropzone.classList.remove("border-white/60", "bg-white/[0.08]"); const file = event.dataTransfer.files[0]; if (file) { await uploadFile(file); } }); fileInput.addEventListener("change", async () => { const [file] = fileInput.files; if (file) { await uploadFile(file); } }); downloadForm.addEventListener("submit", async (event) => { event.preventDefault(); downloadResult.textContent = "checking duplicate history..."; try { const dup = await api(`/api/history/check?url=${encodeURIComponent(downloadUrl.value)}`); let force = false; if (dup.exists) { force = window.confirm("동일 URL 다운로드 이력이 있습니다. 계속 진행할까요?"); if (!force) { downloadResult.textContent = "cancelled"; return; } } const data = await api("/api/download", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: downloadUrl.value, start: startTime.value, end: endTime.value, force, }), }); downloadResult.textContent = data.message; } catch (error) { downloadResult.textContent = error.message; } }); connectWS(); setStatus("idle", 0);