# INFINITH SaaS Database Schema V3

**작성일**: 2026-04-05
**마이그레이션 파일**: `supabase/migrations/20260405_saas_schema_v3.sql`

## 설계 원칙

1. **CLINIC-CENTRIC**: 병원 1개 = 1행. URL이 달라도 같은 병원이면 같은 행
2. **TIME-SERIES**: 채널 메트릭은 INSERT-only 스냅샷 (시계열 쿼리)
3. **SEPARATION**: 원시 데이터 / 분석 리포트 / 콘텐츠 전략 분리
4. **LOOP-READY**: 매 분석이 이전 데이터를 참조해 전략 자동 조정
5. **MULTI-TENANT**: user_id 기반 접근 제어 (미래 auth)

---

## ERD (Entity Relationship)

```
clinics (병원 마스터)
 ├─ analysis_runs (분석 실행 히스토리) ← 매주 1행씩 쌓임
 │    ├─ channel_snapshots (채널별 시계열 메트릭) ← INSERT-only
 │    ├─ screenshots (스크린샷 증거)
 │    └─ performance_metrics (성과 메트릭)
 │         └─ strategy_adjustments (전략 조정 근거)
 ├─ channel_configs (사용자 수동 채널 연결)
 ├─ content_plans (콘텐츠 기획 — 활성 1개)
 │    └─ content_performance (개별 콘텐츠 성과)
 └─ (marketing_reports) ← 레거시 호환
```

---

## 테이블 상세

### 1. `clinics` — 병원 마스터
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| user_id | UUID | 소유자 (미래 auth) |
| url | TEXT UNIQUE | 대표 URL |
| name | TEXT | 한국어 병원명 |
| name_en | TEXT | 영문 병원명 |
| domain | TEXT | 도메인 |
| address, phone | TEXT | 기본 정보 |
| established_year | INT | 개원 연도 |
| services | TEXT[] | 시술 목록 |
| branding | JSONB | 컬러, 폰트, 로고, 태그라인 |
| social_handles | JSONB | 검증된 소셜 핸들 |
| verified_channels | JSONB | Phase 1 결과 캐시 |
| analysis_frequency | TEXT | 'manual' / 'daily' / 'weekly' / 'monthly' |
| last_analyzed_at | TIMESTAMPTZ | 마지막 분석 시간 |

### 2. `analysis_runs` — 분석 실행 히스토리
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| clinic_id | UUID FK → clinics | |
| status | TEXT | pending/discovering/collecting/generating/complete/partial/error |
| scrape_data | JSONB | Firecrawl 원시 데이터 |
| raw_channel_data | JSONB | API 수집 원시 데이터 |
| analysis_data | JSONB | 시장 분석 |
| vision_analysis | JSONB | Vision 분석 결과 |
| report | JSONB | AI 리포트 |
| channel_errors | JSONB | 채널별 에러 기록 |
| trigger | TEXT | 'manual' / 'scheduled' / 'webhook' |

### 3. `channel_snapshots` — 채널별 시계열 ⭐ 핵심
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| clinic_id | UUID FK → clinics | |
| run_id | UUID FK → analysis_runs | |
| channel | TEXT | youtube/instagram/facebook/gangnamUnni/... |
| handle | TEXT | @handle 또는 URL |
| followers | INT | 구독자/팔로워 |
| posts | INT | 게시물/영상 수 |
| total_views | BIGINT | 총 조회수 |
| rating | NUMERIC(3,1) | 평점 |
| reviews | INT | 리뷰 수 |
| health_score | INT | 0-100 |
| details | JSONB | 상세 (top videos, latest posts 등) |
| screenshot_url | TEXT | 채널 랜딩 스크린샷 |
| captured_at | TIMESTAMPTZ | 캡처 시간 |

**핵심**: INSERT-only. 절대 UPDATE 하지 않음. `captured_at` 기준 시계열 쿼리.

