// Part 12 of 15 of the operator console app script. // // Submission reload/import/upload and knowledge entry creation. // // Load order is significant: these are classic (non-module) scripts sharing one // global scope, and index.html must keep them in the order listed above/below. // Splitting is a pure line-range extraction of the former single app.js. async function rerunSelectedEnrichment() { const ids = selectedBulkSubmissionIds(); const button = document.getElementById("bulk-rerun"); if (!ids.length) { setImportStatus("재분석할 제출이 없습니다.", "bad"); return; } if (button) button.disabled = true; setImportStatus(`${ids.length}건 재분석을 시작합니다.`, "warn"); try { for (const [index, submissionId] of ids.entries()) { setImportStatus(`${index + 1}/${ids.length} 재분석 중 · ${submissionId}`, "warn"); await apiJson(`/api/submissions/${encodeURIComponent(submissionId)}/rerun-enrichment`, { method: "POST", body: JSON.stringify({}), }); } state.selectedCaseId = ids[0]; await refreshFromApi(); setImportStatus(`${ids.length}건 재분석 완료`, "ok"); } catch (errorValue) { // Best-effort refresh so already-rerun submissions aren't left stale in the // UI when the loop fails partway through. try { await refreshFromApi(); } catch (_refreshError) { /* keep original error */ } setImportStatus(errorValue.message, "bad"); } finally { if (button) button.disabled = false; } } function resetQueueFiltersForImport() { state.filters.risk = "all"; state.filters.source = "all"; state.filters.decision = "all"; state.filters.sort = "newest"; state.filters.query = ""; document.querySelectorAll("#risk-filter button").forEach((button) => { button.classList.toggle("active", button.dataset.risk === "all"); }); document.getElementById("source-filter").value = "all"; document.getElementById("decision-filter").value = "all"; document.getElementById("queue-sort").value = "newest"; document.getElementById("queue-search").value = ""; } function importedFolderStatusMessage(payload, fallbackLabel) { return window.OperatorSubmissionImport.importedFolderStatusMessage(payload, fallbackLabel); } function importedSubmissionStatusMessage(payload, submissionId) { return window.OperatorSubmissionImport.importedSubmissionStatusMessage(payload, submissionId); } async function reloadSubmissions() { const button = document.getElementById("reload-submissions"); const folderInput = document.getElementById("submission-folder"); const folderPath = (folderInput?.value || "").trim(); const endpoint = folderPath ? "/api/submissions/import-folder" : "/api/submissions/reload"; const body = folderPath ? { path: folderPath } : {}; button.disabled = true; setImportStatus("제출 폴더를 읽는 중입니다.", "warn"); try { const payload = await apiJson(endpoint, { method: "POST", body: JSON.stringify(body), }); // Snapshot the pre-reload ids BEFORE applyBootstrap replaces `submissions`, // otherwise the imported-diff below always compares against the new list and // is always empty (no new case is ever auto-selected). const existingIds = new Set(submissions.map((submission) => submission.id)); applyBootstrap(payload); if (folderPath) { folderInput.value = ""; const importedFolderSubmissions = payload.submissions || []; if (importedFolderSubmissions[0]) { state.selectedCaseId = importedFolderSubmissions[0].id; resetQueueFiltersForImport(); setImportStatus(importedFolderStatusMessage(payload, folderPath), "ok"); } else { setImportStatus("추가된 제출 없음", "idle"); } } else { const importedSubmissions = (payload.submissions || []).filter((submission) => !existingIds.has(submission.id)); if (importedSubmissions.length) { state.selectedCaseId = importedSubmissions[0].id; resetQueueFiltersForImport(); setImportStatus(importedSubmissionStatusMessage(payload, importedSubmissions[0].id), "ok"); } else { setImportStatus("추가된 제출 없음", "idle"); } } renderAll(); } catch (errorValue) { setImportStatus(errorValue.message, "bad"); } finally { button.disabled = false; } } function updateSubmissionImageName() { const input = document.getElementById("submission-image"); const target = document.getElementById("submission-image-name"); if (!input || !target) return; target.textContent = input.files[0]?.name || "선택된 파일 없음"; } async function uploadSubmissionImage() { const input = document.getElementById("submission-image"); const button = document.getElementById("upload-submission-image"); const file = input?.files?.[0]; if (!file) { setImportStatus("추가할 사진을 선택하세요.", "bad"); return; } button.disabled = true; setImportStatus("사진을 현재 제출 폴더에 넣는 중입니다.", "warn"); try { const image = await readKnowledgeImage(file); const payload = await apiJson("/api/submissions/upload-image", { method: "POST", body: JSON.stringify({ image }), }); applyBootstrap(payload); input.value = ""; updateSubmissionImageName(); if (payload.uploadedSubmissionId) { state.selectedCaseId = payload.uploadedSubmissionId; resetQueueFiltersForImport(); switchView("workbench"); switchWorkbenchTab("evidence"); } setImportStatus(`${payload.uploadedSubmissionId || file.name} 사진이 추가되었습니다. 새 심사 건으로 바로 선택했습니다.`, "ok"); renderAll(); } catch (errorValue) { setImportStatus(errorValue.message, "bad"); } finally { button.disabled = false; } } async function readKnowledgeImage(file) { return window.OperatorSubmissionImport.fileToImagePayload(file); } function updateKnowledgeImageName() { const input = document.getElementById("knowledge-image"); const target = document.getElementById("knowledge-image-name"); if (!input || !target) return; target.textContent = input.files[0]?.name || "선택된 파일 없음"; } async function addKnowledgeEntry(event) { event.preventDefault(); const name = document.getElementById("knowledge-name").value.trim(); const type = document.getElementById("knowledge-type").value; const aliases = document .getElementById("knowledge-aliases") .value.split(",") .map((item) => item.trim()) .filter(Boolean); const keywords = document .getElementById("knowledge-keywords") .value.split(",") .map((item) => item.trim()) .filter(Boolean); const memo = document.getElementById("knowledge-memo").value.trim(); const imageInput = document.getElementById("knowledge-image"); const status = document.getElementById("knowledge-entry-status"); const submitButton = event.target.querySelector('button[type="submit"]'); if (!name || !memo) return; submitButton.disabled = true; status.textContent = "기준 DB에 저장하는 중입니다."; try { const image = await readKnowledgeImage(imageInput.files[0]); const payload = await apiJson("/api/knowledge/manual", { method: "POST", body: JSON.stringify({ name, type, aliases, keywords, memo, image }), }); applyBootstrap(payload); event.target.reset(); updateKnowledgeImageName(); status.textContent = image ? "참조 이미지와 이미지 지문을 저장했습니다." : "기준 DB에 저장했습니다."; renderAll(); } catch (errorValue) { status.textContent = errorValue.message; } finally { submitButton.disabled = false; } }