# CLAUDE.md — RoomfitV2

## 물리 모델 (MANDATORY — 코드 작성 전 반드시 이해할 것)

Roomfit은 **전류 제어 기반 디지털 웨이트 머신**이다. 물리적 추(weight plate)가 없다.
모터에 흐르는 전류(Iq)가 토크를 만들고, 그 토크가 케이블을 통해 사용자에게 **저항**으로 전달된다.
즉 `전류 = 토크 = 사용자가 느끼는 무게`이다.

### 핵심 인과 관계

```
사용자가 케이블을 당김
  → 엔코더가 position 변화 감지
  → MCU가 (모드 + region + 설정무게)에 따라 전류 명령(icmd) 계산
  → 모터가 icmd만큼 전류를 흘려 토크(저항) 생성
  → ifb로 실제 전류 측정 (icmd와 차이 = 제어 오차)
  → fLoad = icmd를 kg으로 역산한 값 (사용자 체감 무게)
```

**icmd는 디버깅용이 아니라 이 기구의 핵심이다.** fLoad는 icmd의 파생값이다.

### 운동 역학 (Kinematics)

position → speed(미분) → accel(2차 미분). MCU가 50ms 주기로 계산.
- **speed 양수**: concentric phase (당기는 중, 근육 수축)
- **speed 음수**: eccentric phase (놓는 중, 근육 이완)
- **accel**: 폭발력 지표. power = fLoad × speed

### 무게 모드별 토크 곡선 (`WP_WeightMode` — MCU 실제 enum)

MCU 펌웨어는 6개의 저항 모드를 지원한다 (소스: `docs/reference/mcu-source/Core/Inc/WESPION_Def.h`,
런타임 파라미터는 `WESPION_App.c`):

| 코드 | 모드 | 물리적 동작 | 핵심 파라미터 |
|---|---|---|---|
| 0 | **Normal** (`constant`) | 일정 저항. position/speed 무관 동일 `icmd` | `WeightSet[L/R]` |
| 1 | **Eccentric** (`negative`) | concentric은 정상, eccentric에서 `F_EccSet`까지 force 가산. 사용자가 놓을 때 더 무거움 | `F_EccSet`, `EccSpdUp`, `EccSpdDown`, `F_EccLimit` |
| 2 | **Elastic** (`squeeze`) | position이 커질수록 force 점증 (고무밴드 모방) | 내부 lookup |
| 3 | **Isokinetic** | PI 컨트롤러가 cable speed를 `Iso.Vtarget`에 클램프. 빨리 당기면 저항 ↑. 하강 시 `DecayRate`로 감쇠, `Fmin` 미만으로 안 떨어짐 | `Iso.Vtarget`, `Iso.Kp`, `Iso.Ki`, `Iso.DecayRate`, `Iso.Fmin`, `Iso.Fmax` |
| 4 | **Hydraulic** | `F = baseF × (speed/Vref)^n`, `[baseF×minRatio, Vmax]`에 클램프. 속도-파워 법칙. **VBT MCV 무효** | `Hydro.Vref`, `Hydro.Vmax`, `Hydro.n`, `Hydro.minRatio` |
| 5 | **Vibration** (`Vib`) | 기본 force에 `AmplitudeKg × sin(2π × Freq × t)` 진동 추가. `MaxRatio`로 진폭 상한 | `Vib.Freq`, `Vib.AmplitudeKg`, `Vib.MaxRatio` |

VBT 메트릭 관점:
- **Normal / Eccentric / Elastic** — MCV/peak velocity/power가 사용자 effort의 직접 지표 (`WeightMode.velocityMeaningful = true`)
- **Isokinetic / Hydraulic / Vibration** — 속도가 컨트롤러나 모드 정의에 의해 클램프/왜곡되어 MCV는 의미 없음. `StandardVbtMetricsCalculator`가 자동으로 velocity/power 메트릭을 null 처리한다.

### VBT 지표 계산 출처 (MCU vs 앱)

MCU가 50ms마다 position/speed/accel/fLoad를 모두 계산해서 보낸다. 앱은 **원칙적으로 MCU 값 그대로 사용**, **평균 concentric 속도(MCV) 하나만 position에서 재계산** (T-Force gold standard, 샘플링 지터 면역).

