기능 실효성 평가에서 확정된 판별력 붕괴를 고친다. 신호조합 224개 전수에서 77.7% 가 high, 56% 가 정확히 100점에 붙고 low 는 3.6% 뿐이었다. 큐 기본 정렬이 riskScore 내림차순인데 전건이 100 이면 정렬이 no-op 이므로, 점수가 운영자에게 주는 우선순위 정보가 0비트였다. 동인은 하나가 아니라 둘이었고 둘 다 "같은 사실을 여러 행으로 센다"는 같은 형태다. 라벨 풀 (WEB_ENTITY_MAX_POINTS = 20) category 없는 raw Vision 웹 엔티티가 이름별 버킷이라 서로 dedup 되지 않고 round(40*conf) 씩 무한 누적됐다. Vision 은 maxResults=10 이라 모든 이미지에 라벨을 붙이므로, 이미지 매칭이 0건이어도 0.8 짜리 라벨 3개면 100/high 였고 웹에 전혀 없는 오리지널 사진이 66/medium 이었다. 모집단 전체에 상수로 존재하는 신호는 판별에 기여할 수 없다. medium 임계(30) 아래로 묶어 라벨이 케이스를 물들일 수는 있어도 밴드를 정하지는 못하게 했다. category(character/celebrity/work) 가 붙은 엔티티는 풀에서 제외한다 — 그것은 raw 라벨이 아니라 권리 분류이고 엔티티별 +45 를 유지한다. 조사 단계에서 "test_two_distinct_web_entities_stack_into_high_risk 가 이 동작을 고정하므로 함께 개정해야 한다"고 봤으나 틀렸다. 그 테스트는 category 있는 엔티티를 다루므로 두 분기를 가르면 개정 없이 통과한다. 웹 존재 풀 (WEB_PRESENCE_MAX_POINTS = 60) Vision 응답 1회가 "이 이미지가 웹에 있다"는 하나의 사실을 full / partial / page / visual 4개 상관된 뷰로 동시에 보고하는데, 스코어러가 이를 독립 신호로 합산해 45+35+35+10 = 125 -> 상한 100 이 됐다. 널리 게시된 평범한 이미지와 명백한 침해물이 같은 100/high 로 수렴한 이유다. high 임계(70) 아래로 묶었다. 원칙: 웹에 존재한다는 사실만으로는 high 가 되지 않는다. high 가 되려면 두 번째 종류의 신호(인물, 지식DB 지문 매칭, 승격된 검색 히트)가 필요하다. 존재 자체는 침해가 아니고 그 판단은 운영자의 몫이다. 측정 (224조합, v1 -> v2) 웹 신호만으로 high 가 되는 조합 2/7 -> 0/7 100점 포화 66.5% -> 51.3% 웹에 없는 오리지널 사진, 라벨만 100/high -> 20/low 이미 안전 확인한 자산(cleared) 100/high -> 20/low 웹 존재 광범위(4개 매치 타입) 100/high -> 60/medium 완전일치 사본 + 식별가능 인물 80/high -> 80/high (불변) 명백한 침해(전 신호) 100/high -> 100/high (불변) 전체 high 비율은 82.6% -> 76.3% 로 크게 안 움직인다. 이 격자는 신호 축을 균등 샘플링하므로(지식DB 강매칭이 1/4) 실제 큐 분포의 대리값이 아니며, 실 DB 6건도 100x4 -> 95x4 로 밴드가 바뀌지 않는다 — 그 4건은 전부 "완전일치 사본 + 인물" 이라 high 가 맞다. 고친 것은 거짓 high 이지 진짜 high 가 아니다. 두 상한은 신호의 구조적 성질에서 나온 값이지 캘리브레이션 결과가 아니다. 판정 30~50건이 쌓이면 decision_stats 교차표로 임계와 함께 재조정해야 한다. 라운드트립 버킷 보존 테스트(test_review_fixes.py)는 신뢰도 1.0 에서 두 엔티티가 모두 캡에 붙어 성질이 가려졌다. 검증 대상은 점수가 아니라 엔티티 이름 버킷이 직렬화를 넘어 살아남는가이므로, 풀 아래(신뢰도 0.2)에서 재작성하고 동일 엔티티 반복이 여전히 1건으로 접히는지도 함께 본다. 테스트 5건 추가. pytest 570 passed(착수 전 565), 린트 클린.
16 KiB
기능 실효성 평가 — "이 도구가 실제 업무에 도움이 되는가"
2026-08-08 · 6축 조사 + 축별 적대적 반증(에이전트 12). 모든 수치는 실코드·실DB·실모델 실행 결과다.
결론
검색·기록 노동은 실제로 줄여준다. 심사할 건수는 전혀 줄여주지 못한다. 현재 이 도구는 위험 판별기가 아니라 증거 수집·기록기다.
| 축 | 판정 |
|---|---|
| 증거 수집 자동화 | 작동 — 단, 탐지력 이득이 아니라 배치·영속화·감사 이득 |
| 위험 점수 판별력 | 미작동 — 전건 high 수렴, 우선순위 정보 0비트 |
| LLM 증거 판독 | 격리는 성공, 산출물 가치는 미검증 — 운영 DB 기준 한 번도 표시된 적 없음 |
| 콘솔 작업 경제성 | 승인/보류 루프만 빠름 — 반려·일괄·감사는 병목 |
| 운영 준비도 | 폐쇄망 반입·백업은 견고, 신원·처리량은 미구현 |
1. 실제로 도움이 되는 것 (반증 통과)
- Cloud Vision 웹탐지가 신규 제출 전건에 자동 1회 실행되고 결과가 케이스에 영구 저장 + 감사기록으로 남는다
(
integrations/cloud_vision_web_detection.py:39-98,jobs/batch_analyzer.py:77-84,sqlite_store.py:430-446). 탐지 인덱스는 운영자가 Lens에 직접 넣는 것과 동일하므로 이득은 탐지력이 아니라 배치·영속화·감사다. - 승인/보류 키보드 1타 + 자동 다음 케이스 이동 (
app-keyboard-batch.js:182-200). 저위험 대량 승인 루프는 실제로 빠르다. - 중복 탐지가 실증적으로 일한다 — 재저장(JPEG q90)/80%축소/50%축소/밝기+10% 변형 24케이스 중 24건 그룹핑, 같은 24케이스에서 SHA-256 완전일치는 0건. 지각 해시 레이어가 exact 해시가 못 잡는 재제출을 잡는다. 갭은 5% 크롭 6/6 미탐(해밍 2~5비트 vs 임계 0.97의 1비트 예산).
- 판정 시점 스냅샷 · 메모 없는 반려 차단(서버+클라이언트) · 대상별 내보내기 화이트리스트
(
store_workflow.py:43-85,:21-37APPLICANT 5필드 vs INTERNAL 10필드). - 백업/복구 왕복 실증 성공 (
Connection.backup+ pre-restore 구제 사본 + 사이드카 제거). - 폐쇄망 Python 반입 절차 완결 — 런타임 서드파티 3개 핀 고정, GUI CDN 참조 0건, Pretendard 동봉, YuNet 가중치 실물이 문서 표의 232,589 bytes와 일치.
- SSRF 가드 실동작, 쿼터 원자 예약(try_reserve/release), 쿼리 시그니처 dedup으로 재분석 시 Naver/CSE 재소모 0.
- LLM이 점수·밴드를 절대 움직이지 못하도록 격리 (
risk_scoring.py:89, 101-102). Ollama 부재 시 읽을 수 있는 failure 행 1건으로 끝나고 점수 영향 없음(실측 2.01s / 0.19s).
2. 판별력 붕괴 — 이 도구가 "심사 건수"를 못 줄이는 이유
신호조합 224개 전수 계산: high 174건(77.7%) / medium 42건(18.8%) / low 8건(3.6%), 126건(56.2%)이 정확히 100점. 실 DB 6건도 100×4 / 45×2, low 0건.
지배적 동인은 match 타입 스택이 아니라 web entity 누적이다 (analysis/risk_scoring.py:159-160, 168-174).
버킷이 entity 이름별로 분리돼 서로 dedup되지 않고 round(40 × confidence)씩 쌓인다. Vision은 maxResults=10
(cloud_vision_web_detection.py:47)이라 모든 이미지가 라벨 10행을 받는다. 실측:
| 입력 | 결과 |
|---|---|
| entity 2개만 (이미지 매칭 0건) | 72 / high |
| entity 3개만 (이미지 매칭 0건) | 100 / high |
| 웹에 전혀 없는 오리지널 사진 (dog/pet/grass) | 66 / medium |
| full+partial+visual+page 각 1건 (평범한 웹 존재) | 100 / high |
| entity4 + full + partial + visual + page + 얼굴 (명백 침해) | 100 / high |
이 누적은 사고가 아니라 테스트가 고정한 동작이다 — test_risk_scoring.py:234-258이
"서로 다른 엔티티 2개는 반드시 high로 쌓여야 한다"를 assert한다. 따라서 기존 테스트로는 절대 잡히지 않는다.
파생 결과:
- 큐 기본 정렬이 riskScore 내림차순(
app-selectors.js:304)인데 전건 100이면 정렬이 no-op다. - low 밴드가 사실상 도달 불가 — low 8건은 전부 "아무것도 못 찾음" 조합뿐이라 "안 봐도 된다"는 결론을 못 준다.
- 신호 우선순위 역전 — 법적으로 가장 결정적인 EXIF/IPTC 권리 표기가 0점이라
(c) Getty Images단독 제출은 0점/low인데, Vision의 추측 라벨 3개는 100/high다 (risk_scoring.py:80-88PROVENANCE는 reason만 남기고 continue). - 운영자 되먹임이 순증 노동 — irrelevant 표시는 버킷당 "점수 매기는 첫 미표시 행"만 밀어내므로,
실 DB 제출물 4번을 100→35로 내리려면 google 증거 51행을 개별로 표시해야 한다. 일괄 API가 없다(개별
/api/evidence/{id}/status뿐). - cleared_reference가 재심사를 못 막는다 — FINGERPRINT 분기에서만 0점이라 [cleared_reference + 평범한 Vision 응답] = 100 high. 외부 호출 쿼터도 그대로 소모한다.
정답 라벨 0건: submissions 6건 전부 unreviewed, corrections 0행, Operator decision created 감사 이벤트 0건.
밴드 임계 70/30이 실제 판정과 한 번도 대조된 적이 없다.
3. 검증 코퍼스가 없다 (전제 정정)
data/copyrighter.sqlite3의 evidence 292행 중 288행이 2026-05-26, 이 리포의 첫 커밋은 2026-06-09이다.
즉 DB는 git 이력보다 2주 앞선 구빌드 스냅샷이며 6~8월 두 달치 변경이 반영돼 있지 않다:
- 저장 entity 행 40개 전부
entity=null→ 리스코어 시 0점 (현행 코드는 저장함) - 저장 entity confidence가 14.862 (현행 코드는 [0,1] 클램프)
- 증거 292행 전부
summary/domainSource/faceTier/verified/groundedClaims키 부재 - submissions 6건 전부
scorerVersion부재,submittedEpoch=0
판별력 판정을 확정하려면 실이미지 30~50장을 현행 코드로 새로 돌려야 한다. 그것이 밴드 캘리브레이션의 출발점이기도 하다.
4. 이번에 수정한 것 (P0)
(1) 폴더 정리가 판정 기록을 지우던 문제 — 확정 후 수정
실증: record_decision('3','rejected') → export 1행 → 원본 파일 제거 → _prune_missing_submission_files →
submissions 0행 / evidence 0행(cascade) / export 0행.
sqlite_store.py:_prune_missing_submission_files에 판정 완료 가드를 넣었다. 판정된 케이스는 원본 파일이 없어도
보존하고 assetMissing 마커 + 감사 이벤트를 1회만 남긴다(매 리로드마다 재기록되면 500행 감사 캡을 먹는다).
미판정 케이스의 프루닝 동작은 그대로다.
(2) 비상 차단 스위치가 장식이던 문제 — 확정 후 수정
대조실험: 스텁 어댑터로 계수 → CONTROL 5회 / emergency_disable_external_providers() 후 5회로 동일.
원인은 스위치가 DB 플래그만 쓰고 자동 보강 경로가 그 플래그를 아예 읽지 않은 것. 게이트는 수동 검색에만 있었다.
_external_calls_blocked(provider_id)를 신설(store_enrichment.py)하고 자동 외부 호출 4개 경로 전부에 배선했다:
| 경로 | provider |
|---|---|
_auto_naver_search |
naver |
_auto_google_custom_search |
google_search |
_rerun_google_image_search |
google |
_google_face_crop_web_detection |
google (생체 전송이라 상위 게이트에 의존하지 않고 독립 게이트) |
시드 경로 Vision 배치 (sqlite_store.py) |
google → ExternalApiPolicy(disabled=True) |
게이트는 enabled가 아니라 operatorDisabled 를 본다. 전자는 연결성/헬스에도 반응하므로 운영자가 멈추라고
한 적 없는 트래픽까지 죽인다. 후자는 개별 토글과 비상 스위치만 세우는 의도적 정지이고 리로드에도 유지된다.
(3) 생체정보(얼굴 크롭)가 보존기간을 초과 생존하던 문제 — 확정 후 수정
두 경로가 있었다: (a) 스냅샷이 face-crops를 통째로 담는데 프루닝은 나이가 아니라 개수 10개 기준이라
90일에 파기됐어야 할 크롭이 암호화 없는 zip 안에 남았다. (b) 복구는 copyfileobj로 새로 쓰므로 mtime이 리셋되고,
purge가 mtime을 보므로 파기 대상이 복구 직후부터 다시 90일을 산다.
store_backup.py의 _MEDIA_SOURCES에서 face-crops를 제외하고, 복구 시에도 _RETENTION_BOUND_MEDIA로
레거시 아카이브의 크롭을 건너뛴다. 크롭은 원본 이미지에서 재분석으로 재생성되므로 손실 비용이 없다.
(4) 오프라인 설치 문서가 계약 위반 모델을 지목하던 문제
offline-install.md가 qwen2.5:0.5b-instruct 반입을 지시했다 — 같은 리포가 55.7초 + 프롬프트 형식 예시 반향으로
출력 계약 위반 기록을 남긴 모델이다. 문서대로 반입하면 타깃에서 요약이 조용히 죽는다.
qwen2.5:7b로 정정하고, ollama 바이너리 반입 누락·GPU 전제·"env 비우면 LLM 꺼진다"는 거짓 서술
(실측: build_provider_runtime({}) → enabled=True, compliance 문자열까지 참으로 표시)을 함께 고쳤다.
검증: .\verify.ps1 — ruff 클린, pytest 565 passed(착수 전 562, 신규 회귀 테스트 3건).
5. 남은 우선순위
P1 — 판별력 복구 (이것이 "업무 도움" 여부를 가르는 축)
web entity 누적 캡→ 완료(2026-08-09,SCORER_VERSIONv1→v2). 아래 6절 참조. 조사 단계의 "test_risk_scoring.py:234-258이 현 동작을 고정한다"는 예측은 틀렸다 — 그 테스트는category가 붙은 엔티티(+45 분기)를 다루고, 포화를 만든 것은category없는 raw 라벨(+round(40*conf)분기)이다. 두 분기를 갈라 라벨만 캡했으므로 그 테스트는 개정 없이 통과한다.- PROVENANCE(EXIF/IPTC 권리 표기)에 배점. 단 실이미지 56장에서 증거 행 0건이었고,
방송캡처 분기는
MIN_CAPTURE_CATEGORIES=2에서 geometry가 1개 카테고리라 자막 없는 방송 캡처는 구조적으로 도달 불가다 (provenance.py:85-88). 배점 전에 이 분기부터 고칠 것. - 매칭 호스트 기반 신뢰출처 구분. 호스트 데이터는 이미 전 매칭 행에 저장돼 있고(실측 105행에서 도메인 추출 가능) 점수만 안 쓴다.
- 밴드 재캘리브레이션은 판정 30~50건이 쌓인 뒤.
decision_stats교차표는 이미 구현돼 있다.
P2 — 운영
- 운영자 신원. 현재 감사 actor가 전부 하드코딩 리터럴(
rights.ops/admin.ops/system)이라 "누가 반려했나"에 답할 수 없다. - 레거시 llm 행 마이그레이션.
summary키가 없는 9행이_provider_item_has_result를 통과해only_if_missing경로를 막으므로 6개 제출물 어디에도 요약이 영원히 생성되지 않는다. 커버리지 패널은 "했다"고 보고한다. - LLM 프롬프트의
domain이 실호스트가 아니라 provider 이름(store_serialization.py:298이data['provider']를 우선). 저작권 심사에서 가장 중요한 "어디에 떠 있나"를 모델이 원천적으로 말할 수 없다. 같은 케이스 55행 중 41행이 url을 갖고 있는데 프롬프트 12줄 전부domain=google이다. - 자동 쿼리 3슬롯을 페이지 제목이 전부 선점(우선순위 100 vs entity 88/82)해 권리 지향 쿼리가 구조적으로 실행되지 않음
_clean_title이 말줄임표를 제거하지 않아 저장 제목 39개 중 13개(33%)가 잘린 채 검색어가 된다.
- 판정당 bootstrap 전량 왕복 2회. 300케이스 실측 9.58MB, 유효 델타는 36KB.
POST
/decision응답이 이미 완결된 케이스 페이로드인데 버리고/api/bootstrap을 다시 친다. - 반려 경로의 키보드 단절(제출 단축키 없음), 감사 로그 500건 고정 상한 + 필터·내보내기 부재.
6. P1 1차 — 신호 풀 도입 (2026-08-09, SCORER_VERSION v1 → v2)
포화의 동인은 하나가 아니라 둘이었다. 둘 다 "같은 사실을 여러 행으로 세는" 같은 형태다.
| 풀 | 상한 | 무엇을 묶나 | 근거 |
|---|---|---|---|
WEB_ENTITY_MAX_POINTS |
20 (medium 30 미만) | category 없는 raw Vision 라벨 |
Vision 은 모든 이미지에 라벨을 최대 10개 붙인다. 모집단 전체에 상수로 존재하는 신호는 판별에 기여할 수 없다 |
WEB_PRESENCE_MAX_POINTS |
60 (high 70 미만) | full / partial / page / visual / url | 응답 1회가 "이 이미지가 웹에 있다"는 하나의 사실을 4개 상관된 뷰로 보고한다. 합산은 45+35+35+10=125 로 상한을 넘겨 웹에 조금이라도 있으면 전부 100 이 됐다 |
category(character/celebrity/work)가 붙은 엔티티는 풀에서 제외했다 — 그것은 raw 라벨이 아니라 권리 분류이고 엔티티별 +45 를 유지한다.
설계 원칙: 웹에 존재한다는 사실만으로는 high 가 되지 않는다. high 가 되려면 두 번째 종류의 신호 (인물, 지식DB 지문 매칭, 승격된 검색 히트)가 필요하다. 존재 자체는 침해가 아니고, 그 판단이 운영자의 몫이다.
신호조합 224개 전수 재측정 (v1 → v2):
| 케이스 | v1 | v2 |
|---|---|---|
| 웹에 없는 오리지널 사진, 라벨만 | 100 / high | 20 / low |
| 팀이 이미 안전 확인한 자산(cleared) | 100 / high | 20 / low |
| 인물 사진, 라벨만 | 100 / high | 55 / medium |
| 웹 존재 광범위(4개 매치 타입) | 100 / high | 60 / medium |
| 웹 존재 + 배경 인물 | 100 / high | 70 / high |
| 완전일치 사본 + 식별가능 인물 | 80 / high | 80 / high |
| 지식DB 매칭 + 인물 | 100 / high | 100 / high |
| 명백한 침해(전 신호) | 100 / high | 100 / high |
다른 신호 없이 웹 신호만으로 high 가 되는 조합: 2/7 → 0/7. 100점 포화 조합 66.5% → 51.3%.
정직한 한계: 224조합 전수의 high 비율은 82.6% → 76.3% 로 크게 안 움직인다. 이 격자는 신호 축을 균등 샘플링하므로(지식DB 강매칭이 1/4을 차지) 실제 큐 분포의 대리값이 아니다. 실 DB 6건도 100×4 → 95×4 로 밴드가 바뀌지 않는다 — 그 4건은 전부 "웹에 완전일치 사본 + 인물"이라 high 가 맞다. v2 가 고친 것은 거짓 high (웹에 없음/라벨만/이미 안전 확인)이지, 진짜 high 를 줄인 게 아니다.
풀 상한 60/20 은 신호의 구조적 성질에서 나온 값이지 캘리브레이션 결과가 아니다. 판정 30~50건이 쌓이면
decision_stats 교차표로 임계와 함께 재조정해야 한다.
적대적 검증에서 반증된 항목 (되돌리려는 시도 방지용 기록)
폐쇄망에서 증거 썸네일이 안 보인다→ 런타임 외부 호출은 허용된 설계이고http_app.py:510-511에 근거 주석이 있다. 다만 서버 egress와 브라우저 egress는 다른 문제이므로 납품 전 타깃망에서 썸네일 1장 실로드 확인은 남긴다.중복 그룹핑 backfill이 미작동→ reload/import-folder/upload 3개 라우트 전부가 backfill을 거친다. 배지가 안 뜬 건 테스트 이미지 6장이 서로 다른 사진이라서다.재분석이 Naver/CSE 쿼터를 재소모→ 시그니처 dedup으로 0콜. 실제 재소모는 Vision 쪽(dedup 없음 + 얼굴크롭 최대 3콜).쿼터로 스킵된 쿼리가 영구 등재돼 재시도 불가(7/24 H-3)→_RETRYABLE_SKIP_DOMAIN_SOURCES로 이미 수정됨.판정 내보내기가 decidedAt/memo를 빈 값으로 낸다→ 현행 경로는 정상. 실 DB의 공백 63행은 전부 비판정 이벤트다.TLS 부재가 취약점→ 기본 바인드 127.0.0.1,0.0.0.0참조 0건. 미착수로 문서화된 항목이지 숨은 취약점이 아니다.