# task-2354 Phase 0 사전 조사 보고서: CRM 매핑 + PII 암호화 설계

**작성자**: 스바로그 (dev6, 페룬 위임)
**작성일**: 2026-05-02
**대상**: 회장 승인 게이트 (Phase 1 진행 전 필수)
**상태**: 초안 — 회장 검토 대기

---

## 1. 요약 (SCQA)

**Situation**: InsuRo가 ohmymanager 매트릭스 캡처 + history + CRM 자동 연동 기능(task-2354, Phase 0~4)을 구현하려 한다.

**Complication**: 기존 customers 테이블에 이름/생년월일이 평문 저장되어 있고, 컴포지트 유니크 제약이 없으며, 현재 Extension background.js가 자동 push(ingest 경로)로 설계되어 있어 신 설계의 "명시적 클릭만" 원칙과 충돌한다.

**Question**: Phase 1 진행 전에 (1) 기존 CRM 스키마와 신 설계의 매핑을 확정하고, (2) PII 암호화 방식을 결정하고, (3) 자동 push 경로 분리 전략을 확정해야 한다.

**Answer**: pgcrypto 기반 애플리케이션 레벨 암호화 채택, customer_key_hash 유니크 제약 추가 마이그레이션 필요, background.js OHMY_CAPTURED → 신 플로팅 버튼 경로로 분리 필요. 기존 customers 컬럼은 대부분 활용 가능하나 encrypted_name/encrypted_dob 신규 컬럼 추가 필요.

---

## 2. 기존 CRM 스키마

### 2-1. customers 테이블 컬럼 및 제약

원본: `/home/jay/projects/InsuRo/supabase/migrations/20260308182257_ac547eb8-a607-4c4a-8d7d-e724a8a516f5.sql`

컬럼 목록:
- `id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY`
- `agent_id UUID NOT NULL` — FA 사용자 ID
- `name TEXT NOT NULL` — **평문 저장. PII 위험.**
- `phone TEXT` — 평문
- `email TEXT` — 평문
- `birth_date DATE` — **평문 저장. PII 위험.**
- `gender TEXT` — 평문 ('M'/'F' 등, CHECK 제약 없음)
- `occupation TEXT`
- `address TEXT`
- `family_info JSONB DEFAULT '{}'`
- `income_range TEXT`
- `total_premium INTEGER DEFAULT 0`
- `interest_areas TEXT[] DEFAULT '{}'`
- `stage customer_stage NOT NULL DEFAULT 'lead'` — ENUM: lead/initial_consultation/needs_analysis/proposal_sent/under_review/contracted/maintenance
- `tags TEXT[] DEFAULT '{}'`
- `ai_summary TEXT DEFAULT ''`
- `privacy_consent_at TIMESTAMPTZ`
- `next_contact_date DATE`
- `created_at TIMESTAMPTZ NOT NULL DEFAULT now()`
- `updated_at TIMESTAMPTZ NOT NULL DEFAULT now()`

제약 확인:
- PRIMARY KEY: id
- FK: agent_id → auth.users(id) ON DELETE CASCADE (20260309160000 마이그레이션에서 추가됨)
- RLS: ENABLE ROW LEVEL SECURITY, 4개 CRUD 정책 (agent_id = auth.uid() 기반)
- **컴포지트 유니크 제약 없음** (이름+생년월일+성별 조합 유니크 미존재)
- **gender 컬럼에 CHECK 제약 없음** (자유 텍스트)
- 인덱스: idx_customers_agent_id (20260309160000에서 추가)

### 2-2. 후속 마이그레이션 변경 이력

- `20260309160000_db-safety-improvements.sql`: FK fk_customers_agent_id 추가 (agent_id → auth.users ON DELETE CASCADE), idx_customers_agent_id 인덱스 추가
- `20260427190000_policy_analyses_customer_id.sql`: policy_analyses 테이블에 customer_id UUID REFERENCES customers(id) FK 추가 (CRM 연동 선례)
- 그 외 customers 테이블 구조 직접 변경 마이그레이션 없음 (컬럼 추가/변경 이력 없음)