| 지표 | 출처 |
|---|---|
| ROM, MCV, MPV displacement, Work (Δs) | MCU position |
| Peak velocity, Instantaneous power | MCU speed 직접 |
| MPV propulsive 판정 (accel > −g) | MCU accel 직접 |
| Force / Work / Impulse / RFD / TUT | MCU fLoad 직접 |
| **Mean concentric velocity (MCV)** | **앱 재계산** — `\|Δposition\| / Δtime` |

Mapper (`motion_sample_mapper.dart`)는 순수 passthrough. 모든 VBT 연산은 `StandardVbtMetricsCalculator` 한 곳에 집중. 상세는 [`docs/reference/vbt-metrics-data-flow.md`](docs/reference/vbt-metrics-data-flow.md).

### Region (포지션 구간) — `WP_RegionStatus`

MCU가 cable position을 8단계 zone으로 분류 (`WESPION_Def.h:122-132`):

| 코드 | 이름 | 의미 |
|---|---|---|
| -1 | `block` | 안전 차단 / 범위 무효. rep 카운트 금지 |
| 0 | `ground` | 케이블 완전 수축, 기계적 바닥 |
| 1 | `base` | 베이스 휴식 위치 (사용자 대기) |
| 2 | `idle` | base와 ROM 사이 자유 영역. rep 시작 전 |
| 3 | `loSoft` | ROM 저단 soft entry. force ramp-in |
| 4 | `rom` | 사용자가 calibrate한 Range-of-Motion. **실제 운동 구간** |
| 5 | `hiSoft` | ROM 상단 soft exit. force ramp-out |
| 6 | `over` | ROM 초과. 안전 이벤트 |

Rep detection은 `idle → rom` 전이를 rep 시작으로, `rom → idle` 하강 전이를 완료로 잡으면 가장 정확하다.

### Drive Status — `WP_DriveStatus`

부팅 → 캘리브레이션 → 운동 모드의 단계 (`WESPION_Def.h:46-52`):

| 코드 | 이름 | 의미 |
|---|---|---|
| 0 | `debug` | 개발 상태, 모터 게이트 off |
| 1 | `encoderInit` | 엔코더 초기화 진행 중. **position 신뢰 X** |
| 2 | `calibration` | 위치 캘리브레이션 진행 중. **position settling 중** |
| 3 | `runGym` | 정상 운동 모드. position/speed/force가 valid한 **유일한** 상태 |

Rep detection은 `runGym`일 때만 sample을 feed해야 한다. `SixStateFsmRepDetector`는 `SideSample.driveStatus.isWorkoutReady`를 체크해서 다른 상태에서는 무조건 무시한다.

### 전압과 안전

voltage는 DC 전원부 전압. 모터가 큰 전류를 소모하면 순간 전압 강하가 발생한다.
정상 범위 이탈 시 FailSafeError가 발생하며 기구가 비상 정지한다.

### 좌/우 독립

모든 물리량이 L/R 독립이다. 좌우 모터는 별도로 제어되며, 무게/모드/속도가 각각 다를 수 있다.

## Architecture Rules (MANDATORY)

### Monorepo Layout

RoomfitV2는 **pub workspaces 기반 듀얼 앱 모노레포**다 (Dart 3.5+, Flutter 3.24+ 네이티브).
루트 `pubspec.yaml` 이 workspace coordinator, 각 앱/패키지가 member. 상세는 [`docs/b2b/monorepo-structure.md`](docs/b2b/monorepo-structure.md).

```
RoomfitV2/
├── apps/
│   ├── b2c/                 # roomfit_v2 — Member 앱 (self 세션, B2C 기존 경로)
│   │   ├── lib/
│   │   ├── test/
│   │   ├── integration_test/
│   │   ├── ios/ android/
│   │   └── pubspec.yaml     # name: roomfit_v2, resolution: workspace
│   └── b2b/                 # roomfit_studio — Owner/Trainer 앱 (shop 운영 + supervised)
│       └── pubspec.yaml     # name: roomfit_studio, resolution: workspace
├── packages/
│   ├── roomfit_protocol/    # Pure Dart — packet codec, Command/Response, Transport interface
│   ├── roomfit_device/      # Pure Dart — DeviceGateway, DeviceState (freezed), BleLogger contract
│   ├── roomfit_ble/         # Flutter package — BLE scanner + BleTransport (flutter_blue_plus)
│   ├── roomfit_exercise/    # Pure Dart — rep segmentation + VBT metrics
│   ├── roomfit_firmware/    # Pure Dart — YModem + IAP + FirmwareUpdateSession
│   ├── roomfit_shop/        # Pure Dart — Center/Membership/Role + ShopRepository interface (B2B 공유 도메인)
│   └── design_system/       # Flutter — 토큰/컴포넌트
└── pubspec.yaml             # 루트 workspace coordinator (name: roomfit_workspace)
```

