# InsuRo Phase 2A: 플랜 데이터 정렬 + 서버사이드 플랜 검증

## 작업 개요
5단계 플랜 전략에 맞춰 DB 스키마/데이터 정렬 + 서버사이드 플랜 검증 구현

## 배경
현재 문제:
- subscription_plans 시드 데이터: Free/Basic/Pro/Enterprise (4개) → 전략과 불일치
- 확정 전략: 무료/프로/프리미엄(100명 한정)/엔터프라이즈/히든(가족) (5개)
- feature_definitions: menu/ai_token/limit/org 4개 카테고리 → 모델/채널/노하우 누락
- ai_config: 싱글톤 (전체 사용자 동일 모델) → 플랜별 모델 차별화 필요
- 월간 사용량: 표시만 하고 실제 차단 없음
- 채널 접근: 모든 사용자 전 채널 접근 가능 → 무료=1채널 제한 필요

## 작업 항목 (5개)

### 1. subscription_plans 5단계 플랜 정렬
**Supabase migration 작성** (새 migration 파일)

**변경 사항**:
- "Basic" 플랜 → 비활성화 (is_active=false, 기존 구독자 보호)
- "Free" → name: "무료", description 한글화
- "Pro" → name: "프로", price: 59000
- 신규 "프리미엄" 플랜 추가:
  - plan_type: "individual"
  - price: 99000
  - is_active: true, is_public: true
  - max_capacity: 100 (★ 신규 컬럼)
- "Enterprise" → name: "엔터프라이즈", 유지
- 신규 "히든" 플랜 추가:
  - plan_type: "individual"
  - price: 0
  - is_active: true, is_public: false (★ 목록에 노출 안 됨)
  - description: "가족 전용 플랜"

**스키마 변경**: `subscription_plans`에 `max_capacity` 컬럼 추가 (integer, nullable, default null)
- null = 무제한, 숫자 = 해당 수만큼만 구독 가능

**각 플랜 features JSON**:
```json
// 무료
{
  "ai_generate": true,
  "max_contents_per_month": 5,
  "max_channels": 1,
  "allowed_channels": ["blog"],
  "ai_model_tier": "flash",
  "content_calendar": false,
  "insurance_tools": true,
  "ai_health_analysis": false,
  "ai_design_proposal": false,
  "digital_namecard": false,
  "keyword_tools_basic": false,
  "keyword_tools_full": false,
  "keyword_tools_algorithm": false,
  "knowhow_basic": false,
  "knowhow_premium": false,
  "knowhow_critical": false,
  "crm_access": false,
  "org_management": false,
  "agent_management": false,
  "calculator_pv": true,
  "calculator_savings": false
}

// 프로
{
  "ai_generate": true,
  "max_contents_per_month": 100,
  "max_channels": 5,
  "allowed_channels": ["blog", "instagram", "threads", "kakao", "script"],
  "ai_model_tier": "pro",
  "content_calendar": true,
  "insurance_tools": true,
  "ai_health_analysis": true,
  "ai_design_proposal": true,
  "digital_namecard": true,
  "keyword_tools_basic": true,
  "keyword_tools_full": false,
  "keyword_tools_algorithm": false,
  "knowhow_basic": true,
  "knowhow_premium": false,
  "knowhow_critical": false,
  "crm_access": true,
  "org_management": false,
  "agent_management": false,
  "calculator_pv": true,
  "calculator_savings": true
}

// 프리미엄 (100명 한정)
{
  "ai_generate": true,
  "max_contents_per_month": -1,
  "max_channels": 5,
  "allowed_channels": ["blog", "instagram", "threads", "kakao", "script"],
  "ai_model_tier": "top",
  "content_calendar": true,
  "insurance_tools": true,
  "ai_health_analysis": true,
  "ai_design_proposal": true,
  "digital_namecard": true,
  "keyword_tools_basic": true,
  "keyword_tools_full": true,
  "keyword_tools_algorithm": false,
  "knowhow_basic": true,
  "knowhow_premium": true,
  "knowhow_critical": false,
  "crm_access": true,
  "org_management": false,
  "agent_management": false,
  "calculator_pv": true,
  "calculator_savings": true
}

// 엔터프라이즈
{
  "ai_generate": true,
  "max_contents_per_month": -1,
  "max_channels": 5,
  "allowed_channels": ["blog", "instagram", "threads", "kakao", "script"],
  "ai_model_tier": "top",
  "content_calendar": true,
  "insurance_tools": true,
  "ai_health_analysis": true,
  "ai_design_proposal": true,
  "digital_namecard": true,
  "keyword_tools_basic": true,
  "keyword_tools_full": true,
  "keyword_tools_algorithm": false,
  "knowhow_basic": true,
  "knowhow_premium": true,
  "knowhow_critical": false,
  "crm_access": true,
  "org_management": true,
  "agent_management": true,
  "calculator_pv": true,
  "calculator_savings": true
}

// 히든 (가족)
{
  "ai_generate": true,
  "max_contents_per_month": -1,
  "max_channels": 5,
  "allowed_channels": ["blog", "instagram", "threads", "kakao", "script"],
  "ai_model_tier": "top",
  "content_calendar": true,
  "insurance_tools": true,
  "ai_health_analysis": true,
  "ai_design_proposal": true,
  "digital_namecard": true,
  "keyword_tools_basic": true,
  "keyword_tools_full": true,
  "keyword_tools_algorithm": true,
  "knowhow_basic": true,
  "knowhow_premium": true,
  "knowhow_critical": true,
  "crm_access": true,
  "org_management": true,
  "agent_management": true,
  "calculator_pv": true,
  "calculator_savings": true
}
```

