POSA_Copyrighter/docs/PROJECT_MAP.md
changukyu 975c252201 feat: 증거 일괄 표시 — 51번 클릭하던 되먹임을 1번으로
8/8 실효성 평가가 "운영자 되먹임이 순증 노동"이라고 적은 항목이다. 스코어러는
점수를 만든 행이 표시돼야 점수를 내려놓는데, 단건 API(/api/evidence/{id}/status)
하나뿐이라 실 DB 제출물 하나를 100 에서 35 로 내리려면 google 증거 51행을
개별로 눌러야 했다. 되먹임이 노동보다 비싸면 아무도 되먹이지 않는다.

증거 그룹 헤더의 개수 옆에 [전부 무관] 을 붙이고, POST /api/evidence/status-batch
를 신설했다.

서버는 한 트랜잭션, 리스코어 1회, 감사 이벤트 1건으로 묶는다. 감사 부분은 그
자체로 중요하다 — 이건 한 번의 운영자 동작이고, 51건을 쓰면 동작을 잘못
표현할 뿐 아니라 500행 감사 캡을 클릭 한 번에 먹는다. 행별 who/when/note 는
각 증거 payload 에 그대로 남으므로 개별 행이 설명 불가가 되지는 않는다.

원자성: 남의 제출물 증거 id 가 섞이면 아무것도 쓰지 않고 전체가 실패한다.
절반만 표시된 상태로 끝나면 운영자는 무엇이 반영됐는지 알 수 없다.

상한은 500 이다. 일괄 판정 상한(200)보다 큰 이유는 단위가 다르기 때문이다 —
그쪽은 케이스 수고 이쪽은 한 케이스 안의 증거 행 수다.

그룹 액션은 irrelevant(무관)로 보낸다. 행별 "미사용" 버튼은 false_positive
(오탐)에 매핑돼 있는데, 한 제공자의 결과 전체를 오탐이라 단정하는 것은 과한
주장이다. 점수 효과는 같고(스코어러는 둘 다 건너뛴다) 주장만 정직해진다.

확인 대화는 두지 않았다. 이 동작은 행마다 되돌릴 수 있고, 여기서 줄이려는
것이 바로 그 클릭이다. 되돌릴 수 없는 느낌이 드는 것은 판정이고 확인 대화는
거기에만 있다.

실렌더(Playwright 1440x900, 실제 서버): 51행 그룹에서 1타로
{"status":"irrelevant","evidence_ids":[51개]} 전송, 콘솔 에러 0.

검증: ruff clean, pytest 636 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
2026-09-21 14:34:05 +09:00

17 KiB
Raw Blame History

PROJECT MAP

어디에 무엇이 있는지에 대한 참조 문서. 작업 전 여기서 위치를 찾고 필요한 부분만 연다 (전체 소스 재스캔 금지). 새 파일·라우트·스키마가 생기면 해당 섹션을 갱신한다.

무엇을 했는지의 기록은 INDEX.md에 있다. 이 문서는 현재 구조만 다룬다.

한 줄 요약

이미지 저작권 위험을 자동 분석하고, 운영자가 콘솔에서 근거를 확인해 승인/보류/반려를 판정하는 폐쇄망(air-gapped) 도구. Python 3.13 표준 http.server + SQLite + 정적 SPA. 런타임 서드파티는 이미지 처리 3종(numpy/opencv-python-headless/pillow)뿐.

검증 (단일 명령)

.\verify.ps1              # ruff + pytest 전체 — 커밋 전 게이트, CI도 이 명령
.\verify.ps1 -SkipLint    # 빠른 내부 루프
.\verify.ps1 -Coverage    # src/ 커버리지 포함

설정은 pytest.ini(testpaths·pythonpath·timeout)와 ruff.toml(py313·line-length 120)에 있다.

실행

python run_copyrighter_server.py            # 기본 127.0.0.1:9500

런처가 PYTHONPATH=src와 작업 디렉터리를 고정한다. 옵션: --host --port --db --images --static --env.


소스 지도 (src/rights_filter/)

analysis/ — 이미지 분석과 위험도 산출

