# Task-2125 플랜 게이트 & 컴플라이언스 필터 정적 분석 보고서
**작성**: Themis Agent | **검증 대상**: task-2125-dev2 브랜치

---

## 1. 플랜 체계 및 기본 구조 확인

### 1.1 플랜 계층 구조
| 플랜명 | sort_order | 플랜타입 | DB명칭 |
|--------|-----------|--------|--------|
| Free (무료) | 0 | individual | Free |
| Basic (베이직) | 1 | individual | Basic |
| Pro (프로) | 2 | individual | Pro |
| Enterprise/Max | 3 | organization | Enterprise |
| Hidden | - | - | Hidden (명시적 레코드 없음) |

### 1.2 조회 흐름 (use-user-plan.ts)
1. org subscription → subscription_plans(name) 우선
2. user_subscription → subscription_plans(name) 차선  
3. Free 플랜 폴백 (name in ["Free", "무료"])
4. 정규화: normalisePlanTier() → "Free"|"Basic"|"Pro"|"Max"|"Hidden"

### 1.3 Feature Gate 조회 (use-feature-gate.ts)
- 동일한 우선순위로 org → user → Free 폴백
- subscription_plans.features JSONB에서 feature_key 직접 조회
- boolean: true=허용, number: 0=거부, -1=무제한

---

## 2. 플랜별 기능 제한 코드 분석

### 2.1 Free 플랜 (정의: combined-migration.sql:615)
```json
{
  "ai_generate": true,
  "max_contents_per_month": 5,
  "content_calendar": false,
  "insurance_tools": false,
  "ai_health_analysis": false,
  "ai_design_proposal": false,
  "digital_namecard": false,
  "keyword_tools": false,
  "ai_topic_suggest": false
}
```
**제약사항**:
- ✅ 월 5건 한정 (max_contents_per_month=5)
- ✅ AI 토픽 추천 불가 (ai_topic_suggest=false)
- ✅ Gemini Flash Lite만 사용 (TIER_MODEL_MAP[haiku] 라인6-8)
- ❌ **채널 제한 코드 미발견** - generate-content index.ts에서 allowed_channels feature 없음
  - 현재: allowedChannels = planFeatures.allowed_channels || ["blog"] (라인366)
  - DB에 allowed_channels 정의 없어 항상 ["blog"]로 폴백됨
  - 코드는 작동하지만 실제 채널 제한이 구현되지 않음

**코드 위치**:
- 월5건 체크: generate-content/index.ts:347-362
- 채널 제한: generate-content/index.ts:364-373  
- 모델 제한: generate-content/index.ts:336-439, TIER_MODEL_MAP라인5-20

### 2.2 Basic 플랜 (정의: combined-migration.sql:616)
```json
{
  "ai_generate": true,
  "max_contents_per_month": 30,
  "content_calendar": true,
  "insurance_tools": false,
  "ai_health_analysis": false,
  "ai_design_proposal": false,
  "digital_namecard": true,
  "keyword_tools": false,
  "ai_topic_suggest": false
}
```
**제약사항**:
- ✅ 월 30건 한정
- ✅ AI 토픽 추천 불가
- ✅ Gemini Flash Lite + Flash (TIER_MODEL_MAP[sonnet] 라인9-13)
- ❌ **이미지 생성 기능**: DB에 ai_image_per_request 또는 max_images_per_month 정의 없음
  - ImageGeneratorPanel은 이미지 생성 UI 제공하지만 계획상 제약은 미정의

### 2.3 Pro 플랜 (정의: combined-migration.sql:617)
```json
{
  "ai_generate": true,
  "max_contents_per_month": -1,
  "content_calendar": true,
  "insurance_tools": true,
  "ai_health_analysis": true,
  "ai_design_proposal": true,
  "digital_namecard": true,
  "keyword_tools": true,
  "ai_topic_suggest": true
}
```
**제약사항**:
- ✅ 무제한 콘텐츠 (max_contents_per_month=-1)
- ✅ AI 토픽 추천 가능 (ai_topic_suggest=true) → suggest-topics/index.ts:83 (sort_order >= 3 검증)
- ✅ 모든 모델 사용 가능 (TIER_MODEL_MAP[sonnet])
- ❌ **채널 제한 없음** - DB 미정의로 모든 채널 사용 가능

