feat: 선례 승계 — 재제출을 다시 심사하지 않는다
8/8 실효성 평가의 결론은 "검색·기록 노동은 줄여주나 심사 건수는 전혀 못 줄인다"
였다. 이 커밋은 그 축을 직접 건드리는 첫 기능이다.
중복 탐지는 이 도구에서 실증으로 검증된 유일한 판별 기능이다 — 재저장(JPEG q90)
/80%축소/50%축소/밝기+10% 변형 24건 중 24건 그룹핑, 같은 24건에서 SHA-256
완전일치는 0건. 그런데 그 결과를 쓰는 곳이 없었다. 서버는 store_duplicates.py
에서 priorDecisions 와 conflicting 을 계산해 내려보내는데, 콘솔은 그것을 큐
배지의 title 속성에만 넣었다. 마우스를 올린 사람만 "이거 지난주에 반려한
이미지"임을 알 수 있었고, 재제출은 처음부터 다시 심사됐다.
서버는 건드리지 않았다. 자리만 옮겼다.
큐 배지 "동일 이미지 3건 · 선례 반려" (title 이 아니라 본문에)
판정 패널 결정 메모 바로 위에 선례 배너 + [선례 승계] 1타
승계는 별도 판정 경로를 만들지 않는다. 메모를 채워 기존 setDecision 을 그대로
타므로 반려 메모 필수 검사 · 판정 시점 스냅샷 · 감사 이벤트가 유지되고, 승계
사실은 메모로 감사에 남는다("선례 승계: 동일 이미지 SUB-OLD1, SUB-OLD2 와 같은
판정").
판정 불일치는 승계 대상이 아니다. 같은 이미지에 서로 다른 판정이 있다는 것은
자동 처리할 상황이 아니라 사람이 봐야 하는 신호이므로, conflicting 이면 버튼을
아예 내지 않고 색을 갈라 표시한다.
실렌더 확인(Playwright 1440x900, 실제 서버 + 부트스트랩 스텁):
세 상태(승계 가능 / 판정 불일치 / 선례 없음)의 배너 문구와 버튼 유무가 의도대로,
승계 클릭 시 POST 본문이 위 메모 그대로, 콘솔 에러 0, 가로 오버플로 없음.
검증: ruff clean, pytest 622 passed (정적 3건 추가).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
This commit is contained in:
parent
624f435b82
commit
4e8e4b3181
9 changed files with 227 additions and 1 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
# Project INDEX
|
# Project INDEX
|
||||||
|
|
||||||
|
- 2026-09-21 · **선례 승계** — 재제출을 다시 심사하지 않게 한다. 8/8 실효성 평가의 미해결 결론("검색·기록 노동은 줄여주나 심사 건수는 못 줄인다")을 직접 겨냥한 첫 기능이다. 중복 탐지는 이 도구에서 실증으로 검증된 유일한 판별 기능인데(재저장·80%/50%축소·밝기 변형 24/24 그룹핑, 같은 24건 SHA-256 일치 0건), 서버가 계산해 내려보내던 `priorDecisions`·`conflicting` 이 **큐 배지의 title 속성에만** 있었다 — 마우스를 올린 사람만 보는 자리라 아무것도 막지 못했다. 서버 변경 없이 콘솔만 고쳤다: 배지 본문에 선례 표시, 판정 패널(결정 메모 바로 위)에 선례 배너 + `선례 승계` 1타. 승계는 별도 경로를 만들지 않고 메모를 채워 기존 `setDecision` 을 타므로 반려 메모 필수·판정 스냅샷·감사 이벤트가 그대로 유지된다. **판정 불일치(conflicting)면 승계를 제안하지 않는다** — 그때가 사람이 봐야 하는 상황이다. 실렌더 확인(Playwright, 1440×900): 세 상태(승계 가능/불일치/선례 없음) 배너·버튼 유무가 의도대로였고 승계 클릭 시 `{"decision":"rejected","memo":"선례 승계: 동일 이미지 SUB-OLD1, SUB-OLD2 와 같은 판정"}` 전송, 콘솔 에러 0. **ruff clean · pytest 622 passed**(정적 3건 추가)
|
||||||
|
|
||||||
- 2026-09-21 · 다이어그램 용도를 **내부 문서로 확정**하고 그 기준으로 부채를 재판정. 한글 폰트 미번들·영어 뷰어 UI 는 archify 한계라 spec 으로 못 고치는데, 사내 머신에는 한글 폰트가 있으므로 low 로 내렸다(인수물로 승격하면 둘이 먼저 blocker — PROJECT_MAP 다이어그램 절에 승격 조건으로 명시). 이어서 **코드↔그림 리터럴 드리프트 게이트** `tests/test_diagram_facts.py` 19건 신설 — 그림에 하드카피된 코드 유래 값(SCORER_VERSION · SCHEMA_VERSION · 믹스인 8개 · `app-*.js` 15개 · 바디 10MB · 인증 10회/60초 · 프롬프트 12행 · 풀 상한 60/20 · 기본 모델 · 런타임 의존성 3종)을 실제 코드 상수와 대조하고, 상수가 아닌 "지문 유사도 0.9+ → 80점" 은 실제로 채점해 확인한다. SCORER_VERSION v3→v4 와 MAX_PROMPT_EVIDENCE 12→16 을 실제로 넣어 **영향받는 세 다이어그램 전부에서 차단됨**을 실증. 발견 가능성도 정리 — README 문서 색인에 등재, `docs/diagrams/README.md` 신설(폴더 안에 '직접 수정 금지'·재생성 명령·CSP 주의). **ruff clean · pytest 619 passed**(사실 게이트 +19)
|
- 2026-09-21 · 다이어그램 용도를 **내부 문서로 확정**하고 그 기준으로 부채를 재판정. 한글 폰트 미번들·영어 뷰어 UI 는 archify 한계라 spec 으로 못 고치는데, 사내 머신에는 한글 폰트가 있으므로 low 로 내렸다(인수물로 승격하면 둘이 먼저 blocker — PROJECT_MAP 다이어그램 절에 승격 조건으로 명시). 이어서 **코드↔그림 리터럴 드리프트 게이트** `tests/test_diagram_facts.py` 19건 신설 — 그림에 하드카피된 코드 유래 값(SCORER_VERSION · SCHEMA_VERSION · 믹스인 8개 · `app-*.js` 15개 · 바디 10MB · 인증 10회/60초 · 프롬프트 12행 · 풀 상한 60/20 · 기본 모델 · 런타임 의존성 3종)을 실제 코드 상수와 대조하고, 상수가 아닌 "지문 유사도 0.9+ → 80점" 은 실제로 채점해 확인한다. SCORER_VERSION v3→v4 와 MAX_PROMPT_EVIDENCE 12→16 을 실제로 넣어 **영향받는 세 다이어그램 전부에서 차단됨**을 실증. 발견 가능성도 정리 — README 문서 색인에 등재, `docs/diagrams/README.md` 신설(폴더 안에 '직접 수정 금지'·재생성 명령·CSP 주의). **ruff clean · pytest 619 passed**(사실 게이트 +19)
|
||||||
|
|
||||||
- 2026-09-21 · 다이어그램 감사 후 **드리프트 게이트 신설**. 에이전트 6축 감사가 주간 한도로 절반(2축)만 완주했고 검증 단계도 죽어, 완주분 15건을 저널에서 복구한 뒤 결정적 주장은 직접 재검증했다. 확정된 구멍 — 그림 4종의 재생성 절차가 **문서대로는 실행 불가**(`archify` 실행파일이 PATH 에 없음), 소스↔산출물 동기화를 보는 장치 0개, 사이드카 24개가 개발 머신 절대경로를 담은 채 미추적·비ignore, 임베드 폰트에 **한글 구간 0건**(`U+AC00` 매치 0 · 폴백은 중국어 폰트뿐). 실수정: `scripts/rebuild-diagrams.ps1`(validate→deliver→visual-check→manifest, 실행 확인)과 `scripts/update_diagram_manifest.py`, 수신증 `docs/diagrams/manifest.json`, 게이트 `tests/test_diagram_sync.py` 14건(HTML 직접 수정·spec 만 수정 두 시나리오 모두 실제로 차단됨을 실증), `.gitignore` 에 사이드카 제외(미추적 24→0), `.gitattributes` 에 `linguist-generated -diff merge=binary`. 재생성해도 4종 sha256 불변 — 바이트 결정성 재확인. 한글 폰트·영어 뷰어 UI·architecture 좌표 하드코딩·lifecycle 손튜닝 8개소는 PROJECT_MAP 부채 표에 등재. **ruff clean · pytest 600 passed**(게이트 +14)
|
- 2026-09-21 · 다이어그램 감사 후 **드리프트 게이트 신설**. 에이전트 6축 감사가 주간 한도로 절반(2축)만 완주했고 검증 단계도 죽어, 완주분 15건을 저널에서 복구한 뒤 결정적 주장은 직접 재검증했다. 확정된 구멍 — 그림 4종의 재생성 절차가 **문서대로는 실행 불가**(`archify` 실행파일이 PATH 에 없음), 소스↔산출물 동기화를 보는 장치 0개, 사이드카 24개가 개발 머신 절대경로를 담은 채 미추적·비ignore, 임베드 폰트에 **한글 구간 0건**(`U+AC00` 매치 0 · 폴백은 중국어 폰트뿐). 실수정: `scripts/rebuild-diagrams.ps1`(validate→deliver→visual-check→manifest, 실행 확인)과 `scripts/update_diagram_manifest.py`, 수신증 `docs/diagrams/manifest.json`, 게이트 `tests/test_diagram_sync.py` 14건(HTML 직접 수정·spec 만 수정 두 시나리오 모두 실제로 차단됨을 실증), `.gitignore` 에 사이드카 제외(미추적 24→0), `.gitattributes` 에 `linguist-generated -diff merge=binary`. 재생성해도 4종 sha256 불변 — 바이트 결정성 재확인. 한글 폰트·영어 뷰어 UI·architecture 좌표 하드코딩·lifecycle 손튜닝 8개소는 PROJECT_MAP 부채 표에 등재. **ruff clean · pytest 600 passed**(게이트 +14)
|
||||||
|
|
|
||||||
|
|
@ -969,3 +969,40 @@ def test_operator_gui_explains_a_retained_case_whose_source_file_is_gone():
|
||||||
assert "MISSING_ASSET_NOTE" in script
|
assert "MISSING_ASSET_NOTE" in script
|
||||||
assert "원본 파일 없음" in script
|
assert "원본 파일 없음" in script
|
||||||
assert ".thumb-missing" in styles
|
assert ".thumb-missing" in styles
|
||||||
|
def test_operator_gui_surfaces_a_prior_decision_on_the_same_image():
|
||||||
|
# 중복 탐지는 이 도구에서 실증으로 검증된 유일한 판별 기능인데, 그 결과인
|
||||||
|
# priorDecisions 는 큐 배지의 title 속성에만 있었다 — 마우스를 올린 사람만
|
||||||
|
# 볼 수 있는 자리라 재제출을 처음부터 다시 심사하는 것을 막지 못했다.
|
||||||
|
script = _read_app()
|
||||||
|
markup = _read(INDEX)
|
||||||
|
styles = _read(STYLES)
|
||||||
|
|
||||||
|
assert "priorDecisionSummary" in script
|
||||||
|
assert "setPriorDecisionBox" in script
|
||||||
|
assert 'id="prior-decision-box"' in markup
|
||||||
|
assert ".prior-decision" in styles
|
||||||
|
|
||||||
|
# 배지 본문에도 선례가 실려야 한다(title 에만 있으면 훑는 동안 안 보인다).
|
||||||
|
assert "priorNote" in script
|
||||||
|
|
||||||
|
|
||||||
|
def test_operator_gui_inherits_a_prior_decision_through_the_normal_decision_path():
|
||||||
|
# 승계가 별도 판정 경로를 만들면 반려 메모 필수 검사·판정 스냅샷·감사 이벤트를
|
||||||
|
# 우회하게 된다. setDecision 을 그대로 타야 한다.
|
||||||
|
script = _read_app()
|
||||||
|
|
||||||
|
assert "async function inheritPriorDecision" in script
|
||||||
|
assert "data-inherit-decision" in script
|
||||||
|
assert "await setDecision(decision" in script
|
||||||
|
assert "선례 승계" in script
|
||||||
|
|
||||||
|
|
||||||
|
def test_operator_gui_does_not_offer_inheritance_when_prior_decisions_conflict():
|
||||||
|
# 같은 이미지에 서로 다른 판정이 있으면 자동 제안 대상이 아니다 —
|
||||||
|
# 그때가 바로 사람이 봐야 하는 상황이다.
|
||||||
|
script = _read_app()
|
||||||
|
|
||||||
|
assert "conflicting" in script
|
||||||
|
assert "prior-decision-conflict" in script
|
||||||
|
# 만장일치일 때만 inheritable 이 채워진다.
|
||||||
|
assert "unanimous ? priors[0].decision : null" in script
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,15 @@ function bindEvents() {
|
||||||
document.getElementById("upload-submission-image").addEventListener("click", uploadSubmissionImage);
|
document.getElementById("upload-submission-image").addEventListener("click", uploadSubmissionImage);
|
||||||
document.getElementById("rerun-enrichment").addEventListener("click", rerunEnrichment);
|
document.getElementById("rerun-enrichment").addEventListener("click", rerunEnrichment);
|
||||||
|
|
||||||
|
// 선례 승계 버튼은 케이스마다 다시 그려지므로 컨테이너에 위임한다.
|
||||||
|
document.getElementById("prior-decision-box").addEventListener("click", (event) => {
|
||||||
|
|
||||||
|
const button = event.target.closest("[data-inherit-decision]");
|
||||||
|
|
||||||
|
if (button) inheritPriorDecision(button.dataset.inheritDecision);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("add-from-case").addEventListener("click", addKnowledgeEntryFromCase);
|
document.getElementById("add-from-case").addEventListener("click", addKnowledgeEntryFromCase);
|
||||||
|
|
||||||
document.getElementById("mark-irrelevant").addEventListener("click", () => markEvidenceIrrelevant());
|
document.getElementById("mark-irrelevant").addEventListener("click", () => markEvidenceIrrelevant());
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,21 @@ function duplicateBadgeHtml(submission) {
|
||||||
|
|
||||||
const conflictNote = group.conflicting ? " · 판정 불일치" : "";
|
const conflictNote = group.conflicting ? " · 판정 불일치" : "";
|
||||||
|
|
||||||
return `<button type="button" class="duplicate-badge${tone}" data-duplicate-group="${escapeHtml(submission.id)}" title="${escapeHtml(`${priorText}${conflictNote}`)}">동일 이미지 ${group.size}건${conflictNote}</button>`;
|
// 선례를 배지 본문에 싣는다. title 에만 두면 마우스를 올린 사람만 알 수 있고,
|
||||||
|
// 큐를 훑는 동안에는 "이미 판정한 이미지"가 보이지 않는다.
|
||||||
|
const summary = priorDecisionSummary(submission);
|
||||||
|
|
||||||
|
const priorNote = group.conflicting
|
||||||
|
|
||||||
|
? conflictNote
|
||||||
|
|
||||||
|
: summary && summary.inheritable
|
||||||
|
|
||||||
|
? ` · 선례 ${decisionLabels[summary.inheritable] || summary.inheritable}`
|
||||||
|
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `<button type="button" class="duplicate-badge${tone}" data-duplicate-group="${escapeHtml(submission.id)}" title="${escapeHtml(`${priorText}${conflictNote}`)}">동일 이미지 ${group.size}건${priorNote}</button>`;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -448,3 +448,29 @@ function selectedBulkSubmissionIds() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 선례 승계. 별도 판정 경로를 만들지 않고 메모만 채워 setDecision 에 넘긴다 —
|
||||||
|
// 반려 메모 필수 검사, 판정 시점 스냅샷, 감사 이벤트 같은 기존 보호장치를 그대로
|
||||||
|
// 타야 하기 때문이다. 승계했다는 사실은 메모로 감사에 남는다.
|
||||||
|
async function inheritPriorDecision(decision) {
|
||||||
|
|
||||||
|
const submission = getSelectedCase();
|
||||||
|
|
||||||
|
const summary = submission ? priorDecisionSummary(submission) : null;
|
||||||
|
|
||||||
|
// 화면이 그 사이 바뀌었으면(다른 케이스 선택, 선례 판정 변경) 조용히 무시한다.
|
||||||
|
if (!summary || summary.inheritable !== decision) return;
|
||||||
|
|
||||||
|
const memoField = document.getElementById("decision-memo");
|
||||||
|
|
||||||
|
const peers = summary.priors.map((item) => item.submissionId).join(", ");
|
||||||
|
|
||||||
|
if (memoField && !memoField.value.trim()) {
|
||||||
|
|
||||||
|
memoField.value = `선례 승계: 동일 이미지 ${peers} 와 같은 판정`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await setDecision(decision, decision === "rejected");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,8 @@ function renderNoSelectedCase() {
|
||||||
document.getElementById("case-reasons").innerHTML = `<div class="empty-state">표시할 케이스가 없습니다.</div>`;
|
document.getElementById("case-reasons").innerHTML = `<div class="empty-state">표시할 케이스가 없습니다.</div>`;
|
||||||
document.getElementById("evidence-groups").innerHTML = `<div class="empty-state">API 연결 실패 또는 제출 이미지 없음 상태입니다.</div>`;
|
document.getElementById("evidence-groups").innerHTML = `<div class="empty-state">API 연결 실패 또는 제출 이미지 없음 상태입니다.</div>`;
|
||||||
document.getElementById("recommendation-box").innerHTML = `<span class="muted">실제 API 데이터가 로드될 때까지 판정하지 않습니다.</span>`;
|
document.getElementById("recommendation-box").innerHTML = `<span class="muted">실제 API 데이터가 로드될 때까지 판정하지 않습니다.</span>`;
|
||||||
|
|
||||||
|
setPriorDecisionBox(null);
|
||||||
document.getElementById("derived-preview").innerHTML = `<span class="muted">자동 후보 생성 없음</span>`;
|
document.getElementById("derived-preview").innerHTML = `<span class="muted">자동 후보 생성 없음</span>`;
|
||||||
// Clear the evidence-gap recommendations too: they belong to the case that was
|
// Clear the evidence-gap recommendations too: they belong to the case that was
|
||||||
// just deselected, and leaving them rendered invited running suggested queries
|
// just deselected, and leaving them rendered invited running suggested queries
|
||||||
|
|
@ -322,6 +324,8 @@ function renderCaseReview() {
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
setPriorDecisionBox(submission);
|
||||||
|
|
||||||
document.getElementById("derived-preview").innerHTML = `
|
document.getElementById("derived-preview").innerHTML = `
|
||||||
|
|
||||||
<strong>${submission.derivedPreview.automatic ? "자동 후보 생성 예정" : "자동 이력 없음"}</strong>
|
<strong>${submission.derivedPreview.automatic ? "자동 후보 생성 예정" : "자동 이력 없음"}</strong>
|
||||||
|
|
@ -335,3 +339,72 @@ function renderCaseReview() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 동일 이미지 선례를 판정 패널에 세운다.
|
||||||
|
//
|
||||||
|
// 이 정보는 서버가 이미 보내고 있었다(store_duplicates.py 의 priorDecisions).
|
||||||
|
// 문제는 자리였다 — 큐 배지의 title 속성에만 있어서, 운영자가 재제출을 처음부터
|
||||||
|
// 다시 심사해도 아무것도 막지 못했다. 중복 탐지는 이 도구에서 실증으로 검증된
|
||||||
|
// 유일한 판별 기능이므로(재저장·축소·밝기 변형 24/24 그룹핑, 같은 24건에서
|
||||||
|
// SHA-256 일치 0건) 그 결과를 판정 지점에 올린다.
|
||||||
|
function setPriorDecisionBox(submission) {
|
||||||
|
|
||||||
|
const box = document.getElementById("prior-decision-box");
|
||||||
|
|
||||||
|
if (!box) return;
|
||||||
|
|
||||||
|
const summary = submission ? priorDecisionSummary(submission) : null;
|
||||||
|
|
||||||
|
if (!summary) {
|
||||||
|
|
||||||
|
box.innerHTML = "";
|
||||||
|
|
||||||
|
box.hidden = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const peers = summary.priors.map((item) => item.submissionId).join(", ");
|
||||||
|
|
||||||
|
if (summary.conflicting) {
|
||||||
|
|
||||||
|
// 같은 이미지에 서로 다른 판정이 있다. 승계를 제안하지 않는다 — 자동화할
|
||||||
|
// 상황이 아니라 운영자가 봐야 하는 신호다.
|
||||||
|
box.innerHTML = `
|
||||||
|
|
||||||
|
<strong class="prior-decision-conflict">동일 이미지 ${summary.size}건 · 판정 불일치</strong>
|
||||||
|
|
||||||
|
<span class="muted">${escapeHtml(peers || "-")}</span>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
} else if (!summary.inheritable) {
|
||||||
|
|
||||||
|
box.innerHTML = `
|
||||||
|
|
||||||
|
<strong>동일 이미지 ${summary.size}건</strong>
|
||||||
|
|
||||||
|
<span class="muted">아직 판정된 선례 없음</span>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const label = decisionLabels[summary.inheritable] || summary.inheritable;
|
||||||
|
|
||||||
|
box.innerHTML = `
|
||||||
|
|
||||||
|
<strong>동일 이미지 선례 · ${escapeHtml(label)}</strong>
|
||||||
|
|
||||||
|
<span class="muted">${escapeHtml(peers)}</span>
|
||||||
|
|
||||||
|
<button class="row-action" type="button" data-inherit-decision="${escapeHtml(summary.inheritable)}">선례 승계</button>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
box.hidden = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -308,3 +308,36 @@ function filteredSubmissions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 동일 이미지에 이미 내려진 판정 요약. 서버는 이 값을 duplicateGroup 에 담아
|
||||||
|
// 이미 내려보내고 있었지만, 콘솔은 그것을 큐 배지의 title 속성에만 넣었다 —
|
||||||
|
// 마우스를 올려야 보이는 자리라 재제출을 처음부터 다시 심사하는 것을 못 막았다.
|
||||||
|
//
|
||||||
|
// 승계는 선례가 **만장일치**일 때만 제안한다. conflicting 은 같은 이미지에 서로
|
||||||
|
// 다른 판정이 내려졌다는 뜻이고, 그건 자동 처리할 상황이 아니라 사람이 봐야 하는
|
||||||
|
// 신호다.
|
||||||
|
function priorDecisionSummary(submission) {
|
||||||
|
|
||||||
|
const group = submission && submission.duplicateGroup;
|
||||||
|
|
||||||
|
if (!group || !group.size) return null;
|
||||||
|
|
||||||
|
const priors = group.priorDecisions || [];
|
||||||
|
|
||||||
|
const decisions = new Set(priors.map((item) => item.decision));
|
||||||
|
|
||||||
|
const unanimous = !group.conflicting && priors.length > 0 && decisions.size === 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
size: group.size,
|
||||||
|
|
||||||
|
priors,
|
||||||
|
|
||||||
|
conflicting: Boolean(group.conflicting),
|
||||||
|
|
||||||
|
inheritable: unanimous ? priors[0].decision : null,
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@
|
||||||
<span class="score-pill risk-pending" id="floating-case-score">대기</span>
|
<span class="score-pill risk-pending" id="floating-case-score">대기</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="recommendation-box" id="recommendation-box"></div>
|
<div class="recommendation-box" id="recommendation-box"></div>
|
||||||
|
<div class="prior-decision" id="prior-decision-box" hidden></div>
|
||||||
<div class="llm-draft" id="llm-draft" hidden></div>
|
<div class="llm-draft" id="llm-draft" hidden></div>
|
||||||
<label class="memo-field" for="decision-memo">
|
<label class="memo-field" for="decision-memo">
|
||||||
<span>결정 메모</span>
|
<span>결정 메모</span>
|
||||||
|
|
|
||||||
|
|
@ -3031,6 +3031,37 @@ tbody tr.selected-row,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Duplicate group badge -------------------------------------------- */
|
/* --- Duplicate group badge -------------------------------------------- */
|
||||||
|
/* 동일 이미지 선례. 판정 버튼 바로 위에 서서 "이건 이미 판정한 이미지"를
|
||||||
|
먼저 읽게 한다 — 이 정보는 원래 큐 배지의 title 에만 있어 마우스를 올린
|
||||||
|
사람만 볼 수 있었다. */
|
||||||
|
.prior-decision {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--green-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--green-tint);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prior-decision strong {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 판정 불일치는 승계를 제안하지 않는 상태다. 같은 초록으로 두면 "정리된 것"으로
|
||||||
|
읽히므로 색을 갈라 운영자가 직접 봐야 하는 신호임을 드러낸다. */
|
||||||
|
.prior-decision:has(.prior-decision-conflict) {
|
||||||
|
border-color: var(--amber-line);
|
||||||
|
background: var(--amber-tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prior-decision-conflict {
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
.duplicate-badge {
|
.duplicate-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue