# 맥락노트: InsuRo 복합설계 AI 계산기

**status**: in-progress
**작성일**: 2026-04-30
**최종 갱신**: 2026-05-02 (brainstorming 종합 결정 반영)

---

## 결정 근거

### 왜 ohmymanager 데이터인가?
- FA들이 이미 사용하는 보험료 비교 도구
- 매트릭스 자체가 보험사 × 담보 × 보험료 정보 풍부 (한 화면에 300+ 셀)
- 회장 워크플로우 정합: FA가 화면에 띄워놓은 데이터를 InsuRo가 직접 가져옴

### 왜 직접 크롤링(Phase 1)을 폐기했나? (2026-05-01 피벗)
- ohmymanager 약관 "데이터 재활용 금지" 리스크
- IP 차단 위험 (3명 FA × 3일 분산해도 단방향 크롤링)
- 사용자 워크플로우와 부정합: 사용자가 입력하는 조건과 백엔드 캐시가 어긋날 수 있음
- → **Chrome Extension 자동 캡처**로 전환 (사용자 = 진실의 원천)

### 왜 사이드로드 Extension인가?
- 인카 FA 한정 배포 (스토어 등록 불필요)
- ohmymanager 약관 우회: 사용자가 자기 화면 데이터를 자기 도구(InsuRo)로 가져옴 = 합법
- 회장이 zip 직접 배포 통제 가능

### 왜 32개 담보가 아닌 매트릭스 전체 캡처인가? (2026-05-02 결정)
- Phase 1 시절 32개 담보 화이트리스트 → 이젠 사용자가 ohmymanager에서 선택한 담보만 캡처
- ohmymanager가 진실의 원천(A안 미러링) → 사용자가 보는 담보 = InsuRo가 받는 담보
- → 화이트리스트 불필요, ohmymanager 전체 매트릭스가 입력

### 왜 LLM을 안 쓰는가?
- "AI 계산기"이지만 순수 조합 최적화 문제
- C(N,3) 열거 + 그리디 = 빠르고 정확하고 결정론적
- LLM은 느리고 비싸고 비결정적

---

## 2026-05-02 brainstorming 종합 결정 (★ 최종)

### Q1-3: 확장 감지 + UI (이미 task-2349, task-2351에서 구현됨)
- **Q1 버전 협상**: 필요
- **Q2 감지 채널**: ③ 하이브리드 — DOM 마커(`document.documentElement.dataset.insuroHelper`) + postMessage HELLO 폴백
- **Q3 버전 정책**: (c) 메이저/마이너 분기 — major mismatch=잠금, minor mismatch=토스트 안내
- **결과**: EXT_ID 환경변수 의존 제거, 사이드로드 호환

### Q4: 캡처 범위
- **결정**: ohmymanager **매트릭스 전체** (보험사 × 담보 + 1차 입력 + 플랜)
- **이유**: 회장이 보내준 캡처에서 화면에 보험사 6+ × 담보 30+ 셀 풍부. ProductPremiums API 응답만으로는 불충분. DOM 스크래핑이 정확.
- **제외**: silent 백그라운드 fetch (ohmymanager 약관 위반 위험)

### Q5: 만기 vs 납입기간
- **결정**: ohmymanager 만기 드롭다운이 "20년/100세" 같은 통합 형식 (앞=납입기간, 뒤=만기연령)
- **처리**: `payment_term + maturity_age` 분리 파싱
- **회장 가치 제안**: 같은 고객의 20년납 vs 30년납 총 납입액 비교 → 핵심 분석 차원

### Q6: 실시간 통신
- **결정**: **Y1(자동 push) 폐기 + Z-①(플로팅 버튼) 단일** (회장 단순화 결정)
- **이유**: 실시간 변동은 history 비교 가치를 살리지 않음. 클릭 시점만 명시적 캡처가 의미 있음.
- **버튼 위치**: ohmymanager 우측 하단 fixed (DOM 충돌 회피)

### Q7 (담보/필터 반응): A 미러링
- **결정**: ohmymanager가 진실의 원천. InsuRo는 표시만.
- **이유**: 사용자 혼동 0. 단일 진실 원천. ohmymanager에서 선택한 담보 = InsuRo가 분석할 담보.