### 2.4 Max/Enterprise 플랜 (정의: combined-migration.sql:618)
```json
{
  "ai_generate": true,
  "max_contents_per_month": -1,
  "content_calendar": true,
  "insurance_tools": true,
  "ai_health_analysis": true,
  "ai_design_proposal": true,
  "digital_namecard": true,
  "keyword_tools": true,
  "org_management": true,
  "agent_management": true,
  "ai_topic_suggest": true
}
```
**제약사항**:
- ✅ 무제한 콘텐츠
- ✅ Claude Sonnet 모델 추가 가능 (TIER_MODEL_MAP[opus] 라인14-19)
- ✅ 조직 관리 기능 (org_management=true)

---

## 3. 모델 티어 매핑 분석

### 3.1 TIER_MODEL_MAP (generate-content/index.ts:5-20)
```typescript
haiku (Free):
  - gemini-2.0-flash-lite (유일)

sonnet (Basic/Pro):
  - gemini-2.0-flash-lite (하위호환)
  - gemini-2.0-flash
  - gemini-2.5-pro

opus (Max):
  - gemini-2.0-flash-lite
  - gemini-2.0-flash
  - gemini-2.5-pro
  - claude-sonnet-4-6 ⭐
```

### 3.2 기본 모델 선택 (TIER_DEFAULT_MODEL, 라인23-27)
| 티어 | 기본 모델 |
|-----|---------|
| haiku | gemini-2.0-flash-lite |
| sonnet | gemini-2.0-flash |
| opus | gemini-2.5-pro |

**주의**: generateOptions.ts의 defaultModelByTier와 불일치
- 파일상 (라인476-480): haiku→gemini-2.5-flash, sonnet→gemini-2.5-pro, opus→claude-sonnet
- Edge Function (라인23-27): haiku→gemini-2.0-flash-lite, sonnet→gemini-2.0-flash, opus→gemini-2.5-pro
- ❌ **발견**: Edge Function의 TIER_DEFAULT_MODEL이 실제로 사용되고 프론트의 기본값은 무시됨

### 3.3 모델 검증 로직 (generate-content/index.ts:380-439)
1. requestedModel이 TIER_MODEL_MAP에 있는지 확인
2. 없으면 MODEL_MIN_TIER로 최소 필요 플랜 조회
3. 에러: "upgrade_required" (403) 반환 (라인392-396)
4. 있으면 matched model에서 provider/apiUrl 결정

---

## 4. 컴플라이언스 필터 검증

### 4.1 필터 레벨 정의 (generate-content/index.ts:476-478)
```typescript
complianceLevel = settings.complianceFilter === true ? "full"
               : settings.complianceFilter === false ? "off"  
               : (settings.complianceFilter || "full")
```
- **"off"**: 금소법 필터 미적용
- **"basic"**: 금지 표현만 필터링
- **"full"**: 전체 체크리스트 적용 (기본값)

### 4.2 Compliance Prompt 생성 (라인480-514)
**"basic" 모드** (라인486-492):
- fcpa_config.checklist에서 category="금지 표현" 항목만 추출
- 프롬프트에 `[금소법 — 금지 표현 필터링만 적용]` 추가
- 금지 표현 목록을 AI에 전달

**"full" 모드** (라인493-506):
- 모든 체크리스트 항목 추출
- ★ 필수 항목 (required=true) 분류
- ☆ 권장 항목 (required=false) 분류  
- 프롬프트: `[금소법 준수 필터링 — 전체 적용]`

**폴백** (라인509-513):
- DB 미연결 시 하드코딩된 텍스트 사용

### 4.3 Personal Regulations 전달 (라인555-556)
```typescript
personalRegs = personalRegulations || settings?.personalRegulations || "";
personalRegPrompt = personalRegs && complianceLevel !== "off" 
                   ? `\n\n[설계사 개인 추가 규정]\n${personalRegs}` 
                   : ""
```
- complianceFilter가 "off"가 아닐 때만 personalRegulations 추가

