[
  {
    "id": "SEC-01",
    "title": "sessions_update RLS WITH CHECK 약함 — trainer가 immutable 컬럼 변조 가능",
    "severity": "critical",
    "area": "security",
    "agent": "보안",
    "current": "현재 sessions_update 정책은 user_id = auth.uid() OR trainer_id = auth.uid() 만 검사. WITH CHECK가 user_id/trainer_id/center_id/session_type을 immutable로 강제하지 않음.",
    "risk": "트레이너가 자기 supervised 세션의 user_id를 다른 회원으로 UPDATE → 세션을 다른 회원에게 \"이전\" 가능. trainer_id를 본인으로 + user_id를 본인으로 + session_type=self로 UPDATE → supervised 세션을 self로 세탁 가능.",
    "location": "supabase/migrations/20260419004009_b2b_shop_hierarchy.sql:147-149",
    "proposal": "BEFORE UPDATE 트리거 enforce_session_immutable_columns()로 4개 컬럼(user_id/trainer_id/center_id/session_type) 변경 차단. RLS WITH CHECK는 INSERT 시점에만 강제되므로 트리거가 가장 단단함."
  },
  {
    "id": "SEC-02",
    "title": "Shadow email 선점 공격 — 외부인이 가짜 가입 후 trainer가 invite 시 회원 supervised 데이터 탈취",
    "severity": "critical",
    "area": "security",
    "agent": "보안",
    "current": "invite_member edge function이 listUsers로 기존 user 검색 후 같은 email이면 existing.id 무조건 재사용 → 본인 동의 없이 멤버십 매핑.",
    "risk": "외부인이 회원 email로 미리 가입해두면 트레이너가 invite 시 외부인 user_id가 멤버십에 매핑되어 그 회원의 supervised 데이터 SELECT 가능 (RLS 통과). 운영적으로도 trainer가 invite 누르기 전 회원 가입 안내 안 하면 중간자 탈취 가능.",
    "location": "supabase/functions/invite_member/index.ts:111-130",
    "proposal": "기존 user 발견 시 pending_invitations만 만들고 멤버십 미생성. 회원이 로그인한 상태에서 accept 분기를 호출하면 JWT.sub가 invite의 user_id와 일치할 때만 멤버십 INSERT. 신규(shadow) user 경로는 그대로."
  },
  {
    "id": "DB-01",
    "title": "load_velocity_profile / daily_wellness / rep_detection_run RLS가 trainer 무인지 — supervised 시 LVP/wellness 못 씀",
    "severity": "critical",
    "area": "db",
    "agent": "DB",
    "current": "이 3개 테이블의 RLS는 user_id = auth.uid() (또는 ws.user_id = auth.uid()) 만 허용. supervised 세션에서 auth.uid()는 트레이너이므로 회원 LVP/wellness/rep_detection_run을 read/write 거부됨.",
    "risk": "supervised 세션의 핵심 BP 분석 데이터(LVP 추정, wellness 점수, rep detector 검증)가 누락. 트레이너가 측정한 운동의 1RM 추정/속도 손실 추세가 회원 데이터에 쌓이지 않음.",
    "location": "supabase/migrations/20260416012301_add_vbt_analytics.sql:147-153, 204-211",
    "proposal": "is_shop_staff + shared_shop_with_member 헬퍼를 활용해 staff path를 OR로 추가. lvp + rep_detection_run은 staff write/read 허용. daily_wellness는 read만 staff 허용 (개인정보 신중)."
  },
  {
    "id": "UX-01",
    "title": "BLE 중간 끊김 무방비 — LiveWorkoutScreen에 disconnect 핸들러 없음, 무한 hang 가능",
    "severity": "critical",
    "area": "ux",
    "agent": "아키텍처 + UX",
    "current": "setExecutorProvider가 BLE transport 상태를 감시하지 않음. LiveWorkoutScreen에도 connection state listener 없음.",
    "risk": "세트 진행 중 기기 연결이 끊기면 icmd/StopReport가 sink되어 사용자 화면이 영원히 매달림. SessionManager.forceComplete는 도메인에 존재하지만 disconnect 트리거 미연결.",
    "location": "apps/b2b/lib/features/workout/presentation/screens/live_workout_screen.dart, live_workout_flow_screen.dart",
    "proposal": "_LiveWorkoutFlowScreenState에서 ref.listen(deviceStateViewProvider, ...) 추가. connected → disconnected/error 전이 시 setExecutorProvider.notifier.forceComplete() 호출 + AlertDialog \"기기 연결이 끊어졌습니다\" + 재연결/수업종료 액션."
  },
  {
    "id": "UX-02",
    "title": "Stage 1 수동 기록의 빈 세션 row 누수 — _pickExercise가 즉시 startSupervised 호출, 백버튼 시 정리 없음",
    "severity": "critical",
    "area": "ux",
    "agent": "UX",
    "current": "SupervisedWorkoutScreen._pickExercise가 운동 선택 직후 repo.startSupervised + repo.addEntry 즉시 INSERT. 사용자가 다음 단계에서 백버튼 누르면 정리 코드 없음.",
    "risk": "ended_at = NULL 좀비 세션 + 빈 entry row가 DB에 영구 잔존. 회원 detail의 \"최근 세션\" 리스트가 빈 세션으로 오염되고, 통계/total_volume_load 계산도 어긋남.",
    "location": "apps/b2b/lib/features/workout/presentation/screens/supervised_workout_screen.dart:44-100",
    "proposal": "lazy startSupervised — _pickExercise는 운동 state만 업데이트하고 DB INSERT 0건. _addSet 첫 호출 시 _sessionId == null 체크 후 그때 startSupervised + addEntry. _finalize는 _sessionId == null이면 단순 pop."
  },

  {
    "id": "DOM-01",
    "title": "6개 weight mode 중 normal만 노출 — 모드 선택 UI 없음, SetWeightModeCommand 미송출",
    "severity": "high",
    "area": "domain",
    "agent": "아키텍처",
    "current": "WeightMode enum은 normal/eccentric/elastic/isokinetic/hydraulic/vibration 6개. SessionManager.addExerciseEntry가 모드 받지만 LiveWorkoutFlowScreen:93은 WeightMode.normal 고정. SetExecutor.startSet은 SetWeightModeCommand 자체를 안 보냄.",
    "risk": "6개 모드의 차별화된 운동 효과(eccentric overload, 등속, hydraulic 등)를 트레이너가 활용 불가. CLAUDE.md의 핵심 도메인 기능이 사실상 hidden.",
    "location": "apps/b2b/lib/features/workout/presentation/screens/live_workout_flow_screen.dart:93, packages/roomfit_workout/lib/src/domain/use_cases/set_executor.dart:48-53",
    "proposal": "ExerciseSelect 다음에 모드 선택 단계 추가. SetExecutor.startSet에 weightMode 파라미터 추가하고 SetWeightModeCommand 송출. 기본은 normal."
  },
  {
    "id": "DOM-02",
    "title": "WeightMode.velocityMeaningful 무시 — Hydraulic / Isokinetic / Vibration 세트에 잘못된 VL% 표시 위험",
    "severity": "high",
    "area": "domain",
    "agent": "아키텍처",
    "current": "CLAUDE.md 도메인 룰: isokinetic/hydraulic/vibration에서 MCV/VL은 의미 없음 (velocityMeaningful=false). 그러나 LiveMetricsPanel·SetSummaryScreen·_VelocityLossFeedback이 모드 무관 동일 표시.",
    "risk": "트레이너가 hydraulic 세트에 \"목표 초과\" 같은 잘못된 피드백을 받음 → 운동 처방 오류 가능. StandardVbtMetricsCalculator는 자동 null 처리하지만 UI는 0/null을 그대로 표시.",
    "location": "packages/roomfit_protocol/lib/src/constants/command_codes.dart:205, apps/b2b/lib/features/workout/presentation/widgets/live_metrics_panel.dart, set_summary_screen.dart",
    "proposal": "entry.weightMode.velocityMeaningful 체크해 metrics 영역 숨김 또는 \"이 모드에서는 측정 안 됨\" 표시. Vibration 진폭 / Iso target velocity 같은 모드별 라이브 지표 별도 표시."
  },
  {
    "id": "DOM-03",
    "title": "Side.left 하드코딩 — 양손/한손 운동 분기 없음",
    "severity": "high",
    "area": "domain",
    "agent": "아키텍처",
    "current": "live_workout_flow_screen.dart:97, 145에서 side: Side.left 박혀있음. GripDef.isUnilateral은 라벨로만 표시되고 의사결정에 미반영. entry에 저장만 됨.",
    "risk": "양손 운동도 좌측 한쪽 데이터만 수집 → 좌/우 비대칭 분석 불가. 한손(unilateral) 운동 시 사용자가 어느 쪽으로 잡는지 선택 불가.",
    "location": "apps/b2b/lib/features/workout/presentation/screens/live_workout_flow_screen.dart:97, 145",
    "proposal": "unilateral grip이면 Side picker (왼쪽/오른쪽) 노출. bilateral이면 양측 동시 측정 + L/R 별도 rep detector 결과 표시."
  },
  {
    "id": "DOM-04",
    "title": "TrainingGoal picker 없음 → VL 피드백이 영원히 안 뜸",
    "severity": "high",
    "area": "domain",
    "agent": "아키텍처",
    "current": "SetSummaryScreen._VelocityLossFeedback이 entry.trainingGoal != null일 때만 표시. UI에서 goal을 입력받는 화면이 없어 항상 null.",
    "risk": "Velocity Loss 가이드(power/strength/hypertrophy/endurance 별 권장 VL%)가 영원히 화면에 안 뜸. 도메인 모델은 완성됐으나 UX 진입점 부재.",
    "location": "apps/b2b/lib/features/workout/presentation/screens/set_summary_screen.dart:37, packages/roomfit_workout (TrainingGoal)",
    "proposal": "ExerciseSelect 또는 모드 선택 단계에서 TrainingGoal 4지 선다 picker 추가. 기본은 null 또는 사용자 마지막 선택 기억."
  },
  {
    "id": "DOM-05",
    "title": "multi-exercise 세션 UI 부재 — SessionManager는 지원, UI는 운동 1개만",
    "severity": "high",
    "area": "domain",
    "agent": "아키텍처",
    "current": "SessionManager.addExerciseEntry는 반복 호출 가능 + entryOrder 자동 증가. switchToEntry 존재. 그러나 LiveWorkoutFlowScreen._startNextSet은 같은 entry에 set만 추가, SetSummaryScreen action도 next/finish 두 가지뿐.",
    "risk": "한 수업에서 운동 여러 개(superset, 가슴+등 등)를 기록 불가 → 수업 1회당 세션 N개로 쪼개져 통계가 흐트러짐.",
    "location": "apps/b2b/lib/features/workout/presentation/screens/live_workout_flow_screen.dart:141-150, set_summary_screen.dart:47-71",
    "proposal": "SetSummary action에 \"다른 운동 추가\" 추가 → ExerciseSelect 재진입 → addExerciseEntry 호출 후 새 entry로 startSet. SessionSummary는 entries 리스트로 그룹화."
  },
  {
    "id": "DOM-06",
    "title": "pause / adjustWeight / emergencyStop UI 미연결 — 세트 중 즉시 정지 불가",
    "severity": "high",
    "area": "domain",
    "agent": "아키텍처",
    "current": "SetExecutor.adjustWeight, SessionManager.pauseSession/resumeSession, EmergencyStopCommand·AdjustWeightCommand·SetWeightPowerCommand(0) 모두 도메인에 존재. SetControlBar는 \"세트 완료\" 버튼 1개뿐.",
    "risk": "트레이너가 회원 안전을 위해 세트 중 즉시 정지하거나 무게를 낮출 수 없음. 사고 위험.",
    "location": "apps/b2b/lib/features/workout/presentation/widgets/set_control_bar.dart:43-55",
    "proposal": "SetControlBar에 ±무게 / 일시정지 / 비상정지 액션 추가. 비상정지는 큰 빨간 버튼."
  },
  {
    "id": "UX-03",
    "title": "Resumable 세션 부재 — 좀비 세션(ended_at=NULL) 발생 시 복구 흐름 없음",
    "severity": "high",
    "area": "ux",
    "agent": "UX",
    "current": "sessionManagerProvider가 메모리 only. 앱 재시작 시 _sessionId 소실 → DB에는 ended_at=NULL 세션만 남음.",
    "risk": "트레이너 복귀 시 \"어제 못 끝낸 세션\" 안내 + 강제 종료 처리 미구현. 좀비 row가 누적되면서 통계 오염.",
    "location": "apps/b2b/lib/features/workout/domain/providers/workout_providers.dart (sessionManagerProvider)",
    "proposal": "앱 시작 시 본인이 trainer인 ended_at=NULL 세션 조회. 있으면 \"진행 중이던 세션이 있습니다 — 종료 / 삭제\" 다이얼로그."
  },
  {
    "id": "UX-04",
    "title": "B2C 회원 앱이 supervised 세션을 라벨링 안 함 — 회원이 누가 기록했는지 모름",
    "severity": "high",
    "area": "ux",
    "agent": "UX",
    "current": "apps/b2c/lib에서 trainer_id|session_type|supervised grep 0건. 회원 앱은 session_type을 무시하고 self/supervised를 한 리스트에 섞음.",
    "risk": "\"트레이너 OO이 기록함\" 라벨 누락 → 회원 입장에선 누가 기록했는지 모름. 세션 신뢰성/투명성 문제.",
    "location": "apps/b2c/lib/features/workout/presentation/ (history_screen 등)",
    "proposal": "B2C history/session detail에 session_type 배지 추가 (\"트레이너 기록\" vs \"내 기록\"). trainer_id로 trainer 이름 fetch해 표시."
  },

  {
    "id": "UX-05",
    "title": "Invite v0 — 실제 이메일 발송 없음, 운영자 수동 magic link",
    "severity": "medium",
    "area": "ux",
    "agent": "UX",
    "current": "docs/b2b/hierarchy.md:58-61: \"v0 email 발송 생략, owner가 magic link를 수동 콘솔에서 트리거\". 회원 관점에서 \"초대받았다\"는 신호 0건.",
    "risk": "시나리오 자체가 \"owner가 카톡으로 알려줌\" 상태. 자동화 갭이 운영 비용 증가 + 회원 가입 누락.",
    "location": "docs/b2b/hierarchy.md, supabase/functions/invite_member/index.ts",
    "proposal": "invite_member edge function에 Resend / Supabase Email 연동. 매직 링크 + 샵 컨텍스트 포함 한국어 템플릿."
  },
  {
    "id": "UX-06",
    "title": "동시성 — sessionManager / liveWorkoutContext가 single-instance, 다중 회원 병렬 시 cross-wire 위험",
    "severity": "medium",
    "area": "ux",
    "agent": "UX",
    "current": "앱 전역 single-instance Riverpod 프로바이더. 트레이너가 회원 A 세션 시작 → 회원 B로 이동하면 컨텍스트 덮어씀.",
    "risk": "PT샵에서 트레이너 1명이 N대 기기 동시 운영 가정 시 데이터 cross-wire. 회원 A 세트가 회원 B에게 기록될 수 있음.",
    "location": "apps/b2b/lib/features/workout/domain/providers/workout_providers.dart",
    "proposal": "v1: 시나리오에 명시적으로 \"1대1 가정\"이라 표시 + 새 세션 시작 시 기존 진행 중 세션 경고. v2: family provider로 전환 (memberUserId key)."
  },
  {
    "id": "UX-07",
    "title": "트레이너 다중 샵 — RLS는 OK, UI는 single-shop 가정 (가장 최근 1개)",
    "severity": "medium",
    "area": "ux",
    "agent": "UX",
    "current": "currentRoleProvider가 joinedAt desc로 1개만 반환. 두 샵에 trainer로 초대받은 사람은 첫 샵을 못 봄.",
    "risk": "체인 PT샵 / 프리랜서 트레이너가 여러 샵에서 일하는 케이스 차단.",
    "location": "apps/b2b/lib/features/shop/domain/providers/shop_providers.dart",
    "proposal": "현재 활성 샵 picker 추가 + activeShopIdProvider StateProvider. ShopHomeScreen 헤더에 샵 이름 + 전환 버튼."
  },
  {
    "id": "UX-08",
    "title": "OwnerDashboard 부재 — 트레이너별 활동/매출 통계 없음",
    "severity": "medium",
    "area": "ux",
    "agent": "UX",
    "current": "ShopHomeScreen은 멤버 리스트만. 트레이너별 일/주 단위 세션 수, 회원 진척도 같은 KPI 부재.",
    "risk": "오너가 운영 판단 불가. 매출/생산성 추적 없음.",
    "location": "apps/b2b/lib/features/shop/presentation/screens/shop_home_screen.dart, docs/b2b/hierarchy.md:127-135",
    "proposal": "OwnerDashboard 신규 화면 — 트레이너별 supervised 세션 수, 평균 세션 길이, 이탈률, 신규 회원 차트."
  },
  {
    "id": "UX-09",
    "title": "트레이너 이탈 / 오너 이양 흐름 0건",
    "severity": "medium",
    "area": "ux",
    "agent": "UX",
    "current": "memberships_owner_delete RLS 정책만 존재. UI에 trainer 제거 버튼 없음. centers.owner_id가 단일 컬럼 + one_owner_per_center UNIQUE INDEX이지만 이양 RPC/UI 부재.",
    "risk": "오너 사망/탈퇴 시 샵 잠김. 트레이너 이탈 처리 운영자가 수동 SQL.",
    "location": "supabase/migrations/20260419004009_b2b_shop_hierarchy.sql, apps/b2b shop screens",
    "proposal": "trainer 제거 UI (오너만) + 오너 이양 RPC (transfer_ownership(new_owner_id) atomic transaction)."
  },
  {
    "id": "DB-02",
    "title": "centers 테이블이 빈약 — 주소/영업시간/활성 여부 컬럼 없음",
    "severity": "medium",
    "area": "db",
    "agent": "DB",
    "current": "현재 centers는 id/name/owner_id/created_at만 (20260413114331_*.sql:202-207).",
    "risk": "폐업/일시 휴업 표현 불가, 주소 검색 불가. is_shop_staff가 폐업 샵의 staff에게도 영원히 true.",
    "location": "supabase/migrations/20260413114331_*.sql:202-207",
    "proposal": "centers에 address text, business_hours jsonb, status enum (active/inactive/closed), closed_at timestamptz 추가. is_shop_staff 헬퍼에 status='active' 조건 추가."
  },
  {
    "id": "DB-03",
    "title": "center_memberships에 left_at / soft-delete 없음 — trainer 퇴사 시 과거 supervised의 RLS staff path 즉시 끊김",
    "severity": "medium",
    "area": "db",
    "agent": "DB",
    "current": "membership row를 DELETE하면 is_shop_staff false → trainer가 작성한 과거 supervised 세션을 본인은 더 이상 못 보고, 새 staff도 read 불가.",
    "risk": "트레이너 퇴사 후 본인이 만든 세션 조회 불가 → 데이터 단절. 신규 staff가 회원 히스토리 조회 못함.",
    "location": "supabase/migrations/20260419004009_b2b_shop_hierarchy.sql:31-38",
    "proposal": "center_memberships에 left_at timestamptz nullable 추가. is_shop_staff 헬퍼는 left_at IS NULL 조건. soft-delete 패턴."
  },
  {
    "id": "DB-04",
    "title": "workout_sessions에 status / canceled 컬럼 없음 + 트레이너는 자기 supervised 세션 삭제 불가",
    "severity": "medium",
    "area": "db",
    "agent": "DB",
    "current": "\"수업 중단/취소\" 표현 수단 없음. sessions_delete는 user_id = auth.uid()만 허용 → 트레이너가 잘못 만든 세션 정리 불가.",
    "risk": "잘못 시작한 supervised 세션이 영구 잔존. 회원 데이터에 \"취소\" 라벨 없이 빈 세션 노출.",
    "location": "supabase/migrations/20260419004009_b2b_shop_hierarchy.sql:151-152",
    "proposal": "workout_sessions에 status enum (active/completed/canceled) 추가. sessions_delete 정책에 trainer_id = auth.uid() OR 추가 (자기 작성 supervised 한정)."
  },
  {
    "id": "DOM-07",
    "title": "SetCutoffTrigger 자동 cutoff 미구현 — VL10/20/30/40, targetReached, timeout 모두 항상 user로",
    "severity": "medium",
    "area": "domain",
    "agent": "아키텍처",
    "current": "SetExecutor.completeSet는 cutoffTrigger: SetCutoffTrigger.user 박힘. VBT 자동 종료 로직(velocity loss 임계 도달, 목표 reps 달성)이 도메인 enum에는 있고 호출부엔 없음.",
    "risk": "VL 자동 cutoff(예: VL30% 도달 시 자동 종료)는 VBT 핵심 기능 — 미구현 시 트레이너가 직접 판단해야 함. 데이터 라벨링도 모두 user로 박혀 분석 어려움.",
    "location": "packages/roomfit_workout/lib/src/domain/use_cases/set_executor.dart:120",
    "proposal": "SetExecutor에 onRepCompleted에서 VL 임계/targetReached 감지 → 자동 completeSet(trigger: vlXX/targetReached) 호출. 트레이너 수동 종료는 user로."
  },
  {
    "id": "SEC-03",
    "title": "B2C self 세션의 center_id가 항상 NULL인지 클라이언트 측 강제 없음 — 향후 노출 가능",
    "severity": "medium",
    "area": "security",
    "agent": "보안",
    "current": "현재 b2c가 center_id를 안 보내서 안전하지만, SupabaseWorkoutRepository.createSession은 인자대로 INSERT.",
    "risk": "누가 잘못 호출하면 self 세션에 center_id가 박혀 staff에 노출. 회원 프라이버시 침해.",
    "location": "packages/roomfit_workout/lib/src/data/supabase_workout_repository.dart:31",
    "proposal": "서버측 트리거: session_type='self' AND center_id IS NOT NULL이면 center_id := NULL 강제 (또는 RAISE EXCEPTION)."
  },
  {
    "id": "SEC-04",
    "title": "invite_member rate limit 없음 + listUsers O(N) scan — DoS / enumeration 가능",
    "severity": "medium",
    "area": "security",
    "agent": "보안",
    "current": "listUsers({ perPage: 1000 })가 사용자 수 증가 시 수초 latency. 같은 user를 N개 샵이 동시에 owner/trainer/member로 등록 가능.",
    "risk": "spam/enumeration 가능. 사용자 증가 시 invite latency 폭발.",
    "location": "supabase/functions/invite_member/index.ts:111",
    "proposal": "auth.users에 email 인덱스 활용 RPC 도입 또는 admin 직접 쿼리. invite endpoint에 sliding window rate limit (Supabase Edge Functions middleware)."
  },

  {
    "id": "DOC-01",
    "title": "대시보드의 BLE 시퀀스가 실제 코드와 불일치 — SetWeightCommand는 안 보내고 SetAutoWeight를 누락",
    "severity": "fyi",
    "area": "docs",
    "agent": "아키텍처",
    "current": "기존 b2b-scenarios.html은 SetWeightPower(1) → SetWeight(L,R) → StartReport로 그렸지만 실제 SetExecutor.startSet은 StartReport → SetAutoWeight(1) → SetWeightPower(1) 순서. SetWeightCommand는 호출되지 않음.",
    "risk": "문서 신뢰성 저하. 새로 합류한 개발자가 잘못된 순서로 펌웨어 디버깅.",
    "location": "packages/roomfit_workout/lib/src/domain/use_cases/set_executor.dart:48-53, b2b-scenarios.html:336-362",
    "proposal": "b2b-scenarios.html BLE 시퀀스 박스 수정. 무게 변경은 별도 SetTargetWeightCommand가 adjustWeight 호출 시에만 송출이라고 명시."
  },
  {
    "id": "DOC-02",
    "title": "데이터 플로우 박스가 workout_sessions만 그렸지만 실제 7개 테이블에 INSERT",
    "severity": "fyi",
    "area": "docs",
    "agent": "DB",
    "current": "Stage 2 finalize 시 workout_sessions / exercise_entries / exercise_sets / reps / rom_estimates / load_velocity_profile / rep_detection_run 7개에 INSERT.",
    "risk": "일부는 RLS 미정렬(DB-01). 데이터 플로우 시각화에 7개 모두 표시 필요.",
    "location": "b2b-scenarios.html (data flow 섹션)",
    "proposal": "b2b-scenarios.html 데이터 플로우 박스에 7개 테이블 트리 표시. RLS 정합성 OK/NG 표기 (DB-01 해결 후)."
  },
  {
    "id": "DOC-03",
    "title": "claim 플로우 미정 — 시나리오는 \"pending\"으로만 표시, 실제 트리거/Auth hook 0건",
    "severity": "fyi",
    "area": "docs",
    "agent": "보안 + DB",
    "current": "docs/b2b/shadow-accounts.md:12가 명시. shadow가 본인 email로 가입 시 pending_invitations stale row 갱신/정리 트리거 없음.",
    "risk": "shadow user_id가 그대로 살아있어 자동 승계되지만 명시 보장 부재. 운영 중 shadow stale row 누적.",
    "location": "docs/b2b/shadow-accounts.md, supabase/migrations/20260419004009_b2b_shop_hierarchy.sql",
    "proposal": "Supabase Auth on_auth_user_created 트리거 정의 — email 매치되는 pending_invitations row를 active로 전환 + 멤버십 INSERT + invitation cleanup."
  }
]
