🐯 VBT엔진 → SPX 체험 플로우

작성: 2026-03-01 황호랑 · 상태: 황성우님 리뷰 대기
v0.2.0
📋 PRD
🖼 와이어프레임
📅 구현 계획

1. 배경 & 문제

현재 상태 — 두 개의 분리된 시스템
[시뮬레이터 전용]                    [진짜 엔진 (미연결)]
VbtPhysicsEngine                    vbt_engine/
  → SimResult, SimRep                → VbtPipeline (LPF+속도)
  → WorkoutScreen 직접 소비            → RepSegmentationService
  → ReportScreen 직접 소비             → VbtFeedbackService
                                      → RepFeedback, SetReport, SessionReport
BleVbtBridge
  → BLE 데이터를 SimResult로 변환 ← 엔진 안 거침!

문제

  1. BLE 실데이터가 들어와도 진짜 엔진(파이프라인, 세그먼테이션, 피드백)을 안 쓴다
  2. UI가 SimResult/SimRep에 하드 커플링 → RepFeedback/SetReport와 호환 불가
  3. 리포트 화면이 시뮬레이터 전용 로직(grade, badType)에 의존
  4. 세션 리포트가 아예 없음

왜 지금 해야 하나

2. 목표

SPX 체험 플로우에서 진짜 VBT 엔진을 태워서, BLE 실데이터가 엔진을 거쳐 피드백까지 나오게 한다.

성공 기준

#기준측정
1BLE → VbtPipeline → RepSegmentation → VbtFeedback 풀체인시뮬레이션 모드 e2e 확인
2WorkoutScreen — RepFeedback 기반 실시간 피드백렙 완료 시 MCV, VLoss, 등급
3ReportScreen — SetReport 기반 세트 리포트피로곡선, L/R밸런스, AI코치
4SessionReport 신규 화면세트간 비교, 피로 프로파일
5Sim/BLE 동일 UI데이터 소스만 다르고 UI 동일

비목표

3. 아키텍처

목표 아키텍처
BLE → VbtPipeline → StreamingRepDetector → VbtFeedbackService ─┐
Sim → VbtPhysicsEngine → (어댑터) ──────────────────────────────┘
                                                                 ↓
                                              VbtWorkoutViewModel
                                              (RepFeedback stream)
                                                    ↓
                                              WorkoutScreen (렙 피드백 UI)
                                                    ↓ 세트 종료
                                              SetReport → ReportScreen
                                                    ↓ 세션 종료
                                              SessionReport → SessionReportScreen

A. VbtWorkoutViewModel (신규)

class VbtWorkoutViewModel extends ChangeNotifier {
  final VbtDataSource source;
  final VbtPipeline pipeline;
  final StreamingRepDetector repDetector;
  final VbtFeedbackService feedbackService;

  List<VbtSample> positionHistory;
  List<RepFeedback> completedReps;
  RepFeedback? latestRep;
  SetReport? currentSetReport;
  SessionReport? sessionReport;

  WorkoutPhase phase;
  int currentSet, currentRep;
}

B. VbtDataSource (인터페이스)

abstract class VbtDataSource {
  Stream<(int timeMs, double posL, double posR)> get samples;
  bool get isConnected;
  void start();
  void stop();
}

class BleVbtSource implements VbtDataSource { ... }
class SimVbtSource implements VbtDataSource { ... }

C. 기존 엔진 모듈 (전부 재사용)

모듈Lines상태역할
vbt_pipeline.dart142LPF + 속도 계산
rep_segmentation_service.dart808Post-hoc 렙 분할
streaming_rep_detector.dart417실시간 렙 감지
vbt_feedback_service.dart438피드백 생성
vbt_feedback_models.dart288데이터 모델
butterworth_lpf.dart87노이즈 필터
turning_point_detector_v3.dart165전환점 감지
load_velocity_profile.dart2521RM 추정

4. 미결 질문

