# task-2896 보고서 — 소식지 검토 Workflow Phase 1 (스키마 017 + 상태전이 함수)

- **팀**: 개발1팀 (dev1-team) · **팀장**: 헤르메스
- **레벨**: Lv.3 · **프로젝트**: InsuRo · **날짜**: 2026-08-05
- **브랜치**: `task/task-2896-dev1` · **worktree**: `/home/jay/projects/InsuRo/.worktrees/task-2896-dev1`
- **base**: origin/main `d76a47e` (clean)

## S — Situation (상황)
소식지 PDF/PPT 업로드 → AI 추출(보험사명·제목·본문)이 **사람 검토 없이 곧바로 발행**됨(보험사명 오귀속·숫자 환각·잘림 = 금소법 대외사고 위험). 회장 승인 MVP(미팅 3사이클)로 "발행 전 사람 1회 검토·교정 게이트"를 도입. 본 태스크는 그 **Phase 1 = 백엔드 척추만**(스키마 마이그레이션 + 상태기계). 검토 UI·자동검증 워커는 Phase 2/3(별도).

## C — Complication (문제)
- "저장 = 곧 발행" 구조를 fail-closed(사람 승인 없이 published 불가)로 바꾸되 **기존 발행 흐름 회귀 0** 필요.
- 상태변경과 감사이벤트가 **같은 트랜잭션**이어야(이벤트 없는 상태변경 원천 차단).
- 소식지 실제 저장 테이블·PK 타입을 먼저 특정해야(FK 설계).

## Q — Question (질문)
기존 소식지 CRUD/발행을 깨지 않으면서, AI가 절대 published에 도달할 수 없는(오직 사람+승인+검토) 상태기계와 append-only 감사 토대를 어떻게 안전하게 세우는가?

## A — Answer (구현 요약)

### 수정 파일별 검증 상태
| 파일 | 변경 내용 | grep 검증 | 상태 |
|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-2896-dev1/server/migrations/017_newsletter_review_workflow.sql | 마이그레이션 017 (컬럼+감사테이블+원자함수+backfill, idempotent) | grep "transition_newsletter_review" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2896-dev1/server/newsletter_review.py | 상태전이 중앙함수 fail-closed | grep "transition_review_status" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2896-dev1/server/tests/test_newsletter_review.py | 189 케이스 테스트 스위트 | grep "ALLOWED_TRANSITIONS" OK | verified |


### 선행 조사 (실측 확정)
- **소식지 저장 테이블 = `newsletters`** (전용 테이블, premium_data와 별개).
- **`newsletters.id` = UUID** (`uuid PRIMARY KEY DEFAULT gen_random_uuid()`) → `review_events.doc_id`도 uuid FK.
- 기존 `newsletters.status`('pending'/'analyzing'/'completed'/'failed')는 그대로 두고, **`review_status`는 완전 별개 신규 컬럼**.
- 업로드 흐름: `POST /api/insuro/upload-to-drive`·`parse-premium-file` (server/main.py). status='completed'가 현재 곧 발행.

### 생성 파일 (전부 신규 — 기존 파일 수정 0)
1. `server/migrations/017_newsletter_review_workflow.sql` (256줄)
   - (A) newsletters 신규 컬럼 16종 **전부 NULLABLE+DEFAULT**: review_status(DEFAULT 'published' — 기존행 자동 보호), source_file_ref jsonb(+sha256), extract_mode, model_version, insurer/title/body 각 _extracted/_edited, validation_flags jsonb DEFAULT '[]', field_confidence jsonb DEFAULT '{}', reviewer_id uuid, reviewed_at/approved_at/published_at.
   - (B) `newsletter_review_events` append-only 감사 테이블: UPDATE 차단 트리거 + RLS default-deny(정책0+authenticated/anon REVOKE, service_role 서버경로만). DELETE 차단 트리거는 **의도적 미생성**(ON DELETE CASCADE와 충돌 회피 — 부모 삭제 인질화 방지, 무결성은 UPDATE차단+RLS로).
   - (C) 원자 함수 `transition_newsletter_review(...)` plpgsql: FOR UPDATE 락+낙관동시성(STATUS_MISMATCH), 전이표 CASE 16개, published fail-closed 재검증, UPDATE newsletters + INSERT events **단일 트랜잭션**.
   - (D) backfill(멱등): extract_mode='legacy'·*_edited=현재값 COALESCE, genesis 이벤트 doc당 1건 WHERE NOT EXISTS.
   - idempotent: ADD COLUMN IF NOT EXISTS / CREATE TABLE·INDEX IF NOT EXISTS / CREATE OR REPLACE FUNCTION / DROP TRIGGER·POLICY IF EXISTS / COALESCE·WHERE NOT EXISTS.
   - ⚠️ **적용 = 회장 rollout**(Supabase SQL editor). supabase/migrations/ 미러도 rollout 단계.
2. `server/newsletter_review.py` (223줄)
   - `transition_review_status(doc_id, from_status, to_status, actor, actor_type, action, presented, diff, reviewer_id, sb)` **단일 중앙 함수**.
   - `ALLOWED_TRANSITIONS` 16개 상수(SQL CASE와 100% 일치), `REVIEW_STATUSES` 10개, 예외 계층(.code).
   - **published fail-closed 3중 잠금**: from='approved' AND actor_type='human' AND reviewer_id 존재. AI/워커 절대 불가.
   - editing→approved 직행 금지(전이표 부재). Python 검증 실패 시 `sb.rpc` **미호출**(상태변경0·이벤트0). 통과 시 RPC 정확히 1회(원자성 위임).