파일 역할
preprocessing.py 디코드, 외부 전송용 파생물 생성(메타데이터 제거), 얼굴 크롭 파생물
fingerprints.py 이미지 지문. _perceptual_hash는 DCT pHash가 아니라 8x8 평균해시(aHash) — 이름과 실제가 다름(알려진 부채)
face_person_detection.py YuNet(ONNX) 얼굴 검출 + 인물 등급(identifiable/background). 가중치는 models/(gitignore)
internal_analyzer.py 내부 분석 오케스트레이션
risk_scoring.py RiskScorer.score() — 점수·밴드 산출. SCORER_VERSION 은 가중치/버킷/밴드 변경 시 반드시 올린다. RiskScorer(trusted_hosts) 는 지식DB 의 활성 trusted_host 항목(이름·별칭이 도메인)을 받아 그 호스트에서 발견된 웹 탐지 행을 0점 처리한다 — 행 단위 제외라 자사 사본과 스크래퍼 게시가 같이 있으면 후자는 그대로 센다
evidence_normalizer.py, evidence_enrichment.py 증거 정규화·보강
search_query_generation.py, search_result_promoter.py 자동 검색 쿼리 생성, 검색 결과의 증거 승격
llm_assistance.py, llm_prompts.py LLM을 요약기가 아닌 증거 판독기로 사용. 입력 화이트리스트 9필드 + 상위 12건 상한, format=json 구조화 인용, 라벨 집합 포함 검사로 미인용 claim 폐기
provenance.py 출처 신호 — EXIF/IPTC 권리 표기 추출(화이트리스트 6필드), 방송캡처 포렌식. 둘 다 현재 0점 라벨(캘리브레이션 전)
derivatives.py, reason_builder.py 파생물 기록, 사유 문자열 조립

server/ — HTTP + 저장소

http_app.py (559줄) 가 라우팅·인증·보안 헤더를 담당하고, 저장소는 CopyrighterStore 하나를 믹스인 8개로 조립한다 (sqlite_store.py:77).

파일 역할
http_app.py build_server(), CopyrighterHandler. 라우팅 / Bearer+세션쿠키 인증 / CSP·CSRF / 바디 상한 10MB
auth_rate_limit.py 실패한 인증 시도 스로틀(고정 윈도우 10회/60초, 클라이언트 주소별)
sqlite_store.py CopyrighterStore 조립 + 시드·리스코어
store_schema.py DDL·제약·타입 컬럼 마이그레이션. SCHEMA_VERSION
store_persistence.py _put/_all 등 저수준 upsert (PK 충돌 시 update)
store_serialization.py 도메인 ↔ DB payload 왕복. 라운드트립은 원 점수를 재현해야 한다(과거 위반 이력 있음)
store_enrichment.py provider 상태 동기화, 얼굴 증거/크롭, 재분석, 자동 Naver/Google 검색 드라이버
store_operations.py 운영자 액션(판정·메모·재분석 요청)
store_workflow.py 일괄 판정(MAX_BATCH_DECISIONS), 워크플로
store_search_candidates.py 검색 후보 수집·유사도 비교
store_page_scrape.py 후보 페이지/스타일시트 파싱
store_remote_fetch.py 네트워크 경계. SSRF 가드 — 사전 URL 검사 + _guarded_create_connection(단일 DNS 해석으로 rebinding TOCTOU 차단) + 리다이렉트 홉 재검증
store_queue.py, store_duplicates.py, store_backup.py, store_text.py, store_url_utils.py, store_constants.py 큐, 지문 중복, 백업/복구, 텍스트·URL 유틸, 상수
image_store.py 제출 이미지 파일 저장소 + manifest
env_file.py .env 로더 (utf-8-sig — BOM이 첫 키에 붙지 않도록)

integrations/ — 외부 어댑터

naver_search.py, google_custom_search.py, cloud_vision_web_detection.py, web_detection_result_mapper.py, http_json.py. 쿼터/정책은 search_policy.py(SearchApiPolicy — try_reserve/release 로 원자적 예약)와 external_policy.py(ExternalApiPolicy). 둘 다 날짜 경계 윈도우 리셋 있음. env_clients.py 가 .env → 어댑터/정책 조립(build_provider_runtime).