### 2-3. 관련 테이블 목록 (customer_* 및 CRM 연관)

모두 customers.id FK 기반:

- `customer_insurance` — 고객 보험 가입 이력 (company_name, product_name, monthly_premium, start_date, end_date)
- `customer_notes` — 상담 메모 (agent_id, content, note_type)
- `customer_ai_summaries` — AI 요약 이력 (summary, trigger_type)
- `customer_call_logs` — 통화 기록 (audio_url, transcript, duration_seconds)
- `customer_chat_tokens` — 고객 채팅 토큰 (token, is_active)
- `customer_activities` — 활동 타임라인 (activity_type, title, detail JSONB, related_table, related_id)
- `customer_consents` — 개인정보 동의 (consent_type, status, consent_date, expiration_date)
- `conversations` / `conversation_messages` — FA-고객 채팅
- `policy_analyses` — 증권분석 (customer_id FK로 고객 연결됨)

### 2-4. 기존 고객 등록 API 엔드포인트 (server/main.py)

- `POST /api/insuro/register-temporary-customer` — 라인 6394
  - 인증: verify_jwt (Bearer JWT)
  - 입력: customer_name (필수), customer_birth (선택, YYYYMMDD), analysis_id (선택)
  - 동작: customers 테이블 INSERT (stage='lead', tags=['temporary']), policy_analyses에 customer_id 연결
  - 주목: 평문 INSERT. 고객 등록 시 암호화 없음.

- `GET /api/insuro/search-customers` — 라인 6453
  - 인증: verify_jwt
  - 입력: q (이름 검색)
  - 동작: customers 테이블에서 name ilike 검색. **name 평문 검색이므로 암호화 후 수정 필요.**

- `GET /api/insuro/customers/{customer_id}/summaries` — 라인 3359
  - 인증: verify_jwt + require_feature("crm_ai_analysis")
  - 동작: conversation_summaries 조회

---

## 3. CRM 프론트 동작 요약

**CrmCustomers.tsx** (`/home/jay/projects/InsuRo/src/pages/CrmCustomers.tsx`): Supabase 클라이언트를 직접 사용하여 customers 테이블을 SELECT (agent_id = session.user.id 필터, 생성일 내림차순). 조회 컬럼: id, name, phone, email, stage, tags, ai_summary, next_contact_date, created_at, occupation, birth_date, gender. 고객 등록 폼에서 name/phone/email/birth_date/gender/occupation/address/income_range/stage/tags/memo를 입력받아 직접 INSERT. birth_date는 YYYY-MM-DD 형식. gender는 자유 텍스트. **평문으로 DB에 직접 저장됨.**

**CrmCustomerDetail.tsx** (`/home/jay/projects/InsuRo/src/pages/CrmCustomerDetail.tsx`): useParams로 customer id 받아 customers 테이블 SELECT. Customer 타입에 name, birth_date, gender, phone, email, address, income_range, family_info, total_premium, interest_areas 포함. 탭 구성: 기본정보, 보험이력(customer_insurance), 상담노트(customer_notes), 통화기록(customer_call_logs), 동의(customer_consents), 타임라인(customer_activities), AI요약(SummaryTab). 화면에 name/birth_date를 평문 표시함. 암호화 도입 시 서버에서 복호화 후 마스킹 응답 필요.

---

## 4. PII 암호화 설계

### 4-1. Supabase Vault 사용 가능 여부

마이그레이션 전체 검색 결과: `vault.secrets`, `vault.create_secret`, `CREATE EXTENSION vault` 사용 이력 **없음**.

