docs: CLAUDE.md / PROJECT_MAP 신설, 7/11 체크리스트 재확인

세션 진입 문서가 없어 41.6k 라인 · 70모듈 구조를 매번 재발견하고 있었다.

- CLAUDE.md: 진입점 + 이 프로젝트에만 해당하는 규칙(폐쇄망 의존성 금지,
  app-*.js 로드 순서 계약, SCORER_VERSION 갱신 의무, 직렬화 라운드트립 불변식,
  LLM 은 실모델로 프로브, cp949 비ASCII 출력 주의)
- docs/PROJECT_MAP.md: 파일·라우트·스키마 위치, app-*.js 로드 순서, 알려진 부채
- README: verify 명령, app-*.js 구조, 인증 스로틀 동작 설명

7/11 리메디에이션 체크리스트를 코드와 대조해 갱신했다. 미체크 항목 중 6건은
이미 수정돼 있었고(naver 멀티페이지 쿼터, #10 Origin/Referer CSRF, env_file
BOM, daily-limit fail-open, __main__ 경로 앵커링, 그리고 Batch 8 전체가
Batch 6 과 중복) 각 줄에 확인 근거를 남겼다. 남은 미체크는 전부 근거가 기재된
명시적 연기다.
This commit is contained in:
changukyu 2026-08-08 09:49:55 +09:00
parent 60903ec1f7
commit 76a06f2ff1
5 changed files with 262 additions and 17 deletions

36
CLAUDE.md Normal file
View file

@ -0,0 +1,36 @@
# POSA Copyrighter
이미지 저작권 위험 자동 분석 + 운영자 판정 콘솔. **폐쇄망(air-gapped) 납품 대상.**
## 먼저 읽기
`docs/PROJECT_MAP.md` (구조·라우트·스키마 위치) → `docs/INDEX.md` (작업 이력).
전체 소스 재스캔 금지 — MAP 에서 위치를 찾고 필요한 부분만 연다.
## 검증
```powershell
.\verify.ps1 # ruff + pytest. 커밋 전 게이트, CI 도 이 명령 하나
```
모든 패치를 적용한 뒤 **마지막에 1회**만 실행한다.
## 이 프로젝트의 규칙
- **폐쇄망 우선**: 새 런타임 의존성 추가는 기본적으로 금지. 필요하면 오프라인 휠 반입 절차
(`docs/operations/offline-install.md`)까지 함께 처리한다. CDN·원격 fetch·자동업데이트 금지.
- **`app-*.js` 로드 순서는 계약**: 클래식 스크립트 15개가 전역 스코프를 공유한다. 추가·제거·
재배치 시 `index.html` 과 `test_static_workbench.py` 의 `APP_SCRIPT_NAMES` 를 함께 고친다.
- **점수를 바꾸면 `SCORER_VERSION` 을 올린다** (`analysis/risk_scoring.py`). 이 스탬프가
가중치 변경과 스코어링 버그를 사후 구분하는 유일한 근거다.
- **직렬화 라운드트립은 원 점수를 재현해야 한다** (`store_serialization.py`). 과거에
`result_url` → `url` 별칭이 리스코어 시 0점을 45점으로 바꾼 사고가 있었다.
- **LLM 은 요약기가 아니라 증거 판독기**다. 인용 없는 claim 은 폐기한다. 프롬프트나 모델을
바꾸면 가짜 transport 가 아니라 **실모델로 프로브**한다 — 형식 예시 반향 결함은 실모델에서만
드러났다.
- **비ASCII 출력 주의**: 콘솔이 cp949 라 em-dash 등이 `UnicodeEncodeError` 로 프로세스를
죽인 이력이 있다. 스크립트 stdout 에 넣지 말 것.
## 커밋
의미 있는 묶음으로 1커밋 → `docs/INDEX.md` 에 결과 기록. 원격은 Forgejo(`tea` CLI, `gh` 아님).