domain/ · governance/ · jobs/ · admin/

  • domain/records.py — Evidence, EvidenceSource, ScoreResult 등 핵심 레코드
  • governance/ — 접근·보정·보존 정책 (얼굴 크롭 보존기간 등)
  • jobs/batch_analyzer.py — 배치 분석, retry_policy.py, usage_limits.py
  • admin/ — 운영자 리뷰 핸들러·프레젠터

운영자 콘솔 (web/operator-gui/)

빌드 스텝 없음. 전부 클래식 스크립트(모듈 아님)라 하나의 전역 스코프를 공유한다.

index.html → styles.css + 보조 스크립트 4개(operator-labels.js, submission-import.js, evidence-guidance.js, operator-search.js) → app-*.js 15개.

app-*.js 로드 순서 (변경 금지 — 순서가 곧 계약)

app-state.js            상태·라벨 임포트 (나머지 전부가 읽는다)
app-api.js              토큰 프롬프트, fetch 래퍼, bootstrap/refresh
app-format.js           커버리지 계산, HTML/URL 이스케이프, 라벨 포맷터
app-selectors.js        상태 파생 조회 (선택 케이스, 위험/출처 클래스, 증거 필터)
app-render-queue.js     큐 목록, 임포트 상태, 케이스 리뷰 셸
app-render-evidence.js  증거 그룹화·정렬·마크업
app-render-panels.js    지식DB·후보·보정·provider·감사 패널, renderAll
app-navigation.js       뷰/탭 전환, 케이스 선택, 판정, 수동 검색, 재분석
app-keyboard-batch.js   키보드 심사 모드, 중복 그룹, 일괄 판정
app-decision-tools.js   메모 프리셋, 판정 내보내기, 백업, cleared reference, LLM 초안
app-stats.js            판정 통계·캘리브레이션
app-submissions.js      제출물 리로드/임포트/업로드, 지식 항목 생성
app-collections.js      후보 수집·기각·선택·승격
app-admin.js            지식/보정/provider/증거상태 관리 액션
app-bootstrap.js        bindEvents, init, DOM 리스너 ← 유일하게 로드 시점에 실행됨

이 순서는 tests/operator_gui/test_static_workbench.py 의 test_index_loads_every_app_part_exactly_once_in_declared_order 가 고정한다. 파일을 추가·제거·재배치하면 그 테스트의 APP_SCRIPT_NAMES 와 index.html 을 함께 고친다.


HTTP 라우트 (http_app.py)

