# InsuRo 복합설계 AI 계산기 — Phase 1 (MVP)

## 작업 레벨: Lv.4 (한정승인 — 팀장이 전체 일괄 진행)

## 프로젝트
- InsuRo 서버: `/home/jay/projects/InsuRo/server`
- InsuRo 프론트: `/home/jay/projects/InsuRo`

## 3문서 (필수 참조)
- 계획서: `/home/jay/workspace/memory/plans/insuro-composite-design/plan.md`
- 맥락노트: `/home/jay/workspace/memory/plans/insuro-composite-design/context-notes.md`
- 체크리스트: `/home/jay/workspace/memory/plans/insuro-composite-design/checklist.md`
- 미팅 기록: `/home/jay/workspace/memory/meetings/2026-04-30-composite-design-calculator.md`

★ 위 3문서를 반드시 읽고 전체 맥락을 파악한 뒤 작업할 것.

## Phase 1 범위 (순서대로)

### Step 1: DB 마이그레이션

파일: `server/migrations/009_composite_design_tables.sql`

```sql
CREATE TABLE IF NOT EXISTS ohmy_plans (
  id SERIAL PRIMARY KEY,
  plan_id TEXT NOT NULL UNIQUE,
  plan_name TEXT NOT NULL,
  plan_type_name TEXT,
  insurance_type TEXT CHECK (insurance_type IN ('F','L','LF')),
  min_m_age INT DEFAULT 0,
  max_m_age INT DEFAULT 0,
  min_f_age INT DEFAULT 0,
  max_f_age INT DEFAULT 0,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE IF NOT EXISTS ohmy_coverages (
  id SERIAL PRIMARY KEY,
  coverage_cd TEXT NOT NULL UNIQUE,
  coverage_name TEXT NOT NULL,
  guide_amount INT DEFAULT 1000,
  coverage_seq INT DEFAULT 0
);

CREATE TABLE IF NOT EXISTS ohmy_premiums (
  id BIGSERIAL PRIMARY KEY,
  plan_id TEXT NOT NULL,
  coverage_cd TEXT NOT NULL,
  age INT NOT NULL,
  gender TEXT NOT NULL CHECK (gender IN ('M','F')),
  insurer_code TEXT NOT NULL,
  insurer_name TEXT NOT NULL,
  premium INT NOT NULL DEFAULT 0,
  collected_at TIMESTAMPTZ DEFAULT now(),
  is_active BOOLEAN DEFAULT true
);
CREATE INDEX IF NOT EXISTS idx_ohmy_prem_query ON ohmy_premiums(plan_id, age, gender, coverage_cd);

CREATE TABLE IF NOT EXISTS ohmy_raw_responses (
  id BIGSERIAL PRIMARY KEY,
  plan_id TEXT NOT NULL,
  age INT NOT NULL,
  gender TEXT NOT NULL,
  raw_json JSONB NOT NULL,
  collected_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE IF NOT EXISTS ohmy_crawl_logs (
  id BIGSERIAL PRIMARY KEY,
  fa_id TEXT NOT NULL,
  plan_id TEXT NOT NULL,
  age INT NOT NULL,
  gender TEXT NOT NULL,
  status_code INT,
  ip_address TEXT,
  crawled_at TIMESTAMPTZ DEFAULT now()
);
```

★ Supabase Management API로 실행:
```python
from dotenv import load_dotenv
import os, requests
load_dotenv(dotenv_path='/home/jay/workspace/.env.keys')
url = os.environ.get('INSURO_NEW_SUPABASE_URL','')
ref = url.replace('https://','').split('.')[0]
mgmt_key = os.environ.get('SUPABASE_ACCESS_TOKEN','')
with open('server/migrations/009_composite_design_tables.sql') as f:
    sql = f.read()
resp = requests.post(f'https://api.supabase.com/v1/projects/{ref}/database/query',
    headers={'Authorization': f'Bearer {mgmt_key}', 'Content-Type': 'application/json'},
    json={'query': sql}, timeout=30)
print(f"Status: {resp.status_code}")
```

### Step 2: 플랜 목록 + 담보 목록 초기 데이터

파일: `server/config/ohmy_plans.json` — 74개 plan_id 목록 (이전 분석에서 확보)
파일: `server/config/ohmy_target_coverages.json` — 32개 수집 대상 coverage_cd

32개 대상 담보 (coverage_cd는 ohmymanager API 응답에서 확인):
1. 상해후유장해(3~100%)
2. 상해사망
3. 질병후유장해(3~100%)
4. 질병사망
5. 암진단비(유사암제외)
6. 유사암진단비
7. 항암방사선약물치료비(최초1회한)
8. 표적항암약물허가치료비(최초1회한)
9. 항암중입자방사선치료비(최초1회한)
10. 하이클래스암주요치료비(10년)
11. 암수술비(매회,유사암포함)
12. 다빈치로봇암수술비
13. 뇌혈관질환진단비
14. 뇌졸중진단비
15. 뇌출혈진단비
16. 허혈성심장질환진단비
17. 급성심근경색증진단비
18. 혈전용해치료비
19. 상해수술비
20. 질병수술비
21. 상해(1~5종)수술비(5종기준)
22. 질병(1~5종)수술비(5종기준)
23. 질병중환자실입원비
24. 상해중환자실입원비
25. 간병인상해입원일당(요양병원포함)
26. 간병인질병입원일당(요양병원포함)
27. 상해간호간병통합서비스
28. 질병간호간병통합서비스
29. 통풍진단비
30. 대상포진진단비
31. 골절진단비(치아파절제외)
32. 화상진단비

