# 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` — 33개 수집 대상 coverage_cd

33개 대상 담보 (coverage_cd는 ohmymanager API 응답에서 확인):
상해후유장해(3-100%), 상해사망, 질병후유장해(3-100%), 질병사망,
암진단비(유사암제외), 유사암진단비, 항암방사선약물치료비, 표적항암약물허가치료비,
뇌출혈입원일자진단비, 항암증입자양자치료비, 하이클래스수술요치료비(10년),
암수술비(해외유사암포함), 다빈치로봇수술비, 뇌혈관질환진단비, 뇌출중진단비,
뇌출혈진단비, 허혈심장질환진단비, 급성심근경색진단비, 혈전용해치료비,
상해수술비, 질병수술비, 상해(1-5급)수술비(5종기준), 질병(1-5급)수술비(5종기준),
질병중환자실입원비, 상해중환자실입원비, 간병인상해입원일당(요양병원포함),
간병인질병입원일당(요양병원포함), 상해간호간병통합서비스, 질병간호간병통합서비스,
통풍진단비, 대상포진진단비, 관절진단비(치아파절제외), 화상진단비

### 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 범위 내에서만 조회
- **33개 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` — 33개 담보 목록
- `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 테이블에 33개 담보 존재
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 성공