# 저장 모델 — Supabase 스키마

> **DB는 정답지가 아니라 서비스의 일부**. 컬럼 하나가 어떤 도메인 개념을 담고 있는지가 핵심.
> 마이그레이션 파일과 1:1 비교하며 읽을 것.

## 마이그레이션 순서

1. `supabase/migrations/20260413114331_create_workout_schema.sql` — 1차 (sessions/entries/sets/reps/rom/raw_samples/centers)
2. `supabase/migrations/20260415195832_add_vbt_analytics.sql` — 2차 (BP 메트릭 + Hooper + LVP + exercise_metadata + rep_detection_run)

## 테이블 분류

```
운동 데이터       사용자 입력         도메인 파라미터       QA / 운영
────────────     ──────────────     ─────────────────    ──────────
workout_sessions daily_wellness     exercise_metadata    rep_detection_run
exercise_entries                                          centers
exercise_sets                                             center_memberships
reps                                                      raw_motion_samples (P2)
rom_estimates
load_velocity_profile
```

## 1. workout_sessions (1세션 = 1행)

| 컬럼 | 타입 | 의미 |
|---|---|---|
| `id` | uuid PK | |
| `user_id` | uuid → auth.users | |
| `device_id` | text | BLE peripheral ID |
| `firmware_version` | text | MCU 버전 (분석 segment용) |
| `started_at`, `ended_at` | timestamptz | |
| `rating`, `notes` | smallint, text | (UI 미구현, 컬럼만) |
| **BP 추가**: | | |
| `duration_min` | int | sRPE 계산용 |
| `session_rpe` | smallint (1-10) | Foster |
| `mood` | text enum | great/good/ok/tired/bad |
| `srpe_load` | int **GENERATED** | `session_rpe × duration_min` |
| `total_volume_load_kg` | real | Σ exercise_sets.volume_load_kg |
| `total_work_kj` | real | Σ reps.work_J / 1000 |
| `mean_vl_pct` | real | session 내 set들의 평균 VL% |
| `nm_readiness` | real | 첫 warmup MCV / 7일 baseline |

## 2. exercise_entries (종목당 1행)

| 컬럼 | 타입 | 의미 |
|---|---|---|
| `id`, `session_id` | uuid | |
| `exercise_id` | text | 앱 번들 운동 ID (e.g. `barbell-bench-press`) |
| `grip_id` | text | 그립 변형 |
| `is_unilateral` | bool | 한쪽씩 운동 여부 |
| `entry_order` | smallint | 세션 내 순서 (0-based) |
| `weight_mode` | enum | normal/eccentric/elastic/isokinetic/hydraulic/vibration |
| `training_goal` | enum | power/strength/hypertrophy/endurance — VL threshold 결정 |

## 3. exercise_sets (세트당 1행)

| 컬럼 | 타입 | 의미 |
|---|---|---|
| `id`, `entry_id` | uuid | |
| `side` | enum | left/right |
| `set_index` | smallint | entry 내 순서 |
| `started_at`, `ended_at` | timestamptz | |
| `rep_count`, `weight_kg`, `weight_mode` | | |
| `avg_mcv_mmps`, `peak_velocity_mmps`, `velocity_loss_pct`, `total_tut_ms` | real / int | `ExerciseSet` 집계 |
| `rom_min_mm`, `rom_max_mm`, `rom_confidence` | real | RomEstimate snapshot |
| `memo` | text | 사용자 메모 |
| **BP 추가**: | | |
| `rpe`, `rir` | smallint | 사용자 입력 (post-set sheet) |
| `rest_before_ms` | int | 직전 set 종료 ↔ 이 set 시작 (현재 미기록) |
| `volume_load_kg` | real | `weight × reps` (자동) |
| `estimated_1rm_kg` | real | LVP 또는 single-rep e1RM (현재 미기록) |
| `target_rep_count` | smallint | 사용자 계획 rep (QA용, 미사용) |
| `cutoff_trigger` | enum | user / velocity_loss_N / target_reached / timeout |
| `warmup_detected` | bool | LVP 재계산 대상 (현재 미기록) |

