# 운영자 워크벤치 효율 개선 설계

> 날짜: 2026-06-11
> 주제: operator-workbench-efficiency
> 목표: 운영자 일일 업무 효율 — 검색 보강 수작업 축소, 케이스 판단 속도 향상

## 배경과 목표

운영자의 두 가지 병목을 해소한다:

1. **검색 보강 수작업** — 자동 생성 쿼리가 빗나갔을 때 운영자가 추천 쿼리를 입력칸에 옮겨 하나씩 실행하는 반복 작업.
2. **케이스 판단 속도** — 상세 검토 화면에서 근거를 읽고 승인/보류/반려를 결정하는 데 걸리는 시간.

설계 전 코드 검증으로 확인한 사실: 쿼리 이력 재실행 버튼(`web/operator-gui/app.js`의 `rerunHistoricalQuery`), 근거 보강 추천 패널(`renderEvidenceNextActions`), 서버측 Google Custom Search 자동 보강(`sqlite_store.py`의 `_auto_google_custom_search`), 얼굴 크롭 생성(`build_face_crop_derivatives`)은 이미 존재한다. 이 설계는 검증된 실제 갭만 다룬다.

## 기능 목록

| ID | 기능 | 노력 | 해소 병목 |
|----|------|------|-----------|
| F1 | 추천 쿼리 원클릭/일괄 실행 | S | 검색 수작업 |
| F2 | Google 수동 검색 재노출 | S | 검색 수작업 |
| F3 | 얼굴 크롭 썸네일 표시 | M | 판단 속도 |
| F4 | 재분석 증거 diff 뷰 | M | 판단 속도 |
| F5 | KB 등록된 기준 정비(검색/필터/편집/비활성) | M | 판단 속도·오탐 정리 |

구현 순서: F1 → F2 → F5 → F3 → F4. 기능 간 의존성은 없으며 효과 대비 노력 순이다.

---

## F1. 추천 쿼리 원클릭/일괄 실행

### 현재 동작

근거 보강 추천 패널(`renderEvidenceNextActions`)의 추천 쿼리 버튼은 수동 검색 입력칸(`#manual-query`)을 채우기만 한다(`applySuggestedQuery`). 운영자는 쿼리마다 버튼 클릭 → 검색 실행을 반복해야 한다.

### 변경 사항 (GUI만, 서버 변경 없음)

- 추천 쿼리 버튼마다 **"바로 실행"** 보조 버튼을 추가한다. 클릭 시 기존 `/api/search/manual` 엔드포인트를 해당 쿼리·기본 제공자(naver)로 즉시 호출한다.
- 패널 상단에 **"모두 실행"** 버튼을 추가한다. 추천 쿼리(최대 4개)를 순차 호출한다. 병렬 호출하지 않는다(쿼터 계측과 상태 표시 단순화).
- 쿼리별 진행 상태를 인라인으로 표시한다: `실행 중…` → `완료(근거 N건)` 또는 실패 사유(쿼터 초과, 제공자 비활성 등). 실패 사유는 서버가 반환하는 기존 `SEARCH_SKIPPED`/오류 메시지를 그대로 사용한다.
- 기존 "입력칸 채우기" 동작은 유지한다(운영자가 쿼리를 수정하고 싶을 때).
- 실행 완료 후 케이스 증거 목록을 갱신한다(기존 수동 검색 완료 후 갱신 로직 재사용).

### 수용 기준

- 추천 쿼리를 클릭 한 번으로 실행하고 결과 근거가 증거 목록에 합류한다.
- "모두 실행"은 일부 쿼리가 실패해도 나머지를 계속 실행하고, 쿼리별 결과를 각각 표시한다.

---

## F2. Google 수동 검색 재노출

### 현재 동작과 안전 계약

서버는 수동 검색(`manual_search`)과 후보 수집(`collect_keyword_candidates`)에서 `google_search` 제공자를 이미 지원하고, 자동 보강도 Google Custom Search를 사용한다. 그러나 GUI는 `retiredProviderIds = ["google_search"]`(app.js:14)로 수동 검색·후보 수집 선택지에서 의도적으로 제외했고, `tests/operator_gui/test_static_workbench.py::test_safety_rules_are_visible_in_ui_contract`가 `option value="google_search"` 부재를 안전 규칙으로 고정하고 있다.

