자유 기록 통계 줄 — 종목 단위 무게×총횟수 입력('총계' 세트 타입) (2026-08-31)
- 기간: 2026-08-31 (세션 1, 오너 문답 3라운드로 범위 확정 — "자유 기록에서 통계반영용 볼륨 나중에 입력" → "종목 정하고, 세트별 구분 없이 종목 단위로" → "입력은 무게×총횟수, 21-15-9면 그냥 45번" → 적용 범위 "자유 기록에 붙이기" 확인 → go)
- 랜딩: PR #1008 — 마이그레이션
20260831180000Production 적용 완료(postcheck 통과), 랜딩 직렬화 절차(§15) 준수 - 설계서: 없음(이슈 #1003 본문의 분석·Phase 계획이 정본, "예상 효과·개선사항" 표 포함)
- 정본:
src/react/ui/shared/noteStatLines.ts(줄 완성 판정) ·barbelicCopy.tsAGGREGATE_SET_TYPE('총계' 라벨 — 선택 메뉴 밖) ·completedWorkoutWriteContract.tsCOMPLETED_WORKOUT_SET_TYPES(+total) · 전개는barbelicMappers.tsnote 분기 - 도구: 없음
- 게이트:
tests/react/noteStatLines.test.mjs(전개·거부·타입 봉인·서버 필터·배선 앵커 5건) + designContractnoteStatLines등재 - 버그리포트: 없음(신규 기능 — 결함 수리 아님)
- 계약: exercise_sets.set_type warmup|main|top → + total · e1RM 관측 추출(refresh_user_strength_estimation_projection_core)이 total 제외 · 완료 쓰기 DTO note에
statLines키(클라 전개 전용 — 서버 payload로는 나가지 않음)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 서버 — 'total' 타입 수용 + 관측 추출 제외 필터(20260831180000) | ✅ PR #1008 · Production |
| Phase 2 | 클라 — 자유 기록 통계 줄 입력(모바일·데스크톱)·전개·표시·잠금 | ✅ PR #1008 |
| Phase 3 | 게이트·문서 | ✅ 본 PR |
1. 배경
와드형 확장 논의(#988, 보류)에서 갈라져 나온 트랙. 유저 A가 "크로스핏 클래스"처럼 세트를 하나하나 적지 않는 운동을 자유 기록으로만 남기면 그날 볼륨 통계가 0이었다. 오너 요구: 자유 기록에 "종목 + 무게 × 총횟수" 줄을 붙여(21-15-9 스러스터 = "40kg × 45회") 볼륨에 반영, 나중 입력도 가능하게. 적용 범위는 자유 기록 전용(일반 기록 UI 비노출).
2. 문제 제기
- 자유 기록(entry_kind='note')은 세트가 없어 모든 통계에 불참 — 우회는 "종목 1개+세트 1줄 대충 입력"뿐인데, 이건 세트 수·PR·추정 1RM을 오염시킨다(45렙짜리 한 세트가 e1RM 후보로 들어감).
- 별도 "수동 볼륨 컬럼"을 만들면 총량 집계 RPC 전부(홈·하루·주월·리포트·월로그)에 두 번째 합산 원천을 심어야 한다(~8~10본 재발행).
3. 해결 방안
원칙 (오너 확정)
- 입력은 볼륨 숫자가 아니라 무게 × 총횟수(볼륨은 계산). 세트별 구분 없음, 여러 줄 허용(무게 다르면 줄 추가).
- 적용 범위: 자유 기록 전용 — 일반 종목 기록·계획·그룹 보드에는 비노출.
- 값 발명 금지: 불완전한 줄은 저장 게이트가 막고 이유 문구로 안내(#975 틀).
접근 — 세트 타입 '총계'(total) 하나로
저장 형태 = 종목 행 + '총계' 타입 세트 1줄. schema.sql의 set_type 사용 214곳 전수 분류 실측으로, 기존 통계 산식이 이미 원하는 방향으로 갈라져 있음을 확인했다: 볼륨 = sum(stats_effective_load × reps)(타입 무관 — 자동 합산), 세트 수·메인 렙수 = set_type='main' 필터(자동 제외), PR·탑세트·Max Rep 관측 = in ('main','top')(자동 제외), 맨몸 유효 무게 = 기존 트리거. 집계 RPC 재발행 0으로 끝난다.
4. 적용한 내용
- Phase 1 (서버): set_type CHECK에 total 추가. 작업 중 드러난 함정 — e1RM 관측 추출(insert)에 타입 필터가 없어 전 타입을 관측 테이블로 나르는데, 관측 테이블 CHECK는 warmup|main|top뿐이라 total 세트가 있으면 통계 갱신 전체가 실패한다. 추출에
set_type <> 'total'필터를 동반(마지막 재발행 본문 그대로 + 한 줄 델타, postcheck가 이전 조건 생존까지 단언 — 되덮기 3연전 교훈). - Phase 2 (클라): 완료 쓰기 매퍼의 note 분기가 statLines를 [종목 행 + 총계 세트]로 전개 — 값 검증은 기존 정본 재사용(무게 ≤999,999.99·총횟수 1~2,000, 원인 문구 자동). 모바일(WfStatLines)·데스크톱(DkpNoteCard) 편집 카드에 "통계 반영 (선택)" 줄 — 종목 synonym 검색, 무게×총횟수 2칸, 삭제. 완료 기록(라이브·지난 기록) 전용, planMode(계획·보드) 비노출. 초안 왕복·저장 직렬화의 키 화이트리스트 2곳(BUG-024 무늬)에 statLines 명시 등재. 수정 재진입 시 총계 세트는 '총계' 라벨 고정(타입 변경 잠금 — 바꾸면 통계 의미가 달라짐), 일반 세트 타입 메뉴(SET_TYPE_OPTIONS)에는 미등재.
5. 적용 결과
| 항목 | 전 | 후 |
|---|---|---|
| 자유 기록만 남긴 날 볼륨 | 0 (통계 불참) | 무게×총횟수 합산 (예: 40kg×45회 = 1,800kg) |
| 우회 입력(가짜 1세트)의 통계 오염 | 세트 수 +1·e1RM 후보 오염 | 총계 타입 = 세트 수·PR·e1RM 자동 제외 |
| 집계 RPC 재발행 | (별도 컬럼 안이면 ~8~10본) | 0본 — 타입 필터 실측으로 회피 |
| 검증 | — | check 전 체인 2,235/2,235 · tsc 0 · postcheck Production 통과. CI 미실행(Actions 결제 차단 지속 — 로컬 전 체인 대체) |
6. 이번 개선으로 향상된 것
- 세트 타입 4종째('총계')가 생겨, 이후 "집계형 기록"이 필요한 표면이 스키마 변경 없이 재사용 가능.
- e1RM 관측 추출의 타입 무필터가 봉합돼, 향후 타입 추가 시 같은 함정이 postcheck로 잡힌다.
남은 것
- 피드 카드 표시: 피드의 세트 카드 조회(get_session_presentation_mains_json_v2)는 main/top(없으면 warmup)만 골라, 총계 세트만 있는 종목은 피드 카드에 이름만 뜨고 세트 줄이 비어 보인다 — 본인 세션 상세·통계는 정상. 노출 수요 확인 후 후속.
- 오너 실사용 확인(문구·입력 느낌) — 자동 증거(테스트·postcheck·Production 적용)는 통과.