각 앱 내부 계층:

```
apps/{b2c,b2b}/lib/
├── core/                    # Composition root (logger, providers wiring)
│   ├── logger/              # AppLogger + LogViewerScreen + AppLoggerBleBridge
│   └── providers/           # Riverpod DI wiring (uses package classes)
└── features/
    └── {feature}/
        ├── domain/          # Facade providers + re-exports from roomfit_ble/roomfit_device
        │   └── providers/
        └── presentation/    # Pure UI — only imports domain providers
            ├── screens/
            └── widgets/
```

### Layer Separation (MANDATORY — 4계층, 엄격 분리)

하나의 기능은 반드시 4계층 중 **알맞은 위치**에 갈라 놓는다. 같은 하드웨어·같은 비즈니스 로직을 b2c / b2b가 공유하면, 공유 부분은 **무조건 `packages/`로 올라가야 한다**. 앱 안에 중복 구현해서는 안 된다.

| 계층 | 위치 | Flutter 허용 | 역할 | 예시 |
|------|------|:---:|------|------|
| **1. Protocol** | `packages/roomfit_protocol` | ❌ | MCU 와이어 포맷 정의 — Command/Response 클래스, BLE UUID, 상수 | `SetAutoWeightCommand`, `ReportResponse.weight.isOn` |
| **2. Device** | `packages/roomfit_device` (+ `roomfit_ble` transport) | ❌ | 프로토콜 바이트 → 구조화된 상태 스트림 | `DeviceState`, `WeightState`, `Stream<DeviceState>` |
| **3. Domain** | `packages/roomfit_workout`, `packages/roomfit_exercise`, `packages/roomfit_shop`, `packages/roomfit_firmware` | ❌ (pure Dart) | 비즈니스 로직 + 유스케이스. 하드웨어 신호 해석, 세션 생명주기, VBT 계산, 진단 등 | `SetExecutor`, `SessionManager`, `RestDetector`, `NextSetPreset`, `SetCutoffTrigger` |
| **4a. 공유 UI** | `packages/design_system` | ✅ | 둘 이상의 앱이 쓰는 위젯·토큰·다이얼로그 | `AppCard`, `RestPauseDialog`, `WeightBadge` |
| **4b. 앱 wiring** | `apps/{b2c,b2b}/lib/features/.../` | ✅ | Riverpod provider · Navigator · 앱 특화 UI 조립 | `restDetectedProvider`, `LiveWorkoutScreen`, 플로우 coordinator |

**판정 규칙**:
- "b2c가 같은 하드웨어로 self 세션 돌릴 때 **동일 동작이 필요한가**?" → **Yes면 3번 Domain 또는 4a 공유 UI에 위치**. 앱 features/ 안에 쓰면 안 됨.
- Flutter 위젯이 필요하면 `design_system`. Dart 로직만이면 해당 도메인 패키지.
- Riverpod / Navigator / 화면 조립은 앱 wiring (4b)에만.

**실제 케이스 — 휴식 감지 (2026-04-21 추가)**:

```
RestDetector                              → packages/roomfit_workout  (3. Domain, pure Dart)
SetCutoffTrigger.userEndedFromRest        → packages/roomfit_workout  (3. Domain)
showRestPauseDialog / RestDialogAction    → packages/design_system    (4a. 공유 UI)
restDetectorProvider / restDetectedProvider → apps/b2b wiring          (4b. 앱 전용)
SetAutoWeightCommand / SetWeightPowerCommand → packages/roomfit_protocol (1. Protocol)
DeviceState.weight.isOn 스트림             → packages/roomfit_device   (2. Device)
```

