"이 도구가 실제 업무에 도움이 되는가"를 6축으로 조사하고 축마다 결론을 반증시켰다(docs/reviews/2026-08-08-utility-assessment.md). 결론은 "검색·기록 노동은 줄여주나 심사 건수는 못 줄인다"이고, 그 과정에서 실행으로 확정된 P0 결함 4건을 여기서 고친다. 판별력 붕괴(P1)는 별도 작업이다. 폴더 정리가 판정 기록을 지웠다 (sqlite_store.py) _prune_missing_submission_files 가 원본 파일이 없는 submission 행을 판정 여부와 무관하게 delete 했고, foreign_keys=on 이라 evidence 가 cascade 로 함께 사라졌다. 심사 끝난 이미지를 아카이브로 옮기는 평범한 운영 행위가 승인/반려를 DB 와 내보내기 양쪽에서 지운다. 실증: rejected 판정 후 export 1행 -> 파일 제거 -> submissions 0행 / evidence 0행 / export 0행. 판정된 케이스는 파일이 없어도 보존하고 assetMissing 마커를 남긴다. 마커가 필요한 이유는 이 스윕이 매 리로드마다 돌기 때문이다 — 표시 없이 보존만 하면 같은 감사 이벤트가 리로드마다 쌓여 500행 캡을 먹는다. 비상 차단 스위치가 아무것도 막지 않았다 (store_enrichment.py) emergency_disable_external_providers 는 DB 의 enabled/operatorDisabled 만 쓰는데 자동 보강 경로가 그 플래그를 아예 읽지 않았다. 게이트가 걸린 곳은 수동 검색뿐이라, 버튼을 눌러도 폴더 리로드·업로드·rerun-enrichment 가 그대로 Naver/Google 로 나갔다. 스텁 어댑터 대조실험에서 CONTROL 5회 / 차단 후 5회로 완전히 동일했다. _external_calls_blocked(provider_id) 를 신설해 자동 외부 호출 5개 경로에 배선했다: auto naver, auto CSE, rerun Vision, 얼굴 크롭 웹탐지, 시드 배치 (후자는 ExternalApiPolicy(disabled=True) 로 전환). enabled 가 아니라 operatorDisabled 를 본다. enabled 는 연결성/헬스에도 반응하므로 운영자가 멈추라고 한 적 없는 트래픽까지 죽인다. 얼굴 크롭은 생체 파생물을 전송하므로 상위 게이트에 의존하지 않고 독립 게이트를 건다. 생체 크롭이 보존기간을 넘겨 살아남았다 (store_backup.py) 스냅샷이 face-crops 를 통째로 담는데 프루닝은 나이가 아니라 개수 10개 기준이라, 90일에 파기됐어야 할 크롭이 암호화 없는 zip 안에 남았다. 복구는 더 나빴다 — copyfileobj 로 새로 쓰므로 mtime 이 리셋되고, purge 가 mtime 을 보므로 파기 대상이 복구 직후부터 다시 90일을 살았다. 스냅샷에서 제외하고, 복구 시에도 레거시 아카이브의 크롭을 건너뛴다. 크롭은 원본에서 재분석으로 재생성되므로 손실 비용이 없다. 오프라인 설치 문서가 계약 위반 모델을 지목했다 (offline-install.md) qwen2.5:0.5b-instruct 반입을 지시했는데, 같은 리포가 이 모델을 55.7초 + 프롬프트 형식 예시 반향(출력 계약 위반)으로 기록해 두고 기본값을 7b 로 고정해 뒀다. 문서대로 반입하면 타깃에서 요약이 조용히 죽는다. 모델명을 정정하고, 반입물 목록에 없던 ollama 바이너리와 GPU 전제를 추가했으며, "env 를 비우면 LLM 없이 돈다"는 서술을 실측으로 정정했다 — 실제로는 127.0.0.1:11434 로 폴백해 provider 가 enabled=True 로 보고된다. 회귀 테스트 3건 추가. pytest 565 passed(착수 전 562), 린트 클린.
178 lines
11 KiB
Markdown
178 lines
11 KiB
Markdown
# PROJECT MAP
|
|
|
|
> 어디에 무엇이 있는지에 대한 참조 문서. **작업 전 여기서 위치를 찾고 필요한 부분만 연다**
|
|
> (전체 소스 재스캔 금지). 새 파일·라우트·스키마가 생기면 해당 섹션을 갱신한다.
|
|
>
|
|
> 무엇을 했는지의 기록은 [INDEX.md](INDEX.md)에 있다. 이 문서는 **현재 구조**만 다룬다.
|
|
|
|
## 한 줄 요약
|
|
|
|
이미지 저작권 위험을 자동 분석하고, 운영자가 콘솔에서 근거를 확인해 승인/보류/반려를
|
|
판정하는 폐쇄망(air-gapped) 도구. Python 3.13 표준 `http.server` + SQLite + 정적 SPA.
|
|
런타임 서드파티는 이미지 처리 3종(`numpy`/`opencv-python-headless`/`pillow`)뿐.
|
|
|
|
## 검증 (단일 명령)
|
|
|
|
```powershell
|
|
.\verify.ps1 # ruff + pytest 전체 — 커밋 전 게이트, CI도 이 명령
|
|
.\verify.ps1 -SkipLint # 빠른 내부 루프
|
|
.\verify.ps1 -Coverage # src/ 커버리지 포함
|
|
```
|
|
|
|
설정은 `pytest.ini`(testpaths·pythonpath·timeout)와 `ruff.toml`(py313·line-length 120)에 있다.
|
|
|
|
## 실행
|
|
|
|
```powershell
|
|
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` 은 가중치/버킷/밴드 변경 시 반드시 올린다** |
|
|
| `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/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()`) 내용을 검사한다 —
|
|
어느 파일에 있는지가 아니라 콘솔에 그 동작이 있는지를 본다
|
|
|
|
---
|
|
|
|
## 알려진 부채
|
|
|
|
| 항목 | 위치 | 비고 |
|
|
|---|---|---|
|
|
| `_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) | 저장소 코어라 분할 리스크 있음. 별도 작업으로 |
|
|
| 점수가 high 로 포화 | `analysis/risk_scoring.py:159-174` | web entity 가 이름별 버킷이라 dedup 없이 `round(40*conf)` 씩 누적 — 이미지 매칭 0건이어도 entity 3개면 100/high. 신호조합 224개 전수 시 high 77.7%·low 3.6%. `test_risk_scoring.py:234-258` 이 이 동작을 고정하고 있어 함께 개정해야 한다 |
|
|
| 출처 신호 2종 0점 | `analysis/provenance.py` | 캘리브레이션 전. 실측 코퍼스에서 가치 미입증. 방송캡처는 `MIN_CAPTURE_CATEGORIES=2` 에서 geometry 가 1개 카테고리라 자막 없는 캡처는 구조적으로 도달 불가 |
|
|
| 7/11 리뷰 연기 항목 | [reviews/2026-07-11-remediation-checklist.md](reviews/2026-07-11-remediation-checklist.md) | 각 항목에 연기 근거 기재 |
|