### 2. feature_definitions 확장
**Supabase migration**에 추가 INSERT:

새 feature_definitions:
- `max_channels` (category: "channel", value_type: "number") — 최대 채널 수
- `allowed_channels` (category: "channel", value_type: "string") — 허용 채널 목록 (JSON array)
- `ai_model_tier` (category: "model", value_type: "string") — AI 모델 등급 (flash/pro/top)
- `keyword_tools_basic` (category: "menu", value_type: "boolean") — 기본 키워드 분석
- `keyword_tools_full` (category: "menu", value_type: "boolean") — 전체 키워드 분석
- `keyword_tools_algorithm` (category: "menu", value_type: "boolean") — 알고리즘 키워드 분석
- `knowhow_basic` (category: "menu", value_type: "boolean") — 기본 노하우
- `knowhow_premium` (category: "menu", value_type: "boolean") — 프리미엄 노하우
- `knowhow_critical` (category: "menu", value_type: "boolean") — 크리티컬 노하우
- `crm_access` (category: "menu", value_type: "boolean") — CRM 접근

기존 `keyword_tools` → `keyword_tools_basic`으로 rename (migration에서 UPDATE)

### 3. generate-content 함수에 플랜 검증 추가
**파일**: `supabase/functions/generate-content/index.ts`

**추가 로직** (콘텐츠 생성 전):
```typescript
// 1. 사용자 플랜 조회
const plan = await getUserPlan(userId); // org 우선, user 차선, free 폴백

// 2. 월간 사용량 체크
const monthKey = new Date().toISOString().slice(0, 7);
const { count } = await supabase
  .from("contents")
  .select("id", { count: "exact" })
  .eq("user_id", userId)
  .gte("created_at", `${monthKey}-01T00:00:00Z`);

const maxContents = plan.features.max_contents_per_month;
if (maxContents !== -1 && count >= maxContents) {
  return new Response(JSON.stringify({
    error: "monthly_limit_exceeded",
    message: "이번 달 콘텐츠 생성 한도를 초과했습니다.",
    current: count,
    max: maxContents
  }), { status: 429 });
}

// 3. 채널 제한 체크
const allowedChannels = plan.features.allowed_channels || ["blog"];
if (!allowedChannels.includes(requestedChannel)) {
  return new Response(JSON.stringify({
    error: "channel_not_allowed",
    message: "현재 플랜에서 지원하지 않는 채널입니다.",
    allowed: allowedChannels
  }), { status: 403 });
}

// 4. 모델 강제 매핑 (클라이언트 선택 무시)
const modelTier = plan.features.ai_model_tier || "flash";
const actualModel = MODEL_MAP[modelTier]; // flash→gemini-2.0-flash, pro→gemini-2.5-pro, top→claude-sonnet-4-6 등
```

