Skip to content

종목 대표 기록 지표 계약 (record-metric)

이슈 #1100 · 오너 결정 2026-09-02 (A·B·C). 구현: src/react/recordMetric.ts · 서버 20260902100000_record_metric_max_reps_v1.sql.

원칙

  • A. 지표는 종목의 필수 입력 조합이 기계적으로 정한다. 종목마다 사람이 지정하지 않는다. 필수 입력 논리식(required_inputs)이 있으면 그것이, 없으면 recording profile 전체가 필수 입력이다. 예외가 필요하면 종목 속성으로만 덮어쓴다(아직 없음).
  • B. PR(기록)은 실측만. 추정치는 추정 전용. 1RM의 추정 1RM 선(e1RM)은 그래프 보조선으로 유지하되 PR이 아니다. 최대 반복수 추정치(estimated_max_reps_*)와 버티기 추정치도 같다.
  • C. 유산소의 거리×시간 = 중량의 무게×반복수. 대표 기록의 기준은 거리(칼로리는 후순위).
  • 모든 화면(나의 기록 목록 카드·종목 상세 히어로·변화 그래프·PR 목록·리포트 기록 성장·등급표)은 이 계약 하나를 읽는다. 화면별로 지표를 따로 판단하지 않는다.

판정표

필수 입력 조합kind단위대표 기록서버 원천상태
무게 + 횟수 (all: [load, reps])one_rmkg측정 1RM (+ nRM 표, 추정 1RM 보조선)user_exercise_pr_events(target_reps 1..20)연결됨
횟수만 (all: [reps], 맨몸·밴드)max_reps실측 최고 반복수user_exercise_max_rep_observations.max_rep_observed_floor_repsuser_exercise_max_rep_pr_timeline_v1연결됨 (Phase 1)
시간만 (any: [duration])max_hold실측 최고 버티기 시간record_metric_measured_sets_v1(완료 세션 main/top 세트, duration_seconds) → record_metric_hold_pr_timeline_v1연결됨 (#1102)
무게 + 시간 (all: [load, duration])max_hold가장 긴 버티기(그때의 무게 병기) — "무게별" 표는 사용량이 작아 보류같음(load_kg 동반)연결됨 (#1102)
거리 (+시간·칼로리) (any: [distance, duration(, calories)])distance_timem최장 거리(대표 한 숫자) + 기준 거리별 최단 시간 표record_metric_cardio_longest_timeline_v1 · record_metric_cardio_by_distance_v1연결됨 (#1103)
그 밖 (무게+거리, 횟수+시간 등)none없음미정
  • 중량 맨몸 종목(중량 풀업·중량 딥스: all: [load, reps], 체중 계수 1)은 추가 중량 기준 1RM이다(등급표도 추가 중량 기준).
  • 논리식이 문법 위반이면 profile로 판정한다. 논리식·profile 둘 다 없으면 종전 기본(무게+횟수)이다.
  • supported: false인 kind(none)는 화면이 "아직 지표 없음"으로 비운다(잘못된 kg 표시 금지).

유산소 기준 거리 (원칙 C — 거리 × 시간 = 무게 × 반복수, 기준 = 거리)

정본 = SQL public.cardio_reference_distances_v1(archetype_id) 한 곳. 가정 D4(2026-09-02) — 오너가 바꾸면 그 함수만 고친다.

종목군(archetype)기준 거리
run (러닝·트레드밀·걷기·하이킹·계단 등)1km · 3km · 5km · 10km · 21.1km · 42.2km
row-erg / ski-erg500m · 1km · 2km · 5km · 10km
bike (실내자전거·에르그·에어바이크)5km · 10km · 20km · 30km · 40km
swim100m · 200m · 400m · 800m · 1km · 1.5km · 2km
그 밖(캐리·슬레드·핸드스탠드 워크)표 없음 — 최장 거리만
  • 한 세트가 기준 거리 칸에 드는 조건: 기록 거리 ≥ 기준 거리 이고 ≤ 기준 거리 × 1.03. 시간은 기록된 값 그대로(페이스 환산 없음 — 원칙 B). 5.1km 30:00은 5km 칸에 "30:00(5.1km 기록)"으로 든다(실측 상한). 4.8km는 어느 칸에도 들지 않는다.
  • 칸마다 최단 시간 1건(동률은 먼저 달성한 날). 칼로리 기준 표는 후순위.

버티기·유산소 실측 세트 (원칙 B)

record_metric_measured_sets_v1: 완료 세션의 main/top 세트, 프로필 = 세션 기록 프로필(session_exercise_part.recording_fields) 우선·없으면 카탈로그. 횟수(reps)가 프로필에 있으면 제외(그건 최대 반복수 관측의 몫). 시간선 규칙(이전 최고를 넘긴 세트만, 동률 제외, as_of 상한)은 최대 반복수와 같다.

표면별 적용 (Phase 1 = max_reps)

  • 종목 상세 히어로: kind === "max_reps"면 1RM 자리 = 최고 반복수 N회(RM 시트 없음). 값 = currentMaxReps.value.
  • 변화 그래프: 최고 반복수 시간선(maxRepRecords 오름차순), y 눈금 5회 단위, 라벨 "현재 최고 N회".
  • PR 목록: maxRepRecords 최신순 — 날짜 · 최고 · N회 · +Δ회.
  • 리포트 탭 기록 성장: 서버 report_year_growth unit: 'reps' 행 → 화면 단위 . kg 행 최대 4 + 회 행 최대 2(한쪽이 비면 6까지).
  • 종목 상세(max_hold, #1102): 히어로 최고 버티기 1:30(mm:ss, 1시간 이상 h:mm:ss), 변화 그래프 30초 눈금, PR 목록 최고 · 1:30 · +0:10.
  • 종목 상세(distance_time, #1103): 히어로 최장 거리 12.6km, 변화 그래프(km 눈금), PR 목록 최고 · 12.6km · +1.2km, 그 아래 거리별 최고 기록 표(날짜 · 5km (5.1km 기록) · 최단 · 29:00).
  • 리포트 탭 기록 성장: unit: 'seconds', 'meters'km(1/1000 환산, 소수 1자리). 비kg 행끼리는 회 → 초 → km 순으로 최대 2행.
  • 나의 기록 목록 카드: 종전대로 등급표 단위(repsbest_reps 사용 — Phase 1에서 손대지 않음(잔여: 이 계약으로 통일).
  • 데스크톱 종목 상세: 미적용(잔여).

서버 계약

  • get_exercise_pr_detail summary에 measured_max_reps·max_rep_records(최신순 ≤ 64) 추가 — docs/data/app-screen-rpc-contract.md. #1102·#1103: measured_max_hold·max_hold_records(≤ 64), cardio_records { longest, longest_records(≤ 64), by_distance(≤ 8) }.
  • get_volume_overview report_year_growth 행에 unit 추가 — kg | reps | seconds | meters, 연도·단위별 ≤ 10(전체 ≤ 400).
  • 시간선 규칙: 같은 종목의 관측을 observed_on, session_created_at, set_position, source_set_id 순으로 훑으며 이전 최고를 넘긴 관측만 기록으로 남긴다. 동률은 기록이 아니다. as_of 이후 관측은 제외.

직접 입력 (이슈 #1194, 오너 결정 2026-09-03 — #1184 D2)

유저가 바벨릭을 쓰기 전에 세운 기록을 종목의 지표대로 직접 넣는다. 무게 1RM은 종전대로 user_manual_pr_records(save_manual_pr), 나머지 세 지표는 별도 표 user_manual_record_metrics(save_manual_record_metric_v1·delete_manual_record_metric_v1) — 두 표는 섞이지 않는다(1RM 통계 체인 무변경).

kind직접 입력 값합류
one_rmkguser_manual_pr_records종전(추정 1RM보다 크면 대표, PR 이벤트 주입)
max_reps회(정수)user_manual_record_metricsuser_exercise_max_rep_pr_timeline_v1에 관측과 합류
max_hold같음record_metric_hold_pr_timeline_v1
distance_timem + 초(그 거리의 시간)같음record_metric_cardio_longest_timeline_v1(최장 거리) + record_metric_cardio_by_distance_v1(기준 거리 칸, 같은 ±3% 규칙)
  • 지표는 서버가 판정한다: record_metric_kind_v1(exercise_id) = 이 문서 판정표의 SQL 판(클라이언트 recordMetricKindFor와 1:1). 클라이언트가 보낸 metric_kind는 대조용이며 다르면 거부(22023). 무게 1RM 종목(one_rm)은 save_manual_pr로 보내라고 거부, none은 입력 불가.
  • 시간선 합류 규칙: 직접 입력 행은 세션 세트와 같은 "이전 최고를 넘긴 것만 기록" 창을 탄다. 정렬 열은 recorded_at, 날짜 모름(null)은 모든 관측보다 앞(1RM 기준값과 같은 의미). payload 행에 manual: true, source_set_id = 직접 입력 행 id, session_id null, achieved_on은 날짜 모름이면 null(세션 행은 종전대로 날짜 필수). 최신순 정렬에서 null은 맨 뒤.
  • 한도: docs/data/limits-registry.md (회 1~10,000 · 초 1~86,400 · m 1~1,000,000 · 시간 1~86,400). 클라이언트 사전검사 원인 코드 LG_RECORD_*.
  • 화면: 모바일 "내 1RM 기록 관리"(이름 유지, 오너 Q1) 목록은 두 표를 합쳐 지표별 표기(120kg · 22회 · 1분 30초 · 5.05km 25:00), 입력 폼은 지표별 값 칸. 데스크톱 PR 보드 "1RM 직접 입력" 모달도 같은 분기(오너 Q3). 종목 상세 PR 목록의 직접 입력 행은 태그 "직접 입력", 세션 열기 없음.