# task-2840 보고서 — PR-E1 consultation-history/v1 LIST 옵션 reference_id 필터

- 작업 ID: task-2840 (Lv.3, PR-E1)
- 팀: dev1-team (헤르메스 팀장)
- 프로젝트: InsuRo (`/home/jay/projects/InsuRo`)
- 브랜치: `task/task-2840-dev1` / worktree: `/home/jay/projects/InsuRo/.worktrees/task-2840-dev1`
- base: origin/main `6f26ccc` (base_fallback=false, worktree-base marker 기록됨)
- merge_policy: **none — PR OPEN 유지, 자동머지 금지**
- PR: https://github.com/Jeon-Jonghyuk/InsuRo/pull/121 (OPEN, 머지 금지)
- push: `6f26ccc..afc09e4` → `origin/task/task-2840-dev1`

## S — Situation
웹앱(후속 PR-E2)이 **고객별 상담이력**을 조회하려면 서버 LIST 가 특정 고객(`reference_id`)으로 필터링할 수 있어야 한다. 현 `GET /api/insuro/consultation-history/v1` LIST 는 per-FA·ok-only 뷰·`created_at desc`·limit 100·projection 화이트리스트만 있고 **필터 파라미터가 없어** 고객별 조회가 불가했다.

## C — Complication
Codex 조율 결론(B): 클라이언트 측 필터는 (1) limit 100 cap 으로 오래된 레코드 누락, (2) 전 고객 데이터를 브라우저로 노출 → **서버 `reference_id` 필터가 정답**. 단, per-FA 스코프 완화·ok-only 뷰 우회·malformed 값의 무필터 전체 반환은 모두 치명적 데이터 노출이므로 절대 금지.

## Q — Question
per-FA 격리·ok-only·feature/allowlist 게이트를 100% 유지하면서, LIST 에 옵션 `reference_id` 필터만 surgical 하게 얹고 malformed 를 fail-closed 로 처리할 수 있는가?

## A — Answer
`server/routes/consultation_history_v1.py` LIST 핸들러 한 곳만 수정. 옵션 쿼리 파라미터 `reference_id` 추가 + 핸들러 본문 내 UUID 검증(fail-closed) + 조건부 `.eq("reference_id", ...)`. 기존 스코프/뷰/정렬/limit/projection/게이트 전부 불변. 회귀 0.

## 변경 내용 (수정 파일 2개, 모두 allowed_resources 범위 내)
1. `server/routes/consultation_history_v1.py` (+19 / -5)
   - fastapi import 에 `Query` 추가.
   - `list_consultation_history_v1` 시그니처에 `reference_id: str | None = Query(None)` 추가 — Depends 게이트(`_require_feature_enabled` → `_require_pilot_allowlisted`) **뒤**에 위치.
   - **타입을 `str | None` 로 유지**(의도적): `UUID | None` 로 하면 FastAPI 가 파라미터 검증을 Depends 게이트보다 먼저 수행해 feature OFF 상태에서 malformed 가 403 대신 422 를 반환하는 정보 노출 회귀가 생김. 그래서 UUID 검증은 **핸들러 본문 안**에서 수행.
   - `reference_id` 가 주어지면 `UUID(reference_id)` 파싱 시도 → 실패 시 **`{"records": [], "count": 0}` fail-closed 반환**(무필터 전체 반환 금지, DB 접근 0).
   - 유효 시 쿼리 빌더에 `.eq("fa_account_id", fa)` **다음** `.eq("reference_id", reference_id)` 를 얹고 `.order("created_at", desc=True).limit(DEFAULT_LIST_LIMIT).execute()`. per-FA scope 항상 우선.
   - `reference_id` 미지정 시 기존 동작 100% 동일.
2. `server/tests/test_consultation_history_v1.py` (+284) — 검증 테스트 9종 추가(아래).

**미변경(절대 금지 항목 준수)**: DETAIL 핸들러, POST ingest, 마이그레이션, 뷰 정의, `_ALLOWED_RESPONSE_FIELDS` 화이트리스트, feature flag/pilot allowlist Depends 순서·내용, per-FA scope. forbidden_paths(src/·extension/·.github/·server/migrations/·supabase/) 변경 0 (확인: NO_FORBIDDEN_PATHS).