### NEW: history + CRM 연동 (회장 아이디어)
- 캡처 클릭 시점 = history N+1 저장
- 같은 고객(이름+생년월일) 클러스터링
- CRM 자동 매칭/등록 → FA의 영업 자산 자동 누적

### 만기 비교 (회장 통찰: "조회 history 저장 없이는 만기 비교 무의미")
- ohmymanager 한 화면에 만기 1개만 표시 → 단순 미러링으로는 부족
- 해결: **옵션 3 + 옵션 2 결합**
  - 옵션 3: ohmymanager 자체 "만기별 보험료 비교" 탭 활용 (사용자가 그 탭 클릭 시 캡처)
  - 옵션 2: 같은 고객 다른 만기 history 자동 누적 → InsuRo가 비교 UI 자동 생성

---

## 보안/리스크 결정 (2026-05-02 회장 답변)

| # | 결정 | 비고 |
|---|---|---|
| 1 | PII 암호화 ✅ 필수 | Supabase Vault 또는 pgcrypto |
| 2 | 금소법 → 향후 검토 | 일단 기능 우선 |
| 3 | 동명이인 → confirm 다이얼로그(a) + 사후 수동 분리(b) | "이 고객 맞나요?" |
| 4 | DOM 변경 취약성 → API 백업 + 폴백 | Phase 4에서 |
| 5 | 기존 CRM 스키마 조사 → Phase 0 (위임 첫 단계) | 회장 승인 게이트 |
| 6 | history 정리 → 사용자 수동 | 향후 GoogleDrive 백업 옵션 |
| 9 | 디바운스 → matrix hash 동일 체크 (시간 X) | 사용자 의도 존중 |
| 10 | InsuRo 미로그인 → 플로팅 버튼 미표시 | 인증 만료 우회 |
| 15 | CRM UPSERT 패턴 | 동시성 중복 방지 |

### 제외된 항목 (회장 결정)
- #7 가입금액 변경 시 시각 신호 — pass (사용자 클릭으로 충분)
- #11 오프라인 큐 — 인터넷 끊기면 ohmymanager 자체 사용 불가
- #12 DOM 스크래핑 성능 — 30ms 이내, 우려 없음
- #13 ohmymanager 회귀 자동화 — 사용자 신고로 충분

### 추가 검토 (회장 요청)
- #8 모바일 — ohmymanager 모바일 지원함 → Phase 4에서 호환 검증
- #14 새 브라우저 — 시크릿 모드 가이드 + 사이드로드 재설치 안내 강화

---

## 기각된 대안 (역사적 기록)

### 직접 크롤링 (Phase 1 — 2026-04-30 시도, 2026-05-01 폐기)
- 약관 위반 + IP 차단 + 사용자 워크플로우 부정합으로 폐기

### 실시간 push (Y1 — 2026-05-02 brainstorming 중간 단계)
- 회장 단순화 결정으로 폐기. history + 명시적 클릭이 더 가치 있음.

### Silent 백그라운드 fetch (옵션 4)
- ohmymanager 약관 + 서버 부하 위험으로 비추천

### LLM 분석
- 결정론적 알고리즘 우월 (C(N,3) 그리디)

---

## 주의사항

### ohmymanager 측면
- 약관: "데이터 재활용 금지" 존재. 사이드로드 + 인카 한정 + 사용자 명시적 클릭으로 리스크 완화.
- DOM 구조 변경 시 캡처 깨짐 → API 응답 가로채기 백업 필수