현재 사용 중인 확장:
- `pg_cron` — 20260308124600 마이그레이션
- `pg_net` — 20260308124600 마이그레이션
- `pgcrypto` — **20260424180000 마이그레이션에서 이미 활성화됨** (digest 함수로 compliance_consents.content_hash 계산에 사용)

결론: **Supabase Vault 미사용. pgcrypto는 이미 활성화되어 있음.**

### 4-2. 권장 암호화 방식

**애플리케이션 레벨 암호화 (Python서버) + pgcrypto hash 조합** 권장.

근거:
- Supabase Vault: 운영 환경에서 별도 secret 관리 인프라 필요, 현재 미사용 → 도입 비용 높음
- pgcrypto pgp_sym_encrypt: DB 레벨 암호화는 가능하나 키가 쿼리에 노출됨. SQL 로그/explain 등에서 키 노출 위험
- 애플리케이션 레벨: Python server에서 암호화 후 ciphertext TEXT를 DB에 저장. 키는 .env 환경변수 관리. 현재 팀 스택(FastAPI)에 자연스럽게 통합 가능.

권장 구현:
- 라이브러리: `cryptography` 패키지의 Fernet (AES-128-CBC + HMAC, base64 인코딩)
- 암호화: `Fernet(PII_ENCRYPTION_KEY).encrypt(name.encode()).decode()`
- 복호화: `Fernet(PII_ENCRYPTION_KEY).decrypt(ciphertext.encode()).decode()`
- PII_ENCRYPTION_KEY: Fernet.generate_key()로 생성, server/.env에 보관

### 4-3. 키 관리 방안

- 환경변수 이름: `PII_ENCRYPTION_KEY` (Fernet key, 32 bytes URL-safe base64)
- 보관: server/.env (로컬), Supabase 대시보드 → Edge Function Secrets (운영), Railway/Render 등 서버 환경변수
- 키 로테이션: Phase 4+ 과제 (현재는 단일 키)
- **salt 별도 추가**: `CUSTOMER_KEY_SALT` 환경변수 (hash 정규화용, 32자 이상 랜덤 문자열)
- .env.example에 `PII_ENCRYPTION_KEY=` 및 `CUSTOMER_KEY_SALT=` 추가 필요 (Phase 1 시)
- **git 보안**: server/.env는 `.gitignore`에 등록되어 git 이력에 포함되지 않음. 운영 환경 키 주입은 Railway/Render/Supabase 환경변수로 처리(.env 파일 자체를 운영 서버에 배포하지 않음). 키 탈취 방지 위해 .env.example에는 빈 값만 노출.

### 4-4. 매칭 키 hash 정규화 정책

customer_key_hash 계산 pseudo-code (Python):

```python
import hashlib, os

SALT = os.environ["CUSTOMER_KEY_SALT"]

def normalize_name(raw: str) -> str:
    # 공백 제거, 소문자화 (한글은 소문자 없지만 영문 혼용 대비)
    return raw.strip().replace(" ", "").lower()

def normalize_dob(raw: str) -> str:
    # 8자리 숫자만 추출 (19850101 형식)
    digits = "".join(c for c in raw if c.isdigit())
    return digits[:8]  # 8자리 확정

def normalize_gender(raw: str) -> str:
    # 'M' 또는 'F'만 허용
    g = raw.strip().upper()
    return g if g in ("M", "F") else ""

def compute_customer_key_hash(name: str, dob: str, gender: str) -> str:
    n = normalize_name(name)
    d = normalize_dob(dob)
    g = normalize_gender(gender)
    raw = f"{n}|{d}|{g}|{SALT}"
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()
```

