# task-2899 보고서 — 소식지 검토 Workflow Phase 3a (검토 REST API 4종)

## Situation
Phase 2(task-2897, 머지 7e4c672)로 업로드→추출→자동검증9규칙→AI자기검증→grounding→`pending_review` 라우팅 파이프라인이 완성됐다. 그러나 사람이 검토·교정·승인할 **REST 엔드포인트가 없었다** — `transition_review_status`는 함수로만 존재. 검토 UI(Phase 3b)가 소비할 관리자 API가 빠져 있었다.

## Complication
상태전이/발행 fail-closed 3중잠금은 `newsletter_review.transition_review_status`(수정 금지)에 이미 강제돼 있으므로, 라우트가 이를 **우회·재구현하면 안 된다**. 또한 `field_confidence`(신뢰도 숫자)는 UI 미노출·내부보존만 해야 하고, 교정분(edited)은 `*_edited`에만 쓰고 원본 `*_extracted`는 절대 불변이어야 한다.

## Question
서버(main.py + routes + tests)만 건드려, migration/validation.py/src/extension/.github 불변·기존 소식지·업로드·newsletter-chat 회귀 0을 유지하며 관리자 검토 API 4종을 어떻게 노출하는가?

## Answer
신규 라우터 파일 1개 + main.py 등록 2줄 + 테스트 1개(순수 additive). 상태전이는 전부 기존 중앙함수에 위임.

### 생성/수정 파일
- **신규** `server/routes/newsletter_review_v1.py` (223줄) — 관리자 전용 4엔드포인트(전부 `Depends(require_admin)` = verify_jwt(401)→verify_admin(403) lazy 게이트, 순환import 회피).
  1. `GET /api/insuro/newsletters/review-queue` — 경량 목록. 필터 `review_status IN (pending_review,editing,auto_validated,extract_failed)`, 옵션 `?status=` 단일필터(허용밖 값 422). 정렬 = HIGH수 desc→MED수 desc→created_at desc(안정 2단계 정렬). body/field_confidence 미포함.
  2. `GET /api/insuro/newsletters/{id}/review` — 상세. extracted/edited 양쪽 + validation_flags + grounding(`field_confidence._grounding`만 파생) + 메타/타임스탬프. ★ **field_confidence 키 응답 부재**. 없으면 404.
  3. `POST /api/insuro/newsletters/{id}/transition` — 교정·전이. **transition 먼저 호출**(거부 시 edited 미기록→DB 흔적 0), 성공 후에만 edited를 `*_edited`에만 기록(`*_extracted` 불변). `transition_review_status(actor=JWT sub, actor_type='human', reviewer_id=JWT sub, ...)` 위임. UnknownStatus→422, 그 외 ReviewTransitionError(Forbidden/Actor/Human/Publish/StatusMismatch)·RPC예외→409.
  4. `GET /api/insuro/newsletters/insurer-master` — `newsletter_validation.DEFAULT_INSURER_WHITELIST`를 정렬 배열로 반환(import만, validation.py 수정 0).
- **수정** `server/main.py` (정확히 2줄: line 84 router import, line 342 include_router). grep 2건 반영 확인.
- **신규** `server/tests/test_newsletter_review_api.py` (554줄, 16 케이스) — FakeSupabase 더블.

### 머지 판단
- **머지 필요**: Yes (Lv.2 → PR)
- **브랜치**: task/task-2899-dev1
- **워크트리 경로**: /home/jay/projects/InsuRo/.worktrees/task-2899-dev1
- **base**: 7e4c672 (Phase2 HEAD)
- **머지 의견**: 순수 additive 3파일(신규2+2줄추가). 상태전이 재구현 0·`*_extracted` 불변·field_confidence 미노출을 코드+테스트로 보장. 회귀 위험 최소.

## 테스트 결과
- **신규**: `test_newsletter_review_api.py` → **16 passed** (0.10s). 요구 8검증 전부 커버.
- **회귀(핵심)**: `test_newsletter_review.py + test_newsletter_validation.py + test_main.py` → **315 passed** (22.31s).
- **전체 스위트**: **1184 passed, 1 failed** (132s). 실패 1건 = `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` — base(7e4c672, 변경 전 stash)에서도 **동일 재현**(로컬 `.env` 오염). 본 변경과 무관한 pre-existing 실패(메모리 기록된 알려진 이슈와 정합).

## L1 스모크테스트 결과 (실동작 검증)
- 서버 재시작: 해당없음(실 uvicorn+Supabase/JWKS 실연결 불가 환경). 대신 **실제 FastAPI app 객체 로드 + 라우트 dispatch 실호출**로 대체.
- API 응답 확인:
  - 실제 `main.app.routes`에 4개 라우트 **전부 마운트 확인**(review-queue/{id}/review/{id}/transition/insurer-master).
  - 비관리자 실호출 4/4 **403**(verify_admin `admin_access_denied` 로그 실발생 = 게이트 실동작).
  - `insurer-master` admin 실호출 → **200 + `sorted(DEFAULT_INSURER_WHITELIST)` 일치**(예: AIG손해보험/DB손해보험/KB손해보험…).
  - 결과: `=== L1 SMOKE ALL PASS ===`
- 스크린샷: 해당없음(API 백엔드, 프론트 없음).

## 발견 이슈 및 해결
1. **테스트 케이스 6 스펙-코드 불일치(정직 교정)**: 지시서는 `pending_review→published`가 `PublishFailClosed`라 했으나, 실제 `ALLOWED_TRANSITIONS`(수정금지)에 `(pending_review,published)` 키가 없어 **step2 `ForbiddenTransition`이 step4 fail-closed보다 먼저 발동**(published 진입키는 `(approved,published)` 유일). 코드 진실에 맞춰 해당 테스트를 `FORBIDDEN_TRANSITION` 기대로 수정 + 진짜 `PublishFailClosed`(from='approved' & reviewer_id 없음) 경로 별도 테스트 추가로 커버리지 보존. 두 경로 모두 409·rpc 0회(흔적0) 동일 보장.
2. **pyright**: `_get_supabase_client() -> Any` 명시(기존 consultation 라우트 컨벤션)로 supabase `.data` union 오탐 170건 해소 → `pyright routes/newsletter_review_v1.py` 0 errors. (diagnostics의 `main`/`sb_helpers` could-not-resolve는 pyright가 server/ 를 sys.path 루트로 못 잡은 것으로 기존 main.py 전반과 동일한 무해 오탐 — 런타임은 server/ cwd 실행으로 정상.)

## 모델 사용 기록
- 불칸(백엔드): **sonnet** — 라우트+테스트 구현(로직/API 구현이라 sonnet 적정, haiku 미사용).
- 팀장(헤르메스, opus): 설계·스펙 확정·인지검증·L1 스모크·통합만(직접 코딩 0).

## 비고
- 상태전이 3중잠금은 함수/DB가 강제, 라우트는 위임만 → 발행 우회 불가 유지.
- 후속(ANU 위임): 독립검증·머지·Phase 3b(검토 UI: 목록/2-pane 교정/보험사 select). 마이그레이션 017 rollout은 회장.

## 세션 통계
- 총 도구 호출: 0회

