# task-2827 완료 보고 — Integration Phase1 PR-C (CRM 후보검색 + 명시적 고객생성)

- 팀: dev1-team (헤르메스 팀장 / 불칸 백엔드 / 아르고스 테스터)
- 프로젝트: InsuRo
- 레벨: Lv.4 (게이트 G1/G2/G3)
- worktree: `/home/jay/projects/InsuRo/.worktrees/task-2827-dev1` (base=origin/main a23f867, base_fallback=false)
- 브랜치: `task/task-2827-dev1`

## SCQA

**S**: InsuRo 웹앱이 "인슈로로 보내기" 후 전면 FA 확인 매칭을 하려면 서버에 (1) FA 본인 고객 후보검색, (2) 명시적 고객생성 2개 엔드포인트가 필요하다. base(origin/main a23f867)에는 PR-A(계약V2·customers.region)·PR-B(allowlist)가 이미 반영돼 있다.

**C**: 기존 `customer_match.upsert_customer_for_capture`는 ohmy-capture 시 조용한 자동생성만 지원하고, FA가 명시적으로 후보를 확인·생성하는 경로가 없었다. 또한 타 FA 고객 유출 위험(테넌트 경계)과 hash 계산 불일치로 인한 중복/오매칭 위험이 있었다.

**Q**: 타 FA 유출 0 + hash 재사용(중복 구현 0)을 지키면서 후보검색·명시적 생성 엔드포인트를 기존 회귀 없이 추가할 수 있는가?

**A**: 신규 라우터 1개(330줄) + main.py include 2줄로 구현. `_require_fa_account_id`(서버파생 스코프)·`compute_customer_key_hash`/`encrypt_pii`/`normalize_dob`(pii_crypto 재사용)를 그대로 재사용해 중복 구현 0. 검증 테스트 20건 전부 PASS, 전체 회귀 **789→809 passed (기존 감소 0)**, py_compile OK, 실 서버 L1 스모크 통과.

## 생성/수정 파일 (지정 경로 외 변경 0)
```
server/main.py                             |   2 +   (import 1 + include_router 1)
server/routes/crm_customer_match_v1.py     | 330 +   (신규)
server/tests/test_crm_customer_match_v1.py | 494 +   (신규, 테스트)
3 files changed, 826 insertions(+)
```
`git diff --stat a23f867 HEAD`로 검증 — 라우터/테스트/main.py include 2줄 외 변경 0.