View file

@ -47,6 +47,8 @@ python run_copyrighter_server.py --host 127.0.0.1 --port 9500
기본값으로 실행하면 `http://127.0.0.1:9500` 에서 운영자 콘솔이 열립니다. 기본값으로 실행하면 `http://127.0.0.1:9500` 에서 운영자 콘솔이 열립니다.
> 보안: `COPYRIGHTER_AUTH_TOKEN`이 비어 있으면 데이터 라우트가 **인증 없이** 열립니다. 단일 호스트 개발 환경 외에는 반드시 토큰을 설정하세요. > 보안: `COPYRIGHTER_AUTH_TOKEN`이 비어 있으면 데이터 라우트가 **인증 없이** 열립니다. 단일 호스트 개발 환경 외에는 반드시 토큰을 설정하세요.
>
> 토큰을 설정하면 **틀린 토큰을 제시한 시도**는 클라이언트 주소별로 60초당 10회로 제한되고, 초과 시 `429`와 `Retry-After`를 반환합니다. 토큰을 아직 입력하지 않은 요청(콘솔 최초 로드)은 시도로 세지 않으므로 정상 운영자가 잠기지 않습니다.
## 프로젝트 구조 ## 프로젝트 구조
@ -55,17 +57,32 @@ src/rights_filter/ 분석·서버·통합·도메인 로직 (Python)
server/ http.server 기반 API + 정적 파일 서빙, SQLite 저장소 server/ http.server 기반 API + 정적 파일 서빙, SQLite 저장소
analysis/ 이미지 분석·위험도 산출 analysis/ 이미지 분석·위험도 산출
integrations/ Naver / Google / LLM 어댑터 integrations/ Naver / Google / LLM 어댑터
web/operator-gui/ 운영자 콘솔 (정적 SPA: index.html, app.js, styles.css) web/operator-gui/ 운영자 콘솔 (정적 SPA: index.html, app-*.js, styles.css)
tests/ pytest 테스트 tests/ pytest 테스트
operator_gui/ 운영자 GUI 정적 검증 + Playwright E2E operator_gui/ 운영자 GUI 정적 검증 + Playwright E2E
docs/ 기획·설계·운영 문서 docs/ 기획·설계·운영 문서
``` ```
## 테스트 콘솔 스크립트는 관심사별로 `app-*.js` 15개로 나뉘어 있고, **`index.html` 의 로드 순서가
의미를 갖습니다**(클래식 스크립트가 하나의 전역 스코프를 공유). 순서와 목록은
[`docs/PROJECT_MAP.md`](docs/PROJECT_MAP.md) 에 있으며 정적 테스트가 이를 고정합니다.
## 검증 / 테스트
커밋 전 게이트는 **단일 명령**입니다(CI 도 같은 명령을 호출합니다). ruff 린트와 pytest 전체를
순서대로 실행합니다.
```powershell
.\verify.ps1 # 린트 + 테스트 전체
.\verify.ps1 -SkipLint # 빠른 내부 루프 (테스트만)
.\verify.ps1 -Coverage # src/ 커버리지 리포트 포함
```
개별 실행이 필요할 때:
```bash ```bash
# 전체 python -m pytest # 또는 그냥 pytest — pytest.ini 가 경로를 설정합니다
python -m pytest python -m ruff check src tests # 설정: ruff.toml
# 운영자 GUI E2E (Playwright) # 운영자 GUI E2E (Playwright)
playwright install chromium # 빌드 머신에서 1회 — 브라우저 바이너리는 오프라인 번들에 포함 playwright install chromium # 빌드 머신에서 1회 — 브라우저 바이너리는 오프라인 번들에 포함
@ -77,6 +94,8 @@ python -m pytest tests/operator_gui/
## 문서 ## 문서
- [프로젝트 구조 지도 (PROJECT_MAP)](docs/PROJECT_MAP.md) — 파일·라우트·스키마 위치, 로드 순서, 알려진 부채
- [작업 이력 (INDEX)](docs/INDEX.md)
- [프로젝트 소개 및 기술 구현](docs/project-introduction-and-technical-implementation.md) - [프로젝트 소개 및 기술 구현](docs/project-introduction-and-technical-implementation.md)
- [운영 워크리스트](docs/operations/copyrighter-operation-worklist.md) - [운영 워크리스트](docs/operations/copyrighter-operation-worklist.md)
- [오프라인 설치 절차](docs/operations/offline-install.md) - [오프라인 설치 절차](docs/operations/offline-install.md)