### 보안
- PII 평문 저장 절대 금지 (회장 결정 #1)
- JWT 토큰 노출 금지 (.env.keys 사용)
- CRM UPSERT 동시성 처리 필수 (유니크 제약 + 트랜잭션)

### UX
- 사용자 통제권 보장 (Z-① 버튼만 트리거, 자동 push 없음)
- 동명이인 confirm 다이얼로그 = 영업 사고 방지 안전장치
- InsuRo 미로그인 시 플로팅 버튼 미표시 = 우아한 우회

---

## 3 Step Why 자문 (2026-05-02 brainstorming 종합)

**1st Why**: 왜 이 설계(매트릭스 전체 캡처 + Z-① 단독 + history + CRM 연동)가 필요한가?
→ **A**: FA 워크플로우 정합 (ohmymanager 사용 흐름 그대로 보존). 사용자 명시적 클릭으로 데이터 통제권 부여. history 누적으로 만기별 비교 가치 실현. CRM 자동 등록으로 영업 자산화. 단일 task로 4가지 가치 동시 제공.

**2nd Why**: 왜 A(Z-① + history + CRM)가 다른 접근(Y1 자동 push, 단순 미러링, 별도 CRM task)보다 최선인가?
→ **B**:
- Y1 자동 push 대비: 사용자 통제권 보장 + 노이즈 0 + 만기 비교 가치 실현 가능 (자동 push는 옛 데이터를 덮어씀)
- 단순 미러링 대비: 만기 비교 같은 핵심 가치를 history 없이는 살릴 수 없음
- 별도 CRM task 대비: 같은 데이터 흐름(이름+생년월일) 활용. 분리 시 두 번 일.

**3rd Why**: 왜 B(Z-① + history)가 옵션 4(silent fetch)보다 안전한가?
→ **C**: ohmymanager 약관 + 서버 부하 + 자동화 차단 위험. Z-①은 사용자 명시적 액션이라 약관상 자기 화면 데이터의 자기 도구 활용 = 합법. 사용자 통제권 + 운영 안전 둘 다 확보.

**일관성 검증**: A(통제+history+CRM) → B(Y1/미러링/분리 대비 우월) → C(silent 대비 합법+안전). 논리 일관 ✅

---

## 참조 task

- task-2333: Phase 1 직접 크롤링 (폐기)
- task-2336: Chrome Extension 피벗
- task-2346: composite-calculate 정규화
- task-2349: UX A안 + 입력 폼 제거 (cancelled되었으나 결과적으로 머지)
- task-2351: 버전 정책 (c) 메이저/마이너 분기 보충
- **task-2354 Phase 0 (2026-05-02 완료, 회장 승인 대기)**: 사전 조사 + 매핑 + PII 암호화 설계
  - 보고서: `memory/reports/task-2354-phase0-cr_mapping.md`
  - 통합 보고서: `memory/reports/task-2354.md`

---

## 2026-05-02 Phase 1 완료 (코드 + 3종 검증 PASS)

브랜치: `feat/task-2354-phase1` (6 commit, GitHub push 회장 승인 대기)
변경 9 파일 / +839 / -35
- `server/migrations/011_ohmy_capture_history.sql` (신규)
- `supabase/migrations/20260502010000_customers_pii_columns.sql` (신규 — Phase 0.5 최소 인프라)
- `server/pii_crypto.py` (신규 Fernet PII + key_hash + masking)
- `server/main.py` POST/GET `/api/insuro/ohmy-capture(-history)` + 옛 ingest deprecation
- `extension/{content.js, background.js, manifest.json}` (Z-① 플로팅 버튼 + 매트릭스 캡처 + OHMY_MATRIX_CAPTURED)
- `src/pages/CompositeDesign.tsx` 단계 C history UI + 클라이언트 그리디

검증 3종:
- 마아트 독립 검증 — PASS(조건부) / 차단 사항 없음
- Codex 사전 검증 — pass=true (재검증). 1차 critical 1 + high 1 발견 → 즉시 수정 → 재검증 PASS
- Gemini 리뷰 — Phase 1 승인 가능

Codex가 1차에 잡은 Critical/High (재현 + 해소):
- CRITICAL: `extension/content.js sha256Json`의 `JSON.stringify(obj, keys.sort())` 두 번째 인자가 whitelist replacer라 nested 누락 → 재귀 `stableStringify` 도입
- HIGH: 클라이언트 capture_hash 신뢰 → 서버에서 canonical JSON `json.dumps(sort_keys=True)` 기반 재계산

잔여 권고(후속 PR 또는 task-2356 입력):
- DOM 셀렉터 추정 영역 보정 (실 ohmymanager DOM 검증 후)
- migration 011이 server/migrations/에만 존재 → Lovable 표준 미러링 또는 적용 절차 문서화
- matrix JSONB 크기 상한 추가 (현재 검증 없음, Pydantic 길이 검증 외)
- stableStringify가 일반 canonicalizer 아님 (서버가 클라 hash 무시하므로 보안 무영향)

## 2026-05-02 Phase 0 결정 사항 (회장 승인 항목 6건)

페룬(팀장) 위임 → 스바로그(백엔드) 조사 → 마아트(독립 검증 PASS) 후 도출:

### 결정 1 — PII 암호화 방식
- **권장**: Python Fernet (애플리케이션 레벨)
- **근거**: pgcrypto는 SQL 로그에 키 노출 위험. Vault는 인프라 추가 작업 필요. Fernet은 기존 FastAPI 스택 통합 자연스럽고 결정론적.
- **상태**: 회장 결정 대기

### 결정 2 — 키 보관 방식
- **권장**: server/.env (개발) + Railway/Render 환경변수 (운영)
- **추가 고려**: AWS KMS 등 외부 KMS 도입 여부
- **상태**: 회장 결정 대기

### 결정 3 — 기존 평문 customers 데이터 처리
- **권장**: Phase 1은 신규 ohmy_capture_history만 암호화. 기존 customers.name/birth_date 평문 유지. Phase 3에서 일괄 암호화 마이그레이션 또는 별도 task 분리.
- **상태**: 회장 결정 대기

### 결정 4 — 자동 push 경로 차단 시점
- **현황**: background.js의 OHMY_CAPTURED → `/api/insuro/composite-design/ingest` 자동 push가 JWT 설정 시 동작 중
- **권장**: Phase 1 PR과 동시에 신 경로 `OHMY_MATRIX_CAPTURED` → `/api/insuro/ohmy-capture`로 전환, 구 경로 deprecation 시작
- **상태**: 회장 결정 대기

### 결정 5 — ohmy_user_views deprecation
- **현황**: 구 크롤링 시절 "최근 본 조건" 기록. 신 ohmy_capture_history로 대체 가능.
- **권장**: Phase 2에서 deprecation
- **상태**: 회장 결정 대기

### 결정 6 — customers 컴포지트 유니크 제약 방식
- **현황**: customers 테이블에 (name+birth_date+gender) 유니크 제약 부재
- **권장**: Phase 3에서 (agent_id, customer_key_hash) 복합 UNIQUE INDEX 추가
- **상태**: 회장 결정 대기

---

## 마아트 독립 검증 결과 (Phase 0)

- **판정**: PASS (조건부)
- **사실 정합성**: PASS (오류 0건, customers 컬럼/server/main.py 라인/background.js 흐름 모두 검증됨)
- **차단 사항**: 없음
- **권고 4건**: 모두 반영 완료
  - 4-3절 git 보안 조치 추가
  - 4-4절 고정 salt 트레이드오프 명시
  - 6-2절 encrypted_name/encrypted_dob 컬럼 추가
  - 7절 background.js 자동 동작 서술 정정 (JWT 조건부)

---

## Codex 사전 검증 (Phase 0)

- pass=false (기대됨 — 현 코드와 신 설계 간 간극이 Phase 0의 입력)
- critical 1건, high 3건, medium 2건 모두 Phase 0/1/2/3 단계별 해소 매트릭스 작성 (보고서 7절)
- Phase 1 PR 후 Codex 재검증으로 PASS 전환 예상

---

## Phase 2 (task-2356, 2026-05-02) 결정 근거

### 그룹화 키
- `customer_key_hash` (Phase 1 `compute_customer_key_hash(name, normalized_dob, gender)`) 그대로 사용
- 이유: Phase 1에서 이미 계산 + DB 저장됨, Phase 2는 응답에 노출만 하면 됨 (server/main.py L7412/L7467)

### 그룹 정렬 정책
- 그룹 내: `captured_at desc` (최신순)
- 그룹 자체: 가장 최근 캡처가 있는 순서
- 사유: FA가 가장 최근 작업한 고객을 화면 상단에서 즉시 발견 가능

### 만기별 비교 활성 조건
- `items.length >= 2 && new Set(payment_term).size >= 2`
- 사유: 2건 모두 같은 만기면 "비교"가 무의미 — disabled + tooltip 안내

### 총 납입액 공식
- `single_best.total(월) × 12 × parsePaymentTermYears(payment_term)`
- "종신" 등 숫자 없는 term → 20년 폴백 + `is_fallback_term` 플래그 → 열 헤더에 비고 표시

### 추천 만기 산정
- 가장 작은 `total_lifetime`을 가진 item을 추천
- 두 번째 최저와의 차액을 `savingVsNext`로 표시
- 강조 스타일: `ring-2 ring-amber-400 border-amber-400` + `Award` 아이콘 배지

### tab_compare 분리 표시
- `triggerGroups` (인디고 톤 — 메인) vs `tabCompareGroups` (앰버 톤 — 별도 섹션)
- 사유: ohmymanager "만기별 보험료 비교" 탭은 그 자체로 만기 다양성을 가진 캡처 → InsuRo에서 다른 의미로 표시 (회장 결정 옵션 3)

### extension tab_compare 패턴 강화
- Phase 1: `location.pathname.includes("compare")` 단일 검사 (Korean URL 누락 가능)
- Phase 2: `pathname/hash/search` 통합 + 한글("만기"+"비교") 검출
- 사유: ohmymanager가 hash routing이거나 query 사용 시 누락 방지

### 회귀 방지 — 변경하지 않은 영역
- URL `?capture_id=N` 자동 선택 로직 (line ~407 autoSelectedRef)
- `selectedCapture` → composite-calculate-from-matrix 분석 useEffect
- 단계 B 가이드 (`captureHistory.length === 0`) 조건 — trigger+tab_compare 합산이라 그대로 유효
- `recentViews` 옛 경로 (extInstalled 미설치 환경 + 이전 데이터 보존)

### 알고리즘 trade-off
- 클라이언트 그룹화 (서버 그룹 API 미신설): 현재 limit=20 페이지네이션이라 클라이언트 단순 Map으로 충분
- 향후 history가 100건+로 성장하면 서버 측 GROUP BY API 필요할 수 있음 (Phase 3 이후)

### MaturityCompare 비동기 호출 패턴
- `Promise.allSettled` 사용 (`Promise.all` 아님)
- 사유: 한 item이 실패해도 나머지 열은 정상 렌더 — 사용자 경험 우선

---

## 2026-05-02 Phase 3 (task-2359) 결정 근거

### 매칭 lookup 시점 — 캡처 직후 동기 처리
- POST /api/insuro/ohmy-capture가 INSERT 직후 customers를 (agent_id, customer_key_hash)로 조회
- match 발견 시 customer_id 자동 매핑 안 함 (사용자 confirm 대기 — 회장 결정 #3 동명이인 안전장치)
- match 미발견 시 즉시 `upsert_customer_for_capture()` 호출 → customers row 생성 + history.customer_id UPDATE
- 사유: 별도 lookup 엔드포인트보다 단순 + 캡처 직후 사용자가 /composite-design 진입 시 dialog 자동 노출

### auto-link customers row 컬럼 채움 정책
- 평문 customers 흐름 호환 (Phase B에서 운영 마이그레이션 예정)
- name/birth_date 평문 + encrypted_name/encrypted_dob/customer_key_hash 신규 컬럼 dual-write
- stage='lead', tags=['temporary', 'auto_ohmy'] — 자동 등록 식별자
- 사유: Phase 3는 임시 통합. 본격 보안은 Phase B(task-2356 후속)에서 진행

### duplicate 분기 auto-link (마아트 권고 즉시 반영)
- 같은 capture_hash 재캡처 시 INSERT 23505 — 기존 capture row가 customer_id NULL이고 customers 매칭도 없으면 동일하게 auto-link 적용
- 사유: 정상 INSERT 분기와 동작 일관성 보장 (동일 사용자가 동일 매트릭스 재전송해도 customer_id 채워짐)

### CompositeDesign 매칭 dialog 자동 검출
- 별도 GET endpoint 추가 없이 captureHistory list 내에서 customer_key_hash 그룹의 customer_id 있는 항목을 matchCandidate로 사용
- shownMatchDialogIds useRef Set으로 1회만 표시 (사용자가 "취소"로 닫으면 재표시 안 함)
- 사유: 백엔드 API 표면 최소화 + 같은 사용자의 history만 확인하므로 RLS 추가 검증 불필요

### CRM 페이지 통합 — Dialog 기반
- CrmCustomerDetail 별도 페이지 없음 → CrmCustomers의 row 액션 버튼 → Dialog로 history 표시
- 사유: 기존 페이지 구조 보존, 라우팅 변경 최소

### POST /link 엔드포인트 분리 (캡처와 매칭 분리)
- ohmy-capture POST는 idempotent + match 정보만 반환
- 실제 customer_id 매핑은 사용자 confirm 후 POST /link로 분리
- 사유: 사용자 의사결정 후의 명시적 액션 + 동명이인 신규 등록 분기 가능