- 구분자 `|` 사용 (단순 연결 시 "홍길동850101M" vs "홍길동8501010" 충돌 방지)
- SALT는 환경변수에서만 로딩. DB에 저장 안 함.
- **고정 salt 트레이드오프**: 본 설계는 환경변수 고정 salt를 사용. 장점은 동일인 검색(customer_key_hash 일치 검색)이 가능하다는 점. 단점은 salt 탈취 시 알려진 (이름+생년월일+성별) 공간에 대한 사전공격(precomputed dictionary attack) 가능성이 존재. 동명이인+생년월일 가능 조합이 한국 인구 기준 수백만 건 규모이므로 무작위 행 단위 salt 대비 보안성이 낮음. 그러나 (1) salt가 환경변수로만 노출되며 (2) 매칭 검색 기능이 비즈니스에 필수이므로 본 트레이드오프를 채택. 무작위 salt는 검색 불가능 → 매칭 기능 자체가 동작 안 함. 회장 승인 시 본 트레이드오프를 명시 인지 후 결정 요청.

### 4-5. 표시용 마스킹 정책

서버에서 복호화 후 마스킹하여 클라이언트에 반환 (평문 복호화 결과를 클라이언트에 절대 그대로 전달 안 함):

- 이름 마스킹: 첫 글자 + `**` → 예) "홍길동" → "홍**"
  ```python
  def mask_name(name: str) -> str:
      return name[0] + "**" if name else "***"
  ```
- 생년월일 마스킹: YYYY-MM-DD → YYMMDD 6자리 → 예) "1985-01-01" → "850101"
  ```python
  def mask_dob(dob: str) -> str:
      digits = "".join(c for c in dob if c.isdigit())
      return digits[2:8] if len(digits) >= 8 else digits
  ```
- gender: M/F 그대로 표시 (민감도 낮음)
- ohmy_capture_history에서 반환 시: `{"masked_name": "홍**", "masked_dob": "850101", "gender": "M"}`

---

## 5. 캡처 → CRM 매핑

### 캡처 구조 (plan.md 4절) → customers 테이블 컬럼 매핑

**기존 컬럼 활용 가능:**
- 캡처 `client.name` → customers.`name` (단, Phase 1+에서 암호화 도입 시 encrypted_name으로 이전. 기존 name 컬럼은 하위호환 유지)
- 캡처 `client.dob` (8자리 문자열) → customers.`birth_date` (DATE 타입). 변환: "19850101" → "1985-01-01". 신 캡처 데이터는 별도 `encrypted_dob TEXT` 컬럼 추가 필요
- 캡처 `client.gender` ("M"/"F") → customers.`gender` TEXT. 기존 컬럼 활용 가능 (CHECK 제약 없어서 그대로 저장 가능)
- 캡처 `client.age` → customers에 컬럼 없음. birth_date에서 계산 가능하므로 별도 저장 불필요
- 캡처 `insurance_type` → customers에 직접 매핑 컬럼 없음. customer_insurance.coverage_type 또는 interest_areas에 보관 가능
- 캡처 `product_type` → 위와 동일. 신규 저장 불필요 (ohmy_capture_history에만 보관)
- 캡처 `payment_term`, `maturity_age` → ohmy_capture_history에 보관. customers에는 불필요
- 캡처 `plan` → ohmy_capture_history.plan_name에 보관

**신규 컬럼 추가 필요 (ohmy_capture_history에서만):**
- `encrypted_name TEXT NOT NULL` — 암호화된 이름
- `encrypted_dob TEXT NOT NULL` — 암호화된 생년월일
- `customer_key_hash TEXT NOT NULL` — SHA-256 매칭용 hash (인덱스 추가)
- `capture_hash TEXT NOT NULL` — 중복 방지용 매트릭스 hash
- `matrix JSONB NOT NULL` — 매트릭스 전체
- `insurance_type TEXT`, `product_type TEXT`, `payment_term TEXT`, `maturity_age TEXT`, `plan_name TEXT`, `source TEXT`

**customers 테이블 신규 추가 권고:**
- `customer_key_hash TEXT` — 향후 매칭용 (Phase 3에서 채움). 현재 customers에 없음. Phase 1에서는 ohmy_capture_history에만 존재해도 됨.

