kg 표기 최소단위 1kg — "총 62,667.86kg"에서 전 표면 정수 표기까지 (2026-08-31)
- 기간: 2026-08-31 (세션 1, 오너 보고 "kg 소숫점 아래로 쓰는경우 거의 없으니까 앱내 모든 부분에서 1kg 단위로 반올림해줘" — 스크린샷: 일지 월 헤더 "총 62,667.86kg")
- 랜딩: PR #998(
0ae0d0f6, Phase 1~4) — 마이그레이션 없음, Vercel 자동 배포. Actions 결제 블로커로 CI 미실행, 오너 선례 지시로 admin 머지 - 설계서: 없음(이슈 #994 본문의 Phase 계획이 정본, "예상 효과·개선사항" 표 포함)
- 정본:
src/react/kgDisplay.ts(roundKg·kgNum·unitNum — 무게 표기 유일 통로) + "무게 최소단위 1kg" 규칙(오너 D1) - 도구: 없음(전수 감사는 세션 내 병렬 탐색)
- 게이트:
tests/react/kgDisplayRounding.test.mjs(헬퍼 계약 + 대표 경로 + raw toLocaleString+kg 스윕 가드) - 버그리포트: 없음(결함 아닌 표기 정책 트랙)
- 계약: 없음(계약 문서 변경 없음 — 규칙은 kgDisplay.ts 헤더 주석+가드 테스트가 정본)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 공용 반올림 포매터 + 적용 규칙 확정 | ✅ PR #998 |
| Phase 2 | 모바일 전 표면 적용(12파일) | ✅ PR #998 |
| Phase 3 | 데스크톱 전 표면 적용(15파일) | ✅ PR #998 |
| Phase 4 | 가드 테스트 + 전체 회귀 | ✅ PR #998 |
1. 배경
볼륨·합계는 체중 반영 계수(체중 71.6kg × 0.25)와 lb→kg 환산이 섞여 계산되는데, 화면은 계산 결과를 반올림 없이 toLocaleString()으로 찍었다. 일지 월 헤더 "총 62,667.86kg" 같은 소수 노출이 표면마다 제각각(어떤 곳은 이미 Math.round, 어떤 곳은 소수)이었고, 오너가 전면 반올림을 지시했다.
2. 문제 제기
kg 표기가 자리마다 다른 정밀도로 찍혔다
포매터가 화면별 사설 함수 10여 개(hmT·fdT·rpFmt·dkh3Fmt·dkSrchFmt·rcVal·dkbRmVal·roundPrValue…)로 흩어져 있었고, 반올림 여부가 함수마다 달랐다(없음/0.1/1). 전수 감사 결과 모바일·데스크톱 30개 파일 ~90지점.
3. 해결 방안
원칙 (오너 결정, 2026-08-31)
- D1 "무게의 최소단위는 무조건 1kg" — 처음 제안했던 "유저 입력 원본(세트 62.5kg)은 예외" 안을 오너가 기각. 세트 무게·수동 PR 포함 예외 없이 표시 반올림.
- 저장·합산·계산은 원값 유지(표시 직전 1회만 반올림 — 중간 반올림 누적 오차 방지). lb 표기·입력칸 편집 중 값·차트 축 k/t 축약(100kg 단위)·관리자 데이터 점검 화면은 대상 아님.
접근
- 지점별 땜질 대신 계층 중립 정본 헬퍼 1개(src/react/kgDisplay.ts — ui·매퍼 공용, barbelicCopy 선례)로 전 지점 관통 + 신규 지점 방지 스윕 가드.
unitNum은 단위 필드가 같이 다니는 값(PR 등)에서 kg일 때만 반올림(회·cal 통과). - 전수 목록은 병렬 탐색 에이전트의 감사로 확보, 적용도 데스크톱 절반은 병렬 위임 후 검수.
4. 적용한 내용
Phase 1~3 (#998)
- 헬퍼 신설 + 모바일 12파일(홈·일지·리포트·피드·기록 상세·검색·그룹·운동 기록·온보딩 체중 확인)·데스크톱 15파일(홈·일지·로그테이블·볼륨·PR 보드/상세·검색·플랜 에디터·위젯·잔디)·공용 3파일(measureSemantics의 세트 값 텍스트는 kg 원자만, bodyweightSemantics 체중 태그, GrassCal)·매퍼(barbelicMappers — roundPrValue 0.1 정밀도 폐기→1kg, formatTopSet·티커 문자열) 관통.
- lb 이중 분기(플랜 에디터)는 kg 가지만, 유산소 원자(거리·시간·cal)는 불변.
Phase 4 (#998)
kgDisplayRounding.test.mjs: 스크린샷 사례(62,667.86→62,668) 헬퍼 계약, 대표 경로(세트 62.5→63kg·피드 볼륨·체중 태그), 스윕 가드(ui에서 kg 표기를 raw toLocaleString으로 찍는 줄 금지, Math.round 동반 줄 허용, 관리자 제외) — 가드가 감사 누락 1지점(WorkoutRecordParts 완료 버블)을 실적발해 함께 수리.
주요 결정과 그 근거
- 반올림 위치 = 표시 직전 1회: 합산 전에 반올림하면 표면마다 총합이 달라진다.
- 부위별 볼륨 합 ≠ 총합 ±1kg 허용(각각 반올림) — 오너에게 부작용으로 고지.
작업 중 드러난 것
- main 경계 게이트가 이미 깨져 있었다: #996이 recording.ts 주석에 쓴 표현이 게이트의 bare 토큰 검사에 오탐(#947 recordingRequirement 오탐과 동일 유형). Actions 결제 블로커로 CI가 안 돌아 랜딩 시 미검출 — 주석 리워딩 1줄로 이 PR에서 원복(#932 스레드 통지). CI 블로커 기간의 admin 머지는 로컬에서 check 전체를 돌려야 이런 파손을 막는다.
- 표시 정밀도를 단언하던 기존 테스트 1곳(mappers 83.5kg)이 새 규칙과 충돌 — 기대값 동반 갱신(84kg).
5. 적용 결과
| 항목 | 결과 |
|---|---|
| 일지 월 헤더 총 볼륨 | 62,667.86kg → 62,668kg |
| 세트 무게 표시(입력 원본 62.5) | 62.5kg → 63kg (저장은 62.5 유지) |
| PR/추정 1RM 표시 정밀도 | 0.1kg(roundPrValue) → 1kg |
| kg 포매터 | 화면별 사설 ~10개(정밀도 제각각) → 정본 헬퍼 1개 + 스윕 가드 |
| 테스트 | 전체 2,229건 통과(신규 가드 4건), tsc·dead-css·경계(원복 후)·unused·manifest 통과 |
| eslint | 미검증 — 로컬 파손 + Actions 결제 블로커(선행 PR들과 동일 상태) |
| main 경계 게이트 | 실패(오탐, #996 유래) → 원복 통과 |
6. 이번 개선으로 향상된 것
무게 표기가 앱 전체에서 한 규칙
유저가 어느 화면을 봐도 kg는 정수 — "총 62,667.86kg" 같은 소수 노출 소거. 구조적으로 남는 것: kgDisplay 정본 헬퍼 + "새 kg 표기 지점은 헬퍼 경유" 스윕 가드(우회 시 테스트 실패).
남은 것
- 없음(트랙 범위 내). 트랙 밖 잔여 = GitHub Actions 결제/한도 복구(오너) — 복구 후 첫 CI에서 eslint 자연 판정.