3. `server/tests/test_newsletter_review.py` (565줄, **189 케이스**)

### 상태전이표 (SQL ↔ Python 100% 일치)
```
uploaded→extracting[ai,system]  extracting→{extracted,extract_failed}[ai,system]
extract_failed→extracting[ai,system,human]  extracted→auto_validated[ai,system]
auto_validated→pending_review[ai,system]  pending_review→{editing,approved,rejected,extracting}[human]
editing→{pending_review,rejected,extracting}[human]  (editing→approved 직행 없음)
approved→{published,pending_review}[human]  rejected→extracting[human,system]
published = 종결(Phase 1 전이 없음)
```

## 검증 결과

### 테스트 (자동)
- **신규 `test_newsletter_review.py`: 189 passed, 0 failed** (0.38s).
- **전체 server 스위트: 1097 passed, 1 failed** — 유일 실패는 `test_cors_fail_closed_when_ext_origin_unset`(CORS, task 무관).

### ★ 회귀 판정 (독립 검증 — pre-existing 확정)
- diff는 **순수 additive 3파일**(전부 `A`, 기존파일 `M` 0건) → 회귀 구조적 불가.
- CORS 실패 원인 특정: `main.py:54 load_dotenv(parents[1]/.env)`가 **worktree 루트 .env**(worktree_manager가 복사한 프로덕션 `INSURO_EXTENSION_ORIGIN`)를 로드 → 테스트의 `env.pop(INSURO_EXTENSION_ORIGIN)` 무력화 → chrome-extension origin 주입 → assert 실패.
- 결정적 증명: (1) **clean /tmp base(origin/main) worktree에서 해당 테스트 단독 PASS**, (2) 내 worktree에서 루트 `.env`를 숨기면 **PASS**, 되돌리면 FAIL. → **worktree .env 오염 아티팩트, task-2896 코드 무관**(CI clean 환경에서 통과). `.env` 원복 완료.

## L1 스모크테스트
- 서버 재시작: **해당없음**(순수 백엔드 모듈+SQL, 마이그레이션 적용은 회장 rollout·Supabase 필요).
- API 응답 확인: **해당없음**(Phase 1은 라우트 미배선 — 척추만. 상태기계 실호출로 대체).
- 스크린샷: **해당없음**(백엔드).
- 테스트 증거: `189 passed`(test_newsletter_review.py), 전체 스위트 `1097 passed`.
- 실호출 결과(`python3` 직접 실행, MagicMock sb — pytest 외 실동작 확인):
  - AI approved→published → **차단**(HUMAN_REQUIRED), `rpc_called=False` ✓
  - human + reviewer_id=None → **차단**(PUBLISH_FAIL_CLOSED), `rpc_called=False` ✓
  - human + reviewer_id 있음 → **통과**, `rpc_call_count=1`, fn=`transition_newsletter_review` ✓
  - editing→approved 직행 → **차단**(FORBIDDEN_TRANSITION), `rpc_called=False` ✓
  - AI uploaded→extracting → **통과**, `rpc_call_count=1` ✓

## 발견 이슈 및 해결
- **CORS 테스트 1건 실패**: 위 회귀 판정대로 worktree .env 오염 아티팩트로 확정(독립 3중 검증). task 코드 무관·CI clean 통과 → 해결 불요(허위 회귀 배제 완료).
- 미해결 이슈: 없음.

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: `task/task-2896-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2896-dev1`
- **머지 의견**: 순수 additive(신규 3파일, 기존파일 무변경) → 충돌 위험 최소. Critical 게이트(published fail-closed, AI 불가) Python+DB 이중 방어 + 189테스트 전수 통과. 회귀 0(유일 CORS 실패는 pre-existing .env 아티팩트로 독립 확정). merge_policy=none이므로 **머지=코드까지**, 마이그레이션 적용(017)은 회장 rollout(Supabase SQL editor) 별도. Phase 2/3(자동검증 워커·검토 UI)는 머지 후 별도 위임.

## 모델 사용 기록
- 불칸(백엔드) T1+T2 구현: **sonnet** (일반 코딩/로직).
- 아르고스(테스터) 테스트 스위트: **sonnet** (검증 로직).
- 헤르메스(팀장, Opus): 조사·설계·계약고정·통합검증·회귀 독립판정·L1 스모크. 직접 코딩 없음.
- haiku 미사용.

## 커밋
- `abb6818` 불칸: migration 017 + newsletter_review.py
- `db87027` 아르고스: test_newsletter_review.py

## 후속 (ANU/회장)
- **회장 rollout**: `server/migrations/017_newsletter_review_workflow.sql` Supabase SQL editor 적용 + supabase/migrations/ 미러.
- **Phase 2**: 자동검증 워커(ai_queue 결정론 9규칙, 플래그만) + grounding 저장 + AI 업로드 경로에 review_status='uploaded' 명시 배선.
- **Phase 3**: 관리자 검토 UI(2-pane 원문↔추출, 보험사명 마스터 select 필수, 고위험 필드 확인 게이트).

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