인증: COPYRIGHTER_AUTH_TOKEN 설정 시 /api/* 와 미디어 라우트는 Bearer 헤더 또는 세션 쿠키 필요. 정적 셸과 /health 는 열려 있다(운영자가 토큰을 입력할 수 있어야 하므로).

GET — /health · /api/bootstrap · /api/review-queue · /api/submissions/{id}/review · /api/providers · /api/providers/health · /api/audit-events · /api/decision-memo-presets · /api/decisions/export · /api/admin/backups · /api/stats/decisions · /media/ · /knowledge-media/ · /collected-media/ · /face-crop-media/

POST — /api/submissions/{id}/search-auto|rerun-enrichment|decision|clear-reference · /api/submissions/reload|import-folder|upload-image|decision-batch · /api/evidence/{id}/status · /api/evidence/status-batch · /api/search/manual · /api/knowledge/manual · /api/knowledge/{id}/promote-watchlist|exclude-watchlist|deactivate|reactivate · /api/collections/keyword · /api/collections/candidates/promote-batch · /api/collections/candidates/{id}/promote|dismiss · /api/decision-memo-presets (+ /{id}/delete) · /api/admin/backup · /api/providers/emergency-disable

PATCH — /api/knowledge/{id}


SQLite 스키마 (store_schema.py, SCHEMA_VERSION = 2)

제약 있는 테이블 7개 — submissions, evidence, providers, knowledge_entries, collection_candidates, corrections, audit_events. 추가로 submission_queues.

각 테이블은 조회·정렬용 타입 컬럼 + 전체 도메인 객체를 담은 payload JSON 컬럼 구조. evidence.submission_id 는 submissions(id) 에 on delete cascade. WAL 적용됨. DB 기본 경로는 data/copyrighter.sqlite3(gitignore).


테스트 (tests/, 562건)

  • tests/rights_filter/ — 단위·통합. conftest.py 가 src 를 sys.path 에 넣는다
  • tests/operator_gui/ — 정적 검증 + Playwright E2E.
    • Page Object: pages/workbench_page.py (OperatorWorkbench). 업로드 대기는 UPLOAD_ANALYSIS_TIMEOUT_MS(30초) — 업로드는 서버에서 실분석을 돌리므로 일반 대기보다 길다
    • Chromium 없으면 자동 skip(importorskip)
  • 정적 테스트는 app-*.js 전체를 이어붙여(_read_app()) 내용을 검사한다 — 어느 파일에 있는지가 아니라 콘솔에 그 동작이 있는지를 본다

다이어그램

소스 JSON 과 산출물 HTML 이 같은 폴더에 쌍으로 있다. HTML 직접 수정 금지 — 고칠 것은 언제나 spec JSON 이고, 재생성은 스크립트 하나다:

.\scripts
ebuild-diagrams.ps1                  # validate -> deliver -> visual-check -> manifest
.\scripts
ebuild-diagrams.ps1 -SkipVisualCheck  # 브라우저 없는 호스트

게이트 두 개가 이 계약을 강제한다(둘 다 브라우저·Node·archify 없이 도는 순수 비교):

  • tests/test_diagram_sync.py — 산출물이 spec 보다 낡았나. manifest.json 의 sha256 대조. HTML 을 직접 고치거나 spec 만 고치고 재생성을 잊으면 막힌다.
  • tests/test_diagram_facts.py — spec 이 코드보다 낡았나. 그림에 하드카피된 코드 유래 값(SCORER_VERSION, SCHEMA_VERSION, 믹스인 개수, app-*.js 개수, 바디 상한, 프롬프트 12행 상한, 풀 상한, 기본 모델, 런타임 의존성 3종)을 실제 코드 상수와 대조한다. 지식DB 엣지의 "0.9+ → 80점" 은 상수가 아니라 인라인 리터럴이라 실제로 채점해서 확인한다.

네 시나리오(HTML 직접 수정 · spec 만 수정 · SCORER_VERSION 상승 · 프롬프트 상한 변경) 모두 실제로 만들어 차단을 확인했다.

문서 재생성 전용 요구사항 (런타임 아님, 폐쇄망 타깃에는 불필요): Node 18+ 와 archify 스킬($env:ARCHIFY_HOME, 기본값 ~/.claude/skills/archify), visual-check 단계에만 Chrome/Chromium. 산출 당시 generator 는 archify 2.17.0-dev.1 이고 manifest.json 에 기록된다 — 스킬 버전이 바뀌면 같은 spec 에서도 바이트가 달라지므로, 해시가 깨졌을 때 코드 변경과 툴 변경을 구분하는 근거가 된다.

용도: 내부 문서다 (2026-09-21 결정). 고객 인수물이 아니므로 납품 품질 기준을 적용하지 않는다 — 아래 부채 표의 한글 폰트·영어 뷰어 UI 항목이 low 인 이유가 이것이다. 인수물로 승격하면 그 둘이 먼저 blocker 가 되므로, 승격 시 이 줄부터 고친다.

모두 자체 완결 HTML 이라 외부 요청이 없다(9/21 감사에서 실측: src/href/@import/fetch 모두 0건). 단 file:// 로 여는 산출물이다 — 운영자 콘솔의 static_root 로 서빙하면 app-shell CSP(script-src 'self', http_app.py:514)가 인라인 스크립트 4개를 차단해 상호작용이 죽는다. 서빙이 필요하면 CSP 완화가 아니라 별도 라우트에서 nonce 정책을 쓴다.

파일 내용 타입
evidence-dataflow.html 증거 데이터 흐름 5단(수집·경계 검사·정규화/채점·영속화·소비). 라운드트립 불변식이 중심 dataflow
system-architecture.html 시스템 구성. 폐쇄망 경계와 그 밖의 외부 검색 API, SSRF 가드 앞의 호출 게이트 architecture
operator-review-workflow.html 운영자 심사 4레인(제출·자동·심사·차단). 비상 차단 시 심사가 이어지는 경로 포함 workflow v2
submission-lifecycle.html 제출물 상태 전이. 분석 실패·보류가 종료가 아니라 분석으로 복귀하는 두 고리 lifecycle

알려진 부채

항목 위치 비고
_perceptual_hash 가 pHash가 아닌 aHash analysis/fingerprints.py:47 균일/저디테일 이미지 유사도 오탐. 교체 시 0.9 임계 재보정 필요
src 4파일이 800줄 초과 store_page_scrape.py(972) store_operations.py(968) sqlite_store.py(904) store_enrichment.py(853) 저장소 코어라 분할 리스크 있음. 별도 작업으로
밴드 임계·풀 상한 미캘리브레이션 analysis/risk_scoring.py 판정 이력 0건이라 70/30 임계가 실제 판정과 대조된 적 없다. v3 의 두 풀(WEB_PRESENCE_MAX_POINTS 60 은 제공자별 / WEB_ENTITY_MAX_POINTS 20)은 신호의 구조적 성질로 정한 값이고 상관 검증 전이다 — 판정 30~50건 후 decision_stats 교차표가 첫 근거가 된다
출처 신호 2종 0점 analysis/provenance.py 캘리브레이션 전. 실측 코퍼스에서 가치 미입증. 방송캡처는 MIN_CAPTURE_CATEGORIES=2 에서 geometry 가 1개 카테고리라 자막 없는 캡처는 구조적으로 도달 불가
테스트가 로컬 LLM 에 실접속 integrations/env_clients.py:260 _llm_assistant 가 항상 OLLAMA_BASE_URL 기본값(127.0.0.1:11434)으로 클라이언트를 만들므로, provider_runtime 없이 만든 store 를 쓰는 테스트는 ollama 가 떠 있으면 실호출을 한다. test_http_app.py 3건이 그 상태에서 소켓 타임아웃으로 실패한다(OLLAMA_BASE_URL=http://127.0.0.1:1 로 돌리면 통과). 폐쇄망 프로젝트의 테스트가 외부 서비스의 부재에 의존하는 상태
다이어그램에 한글 폰트 미번들 (내부 문서라 low) docs/diagrams/*.html 임베드된 JetBrains Mono 의 unicode-range 6개에 한글 구간이 없다(U+AC00 매치 0). 폴백 스택은 중국어 폰트뿐(Noto Sans Mono CJK SC/PingFang SC/Microsoft YaHei). 렌더 텍스트의 40~72% 가 한글이라 타깃 폰트에 의존한다 — 한국어 폰트 없는 최소 설치 호스트에서는 두부(tofu). archify 에 저자용 폰트 옵션이 없어 spec 으로는 못 고친다(레이아웃 폭은 전각으로 측정되므로 깨짐이 아니라 글자 문제). 내부 문서로 확정(9/21) 되어 현재는 low — 사내 머신에는 한글 폰트가 있다. 인수물로 승격하면 blocker
뷰어 UI 가 영어 고정 (내부 문서라 low) docs/diagrams/*.html locale enum 이 en/zh-CN 뿐이고 템플릿이 <html lang="en"> 하드코딩. 저자 내용은 한국어인데 조작 UI·aria 라벨·범례 제목은 영어. 스크린리더가 한글 본문을 영어로 읽는다
architecture spec 만 좌표 하드코딩 docs/diagrams/system-architecture.architecture.json 컴포넌트 11개 전부 pos+size 수기, size 는 [124,62] 고정. 라벨 21자부터 validate 하드 실패(19·20자 통과, 21·22자 실패 — 9/21 실측. 현재 store_remote_fetch 18자 → 여유 2글자). workflow spec 은 기하 하드코딩 0개이므로 같은 저장소에 더 나은 패턴이 있다
lifecycle 기하 손튜닝 8개소 docs/diagrams/submission-lifecycle.lifecycle.json labelDy×5·channelX×2·yOffset×1 이 현재 배치 전제의 절대 픽셀이다. 상태를 하나 추가하면 손대지 않은 기존 전이까지 검증 실패 — 9/21 실측: waiting 레인 col 1 에 상태 1개 + 전이 1개만 넣었는데 오류 5건이 났고 그중 3건이 손대지 않은 기존 t-reject 쪽이었다. 상태 추가 시 전량 재검증 필요
7/11 리뷰 연기 항목 reviews/2026-07-11-remediation-checklist.md 각 항목에 연기 근거 기재