유산소 세트 시간·거리 입력 — 초·m 단일 입력에서 분|초·km|m 토글과 [분]:[초] 입력까지 (2026-08-25)
- 기간: 2026-08-25 (1세션, 오너 지시 원문: "유산소 세션에서 초단위 말고 분+초 단위로 시간 입력할 수 있게 옵션 추가해줘" + 실기기 피드백 3회 8건)
- 랜딩: PR #790(Phase 1,
edfc04ce) · PR #793(Phase 3,eecea0a5) · PR #796(Phase 4,04b5f2d3) · PR #804(Phase 5,0827b2b3) — 마이그레이션 없음, Vercel 자동 배포 - 설계서: 없음(소건) — Phase 계획서·예상 효과 표는 이슈 #788 본문
- 정본:
src/react/ui/mobile/screens/WorkoutRecordParts.tsxWfMinSecInput([분]:[초])·WfKmInput(km)·WfMeasureJoin(in 구분자) · 종목별 상태durOv·distOv(WorkoutRecord.tsx) · 표시행 구분자formatSetMeasurements(measureSemantics.ts) - 도구: 없음
- 게이트:
tests/react/workoutRecordMinSecDuration.test.mjs(회귀 10건) - 버그리포트: 없음(기능 건) — 같은 보고 턴에서 나온 별건 도크 결함은 BUG-028(이슈 #801)로 분리
- 계약: 변경 없음 —
durationSeconds(초)·distanceMeters(m)가 저장 정본 그대로, 마커 유지
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 분·초 입력 + 종목별 시간 단위 토글, 3표면 배선 | ✅ PR #790 (edfc04ce) |
| Phase 2 | CI·머지·작업 기록 | ✅ PR #791 (2d1e78d7) |
| Phase 3 | 오너 실기기 피드백 4건 — MM:SS 단일 입력·분|초 토글·옵션 행 필드 게이트·in 구분자 | ✅ PR #793 (eecea0a5) |
| Phase 4 | 오너 실기기 피드백 2차 3건 — 콜론 고정 분·초 분리 입력·자동 0 제거·in 글씨 통일 | ✅ PR #796 (04b5f2d3) |
| Phase 5 | 오너 지시 — 거리 km|m 토글·km 입력 + 표시행 구분자 in 일반 적용 | ✅ PR #804 (0827b2b3) |
1. 배경
유산소(러닝 등 시간 기록) 종목의 세트 시간 입력이 "초" 단일 필드였다. 20분 러닝을 기록하려면 1200을 암산해 입력해야 했고, 표시(formatDurationSeconds)는 이미 mm:ss라 입력과 표시의 단위가 서로 달랐다. 오너가 2026-08-25 분+초 입력 옵션을 지시했다(이슈 #788).
2. 문제 제기
시간 입력만 원자 단위(초) 그대로라 사용자가 환산을 대신했다
무게는 kg/lb/% 토글(unitOv), 휴식은 분·초 2필드(patchRest)가 이미 있었지만, 세트 시간은 세 입력 표면(세트 편집 행·빠른 추가·라이브 세트 컴포저) 모두 초 단일 입력뿐이었다.
3. 해결 방안
원칙 (오너 지시, 2026-08-25)
"초단위 말고 분+초 단위로 시간 입력할 수 있게 옵션 추가" — 저장 정본(durationSeconds)은 손대지 않고 입력 표면만 바꾼다.
접근
| 대안 | 판단 |
|---|---|
| 분·초 2필드를 한 칸에 + 종목별 초|분:초 토글, 기본값은 데이터 추론 | 채택 — kg/lb unitOv 패턴 재사용, 계약·그리드 불변 |
| 분:초를 무조건 기본으로 | 기각 — 오너 요청이 "옵션", 초 단일에 익숙한 흐름 보존 |
| mm:ss 텍스트 파싱 단일 입력 | 기각 — number 키패드 상실, 파싱 오류 표면 추가 |
| localStorage 사용자 설정 | 기각 — UI 계층 no-storage 계약 위반, 데이터 추론으로 사실상 지속성 확보 |
4. 적용한 내용
Phase 1 — 분:초 입력 + 종목별 시간 단위 토글 (#790)
WfMinSecInput(WorkoutRecordParts): 분·초 2필드 한 칸. 합성값은 기존mPatch(durationSeconds) 경로 그대로, 초 필드 0–59 클램프(patchRest와 동일 규칙), 소수점 초 유지, 외부 값 변경(아래 세트 동일 적용)은 드래프트 합성값 불일치 감지로 재파생.- 표면 3곳 배선: 세트 편집 행 · 빠른 추가 컴포저 · 세트 컴포저(라이브).
data-lg-field="workout.set.duration"마커는 한 칸 래퍼에 유지(designContract 레지스트리 동일). - 토글: 편집 옵션 "시간 단위" 행(
wfx-seg) + 컴포저 "세트 내용" 캡 우측 세그. 종목별 상태durOv, 기본값 = 해당 종목 세트에 60초 이상 시간 보유 시 분:초(kg/lb 데이터 추론과 동일 패턴). - CSS:
.minsec한 칸 2입력 + 편집 행 여백 조정(입력폭 23→28px).
주요 결정과 그 근거
- 기본값 데이터 추론: 토글 상태는 세션 로컬(UI no-storage 계약)이라 지속성이 없다. 유산소 기록은 사실상 전부 60초 이상이므로 "기존 기록 보유 → 분:초"가 매 세션 자동 복원과 같은 효과를 낸다 —
unitOv(loadLb 보유 → lb)와 같은 전례. - 한 칸 2입력: 세트 편집 행이 고정 그리드(
20px 60px 1fr 13px 1fr 30px)라 필드 수가 늘면 파손 — 기존 필드 박스 안에서만 나눴다.
Phase 3 — 오너 실기기 피드백 반영 (#793)
오너가 계획 작성 화면 스크린샷과 함께 4건을 지시했다(2026-08-25 채팅):
- 옵션 행 필드 게이트 — "러닝에 무게 단위가 나와야 할 이유는 없잖아?" → 무게 단위 행은 기록 프로필에
load가 있을 때만 노출. 옵션 행은 기록 필드를 따라간다는 원칙이 섰다. - 토글 선택지 = 분 | 초 — "분:초" 라벨 대신 두 단위 이름, 분 우선.
- 분 모드 = MM:SS 단일 입력 — "MM분 SS초 대신 MM:SS. 입력하는 부분이 조금 작으니까" → 2필드를 버리고 타이머식 자릿수 밀기 한 입력("3030" 타이핑 → "30:30" 표기, blur 시 "0:90" → "1:30" 정규화). 숫자 키패드에 콜론이 없어 자유 텍스트 파싱 대신 자릿수 밀기를 골랐다.
- 거리 다음 시간 구분자 × → in — "5km in 30:00" 의미.
WfMeasureJoin이 거리→시간 사이만 in, 그 외 × 유지. 편집 행 13px 구분자 컬럼에 맞게j-in(10.5px) 축소.
Phase 4 — 오너 실기기 피드백 2차 반영 (#796)
Phase 3 배포 화면(MM:SS 자릿수 밀기 단일 입력)을 보고 오너가 3건을 추가 지시했다(2026-08-25 채팅):
- 콜론 고정 + 분·초 분리 입력 — "입력칸 하나에 한번에가 아니라, 가운데 :는 고정, 왼쪽 클릭=분· 오른쪽 클릭=초" → 자릿수 밀기를 버리고
[분]:[초]두 입력. 콜론은 고정 텍스트, 분 우측·초 좌측 정렬로 콜론에 붙는 타이머 표기. 초 칸은 0–59 클램프(휴식 입력과 동일)·소수점 허용. - 자동 0 제거 + 탭 시 전체 선택 — 값 0·빈 값은 두 칸 모두 빈 칸, 탭하면
select()로 기존 텍스트 전체 선택 상태에서 바로 덮어쓴다. 저장된 값의 표기(20:00의 "00")는 유지 — 자동 삽입이 아니라 실제 값. - in 글씨 통일 — in 구분자를 표면별 단위 라벨(m·kg)과 동일 값으로: 편집 행 12px/600(=setbox-unit), 빠른 추가 11px/600(=qa-line qa-unit), 컴포저 11px/700(=spv-unit). computed style 실측으로 일치 확인.
Phase 5 — 거리 km|m + 표시행 in 일반 적용 (#804)
오너 지시(2026-08-25 채팅, 라이브 화면 스크린샷): "거리도 km, m를 구분할 수 있게 해줘".
WfKmInput: km 단일 입력 — [분]:[초]와 같은 규칙(자동 0 없음·탭 전체 선택·소수점·blur 정규화), 저장은distanceMeters(m 정본) 환산(3.5 → 3500).- 종목별 거리 단위 토글(km|m): 편집 옵션 "거리 단위" 행 + 컴포저 캡(시간 세그와 나란히 — 321px 캡에 세그 2개 84+76px 수납 실측). 기본값 = 1km 이상 기록 보유 시 km(같은 데이터 추론 패턴).
- 표시행 구분자도 in: 오너 스크린샷의 세트 행이 "3500m × 30:00"으로 입력(in)과 어긋나 있어, 같은 지시("거리 x 시간인 경우 × 말고 in")를
formatSetMeasurements까지 일반 적용 — 세트 행·완료 메모·피드 카드·세션 상세·데스크톱 일지의 유산소(거리+시간) 조합만 in, 무게×횟수는 × 그대로.
작업 중 드러난 것
- 분·초를 매 렌더 합성값에서 파생하면 초 필드의 소수점 타이핑("34.")이 끊긴다 — 드래프트 보유 + 외부 변경만 재동기화가 정답(컨트롤드 파생의 고전 함정).
- ESLint 게이트는 억제 주석에
-- 사유(영어 관행) 그리고eslint-baseline.jsonsuppressionsByFile등록을 함께 요구한다 — 사유만 붙이면 "new ESLint suppression requires review"로 다시 떨어진다. - PowerShell 5.1에서 한글·따옴표 섞인 본문을 gh/git 인자로 직접 넘기면 깨진다 —
--body-file/-F파일 경유가 정본 절차.
5. 적용 결과
| 항목 | 결과 |
|---|---|
| 20분 입력 | "1200" 환산 암산 → 분 칸에 "20"(초 칸은 빈 칸 유지 가능) |
| 기존 기록(예: 1830초) 재편집 표기 | 1830 단일 → [30]:[30], 탭하면 해당 칸 전체 선택 |
| 빈 값·0 표기 | "0:00" 자동 채움 → 두 칸 모두 빈 칸 |
| 시간 입력폭(375px 실측) | 컴포저 분·초 각 60 → 84px, 편집 행 각 28 → 34px(콜론 고정) |
| 러닝 편집 옵션 | 무게 단위 행 노출 → 비노출(load 프로필에만) |
| 거리×시간 구분자 | × → in (편집 행 13px 컬럼 수납 실측) |
| 저장값·통계·표시 | 변경 없음(초 정본 그대로) — 회귀 테스트로 고정 |
| 거리 입력(예: 3.5km) | m 환산 암산("3500") → km 칸에 "3.5" |
| 표시행 구분자(세트 행·피드·상세) | "3500m × 30:00" → "3500m in 30:00" (무게×횟수는 × 유지) |
| 컴포저 회귀(기존 4건+신규 10건)+영향권 13파일 171건 | 전부 통과, CI verify 4회(#790·#793·#796·#804) 통과 |
| 375px 레이아웃 실측(로컬 vite+실 CSS+SSR 실마크업) | 3표면 신규 가로 오버플로 0 |
| 실기기 화면 왕복 | 오너 iOS 실기기 확인 완료(2026-08-26) — 피드백 2회(Phase 3·4) 반영 뒤 "잘 적용되었어", 세션 종결 지시 |
6. 이번 개선으로 향상된 것
유산소 기록 입력이 표시 단위와 같아졌다
입력(분+초)과 표시(mm:ss)의 단위가 일치 — 환산 암산 제거.
구조적으로 남는 것
WfMinSecInput·WfKmInput— durationSeconds·distanceMeters를 편집하는 다른 표면(향후 데스크톱 등)에서 재사용 가능한 단위 입력 짝.WfMeasureJoin(입력)·formatSetMeasurements(표시) — 기록 항목 구분자 규칙(거리→시간 = in)의 소유처 2곳.- "옵션 행은 기록 필드를 따라간다" — 편집 옵션 노출 원칙(무게 단위 = load 프로필에만).
workoutRecordMinSecDuration.test.mjs— 초 단일/분(MM:SS) 두 모드 + in 구분자의 마크업 회귀 가드.
남은 것
- 종결 — 오너 실기기 확인(2026-08-26), 이슈 #788 닫음.
- 데스크톱 계획 편집기 시간·거리 입력(DesktopPlanEditorCardParts)은 단일 필드 그대로 — 별도 트랙.