### 신규 vs 기존 구분

기존 컬럼 그대로 사용:
- customers.gender (M/F)
- customers.birth_date (DATE, 변환 필요)
- customers.stage, tags, occupation, phone, email 등 기타 CRM 필드

기존 컬럼 암호화 전환 필요 (Phase 3):
- customers.name → encrypted_name (Text) 전환 또는 병행 운영
- customers.birth_date → encrypted_dob 병행 추가

신규 추가 필요:
- customers.`customer_key_hash TEXT` (Phase 3 매칭 시)
- ohmy_capture_history 테이블 전체 (Phase 1)

---

## 6. Phase 1 추가 마이그레이션 SQL 초안

주의: 아래 SQL은 설계 초안임. 실제 적용은 Phase 1 PR에서.

### 6-1. ohmy_capture_history 테이블 (plan.md 5절 그대로)

```sql
CREATE TABLE IF NOT EXISTS ohmy_capture_history (
  id BIGSERIAL PRIMARY KEY,
  user_id UUID NOT NULL,
  customer_id UUID,                         -- CRM 고객 ID (Phase 3 매칭 후)
  customer_key_hash TEXT NOT NULL,          -- SHA-256(name|dob|gender|salt)
  encrypted_name TEXT NOT NULL,             -- Fernet 암호화
  encrypted_dob TEXT NOT NULL,              -- Fernet 암호화
  gender TEXT NOT NULL CHECK (gender IN ('M','F')),
  insurance_type TEXT,
  product_type TEXT,
  payment_term TEXT,                        -- "20년"
  maturity_age TEXT,                        -- "100세"
  plan_name TEXT,
  matrix JSONB NOT NULL,
  capture_hash TEXT NOT NULL,
  source TEXT DEFAULT 'trigger',
  captured_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_ohmy_capture_user ON ohmy_capture_history(user_id, captured_at DESC);
CREATE INDEX idx_ohmy_capture_customer ON ohmy_capture_history(customer_id, captured_at DESC);
CREATE INDEX idx_ohmy_capture_keyhash ON ohmy_capture_history(customer_key_hash);
CREATE UNIQUE INDEX idx_ohmy_capture_dedupe ON ohmy_capture_history(user_id, capture_hash);

ALTER TABLE ohmy_capture_history ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read own capture history" ON ohmy_capture_history
  FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own capture history" ON ohmy_capture_history
  FOR INSERT WITH CHECK (auth.uid() = user_id);
```

### 6-2. customers 컴포지트 유니크 제약 (Phase 3 전 필요)

현황: customers 테이블에 (name+birth_date+gender) 또는 customer_key_hash 유니크 제약 없음. UPSERT 동시성 보장 불가.

권고: customer_key_hash 컬럼 추가 + 유니크 제약 부여 방식 (평문 컬럼 조합보다 안전).

```sql
-- Phase 3 마이그레이션 초안 (지금 적용 안 함)
ALTER TABLE customers ADD COLUMN IF NOT EXISTS customer_key_hash TEXT;
ALTER TABLE customers ADD COLUMN IF NOT EXISTS encrypted_name TEXT;
ALTER TABLE customers ADD COLUMN IF NOT EXISTS encrypted_dob TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_key_hash_per_agent
  ON customers(agent_id, customer_key_hash)
  WHERE customer_key_hash IS NOT NULL;
-- 주의: (agent_id, customer_key_hash) 복합 유니크 — 동일 FA의 동일 고객 중복 방지
-- 다른 FA가 같은 고객 등록 = 별도 row (정상)
-- name/birth_date 컬럼은 하위호환을 위해 NOT NULL 유지(기존 데이터 보존),
-- 신규 INSERT부터는 encrypted_name/encrypted_dob에 암호문, name/birth_date에 마스킹 텍스트 또는 빈 문자열 저장 정책으로 전환
```

