// Part 1 of 15 of the operator console app script. // // Label imports and the in-memory client state every other part reads. // // 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. const { riskLabels, decisionLabels, providerLabels, sourceLabels, providerStatusLabels, candidateStatusLabels, evidenceStatusLabels, knowledgeStatusLabels, provenanceLabels, knowledgeTypeLabels, } = window.OperatorLabels; const retiredProviderIds = new Set(); const operatorSearchProviders = [ { id: "naver", label: providerLabels.naver }, { id: "google_search", label: providerLabels.google_search }, ]; const submissions = []; const providers = []; const knowledgeEntries = []; const searchCoverage = {}; let suggestedQueryRunSummary = null; // { caseId, message } — 현재 케이스의 마지막 추천 쿼리 실행 결과 const knowledgeFilters = { query: "", type: "all", status: "all", active: "all" }; let editingKnowledgeEntryId = ""; let activeRerunAddedIds = new Set(); // 현재 렌더 중인 케이스의 lastRerunDiff.addedEvidenceIds const DEFAULT_COVERAGE_THRESHOLDS = Object.freeze({ coverageGoodRate: 70, coverageWarnRate: 40, queryGoodRate: 70, queryWarnRate: 40, }); const coverageThresholds = { ...DEFAULT_COVERAGE_THRESHOLDS }; const collectionCandidates = []; const corrections = []; const auditEvents = []; const decisionMemoPresets = []; const state = { currentView: "queue", workbenchTab: "evidence", knowledgeTab: "collect", currentCollectionQuery: "", currentCollectionProvider: "", selectedCaseId: submissions[0]?.id || "", apiError: "", submissionQueue: null, intakeConflicts: [], importStatus: { tone: "idle", message: "로컬 제출 저장소 변경 대기 중", }, filters: { risk: "all", source: "all", decision: "all", sort: "risk", query: "", }, }; const viewNames = new Set(["queue", "workbench", "knowledge", "providers", "stats", "audit"]); // Bearer token for a server started with COPYRIGHTER_AUTH_TOKEN. Held in memory // only (never localStorage/sessionStorage): the first authenticated request makes // the server mint an HttpOnly session cookie, which is what subsequently // authorizes /media requests that cannot carry a header. let authToken = ""; // 판정·증거 표시를 누가 했는지 감사 로그에 남기는 운영자 이름. 인증이 아니라 기록용이다 // (권한은 공유 토큰이 정한다). 비밀이 아니므로 브라우저별로 기억해 매번 묻지 않는다. const OPERATOR_NAME_KEY = "copyrighter.operatorName"; let operatorName = (() => { try { return (window.localStorage.getItem(OPERATOR_NAME_KEY) || "").trim(); } catch (_error) { return ""; } })(); function setOperatorName(value) { operatorName = String(value || "").trim().slice(0, 40); try { window.localStorage.setItem(OPERATOR_NAME_KEY, operatorName); } catch (_error) { // 저장이 막힌 브라우저에서도 이번 세션 동안은 동작한다. } } // 이름 없이 판정하면 감사 로그의 "누가"가 비어 버린다. 판정 경로들이 이것으로 막는다. function requireOperatorName(showError) { if (operatorName) return true; showError("판정 전에 왼쪽 아래 '운영자' 칸에 이름을 입력하세요 — 감사 기록에 남습니다."); document.getElementById("operator-name")?.focus(); return false; } let pendingAuthPrompt = null;