# task-2860 보고서 — 상담이력 삭제 기능 (웹앱 삭제 버튼 + 서버 DELETE 실삭제)

- 팀: dev1-team (헤르메스/불칸/이리스)
- 레벨: Lv.3 · merge_policy: none (ANU 검증·머지·배포)
- 브랜치: `task/task-2860-dev1` (origin push 완료)
- 프로젝트: InsuRo · worktree: `/home/jay/projects/InsuRo/.worktrees/task-2860-dev1`
- 작성: 2026-07-26

## Situation
회장 결정: "안 보이는 레코드가 Supabase에 쌓이면 낭비. 저장했으면 웹앱에서 보이고, 사용자가 히스토리를 삭제할 수 있게 + Supabase에서 실제로 삭제되게 하면 DB 낭비 없다." → 상담이력 실삭제(hard delete) 기능 신규.

## Complication
- 삭제는 **per-FA 소유 검증** 필수(타 FA 레코드 삭제 불가 = IDOR 차단).
- 계약 5-enum 불변(신규 에러코드 신설 금지). RLS 우회 실삭제(soft-delete 아님).
- consultation ingest/GET/조합 로직 회귀 0, 확장 불변, 스키마/마이그레이션 변경 없음.

## Question
게이트(flag+allowlist+service-role)를 유지하면서, per-FA 소유 레코드만 Supabase 원본 테이블에서 실제로 제거하고, 웹앱에서 확인 다이얼로그 → 목록 즉시 반영 → 토스트로 완결되게 하려면?

## Answer (구현)

### 서버 — DELETE 엔드포인트 (불칸)
파일: `server/routes/consultation_history_v1.py` (+63줄), `server/tests/test_consultation_history_v1_delete.py` (신규 255줄)
- `@router.delete(DETAIL_PATH)` (`/api/insuro/consultation-history/v1/{record_id}`) 신규.
- 게이트: GET detail 과 **동일 Depends 순서** — `_require_feature_enabled` → `_require_service_role_backend` → `_require_pilot_allowlisted`(회장 계정 allowlist만, fa_account_id 서버파생 반환).
- **per-FA hard delete**: 뷰가 아닌 원본 `TABLE_NAME` 에 `.delete().eq("id", record_id).eq("fa_account_id", fa_account_id).execute()` (service-role RLS 우회). soft-delete 아님 — 행 실제 제거.
- 404 조건(전부 기존 `_generic_error(404, "CONSULTATION_HISTORY_NOT_FOUND")` 재사용, 신규 enum 0): ① malformed UUID(DB 접근 전 fail-fast) ② 존재하지 않음 ③ 타 FA 소유(0건 삭제 → 404, IDOR 차단, 존재/소유 분기 미노출).
- 성공: PII 없는 감사 로그 `logger.info("...deleted id=%s fa=%s", ...)` 후 `{"ok": True, "deleted_id": record_id}` 반환.
- DB 예외 → `_generic_error(500, "CONSULTATION_HISTORY_DELETE_FAILED")`. 계약 5-enum(FEATURE_DISABLED/IDEMPOTENCY_CONFLICT/TENANT_REFERENCE_MISMATCH/CLIENT_AUTHORITY_FIELD_FORBIDDEN/REFERENCE_SHAPE_INVALID) 불변.

### 웹앱 — 삭제 UI (이리스)
파일: `src/components/composite/ConsultationHistoryView.tsx`, `src/pages/CompositeDesign.tsx`, `src/components/composite/__tests__/ConsultationHistoryView.test.tsx`
- `ConsultationRecordCard` 우상단(날짜 옆)에 휴지통 버튼(`Trash2`, `aria-label="상담이력 삭제"`, `data-testid="consultation-delete-btn"`) — `onDelete` prop 정의 시에만 렌더(기존 테스트 무영향).
- 클릭 → `window.confirm("이 상담이력을 삭제할까요? 되돌릴 수 없습니다")` → 확인 시 `onDelete(record.id)`.
- `CompositeDesign.tsx` `handleDeleteConsultation`: 기존 `getAuthHeader` 로 `DELETE .../consultation-history/v1/{id}` 호출 → 성공 시 `setConsultationRecords` 로 목록 즉시 제거 + `toast({title:"삭제되었습니다"})`, 실패/예외 시 destructive 토스트. `<ConsultationHistoryView onDelete={handleDeleteConsultation} />` 배선.