기존 데이터 중복 가능성: 현재 유니크 제약 없으므로 같은 이름+생년월일을 가진 customers row가 이미 존재할 가능성 있음. Phase 3 마이그레이션 전 `SELECT name, birth_date, gender, agent_id, COUNT(*) FROM customers GROUP BY name, birth_date, gender, agent_id HAVING COUNT(*) > 1` 조회로 사전 점검 필요 (조사만, 정리는 별도 task).

### 6-3. 인덱스

위 6-1에 포함. ohmy_capture_history 4개 인덱스 (user+날짜, customer+날짜, keyhash, dedupe unique).

---

## 7. Codex 우려 대응 매트릭스

우려 1 — [critical] `/api/insuro/composite-design/ingest` 자동 push 경로가 신 설계 "명시적 클릭만" 원칙과 충돌:
- Phase 0(현재): 충돌 구조 확인 완료. background.js의 OHMY_CAPTURED → pushToInsuro → `/composite-design/ingest` 흐름이 이미 구현되어 InsuRo JWT 설정 상태에서 자동 동작 중 (background.js의 JWT 미설정 가드는 존재하나, 일단 JWT가 설정되면 명시적 클릭 없이도 push 발생).
- Phase 1: 신 엔드포인트 `POST /api/insuro/ohmy-capture` 추가. content.js에 플로팅 버튼(Z-①) 추가. background.js에 새 메시지 타입 `OHMY_MATRIX_CAPTURED` 분기 추가.
- Phase 1 완료 후: 기존 `/composite-design/ingest` 엔드포인트에 deprecation 헤더 추가. background.js의 OHMY_CAPTURED 핸들러는 유지하되 신 경로로 리디렉션.
- Phase 2: `/composite-design/ingest` 운영 정지. OHMY_CAPTURED 핸들러 제거 또는 no-op 처리.

우려 2 — [high] `ohmy_user_views`, `ohmy_plans`, `ohmy_premiums`와 신 `ohmy_capture_history` 이중화:
- Phase 0(현재): 이중화 구조 확인. ohmy_user_views는 plan_id+age+gender만 저장(매트릭스 없음), ohmy_premiums는 담보별 보험료 캐시. 신 ohmy_capture_history는 전체 매트릭스 JSONB 저장. 용도 완전히 다름.
- 결론: 이중화 아님. ohmy_plans/premiums는 구 크롤링 기반 캐시(운영 정지됨), ohmy_capture_history는 신 캡처 기반. 공존 가능. ohmy_user_views는 Phase 1 이후 redundant → Phase 2에서 deprecation 검토.

우려 3 — [high] customers.name/birth_date 평문 저장 vs 신 PII 암호화 정책:
- Phase 0(현재): 충돌 확인. 기존 customers에 name/birth_date 평문 저장, 암호화 없음.
- Phase 1: ohmy_capture_history에만 encrypted_name/encrypted_dob 저장. 기존 customers는 건드리지 않음.
- Phase 3: customers에 customer_key_hash, encrypted_name, encrypted_dob 컬럼 추가 마이그레이션. 기존 평문 name은 하위호환 유지 후 별도 암호화 배치 처리 (Phase 4+ 과제).

우려 4 — [high] customers 컴포지트 유니크 제약 부재:
- Phase 0(현재): 제약 없음 확인. UPSERT 동시성 보장 불가.
- Phase 1: ohmy_capture_history에 (user_id, capture_hash) UNIQUE INDEX로 캡처 중복 방지.
- Phase 3: customers에 (agent_id, customer_key_hash) UNIQUE INDEX 추가. 트랜잭션 UPSERT 구현.

우려 5 — [medium] `/composite-design` UI 재구성 필요:
- Phase 0(현재): 현재 UI가 32개 담보 직접 입력 흐름 기반임을 확인. 신 설계는 history 목록 + 캡처 클릭 흐름.
- Phase 1: history 목록 카드 UI(단계 C) 구현으로 해결. 기존 담보 입력 폼은 task-2349에서 이미 제거됨.

