// Part 10 of 15 of the operator console app script. // // Memo presets, decision export, backup, cleared reference, LLM draft. // // 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. // --- Decision memo presets --------------------------------------------- function renderMemoPresets() { const target = document.getElementById("memo-presets"); if (!target) return; const chips = decisionMemoPresets .map( (preset) => ``, ) .join(""); target.innerHTML = `${chips}`; } function applyMemoPreset(presetId) { const preset = decisionMemoPresets.find((item) => item.id === presetId); const memo = document.getElementById("decision-memo"); if (!preset || !memo) return; memo.value = memo.value.trim() ? `${memo.value.trim()} ${preset.body}` : preset.body; memo.focus(); } async function saveCurrentMemoAsPreset() { const memo = document.getElementById("decision-memo"); const error = document.getElementById("memo-error"); const body = memo?.value.trim() || ""; if (!body) { if (error) error.textContent = "저장할 메모 내용이 없습니다."; return; } const label = window.prompt("프리셋 이름 (예: 연예인 초상)", body.slice(0, 20)); if (!label) return; try { const presets = await apiJson("/api/decision-memo-presets", { method: "POST", body: JSON.stringify({ label, body }), }); replaceCollection(decisionMemoPresets, presets); renderMemoPresets(); if (error) error.textContent = ""; } catch (errorValue) { if (error) error.textContent = `프리셋 저장 실패: ${errorValue.message}`; } } // --- Decision export ---------------------------------------------------- async function exportDecisions() { const status = document.getElementById("bulk-decision-status"); const audience = window.confirm( "내부 보관용(증거·점수 포함)으로 내보내려면 확인, 신청 채널 전달용(판정·메모만)으로 내보내려면 취소를 누르세요.", ) ? "internal" : "applicant"; try { const payload = await apiJson( `/api/decisions/export?audience=${encodeURIComponent(audience)}`, ); if (!payload.rows.length) { if (status) status.textContent = "내보낼 판정 완료 케이스가 없습니다."; return; } downloadTextFile( `copyrighter-decisions-${audience}.csv`, payload.csv, "text/csv;charset=utf-8", ); if (status) status.textContent = `${payload.rows.length}건을 ${audience === "internal" ? "내부 보관용" : "신청 채널 전달용"} CSV로 내보냈습니다.`; } catch (errorValue) { if (status) status.textContent = `내보내기 실패: ${errorValue.message}`; } } function downloadTextFile(filename, text, mimeType) { // BOM so Excel opens the Korean CSV as UTF-8 instead of the system codepage. const blob = new Blob([`${text}`], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } // --- Backup ------------------------------------------------------------- async function createBackupSnapshot() { const status = document.getElementById("bulk-decision-status"); const button = document.getElementById("create-backup"); if (button) button.disabled = true; if (status) status.textContent = "백업 스냅샷을 만드는 중…"; try { const result = await apiJson("/api/admin/backup", { method: "POST" }); const megabytes = (Number(result.sizeBytes || 0) / (1024 * 1024)).toFixed(1); if (status) { status.textContent = `백업 완료: ${result.file} (${megabytes}MB · 미디어 ${result.mediaFiles}개)`; } await refreshFromApi(); } catch (errorValue) { if (status) status.textContent = `백업 실패: ${errorValue.message}`; } finally { if (button) button.disabled = false; } } // --- Cleared reference (positive knowledge) ----------------------------- async function registerClearedReference() { const submission = getSelectedCase(); if (!submission) return; const label = window.prompt( "클리어 기준 이름 (예: 자사 보유 스톡 · 라이선스 확인됨)", submission.title || submission.id, ); if (label === null) return; const note = window.prompt("메모(선택) — 왜 클리어인지", "") || ""; try { const payload = await apiJson( `/api/submissions/${encodeURIComponent(submission.id)}/clear-reference`, { method: "POST", body: JSON.stringify({ label: label.trim(), note }) }, ); applyBootstrap(payload); renderAll(); } catch (errorValue) { showApiError(errorValue.message); } } // --- LLM draft (persist + show + pull into memo) ------------------------ function llmSummaryFor(submission) { const evidence = submission && Array.isArray(submission.evidence) ? submission.evidence : []; // The freshest LLM summary that actually carries body text. return ( evidence .filter((item) => item.source === "llm" && String(item.summary || "").trim()) .slice(-1)[0] || null ); } let currentLlmDraftText = ""; function renderLlmDraft(submission) { const box = document.getElementById("llm-draft"); if (!box) return; const summary = llmSummaryFor(submission); if (!summary) { box.hidden = true; box.innerHTML = ""; currentLlmDraftText = ""; return; } currentLlmDraftText = String(summary.summary || ""); const urls = Array.isArray(summary.sourceUrls) ? summary.sourceUrls : []; const sourceNote = urls.length ? `연결 근거 ${urls.length}건` : "연결 근거 없음"; box.hidden = false; box.innerHTML = `
내부 요약(초안) ${escapeHtml(sourceNote)} · 최종 판정은 운영자

${escapeHtml(currentLlmDraftText)}

`; } function pullLlmDraftIntoMemo() { const memo = document.getElementById("decision-memo"); if (!memo || !currentLlmDraftText) return; memo.value = memo.value.trim() ? `${memo.value.trim()}\n${currentLlmDraftText}` : currentLlmDraftText; memo.focus(); }