한도 정본 (Limits Registry)
이슈 #938 Phase 4 (2026-08-31). 사용자 데이터에 걸린 모든 저장·표시 한도의 정본 장부다. 값을 바꿀 때는 이 문서와 코드 정의를 함께 바꾼다 — tests/react/limitsRegistry.test.mjs가 이 표의 값과 코드 상수·SQL 본문이 일치하는지 잠근다(어긋나면 CI 실패).
원칙 (오너 결정, 이슈 #938 Phase 0)
- 한도를 넘은 데이터는 거부·에러가 아니라 안내·절삭·페이지네이션으로 처리한다. 성능 한도의 원래 목적은 응답 크기이지 사용자 차단이 아니다.
- 같은 한도는 한 곳에서 정의하고 클라·서버가 같은 값을 쓴다. 클라 정본은
completedWorkoutWriteContract.ts(쓰기)·appPerformanceBudgets.ts(읽기), 서버 정본은supabase/schema.sql의 마지막 재발행이다. - 새 한도는 산정 근거를 함께 적는다. "몇 행 × 행당 몇 바이트 + 여유 몇 %" 없이 숫자만 적힌 한도는 이 장부에 들어올 수 없다.
- 여유폭을 주기적으로 잰다.
node scripts/audit-limit-headroom.mjs— Production 실측 최대값 대비 여유가 20% 아래로 내려간 한도를 찾아낸다(#252의 "여유 2세트" 재발 방지).
쓰기 한도 (넘으면 저장 거부 — 클라가 원인 문구로 사전 안내)
| 한도 | 값 | 정의 위치 (클라 / 서버) | 산정 근거 | 실측 최대 (2026-08-30) |
|---|---|---|---|---|
| 계획 세션 총 세트 | 240 | COMPLETED_WORKOUT_WRITE_LIMITS.maxExpandedSets / enforce_planned_session_set_limit·validate_plan_payload_v3_structure_core | 완료 세션과 동일값 (D1) — 계획이 완료보다 작을 이유 없음. 클라 사전검사 LG_PLAN_SET_LIMIT | 계획 21 · 완료 p99 34 |
| 완료 세션 종목 수 | 48 | maxExercises / validate_completed_workout_payload_v4_structure_core·enforce_completed_session_exercise_limit | 실측 최대 13의 3.7배 (D2). 원인 문구 LG_WORKOUT_EXERCISE_LIMIT | 13 |
| 완료 종목당 세트 | 64 | maxSetsPerExercise / 위 구조 검증기·enforce_completed_exercise_set_limits | 실측 최대 30의 2.1배 (D2) — 종전 32는 여유 2세트뿐이었다. 원인 문구 LG_WORKOUT_EXERCISE_SET_LIMIT | 30 |
| 완료 세션 총 세트 | 240 | maxExpandedSets / 위 구조 검증기·트리거 | 유지 (D2) — 실측 최대 59의 4배. 원인 문구 LG_WORKOUT_SET_LIMIT | 59 |
| 세트당 반복(reps) | 2,000 | maxReps / validate_session_payload_v5_core(구 validate_completed_workout_payload_v4·validate_plan_payload_v3)·exercise_set_part CHECK 2종 | 줄넘기·더블언더 수용 (D3) — 실측 최대 215의 9배. 완료 경로 원인 문구 LG_WORKOUT_REPS_LIMIT | 215 |
| 세트 간 휴식 | 3,600초 절삭 | maxRestSeconds(매퍼가 절삭) / clamp_exercise_set_rest_seconds 트리거(절삭)·CHECK(안전망) | 거부 아님 (D4) — 앱이 자동 측정하므로 자리 비움이 저장을 막으면 안 됨 | 1,013초 |
| 계획 제목 / 메모 | 512B / 4,096B | savePlan 사전검사(LG_PLAN_TITLE_LIMIT·LG_PLAN_NOTE_LIMIT) / session CHECK(계획도 같은 테이블의 status = planned 행, 이슈 #1215) | 완료 세션 제목·메모와 동일값 | — |
| 완료 제목 / 메모 / 세트 메모 | 512B / 4,096B / 1,024B | maxSessionTitleBytes·maxSessionNoteBytes·maxSetNoteBytes — 원인 문구 LG_WORKOUT_TITLE_LIMIT·LG_WORKOUT_NOTE_LIMIT·LG_WORKOUT_SET_NOTE_LIMIT / session·exercise_set_part CHECK | 계획 제목·메모와 동일값. 종목 메모·리뷰(4,096B)도 LG_WORKOUT_NOTE_LIMIT | 메모 최대 286B |
| 무게(load) | 999,999.99kg | maxLoad·savePlan 사전검사(LG_PLAN_LOAD_LIMIT)·완료 경로 원인 문구 LG_WORKOUT_LOAD_LIMIT / numeric(8,2) | 스키마 자릿수 | 300kg대 |
| 보조 무게(assist_kg) | 0 초과~400kg | maxAssistKg / exercise_set_part_atomic_assist_check(계획 세트도 같은 테이블 — 구 planned_sets_atomic_assist_check 소멸)·validate_session_payload_v5_core·보드 검증기 | 체중 상한 400kg과 동일값 — 보조 무게는 체중에서 빼는 값이라 체중을 넘길 이유가 없다 (이슈 #1200 D3) | — |
| 무게 배수(load_multiplier) | 1 또는 2 | loadMultiplierOf(허용값 밖은 1) / exercises_load_multiplier_check·session_exercise_part_load_multiplier_check·training_effective_load | 덤벨 2개·더블 케틀벨만 2 — 유저는 한 개 무게를 적고 볼륨만 ×2 (이슈 #1200 D1) | — |
| 기록 방식 원자 수 | 3 | maxRecordingFields / 원자 CHECK 3종 + byte 묶음 CHECK 2종 | 제품 정의(무게·보조·횟수·시간·거리·칼로리 중 ≤3 조합) — 유산소류 3원자 프로필 수용 (이슈 #932 D3, 채움 판정은 required_inputs가 별도 담당) | 전 카탈로그 ≤3 |
| 즐겨찾기 저장 | 128 | prFavorites.maxFavorites / replace_user_exercise_favorites | 종전 유지 — 월 로그 호출(30/호출)은 분할로 수용 (D6) | 7 |
| 1RM 직접 입력 | 0 초과~5,000kg | saveManualPr 사전검사(LG_PR_VALUE_LIMIT) / save_manual_pr·complete_onboarding_v2 | 서버 기존값 정합(#975 Phase 1-2) — 온보딩 초기 1RM과 동일값 | 300kg대 |
| 직접 입력 최대 반복수 | 1~10,000회(정수) | saveManualRecordMetric 사전검사(LG_RECORD_REPS_LIMIT) / user_manual_record_metrics_value_check·save_manual_record_metric_v1 | 세트당 반복 2,000의 5배 — 하루 누적형(버피 챌린지 등) 수용 (이슈 #1194) | — |
| 직접 입력 최고 버티기 | 1~86,400초 | 동 함수 사전검사(LG_RECORD_HOLD_LIMIT) / 동 CHECK·RPC | 24시간 = 하루 상한 (이슈 #1194) | — |
| 직접 입력 유산소 거리 / 시간 | 1~1,000,000m / 1~86,400초 | 동 함수 사전검사(LG_RECORD_DISTANCE_LIMIT·LG_RECORD_SECONDS_LIMIT) / 동 CHECK·RPC | 1,000km·24시간 — 울트라 종목 수용 (이슈 #1194) | — |
| 체중 | 20~400kg | saveProfileBodyMetric 사전검사(LG_BODY_WEIGHT_LIMIT) / body_metrics_weight_kg_check·complete_onboarding_v2 | 서버 CHECK 기존값 정합(#975 Phase 1-3) | — |
| 골격근량 | 5~120kg | 동 함수 사전검사(LG_BODY_MUSCLE_LIMIT) / body_metrics_skeletal_muscle_kg_check | 서버 CHECK 기존값 정합(#975 Phase 1-3) | — |
| 체지방률 | 1~80% | 동 함수 사전검사(LG_BODY_FAT_LIMIT, kg 입력 → % 유도 후 검사) / body_metrics_body_fat_percent_check | 서버 CHECK 기존값 정합(#975 Phase 1-3) | — |
읽기 한도 (응답 크기 — 넘치면 페이지·분할·절삭 플래그, 바이트 천장만 에러)
| 화면 | 행 한도 | 바이트 천장 | 정의 위치 (클라 예산 / 서버) | 산정 근거 |
|---|---|---|---|---|
| 세션 검색 | 120/페이지 · keyset 페이지네이션으로 전체 | 1,200,000/페이지 | sessionSearch / get_session_search | D5 — 하드컷 폐지, 페이지 수집 상한 40(=4,800세션) |
| 계획 상세 | 세트 240 | 2,000,000 | plannedSessionDetail / get_planned_session_detail | 쓰기 상한 240과 정합 — 완료 상세와 동일 천장 |
| 세션 상세 | 종목 48 · 종목당 64 · 총 240 | 2,000,000 | sessionDetail / get_session_detail | 쓰기 상한과 동일값 — 저장은 되는데 못 여는 조합 금지 |
| 월 로그(데스크톱) | 31일 × 30종목/호출 · 즐겨찾기 >30은 ≤5회 분할 병합 | 900,000/호출 | logTableMonth / get_log_table_month(_v2_core) | 계약 최대 930행 × 실측형 행 ~700B ≈ 650KB + 여유 ~38% (D7 — 종전 180KB는 산수 결함) |
| 종목 카탈로그 | 4,096 | 2,400,000 | exerciseCatalog / get_exercise_catalog | 현행 1,072행·성장 추세 반영 2배 상향 (D8) — 초과 시 페이지네이션 도입이 다음 수단 |
| 종목 카탈로그 변경분 | 1,000/페이지 (기본 500) · 동기화당 ≤8쪽 | 2,400,000/페이지 | exerciseCatalogChanges / get_exercise_catalog_changes | 이슈 #1238 Phase 3 — 항목 모양이 전체 카탈로그와 같아 천장 동일; 평소 응답 0~수 행, 8쪽 안에 못 끝나면 전체 카탈로그로 |
| 검색 정렬 신호 | mine 512 · usage 4,096 | 600,000 | exerciseSearchSignals / get_exercise_search_signals_v1 | mine = 한 사용자가 완료한 종목 수 실측 최대 ~130의 4배(최신순이라 초과분은 가장 오래된 것부터 절삭·플래그) · usage = 카탈로그 행 상한과 동일(사용자 수 내림차순 절삭·플래그) · 4,096×~75B + 512×~110B ≈ 363KB + 여유 65% |
| 볼륨 리포트 | 일 3,660 · 기간 1,670 · 연간 210 | 3,000,000 (3층 통일) | volumeOverview / get_volume_overview(선택자·annual_v3·v3_core) | 계약 상한 행수 만재 시에도 수용 (D7) — 층마다 다르던 천장(2.1/1.8MB) 정리 |
| 프로필 피드 | 20/페이지 (keyset) | 3,000,000/페이지 | profileFeed / get_profile_feed | 종전 유지 — 페이지네이션의 표준형 |
남은 방향 (이번 트랙 범위 밖, 장부에만 기록)
- 바이트 천장 초과의 전면 "절삭+표시" 전환 (D7 후반): 지금은 재예산으로 즉시 위험만 제거했다. 화면별 서버 절삭 루프는 별도 트랙.
- 세션 상세 열람 가드: 절삭 전환 대신 쓰기 상한과 동일값 유지 — 상세는 편집 원본이라 잘린 데이터로 편집·저장하면 세트가 사라진다(데이터 손실). 위반 세션은 쓰기 계약상 생길 수 없고, 인입 경로는 자체 절삭한다.
그룹 보드 reps 0..1,000해소(#975 D2, 20260831160000): 세트 reps는 #950이, 이종 복합 동작별 reps(parts[].reps)는 #975가 1,000 → 2,000으로 정렬 — 완료 상한과 일치.- 그룹 화면들의 크기 설계: 절삭 신호(truncated)는 #975 Phase 5-2로 화면까지 배선됨 (보드 댓글 "이전 댓글은 더 있어요"·라운지 "이전 대화는 더 있어요"). 오래된 내역을 더 불러오는 페이지네이션은 여전히 없다 — 보드 계약 트랙에서 처리.
새 한도를 추가할 때
- 이 장부에 행을 추가한다 — 값·정의 위치·산정 근거·(있으면) 실측.
tests/react/limitsRegistry.test.mjs에 잠금 행을 추가한다.- 사용자를 거부하는 한도라면, 클라 사전검사와 원인 문구(
PUBLIC_ERROR_MESSAGES)를 함께 단다.