# 데이터 계층 — MCU 50ms부터 Session까지

> **한 줄 요약**: `MotionSample → Rep → Set → Entry → Session` 5단계, 위로 갈수록 집계되고 row가 줄어든다.

## 5계층 한눈에

```
WorkoutSession              1개  (오늘 운동 세션 전체)
  ├─ ExerciseEntry          5개  (벤치, 스쿼트, ... 종목별)
  │   └─ ExerciseSet        15개 (종목당 3-5세트)
  │       └─ CompletedRep   120개 (세트당 8-12rep)
  │           └─ MotionSample 7,200개 (rep segmentation 동안만 메모리 유지)
  └─ RomEstimate            5개  (종목별 ROM 학습값)
```

위 숫자는 "5종목 × 3세트 × 8rep × 4초/rep" 가정의 평균 세션.

## 각 계층

### 1. MotionSample (raw)

**무엇**: MCU가 50ms 주기로 보내는 36바이트 dev report.
**위치**: `packages/roomfit_exercise/lib/src/sample/motion_sample.dart`
**필드**: 좌우 position/speed/accel/icmd/ifb/fLoad + 공통 voltage/weightMode/region/weightSet/driveStatus.
**저장**: rep segmentation 동안만 메모리에 유지. 완료 후 해제.

**선택적 영속**: `RecordedSession` (로컬 JSON) 또는 `raw_motion_samples` 테이블.
- 로컬 JSON은 `SessionRecorder` 켰을 때만.
- DB 테이블은 **스키마는 있지만 실제 write 코드 없음** (P2 미구현).

### 2. SideSample

**무엇**: `MotionSample`을 좌·우로 쪼갠 view.
**위치**: `packages/roomfit_exercise/lib/src/sample/side_sample.dart`
**왜**: rep detection 알고리즘이 side-agnostic이라 한쪽씩 처리.

`MotionSample` 1개 → `SideSample` 2개 (left + right).

### 3. RepSegment / RepSegmentSnapshot

**무엇**: 한 rep을 phase별로 쪼갠 sample 모음 (`concentric / peakHold / eccentric / valleyHold`).
**위치**: `packages/roomfit_exercise/lib/src/segmentation/rep_segment.dart`
**언제 만들어지나**: `SixStateFsmRepDetector`가 rep 시작/완료를 감지하면 buffer를 snapshot으로 잘라 `RepCompleted` 이벤트와 함께 emit.
**저장**: 메모리 only. `CompletedRep` 계산 후 폐기.

### 4. CompletedRep

**무엇**: 한 rep의 최종 메트릭 번들.
**위치**: `packages/roomfit_exercise/lib/src/metrics/completed_rep.dart`
**계산**: `StandardVbtMetricsCalculator.calculate(segment)`.
**저장**: ✅ Supabase `reps` 테이블 1:1.

**필드**:
- Context: `side, repIndex, startTimeMs, endTimeMs, weightMode, weightSetKg`
- Timing: `repDurationMs, concentricTimeMs, eccentricTimeMs, turnaroundTimeMs`
- ROM: `romMm, concentricRomMm, eccentricRomMm`
- Velocity (mm/s): `mean/peakConcentric, mean/peakEccentric, meanPropulsive`
- Force (kg): `mean/peakConcentric, mean/peakEccentric`
- Power (W): `mean/peakConcentric`
- TUT: `timeUnderTensionMs, loadWeightedTutKgMs`
- Roomfit 고유: `mean/peakConcentricIcmdMa, regionTransitions, icmdFLoadErrorPct, icmdIfbRmsErrorMa`
- BP advanced: `concentricWorkJ, eccentricWorkJ, concentricImpulseNs, rfdInitial100msNps`

자세한 메트릭 정의는 [vbt-metrics.md](vbt-metrics.md).

### 5. ExerciseSet (set-level 집계)

**무엇**: 한 세트의 모든 rep을 모아 set-level 메트릭 집계.
**위치**: `packages/roomfit_exercise/lib/src/analysis/exercise_set.dart`
**계산**: rep들을 받아 `averageMeanConcentricVelocity`, `velocityLossPercent`, `totalTimeUnderTension` 계산.
**저장**: ✅ Supabase `exercise_sets` 테이블.

### 6. SetResult (앱 레이어 wrapper)

**무엇**: `ExerciseSet` + 사용자 입력(RPE/RIR) + 세션 메타.
**위치**: `lib/features/workout/domain/models/set_state.dart`
**왜 따로**: pure Dart `ExerciseSet`은 metrics만, app layer는 RPE/RIR/cutoff_trigger/volume_load 등 사용자/UI 컨텍스트가 필요.