**MODEL_MAP 정의**:
```typescript
const MODEL_MAP: Record<string, { provider: string; model: string }> = {
  flash: { provider: "gemini", model: "gemini-2.0-flash" },
  pro: { provider: "gemini", model: "gemini-2.5-pro" },
  top: { provider: "claude", model: "claude-sonnet-4-6" }
};
```

**중요**: 기존 ai_config 싱글톤은 폴백으로 유지. 플랜에 ai_model_tier가 있으면 우선 적용.

### 4. 100명 한정 프리미엄 — 구독 트랜잭션 잠금
**파일**: 구독 생성 관련 함수 또는 새 Edge Function

**로직**:
```sql
-- Supabase RPC function 생성
CREATE OR REPLACE FUNCTION subscribe_to_plan(
  p_user_id UUID,
  p_plan_id UUID
) RETURNS JSON AS $$
DECLARE
  v_plan subscription_plans%ROWTYPE;
  v_current_count INTEGER;
BEGIN
  -- 플랜 조회 (FOR UPDATE로 잠금)
  SELECT * INTO v_plan FROM subscription_plans WHERE id = p_plan_id FOR UPDATE;

  IF v_plan IS NULL THEN
    RETURN json_build_object('error', 'plan_not_found');
  END IF;

  -- max_capacity 체크
  IF v_plan.max_capacity IS NOT NULL THEN
    SELECT COUNT(*) INTO v_current_count
    FROM user_subscriptions
    WHERE plan_id = p_plan_id AND status = 'active';

    IF v_current_count >= v_plan.max_capacity THEN
      RETURN json_build_object('error', 'plan_full', 'current', v_current_count, 'max', v_plan.max_capacity);
    END IF;
  END IF;

  -- 기존 구독 취소
  UPDATE user_subscriptions SET status = 'cancelled'
  WHERE user_id = p_user_id AND status = 'active';

  -- 새 구독 생성
  INSERT INTO user_subscriptions (user_id, plan_id, status, started_at)
  VALUES (p_user_id, p_plan_id, 'active', NOW());

  RETURN json_build_object('success', true);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```

### 5. CRM 페이지 플랜 게이팅
**수정 파일들** (CRM 관련 페이지 전체):
- `src/pages/CRM.tsx` (또는 CRM 메인)
- `src/pages/CustomerDetail.tsx`
- 기타 `/crm/*` 경로 페이지들

**변경**: 각 CRM 페이지 컴포넌트를 PlanGuard로 감싸기
```tsx
import { PlanGuard } from "@/components/PlanGuard";

export default function CRMPage() {
  return (
    <PlanGuard requiredPlan="pro">
      {/* 기존 CRM 내용 */}
    </PlanGuard>
  );
}
```

**주의**: PlanGuard의 requiredPlan 체크 로직 확인 필요
- 현재: "premium" 체크만 존재 (isPremium = Premium || Enterprise)
- 추가: "pro" 체크 지원 (isPro = Pro || Premium || Enterprise || Hidden)
- `src/components/PlanGuard.tsx` 수정 필요

**PlanGuard 플랜 계층 구조**:
```
무료 < 프로 < 프리미엄 < 엔터프라이즈 = 히든
```
requiredPlan="pro" → 프로 이상이면 통과
requiredPlan="premium" → 프리미엄 이상이면 통과

## 파일 영향 범위
- 생성: Supabase migration 파일 (1~2개), Supabase RPC function
- 수정: `supabase/functions/generate-content/index.ts`, `src/components/PlanGuard.tsx`, CRM 페이지들
- 삭제: 없음

## 검증 기준
1. `npm run build` 성공
2. Supabase migration 적용 성공 (`supabase db push` 또는 로컬 검증)
3. subscription_plans에 5개 플랜 존재 확인 (SQL 쿼리)
4. feature_definitions에 새 카테고리 확인
5. generate-content: 사용량 초과 시 429 응답 확인 (테스트 코드)
6. CRM 페이지에 PlanGuard 적용 확인
7. pyright 타입 체크 에러 0건

## 프로젝트 경로
- `/home/jay/projects/InsuRo/`
- Supabase 프로젝트: 로컬 migrations 디렉토리 확인 후 작업
- Edge Functions: `supabase/functions/` 디렉토리
