종목 대표 기록 지표 계약 (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_rm | kg | 측정 1RM (+ nRM 표, 추정 1RM 보조선) | user_exercise_pr_events(target_reps 1..20) | 연결됨 |
횟수만 (all: [reps], 맨몸·밴드) | max_reps | 회 | 실측 최고 반복수 | user_exercise_max_rep_observations.max_rep_observed_floor_reps → user_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_time | m | 최장 거리(대표 한 숫자) + 기준 거리별 최단 시간 표 | 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-erg | 500m · 1km · 2km · 5km · 10km |
bike (실내자전거·에르그·에어바이크) | 5km · 10km · 20km · 30km · 40km |
swim | 100m · 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_growthunit: '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행. - 나의 기록 목록 카드: 종전대로 등급표 단위(
reps)·best_reps사용 — Phase 1에서 손대지 않음(잔여: 이 계약으로 통일). - 데스크톱 종목 상세: 미적용(잔여).
서버 계약
get_exercise_pr_detailsummary에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_overviewreport_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_rm | kg | user_manual_pr_records | 종전(추정 1RM보다 크면 대표, PR 이벤트 주입) |
max_reps | 회(정수) | user_manual_record_metrics | user_exercise_max_rep_pr_timeline_v1에 관측과 합류 |
max_hold | 초 | 같음 | record_metric_hold_pr_timeline_v1 |
distance_time | m + 초(그 거리의 시간) | 같음 | 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_idnull,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 목록의 직접 입력 행은 태그 "직접 입력", 세션 열기 없음.