**이 설계는 해당 안전 규칙을 의도적으로 개정한다** (운영자 결정: 2026-06-11 승인). 유지되는 안전 경계: 텍스트 쿼리 기반 검색만 허용하며, 이미지 업로드 역검색 UI는 어떤 제공자에도 추가하지 않는다(`reverse search` 부재 검증은 유지).

### 변경 사항

- **GUI**: `operatorSearchProviders`에 `google_search`("구글 근거 검색" — `operator-labels.js`에 라벨 기존재)를 추가하고 `retiredProviderIds`에서 제거한다. 수동 검색(`#manual-query-provider`)과 후보 수집(`#collection-provider`) 선택지에 노출한다.
- 어댑터 미설정(서버 providers 페이로드의 `enabled: false` 또는 `requiredEnv` 미충족) 시 선택지는 보이되 비활성화하고 사유를 툴팁/캡션으로 표시한다. 제공자 카드의 쿼터 표시는 기존 그대로 사용한다.
- **테스트**: `test_safety_rules_are_visible_in_ui_contract`의 `google_search` 부재 단언을 존재 단언으로 교체하고, 테스트 독스트링에 개정 사유와 날짜를 남긴다. `reverse search` 부재 단언은 유지한다.
- **서버 변경 없음.**

### 수용 기준

- 운영자가 수동 검색에서 네이버/구글을 선택해 실행할 수 있다.
- Google 어댑터 미설정 환경에서는 선택지가 비활성화되고 사유가 보인다.
- 이미지 업로드 역검색 UI는 여전히 존재하지 않는다.

---

## F3. 얼굴 크롭 썸네일 표시

### 현재 동작

`HeuristicFacePersonDetector`가 `face_boxes` 좌표를 반환하고, `_google_face_crop_web_detection`(sqlite_store.py:1928)이 `build_face_crop_derivatives`로 크롭을 만들어 Google 웹 탐지에 보낸다. 크롭은 메모리에서만 쓰이고 운영자에게 보이지 않는다. GUI는 "얼굴 영역 웹 근거" 라벨과 크롭 인덱스 메타만 표시한다.

### 변경 사항

- **크롭 생성 일원화**: 분석 시 얼굴 감지 결과(face_boxes)로 크롭을 한 번 생성해 디스크에 저장하고, Google 얼굴 크롭 웹 탐지는 저장된 크롭을 재사용한다(현재의 중복 감지 제거).
- **저장 위치**: 데이터 디렉터리 하위 `face-crops/{submission_id}/crop-{N}.jpg`. `knowledge_media_path`/`collected_media_path`(sqlite_store.py:1697, 1704)와 같은 패턴으로 `face_crop_media_path`를 추가한다.
- **서빙**: `http_app.py`에 `GET /face-crop-media/{path}` 라우트를 추가하고, 기존 untrusted 미디어 응답 헤더(nosniff + sandbox CSP)를 동일하게 적용한다.
- **페이로드**: `review()` 응답과 부트스트랩 submission에 `faceCrops: [{index, url, box: [x, y, w, h]}]`를 추가한다. 얼굴이 없으면 빈 배열.
- **GUI**: 워크벤치 이미지 패널 하단에 크롭 썸네일 스트립을 표시하고, 클릭 시 확대(기존 이미지 확대 패턴 재사용). "얼굴 영역 웹 근거" 증거 행의 크롭 인덱스를 해당 썸네일과 연결해 어떤 얼굴에서 나온 근거인지 시각적으로 잇는다.
- **재분석 시**: 크롭을 재생성하고 이전 크롭 파일은 교체한다(같은 경로 덮어쓰기, 잔여 인덱스 파일 삭제).

### 거버넌스 경계