View file

@ -1,5 +1,7 @@
# Project INDEX # Project INDEX
- 2026-08-08 · 프로젝트 분석 후 인프라·구조·보안 정비 — **원격 백업 복구**(20커밋이 로컬에만 있었음: `main` 2 + 작업브랜치 18 → Forgejo push), **단일 verify 게이트 신설**(`verify.ps1` + `pytest.ini` + `ruff.toml`, ruff 0.12.0 dev 핀; 린트 18건 정리), **`app.js` 5,428줄 → `app-*.js` 15개 분할**(순수 라인범위 추출로 바이트 단위 라운드트립 검증, 로드 순서를 정적 테스트로 고정), **보안 2종 실수정**(SSRF DNS-rebinding TOCTOU — 단일 DNS 해석 후 그 답변으로만 연결하는 `_guarded_create_connection`; 실패 인증 스로틀 — 자격증명 제시 실패만 10회/60초, `429`+`Retry-After`), 7/11 체크리스트 재확인(6건은 이미 수정돼 있던 낡은 항목, Batch 8 은 Batch 6 과 중복이라 정리), `CLAUDE.md`·`docs/PROJECT_MAP.md` 신설. E2E 플레이크(업로드 대기 10초 → `UPLOAD_ANALYSIS_TIMEOUT_MS` 30초) 해소. **pytest 562 passed**(착수 전 533/1 failed), 린트 클린. 실렌더 확인: 큐·케이스 심사 정상 렌더, 스크립트 15개 전부 200, 새로고침 후 콘솔 메시지 0
- 2026-07-24 · 멀티에이전트 프로젝트 점검(에이전트 109, 적대적 검증 포함) — pytest 377/379, 런타임 스모크 9/9 통과, 신규 CONFIRMED 43건(HIGH 7) + 7/11 체크리스트 중 3건 실수정 확인 + 기능 제안 상위 5 선정 → [docs/reviews/2026-07-24-project-review.md](reviews/2026-07-24-project-review.md) - 2026-07-24 · 멀티에이전트 프로젝트 점검(에이전트 109, 적대적 검증 포함) — pytest 377/379, 런타임 스모크 9/9 통과, 신규 CONFIRMED 43건(HIGH 7) + 7/11 체크리스트 중 3건 실수정 확인 + 기능 제안 상위 5 선정 → [docs/reviews/2026-07-24-project-review.md](reviews/2026-07-24-project-review.md)
- 2026-07-26 · 운영자 콘솔 실렌더 검증 + 결함 5종 수정 (`d20e6d6`) — 서버 기동 불가(cp949에서 em-dash `UnicodeEncodeError`로 프로세스 사망, 납품 환경 치명) · 요약이 모델이 못 본 78건을 "연결 근거"로 주장(B단계가 절반만 막았음) · `grounded/droppedClaims` 미영속화 · app.js가 서버 그룹 분류를 자체 매핑으로 덮어씀 · 기본 모델이 클라이언트/배선에서 불일치(미설치 모델 지목). 실렌더 확인: 출처 표기 그룹 자체 섹션 렌더, `[E1,E2...]` 인용 2단 요약, `sourceIds` 78→10, qwen2.5:7b 6.0초. **pytest 534 passed** - 2026-07-26 · 운영자 콘솔 실렌더 검증 + 결함 5종 수정 (`d20e6d6`) — 서버 기동 불가(cp949에서 em-dash `UnicodeEncodeError`로 프로세스 사망, 납품 환경 치명) · 요약이 모델이 못 본 78건을 "연결 근거"로 주장(B단계가 절반만 막았음) · `grounded/droppedClaims` 미영속화 · app.js가 서버 그룹 분류를 자체 매핑으로 덮어씀 · 기본 모델이 클라이언트/배선에서 불일치(미설치 모델 지목). 실렌더 확인: 출처 표기 그룹 자체 섹션 렌더, `[E1,E2...]` 인용 2단 요약, `sourceIds` 78→10, qwen2.5:7b 6.0초. **pytest 534 passed**
- 2026-07-25 · C단계: 출처 신호 2종 (`5a9fcc0`) — EXIF/IPTC 권리 표기 추출(화이트리스트 6필드, GPS·시리얼·Make/Model 미판독) + 방송캡처 포렌식(레터박스·자막밴드·프레임크기, 독립 카테고리 2개 이상). 하나의 `EvidenceSource.PROVENANCE`·하나의 GUI 그룹으로 배관 12곳 비용 1회 지불. **둘 다 0점 라벨**(캘리브레이션 전). 실측: 실제 이미지 6장에서 캡처 오탐 0·권리메타 히트 0(정밀도 확인, 가치는 이 코퍼스에서 미입증). XMP/C2PA는 `defusedxml` 미반입으로 제외. **pytest 531 passed** - 2026-07-25 · C단계: 출처 신호 2종 (`5a9fcc0`) — EXIF/IPTC 권리 표기 추출(화이트리스트 6필드, GPS·시리얼·Make/Model 미판독) + 방송캡처 포렌식(레터박스·자막밴드·프레임크기, 독립 카테고리 2개 이상). 하나의 `EvidenceSource.PROVENANCE`·하나의 GUI 그룹으로 배관 12곳 비용 1회 지불. **둘 다 0점 라벨**(캘리브레이션 전). 실측: 실제 이미지 6장에서 캡처 오탐 0·권리메타 히트 0(정밀도 확인, 가치는 이 코퍼스에서 미입증). XMP/C2PA는 `defusedxml` 미반입으로 제외. **pytest 531 passed**