Q1. 세트 수 사전 설정? — 체험 플로우에서 "3세트" 미리 정하나, 자유롭게 추가?
Q2. 세션 = 단일 운동? — SPX 체험은 한 운동만? 여러 운동?
Q3. 결과 저장 — 기존 spoex_results 유지? 새 스키마?
Q4. QR 등록 — 기존 QR 등록 웹 연동 유지?

체험 플로우

Exercise
Pick
Goal
Select
Grip
Select
Weight
Input ✨
Workout
Screen 🔄
Set Report 🔄
Session
Report ✨

✨ 신규 · 🔄 리팩터 · 무표시 기존 유지 · Set Report↔Workout 루프(다음 세트)

Workout Screen

┌─────────────────────────────────────────────────────────┐ │ Lat Pulldown · Set 2/3 · 60kg [일반|Pro] │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Position Chart (L/R) │ │ │ │ 실시간 position 그래프 (상: L, 하: R) │ │ │ │ 렙 구간 하이라이트 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 현재 렙 #5 │ │ CMV │ │ V-Loss │ │ │ │ │ │ 0.52 m/s │ │ -12% │ │ │ │ [GOOD] │ │ ↓ 이전 대비 │ │ 🟢 적정 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ ┌──── Pro 영역 (토글 시 표시) ─────────────────────┐ │ │ │ PCV: 0.68 │ Power: 245W │ ROM: 312mm │ │ │ │ L/R: 3.2% │ Intent: 0.85 │ Force: 580N │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 렙 히스토리 (미니 바 차트) │ │ │ │ ■ ■ ■ ■ ■ (CMV 바, 색=등급) │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ [ ■ 세트 종료 ] │ └─────────────────────────────────────────────────────────┘ 데이터: VbtWorkoutViewModel · latestRep → 현재 렙 카드 · completedReps → 렙 히스토리 · positionHistory → Position Chart

Set Report Screen

┌─────────────────────────────────────────────────────────┐ │ 세트 리포트 · Set 2 · Lat Pulldown 60kg │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │ │ Avg CMV │ │ V-Loss │ │ Reps │ │ Power │ │ │ │ 0.48 m/s │ │ 22% │ │ 10 │ │ 238W │ │ │ │ 🟢 적정 │ │ 🟡 목표근접│ │ │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 피로 곡선 (CMV Bar Chart) │ │ │ │ ███ ██▓ ██▓ ██░ ██░ █▓░ █░░ █░░ ░░░ ░░░ │ │ │ │ R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 │ │ │ │ ── 안정권 ────────── ── 피로권 ── │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ L/R 밸런스 │ │ │ │ ◀──── L 51% ════╪════ R 49% ────▶ │ │ │ │ 편차: 2.1% ✅ 정상 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌──── Pro 영역 ────────────────────────────────┐ │ │ │ Con/Ecc: 0.72 │ TUT: 42s │ │ │ │ ROM: 315±8mm │ Best Rep: #2 (0.55m/s) │ │ │ │ │ │ │ │ # │ CMV │ PCV │ ROM │ Pwr │ L/R │ VL% │ │ │ │ 1 │ 0.55 │ 0.71 │ 320 │ 262 │ 1.2 │ - │ │ │ │ 2 │ 0.54 │ 0.69 │ 318 │ 258 │ 2.1 │ -2% │ │ │ │ ⋮ │ │ │ │ │ │ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 🤖 AI 코치 │ │ │ │ "22% V-loss — 근비대 목표(25%) 범위 내. │ │ │ │ 다음 세트 60kg 유지 권장. │ │ │ │ R7~R10 ROM 12% 감소 → 가동범위 집중." │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ [ ◀ 홈 ] [ 📖 용어집 ] [ ▶ 다음 세트 ] │ │ or [ 📊 세션 리포트 ] │ └─────────────────────────────────────────────────────────┘ 데이터: SetReport · setMcv, totalVLoss, reps.length, meanPower · reps[].mcv → 피로곡선 · asymmetry → L/R · insights[] → AI 코치 · reps[] → Pro 테이블