### Step 3: 수집 스크립트

파일: `server/scripts/ohmy_premium_collector.py`

핵심 요구사항:
- ohmymanager API: GET https://mmlfcp.ohmymanager.com/api/ProductPremiums?plan_id=X&insurance_type=T&age=N&gender=G
- JWT 인증: .env.keys의 OHMY_JWT_FA1, OHMY_JWT_FA2, OHMY_JWT_FA3
- 3명 FA 토큰 로테이션 (각각 다른 패턴)
- **포아송 분포 간격**: 평균 9분 (numpy.random.poisson 또는 random.expovariate)
- **3가지 패턴**: FA1 새벽형(2-8시 집중), FA2 야행형(18-2시 집중), FA3 불규칙(24시간 균등)
- **조회 순서 랜덤**: (plan_id, age, gender) 튜플 리스트를 shuffle
- **5세 단위**: age = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]
  - 각 플랜의 min/max_age 범위 내에서만 조회
- **32개 coverage_cd 필터**: API 응답에서 대상 담보만 추출하여 ohmy_premiums INSERT
- **raw JSON 보존**: 전체 응답을 ohmy_raw_responses에 저장
- **crawl_logs 기록**: 매 호출 결과 기록
- **circuit breaker**: 403 응답 3회 연속 시 자동 중단 + Telegram 알림
- **체크포인트**: 수집 진행률을 로컬 파일로 추적 (중단 후 재개 가능)
- **IP 분산**: ★ Phase 1에서는 단일 서버 실행 (IP 분산은 Phase 2). 대신 호출 간격을 넉넉히 (평균 9분)

★ 보안: JWT 토큰을 코드에 하드코딩 금지. .env.keys에서 로드.

### Step 4: 최적 조합 계산 엔진

파일: `server/composite_calculator.py`

입력:
```python
{
  "age": 40,
  "gender": "M",
  "insurance_type": "F",  # 손보
  "plan_id": "000000111041",
  "selected_coverages": [
    {"coverage_cd": "a001", "amount": 1000},
    {"coverage_cd": "a017", "amount": 1000},
    ...
  ]
}
```

로직:
1. ohmy_premiums에서 해당 plan_id + age + gender의 보험사별 보험료 조회
2. 선택된 담보만 필터
3. 보험사 목록 추출 (N개)
4. **1사 최저가**: 모든 담보 합산이 가장 낮은 단일 보험사
5. **2사 조합**: C(N,2) 조합 열거 → 담보별 두 보험사 중 최저가 배분 → 합산 최소인 조합
6. **3사 조합**: C(N,3) 조합 열거 → 담보별 세 보험사 중 최저가 배분 → 합산 최소인 조합
7. 제약: 보험사당 합산 최소 1만원
8. 절감률: (1사 최저가 - N사 조합) / 1사 최저가 × 100

출력:
```python
{
  "single_best": {"insurer": "메리츠화재", "total": 290410, "coverages": [...]},
  "dual_best": {"insurers": ["메리츠", "롯데"], "total": 265000, "saving_pct": 8.7, "coverages": [...]},
  "triple_best": {"insurers": ["메리츠", "롯데", "현대"], "total": 252000, "saving_pct": 13.2, "coverages": [...]}
}
```

### Step 5: API 엔드포인트

`server/main.py`에 추가:
- `POST /api/insuro/composite-calculate` — 최적 조합 계산 (인카 RLS)
- `GET /api/insuro/composite-coverages` — 32개 담보 목록
- `GET /api/insuro/composite-plans` — 플랜 목록 (보종별 필터)

### Step 6: 프론트엔드 UI

파일: `src/pages/CompositeDesign.tsx` (신규)

- 조건 입력: 성별 토글 + 나이 드롭다운(5세 단위) + 보종 + 플랜 + 담보 체크리스트
- 결과: 3열 카드 (1사/2사/3사) + 절감률 뱃지 + 담보별 상세
- 인카 소속 아닌 경우: "내부 전용 기능" 안내
- 면책 문구: "기준일: YYYY-MM-DD / 참고용, 실제 보험료와 다를 수 있습니다"

네비게이션: `분석 & 도구` 메뉴에 "복합설계 계산기" 추가

### Step 7: 검증

- DB 5테이블 생성 확인
- 수집 스크립트 드라이런 (1건 테스트 조회)
- 최적 조합 결과 정확성 (수동 대조 3건)
- API 인증 (인카 RLS) 확인
- npm run build 성공

## affected_files
- `server/migrations/009_composite_design_tables.sql` (신규)
- `server/config/ohmy_plans.json` (신규)
- `server/config/ohmy_target_coverages.json` (신규)
- `server/scripts/ohmy_premium_collector.py` (신규)
- `server/composite_calculator.py` (신규)
- `server/main.py` (수정 — API 3개 추가)
- `src/pages/CompositeDesign.tsx` (신규)
- `src/components/navigation/navigationConfig.ts` (수정 — 메뉴 추가)
- `src/config/routes.ts` (수정 — 라우트 추가)

## 검증 시나리오
1. ohmy_plans 테이블에 74개 플랜 존재
2. ohmy_coverages 테이블에 32개 담보 존재
3. 수집 스크립트 1건 테스트: plan_id=000000111041, age=40, gender=M → premiums INSERT 확인
4. POST /api/insuro/composite-calculate → 1사/2사/3사 결과 반환
5. 절감률이 양수인지 확인 (3사가 1사보다 저렴)
6. 인카 소속 아닌 사용자 접근 시 403
7. npm run build 성공