- 크롭은 내부 분석용 파생 파일이다. 원본/축소본/파생 파일에 적용되는 보존·삭제 정책(R26) 대상에 포함되며, 제출 레코드 삭제 시 크롭 디렉터리도 함께 삭제한다.
- 크롭은 좌표 기반 이미지 절단일 뿐 얼굴 임베딩·생체 템플릿이 아니다(R21/R22 경계 유지). 신청자 화면에 노출되지 않는다(R18 — 운영자 GUI 전용 라우트).

### 수용 기준

- 얼굴이 감지된 케이스의 워크벤치에서 크롭 썸네일이 보이고 클릭으로 확대된다.
- 얼굴 영역 웹 근거 행에서 해당 크롭을 식별할 수 있다.
- 얼굴이 없는 케이스·크롭 디코드 실패 시 화면 오류 없이 스트립이 생략된다.

---

## F4. 재분석 증거 diff 뷰

### 현재 동작

`rerun_enrichment`(sqlite_store.py:1140)는 증거를 재수집하고 재점수화하지만, 운영자는 무엇이 달라졌는지 전체 증거를 다시 훑어야 안다.

### 변경 사항

- **서버**: `rerun_enrichment` 시작 시 현재 증거 ID 집합과 점수를 스냅샷하고, 완료 후 비교해 submission JSON 페이로드에 저장한다:

  ```json
  "lastRerunDiff": {
    "at": "<재분석 시각 라벨>",
    "scoreBefore": 62, "scoreAfter": 78,
    "addedEvidenceIds": ["..."],
    "removedEvidenceIds": ["..."],
    "removedSummaries": [{"source": "naver", "reason": "..."}]
  }
  ```

  제거된 증거는 행이 사라지므로 요약(`source`, `reason`)을 함께 저장한다. JSON 페이로드 필드 추가만으로 충분하며 스키마 마이그레이션은 없다. 이전 분석이 없던 제출(최초 분석)은 diff를 기록하지 않는다.
- **GUI**:
  - 점수 변화 배지: `62 → 78 (↑16)` 형태로 워크벤치 상단 점수 옆에 표시. 색이 아닌 기호·텍스트로 방향을 표기한다(기존 접근성 규칙).
  - `addedEvidenceIds`에 해당하는 증거 행에 "신규" 배지와 하이라이트를 표시한다.
  - 제거된 증거는 "이번 재분석에서 제거됨 (N건)" 접이식 요약으로 표시한다.
  - diff는 다음 재분석 때 덮어써진다. 별도 "확인" 처리 없이 항상 마지막 재분석 기준으로 표시한다(KISS — 운영자 피드백 후 필요하면 확인/해제 추가).

### 수용 기준

- 재분석 직후와 케이스 재방문 시 신규 증거·제거 증거·점수 변화가 한눈에 보인다.
- 최초 분석만 있는 케이스에는 diff UI가 나타나지 않는다.

---

## F5. KB 등록된 기준 정비

### 현재 동작

기준 데이터베이스 화면의 "등록된 기준" 탭(`data-knowledge-panel="registered"`)은 `#knowledge-list` 단순 목록뿐이다. 워치리스트 승격/오탐 제외(`promote_watchlist_entry`/`exclude_watchlist_entry`)는 있으나, 항목 내용 수정·임의 비활성·목록 검색/필터가 없다. 등록만 가능한 KB는 오염 항목이 누적될수록 오탐을 만든다.

### 변경 사항

- **GUI(등록된 기준 탭)**:
  - 검색 입력: 이름·별칭·키워드 부분일치(클라이언트 측 — 부트스트랩에 전체 항목이 이미 포함됨).
  - 필터: 유형(연예인/작품/캐릭터/게임/반려 참조), 항목 상태(watchlist/confirmed/excluded), 활성/비활성.
  - 항목별 인라인 편집: 별칭, 검색 키워드, 정책 메모를 펼침 폼으로 수정. 이름·유형·출처(provenance)는 수정 불가(출처 추적성 보존).
  - 비활성화 버튼(사유 메모 선택), 재활성화 버튼(**사유 메모 필수** — 기존 GUI 설계 문서의 "Reactivate entry only with memo" 규칙).
  - 자동 누적/수동 등록 출처 구분 표시는 기존 그대로 유지한다.