### 4. `screenshots` — 스크린샷 증거
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| clinic_id, run_id | UUID FK | |
| channel | TEXT | 어떤 채널 |
| page_type | TEXT | main/doctors/surgery/landing |
| url | TEXT | 이미지 URL (Supabase Storage) |
| source_url | TEXT | 원본 페이지 URL |
| vision_data | JSONB | Gemini Vision 추출 데이터 |

### 5. `content_plans` — 콘텐츠 기획
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| clinic_id | UUID FK → clinics | |
| run_id | UUID FK → analysis_runs | 어떤 분석 기반 |
| brand_guide | JSONB | 브랜딩 가이드 |
| channel_strategies | JSONB | 채널별 전략 |
| content_strategy | JSONB | 필라, 타입, 워크플로우 |
| calendar | JSONB | 4주 캘린더 |
| is_active | BOOLEAN | 현재 활성 기획 |

### 6. `channel_configs` — 사용자 수동 채널 연결
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| clinic_id | UUID FK | |
| channel | TEXT | 채널 종류 |
| handle | TEXT | @handle |
| is_verified | BOOLEAN | 사용자가 직접 확인 |
| access_token | TEXT | OAuth token (미래) |

### 7. `performance_metrics` — 성과 메트릭 (루프 핵심)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| clinic_id, run_id | UUID FK | |
| prev_run_id | UUID FK | 이전 분석 (비교 기준) |
| channel_deltas | JSONB | 채널별 변화량 |
| kpi_progress | JSONB | KPI 달성률 |
| top_performing_content | JSONB | 성과 좋은 콘텐츠 |
| underperforming_channels | TEXT[] | 성과 미달 채널 |
| strategy_suggestions | JSONB | AI 전략 조정 제안 |

### 8. `content_performance` — 개별 콘텐츠 성과
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| clinic_id | UUID FK | |
| plan_id | UUID FK | 어떤 기획의 콘텐츠 |
| channel, content_type | TEXT | 채널 + 유형 |
| views, likes, comments, shares | INT | 반응 지표 |
| engagement_rate | NUMERIC | 참여율 |
| performance_score | INT | 0-100 |

### 9. `strategy_adjustments` — 전략 조정 히스토리
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | UUID PK | |
| clinic_id, plan_id | UUID FK | |
| performance_id | UUID FK | 어떤 성과 분석 기반 |
| adjustment_type | TEXT | frequency_change/pillar_shift/channel_add/... |
| description | TEXT | "YouTube Shorts 주 3회 → 5회로 증가" |
| reason | TEXT | "Shorts 조회수가 Long-form 대비 300% 높음" |
| before_value, after_value | JSONB | 변경 전/후 값 |

---

## Views (트렌드 쿼리용)

### `channel_latest`
각 채널의 최신 스냅샷만 반환. `DISTINCT ON` + `ORDER BY captured_at DESC`.

### `channel_weekly_delta`
현재 vs 7일 전 스냅샷을 `LATERAL JOIN`으로 비교. 팔로워 변화율(%) 자동 계산.

---

## 성과 → 기획 루프 플로우

```
Week N:
  1. analysis_run 생성
  2. channel_snapshots INSERT (현재 메트릭)
  3. performance_metrics 생성 (이전 run과 비교)
     → channel_deltas: {youtube: +2%, instagram: +7.1%}
     → kpi_progress: [{metric: "YouTube 구독자", progress: 97.4%}]
     → strategy_suggestions: ["Instagram Reels 빈도 증가 권장"]
  4. strategy_adjustments INSERT (조정 내역)
  5. content_plans UPDATE (조정 반영)
     → Content Director가 strategy_suggestions를 기반으로 캘린더 자동 조정
  6. report 생성 (이전 분석 대비 변화 포함)

Week N+1:
  → 2번부터 반복. channel_snapshots에 데이터가 쌓이면서 트렌드 정확도 ↑
```

---

## 기존 호환성

| 기존 테이블 | 상태 | 이관 계획 |
|-----------|------|---------|
| `marketing_reports` | 유지 (deprecated) | 기존 API 호환 유지, 새 데이터는 새 테이블에만 저장 |
| `scrape_results` | 유지 | 캐시용 |