177
docs/PROJECT_MAP.md Normal file
View file

@ -0,0 +1,177 @@
# 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) | 저장소 코어라 분할 리스크 있음. 별도 작업으로 |
| 출처 신호 2종 0점 | `analysis/provenance.py` | 캘리브레이션 전. 실측 코퍼스에서 가치 미입증 |
| 7/11 리뷰 연기 항목 | [reviews/2026-07-11-remediation-checklist.md](reviews/2026-07-11-remediation-checklist.md) | 각 항목에 연기 근거 기재 |

View file

@ -3,6 +3,11 @@
> 기준 리뷰: `docs/reviews/2026-07-11-project-review.md` (HEAD 89c1fe4) > 기준 리뷰: `docs/reviews/2026-07-11-project-review.md` (HEAD 89c1fe4)
> 신규 HIGH 9 + 유닛 추가 HIGH 3(통합/프론트) + MEDIUM ~28 + LOW ~14. > 신규 HIGH 9 + 유닛 추가 HIGH 3(통합/프론트) + MEDIUM ~28 + LOW ~14.
> 배치는 risk-per-effort 순. 각 배치 후 `pytest tests/rights_filter -q` 통과 확인. > 배치는 risk-per-effort 순. 각 배치 후 `pytest tests/rights_filter -q` 통과 확인.
>
> **2026-08-08 재확인**: 미체크로 남아 있던 항목을 코드와 대조했다. 6건은 이미 수정돼 있었고
> (체크리스트가 낡았던 것 — 각 항목에 확인 근거 기재), 보안 2건(#8 rate limit, SSRF
> DNS-rebinding TOCTOU)은 실제로 열려 있어 이번에 수정했다. 남은 미체크는 전부 **명시적 연기**
> 이며 각 줄에 근거가 있다.
## Batch 1 — 스코어링 파이프라인 복구 (최우선) ✅ 완료 ## Batch 1 — 스코어링 파이프라인 복구 (최우선) ✅ 완료
직렬화 라운드트립 필드 손실 1개 패턴이 HIGH 3건을 만듦. 프로모터 3건은 배포 즉시 노출되므로 동일 배치. 직렬화 라운드트립 필드 손실 1개 패턴이 HIGH 3건을 만듦. 프로모터 3건은 배포 즉시 노출되므로 동일 배치.
@ -46,8 +51,8 @@
- [x] LOW `store_remote_fetch.py` — SSRF `not is_global`(CGNAT 등 특수대역 차단) - [x] LOW `store_remote_fetch.py` — SSRF `not is_global`(CGNAT 등 특수대역 차단)
- [x] LOW `env_clients.py` — NAVER display 3종 클램프(1~100) - [x] LOW `env_clients.py` — NAVER display 3종 클램프(1~100)
- [x] M `env_clients.py` — daily-limit malformed → 보수적 100(fail-open 제거) + 테스트 갱신 - [x] M `env_clients.py` — daily-limit malformed → 보수적 100(fail-open 제거) + 테스트 갱신
- [ ] M `naver_search.py` — 멀티페이지 쿼터 실제 요청 수 (미착수) - [x] M `naver_search.py` — 멀티페이지 쿼터 실제 요청 수 (2026-08-08 확인: `_requested_calls` + `try_reserve(requested_calls=…)`/`release` 로 이미 구현돼 있었음. 항목이 낡았던 것)
- [ ] M `store_remote_fetch.py` — SSRF DNS-rebinding TOCTOU (미착수 — 커스텀 opener 필요) - [x] M `store_remote_fetch.py` — SSRF DNS-rebinding TOCTOU (2026-08-08 수정: `_guarded_create_connection` 이 getaddrinfo 를 **1회만** 수행하고 그 답변의 모든 주소를 검증한 뒤 그중 하나로만 연결. `_Guarded*Connection`/`*Handler` 로 opener 에 배선 — 사전검사와 실제 다이얼의 해석이 갈라지지 않는다. 리바인딩 회귀 테스트 포함 14건)
- [ ] LOW: EUC-KR 스니핑, dead code 삭제(레이아웃 테스트가 고정 → 유지) - [ ] LOW: EUC-KR 스니핑, dead code 삭제(레이아웃 테스트가 고정 → 유지)
## Batch 5 — HTTP 보안 ✅ (M10/M11/#6) ## Batch 5 — HTTP 보안 ✅ (M10/M11/#6)
@ -56,8 +61,8 @@
- [x] #6 앱셸 CSP/X-Frame-Options/nosniff + 테스트 - [x] #6 앱셸 CSP/X-Frame-Options/nosniff + 테스트
- [x] #8(부분) 토큰 길이<32 경고 + static 부재 경고(`__main__`) - [x] #8(부분) 토큰 길이<32 경고 + static 부재 경고(`__main__`)
- [ ] #7 쿠키 분리+Secure (연기 — Secure는 localhost HTTP서 쿠키 미전송→인증 붕괴; HTTPS 전제 필요) - [ ] #7 쿠키 분리+Secure (연기 — Secure는 localhost HTTP서 쿠키 미전송→인증 붕괴; HTTPS 전제 필요)
- [ ] #8 rate limit (미착수) - [x] #8 rate limit (2026-08-08 수정: `server/auth_rate_limit.py` — **틀린 자격증명을 제시한** 시도만 클라이언트 주소별 고정윈도우 10회/60초로 제한, 초과 시 `429` + `Retry-After`. 성공하면 카운터 초기화. 토큰 미입력 요청(콘솔 최초 로드가 여러 건 발생시킴)은 세지 않으므로 정상 운영자가 잠기지 않는다. 테이블은 1024 클라이언트로 상한. 테스트 12건)
- [ ] #10 Origin/Referer CSRF 폴백 (미착수) - [x] #10 Origin/Referer CSRF 폴백 (2026-08-08 확인: `_origin_ok()` 로 이미 구현돼 있었음 — 7/24 리뷰가 지적한 낡은 항목)
## Batch 6 — 프론트엔드 ✅ ## Batch 6 — 프론트엔드 ✅
- [x] H8 이미지 뷰어 버튼 구현 + 케이스별 리셋 - [x] H8 이미지 뷰어 버튼 구현 + 케이스별 리셋
@ -92,16 +97,22 @@
- [ ] #20 ScoreResult.reasons tuple (연기 — 테스트 다수 `== []` 단언 충돌) - [ ] #20 ScoreResult.reasons tuple (연기 — 테스트 다수 `== []` 단언 충돌)
## Batch 7 — env/config/docs ## Batch 7 — env/config/docs
- [ ] `env_file.py:12` — UTF-8-sig BOM - [x] `env_file.py:12` — UTF-8-sig BOM (2026-08-08 확인: `read_text(encoding="utf-8-sig")` 로 이미 적용됨)
- [ ] daily-limit fail-open → 보수적 기본 - [x] daily-limit fail-open → 보수적 기본 (2026-08-08 확인: `_daily_limit()` 이 malformed 값에 warning + 100 적용. Batch 4 의 동일 항목과 중복 기재였음)
- [ ] `env_clients.py:239` — LLM assistant OLLAMA_* 게이팅 - [ ] `env_clients.py:239` — LLM assistant OLLAMA_* 게이팅 (연기 — env={} 로 LLM 을 기대하는 테스트 다수가 의존)
- [ ] `__main__.py` — 경로 ROOT 앵커링 + static 부재 경고 - [x] `__main__.py` — 경로 ROOT 앵커링 + static 부재 경고 (2026-08-08 확인: `6ab6da3` 에서 적용됨)
- [ ] offline-install.md — Ollama/Playwright/lock 해시 - [ ] offline-install.md — Ollama/Playwright/lock 해시
- [ ] python 3.13 가드, .gitignore, package.json 정리 - [ ] python 3.13 가드, .gitignore, package.json 정리
## Batch 8 — 에러핸들링/기타 백로그 ## Batch 8 — 에러핸들링/기타 백로그
- [ ] #14 무음 예외 로깅
- [ ] #16 skip 사유 reasons 오염 > **2026-08-08: 이 배치는 Batch 6 과 중복된 착수 전 계획 목록이었다.** #14·#16·#18·#19 는
- [ ] #19 EXIF/GPS PIL 폴백 경고 > Batch 6 에서 이미 처리돼 [x] 로 기재돼 있고, #20 은 같은 곳에 연기 근거와 함께 남아 있다.
- [ ] #20 ScoreResult.reasons tuple > 여기서 중복 추적하지 않는다 — 상태는 Batch 6 이 기준.
- [ ] #18 dedup 핫패스 타깃 쿼리
- [x] #14 무음 예외 로깅 → Batch 6 참조 (잔여 swallow 는 Batch 6 에 별도 미체크로 남음)
- [x] #16 skip 사유 reasons 오염 → Batch 6 참조
- [x] #19 EXIF/GPS PIL 폴백 경고 → Batch 6 참조 (테스트 실행 로그에서 실제 warning 발생 확인:
`preprocessing.py:52 external derivative fell back to original bytes…`)
- [x] #18 dedup 핫패스 타깃 쿼리 → Batch 6 참조
- [ ] #20 ScoreResult.reasons tuple → Batch 6 의 연기 항목과 동일 건