Skip to content

한도 정본 (Limits Registry)

이슈 #938 Phase 4 (2026-08-31). 사용자 데이터에 걸린 모든 저장·표시 한도의 정본 장부다. 값을 바꿀 때는 이 문서와 코드 정의를 함께 바꾼다 — tests/react/limitsRegistry.test.mjs가 이 표의 값과 코드 상수·SQL 본문이 일치하는지 잠근다(어긋나면 CI 실패).

원칙 (오너 결정, 이슈 #938 Phase 0)

  1. 한도를 넘은 데이터는 거부·에러가 아니라 안내·절삭·페이지네이션으로 처리한다. 성능 한도의 원래 목적은 응답 크기이지 사용자 차단이 아니다.
  2. 같은 한도는 한 곳에서 정의하고 클라·서버가 같은 값을 쓴다. 클라 정본은 completedWorkoutWriteContract.ts(쓰기)·appPerformanceBudgets.ts(읽기), 서버 정본은 supabase/schema.sql의 마지막 재발행이다.
  3. 새 한도는 산정 근거를 함께 적는다. "몇 행 × 행당 몇 바이트 + 여유 몇 %" 없이 숫자만 적힌 한도는 이 장부에 들어올 수 없다.
  4. 여유폭을 주기적으로 잰다. node scripts/audit-limit-headroom.mjs — Production 실측 최대값 대비 여유가 20% 아래로 내려간 한도를 찾아낸다(#252의 "여유 2세트" 재발 방지).

쓰기 한도 (넘으면 저장 거부 — 클라가 원인 문구로 사전 안내)

한도정의 위치 (클라 / 서버)산정 근거실측 최대 (2026-08-30)
계획 세션 총 세트240COMPLETED_WORKOUT_WRITE_LIMITS.maxExpandedSets / enforce_planned_session_set_limit·validate_plan_payload_v3_structure_core완료 세션과 동일값 (D1) — 계획이 완료보다 작을 이유 없음. 클라 사전검사 LG_PLAN_SET_LIMIT계획 21 · 완료 p99 34
완료 세션 종목 수48maxExercises / validate_completed_workout_payload_v4_structure_core·enforce_completed_session_exercise_limit실측 최대 13의 3.7배 (D2). 원인 문구 LG_WORKOUT_EXERCISE_LIMIT13
완료 종목당 세트64maxSetsPerExercise / 위 구조 검증기·enforce_completed_exercise_set_limits실측 최대 30의 2.1배 (D2) — 종전 32는 여유 2세트뿐이었다. 원인 문구 LG_WORKOUT_EXERCISE_SET_LIMIT30
완료 세션 총 세트240maxExpandedSets / 위 구조 검증기·트리거유지 (D2) — 실측 최대 59의 4배. 원인 문구 LG_WORKOUT_SET_LIMIT59
세트당 반복(reps)2,000maxReps / validate_session_payload_v5_core(구 validate_completed_workout_payload_v4·validate_plan_payload_v3exercise_set_part CHECK 2종줄넘기·더블언더 수용 (D3) — 실측 최대 215의 9배. 완료 경로 원인 문구 LG_WORKOUT_REPS_LIMIT215
세트 간 휴식3,600초 절삭maxRestSeconds(매퍼가 절삭) / clamp_exercise_set_rest_seconds 트리거(절삭)·CHECK(안전망)거부 아님 (D4) — 앱이 자동 측정하므로 자리 비움이 저장을 막으면 안 됨1,013초
계획 제목 / 메모512B / 4,096BsavePlan 사전검사(LG_PLAN_TITLE_LIMIT·LG_PLAN_NOTE_LIMIT) / session CHECK(계획도 같은 테이블의 status = planned 행, 이슈 #1215)완료 세션 제목·메모와 동일값
완료 제목 / 메모 / 세트 메모512B / 4,096B / 1,024BmaxSessionTitleBytes·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.99kgmaxLoad·savePlan 사전검사(LG_PLAN_LOAD_LIMIT)·완료 경로 원인 문구 LG_WORKOUT_LOAD_LIMIT / numeric(8,2)스키마 자릿수300kg대
보조 무게(assist_kg)0 초과~400kgmaxAssistKg / exercise_set_part_atomic_assist_check(계획 세트도 같은 테이블 — 구 planned_sets_atomic_assist_check 소멸)·validate_session_payload_v5_core·보드 검증기체중 상한 400kg과 동일값 — 보조 무게는 체중에서 빼는 값이라 체중을 넘길 이유가 없다 (이슈 #1200 D3)
무게 배수(load_multiplier)1 또는 2loadMultiplierOf(허용값 밖은 1) / exercises_load_multiplier_check·session_exercise_part_load_multiplier_check·training_effective_load덤벨 2개·더블 케틀벨만 2 — 유저는 한 개 무게를 적고 볼륨만 ×2 (이슈 #1200 D1)
기록 방식 원자 수3maxRecordingFields / 원자 CHECK 3종 + byte 묶음 CHECK 2종제품 정의(무게·보조·횟수·시간·거리·칼로리 중 ≤3 조합) — 유산소류 3원자 프로필 수용 (이슈 #932 D3, 채움 판정은 required_inputs가 별도 담당)전 카탈로그 ≤3
즐겨찾기 저장128prFavorites.maxFavorites / replace_user_exercise_favorites종전 유지 — 월 로그 호출(30/호출)은 분할로 수용 (D6)7
1RM 직접 입력0 초과~5,000kgsaveManualPr 사전검사(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·RPC24시간 = 하루 상한 (이슈 #1194)
직접 입력 유산소 거리 / 시간1~1,000,000m / 1~86,400초동 함수 사전검사(LG_RECORD_DISTANCE_LIMIT·LG_RECORD_SECONDS_LIMIT) / 동 CHECK·RPC1,000km·24시간 — 울트라 종목 수용 (이슈 #1194)
체중20~400kgsaveProfileBodyMetric 사전검사(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_searchD5 — 하드컷 폐지, 페이지 수집 상한 40(=4,800세션)
계획 상세세트 2402,000,000plannedSessionDetail / get_planned_session_detail쓰기 상한 240과 정합 — 완료 상세와 동일 천장
세션 상세종목 48 · 종목당 64 · 총 2402,000,000sessionDetail / get_session_detail쓰기 상한과 동일값 — 저장은 되는데 못 여는 조합 금지
월 로그(데스크톱)31일 × 30종목/호출 · 즐겨찾기 >30은 ≤5회 분할 병합900,000/호출logTableMonth / get_log_table_month(_v2_core)계약 최대 930행 × 실측형 행 ~700B ≈ 650KB + 여유 ~38% (D7 — 종전 180KB는 산수 결함)
종목 카탈로그4,0962,400,000exerciseCatalog / 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,096600,000exerciseSearchSignals / get_exercise_search_signals_v1mine = 한 사용자가 완료한 종목 수 실측 최대 ~130의 4배(최신순이라 초과분은 가장 오래된 것부터 절삭·플래그) · usage = 카탈로그 행 상한과 동일(사용자 수 내림차순 절삭·플래그) · 4,096×~75B + 512×~110B ≈ 363KB + 여유 65%
볼륨 리포트일 3,660 · 기간 1,670 · 연간 2103,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로 화면까지 배선됨 (보드 댓글 "이전 댓글은 더 있어요"·라운지 "이전 대화는 더 있어요"). 오래된 내역을 더 불러오는 페이지네이션은 여전히 없다 — 보드 계약 트랙에서 처리.

새 한도를 추가할 때

  1. 이 장부에 행을 추가한다 — 값·정의 위치·산정 근거·(있으면) 실측.
  2. tests/react/limitsRegistry.test.mjs에 잠금 행을 추가한다.
  3. 사용자를 거부하는 한도라면, 클라 사전검사와 원인 문구(PUBLIC_ERROR_MESSAGES)를 함께 단다.