Commit graph

3 commits

Author SHA1 Message Date
changukyu
4e8e4b3181 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
2026-09-21 12:05:38 +09:00
changukyu
e38dad83c9 fix: 증거 행 레이아웃·호스트 노출·보존 케이스 설명, 프롬프트 스탬프 갱신
1) 긴 호스트가 제목을 세로 한 줄짜리 컬럼으로 만들었다

   .evidence-title 은 flex-wrap 없는 flex 행이고 source-chip·host-chip 둘 다
   축소를 거부한다(flex: 0 0 auto). 그래서 제목에 남는 폭이 수십 px 이 되고
   overflow-wrap: anywhere 가 그걸 한 단어씩 세로로 쪼갰다. 컨테이너에
   flex-wrap, 제목에 min-width: 14ch 를 줘서 제목이 자기 줄로 내려간다.

   실렌더 측정 (같은 DOM 에 수정 전 CSS 런타임 주입, Playwright):
     360px  제목 51x95px (5줄) -> 246x19px (1줄), 행 높이 95 -> 50
     1280px 제목 9x380px (~20줄) -> 205x19px, 행 높이 380 -> 50

   주석은 데스크톱에서는 해결됐다고 적혀 있었지만, 45자 CDN 호스트로는
   1280px 에서도 재현된다 — 폰 전용 문제가 아니었다.

2) 20ch 로 잘린 호스트를 읽을 방법이 없었다

   칩 title 이 "게시 호스트" 고정 문구여서, 20자 접두어가 같은 두 CDN 호스트를
   링크를 열지 않고 구분할 수 없었다 — 모노스페이스를 고른 이유가 그 판별이다.
   툴팁에 실제 호스트를 싣는다.

3) 클라이언트 호스트 폴백이 서버보다 짧았다

   _result_host 는 (url, result_url, page_url, source_page_url, blogger_link,
   image_url) 를 순회하는데 evidenceHost 는 3개만 봤다. host 키가 생기기 전에
   저장된 네이버 블로그 행이 bloggerLink 만 가진 경우 칩이 조용히 사라졌다 —
   "레거시 증거가 자기 출처에 눈멀지 않게" 라는 폴백의 목적과 반대다.

4) 보존된 케이스가 설명 없는 깨진 이미지였다

   assetMissing 을 읽는 소비자가 GUI·API·내보내기 어디에도 없었다. 판정은 DB 에
   남지만 콘솔에서는 "판정 후 파일 아카이브" 와 "업로드 손상" 이 구분되지
   않았다. 큐 썸네일과 케이스 이미지에 표시 + 파생물 노트에 사유를 붙인다.

5) PROMPT_VERSION 갱신 (ko-v3-structured -> ko-v4-structured-host)

   847f7a8 이 PROMPT_DATA_FIELDS 의 domain -> host 로 모든 증거 줄의 내용을
   바꿨는데 스탬프는 그대로여서, 한 버전이 서로 다른 두 프롬프트를 가리켰다.
   화이트리스트가 다른 모듈에 있어도 프롬프트의 일부라는 점을 llm_prompts
   독스트링의 규칙에 명시했다. 렌더된 줄이 host= 를 담는지 보는 행위 테스트도
   추가했다(튜플 멤버십 검사로는 안 잡힌다).

   CLAUDE.md 요구대로 실모델 프로브(qwen2.5:7b, 로컬 ollama): grounded 3 /
   dropped 0 / verified True, 형식 예시 반향 없음, 인용 라벨 정확(E1=full match).
   다만 모델은 host 를 문장에 쓰지 않고 page_title 로 서술했다 — 호스트는
   운영자 칩에는 닿지만 서술에는 아직 반영되지 않는다(SYSTEM_PROMPT 과제).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
2026-09-18 23:54:00 +09:00
changukyu
fc4e04f65c refactor: app.js 5,428줄을 관심사별 app-*.js 15개로 분할
단일 파일이 프로젝트 상한(800줄)의 6.8배였다. 분할은 순수 라인범위 추출이며
어떤 구문도 수정·재배치·재포맷하지 않았다 — 15개 파트의 연결 결과가 원본
app.js 와 바이트 단위로 동일함을 확인하고 나눴다(125,689B).

클래식 스크립트(모듈 아님)라 하나의 전역 스코프를 공유하므로 로드 순서가 곧
계약이다. 원본에서 로드 시점에 실행되는 최상위 구문은 파일 끝의 리스너 등록
2개뿐이었고(DOMContentLoaded/hashchange), 나머지 참조는 전부 함수 본문 안이라
순서만 지키면 의미가 보존된다. 그 순서를 index.html 에 명시하고 테스트로 고정:

- test_index_loads_every_app_part_exactly_once_in_declared_order
  각 파트가 정확히 1회, 선언 순서대로 로드되는지 + 디스크의 app-*.js 가
  전부 배선됐는지 + 구 app.js 가 남아있지 않은지
- test_app_parts_stay_within_the_file_size_budget (800줄 상한)

정적 스펙 31곳은 app.js 본문을 문자열로 검사하고 있었다. 이 스펙들의 관심사는
'어느 파일에 있는가'가 아니라 '콘솔에 그 동작이 있는가'이므로, 파트를 로드
순서대로 이어붙여 읽는 _read_app() 으로 바꿨다.

검증: Playwright E2E 6건이 실브라우저에서 통과(콘솔 에러 0 단언 포함).
운영 콘솔 실렌더 확인 — 큐/케이스 심사 정상, 스크립트 15개 전부 200,
새로고침 후 콘솔 메시지 0건.
2026-08-08 09:48:52 +09:00