Skip to content

횟수 기반 종목의 기록 지표 — 종목 상세 연도 목록 수리·최고 반복수 연결·풀업=스트릭트 풀업 병합 (2026-09-02)

  • 기간: 2026-09-02 (1세션, 오너 보고 "횟수 기반의 운동이 리포트에 잘 안잡혀있는 문제가 있어. 특히 풀업이 그럼… 2024년에 풀업을 1만번 했는데도 반영이 안되어있음")
  • 랜딩: PR #1110(Phase 0~2, db596f82) — 마이그레이션 20260902100000_record_metric_max_reps_v1·20260902110000_pull_up_identity_merge_v1, Production 적용 = 릴리스 PR #1113 [Release v0.11.0](2026-09-02 04:52 UTC, 실측 확인 — 이슈 [반영완료] 닫음)
  • 설계서: 없음 — 분석·Phase 계획·예상 효과 = 이슈 #1100 본문(정정 2회 포함)
  • 정본: docs/contracts/record-metric.md(대표 기록 지표 판정표·표면별 적용) · src/react/recordMetric.ts(단일 판정 함수) · records-props.md 연도 스테퍼 규칙 · user_exercise_max_rep_pr_timeline_v1(실측 최대 반복수 PR 시간선)
  • 도구: Production 롤백 리허설 스크립트(세션 scratchpad dryrun*.mjs, 레포 밖) — 마이그레이션 본문을 DO 블록 안에서 실행하고 끝에 raise로 원자 롤백
  • 게이트: pgTAP record_metric_max_reps_v1(20)·pull_up_identity_merge_v1(12), 클라이언트 mobileRecordsDetailYearStepper(4)·recordMetric(6)·recordMetricPhase1(5), 리포트 성장 어댑터 (연도, 단위)별 상한
  • 버그리포트: bug-report/bug-057-20260902.md
  • 계약: app-screen-rpc-contract.md get_exercise_pr_detail summary(measured_max_reps·max_rep_recordsreport_year_growth.unit · report-props.md report.growth

Phase 현황

Phase내용상태
Phase 0모바일 종목 상세 연도 목록 = 훈련한 해 전부✅ PR #1110
Phase 1대표 기록 지표 계약 + 맨몸 종목 최고 반복수 연결(상세 히어로·변화 그래프·PR 목록·리포트 성장)✅ PR #1110
Phase 2풀업 ↔ 스트릭트 풀업 정체성 병합(+ 체스트 투 바 쌍)✅ PR #1110
Phase 3버티기(시간) 종목 최고 버티기 시간⬜ 별도 이슈 #1102
Phase 4유산소 기준 거리별 최단 시간 표⬜ 별도 이슈 #1103

1. 배경

앱의 기록 지표는 무게 종목의 1RM 하나였다. 카탈로그에는 무게+횟수 523종 외에 횟수만 기록하는 맨몸·밴드 종목 302종, 시간만 기록하는 버티기 종목 111종, 거리·시간 유산소 26종이 있는데, 이들의 "기록"은 화면 어디에도 정의돼 있지 않았다. 종목 상세·리포트·PR 목록은 각자 1RM 유무로 화면을 갈랐고, 그 결과 오너가 4년간 기록한 풀업(풀업 계열 2024년 4,966회)이 종목 상세에서 올해 것만 보였다.

2. 문제 제기

모바일 종목 상세가 연도 목록을 "추정 1RM이 있는 해"로만 만들었다

RecordsDetail.tsx의 연도 스테퍼는 estimatedByYear 키만 썼다. 맨몸 종목은 추정 1RM이 없어 목록이 비고 올해 하나로 채워졌다. 서버 available_years(2022~2026)는 미리 받아 두고도 쓰지 않았다. 데스크톱은 합집합을 써서 정상. v59 배선(#279)부터 잠복.

맨몸 종목의 기록 지표가 없었다

측정 PR 이벤트·추정 1RM 관측은 외부 중량 > 0 세트만 후보다. 최대 반복수 관측(user_exercise_max_rep_observations, 정책 max-reps v1)은 쌓이고 있었지만(풀업 99건·최고 30회) 나의 기록 목록 카드에만 쓰였다. 리포트 "주요 종목 기록 성장"은 1RM 이벤트만 읽었다.

풀업 기록이 두 정체성으로 갈라져 있었다

WodUp "Strict Pull-Up"(global:88) → 스트릭트 풀업 aa96524f…, "Pull-Up"(global:6) → 풀업 498579ef…. 2024년 스트릭트 풀업 3,886회 vs 풀업 50회. 종목 상세는 종목 하나만 집계한다(#1060에서 별칭 합산 폐지).

3. 해결 방안

원칙 (오너 결정 2026-09-02)

  • A. "대표 기록 지표는 필수 입력 조합에서 자동으로 정한다" — 무게+횟수=1RM / 횟수만=최대 반복수 / 시간만=최대 버티기 / 무게+시간=무게별 최대 버티기 / 거리=기준 거리별 최단 시간. 오너: "원래 내가 하려던 방향과 정확히 일치".
  • B. "추정치는 무조건 추정에만. PR기록은 무조건 실측만 해당."
  • C. "유산소의 거리 x 시간이 중량운동의 무게 x 반복수에 정확히 일치… 일단 기준은 거리로."
  • D. "보통 풀업이라 하면 스트릭트니까… 키핑을 따로 빼고 풀업은 스트릭트 풀업이랑 합치는게 맞지 않을까" + "'스트릭트 풀업'이라는 명칭 자체는 '풀업'의 synonym중 하나로 남아있어야해".
  • E. Phase 0~2 한 트랙, Phase 3·4 별도 이슈.

접근

대안판단
연도 목록 = availableYears ∪ estimatedByYear ∪ yearStatsByYear(데스크톱 규칙)채택 — 서버 무변경
최대 반복수 PR을 새 테이블·파이프라인으로 물질화기각 — 이미 쌓이는 관측에서 읽기 시 파생(시간선 함수)으로 충분. 필요 시 후속 물질화
1RM PR 이벤트 테이블에 회 단위 행을 섞기기각 — nRM 상태·어댑터가 target_reps 1..20을 전제, 오염 위험
리포트 성장 행 정렬을 단위 무관 %로 통일기각 — kg 행 종전 순서 유지, kg 최대 4 + 회 최대 2 자리 규칙
병합 시 살리는 id = 스트릭트(등급표 보유·사용 124행)기각 — 공식 slug 해시로 id를 해석하는 경로(Motra 매핑·기본 즐겨찾기 시드·테스트)가 있어 이름과 맞는 pull-up 해시를 살리고 등급표·역할 소속을 옮김
정책 exercise-model 버전 올림미실시 — 병합은 2.0.0 계약의 merge_or_split 조항 적용이지 계약 변경이 아님(20260821380000 선례와 동일)

4. 적용한 내용

Phase 0 — 연도 목록 (#1110)

RecordsDetail.tsx 스테퍼 목록 합집합(내림차순, 비면 올해), records-props.md availableYears 행 신설. 렌더 테스트 4건(수리 전 main에서 2건 실패 확인).

Phase 1 — 대표 기록 지표 (#1110, 20260902100000)

  • 계약 record-metric.md + recordMetricFor(one_rm·max_reps 연결, max_hold·distance_time 예약 = supported:false). 논리식 트리 순회는 recordingRequirement.ts가 소유(트리 키 토큰이 ts-boundary 게이트에 걸림).
  • 서버: user_exercise_max_rep_pr_timeline_v1(이전 최고를 넘긴 실측 관측만, 동률 제외, as_of 상한), get_exercise_pr_detail summary measured_max_reps·max_rep_records(최신순 ≤ 64, 계약 v4 유지), get_volume_overview_v3_core report_year_growth unit(kg|reps) + 회 행 연도별 ≤ 10. 정본 함수 본문을 스크립트로 추출·치환해 재발행(손 복사 0).
  • 클라이언트: 어댑터(추가 필드 검증·(연도,단위)별 상한 10·전체 200), 매퍼(measuredMaxReps·maxRepRecords·recordMetric), 종목 상세(히어로 "최고 반복수 N회"·"최고 반복수 변화" 그래프 5회 눈금·회 단위 PR 목록), 리포트 성장(회 표시·자리 규칙). 마커 prDetailMaxReps 등록.

Phase 2 — 풀업 병합 (#1110, 20260902110000)

20260821380000 절차 그대로: 잠금 → 세션 엔트리·플랜 세트(%기준 종목 포함)·수동 PR·즐겨찾기(dedupe/densify)·외부 매핑·해석 구성요소·아키타입 대표·역할 소속(행 이동, 삭제 아님 — 등급표 metadata가 참조)·등급표·프로페션 표준 구성원 배열 → synonym 이동(흡수 default '스트릭트 풀업' → 메인 비-default) → 흡수 행 삭제(cascade) → 영향 유저 통계 인라인 재생(#1033 명시 신원 경로) + 무결성 검증 → postcheck(사실 총량: 엔트리·세트·횟수 합 보존). 부속: 기본 즐겨찾기 시드 'pull-up' 재발행.

주요 결정과 그 근거

  • 읽기 시 파생 vs 물질화: 관측 테이블에 (user, exercise, observed_on) 인덱스가 있고 종목당 수백 행 규모라 RPC 안 파생이 충분하다. 물질화는 표본이 커질 때 후속.
  • 살리는 id: 공식 종목 id = slug 해시라 "이름 = slug"가 어긋나면 시드·매핑이 조용히 빈다(기본 즐겨찾기 시드는 join이 비어 6개만 심는다). 그래서 이름과 맞는 해시를 살렸다.

작업 중 드러난 것

  • refresh_user_max_rep_projection 직접 호출은 세대 울타리에 막힌다 — pgTAP 시드는 refresh_user_exercise_stats_from(정식 경로)로.
  • exercise_sets_reps_result_check: completed는 target_reps = reps, rep_failure는 reps < target_reps; effort_level이 있으면 effort_scale_version '1.0.0' 필수.
  • 테스트 감사 신고(pending-changes)는 manifest 등재 파일에만 — 미등재 파일을 신고하면 게이트가 거꾸로 실패한다(재확인).
  • Production 롤백 리허설이 pgTAP 로컬 실행을 대신했다: 마이그레이션 본문을 execute $mig$…$mig$로 DO 블록 안에서 돌리고 결과를 raise 메시지에 실어 받는다. 세 차례(Phase 1 시나리오·오너 실데이터·Phase 1+2 연속)로 pgTAP 기대값 일치를 확인.

5. 적용 결과

항목결과
풀업 상세 연도 버튼1개(2026) → 5개(2022~2026) — 오너 실데이터 리허설 available_years [2026…2022]
풀업 상세 히어로1RM "—" → 최고 반복수 30회(2023-12-04), 기록 갱신 7건(5→6→7→8→9→10→30)
풀업 세션 엔트리(오너 계정 포함 전 사용자)118 + 스트릭트 124 → 242, 흡수 종목 행 0, 사실 총량(세트·횟수) 보존 postcheck 통과
영향 사용자 통계 재생3명 정착(오너 95세션·741세트·최고 30회 / 성근 143세션·최고 50회)
리포트 기록 성장회 단위 행 등장(오너: 2023 풀업 9→30회 등), kg 행 순서 불변
로컬 게이트check 전체 2,321건·unused·build 통과
미검증실제 Production 적용(릴리스 후 실측 예정), 그래프·표기의 시각 품질(SSR 렌더로만 확인)

6. 이번 개선으로 향상된 것

맨몸 종목도 "기록"이 있다

풀업·딥스·푸쉬업 사용자가 종목 상세에서 최고 반복수와 그 변화, 회 단위 PR 목록을 본다. 리포트 기록 성장에도 회 행이 오른다.

훈련한 해는 전부 열린다

추정 1RM이 없는 종목(맨몸·밴드·버티기·유산소)에서 종목 상세 연도 이동이 데스크톱과 같은 규칙으로 동작한다.

구조적으로 남는 것

  • 대표 기록 지표 계약 하나(record-metric.md)로 화면이 지표를 판단한다 — 버티기(#1102)·유산소(#1103)는 kind만 붙이면 된다.
  • 종목 병합 절차가 두 번째로 재사용됐다(20260821380000 → 20260902110000). 다음 병합은 이 파일을 복제하면 된다.
  • 마이그레이션 Production 롤백 리허설(pgTAP 시나리오 재현)이 로컬 Docker 없이 검증하는 표준 경로로 굳었다.

남은 것

  • Phase 3 버티기 #1102, Phase 4 유산소 #1103.
  • 데스크톱 종목 상세·나의 기록 목록 카드는 아직 record-metric 계약을 읽지 않는다(목록 카드는 등급표 단위·best_reps로 동작 중).
  • 옛 스트릭트 풀업 id를 캐시한 앱은 다음 카탈로그 갱신까지 검색에 옛 항목이 보일 수 있다.