b2c는 동일한 RestDetector + RestPauseDialog를 그대로 재사용하고, `workout_live_screen.dart` + workout providers 에 wiring 2줄만 추가하면 됨. 만약 v1처럼 앱 안에 `_detectPause()` 로컬 메서드로 박아놨다면 b2c 복붙이 강제돼 버그 2중 관리가 된다.

상세 근거는 [`docs/reference/layer-separation-rest-detection.md`](docs/reference/layer-separation-rest-detection.md).

### Dependency Rules (ABSOLUTE — NO EXCEPTIONS)

```
apps/b2c/presentation → apps/b2c/domain/providers → apps/b2c/core/providers
apps/b2b/presentation → apps/b2b/domain/providers → apps/b2b/core/providers
                                                               ↓
                                                          roomfit_ble ──┐
                                                               ↓        │
                                                          roomfit_device┤
                                                               ↓        │
                                                          roomfit_protocol

roomfit_exercise : 순수 Dart, roomfit_protocol에만 의존. 앱의 features/exercise/data/ 어댑터만 roomfit_device.MotionState ↔ roomfit_exercise.MotionSample 브리지.
roomfit_shop     : 순수 Dart, 양쪽 앱 공유. apps/b2c는 role 조회 수준, apps/b2b는 full CRUD.
roomfit_firmware : 순수 Dart, 양쪽 앱이 OTA 시 사용.
apps/b2c ↔ apps/b2b 상호 import 절대 금지.
```

| From | Can Import | CANNOT Import |
|------|-----------|---------------|
| `apps/{b2c,b2b}/lib/features/.../presentation/` | `domain/`, sibling `presentation/` | `core/`, any `roomfit_*` package directly |
| `apps/{b2c,b2b}/lib/features/.../domain/` | `core/providers/`, `roomfit_ble`, `roomfit_device`, `roomfit_exercise`, `roomfit_shop`, external packages | `presentation/`, `roomfit_protocol` directly (use re-export chain) |
| `apps/{b2c,b2b}/lib/core/` | any `roomfit_*` package, external packages | `apps/*/lib/features/` (reverse coupling), 반대편 앱의 어떤 파일이든 |
| `apps/b2c/**` | `apps/b2b/**` 금지 | |
| `apps/b2b/**` | `apps/b2c/**` 금지 | |
| `packages/roomfit_ble/` | `roomfit_device`, `roomfit_protocol`, `flutter_blue_plus` | App-specific (`roomfit_v2`, `roomfit_studio`, `flutter_riverpod`, `flutter/material`), `freezed_annotation` |
| `packages/roomfit_device/` | `roomfit_protocol`, `freezed_annotation`, `meta` | Flutter, BLE, app-specific |
| `packages/roomfit_exercise/` | `roomfit_protocol`, `meta` | Flutter, BLE, device, app-specific |
| `packages/roomfit_shop/` | `meta` only | Flutter, `roomfit_ble`, `roomfit_device`, app-specific |
| `packages/roomfit_protocol/` | `meta` only | Anything Flutter or app-specific |

### How to Expose Package Types to Presentation

Presentation이 패키지 타입(예: `Command`, `WeightMode`, `DeviceState`)이 필요하면 domain provider에서 `export`로 re-export:

```dart
// domain/providers/device_command_provider.dart
import 'package:roomfit_ble/roomfit_ble.dart';

export 'package:roomfit_ble/roomfit_ble.dart'
    show DeviceState, Command, WeightMode, StartReportCommand, /* ... */;
```

`roomfit_ble` re-exports `roomfit_device`, which in turn re-exports
`roomfit_protocol` — so one `package:roomfit_ble/roomfit_ble.dart`
import reaches the whole stack. Prefer the most specific package
when the code only needs a slice (e.g. `motion_sample_mapper.dart`
only needs `roomfit_device`, not `roomfit_ble`).

Presentation은 domain import만으로 해당 타입을 사용.

### Verification

코드 작성 후 반드시 실행:

