> ⚠️ **[작업 취소됨 — CANCELLED]** task-2910은 실행 도중 **2026-08-06 13:16:57에 `manual --cancel`로 취소**되었습니다.
> - 취소 시점에 task 파일 최상단 `★★★ 작업 취소됨 (CANCELLED) ★★★` 마커 + `.cancelled` 이벤트 + `qc_result:"CANCELLED"` `.done` 생성됨.
> - 팀장(헤르메스)이 task 파일을 읽은 시각(13:13)에는 취소 마커가 없었고, 이후 단계별 취소 신호 재확인을 누락해 취소 사실을 모른 채 구현(커밋 13:21, push 13:22)을 진행함.
> - finish-task 실행 시 `.cancelled` 마커를 정상 감지하여 **실완료 .done 생성을 차단**함. **origin/main 미머지(정상)**, 실완료 아님.
> - 아래 내용은 취소 전 이미 완성된 구현 결과의 기록이며, **작업은 취소 상태로 종결**됨. 잔여물: 미머지 격리 브랜치 `task/task-2910-dev1`(원격 존재, 커밋 e90a9ce). 재개/폐기는 회장·ANU 판단.

# task-2910 보고서 — 소식지 AI분석 소비처 review_status='published' 필터

## Situation (상황)
소식지 검토 워크플로우의 목적은 **"사람 승인(review_status='published') 전엔 소식지 내용을 AI 분석에 사용하지 않는다"**(fail-closed, 오추출·숫자오독 노출 방지)이다. 검토 상태기계·감사테이블(Phase1~3)은 이미 구축되어 있으나, **소비 지점**에서 게이트가 실효되지 않는 갭이 남아 있었다.

## Complication (문제)
AI분석 소비처인 `@app.post("/api/insuro/newsletter-chat")` (server/main.py:6152)가:
```python
query = sb.table("newsletters").select("company_name,month_key,title,extracted_text").order("company_name").limit(20)
```
**`review_status`를 전혀 필터하지 않아**, 미승인(pending_review·editing 등) 소식지 추출본도 그대로 AI 컨텍스트에 섞여 분석에 사용됨 → 사람 검토 게이트가 소비 지점에서 무력화.

## Question (질문)
소비(분석·발행) 경로에서만 승인된 소식지(`review_status='published'`)만 읽도록 하되, 관리/검토 UI 조회는 건드리지 않고 회귀 없이 적용할 수 있는가?

## Answer (해결)
**소비 조회 한 곳(6152)에만 `.eq("review_status", "published")` 필터 1줄 추가.** (surgical)

### 소비 지점 전수 확인
`server/main.py` 내 `newsletters` 테이블 접근 전수 조사 결과:
- **6152 newsletter-chat** — AI분석 소비 경로 → **필터 추가 대상 (유일)**
- 5810 review transition — 검토 상태기계(관리) → 불변
- 5706/5788/5895 업로드 파이프라인 → 불변
- 6423 delete-with-drive — 관리(삭제) 경로 → 불변
- premium-chat(6257~) — newsletters 미조회 → 무관

→ 소비/분석 목적으로 extracted_text를 읽는 지점은 **6152 단 하나**. 관리·검토·업로드 조회는 전부 불변.

### 변경 내용
```diff
-        query = sb.table("newsletters").select("company_name,month_key,title,extracted_text").order("company_name").limit(20)
+        query = sb.table("newsletters").select("company_name,month_key,title,extracted_text").eq("review_status", "published").order("company_name").limit(20)
```
- month_key 지정/미지정(gte) 두 분기 모두 이 필터가 select 직후에 걸리므로 항상 적용.
- 기존 소식지: 마이그레이션 017 backfill로 `review_status='published'` → **그대로 조회됨(회귀 없음)**. 신규 업로드만 승인 전까지 분석에서 제외.

## 수정/생성 파일 목록
- `server/main.py` (1줄 수정, line 6152)
- `server/tests/test_newsletter_chat_review_filter.py` (신규, 199줄, 테스트 3종)

