// Part 7 of 15 of the operator console app script. // // Knowledge, candidates, corrections, providers, audit panels and renderAll. // // 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 filteredKnowledgeEntries() { const query = knowledgeFilters.query.trim().toLowerCase(); return knowledgeEntries.filter((entry) => { const typeOk = knowledgeFilters.type === "all" || entry.type === knowledgeFilters.type; const statusOk = knowledgeFilters.status === "all" || (entry.entryStatus || "confirmed") === knowledgeFilters.status; const activeOk = knowledgeFilters.active === "all" || (knowledgeFilters.active === "active") === Boolean(entry.active); const haystack = [entry.name, ...(entry.aliases || []), ...(entry.keywords || [])].join(" ").toLowerCase(); const queryOk = !query || haystack.includes(query); return typeOk && statusOk && activeOk && queryOk; }); } function renderKnowledgeBase() { const entries = filteredKnowledgeEntries(); document.getElementById("knowledge-list").innerHTML = entries.length ? entries .map((entry) => { const aliases = entry.aliases || []; const keywords = entry.keywords || []; const entryStatus = entry.entryStatus || "confirmed"; const statusMeta = entryStatus === "watchlist" ? `원 케이스: ${escapeHtml(entry.sourceSubmissionId || "-")}미래 검출 ${escapeHtml(entry.contributionCount || 0)}건` : ""; const memoInline = entry.memo && entry.memo.startsWith("외부 후보 수집에서 반영:") ? `${escapeHtml(entry.memo)}` : ""; const memoBlock = memoInline ? "" : `

${escapeHtml(entry.memo)}

`; const lifecycleActions = entryStatus === "watchlist" ? ` ` : entryStatus === "excluded" ? "" : entry.active ? `` : ``; const editForm = editingKnowledgeEntryId === entry.id ? `
` : ""; return `
${ entry.imageAsset ? `${escapeHtml(entry.name)} 참조 이미지` : `` }
${escapeHtml(entry.name)}
${escapeHtml(formatProvenance(entry.provenance))} ${escapeHtml(formatKnowledgeStatus(entryStatus))}
${escapeHtml(formatKnowledgeType(entry.type))} 별칭: ${escapeHtml(aliases.join(", ") || "-")} 키워드: ${escapeHtml(keywords.join(", ") || "-")} 샘플 지문 ${(entry.sampleFingerprints || []).length} ${statusMeta} ${memoInline}
${memoBlock} ${editForm}
${lifecycleActions}
`; }) .join("") : `
조건에 맞는 기준 항목이 없습니다.
`; renderCollectionCandidates(); } function renderCollectionCandidates() { const target = document.getElementById("collection-candidates"); if (!target) return; const visibleCandidates = visibleCollectionCandidates(); target.innerHTML = visibleCandidates.length ? visibleCandidates .map((candidate) => { const promoted = candidate.status === "promoted"; const canPromote = !promoted && (candidate.sampleFingerprints || []).length; return `
${ candidate.imageAsset ? `${escapeHtml(candidate.title || candidate.query)} 후보 이미지` : `` }
${escapeHtml(providerLabels[candidate.provider] || candidate.provider || "검색")} ${escapeHtml(candidate.title || candidate.query)}
${escapeHtml(candidate.query)} ${escapeHtml(formatCandidateSourceType(candidate.sourceCandidateType))} 순위 ${escapeHtml(candidate.rank || "-")} ${escapeHtml(formatCandidateStatus(candidate.status))}
${ candidate.sourceUrl ? `출처 열기` : "" }
`; }) .join("") : `
검색어와 제공자를 선택하면 기준 DB 후보가 표시됩니다.
`; } function visibleCollectionCandidates() { return collectionCandidates.filter( (candidate) => candidate.status === "candidate" && candidate.query === state.currentCollectionQuery && (!state.currentCollectionProvider || candidate.provider === state.currentCollectionProvider), ); } function renderCorrections() { document.getElementById("corrections-list").innerHTML = corrections .map( (correction) => `
보정 ${escapeHtml(correction.decisionId)} · ${escapeHtml(correction.submissionId)}
${escapeHtml(correction.previous)} -> ${escapeHtml(correction.current)} ${escapeHtml(correction.operator)} ${escapeHtml(correction.timestamp)}

${escapeHtml(correction.reason)}

${(correction.derivedEntries || []) .map( (entry) => `${escapeHtml(entry.name)} · ${entry.active ? "활성" : "비활성"}`, ) .join("")}
`, ) .join(""); } function renderProviderEnvStatus(provider) { const requiredEnv = Array.isArray(provider.requiredEnv) ? provider.requiredEnv : []; if (!requiredEnv.length) return ""; const configuredEnv = provider.configuredEnv || {}; const missing = requiredEnv.filter((key) => !configuredEnv[key]); if (!missing.length) { return `환경 변수 확인됨: ${escapeHtml(requiredEnv.join(", "))}`; } return `누락된 환경 변수: ${escapeHtml(missing.join(", "))}`; } function visibleProviderControls() { return providers.filter((provider) => !retiredProviderIds.has(provider.id)); } function renderProviderControls() { document.getElementById("providers-list").innerHTML = visibleProviderControls() .map((provider) => { const percent = provider.quota ? Math.min(100, Math.round((provider.usage / provider.quota) * 100)) : 0; const providerName = formatProviderName(provider); const envStatus = renderProviderEnvStatus(provider); return `
${escapeHtml(providerName)}
${provider.enabled ? "활성" : "중지"} ${escapeHtml(formatReason(provider.compliance))} 최근 성공: ${escapeHtml(formatReason(provider.lastSuccess))} 최근 실패: ${escapeHtml(formatReason(provider.lastFailure))}
${escapeHtml(formatReason(provider.boundary))} ${envStatus}
${provider.usage} / ${provider.quota == null ? "무제한" : provider.quota}${percent}%
`; }) .join(""); } function renderAuditLog() { document.getElementById("audit-body").innerHTML = auditEvents .map( (event) => ` ${escapeHtml(event.timestamp)} ${escapeHtml(event.actor)} ${escapeHtml(formatReason(event.event))} ${escapeHtml(formatReason(event.object))} ${escapeHtml(formatReason(event.change))} `, ) .join(""); } function renderProviderPulse() { const visibleProviders = visibleProviderControls(); const failed = visibleProviders.filter((provider) => provider.enabled && provider.lastFailure !== "없음").length; const disabled = visibleProviders.filter((provider) => !provider.enabled).length; const className = disabled || failed ? "warn" : "ok"; document.getElementById("provider-pulse").innerHTML = ` 외부 검색 tool ${visibleProviders.filter((provider) => provider.enabled).length}/${visibleProviders.length} 활성 · 실패 ${failed} `; } function renderAll() { renderOperatorSearchProviderOptions(); renderProviderPulse(); renderCoverageTabs(); renderImportStatus(); renderQueue(); renderCaseReview(); renderEvidenceSearch(); renderKnowledgeBase(); labelCollectionCandidateActions(); renderCorrections(); renderProviderControls(); renderAuditLog(); renderMemoPresets(); renderBulkDecisionCount(); renderReviewProgress(); } function labelCollectionCandidateActions() { document.querySelectorAll("[data-promote-candidate]").forEach((button) => { const candidate = collectionCandidates.find((item) => item.id === button.dataset.promoteCandidate); button.textContent = candidate?.status === "promoted" ? "반영됨" : "기준 DB 반영"; }); }