# 기능 실효성 평가 — "이 도구가 실제 업무에 도움이 되는가" 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-37` APPLICANT 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-88` PROVENANCE는 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 — 판별력 복구 (이것이 "업무 도움" 여부를 가르는 축)** 1. web entity를 지식DB/워치리스트와 대조된 것만 점수화 + 서술 라벨 누적에 캡. `test_risk_scoring.py:234-258`이 현 동작을 고정하고 있으므로 그 테스트도 함께 개정해야 한다. **`SCORER_VERSION` 을 올린다.** 2. PROVENANCE(EXIF/IPTC 권리 표기)에 배점. 단 실이미지 56장에서 증거 행 0건이었고, 방송캡처 분기는 `MIN_CAPTURE_CATEGORIES=2`에서 geometry가 1개 카테고리라 **자막 없는 방송 캡처는 구조적으로 도달 불가**다 (`provenance.py:85-88`). 배점 전에 이 분기부터 고칠 것. 3. 매칭 호스트 기반 신뢰출처 구분. 호스트 데이터는 이미 전 매칭 행에 저장돼 있고(실측 105행에서 도메인 추출 가능) 점수만 안 쓴다. 4. 밴드 재캘리브레이션은 판정 30~50건이 쌓인 뒤. `decision_stats` 교차표는 이미 구현돼 있다. **P2 — 운영** 5. 운영자 신원. 현재 감사 actor가 전부 하드코딩 리터럴(`rights.ops`/`admin.ops`/`system`)이라 "누가 반려했나"에 답할 수 없다. 6. 레거시 llm 행 마이그레이션. `summary` 키가 없는 9행이 `_provider_item_has_result`를 통과해 `only_if_missing` 경로를 막으므로 **6개 제출물 어디에도 요약이 영원히 생성되지 않는다**. 커버리지 패널은 "했다"고 보고한다. 7. LLM 프롬프트의 `domain`이 실호스트가 아니라 provider 이름(`store_serialization.py:298`이 `data['provider']`를 우선). 저작권 심사에서 가장 중요한 "어디에 떠 있나"를 모델이 원천적으로 말할 수 없다. 같은 케이스 55행 중 41행이 url을 갖고 있는데 프롬프트 12줄 전부 `domain=google`이다. 8. 자동 쿼리 3슬롯을 페이지 제목이 전부 선점(우선순위 100 vs entity 88/82)해 권리 지향 쿼리가 구조적으로 실행되지 않음 + `_clean_title`이 말줄임표를 제거하지 않아 저장 제목 39개 중 13개(33%)가 잘린 채 검색어가 된다. 9. 판정당 bootstrap 전량 왕복 2회. 300케이스 실측 9.58MB, 유효 델타는 36KB. POST `/decision` 응답이 이미 완결된 케이스 페이로드인데 버리고 `/api/bootstrap`을 다시 친다. 10. 반려 경로의 키보드 단절(제출 단축키 없음), 감사 로그 500건 고정 상한 + 필터·내보내기 부재. **적대적 검증에서 반증된 항목** (되돌리려는 시도 방지용 기록) - ~~폐쇄망에서 증거 썸네일이 안 보인다~~ → 런타임 외부 호출은 허용된 설계이고 `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건. 미착수로 문서화된 항목이지 숨은 취약점이 아니다.