1. 배경 & 문제
현재 상태 — 두 개의 분리된 시스템
[시뮬레이터 전용] [진짜 엔진 (미연결)]
VbtPhysicsEngine vbt_engine/
→ SimResult, SimRep → VbtPipeline (LPF+속도)
→ WorkoutScreen 직접 소비 → RepSegmentationService
→ ReportScreen 직접 소비 → VbtFeedbackService
→ RepFeedback, SetReport, SessionReport
BleVbtBridge
→ BLE 데이터를 SimResult로 변환 ← 엔진 안 거침!
문제
- BLE 실데이터가 들어와도 진짜 엔진(파이프라인, 세그먼테이션, 피드백)을 안 쓴다
- UI가
SimResult/SimRep에 하드 커플링 →RepFeedback/SetReport와 호환 불가 - 리포트 화면이 시뮬레이터 전용 로직(grade, badType)에 의존
- 세션 리포트가 아예 없음
왜 지금 해야 하나
- VBT 엔진 코어 80~85% 완성 (실데이터 튜닝만 남음)
- 실기기 검증 다다음주 예정 → 그 전에 파이프라인 연결 완료 필요
- SPOEX 이후 일반 앱에도 동일 구조 재사용
2. 목표
SPX 체험 플로우에서 진짜 VBT 엔진을 태워서, BLE 실데이터가 엔진을 거쳐 피드백까지 나오게 한다.
성공 기준
| # | 기준 | 측정 |
|---|---|---|
| 1 | BLE → VbtPipeline → RepSegmentation → VbtFeedback 풀체인 | 시뮬레이션 모드 e2e 확인 |
| 2 | WorkoutScreen — RepFeedback 기반 실시간 피드백 | 렙 완료 시 MCV, VLoss, 등급 |
| 3 | ReportScreen — SetReport 기반 세트 리포트 | 피로곡선, L/R밸런스, AI코치 |
| 4 | SessionReport 신규 화면 | 세트간 비교, 피로 프로파일 |
| 5 | Sim/BLE 동일 UI | 데이터 소스만 다르고 UI 동일 |
비목표
- 장기 트렌드 (DB 저장 구조 별도)
- 카메라(MediaPipe) 연동
- 일반 앱 플로우 (SPX 키오스크만)
- 실기기 튜닝 (다다음주 별도)
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.dart | 142 | ✅ | LPF + 속도 계산 |
rep_segmentation_service.dart | 808 | ✅ | Post-hoc 렙 분할 |
streaming_rep_detector.dart | 417 | ✅ | 실시간 렙 감지 |
vbt_feedback_service.dart | 438 | ✅ | 피드백 생성 |
vbt_feedback_models.dart | 288 | ✅ | 데이터 모델 |
butterworth_lpf.dart | 87 | ✅ | 노이즈 필터 |
turning_point_detector_v3.dart | 165 | ✅ | 전환점 감지 |
load_velocity_profile.dart | 252 | ✅ | 1RM 추정 |
4. 미결 질문
✅ 확정 (2026-03-01)
Q1. 세트 수: 사전 설정 없음. 1세트 끝 → "리포트 보기" or "한 세트 더" 선택. 세션 리포트는 "운동 끝내기" 시.
Q2. 운동: 1개 운동 플로우. 다른 운동 = 처음부터 다시.
Q3. Supabase: 새 스키마 (구 spoex_results 미사용)
Q4. QR: 새로 구축
체험 플로우
Exercise
Pick
→
Pick
Goal
Select
→
Select
Grip
Select
→
Select
Weight
Input ✨
→
Input ✨
Workout
Screen 🔄
→
Screen 🔄
Set Report 🔄
→
Session
Report ✨
Report ✨
✨ 신규 · 🔄 리팩터 · 무표시 기존 유지
Set Report에서 "한 세트 더" → Workout 루프 / "운동 끝내기" → Session Report
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% 감소 → 가동범위 집중." │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [ ◀ 홈 ] [ 📖 용어집 ] [ 💪 한 세트 더 ] [ 📊 운동 끝내기 ]│
└─────────────────────────────────────────────────────────┘
데이터: 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-1 | VbtDataSource + SimVbtSource | 2h | 시뮬레이터 → 엔진 포맷 |
| 1-2 | BleVbtSource | 2h | BLE → VbtPipeline 직결 |
| 1-3 | VbtWorkoutViewModel | 4h | 풀체인 VM |
| 1-4 | ⚡ 풀체인 검증 (Gate) — Sim 데이터로 Pipeline→Detector→Feedback e2e 돌려서 단위/타입/렙카운트 검증. 이거 통과 전 Phase 2 진입 금지. | 2h | 검증 리포트 |
| 1-5 | WeightInputScreen | 1h | 무게 입력 |
2
UI 리팩터 ~12h
| # | 작업 | 예상 | 산출물 |
|---|---|---|---|
| 2-1 | WorkoutScreen → ViewModel | 4h | SimResult 의존 제거 |
| 2-2 | SetReportScreen → SetReport | 4h | SetReport 렌더링 |
| 2-3 | SessionReportScreen 신규 | 4h | 세션 리포트 |
3
검증 & 폴리싱 ~6h
| # | 작업 | 예상 | 산출물 |
|---|---|---|---|
| 3-1 | 시뮬레이션 e2e 검증 | 2h | 풀플로우 테스트 |
| 3-2 | 일반/Pro 토글 | 2h | UX 스펙 반영 |
| 3-3 | Supabase 저장 업데이트 | 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 작업 전에 반드시 통과. |
| 2 | Streaming vs Post-hoc 렙 불일치 — 운동 중 "5렙" → 리포트에서 "4렙" | 사용자 혼란 | Streaming=실시간 피드백용, Post-hoc=리포트용으로 역할 분리. 차이 발생 시 리포트에 "보정됨" 표시. 실데이터 검증 때 튜닝 포인트. |
| 3 | SimVbtSource 어댑터 품질 — 시뮬레이터 position 생성 방식과 BLE raw encoder 값의 단위/스케일 차이 | 시뮬에서 OK → 실기기에서 실패 | 어댑터에서 단위 맞추고, "시뮬에서 통과 ≠ 실기기 보장" 인지. 실기기 검증은 별도 Phase. |
🟡 관리 가능한 리스크
| # | 리스크 | 영향 | 대응 |
|---|---|---|---|
| 4 | WorkoutScreen 1130줄 리팩터 regression | 기존 기능 깨짐 | 한번에 갈아엎지 않음. ViewModel 병렬 구동 → 점진적 전환. 기존 SimResult 경로 유지하면서 새 경로 추가 → 검증 후 구 경로 제거. |
| 5 | Supabase 스키마 호환 | 저장 깨짐 | 마이그레이션 or 별도 테이블 |
| 6 | 렙 세그먼테이션 파라미터 (실기기 전) | 분할 부정확 | 시뮬레이터 검증 → 실기기 튜닝 |
구현 전략: 점진적 전환
왜 빅뱅 리팩터가 아닌가
WorkoutScreen(1130줄)과 ReportScreen(1087줄)을 한번에 갈아엎으면 regression 리스크가 크다. 대신:
- Phase 1: ViewModel + DataSource를 독립적으로 만들고, 시뮬레이터 데이터로 풀체인 검증 (UI 무관)
- Phase 2: 기존 SimResult 경로를 유지하면서 ViewModel 경로를 병렬 추가. 토글로 전환 가능하게.
- 검증 후: 새 경로가 안정되면 SimResult 경로 제거
이렇게 하면 어느 시점에서든 구버전으로 롤백 가능.