```bash
# 1. presentation → core / any roomfit_* 직접 import 위반
grep -rn "import.*core/\|import 'package:roomfit_" apps/b2c/lib/features/*/presentation/
grep -rn "import.*core/\|import 'package:roomfit_" apps/b2b/lib/features/*/presentation/

# 2. core → features 역방향 위반
grep -rn "import.*features/" apps/b2c/lib/core/
grep -rn "import.*features/" apps/b2b/lib/core/

# 3. 앱 간 상호 import 위반
grep -rn "package:roomfit_studio" apps/b2c/
grep -rn "package:roomfit_v2" apps/b2b/

# 4. roomfit_device / roomfit_exercise / roomfit_shop 패키지의 Flutter 의존 위반
grep -rn "package:flutter\|package:roomfit_v2\|package:roomfit_studio\|AppLogger\|flutter_riverpod" packages/roomfit_device/lib/
grep -rn "package:flutter\|flutter_riverpod\|package:roomfit_ble\|package:roomfit_device" packages/roomfit_exercise/lib/
grep -rn "package:flutter\|flutter_riverpod\|package:roomfit_ble\|package:roomfit_device" packages/roomfit_shop/lib/

# 5. roomfit_ble의 app/UI 의존 위반
grep -rn "package:roomfit_v2\|package:roomfit_studio\|AppLogger\|flutter_riverpod\|package:flutter/material" packages/roomfit_ble/lib/

# 6. 패키지 단독 분석
cd packages/roomfit_device && dart analyze
cd packages/roomfit_ble && flutter analyze
cd packages/roomfit_exercise && dart analyze
cd packages/roomfit_shop && dart analyze

# 7. 앱 단독 분석/테스트
cd apps/b2c && flutter analyze && flutter test
cd apps/b2b && flutter analyze && flutter test

# 8. 중복 도메인 로직 검출 — 같은 클래스/유스케이스가 양쪽 앱에
#    따로 존재하면 안 됨. 하나라도 걸리면 packages/ 로 올려야 함.
comm -12 \
  <(grep -rhoE "class [A-Z][A-Za-z0-9_]+" apps/b2c/lib/features/ | sort -u) \
  <(grep -rhoE "class [A-Z][A-Za-z0-9_]+" apps/b2b/lib/features/ | sort -u) \
  | grep -v "^class _"  # private 위젯만 걸러짐
```

위반이 1건이라도 나오면 커밋 금지.

## Naming Convention

### Commands: `{Verb}{Target}Command`

| Verb | 용도 | 예시 |
|------|------|------|
| `Get` | 조회 (응답 기대) | `GetPositionCommand`, `GetFirmwareVersionCommand` |
| `Set` | 설정 | `SetWeightCommand`, `SetWeightModeCommand` |
| `Start`/`Stop` | 스트림 제어 | `StartReportCommand`, `StopReportCommand` |
| `Reset` | 초기화 | `ResetCalibrationCommand`, `ResetRangeCommand` |
| `Adjust` | 증감 조정 | `AdjustWeightCommand` |
| `Force` | 강제 실행 | `ForceCalibrateCommand` |
| `Read`/`Write`/`Save` | 메모리 | `ReadRomCommand`, `WriteRomCommand`, `SaveFlashCommand` |
| (고유 동사) | 일회성 동작 | `RebootCommand`, `EmergencyStopCommand` |

### Responses: `{Target}Response`

Command와 동일한 Target 명사 공유:
- `SetWeightPowerCommand` → `WeightPowerResponse`
- `SetWeightModeCommand` → `WeightModeResponse`
- `ForceCalibrateCommand` → `ForceCalibrateResponse`

## TDD Cycle (MANDATORY)

1. **RED** — 실패하는 테스트 먼저 작성
2. **GREEN** — 테스트 통과하는 최소 구현
3. **REFACTOR** — 테스트 통과 상태에서 구조 개선

## Tidy First (MANDATORY)

커밋을 반드시 분리:
- **구조적 변경** (structural): 이름 변경, 파일 이동, import 정리 — 동작 변경 없음
- **동작 변경** (behavioral): 새 기능, 버그 수정, 프로토콜 구현

하나의 커밋에 두 유형을 섞지 않는다.

## Essential Commands

```bash
flutter pub get                                 # 루트에서 workspace 해결
cd apps/b2c && flutter test                     # B2C 앱 테스트
cd apps/b2b && flutter test                     # B2B 앱 테스트
dart test packages/roomfit_protocol/            # 패키지 단독 테스트
flutter analyze                                 # 루트에서 전체 분석
cd apps/b2c && flutter run                      # B2C 디바이스 실행
cd apps/b2b && flutter run                      # B2B 디바이스 실행
```