Session Report Screen (신규)

┌─────────────────────────────────────────────────────────┐ │ 세션 리포트 · Lat Pulldown · 2026-03-01 │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │ │ 총 렙 │ │ 총 볼륨 │ │ 피로도 │ │ Best Set│ │ │ │ 30 │ │ 1,800kg │ │ 🟡 적정 │ │ Set 1 │ │ │ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 세트별 MCV 추이 (Line Chart) │ │ │ │ 0.52 ●─── │ │ │ │ 0.48 ───●─── │ │ │ │ 0.44 ───● │ │ │ │ Set1 Set2 Set3 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 세트 비교 테이블 │ │ │ │ Set │ Reps │ CMV │ VL% │ Power │ │ │ │ 1 │ 10 │ 0.52 │ 18% │ 252W │ │ │ │ 2 │ 10 │ 0.48 │ 22% │ 238W │ │ │ │ 3 │ 10 │ 0.44 │ 28% │ 220W │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 피로 프로파일 │ │ │ │ 세트 내 V-Loss: 18% → 22% → 28% (증가) │ │ │ │ 세트 간 MCV: -7.7% → -8.3% (균등 하락) │ │ │ │ 판정: 🟡 Moderate — 적절한 훈련 강도 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ ┌──── Pro 영역 ────────────────────────────────┐ │ │ │ L/R 밸런스 추이 │ │ │ │ Set1: 1.2% → Set2: 2.1% → Set3: 4.8% │ │ │ │ ⚠ 피로 시 좌우 편차 증가 │ │ │ │ 추정 1RM: 82kg (Epley) │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 🤖 AI 코치 (세션) │ │ │ │ "3세트 모두 10렙 완수, V-loss 증가 추세. │ │ │ │ Set3 L/R 편차 4.8% — 피로 시 좌측 약화. │ │ │ │ 다음 세션: 62.5kg 도전 가능, │ │ │ │ 3세트 유지 시 렙 8로 줄이기 권장." │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ [ ◀ 홈 ] [ 📤 결과 저장 ] [ 🔄 새 운동 ] │ └─────────────────────────────────────────────────────────┘ 데이터: SessionReport · totalReps, totalVolume, fatigueProfile, bestSet · sets[].setMcv → MCV 추이 · sets[] → 비교 테이블 · fatigueProfile → 피로 · insights[] → AI 코치

Weight Input Screen (신규)

┌─────────────────────────────────────────────────────────┐ │ 무게 설정 · Lat Pulldown │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌───────────┐ │ │ │ 60.0 │ kg │ │ └───────────┘ │ │ │ │ [ -5 ] [ -2.5 ] [ -1 ] [ +1 ] [ +2.5 ] [ +5 ] │ │ │ │ 최근 사용: 55kg · 60kg · 65kg │ │ │ │ 💡 무게 정보는 1RM 추정, 파워(W) 계산, │ │ 다음 세트 무게 추천에 사용됩니다. │ │ │ │ [ ▶ 운동 시작 ] │ └─────────────────────────────────────────────────────────┘

구현 계획

1

엔진 연결 (핵심) ~11h

#작업예상산출물
1-1VbtDataSource + SimVbtSource2h시뮬레이터 → 엔진 포맷
1-2BleVbtSource2hBLE → VbtPipeline 직결
1-3VbtWorkoutViewModel4h풀체인 VM
1-4⚡ 풀체인 검증 (Gate) — Sim 데이터로 Pipeline→Detector→Feedback e2e 돌려서 단위/타입/렙카운트 검증. 이거 통과 전 Phase 2 진입 금지.2h검증 리포트
1-5WeightInputScreen1h무게 입력
2

UI 리팩터 ~12h