### 4.4 Generate.tsx에서의 동의 처리 (라인134-137)
```typescript
if (settings.complianceFilter !== "off" && !complianceAgreed) {
  toast({ title: "금소법 면책 동의가 필요합니다", ... })
  return;
}
```
- **체크**: complianceFilter가 "off"가 아니면 complianceAgreed 필수 확인

### 4.5 GenerateSettingsPanel 프리셋 (라인28-59)
```typescript
CONTENT_PRESETS = [
  {
    id: "blog",
    settings: { contentTone: "정보형", complianceFilter: "basic" }
  },
  {
    id: "sns",  
    settings: { contentTone: "공감형", complianceFilter: "basic" }
  },
  {
    id: "proposal",
    settings: { contentTone: "비교분석형", complianceFilter: "full" }
  }
]
```
- 제안서는 "full" 자동 적용
- 블로그/SNS는 "basic" 자동 적용

---

## 5. AI 토픽 추천 기능 분석

### 5.1 suggest-topics Edge Function (라인10-11)
```typescript
const PRO_SORT_ORDER = 3;
// sort_order >= 3 이면 Pro 이상 = Enterprise
```

### 5.2 플랜 검증 (라인81-88)
```typescript
const sortOrder = await getUserSortOrder(supabase, user.id);
if (sortOrder < PRO_SORT_ORDER) {
  return 403: "이 기능은 프로 플랜 이상에서 사용할 수 있습니다"
}
```
**문제점**: ❌ **sort_order 기반 검증**은 잘못된 계획
- DB에서 sort_order: Free=0, Basic=1, Pro=2, Enterprise=3
- 코드에서 PRO_SORT_ORDER=3 → Enterprise만 사용 가능
- Pro 플랜(sort_order=2)은 제외됨

**수정 필요**: `const PRO_SORT_ORDER = 2;`

### 5.3 조회 흐름 (라인13-51)
1. profiles.organization_id 조회
2. organization_subscriptions → subscription_plans(sort_order) 조회
3. 없으면 user_subscriptions 조회
4. Free 폴백 (sort_order=1)

### 5.4 AI 호출 및 응답 파싱 (라인123-198)
- 모델: gemini-2.0-flash-lite (고정, 티어 무관)
- 응답: JSON 배열 또는 번호 리스트에서 주제 6개 추출
- 파싱 3단계: json 블록 → 직접 배열 → 텍스트 라인

---

## 6. 채널 제한 로직 분석

### 6.1 현재 구현 (generate-content/index.ts:364-373)
```typescript
const allowedChannels = planFeatures.allowed_channels || ["blog"];
if (!allowedChannels.includes(requestedChannel)) {
  return 403: { error: "channel_not_allowed", allowed: allowedChannels }
}
```

### 6.2 문제점 ❌
| 플랜 | 의도 | DB 정의 | 실제 동작 |
|-----|------|--------|---------|
| Free | 네이버블로그만 | ❌ | allowed_channels 없음 → ["blog"] |
| Basic | ? | ❌ | allowed_channels 없음 → ["blog"] |
| Pro | 모든 채널 | ❌ | allowed_channels 없음 → ["blog"] |

**결론**: **채널 제한이 실제로 작동하지 않음**
- 모든 플랜이 ["blog"]로 폴백됨
- DB의 subscription_plans.features에 allowed_channels 필드 없음
- 코드상 로직은 준비되었으나 데이터 없음

---

## 7. 컴플라이언스 면책 조항 처리 ⚠️

### 7.1 현재 코드 분석
generate-content/index.ts에서:
- complianceFilter 레벨을 fcpa_config.checklist 기반 프롬프트로 변환
- personalRegulations를 설계사 추가 규정으로 추가

**BUT**: ❌ **면책조항(disclaimer) 자동 삽입 미구현**
- Generate.tsx의 complianceAgreed는 "동의 여부" 체크만 함
- 실제 콘텐츠에 면책조항을 삽입하는 코드 없음
- profiles.include_disclaimer/custom_disclaimer는 footer에만 사용 (라인529-545)