## 구현 요약
### 엔드포인트1 — 후보검색 `POST /api/insuro/crm/customer-candidates/search`
- 입력 `{name, dob, gender}` (누락/공백 → 400)
- near-match = **trim + 연속공백 collapse + ILIKE**(공백차이·대소문자만 흡수), 생년월일·성별은 정확 일치. trigram/similarity 신규 인프라 0.
- 쿼리: `.select("id,name,birth_date,gender,phone,region").eq("agent_id", fa_account_id).ilike("name", norm).eq("birth_date", ...).eq("gender", ...)` — **region 실제 SELECT·반환**(PR-A 컬럼 소비, Codex #6)
- 반환 5항목: 이름(mask_name)·생년월일(mask_dob)·성별·**핸드폰(부분 마스킹, 뒷4자리 확인 가능)**·**region**. 0건도 200.

### 엔드포인트2 — 명시적 생성 `POST /api/insuro/crm/customers`
- 입력 `{name, dob, gender, phone(필수), region(선택)}` — phone 비면 400.
- `compute_customer_key_hash(name,dob,gender)` **재사용**(동일 PII_HASH_SALT+normalize_dob) → `(agent_id, customer_key_hash)` 중복 선확인 → 존재 시 **409 + existing_id**(insert 미호출, 조용한 덮어쓰기 0). INSERT 중 23505 충돌도 SELECT 폴백 후 409.
- 신규: dual-write(name/birth_date 평문 + encrypted_name/encrypted_dob/customer_key_hash), phone/region 저장, tags=["manual_fa_confirmed"] → **201 {id, status:"created"}**.
- 기존 `upsert_customer_for_capture` auto-create 경로는 미변경.

## 게이트
- **G1 설계**: affected_files = server/routes, main.py, server/tests만. 타 팀·consultation_history_v1/migration/allowlist/extension 미변경 확인.
- **G2 구현**: 아르고스 검증 테스트 20건 PASS + 전체 회귀 809 passed. 팀장 독립 재확인(pytest 직접 실행).
- **G3 머지**: PR #116 생성(gh pr create, 머지 안 함). **Gemini Code Assist 소비자판 sunset 확인** — `/gemini review` 트리거에 "The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased." 응답. 실 리뷰 산출 불가 → 리뷰 없이 진행(task 머지 금지라 auto-merge 위험 0). **머지 금지**(task 지시) — PR open 유지, ANU 독립검증 대상.

## 테스트 결과
- `pytest tests/test_crm_customer_match_v1.py -q` → **20 passed**
- 전체 회귀 `pytest tests/ -q` → **809 passed** (baseline 789 + 신규 20, 기존 감소 0) — 팀장 직접 재확인
- 커버: tenant경계(.eq agent_id spy), near-match(공백 collapse spy), phone 필수 400, 생성 201+insert 필드 전수, region round-trip, 중복 409+insert 미호출, hash 일관성(6/8자리 dob 동일 hash)
- 프로덕션 코드 버그: 0

## L1 스모크테스트 결과
- **서버 재시작**: 성공 (`uvicorn main:app --port 8011`, 실 프로세스 기동, startup complete)
- **API 응답 확인** (curl, 실 프로세스):
  - `POST /api/insuro/crm/customer-candidates/search` 미인증 → **HTTP 401** (auth 가드 발화, 404 아님 = 라우트 마운트 증명)
  - `POST /api/insuro/crm/customers` 미인증 → **HTTP 401**
  - 없는 경로 `.../crm/does-not-exist` → **404** (대조군)
  - `GET .../customer-candidates/search` → **405** (경로 존재·POST 전용)
  - `GET /openapi.json` → 두 경로 모두 POST로 등록 확인
  - → main.py include_router가 실 프로세스에 반영됨을 확인. (200 full-path는 실 인카 JWT + 라이브 supabase 필요로 이 샌드박스 미가능 — auth 가드가 DB 접근 전 401로 차단하는 설계라 401 확인이 라우트·가드의 유효한 행동 검증.)
- **스크린샷**: 해당없음(백엔드 API)
- Playwright 브라우저: 미사용(백엔드), 스모크 서버 정리 완료(port 8011 free)

## 발견 이슈 및 해결
- **near-match 한계(정직 기록)**: 설계상 ILIKE는 공백차이·대소문자만 흡수하고 "공백 vs 무공백"·진짜 오타(글자 상이)는 흡수하지 않음. 설계가 trigram/유사도 금지(타 고객 오노출 방지)이므로 **의도된 한계**. 테스트는 흡수 가능한 케이스(앞뒤 공백·연속공백)만 검증, docstring에 한계 명시.
- **env 이름 정정**: 암호화 키 실제 env는 `PII_ENCRYPTION_KEY`(지시서 PII_ENC_KEY 아님) — 테스터가 pii_crypto.py 직접 확인해 정정.
- **pyright import 경고**: `pii_crypto`/`main` 미해결 경고는 sys.path 설정상 false-positive(기존 main.py의 composite_calculator/customer_match/pii_crypto import에도 동일 발생). py_compile·런타임 정상.
- **로컬 main 오염 주의(범위 밖)**: 로컬 main이 origin/main보다 stale(cc7476b)하고 dirty(26 staged). worktree는 origin/main 기준 생성(base_fallback=false)이라 무관. 미변경.

## 모델 사용 기록
- 불칸(백엔드 구현): sonnet
- 아르고스(테스터): sonnet
- haiku 미사용(로직 구현·검증이라 sonnet 필수)

## 머지 판단
- **머지 필요**: No (task 명시 "머지 금지" — PR open 유지, ANU 독립검증 후 회장/ANU 결정)
- **브랜치**: task/task-2827-dev1
- **워크트리 경로**: /home/jay/projects/InsuRo/.worktrees/task-2827-dev1
- **머지 의견**: 전체 회귀 809 passed·지정 경로 외 변경 0·타 FA 유출 0·hash 재사용. 코드 품질/충돌 위험 낮음. 단 규제 hard-line(모델A vs C) 등 상위 미결이 있어 **머지는 ANU/회장 판단 대기**.

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