**추가 필드**:
- 사용자 입력: `rpe, rir, memo`
- 자동 계산: `volumeLoadKg (= weightKg × repCount), cutoffTrigger`
- 메타: `restBeforeMs, targetRepCount, warmupDetected, estimated1rmKg`
- 참조: `romEstimate, exerciseSet (rep 집계 view)`

### 7. ExerciseEntryState (종목)

**무엇**: 한 운동 세션 내의 한 종목.
**위치**: `lib/features/workout/domain/models/exercise_entry_state.dart`
**저장**: ✅ Supabase `exercise_entries`.

**필드**: `entryId, exerciseId, gripId, isUnilateral, entryOrder, weightMode, trainingGoal, completedSets[]`.

`trainingGoal` (`power/strength/hypertrophy/endurance`)이 VBT velocity-loss threshold 결정에 쓰임.

### 8. WorkoutSessionState (세션)

**무엇**: 한 번의 운동 세션 전체.
**위치**: `lib/features/workout/domain/models/workout_session_state.dart`
**저장**: ✅ Supabase `workout_sessions`.

**필드**:
- 메타: `sessionId, phase, startedAt, endedAt, deviceId, firmwareVersion`
- 종목 배열: `entries[], currentEntryIndex`
- 종료 시 자동 집계: `durationMin, totalVolumeLoadKg, totalWorkKj, meanVlPct, nmReadiness`
- 사용자 입력: `sessionRpe, mood`

세션 종료 시 `SessionAggregator.compute()`가 모든 set의 raw를 합산.

### 9. RomEstimate (종목별 ROM)

**무엇**: AutoRomLearner가 학습한 종목별 Range-of-Motion.
**위치**: `packages/roomfit_exercise/lib/src/calibration/rom_estimate.dart`
**저장**: ✅ Supabase `rom_estimates` (종목 × side).

`positionMinMm, positionMaxMm, romMm, repCount, confidence`.

## 데이터 흐름 (live → DB)

```
MCU 36B report (50ms)
  │
  ▼
MotionSample (parsed)
  │
  ├─→ left  → SideSample(L) ─┐
  └─→ right → SideSample(R) ─┤
                              ▼
                         FrameCodec ingests → MotionStream
                              ▼
                  SixStateFsmRepDetector (per side)
                              │
                              ▼  RepCompleted(RepSegmentSnapshot)
                  StandardVbtMetricsCalculator
                              ▼
                       CompletedRep
                              ▼
                    ActiveSetState.reps[]
                              │
                              ▼  세트 완료 (SetExecutor.completeSet)
                       SetResult (volume_load 자동)
                              │
                              ▼  PostSetFeedbackSheet (RPE/RIR 캡처)
                       SetResult.copyWith(rpe, rir)
                              ▼
                  SessionManager.recordSetResult
                              ├─→ in-memory state
                              └─→ Repository.saveSetResult
                                       ├─→ exercise_sets insert
                                       └─→ reps batch insert
                  ⋯ (다음 세트 / 다음 종목 반복) ⋯

세션 종료:
                  SessionFeedbackSheet (sessionRpe, mood)
                              ▼
                  SessionManager.completeSession(sessionRpe, mood)
                              ▼
                  SessionAggregator.compute (volume/work/vl 합산)
                              ▼
                  Repository.finalizeSession
                              ▼  workout_sessions UPDATE
```

## 버려지는 것들

| 데이터 | 어디서 버려지나 | 복구 가능? |
|---|---|---|
| `RepPhase.idle` 샘플 | `RepSegment` 만들 때 phase별로만 보관 | ❌ |
| 원시 `MotionSample` 시퀀스 | `CompletedRep` 계산 후 메모리 해제 | ⚠️ `RecordedSession` 켜면 로컬 JSON에 저장 |
| Eccentric phase metric (특정 모드) | `velocityMeaningful = false`면 `null` | ❌ 물리적으로 의미 없음 |
| Drop set 중간 weight 변화 이력 | 현재 추적 안 됨 | ❌ |

## 1세션 = DB 152행

| 테이블 | 1세션당 row |
|---|---:|
| `workout_sessions` | 1 |
| `exercise_entries` | 5 (종목) |
| `exercise_sets` | 15 (종목 × 3세트) |
| `reps` | 120 (5×3×8) |
| `rom_estimates` | 5 |
| `daily_wellness` | 1 (오늘 Hooper) |
| `load_velocity_profile` | 5 (종목별 LVP) |
| **합계** | **~152행** |

상세 컬럼은 [storage-model.md](storage-model.md) 참조.

## 관련 코드

- 모델: `lib/features/workout/domain/models/`
- pure Dart 메트릭: `packages/roomfit_exercise/lib/src/metrics/`, `analysis/`, `segmentation/`
- 리포지토리: `lib/features/workout/data/`
- UI 흐름: `lib/features/workout/presentation/screens/workout_flow_screen.dart`
