B2B 시나리오 — 갭 분석 4-에이전트 (아키텍처 / UX / 보안 / DB) 병렬 검토 · 2026-04-19 초기 · 2026-04-21 PR #66 해결 반영

2026-04-19 · 업데이트 2026-04-21
4 subagents · 27 gaps (PR #66으로 3개 해결)
참조 대시보드: b2b-scenarios.html / b2b-workout-flow.html
PR #66 — SessionConsole v1 완전 포팅 (2026-04-21) → 링크
v1 workout_setup 의 control surface 를 완전 포팅. 이 대시보드의 여러 HIGH gap 이 해결되었고 (DOM-01 6모드 노출 + DOM-05 multi-exercise + DOM-06 adjust/emergency UI), 신규 도메인 (DropSetConfig · ModeIntensity sealed · SetModeParamCommand 0xF5 0x05 · DropSetStepAdvancer) 이 추가되었음. DOM-02 (velocityMeaningful) 는 StandardVbtMetricsCalculator 에서 이미 null 처리 중. 남은 gap 은 각 카드에서 확인.
CRITICAL — 보안/데이터 손상 위험 HIGH — 도메인/UX 핵심 누락 MEDIUM — 운영 갭 / 시나리오 보강 FYI — 대시보드 표기 오류 RESOLVED — PR 에서 해결됨
5
Critical
5
High
(8 중 3 해결)
11
Medium
3
FYI
3
Resolved
🔴 CRITICAL 즉시 수정 권장 — 데이터 손상/탈취/우회 위험
SEC-01
sessions_update RLS의 WITH CHECK 약함 — 트레이너가 자기 세션의 user_id/center_id/trainer_id/session_type을 임의 변조 가능
Critical
현재 sessions_updateuser_id = auth.uid() OR trainer_id = auth.uid()만 검사. WITH CHECK가 immutable 컬럼을 보호하지 않아 supervised 세션을 다른 회원에게 "이전" 또는 self로 세탁 가능. 즉시 트리거 또는 WITH CHECK 강화 필요.
위치: supabase/migrations/20260419004009_b2b_shop_hierarchy.sql:147-149 발견: 보안 에이전트
SEC-02
Shadow email 선점 공격 — 외부인이 가짜 가입 후 트레이너가 그 email을 invite하면 회원 supervised 세션 SELECT 가능
Critical
invite_member/index.ts:111-130listUsers로 기존 user 검색 → 같은 email이면 existing.id 매핑. 외부인이 회원 가입 전에 그 회원 email로 먼저 가입해두면 트레이너 invite 시 외부인 user_id가 멤버십에 매핑되어 supervised 데이터 탈취. 운영적으로 회원에게 가입 안내 강제 또는 신규 user만 허용 옵션 필요.
위치: supabase/functions/invite_member/index.ts:111-130 발견: 보안 에이전트
DB-01
load_velocity_profile / daily_wellness / rep_detection_run RLS가 trainer를 인지 못 함 — supervised 세션이 LVP/wellness를 못 씀
Critical
이 3개 테이블의 RLS는 user_id = auth.uid()만 허용. supervised에서 auth.uid()는 트레이너이므로 회원 LVP를 read/write 거부됨. 핵심 BP 분석 데이터가 supervised 세션에서 누락. 마이그레이션 추가로 staff path 허용 필요.
위치: supabase/migrations/20260416012301_add_vbt_analytics.sql:147-211 발견: DB 에이전트
UX-01
BLE 중간 끊김 무방비 — LiveWorkoutScreen에 disconnect 핸들러 없음, 무한 hang 가능
Critical
setExecutorProvider가 BLE transport 상태를 감시하지 않음. icmd 명령이 sink되면 사용자는 진행 중 세트가 영원히 매달림. SessionManager.forceComplete는 존재하지만 disconnect 트리거 미연결. 시나리오에 "끊김 → forceComplete + 재연결 prompt" 분기 명시 필요.
위치: apps/b2b/lib/features/workout/presentation/screens/live_workout_screen.dart 발견: 아키텍처 + UX 에이전트
UX-02
Stage 1 수동 기록의 빈 세션 row 누수 — _pickExercise가 즉시 startSupervised 호출, 사용자 백버튼 시 정리 없음
Critical
SupervisedWorkoutScreen._pickExercise가 운동 선택 직후 repo.startSupervised를 호출해 workout_sessions + exercise_entries row를 INSERT. 사용자가 다음 단계에서 백버튼 누르면 ended_at = NULL 좀비 세션 + 빈 entry가 DB에 영구 잔존. WillPopScope 또는 dispose() 정리 필요.
위치: apps/b2b/lib/features/workout/presentation/screens/supervised_workout_screen.dart:73 발견: UX 에이전트
🟠 HIGH 도메인/UX 핵심 누락 — 시나리오 자체가 의도와 다르게 좁혀져 있음
DOM-01
6개 weight mode 중 normal만 노출 ✅ PR #66 해결
Resolved
해결: PR #66 에서 WeightModeBottomSheet 로 6 모드 모두 노출 + 모드별 세부 파라미터 (Eccentric level / Iso Vtarget/DecayRate/Fmin/Fmax / Hydro Vmax/n/minRatio / Vib Freq/Amplitude/MaxRatio). SessionConsole._startSet 에서 SetWeightModeCommand + 모드별 파라미터 명령 (SetEccLevelCommand / 신규 SetModeParamCommand) 송출. ModeIntensity sealed class 로 타입 안전.
원발견: 아키텍처 에이전트 해결 PR: #66
DOM-02
WeightMode.velocityMeaningful 무시 — Hydraulic / Isokinetic / Vibration 세트에 잘못된 VL% 표시 위험
High
CLAUDE.md 도메인 룰: 이 3개 모드에서 MCV/VL은 의미 없음. 그러나 LiveMetricsPanel·SetSummaryScreen·_VelocityLossFeedback이 모드 무관 동일 표시. 트레이너가 hydraulic 세트에 "목표 초과" 잘못된 피드백 받을 수 있음.
위치: packages/roomfit_protocol/.../command_codes.dart:205 발견: 아키텍처 에이전트
DOM-03
Side.left 하드코딩 — 양손/한손 운동 분기 없음
High
live_workout_flow_screen.dart:97, 145에서 side: Side.left 박혀있음. GripDef.isUnilateral은 라벨로만 표시되고 의사결정 미반영. unilateral grip이면 Side picker, bilateral이면 양측 동시 측정 분기 필요.
발견: 아키텍처 에이전트
DOM-04
TrainingGoal picker 없음 → VL 피드백이 영원히 안 뜸
High
SetSummaryScreen._VelocityLossFeedbackentry.trainingGoal != null일 때만 표시. 그런데 UI에서 goal을 입력받는 화면이 없어 항상 null. power/strength/hypertrophy/endurance 선택 UI 추가 필요.
발견: 아키텍처 에이전트
DOM-05
multi-exercise 세션 UI 부재 ✅ PR #61 + #66 해결
Resolved
해결: SessionConsole AppBar 에 "다른 운동" 버튼 추가. 탭 시 ExerciseSelectScreen 재진입 → 같은 세션 유지 + 새 ExerciseEntry 추가 (PR #61 에서 coordinator 패턴). PR #66 에서 SessionConsole 재작성 시에도 보존.
원발견: 아키텍처 에이전트 해결 PR: #61, #66
DOM-06
pause / adjustWeight / emergencyStop UI 미연결 ✅ PR #61 + #66 해결
Resolved
해결: SetControlBar 에 무게 조정 스테퍼 + EmergencyStopCommand 버튼 노출 (PR #61). PR #66 의 SessionConsole ControlBox 에서도 ±0.5/±5 stepper + 모드 변경 + 드롭셋 설정 모두 live push. 일시정지 대신 RestDetector 기반 자동 휴식 다이얼로그 (showRestPauseDialog) 로 대체.
원발견: 아키텍처 에이전트 해결 PR: #61, #66
UX-03
Resumable 세션 부재 — 좀비 세션 (ended_at=NULL) 발생 시 복구 흐름 없음
High
sessionManagerProvider가 메모리 only. 앱 재시작 시 _sessionId 소실 → DB에는 ended_at=NULL 세션만 남음. 트레이너 복귀 시 "어제 못 끝낸 세션" 안내 + 강제 종료 처리 미구현.
발견: UX 에이전트
UX-04
B2C 회원 앱이 supervised 세션을 라벨링 안 함 — 회원이 누가 기록했는지 모름
High
apps/b2c/lib에서 trainer_id|session_type|supervised grep 0건. 회원 앱은 session_type 무시하고 self/supervised를 한 리스트에 섞음. "트레이너 OO이 기록함" 라벨 누락 — 회원 입장 매우 혼란.
발견: UX 에이전트
🟡 MEDIUM 운영/시나리오 보강 — 1차 출시 전후 정의 필요
UX-05
Invite v0 — 실제 이메일 발송 없음, 운영자 수동 magic link
Medium
docs/b2b/hierarchy.md:58-61: "v0 email 발송 생략, owner가 magic link를 수동 콘솔에서 트리거". 회원 관점에서 "초대받았다"는 신호 0건. 시나리오 자체가 "owner가 카톡으로 알려줌" 상태. 운영 갭이 대시보드에 미반영.
발견: UX 에이전트
UX-06
동시성 — sessionManager / liveWorkoutContext가 single-instance, 다중 회원 병렬 시 cross-wire 위험
Medium
앱 전역 single-instance 프로바이더. 트레이너가 회원 A의 세션 시작 → 회원 B로 이동하면 컨텍스트 덮어씀. PT샵에서 트레이너 1명이 N대 기기 동시 운영 가능성을 시나리오가 가정 안 함. 명시적으로 "1대1 가정"이라 표시하거나 family provider로 전환.
발견: UX 에이전트
UX-07
트레이너 다중 샵 — RLS는 OK, UI는 single-shop 가정 (가장 최근 1개)
Medium
currentRoleProviderjoinedAt desc로 1개만 반환. 두 샵에 trainer로 초대받은 사람은 첫 샵을 못 봄. 샵 선택 UI 없음.
발견: UX 에이전트
UX-08
OwnerDashboard 부재 — 트레이너별 활동/매출 통계 없음
Medium
hierarchy.md:127-135에 OwnerDashboard 미정의. ShopHomeScreen은 멤버 리스트만. 트레이너별 일/주 단위 세션 수, 회원 진척도 같은 KPI가 없어 오너가 운영 판단 불가.
발견: UX 에이전트
UX-09
트레이너 이탈 / 오너 이양 흐름 0건
Medium
memberships_owner_delete RLS 정책만 있고 UI에 trainer 제거 버튼 없음. centers.owner_id가 단일 컬럼 + one_owner_per_center UNIQUE INDEX이지만 이양 RPC/UI 부재 → 오너 사망/탈퇴 시 샵 잠김.
발견: UX 에이전트
DB-02
centers 테이블이 빈약 — 주소/영업시간/활성 여부 컬럼 없음
Medium
현재 id/name/owner_id/created_at만. 폐업/일시 휴업 표현 불가, 주소 검색 불가. is_shop_staff가 폐업 샵의 staff에게도 영원히 true.
위치: 20260413114331_*.sql:202-207 발견: DB 에이전트
DB-03
center_memberships에 left_at / soft-delete 없음 — trainer 퇴사 시 과거 supervised의 RLS staff path 즉시 끊김
Medium
membership row를 DELETE하면 is_shop_staff false → trainer가 작성한 과거 supervised 세션을 본인은 더 이상 못 보고, 새 staff도 read 불가. left_at 컬럼 + tombstone 정책 필요.
위치: 20260419004009_b2b_shop_hierarchy.sql:31-38 발견: DB 에이전트
DB-04
workout_sessions에 status / canceled 컬럼 없음 + 트레이너는 자기 supervised 세션 삭제 불가
Medium
"수업 중단/취소" 표현 수단 없음. sessions_deleteuser_id = auth.uid()만 허용 → 트레이너가 잘못 만든 세션 정리 불가.
위치: 20260419004009_b2b_shop_hierarchy.sql:151-152 발견: DB 에이전트
DOM-07
SetCutoffTrigger 자동 cutoff 미구현 — VL10/20/30/40, targetReached, timeout 모두 항상 user로
Medium
SetExecutor.completeSetcutoffTrigger: SetCutoffTrigger.user 박힘. VBT 자동 종료 로직(velocity loss 임계 도달, 목표 reps 달성)이 도메인 enum에는 있고 호출부엔 없음.
발견: 아키텍처 에이전트
SEC-03
B2C self 세션의 center_id가 항상 NULL인지 클라이언트 측 강제 없음 — 향후 노출 가능
Medium
현재 b2c가 center_id를 안 보내서 안전하지만, SupabaseWorkoutRepository.createSession은 인자대로 INSERT. 누가 잘못 호출하면 staff에 노출. 서버 트리거로 self 세션의 center_id 강제 NULL 필요.
발견: 보안 에이전트
SEC-04
invite_member rate limit 없음 + listUsers O(N) scan — DoS / enumeration 가능
Medium
listUsers({ perPage: 1000 })가 사용자 수 증가 시 수초 latency. 같은 user를 N개 샵이 동시에 owner/trainer/member로 등록 가능 — 의도인지 확인 필요.
위치: supabase/functions/invite_member/index.ts:111 발견: 보안 에이전트
🔵 FYI 대시보드 자체의 표기 오류 — 즉시 수정
DOC-01
대시보드의 BLE 시퀀스가 실제 코드와 불일치 — SetWeightCommand는 안 보내고 SetAutoWeight를 누락
FYI
기존 대시보드는 SetWeightPower(1) → SetWeight(L,R) → StartReport로 그렸지만 실제는 다름:

❌ 대시보드 표기 (잘못됨)

  1. SetWeightPowerCommand(1)
  2. SetWeightCommand(L=20, R=20)
  3. StartReportCommand

✅ 실제 SetExecutor.startSet

  1. StartReportCommand
  2. SetAutoWeightCommand(isActive=1)
  3. SetWeightPowerCommand(onOff=1)
무게 변경은 별도 SetTargetWeightCommandadjustWeight 호출 시에만 송출.
위치: packages/roomfit_workout/lib/src/domain/use_cases/set_executor.dart:48-53 발견: 아키텍처 에이전트
DOC-02
데이터 플로우 박스가 workout_sessions만 그렸지만 실제 7개 테이블에 INSERT
FYI
Stage 2 finalize 시 workout_sessions / exercise_entries / exercise_sets / reps / rom_estimates / load_velocity_profile / rep_detection_run에 INSERT. 일부는 RLS 미정렬(DB-01). 시각화에 7개 모두 표시 필요.
발견: DB 에이전트
DOC-03
claim 플로우 미정 — 시나리오는 "pending"으로만 표시, 실제 트리거/Auth hook 0건
FYI
docs/b2b/shadow-accounts.md:12가 명시. shadow가 본인 email로 가입 시 pending_invitations stale row 갱신/정리 트리거 없음. shadow user_id가 그대로 살아있어 자동 승계되지만 명시 보장 부재.
발견: 보안 + DB 에이전트