## 4. reps (rep당 1행 — 세션당 ~120행)

기존 30개 컬럼 + BP 6개 신규.

### 기본 (기존)
| 컬럼 | 타입 | 의미 |
|---|---|---|
| `id`, `set_id` | uuid | |
| `side`, `rep_index` | | |
| `started_at`, `ended_at` | timestamptz | |
| `weight_mode`, `weight_kg` | | |
| `duration_ms`, `concentric_ms`, `eccentric_ms`, `turnaround_ms` | int | timing |
| `rom_mm`, `concentric_rom_mm`, `eccentric_rom_mm` | real | mm |
| `mean_conc_velocity, peak_conc_velocity, mean_ecc_velocity, peak_ecc_velocity` | real | mm/s, NULL when 비-VBT 모드 |
| `mean_conc_force, peak_conc_force, mean_ecc_force, peak_ecc_force` | real | kg |
| `mean_conc_power, peak_conc_power` | real | W |
| `tut_ms`, `load_weighted_tut` | int, real | TUT |
| `mean_conc_icmd_ma, peak_conc_icmd_ma, region_transitions, icmd_fload_error_pct` | | Roomfit 고유 |
| `valley_position_mm, peak_position_mm` | real | rep 양 끝 |

### BP 추가 (마이그레이션 2)
| 컬럼 | 타입 | 의미 |
|---|---|---|
| `mean_prop_velocity` | real | MPV (mm/s) |
| `concentric_work_cj` | real | **centijoule** = J × 100 |
| `eccentric_work_cj` | real | |
| `concentric_impulse` | real | N·s |
| `rfd_initial_100ms` | real | N/s |
| `icmd_ifb_rms_error_ma` | real | 모터 컨트롤 품질 |

## 5. rom_estimates (종목 × side)

| 컬럼 | 의미 |
|---|---|
| `entry_id`, `side` (PK) | |
| `position_min_mm`, `position_max_mm`, `rom_mm` | mm |
| `rep_count`, `confidence` | 학습 안정도 |

## 6. raw_motion_samples (P2 — **현재 write 코드 없음**)

스키마는 있고 컬럼 정의됨 (50ms × 세션 시간 × 2 side).
앱이 채우려면:
- `RecordedSession`을 항상 켜고 → 세션 종료 시 batch upsert
- 또는 Tier-2 Storage parquet로 빼는 게 권장 (TimescaleDB 대안 BP)

자세한 스토리지 BP는 BP 리서치 리포트 참고.

## 7. daily_wellness (Hooper)

| 컬럼 | 타입 | 의미 |
|---|---|---|
| `user_id, date` (PK) | | upsert key |
| `hooper_sleep, hooper_fatigue, hooper_soreness, hooper_stress` | smallint 1-7 | |
| `hooper_total` | smallint **GENERATED** | `fatigue + soreness + stress + (8 − sleep)` |
| `note` | text | |

## 8. load_velocity_profile

세션마다 새 row (immutable snapshot).

| 컬럼 | 타입 | 의미 |
|---|---|---|
| `id`, `user_id`, `exercise_id`, `side` | | |
| `slope`, `intercept`, `v0_mps`, `l0_kg` | real | 회귀 결과 |
| `mvt_used` | real | 1RM 추정에 사용한 MVT |
| `estimated_1rm_kg` | real | `(mvt − intercept) / slope` |
| `r_squared` | real | 적합도 |
| `n_data_points` | smallint | warmup set 수 |
| `method` | text | `2pt / multipoint / ml` |
| `velocity_metric` | text | 보통 `mcv` |
| `computed_at` | timestamptz | |
| `session_id` | uuid | |
| `drift_from_previous` | real | 직전 LVP 대비 slope 변화율 |

## 9. exercise_metadata (도메인 상수 시드)

종목별 MVT + 종목 간 1RM transfer.

| 컬럼 | 의미 |
|---|---|
| `exercise_id` (PK) | |
| `display_name` | |
| `mechanics` | compound / isolation |
| `force_type` | push / pull / static / hinge |
| `default_mvt_mps` | 종목별 MVT (bench 0.17 등) |
| `mcv_at_1rm_mps` | 보통 `default_mvt_mps`와 동일 |
| `transfer_parent_id` | "이 종목 1RM ≈ parent 1RM × coef" |
| `transfer_coefficient` | 0.5-1.0 |