우려 6 — [medium] Phase 0 승인 게이트가 코드 경로에서 강제되지 않음:
- Phase 0(현재): 보고서 기반 승인 게이트. 코드 레벨 강제는 없음. 운영 규칙으로 대응.
- Phase 1: Phase 0 보고서 + 회장 승인 확인 후 첫 PR 오픈. PR description에 "Phase 0 승인: [날짜]" 명시 규칙 추가.

---

## 8. 회장 승인 요청 사항

Phase 1 진행 전 다음 항목에 대한 결정이 필요합니다:

결정 1 — **PII 암호화 방식 확정**: 애플리케이션 레벨(Python Fernet) vs pgcrypto pgp_sym_encrypt vs Supabase Vault. 스바로그 권장: **Python Fernet** (기존 스택 통합, 키가 SQL 로그에 노출 없음). Vault는 인프라 추가 작업 필요.

결정 2 — **PII_ENCRYPTION_KEY / CUSTOMER_KEY_SALT 보관 방식**: server/.env (개발) + Railway/Render 환경변수 (운영). 별도 KMS(AWS KMS 등) 필요 여부 결정 요청.

결정 3 — **기존 customers.name/birth_date 평문 데이터 처리 방식**: Phase 1에서는 기존 customers 평문 데이터 그대로 유지(ohmy_capture_history만 암호화). Phase 3에서 암호화 마이그레이션 일괄 처리. 이 방향이 맞는지 확인 요청.

결정 4 — **자동 push 경로 차단 시점**: background.js OHMY_CAPTURED → `/composite-design/ingest` 자동 push를 Phase 1 완료 시점에 차단할지, Phase 2까지 병행 운영할지. 스바로그 권장: **Phase 1 PR과 동시에 신 경로로 전환, 구 경로 deprecation 시작**.

결정 5 — **ohmy_user_views 테이블 처리**: 구 크롤링 시절 "최근 본 조건" 기록 테이블. 신 ohmy_capture_history로 대체 가능. Phase 2에서 deprecation 처리해도 되는지 확인.

결정 6 — **customers 테이블 컴포지트 유니크 제약 방식**: (agent_id, customer_key_hash) 복합 UNIQUE로 결정 확인. 또는 (agent_id, name, birth_date, gender) 평문 복합 유니크로 할지.

---

## 9. 다음 단계 (Phase 1 첫 PR 범위 제안)

회장 승인 후 Phase 1 첫 PR 범위:

PR-1 (백엔드 + 마이그레이션):
- 마이그레이션: ohmy_capture_history 테이블 생성 (6-1 초안 기반)
- server/.env.example에 PII_ENCRYPTION_KEY, CUSTOMER_KEY_SALT 추가
- `POST /api/insuro/ohmy-capture` 엔드포인트 신규 구현
  - Fernet 암호화 (encrypted_name, encrypted_dob)
  - compute_customer_key_hash 함수
  - capture_hash 중복 체크
  - ohmy_capture_history INSERT
- `GET /api/insuro/ohmy-capture-history` 엔드포인트 신규 구현 (마스킹 응답)

PR-2 (Extension content.js):
- 플로팅 버튼(Z-①) 구현
- 매트릭스 DOM 스크래핑 함수
- capture_hash 계산
- background.js에 OHMY_MATRIX_CAPTURED 메시지 타입 추가
- 구 OHMY_CAPTURED → `/composite-design/ingest` 경로 deprecation (신 경로로 전환)

PR-3 (프론트 CompositeDesign):
- 단계 C UI (history 목록 카드, 마스킹 이름/생년월일 표시)
- 항목 클릭 → 1사/2사/3사 분석 결과 표시

---

*본 문서는 Phase 0 산출물입니다. 회장 승인 전 Phase 1 코드 작성 금지.*