- **서버**(기존 `/api/knowledge/manual`, promote/exclude 패턴을 따름):
  - `PATCH /api/knowledge/{id}` — body `{aliases?, keywords?, memo?}` 부분 수정. 빈 body는 400.
  - `POST /api/knowledge/{id}/deactivate` — body `{reason?}`.
  - `POST /api/knowledge/{id}/reactivate` — body `{reason}` 필수, 없으면 400.
  - 세 작업 모두 감사 이벤트를 기록한다: `Knowledge entry updated` / `deactivated` / `reactivated`(변경 전후 요약 포함).
  - 비활성 항목은 점수 계산·유사도 매칭에서 제외된다(기존 `active` 필드 의미 유지 — 신규 로직 아님을 구현 시 검증).

### 수용 기준

- 운영자가 KB 항목을 이름/별칭/키워드로 찾고, 유형·상태·활성으로 거를 수 있다.
- 별칭·키워드·메모를 수정하면 감사 로그에 전후가 남는다.
- 재활성화는 메모 없이 불가능하다.
- 비활성화된 항목은 이후 분석의 위험도에 반영되지 않는다.

---

## 공통 오류 처리

- 외부 검색 실패·쿼터 초과·제공자 비활성: 기존 `SEARCH_SKIPPED`/실패 증거 패턴을 그대로 사용하고, F1·F2의 UI는 그 사유를 쿼리별로 표시한다. 실패가 기존 고위험 근거를 낮추지 않는다는 기존 규칙(R23)은 변경하지 않는다.
- 얼굴 크롭 디코드/저장 실패: 분석을 중단하지 않고 크롭 없이 진행한다(기존 graceful degradation 패턴).
- KB 편집 동시성: 단일 운영 콘솔 전제로 마지막 쓰기 우선. 감사 이벤트로 전후가 남으므로 복구 가능하다.
- 모든 신규 엔드포인트는 기존 핸들러와 동일하게 `ValueError → 400`, `KeyError → 404` 매핑을 따른다.

## 테스트 전략

기존 테스트 패턴을 따른다:

- `tests/rights_filter/server/test_http_app.py`: F3 크롭 페이로드·서빙 라우트, F4 `lastRerunDiff` 생성·최초 분석 시 부재, F5 PATCH/deactivate/reactivate(메모 필수 검증, 감사 이벤트 기록, 비활성 항목 점수 미반영) 테스트 추가.
- `tests/operator_gui/test_static_workbench.py`: F1 바로 실행/모두 실행 컨트롤 존재, F2 안전 계약 개정(google_search 존재 단언 + reverse search 부재 유지), F3 크롭 스트립, F4 diff 배지, F5 검색/필터/편집 컨트롤의 UI 계약 단언 추가.
- `tests/rights_filter/analysis/`: 크롭 생성 일원화에 따른 `preprocessing` 동작 테스트 보강.
- 커버리지 80% 이상 유지.

## 범위 제외

- LLM 쿼리 생성 고도화, 자동 폴백 전환(접근 B) — 별도 라운드.
- 운영자 인증/역할 분리 — 별도 기반 작업으로 분리(이 라운드는 기능 효율에 집중, 감사 이벤트 행위자는 기존 `rights.ops` 고정값 유지).
- 결정 통계 대시보드, 지문 유사도 매트릭스 — 후속 후보.
- 이미지 업로드 역검색 — 어떤 제공자에도 추가하지 않음(안전 경계 유지).
- 신청자 노출 변경 없음 — 모든 신규 표면은 운영자 GUI 전용.

## 수용 기준 요약 (라운드 전체)

1. 운영자가 추천 쿼리를 클릭 한 번 또는 일괄로 실행해 근거를 보강할 수 있다.
2. 운영자가 수동 검색에서 네이버와 구글을 선택할 수 있고, 역검색 UI는 없다.
3. 얼굴 감지 케이스에서 크롭 썸네일로 초상권 판단을 빠르게 할 수 있다.
4. 재분석 후 무엇이 달라졌는지 diff로 즉시 파악할 수 있다.
5. KB 항목을 찾고, 고치고, 비활성화할 수 있으며 모든 변경이 감사 로그에 남는다.