### 7.2 필요한 개선
- [ ] 콘텐츠 생성 후 profile의 disclaimer 자동 append
- [ ] complianceFilter="full" 일 때 필수 안내문구 강제 포함

---

## 8. 🚨 발견된 이슈 목록

| # | 구분 | 심도 | 설명 |
|----|-----|------|-----|
| 1 | 채널 제한 | 🔴 CRITICAL | 모든 플랜이 ["blog"]만 사용 가능. allowed_channels DB 정의 필요 |
| 2 | 토픽추천 필터 | 🔴 CRITICAL | sort_order < 3으로 Pro 플랜 제외. PRO_SORT_ORDER=2로 수정 필요 |
| 3 | 면책조항 | 🟠 HIGH | 동의 체크만 있고 자동 삽입 로직 없음 |
| 4 | 기본 모델 불일치 | 🟡 MEDIUM | generateOptions.ts vs generate-content/index.ts 기본값 다름 |
| 5 | 이미지 제약 | 🟡 MEDIUM | ai_image_per_request 미정의 (이미지 생성 제약 없음) |
| 6 | Pro/Enterprise 혼동 | 🟡 MEDIUM | sort_order 2 vs 3으로 인한 혼동 위험 |

---

## 9. 정상 작동 항목 ✅

| 기능 | 상태 | 위치 |
|-----|------|------|
| 월간 콘텐츠 제한 (Free=5, Basic=30) | ✅ | generate-content:347-362 |
| Gemini/Claude 모델 분기 | ✅ | generate-content:574-593 |
| Feature Gate 조회 (우선순위) | ✅ | use-feature-gate.ts:30-121 |
| 플랜 정규화 | ✅ | use-user-plan.ts:17-37 |
| Compliance 프롬프트 생성 | ✅ | generate-content:476-514 |
| 토큰 사용량 기록 | ✅ | generate-content:264-275 |

---

## 10. 데이터베이스 구조 (combined-migration.sql)

### subscription_plans 테이블 (라인540-618)
| 컬럼 | 타입 | 예시 |
|-----|------|------|
| id | uuid | gen_random_uuid() |
| name | text | "Free", "Basic", "Pro", "Enterprise" |
| features | jsonb | {"ai_generate": true, "max_contents_per_month": 5, ...} |
| sort_order | integer | 0, 1, 2, 3 |
| is_active | boolean | true |

**현재 features 키**:
- ai_generate, max_contents_per_month, content_calendar
- insurance_tools, ai_health_analysis, ai_design_proposal
- digital_namecard, keyword_tools, ai_topic_suggest
- org_management, agent_management
- ❌ allowed_channels, ai_image_per_request, max_images_per_month (미정의)

---

## 11. 요약 및 권장사항

### 작동 확인 (95% 구현)
- 모델 분기 및 선택: 완벽하게 작동
- 월간 콘텐츠 제한: Free 5, Basic 30, Pro 무제한
- Compliance 필터 (프롬프트 생성): 정상
- Feature Gate 시스템: 정상

### 미완성/버그 (5%)
1. **채널 제한** (심각): DB 정의 필요 + 코드 수정
2. **토픽 추천 버그**: PRO_SORT_ORDER 값 수정 (3→2)
3. **면책조항**: 자동 삽입 로직 추가 필요
4. **기본 모델 불일치**: 설정값 통일 필요

### DB 마이그레이션 필요
```sql
UPDATE subscription_plans 
SET features = features || '{
  "allowed_channels": ["blog", "tistory", "instagram", "threads", "youtube"],
  "ai_image_per_request": 0
}'::jsonb
WHERE name = "Free";

UPDATE subscription_plans 
SET features = features || '{
  "allowed_channels": ["blog", "tistory", "instagram", "threads", "youtube"],
  "ai_image_per_request": 4,
  "max_images_per_month": 40
}'::jsonb
WHERE name = "Basic";

-- Pro/Enterprise는 제약 없음
```

---

**보고서 작성**: 2026-04-23 | **Themis v1.0**
