// Part 14 of 15 of the operator console app script. // // Knowledge/correction/provider/evidence-status admin actions. // // 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. function addKnowledgeEntryFromCase() { // The control is labelled "주의 후보 보기" (view watchlist candidates), so show // the registered list filtered to watchlist — it previously opened the blank // manual-registration form, which is a different action entirely. switchView("knowledge"); switchKnowledgeTab("registered"); knowledgeFilters.status = "watchlist"; const statusFilter = document.getElementById("knowledge-status-filter"); if (statusFilter) statusFilter.value = "watchlist"; renderAll(); } async function deactivateCorrectionEntry(correctionId) { const correction = corrections.find((item) => item.id === correctionId); if (!correction) return; // Deactivate the derived knowledge entries through the real backend endpoint // so the change is persisted and audited server-side. The previous version // only mutated local state and appended a fake audit event, so the action was // silently discarded on the next refreshFromApi(). // Only entries that are still active need deactivating. Skipping already- // inactive ones avoids the server's "이미 비활성 상태입니다" 400, which used to // abort the whole loop on the first inactive entry. const entryIds = (correction.derivedEntries || []) .filter((entry) => entry && entry.id && entry.active) .map((entry) => entry.id); if (!entryIds.length) { showApiError("이 보정 항목에는 비활성화할 활성 파생 지식 항목이 없습니다."); return; } const failures = []; try { for (const entryId of entryIds) { try { await apiJson(`/api/knowledge/${encodeURIComponent(entryId)}/deactivate`, { method: "POST", body: JSON.stringify({ reason: "보정 파생 항목 일괄 비활성화" }), }); } catch (_perEntryError) { // One entry failing must not strand the rest. failures.push(entryId); } } } finally { // Always refresh so the UI reflects whatever was actually persisted, even on // a partial failure. Guard it so a refresh error isn't an unhandled rejection. try { await refreshFromApi(); } catch (_refreshError) { /* keep the per-entry failure summary below */ } } if (failures.length) { showApiError(`일부 파생 항목 비활성화에 실패했습니다 (${failures.length}건).`); } } async function toggleProvider(providerId) { const provider = providers.find((item) => item.id === providerId); if (!provider) return; try { await apiJson(`/api/providers/${encodeURIComponent(providerId)}`, { method: "PATCH", body: JSON.stringify({ enabled: !provider.enabled }), }); await refreshFromApi(); } catch (errorValue) { showApiError(errorValue.message); } } async function setEvidenceStatus(evidenceId, status, note = "") { const submission = getSelectedCase(); if (!submission || !evidenceId) return; try { await apiJson(`/api/evidence/${encodeURIComponent(evidenceId)}/status`, { method: "POST", body: JSON.stringify({ submission_id: submission.id, status: evidenceStatusPayload(status), note }), }); await refreshFromApi(); } catch (errorValue) { showApiError(errorValue.message); } } function markEvidenceIrrelevant(evidenceId) { const submission = getSelectedCase(); if (!submission) return; const fallback = submission.evidence.find((item) => item.contributed); const targetId = evidenceId || fallback?.id; void setEvidenceStatus(targetId, "ignored", "운영상 미사용 처리"); } async function saveKnowledgeEntryEdit(form) { const entryId = form.dataset.knowledgeEditForm; const body = { aliases: form.elements.aliases.value.split(",").map((item) => item.trim()).filter(Boolean), keywords: form.elements.keywords.value.split(",").map((item) => item.trim()).filter(Boolean), memo: form.elements.memo.value.trim(), }; try { const payload = await apiJson(`/api/knowledge/${encodeURIComponent(entryId)}`, { method: "PATCH", body: JSON.stringify(body), }); editingKnowledgeEntryId = ""; applyBootstrap(payload); renderAll(); } catch (errorValue) { showApiError(errorValue.message); } } async function deactivateKnowledgeEntry(entryId) { const reason = window.prompt("비활성 사유 메모(선택)"); if (reason === null) return; try { const payload = await apiJson(`/api/knowledge/${encodeURIComponent(entryId)}/deactivate`, { method: "POST", body: JSON.stringify({ reason: reason.trim() }), }); applyBootstrap(payload); renderAll(); } catch (errorValue) { showApiError(errorValue.message); } } async function reactivateKnowledgeEntry(entryId) { const reason = window.prompt("재활성 사유 메모(필수)") || ""; if (!reason.trim()) { showApiError("재활성에는 사유 메모가 필요합니다."); return; } try { const payload = await apiJson(`/api/knowledge/${encodeURIComponent(entryId)}/reactivate`, { method: "POST", body: JSON.stringify({ reason: reason.trim() }), }); applyBootstrap(payload); renderAll(); } catch (errorValue) { showApiError(errorValue.message); } } async function promoteWatchlistEntry(entryId) { try { const payload = await apiJson(`/api/knowledge/${encodeURIComponent(entryId)}/promote-watchlist`, { method: "POST", body: JSON.stringify({}), }); applyBootstrap(payload); renderAll(); } catch (errorValue) { showApiError(errorValue.message); } } async function excludeWatchlistEntry(entryId) { try { const payload = await apiJson(`/api/knowledge/${encodeURIComponent(entryId)}/exclude-watchlist`, { method: "POST", body: JSON.stringify({ reason: "오탐 확인" }), }); applyBootstrap(payload); renderAll(); } catch (errorValue) { showApiError(errorValue.message); } } async function emergencyDisableExternalProviders() { try { await apiJson("/api/providers/emergency-disable", { method: "POST", body: JSON.stringify({}), }); await refreshFromApi(); } catch (errorValue) { showApiError(errorValue.message); } } // 증거 그룹 전체를 한 번에 표시한다. // // 단건 API 만 있던 동안 운영자 되먹임은 순증 노동이었다 — 실 DB 제출물 하나를 // 100 에서 35 로 내리려면 google 증거 51행을 개별로 눌러야 했다. 확인 대화를 // 두지 않는 이유는 이 동작이 되돌릴 수 있고(행마다 다시 표시 가능), // 여기서 줄이려는 것이 바로 그 클릭이기 때문이다. 되돌릴 수 없는 느낌이 드는 // 것은 판정이고, 확인 대화는 거기에만 있다. async function setEvidenceGroupStatus(groupId, status, note = "") { const submission = getSelectedCase(); if (!submission || !groupId) return; const items = groupedEvidence(submission.evidence)[groupId] || []; const evidenceIds = items.map((item) => item.id).filter(Boolean); if (!evidenceIds.length) return; try { await apiJson("/api/evidence/status-batch", { method: "POST", body: JSON.stringify({ submission_id: submission.id, evidence_ids: evidenceIds, status: evidenceStatusPayload(status), note, }), }); await refreshFromApi(); } catch (errorValue) { showApiError(errorValue.message); } }