**Seed**: 5개 주요 compound (bench / squat / deadlift / OHP / row) 마이그레이션에 INSERT.

## 10. rep_detection_run (QA / A/B)

| 컬럼 | 의미 |
|---|---|
| `id`, `set_id` | |
| `detector_version` | e.g. `six-state-fsm-v2.3` |
| `detector_config` | jsonb |
| `detected_rep_count`, `detected_reps` | int + jsonb |
| `target_rep_count` | 사용자 의도 |
| `user_confirmed_count` | post-set UI 응답 |
| `ran_at` | timestamptz |

→ shadow-mode A/B 인프라 준비. 현재 write 코드 없음 (P2).

## 11. centers / center_memberships (B2B용)

체육관 단위 운영. B2B 앱에서 사용.
- `centers` — 체육관 정보
- `center_memberships` — 사용자 ↔ 체육관 매핑 (역할 포함)

## Materialized Views

### `user_daily_load`

```sql
SELECT user_id,
       day,
       SUM(srpe_load),
       SUM(total_volume_load_kg),
       SUM(total_work_kj),
       AVG(mean_vl_pct),
       COUNT(*) as session_count
FROM workout_sessions
GROUP BY user_id, started_at::date;
```

ACWR EWMA 계산의 입력. **nightly refresh 권장** (Edge Function, 현재 미자동화).

## Generated 컬럼

Postgres `GENERATED ALWAYS AS ... STORED` 활용 컬럼:

- `workout_sessions.srpe_load` = `session_rpe × duration_min`
- `daily_wellness.hooper_total` = `fatigue + soreness + stress + (8 − sleep)`

→ **앱이 계산해서 보낼 필요 없음**. INSERT 시 자동.

## RLS — Row Level Security

모든 사용자별 테이블에서 `user_id = auth.uid()` 정책.

| 테이블 | 정책 |
|---|---|
| `workout_sessions` | own row only |
| `exercise_entries` | via session.user_id |
| `exercise_sets`, `reps`, `rom_estimates` | via entry chain |
| `daily_wellness` | own row only |
| `load_velocity_profile` | own row only |
| `exercise_metadata` | authenticated read only (모두 읽기 가능, 쓰기 X) |
| `rep_detection_run` | via set chain |

## Indexes

성능에 중요한 인덱스:
- `idx_sessions_user (user_id, started_at DESC)` — 사용자 최근 세션
- `idx_entries_session`, `idx_sets_entry`, `idx_reps_set` — JOIN 체인
- `idx_lvp_user_exercise_time` — 사용자 종목별 LVP 시계열
- `idx_wellness_user_date` — 사용자 wellness 트렌드
- `idx_sets_warmup` partial — `WHERE warmup_detected`만

## 단위 변환 표 (도메인 ↔ DB)

| 도메인 모델 | DB 컬럼 | 변환 |
|---|---|---|
| `concentricWorkJ` (Joules) | `concentric_work_cj` (centijoule) | × 100 |
| `concentricImpulseNs` (N·s) | `concentric_impulse` (N·s) | 그대로 |
| `rfdInitial100msNps` (N/s) | `rfd_initial_100ms` (N/s) | 그대로 |
| `meanPropulsiveVelocityMmps` (mm/s) | `mean_prop_velocity` (mm/s) | 그대로 |
| `volumeLoadKg` (kg) | `volume_load_kg` (kg) | 그대로 |

## 마이그레이션 적용 상태

⚠️ **로컬 Supabase 환경 또는 staging에 `supabase db push` 안 함**.
마이그레이션 파일만 git에 있고 실제 DB엔 미적용. 적용 전엔 신규 컬럼/테이블 부재.

## 관련 파일

- 마이그레이션: `supabase/migrations/`
- 리포지토리: `lib/features/workout/data/`, `lib/features/wellness/data/`
- 도메인 모델: `lib/features/workout/domain/models/`, `packages/roomfit_exercise/`