## Protocol Package

`packages/roomfit_protocol/` — 순수 Dart, Flutter 의존성 없음.

- `lib/src/commands/command.dart` — 26개 Command 클래스
- `lib/src/responses/response.dart` — 17개 Response 클래스
- `lib/src/responses/response_registry.dart` — 응답 파서 등록
- `lib/src/constants/command_codes.dart` — 코드 상수 + BLE UUID + WeightMode 등
- `lib/src/constants/physical_units.dart` — 단위 변환 (weight, position, voltage, time)
- `lib/src/codec/` — PacketBuilder, FrameCodec, RoomfitFrame

코드가 곧 문서. 별도 프로토콜 문서 불필요.

### ReportResponse 포맷 (CRITICAL)

`0x41` 리포트는 **dev 36B 단일 포맷**이다. 이전에 존재하던 v3 12B fallback과
앱 측 `0xF5 0x20` 활성화 절차는 모두 제거되었다 — dev가 정식 수신 응답이며,
36바이트가 아닌 `0x41` 프레임은 `ReportResponse.fromFrame`이 `FormatException`으로
거부하고 `ResponseRegistry`가 silently drop한다.

#### Dev Report Payload (36 bytes, 50ms 주기)

```
[0-1]   timeTick      50ms/tick        경과 시간
[2-3]   positionL     0.05mm/unit      좌측 모터 위치
[4-5]   positionR     0.05mm/unit      우측 모터 위치
[6-7]   speedL        mm/s, signed     좌측 속도
[8-9]   speedR        mm/s, signed     우측 속도
[10-11] accelL        mm/s², signed    좌측 가속도
[12-13] accelR        mm/s², signed    우측 가속도
[14-15] icmdL         mA, signed       좌측 전류 명령 (Icmd.q × 1000)
[16-17] icmdR         mA, signed       우측 전류 명령
[18-19] ifbL          mA, signed       좌측 피드백 전류 (Ie.q × 1000)
[20-21] ifbR          mA, signed       우측 피드백 전류
[22-23] fLoadL        0.01kg, signed   좌측 계산 부하
[24-25] fLoadR        0.01kg, signed   우측 계산 부하
[26-27] voltage       0.01V            전원 전압
[28]    weightModeL   enum             좌측 무게 모드 (0=normal, 1=eccentric, 2=elastic, 3=isokinetic, 4=hydraulic, 5=vibration)
[29]    weightModeR   enum             우측 무게 모드
[30]    regionL       enum             좌측 포지션 리전 (block=-1 … over=6)
[31]    regionR       enum             우측 포지션 리전
[32-33] weightSetL    0.01kg, signed   좌측 설정 무게
[34-35] weightSetR    0.01kg, signed   우측 설정 무게
```

dev 필드는 항상 valid하다고 가정하라. "v3에서는 0이 들어옴" 같은 caveat은
이제 사실이 아니다. 정상 경로에서 36바이트가 아닌 리포트는 발생하지 않으며,
발생한다면 펌웨어 회귀 버그로 보고 조사 대상이다.

## Logging

`AppLogger` 사용. `print()` 절대 금지.

```dart
final _log = AppLogger('BleTransport');
_log.info('Connected');
_log.error('Failed', error: e, stackTrace: s);
```

인앱 로그 뷰어: 앱 우상단 로그 아이콘으로 접근. release 빌드에서도 동작.

## New Feature Checklist

새 feature 추가 시 (해당 앱이 `apps/b2c` 인지 `apps/b2b` 인지 먼저 결정):

1. `apps/{b2c|b2b}/lib/features/{name}/domain/providers/` — facade provider 먼저 작성
2. `apps/{b2c|b2b}/lib/features/{name}/presentation/` — domain만 import하는 UI 작성
3. 테스트 먼저 (Red → Green → Refactor)
4. `grep -r "core/" apps/{b2c|b2b}/lib/features/{name}/presentation/` 위반 체크
5. `grep -r "package:roomfit_studio\|package:roomfit_v2" apps/{반대편}/` 0건 확인
6. `cd apps/{b2c|b2b} && flutter test && flutter analyze` 통과 확인
7. 구조적 커밋 / 동작 커밋 분리
