diff --git a/src/rights_filter/server/sqlite_store.py b/src/rights_filter/server/sqlite_store.py index e5762c8..63bbbe8 100644 --- a/src/rights_filter/server/sqlite_store.py +++ b/src/rights_filter/server/sqlite_store.py @@ -1218,6 +1218,11 @@ class CopyrighterStore: image_store: LocalSubmissionImageStore | None = None, ) -> dict[str, Any]: submission = self._get("submissions", submission_id) + score_before = int(submission.get("riskScore", 0) or 0) + evidence_before = { + str(item.get("id", "")): item + for item in self._evidence_by_submission().get(submission_id, []) + } submission["lastAnalysis"] = _now_label() self._put("submissions", submission_id, submission) evidence = { @@ -1251,6 +1256,35 @@ class CopyrighterStore: self.add_audit_event("rights.ops", "Analysis run created", submission_id, "operator rerun") self._rescore_submission(submission_id) self._sync_submission_provider_state() + + evidence_after = { + str(item.get("id", "")): item + for item in self._evidence_for_submission(submission_id) + } + rerun_marker_prefix = f"ev-{submission_id}-rerun-" + added_ids = [ + evidence_id + for evidence_id in evidence_after + if evidence_id not in evidence_before and not evidence_id.startswith(rerun_marker_prefix) + ] + removed_items = [ + evidence_before[evidence_id] + for evidence_id in evidence_before + if evidence_id not in evidence_after + ] + refreshed = self._get("submissions", submission_id) + refreshed["lastRerunDiff"] = { + "at": _now_label(), + "scoreBefore": score_before, + "scoreAfter": int(refreshed.get("riskScore", 0) or 0), + "addedEvidenceIds": added_ids, + "removedEvidenceIds": [str(item.get("id", "")) for item in removed_items], + "removedSummaries": [ + {"source": str(item.get("source", "")), "title": str(item.get("title", ""))} + for item in removed_items + ], + } + self._put("submissions", submission_id, refreshed) return self.review(submission_id) def run_auto_search( diff --git a/tests/operator_gui/test_static_workbench.py b/tests/operator_gui/test_static_workbench.py index 558d297..58556b7 100644 --- a/tests/operator_gui/test_static_workbench.py +++ b/tests/operator_gui/test_static_workbench.py @@ -841,3 +841,17 @@ def test_workbench_shows_detected_face_crop_strip(): assert "faceCrops" in script assert "얼굴 영역" in script assert "동일 인물 판정 아님" in script + + +def test_rerun_diff_summary_and_new_evidence_badges_are_rendered(): + html = _read(INDEX) + script = _read(APP_JS) + styles = _read(STYLES) + + assert 'id="rerun-diff-summary"' in html + assert "renderRerunDiffSummary" in script + assert "lastRerunDiff" in script + assert "evidence-new-chip" in script + assert "이번 재분석에서 제거됨" in script + assert ".evidence-new-chip" in styles + assert ".evidence-row-new" in styles diff --git a/tests/rights_filter/server/test_http_app.py b/tests/rights_filter/server/test_http_app.py index b296aa8..29ebfa4 100644 --- a/tests/rights_filter/server/test_http_app.py +++ b/tests/rights_filter/server/test_http_app.py @@ -1249,3 +1249,27 @@ def test_face_crops_are_empty_for_undetectable_images(tmp_path: Path): assert review.get("faceCrops", []) == [] finally: server.shutdown() + + +def test_rerun_enrichment_records_evidence_diff(tmp_path: Path): + static_dir, image_store, store = _fixtures(tmp_path) + server = build_server(host="127.0.0.1", port=0, store=store, image_store=image_store, static_dir=static_dir) + _start(server) + base = f"http://127.0.0.1:{server.server_port}" + + try: + initial = _json(base + "/api/submissions/SUB-API1/review") + assert "lastRerunDiff" not in initial + + rerun = _json(base + "/api/submissions/SUB-API1/rerun-enrichment", method="POST", body={}) + diff = rerun["lastRerunDiff"] + assert isinstance(diff["scoreBefore"], int) + assert isinstance(diff["scoreAfter"], int) + assert isinstance(diff["addedEvidenceIds"], list) + assert isinstance(diff["removedEvidenceIds"], list) + assert isinstance(diff["removedSummaries"], list) + assert diff["at"] + marker_prefix = "ev-SUB-API1-rerun-" + assert all(not evidence_id.startswith(marker_prefix) for evidence_id in diff["addedEvidenceIds"]) + finally: + server.shutdown() diff --git a/web/operator-gui/app.js b/web/operator-gui/app.js index 9925cf9..175081c 100644 --- a/web/operator-gui/app.js +++ b/web/operator-gui/app.js @@ -35,6 +35,8 @@ 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, @@ -1227,6 +1229,8 @@ function renderNoSelectedCase() { document.getElementById("file-facts").innerHTML = ""; document.getElementById("similar-strip").innerHTML = ""; document.getElementById("face-crop-strip").innerHTML = ""; + activeRerunAddedIds = new Set(); + document.getElementById("rerun-diff-summary").innerHTML = ""; document.getElementById("case-reasons").innerHTML = `