허용 범위(`server/main.py`, `server/tests/**`) 내에서만 작업. forbidden_paths(.github/extension/src/migrations/newsletter_review·cross_verify·validation) 무수정.

## 테스트 결과
### 신규 테스트 (server/tests/test_newsletter_chat_review_filter.py) — 3 passed
- `test_eq_review_status_published_called` — month_key 지정 시 `.eq("review_status","published")` 호출 검증
- `test_eq_review_status_called_even_without_month_key` — month_key 미지정(gte 분기)에도 필터 적용 검증
- `test_pending_review_excluded_published_included` — **시맨틱 검증**: published 1건 + pending_review 1건을 넣고 실제 엔드포인트(FastAPI TestClient)를 구동하여, AI system_prompt 컨텍스트에 승인사(승인보험)만 포함되고 미승인사(미승인보험)는 제외됨을 확인

### 전체 회귀 (server/tests/) — 1 failed, 1232 passed
- 실패 1건: `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` — **pre-existing** (base에서 stash 후 동일 단독 실행 시 동일 실패, .env 오염 known issue와 일치). 본 변경으로 인한 **신규 실패 0건**.

## L1 스모크테스트 결과
- **서버 재시작**: 해당없음(로컬 uvicorn 기동은 실 Supabase/JWT/AI 백엔드 의존 → 라이브 데이터 접촉 위험으로 미기동). 대신 **FastAPI TestClient in-process 실동작 검증**으로 대체.
- **API 응답 확인**: `test_pending_review_excluded_published_included`가 실제 `POST /api/insuro/newsletter-chat` 핸들러를 TestClient로 구동 → 200 응답 + 미승인 소식지(미승인보험)가 AI 컨텍스트(system_prompt)에서 **제외**, 승인 소식지(승인보험)만 **포함**됨을 입증. (task의 "미승인 소식지 1건이 newsletter-chat 컨텍스트에 안 들어오는지 확인" 요건을 in-process로 충족)
- **스크린샷**: 해당없음(백엔드 API 필터 작업, UI 무변경)

## 머지 판단
- **머지 필요**: Yes (단, 순서 B — **머지는 ANU가 수행**. merge_policy: none)
- **브랜치**: `task/task-2910-dev1`
- **워크트리 경로**: `/home/jay/workspace/projects/insuro/.worktrees/task-2910-dev1`
- **base**: origin/main `0e601ad`
- **커밋**: `e90a9ce389153585ea568b6daf4b6b50556676e1`
- **push 확인**: 원격 `refs/heads/task/task-2910-dev1` head == 로컬 HEAD `e90a9ce` 일치 확인 완료. (원격 이전 안내 "repository moved → Jeon-Jonghyuk/InsuRo.git" 표시되나 old URL redirect로 push 정상 반영됨)
- **머지 의견**: 순수 additive·surgical(1줄+테스트). 회귀 0(pre-existing 1건 제외). 소비 경로에만 필터 적용, 관리/검토/업로드/상태기계 불변. 마이그레이션 017 backfill 전제로 기존 데이터 회귀 없음. ANU 독립검증·머지·재배포 권장.

## 모델 사용 기록
- 불칸(백엔드): sonnet — 코드 1줄 수정 + 테스트 작성 (일반 코딩/로직)
- 팀장(헤르메스, opus): 설계/소비지점 전수확인/검토/L1 재검증/통합 (직접 코딩 없음)

## 비고
- `/home/jay/workspace/projects/insuro`는 `/home/jay/projects/InsuRo`로의 심볼릭 링크(동일 repo). 두 경로 혼동 아님.
- Pyright import 경고(main/sb_helpers 등 미해결)는 sys.path.insert 런타임 해석 방식 때문에 모든 server 테스트에서 나타나는 pre-existing 사항, 본 작업과 무관.
- 마이그레이션 017 미적용 환경(review_status 컬럼 부재)에 대한 우려는 없음: task 명시대로 017 DEFAULT 'published'로 컬럼 존재 보장.

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


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