## 테스트 결과
`server/tests/test_consultation_history_v1.py` 에 LIST 전용 fluent-query mock(`_ListQueryMock`)·fixture(`ch_list_env`, OK_ONLY_VIEW 만 허용) 신설 후 9 케이스 추가:
- (a) 유효 UUID → fa_account_id + reference_id 두 eq 모두 적용, records/count 정합
- (b) per-FA 격리: reference_id 유무와 무관하게 `.eq("fa_account_id", 서버파생fa)` 항상 적용
- (c) ok-only: `sb.table` 이 `consultation_history_v1_ok_only` 뷰로만 호출(원본 테이블 접근 0)
- (d) malformed reference_id → `{"records":[],"count":0}` + `sb.table.call_count == 0`(fail-closed 실측)
- (e) reference_id 미지정 회귀 0: eq 미적용 / order desc / limit 100 유지
- (f) feature OFF → 403 (parametrize 3종: 없음/유효 UUID/malformed)

**pytest(팀장 독립 재실행)**: `python3 -m pytest tests/test_consultation_history_v1.py -q` → **65 passed, 4 warnings** (신규 9 케이스 PASS + 기존 56 회귀 0). goal_assertion `pytest server/tests/ -k consultation` 충족.

## L1 스모크테스트 결과 (필수 기록)
- 서버 재시작: **성공** — 실제 FastAPI `app`(main.py) in-process 마운트 + 실 HTTP 스택(TestClient)으로 LIST 엔드포인트 라우팅 실행. (별도 uvicorn 프로세스 대신 동일 앱 객체를 실 HTTP 스택으로 구동 — Depends/라우팅/핸들러 전 경로 실동작.)
- API 응답 확인: **feature OFF 상태(flag 안 켬)에서 `GET /api/insuro/consultation-history/v1`** 3 케이스 실호출:
  - `` (무필터) → **403** `{"error_code":"FEATURE_DISABLED"}`
  - `?reference_id=550e8400-...` (유효 UUID) → **403** `FEATURE_DISABLED`
  - `?reference_id=not-a-uuid` (malformed) → **403** `FEATURE_DISABLED`
  → 게이트 우선순위(Depends 가 파라미터 파싱보다 먼저) 실동작에서 보증. feature 미개방 유지.
  - feature ON + supabase mock 경로의 필터/빈결과/스코프 실동작은 pytest(실 HTTP TestClient) 65 passed 로 실측(malformed→sb.table 미호출, 유효UUID→2 eq, ok-only 뷰).
- 스크린샷: 해당없음(백엔드 API — curl 등가 HTTP 응답으로 대체).

## 발견 이슈 및 해결
- 기존 `_make_sb`/`ch_env` mock 은 POST 전용(consultation_history_v1·customers 테이블만 허용, OK_ONLY_VIEW 접근 시 AssertionError) → LIST 테스트에 재사용 불가. **해결**: LIST 전용 fluent mock/fixture 신설(테스트 파일 내). route 코드는 미수정.
- pyright import 경고(`main`, `schemas.*`, `routes.*` 미해결)는 `server/` 를 rootdir 로 실행하는 기존 테스트 공통 패턴으로 **신규 회귀 아님**(기존 테스트도 동일). pytest 는 정상 실행.
- 구현 버그 의심: **없음**(테스터·팀장 실측 일치).

## 머지 판단
- **머지 필요: No (merge_policy=none — PR OPEN 유지, 자동머지 금지)**
- 브랜치: `task/task-2840-dev1`
- 워크트리 경로: `/home/jay/projects/InsuRo/.worktrees/task-2840-dev1`
- 머지 의견: 변경 범위 surgical(route 1곳 + tests), per-FA/ok-only/게이트 불변, 회귀 0, forbidden path 0. 품질상 머지 가능하나 **task 지시(merge_policy none)에 따라 PR 은 OPEN 유지**. 최종 머지 판정은 ANU 독립검증 + 회장 승인 대기.

## 모델 사용 기록
- 불칸(백엔드, route 구현): sonnet
- 아르고스(테스터, 테스트 작성/실행): sonnet
- 헤르메스(팀장): 설계/분배/검토/통합/L1 스모크 (직접 코딩 없음)
- haiku 미사용.

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