#작업예상산출물
2-1WorkoutScreen → ViewModel4hSimResult 의존 제거
2-2SetReportScreen → SetReport4hSetReport 렌더링
2-3SessionReportScreen 신규4h세션 리포트
3

검증 & 폴리싱 ~6h

#작업예상산출물
3-1시뮬레이션 e2e 검증2h풀플로우 테스트
3-2일반/Pro 토글2hUX 스펙 반영
3-3Supabase 저장 업데이트2h새 모델 저장

총 ~29h (3~4일)

파일 구조

lib/features/
├── vbt_engine/                          # 기존 유지
│   ├── vbt_pipeline.dart
│   ├── rep_segmentation_service.dart
│   ├── streaming_rep_detector.dart
│   ├── vbt_feedback_service.dart / models.dart
│   └── ...├── vbt_common/                          # ✨ 신규
│   ├── data/
│   │   ├── vbt_data_source.dart
│   │   ├── sim_vbt_source.dart
│   │   └── ble_vbt_source.dart
│   ├── viewmodel/
│   │   └── vbt_workout_viewmodel.dart
│   └── widgets/
│       ├── fatigue_curve_chart.dart
│       ├── lr_balance_bar.dart
│       ├── rep_feedback_card.dart
│       ├── set_summary_cards.dart
│       ├── session_mcv_chart.dart
│       └── ai_coach_card.dart├── spoex_kiosk/presentation/screens/vbt_demo/
│   ├── vbt_workout_screen.dart       # 🔄
│   ├── vbt_report_screen.dart        # 🔄
│   ├── vbt_session_report_screen.dart # ✨
│   └── vbt_weight_input_screen.dart   # ✨

리스크 & 주의사항

🔴 핵심 리스크 (Phase 1에서 조기 발견 필수)

#리스크영향대응
1풀체인 단위/타입 불일치 — Pipeline→Detector→FeedbackService 간 mm vs m, ms vs s 등피드백 수치 엉망Phase 1-3에서 시뮬레이터 데이터로 풀체인 먼저 돌려서 검증. UI 작업 전에 반드시 통과.
2Streaming vs Post-hoc 렙 불일치 — 운동 중 "5렙" → 리포트에서 "4렙"사용자 혼란Streaming=실시간 피드백용, Post-hoc=리포트용으로 역할 분리. 차이 발생 시 리포트에 "보정됨" 표시. 실데이터 검증 때 튜닝 포인트.
3SimVbtSource 어댑터 품질 — 시뮬레이터 position 생성 방식과 BLE raw encoder 값의 단위/스케일 차이시뮬에서 OK → 실기기에서 실패어댑터에서 단위 맞추고, "시뮬에서 통과 ≠ 실기기 보장" 인지. 실기기 검증은 별도 Phase.

🟡 관리 가능한 리스크

#리스크영향대응
4WorkoutScreen 1130줄 리팩터 regression기존 기능 깨짐한번에 갈아엎지 않음. ViewModel 병렬 구동 → 점진적 전환. 기존 SimResult 경로 유지하면서 새 경로 추가 → 검증 후 구 경로 제거.
5Supabase 스키마 호환저장 깨짐마이그레이션 or 별도 테이블
6렙 세그먼테이션 파라미터 (실기기 전)분할 부정확시뮬레이터 검증 → 실기기 튜닝

구현 전략: 점진적 전환

왜 빅뱅 리팩터가 아닌가

WorkoutScreen(1130줄)과 ReportScreen(1087줄)을 한번에 갈아엎으면 regression 리스크가 크다. 대신:

  1. Phase 1: ViewModel + DataSource를 독립적으로 만들고, 시뮬레이터 데이터로 풀체인 검증 (UI 무관)
  2. Phase 2: 기존 SimResult 경로를 유지하면서 ViewModel 경로를 병렬 추가. 토글로 전환 가능하게.
  3. 검증 후: 새 경로가 안정되면 SimResult 경로 제거

이렇게 하면 어느 시점에서든 구버전으로 롤백 가능.