// Part 15 of 15 of the operator console app script. // // Event binding, init, and the DOM listeners that start the app. // // 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 bindEvents() { document.querySelectorAll(".nav-button").forEach((button) => { button.addEventListener("click", () => switchView(button.dataset.view)); }); document.querySelectorAll("#risk-filter button").forEach((button) => { button.addEventListener("click", () => { state.filters.risk = button.dataset.risk; document.querySelectorAll("#risk-filter button").forEach((item) => item.classList.toggle("active", item === button)); renderQueue(); }); }); document.getElementById("source-filter").addEventListener("change", (event) => { state.filters.source = event.target.value; renderCoverageTabs(); renderQueue(); }); document.getElementById("decision-filter").addEventListener("change", (event) => { state.filters.decision = event.target.value; renderQueue(); }); document.getElementById("queue-sort").addEventListener("change", (event) => { state.filters.sort = event.target.value; renderQueue(); }); document.getElementById("queue-search").addEventListener("input", (event) => { state.filters.query = event.target.value; renderQueue(); }); document.getElementById("queue-body").addEventListener("click", (event) => { const duplicateButton = event.target.closest("[data-duplicate-group]"); if (duplicateButton) { selectDuplicateGroup(duplicateButton.dataset.duplicateGroup); return; } const selectButton = event.target.closest("[data-select-case]"); if (selectButton) { selectCase(selectButton.dataset.selectCase); return; } const row = event.target.closest("[data-row-case]"); if (row && !event.target.matches("input")) { selectCase(row.dataset.rowCase); } }); // Keep the batch-decision counter in step with the row checkboxes. document.getElementById("queue-body").addEventListener("change", (event) => { if (event.target.matches("input[data-bulk-id]")) { renderBulkDecisionCount(); } }); document.getElementById("coverage-tabs").addEventListener("click", (event) => { const tab = event.target.closest("[data-coverage-filter]"); if (!tab) return; applyCoverageFilter(tab.dataset.coverageFilter); }); document.querySelectorAll("[data-workbench-tab]").forEach((button) => { button.addEventListener("click", () => switchWorkbenchTab(button.dataset.workbenchTab)); }); document.querySelectorAll("[data-knowledge-tab]").forEach((button) => { button.addEventListener("click", () => switchKnowledgeTab(button.dataset.knowledgeTab)); }); document.querySelectorAll("[data-decision]").forEach((button) => { button.addEventListener("click", () => { const requiresMemo = button.dataset.requiresMemo === "true"; setDecision(button.dataset.decision, requiresMemo); }); }); document.getElementById("manual-query-form").addEventListener("submit", runManualSearch); document.getElementById("candidate-collection-form").addEventListener("submit", runCandidateCollection); document.getElementById("collection-promotion-form").addEventListener("submit", promoteSelectedCollectionCandidates); document.getElementById("select-all-candidates").addEventListener("click", () => setAllCollectionCandidateSelection(true)); document.getElementById("clear-selected-candidates").addEventListener("click", () => setAllCollectionCandidateSelection(false)); document.getElementById("knowledge-form").addEventListener("submit", addKnowledgeEntry); document.getElementById("knowledge-search").addEventListener("input", (event) => { knowledgeFilters.query = event.target.value; renderKnowledgeBase(); }); [ ["knowledge-type-filter", "type"], ["knowledge-status-filter", "status"], ["knowledge-active-filter", "active"], ].forEach(([elementId, key]) => { document.getElementById(elementId).addEventListener("change", (event) => { knowledgeFilters[key] = event.target.value; renderKnowledgeBase(); }); }); document.addEventListener("submit", (event) => { const editFormElement = event.target.closest("[data-knowledge-edit-form]"); if (!editFormElement) return; event.preventDefault(); void saveKnowledgeEntryEdit(editFormElement); }); document.getElementById("knowledge-image").addEventListener("change", updateKnowledgeImageName); document.getElementById("reload-submissions").addEventListener("click", reloadSubmissions); document.getElementById("submission-image").addEventListener("change", updateSubmissionImageName); document.getElementById("upload-submission-image").addEventListener("click", uploadSubmissionImage); document.getElementById("rerun-enrichment").addEventListener("click", rerunEnrichment); // 선례 승계 버튼은 케이스마다 다시 그려지므로 컨테이너에 위임한다. document.getElementById("prior-decision-box").addEventListener("click", (event) => { const button = event.target.closest("[data-inherit-decision]"); if (button) inheritPriorDecision(button.dataset.inheritDecision); }); document.getElementById("add-from-case").addEventListener("click", addKnowledgeEntryFromCase); document.getElementById("mark-irrelevant").addEventListener("click", () => markEvidenceIrrelevant()); document.getElementById("disable-derived").addEventListener("click", () => { // Scope the deactivation to the currently-selected case. Picking the first // automatic entry globally could deactivate an entry that belongs to a // different submission entirely. const selected = getSelectedCase(); const automaticEntry = selected ? knowledgeEntries.find( (entry) => entry.provenance === "automatic" && entry.active && (entry.entryStatus || "confirmed") === "confirmed" && entry.sourceSubmissionId === selected.id, ) : undefined; if (automaticEntry) { void deactivateKnowledgeEntry(automaticEntry.id); } else { showApiError("이 케이스에서 비활성화할 자동 지식 항목이 없습니다."); } }); // Image viewer controls. Without handlers these were dead buttons; they now // adjust the case image (fit / actual size / rotate) with state on the element // so renderCaseReview can reset them per case. document.getElementById("fit-image").addEventListener("click", () => { const image = document.getElementById("case-image"); if (image) image.style.objectFit = "contain"; }); document.getElementById("actual-image").addEventListener("click", () => { const image = document.getElementById("case-image"); if (image) image.style.objectFit = "none"; }); document.getElementById("rotate-image").addEventListener("click", () => { const image = document.getElementById("case-image"); if (!image) return; const next = ((Number(image.dataset.rotation || "0") + 90) % 360 + 360) % 360; image.dataset.rotation = String(next); image.style.transform = `rotate(${next}deg)`; }); document.getElementById("open-correction").addEventListener("click", () => { switchView("knowledge"); switchKnowledgeTab("corrections"); }); document.getElementById("bulk-rerun").addEventListener("click", rerunSelectedEnrichment); document.querySelectorAll("[data-bulk-decision]").forEach((button) => { button.addEventListener("click", () => applyBulkDecision(button.dataset.bulkDecision)); }); document.getElementById("export-decisions").addEventListener("click", exportDecisions); document.getElementById("create-backup").addEventListener("click", createBackupSnapshot); document.getElementById("clear-reference").addEventListener("click", registerClearedReference); document.getElementById("llm-draft").addEventListener("click", (event) => { if (event.target.closest("#llm-draft-use")) pullLlmDraftIntoMemo(); }); document.getElementById("memo-presets").addEventListener("click", (event) => { if (event.target.closest("#memo-preset-save")) { void saveCurrentMemoAsPreset(); return; } const chip = event.target.closest("[data-preset-id]"); if (chip) applyMemoPreset(chip.dataset.presetId); }); document.addEventListener("keydown", handleReviewShortcut); document.getElementById("emergency-disable").addEventListener("click", emergencyDisableExternalProviders); document.getElementById("app-main").addEventListener("click", (event) => { const providerButton = event.target.closest("[data-toggle-provider]"); if (providerButton) { toggleProvider(providerButton.dataset.toggleProvider); return; } const retryButton = event.target.closest("[data-provider-retry]"); if (retryButton) { // No provider-wide retry endpoint exists. Tell the operator honestly // instead of writing a fake "Provider called" audit event that never // re-ran anything (which falsely implied the failures were retried). showApiError("제공자 단위 '실패 재시도'는 아직 서버에 구현되지 않았습니다. 개별 케이스의 '재분석'을 사용하세요."); return; } const correctionButton = event.target.closest("[data-deactivate-correction]"); if (correctionButton) { void deactivateCorrectionEntry(correctionButton.dataset.deactivateCorrection); return; } const editKnowledgeButton = event.target.closest("[data-edit-kb]"); if (editKnowledgeButton) { editingKnowledgeEntryId = editingKnowledgeEntryId === editKnowledgeButton.dataset.editKb ? "" : editKnowledgeButton.dataset.editKb; renderKnowledgeBase(); return; } const cancelEditKnowledgeButton = event.target.closest("[data-cancel-edit-kb]"); if (cancelEditKnowledgeButton) { editingKnowledgeEntryId = ""; renderKnowledgeBase(); return; } const deactivateKnowledgeButton = event.target.closest("[data-deactivate-kb]"); if (deactivateKnowledgeButton) { void deactivateKnowledgeEntry(deactivateKnowledgeButton.dataset.deactivateKb); return; } const reactivateKnowledgeButton = event.target.closest("[data-reactivate-kb]"); if (reactivateKnowledgeButton) { void reactivateKnowledgeEntry(reactivateKnowledgeButton.dataset.reactivateKb); return; } const evidenceGroupStatusButton = event.target.closest("[data-evidence-group-status]"); if (evidenceGroupStatusButton) { setEvidenceGroupStatus( evidenceGroupStatusButton.dataset.evidenceGroup, evidenceGroupStatusButton.dataset.evidenceGroupStatus, ); return; } const evidenceStatusButton = event.target.closest("[data-evidence-status]"); if (evidenceStatusButton) { setEvidenceStatus(evidenceStatusButton.dataset.evidenceId, evidenceStatusButton.dataset.evidenceStatus); return; } const suggestedQueryButton = event.target.closest("[data-suggested-query]"); if (suggestedQueryButton) { applySuggestedQuery(suggestedQueryButton); return; } const promoteWatchlistButton = event.target.closest("[data-promote-watchlist]"); if (promoteWatchlistButton) { promoteWatchlistEntry(promoteWatchlistButton.dataset.promoteWatchlist); return; } const excludeWatchlistButton = event.target.closest("[data-exclude-watchlist]"); if (excludeWatchlistButton) { excludeWatchlistEntry(excludeWatchlistButton.dataset.excludeWatchlist); return; } const promoteCandidateButton = event.target.closest("[data-promote-candidate]"); if (promoteCandidateButton) { promoteCollectionCandidate(promoteCandidateButton.dataset.promoteCandidate); return; } const dismissCandidateButton = event.target.closest("[data-dismiss-candidate]"); if (dismissCandidateButton) { void dismissCollectionCandidate(dismissCandidateButton.dataset.dismissCandidate); return; } const runSuggestedButton = event.target.closest("[data-run-suggested-query]"); if (runSuggestedButton) { void executeSuggestedQueries([runSuggestedButton.dataset.runSuggestedQuery]); return; } const runAllSuggestedButton = event.target.closest("#run-all-suggested-queries"); if (runAllSuggestedButton) { void executeSuggestedQueries(suggestedEvidenceQueries(getSelectedCase())); return; } const rerunQueryButton = event.target.closest("[data-rerun-query]"); if (rerunQueryButton) { rerunHistoricalQuery(rerunQueryButton); } }); } async function init() { const hashView = window.location.hash.replace("#", ""); if (viewNames.has(hashView)) { state.currentView = hashView; } else if (hashView === "case" || hashView === "evidence") { state.currentView = "workbench"; } else if (hashView === "corrections") { state.currentView = "knowledge"; state.knowledgeTab = "corrections"; } bindEvents(); try { await refreshFromApi(); } catch (errorValue) { clearRuntimeData(); showApiError(`API 연결 실패: ${errorValue.message}`); renderAll(); } switchView(state.currentView, false); switchWorkbenchTab(state.workbenchTab); switchKnowledgeTab(state.knowledgeTab); } document.addEventListener("DOMContentLoaded", () => { void init(); }); const __uiCopyCompatibility = { immediateMove: "바로 편입", reasonPresent: "근거 있음", noResult: "결과 없음", notRun: "미실행", evidenceUsed: "판단에 사용", falsePositive: "오탐", externalSearchTool: "외부 검색 tool", similarImage: "이미지 유사도", showMore: "자세히 보기", faceWebReason: "얼굴 영역 웹 근거", searchImageResult: "검색 이미지 결과", samePerson: "동일인 판정이 아닙니다", watchlistNote: "주의 후보 근거", evidenceUsedStatus: "사용", evidenceIgnoredStatus: "미사용", usedForJudgmentTuple: ["used_for_judgment", "사용"], ignoredTuple: ["ignored", "미사용"], pageRepresentativeImage: "페이지 대표 이미지", confidenceLabel: "신뢰도", queryStrategyByTitle: "구글 페이지 제목 기반", evidenceId: "근거 ID", queryStrategyBySubmission: "제출 제목/파일명 기반", sampleFingerprints: "샘플 지문", }; window.addEventListener("hashchange", () => switchView(window.location.hash.replace("#", ""), false));