-
-
${escapeHtml(formatProvenance(entry.provenance))}
-
${escapeHtml(formatKnowledgeStatus(entryStatus))}
+ 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(formatProvenance(entry.provenance))}
+ ${escapeHtml(formatKnowledgeStatus(entryStatus))}
+
+
+ ${escapeHtml(formatKnowledgeType(entry.type))}
+ 별칭: ${escapeHtml(aliases.join(", ") || "-")}
+ 키워드: ${escapeHtml(keywords.join(", ") || "-")}
+ 샘플 지문 ${(entry.sampleFingerprints || []).length}
+ ${statusMeta}
+ ${memoInline}
+
+
+ ${memoBlock}
+ ${editForm}
-
- ${memoBlock}
-
-
${watchlistActions}
-
- `;
- })
- .join("");
+
+ `;
+ })
+ .join("")
+ : `
조건에 맞는 기준 항목이 없습니다.
`;
renderCollectionCandidates();
}
@@ -3161,30 +3211,57 @@ function markEvidenceIrrelevant(evidenceId) {
-function toggleKnowledgeEntry(entryId) {
+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);
+ }
+}
- const entry = knowledgeEntries.find((item) => item.id === entryId);
- if (!entry) return;
+async function deactivateKnowledgeEntry(entryId) {
+ try {
+ const payload = await apiJson(`/api/knowledge/${encodeURIComponent(entryId)}/deactivate`, {
+ method: "POST",
+ body: JSON.stringify({ reason: "" }),
+ });
+ applyBootstrap(payload);
+ renderAll();
+ } catch (errorValue) {
+ showApiError(errorValue.message);
+ }
+}
- entry.active = !entry.active;
-
- auditEvents.unshift({
-
- timestamp: "방금",
-
- actor: "rights.ops",
-
- event: entry.active ? "Knowledge entry reactivated" : "Knowledge entry deactivated",
-
- object: entry.name,
-
- change: entry.active ? "active with memo" : "inactive",
-
- });
-
- renderAll();
+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);
+ }
}
@@ -3405,6 +3482,27 @@ function bindEvents() {
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);
@@ -3417,11 +3515,10 @@ function bindEvents() {
document.getElementById("mark-irrelevant").addEventListener("click", () => markEvidenceIrrelevant());
document.getElementById("disable-derived").addEventListener("click", () => {
-
- const automaticEntry = knowledgeEntries.find((entry) => entry.provenance === "automatic" && entry.active);
-
- if (automaticEntry) toggleKnowledgeEntry(automaticEntry.id);
-
+ const automaticEntry = knowledgeEntries.find(
+ (entry) => entry.provenance === "automatic" && entry.active && (entry.entryStatus || "confirmed") === "confirmed",
+ );
+ if (automaticEntry) void deactivateKnowledgeEntry(automaticEntry.id);
});
document.getElementById("open-correction").addEventListener("click", () => {
@@ -3490,14 +3587,30 @@ function bindEvents() {
- const knowledgeButton = event.target.closest("[data-toggle-kb]");
-
- if (knowledgeButton) {
-
- toggleKnowledgeEntry(knowledgeButton.dataset.toggleKb);
-
+ 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;
}
diff --git a/web/operator-gui/index.html b/web/operator-gui/index.html
index 17eb6fa..01d8290 100644
--- a/web/operator-gui/index.html
+++ b/web/operator-gui/index.html
@@ -332,6 +332,40 @@
등록된 기준
+
+
+
+
+
+
diff --git a/web/operator-gui/styles.css b/web/operator-gui/styles.css
index 11f43a6..61381c3 100644
--- a/web/operator-gui/styles.css
+++ b/web/operator-gui/styles.css
@@ -2768,3 +2768,24 @@ tbody tr.selected-row,
.suggested-query-actions {
margin-top: 8px;
}
+
+.knowledge-filter-bar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 12px;
+ margin-bottom: 12px;
+}
+
+.knowledge-edit-form {
+ display: grid;
+ gap: 8px;
+ margin-top: 8px;
+ padding: 10px;
+ border: 1px solid #d4d4d8;
+ border-radius: 8px;
+}
+
+.knowledge-edit-actions {
+ display: flex;
+ gap: 8px;
+}