---

## 구현 체크리스트

### Phase 1: DB 마이그레이션 (테이블 생성)

- [x] Supabase Dashboard에서 `20260405_saas_schema_v3.sql` 실행
- [x] 테이블 9개 생성 확인: clinics, analysis_runs, channel_snapshots, screenshots, content_plans, channel_configs, performance_metrics, content_performance, strategy_adjustments
- [x] View 2개 생성 확인: channel_latest, channel_weekly_delta
- [x] RLS 정책 적용 확인
- [x] 인덱스 생성 확인

### Phase 2: Edge Function 전환 — discover-channels

- [x] `discover-channels/index.ts`: `clinics` 테이블에 UPSERT (url 기준)
- [x] `discover-channels/index.ts`: `analysis_runs` 테이블에 INSERT (status: 'discovering')
- [x] `discover-channels/index.ts`: `clinic.verified_channels` 업데이트
- [x] 기존 `marketing_reports`에도 병행 쓰기 유지 (호환성)
- [ ] 검증: 분석 실행 → clinics + analysis_runs 행 생성 확인

### Phase 3: Edge Function 전환 — collect-channel-data

- [x] `collect-channel-data/index.ts`: `channel_snapshots`에 채널별 INSERT
- [x] `collect-channel-data/index.ts`: `screenshots`에 스크린샷 INSERT
- [x] `collect-channel-data/index.ts`: `analysis_runs.raw_channel_data` 업데이트
- [x] `collect-channel-data/index.ts`: `analysis_runs.vision_analysis` 업데이트
- [x] 기존 `marketing_reports.channel_data`에도 병행 쓰기 유지
- [ ] 검증: channel_snapshots에 채널별 행 생성 확인

### Phase 4: Edge Function 전환 — generate-report

- [x] `generate-report/index.ts`: `analysis_runs.report` 업데이트
- [x] `generate-report/index.ts`: `analysis_runs.status = 'complete'`
- [x] `generate-report/index.ts`: `clinics.last_analyzed_at` 업데이트
- [ ] `content_plans` 자동 생성 (transformPlan 로직 서버사이드 이동)
- [x] 기존 `marketing_reports.report`에도 병행 쓰기 유지
- [ ] 검증: analysis_runs.status = 'complete' + report JSONB 생성 확인

### Phase 5: 성과 분석 루프 구현

- [ ] 반복 분석 시 `performance_metrics` 자동 생성 (이전 run 대비)
- [ ] `channel_weekly_delta` View 쿼리 테스트
- [ ] `strategy_suggestions` 생성 로직 (Content Director 연동)
- [ ] `content_plans` 자동 업데이트 (전략 조정 반영)
- [ ] `strategy_adjustments` 히스토리 기록
- [ ] 검증: 2회 연속 분석 → performance_metrics에 변화량 기록 확인

### Phase 6: Frontend 전환

- [ ] `useReport` 훅: analysis_runs + channel_snapshots에서 읽기
- [ ] `useMarketingPlan` 훅: content_plans에서 읽기
- [ ] ReportPage: channel_snapshots 시계열 데이터로 트렌드 차트 추가
- [ ] KPIDashboard: performance_metrics의 달성률 표시
- [ ] 검증: 기존 리포트 URL이 새 테이블에서도 정상 렌더링

### Phase 7: 스케줄링

- [ ] `clinics.analysis_frequency` 기반 자동 분석 트리거
- [ ] Supabase Cron 또는 external scheduler 연동
- [ ] `analysis_runs.trigger = 'scheduled'` 기록
- [ ] 검증: 주간 자동 분석 실행 확인

### Phase 8: 기존 데이터 이관

- [ ] `marketing_reports` → `clinics` + `analysis_runs` 이관 스크립트
- [ ] `marketing_reports.channel_data` → `channel_snapshots` 변환
- [ ] `marketing_reports.report` → `analysis_runs.report` 복사
- [ ] 이관 후 기존 리포트 URL 정상 작동 확인
- [ ] `marketing_reports` deprecated 마킹
