대표 기록 지표 Phase 3·4 — 버티기 최고 시간·유산소 최장 거리와 거리별 최단 시간 (2026-09-02)
- 기간: 2026-09-02 (1세션, #1100 트랙 이어서 — 오너 지시 "phase 3~4 다 같이 진행해줘")
- 랜딩: PR #1131(Phase 3·4,
09a5894c) — 마이그레이션20260902150000_record_metric_hold_cardio_v1, Production 적용 = 릴리스 PR(main → production) - 설계서: 없음 — 실측·설계·예상 효과 = 이슈 #1102·#1103 코멘트
- 정본:
docs/contracts/record-metric.md(판정표·유산소 기준 거리 표·실측 세트 규칙) · SQLcardio_reference_distances_v1(기준 거리 정본) ·record_metric_measured_sets_v1(실측 세트 후보) - 도구: Production 롤백 리허설 스크립트(세션 scratchpad, 레포 밖)
- 게이트: pgTAP
record_metric_hold_cardio_v1(22) · 클라이언트recordMetricPhase3(5)·recordMetric(갱신) - 버그리포트: 없음(기능 추가) — 선행 결함은
bug-057-20260902.md - 계약:
app-screen-rpc-contract.mdget_exercise_pr_detailsummary(measured_max_hold·max_hold_records·cardio_records)·report_year_growth.unit4종 ·records-props.md·report-props.md
Phase 현황 (#1100 트랙 전체)
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 0~2 | 연도 목록 수리·최대 반복수·풀업 병합 | ✅ PR #1110 → 릴리스 v0.11.0 (기록: 2026-09-02-record-metric-1100.md) |
| Phase 3 | 버티기(시간) 종목 = 최고 버티기 시간 | ✅ PR #1131 |
| Phase 4 | 유산소 = 최장 거리 + 기준 거리별 최단 시간 표 | ✅ PR #1131 |
1. 배경
#1100 Phase 1에서 "지표는 필수 입력 조합이 정한다"는 계약을 세우고 횟수 종목(최대 반복수)만 연결했다. 시간만 기록하는 버티기 종목(카탈로그 111종 + 사용자 커스텀, 실사용 매달리기·핸드스탠드 홀드·데드행 등)과 거리 종목(수영 409세트·실내자전거 254·러닝 178·로잉 132)은 기록 지표가 여전히 없었다.
2. 문제 제기
버티기·유산소는 관측 파이프라인이 없다
최대 반복수는 관측 테이블(max-reps 정책 v1)이 있었지만 버티기·유산소는 세트 자체가 유일한 실측이다.
유산소 거리는 임의값이 많다
러닝 5.2·6.7km, 자전거 10.3·12.7km처럼 기준 거리와 정확히 맞지 않는 세트가 많아 "5km 기록"을 정의하려면 규칙이 필요했다. 페이스 환산은 추정이라 원칙 B(PR=실측)에 어긋난다.
3. 해결 방안
원칙 (오너 결정 2026-09-02)
- A·B·C(#1100): 시간만 → 최고 버티기, 거리 → 거리 기준. PR은 실측만.
- 가정 D4(기준 거리 표): run 1/3/5/10/21.1/42.2km · row-erg·ski-erg 500m/1/2/5/10km · bike 5/10/20/30/40km · swim 100/200/400/800m/1/1.5/2km · 그 밖은 최장 거리만. 오너가 바꾸면 SQL 함수 한 곳.
접근
| 대안 | 판단 |
|---|---|
| 관측 테이블·갱신 잡 신설 | 기각 — 사용량이 작고(버티기 ≤ 수십 세트/종목) 사용자×종목 범위 읽기로 충분. 인덱스(sessions user·session_exercises session_id) 확인 |
| 기준 거리 칸에 페이스 환산 시간 | 기각 — 추정치는 PR이 될 수 없다(원칙 B). "기준 거리 이상 ~ 103% 이하" 세트의 실측 시간(실측 상한) + 기록 거리 병기로 대체 |
| 유산소 대표 숫자 = 특정 거리 기록 | 기각 — 종목군·사용자마다 다르다. 최장 거리(실측)를 대표로, 표는 상세에만 |
| 무게+시간(파머스 홀드) 무게별 표 | 보류 — 4종 24세트. 가장 긴 버티기 + 그때의 무게 병기 |
4. 적용한 내용
Phase 3·4 (#1131, 20260902150000)
- 서버:
record_metric_measured_sets_v1(완료 세션 main/top 세트, 세션 기록 프로필 우선, 횟수 프로필 제외) →record_metric_hold_pr_timeline_v1·record_metric_cardio_longest_timeline_v1(이전 최고를 넘긴 세트만)·record_metric_cardio_by_distance_v1(기준 거리별 최단 시간)·cardio_reference_distances_v1.get_exercise_pr_detailsummary 3필드·get_volume_overview_v3_core성장 행 unit seconds/meters(연도·단위별 ≤10, 전체 ≤400). 정본 함수 본문은 스크립트로 추출·치환해 재발행. - 클라이언트:
recordMetric.tsmax_hold·distance_time supported, 어댑터(시간선 최신순·엄격 갱신·기준 거리 창 검증), 매퍼, 종목 상세(히어로·변화 그래프·PR 목록을 지표 종류 하나로 분기 + 거리별 최고 기록 표), 리포트 성장(초·km 표시, kg 4 + 비kg 2 자리, 비kg는 회→초→km 순),formatDistanceMeters, 마커prDetailMaxHold·prDetailLongest·prDetailCardioTable.
주요 결정과 그 근거
- 유산소 대표 = 최장 거리: 무게 종목의 1RM처럼 한 숫자가 필요하고, 실측 세트 단위라 원칙 B를 지킨다. 거리별 표는 nRM 표의 대응물.
- 기준 거리 창 +3%: 정확값이 많은 로잉·수영은 그대로, 러닝·자전거의 5.1km류가 5km 칸에 "실측 상한"으로 들어간다. 기록 거리를 병기해 오해를 막는다.
작업 중 드러난 것
- 디자인 마커 검사기는
data-lg-hook={변수}를const hook = …지역 변수의 문자열 리터럴로만 인식한다 — 변수 이름을hook으로. - 성장 행
previous_value는 numeric 원값(5100.000)이 그대로 나가므로round(…,2)로 맞춰야 kg·reps 행과 표기가 같다. - Production 리허설에서 오너 계정에 수영 기록이 없어 표가 비었다 — 수영 409세트는 다른 사용자(성근) 것.
5. 적용 결과
| 항목 | 결과 |
|---|---|
| pgTAP 시나리오 | 버티기 60→75→90(웜업 120 제외, 70 갱신 아님), 러닝 최장 5.0→5.1→10km, 5km 칸 = 5.1km 29:00, 10km 칸 = 65:00, 4.8km 제외, 성장 seconds·meters 각 2행 — Production 리허설 동일 |
| 오너 실데이터(리허설) | 러닝 1km 5:58·3km 30:00(3.08km 기록)·5km 35:00, 최장 8km; 버티기 최근 기록 6건(60초·30초…); 리포트 성장 seconds/meters 26행; 볼륨 응답 617KB(상한 3MB) |
| 로컬 게이트 | check 전체·unused·build 통과 |
| 미검증 | Production 적용 후 실측(릴리스 뒤), 화면 시각 품질(SSR 렌더로만 확인) |
6. 이번 개선으로 향상된 것
모든 종목 유형에 기록이 있다
무게(1RM)·횟수(최고 반복수)·버티기(최고 시간)·유산소(최장 거리 + 거리별 최고) — 화면은 계약 하나(record-metric.md)로 판단한다.
구조적으로 남는 것
- 실측 세트 후보 함수 하나(
record_metric_measured_sets_v1) 위에 시간선·표·성장이 얹혀 있어 규칙 변경(예: 창 3% → 5%)이 한 곳이다. - 기준 거리 표가 SQL 함수 한 곳에 있어 오너 결정으로 바로 바꿀 수 있다.
남은 것
- 기준 거리 표는 가정 D4 — 오너 확정 대기(바꾸면 함수 한 곳).
- 무게+시간 종목의 무게별 버티기 표, 칼로리 기준 표(원칙 C 후순위).
- 데스크톱 종목 상세·나의 기록 목록 카드는 아직
record-metric계약을 읽지 않는다.