## 생성/수정 파일
- `server/routes/consultation_history_v1.py` (수정, DELETE 엔드포인트 +63줄)
- `server/tests/test_consultation_history_v1_delete.py` (신규, 255줄, 7 케이스)
- `src/components/composite/ConsultationHistoryView.tsx` (수정, onDelete/Trash2/버튼)
- `src/pages/CompositeDesign.tsx` (수정, handleDeleteConsultation + 배선)
- `src/components/composite/__tests__/ConsultationHistoryView.test.tsx` (수정, +3 케이스)

## 테스트 결과
- **pytest** (`-k consultation`): **125 passed, 1 failed**. 유일 실패 `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` = **pre-existing 환경 아티팩트**(우리 브랜치는 이 테스트/`main.py` 미수정 — `git diff 0dae390 HEAD` 빈 출력. worktree 복사 `.env` 에 `INSURO_EXTENSION_ORIGIN` 설정됨 → `_probe_cors(None)` 가 ambient env 흡수). DELETE 신규 7건 전부 PASS. **회귀 0.**
- **npx tsc --noEmit**: exit 0, 에러 0건 클린 (goal_assertion `npx tsc` 충족).
- **npx vitest** (ConsultationHistoryView): **15 passed** (기존 12 + 신규 3). act() 경고는 기존 테스트에도 있던 것, 회귀 아님.

## L1 스모크테스트 결과
- 서버 재시작: 실 FastAPI `app` 을 TestClient 로 in-process 기동해 실 HTTP 요청 검증 수행(ASGI 전 경로 통과). API 응답 확인 결과:
  - `L1-0` DELETE route registered: **True** (라우트 등록 확인)
  - `L1-1` flag OFF → **403 FEATURE_DISABLED** (게이트 배선 확인)
  - `L1-2` happy path(mock sb 1행 삭제) → **200 OK**, body `{"ok": true, "deleted_id": "..."}`
  - `L1-3` 0행 삭제 → **404 CONSULTATION_HISTORY_NOT_FOUND** (per-FA / not-found)
  - 결과: **4/4 PASS** (실 app 요청/응답 통과).
- 프론트 L1: vitest 실 컴포넌트 렌더 + 실 클릭 시뮬레이션 **15 passed** (삭제버튼 렌더 / confirm→onDelete(id) 호출 / 취소 시 미호출 / onDelete 미전달 시 버튼 없음). 라이브 브라우저 E2E(실 Supabase 대상)는 flag+allowlist+service-role+라이브 DB 필요 → **ANU 배포시 검증 소관**(merge_policy=none).
- pytest(서버 DELETE 7 케이스 포함): **125 passed** (회귀 0).

## 발견 이슈 및 해결
- **worktree 동시 커밋 경합**: 불칸/이리스가 같은 worktree에서 병렬 작업 → `git add -A` 인덱스 경합으로 백엔드 WIP가 이리스 커밋에 잠시 혼입. 이리스가 `git reset --soft HEAD~1` 로 프론트 3파일만 재커밋(`8ec63ed`), 백엔드 파일은 uncommitted 복원. 팀장이 백엔드 2파일을 별도 커밋(`201d520`)으로 정정. 최종 이력 클린(2 커밋, 파일 겹침 0), working tree clean 확인.

## 머지 판단
- **머지 필요**: Yes (단, merge_policy=none — 팀장 머지 금지)
- **브랜치**: `task/task-2860-dev1` (origin push 완료)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2860-dev1`
- **커밋**: `201d520`(불칸 백엔드) + `8ec63ed`(이리스 프론트) · base=`0dae390`(현 origin/main)
- **머지 의견**: tsc 클린 + vitest 15/15 + pytest 회귀 0 + L1 실 app 요청 통과. 변경은 allowed_resources 내부만(라우트 1개 추가·프론트 3파일·테스트), 확장/마이그레이션/스키마 불변. **ANU 독립 검증·Codex 검토·머지·CF Pages 배포 소관.**

## 모델 사용 기록
- 불칸(백엔드): sonnet — 라우트/pytest 구현 (로직 구현, haiku 부적합)
- 이리스(프론트): sonnet — 컴포넌트/배선/vitest 구현
- 헤르메스(팀장): 설계·분배·검토·통합·검증(직접 코딩 없음, 커밋 정정만)

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

