POSA_Copyrighter/docs/superpowers/specs/2026-06-11-operator-workbench-efficiency-design.md

14 KiB

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

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

배경과 목표

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

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

설계 전 코드 검증으로 확인한 사실: 쿼리 이력 재실행 버튼(web/operator-gui/app.jsrerunHistoricalQuery), 근거 보강 추천 패널(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_contractoption value="google_search" 부재를 안전 규칙으로 고정하고 있다.

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

변경 사항

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

수용 기준

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

F3. 얼굴 크롭 썸네일 표시

현재 동작

HeuristicFacePersonDetectorface_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.pyGET /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 페이로드에 저장한다:

    "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 항목을 찾고, 고치고, 비활성화할 수 있으며 모든 변경이 감사 로그에 남는다.