Skip to content

디자인 배송 v85 — 종목 상세 대표 기록 지표 표현 · 세션 상세 수행 강도 칼럼 · 종료→저장 단일 카드 · 내 정보 편집 행 · 그룹 보드 직행 (2026-09-03)

  • 기간: 2026-09-03 (세션 1개, 오너 지시 원문 "이거 디자인 반영좀 부탁할게" + 배송 zip 바벨릭 (4).zip = delivery-20260903, base 5e5bca44)
  • 랜딩: PR #1167 (Phase 1~4, ede02361) · PR #1169 (Phase 6, f8fec25e) — 마이그레이션 없음, Vercel 배포 O
  • 설계서: 없음(디자인 배송 — 정본은 zip 안 PR-DRAFT.md · src/react/ui/CHANGE-NOTES.md). 분석·Phase 계획 = 이슈 #1166 본문
  • 정본: src/react/ui/mobile/lib/recordMetric.ts(종목 상세 표기 규칙 — 매퍼 kind 입력 어댑터) · src/react/ui/mobile/lib/IntensityGuide.tsx(세트 강도 가이드 모달) · docs/contracts/records-props.md §대표 기록 지표 · docs/contracts/group-props.md(onOpenBoard) · docs/contracts/profile-screen-props.md(onChangePhoto·onSaveName)
  • 도구: 반입 감사 = 3-way 시험 병합(git merge-file) + CSS 헝크 감사 서브에이전트 1회 + 로그인 없는 브라우저 하네스(워크트리 루트 일회용, 커밋 제외)
  • 게이트: designContract(마커 13건) · check:dead-css(동적 클래스 in-0…in-10·in-over) · check-eslint 억제 베이스라인 · recordMetricPhase1/3 · compositeSessionDetailDisplay · workoutFinishAutoSave · groupScreens · profileHandle · controllerBoundaries(프로필 가드 5) · e2e error-cases/CASE-028(재저장 경로)
  • 버그리포트: bug-066-20260903.md — 복귀 바 뒤 추가 세트 강제 종료 소실(스테이징에서 발견, Production 미배포). 되돌림 4건은 반입 감사에서 차단
  • 계약: records-props.md §대표 기록 지표 = main 데이터 계약(recordMetric.kind·currentMaxReps/currentMaxHold/cardioLongest·cardioByDistance·rank.unit)로 개정 · group-props.md onOpenBoard?(id) 신설, 보드 헤더 "오늘" 칩 · profile-screen-props.md onChangePhoto?(file)·onSaveName?(name) 신설

Phase 현황

Phase내용상태
Phase 0반입 감사 — 3-way 충돌 19곳·되돌림 4건·마커 미등록 9건·종목 상세 이중 구현 발견, 오너 결정 D1~D3✅ 이슈 #1166 본문
Phase 13-way 반입 + 되돌림 복원 + 마커 등록 + 테스트 재조준 5파일✅ PR #1167 (56489354)
Phase 2종목 상세 디자인을 main 데이터 계약 위에 이식(어댑터)✅ PR #1167 (5252c10c)
Phase 3컨테이너 배선(보드 직행·사진·이름) + e2e CASE-028✅ PR #1167 (fa0e0a5d)
Phase 4브라우저 하네스 실렌더 9화면
Phase 5PR 1개·CI 1회·머지 → [스테이징] + 이 기록✅ PR #1167 (ede02361)
Phase 6"한 세트만 더할게요" 복귀 뒤 초안 저장 재개 — 추가 세트 강제 종료 소실 수리(오너 표면 점검 질문에서 발견)✅ PR #1169 (f8fec25e)

1. 배경

디자인 환경(Claude Design)이 9/2~9/3에 모바일 다섯 화면을 손본 배송 delivery-20260903을 냈다. 종목 상세(대표 기록 지표 — 풀업·플랭크·러닝처럼 1RM이 없는 종목의 최고 기록 표현), 세션 상세(세트마다 수행 강도 칼럼과 가이드 팝업), 운동 종료 흐름(확인 카드 하나가 저장까지 보여줌, 완료 화면 복귀 바), 내 정보(사진·이름·아이디 변경 행), 그룹(목록에서 보드 직행).

배송 base(5e5bca44) 이후 main은 47커밋 전진해 있었고, 특히 같은 주제를 main이 병행 구현하고 있었다: 이슈 #1100·#1102·#1103·#1110·#1142가 서버 → 매퍼 → 종목 상세까지 대표 기록 지표를 이미 연결해 둔 상태였다.

2. 문제 제기

2-1. 종목 상세는 같은 기능이 두 곳에서 따로 만들어져 있었다

유저 A가 풀업 상세를 열면, main은 매퍼가 내려준 recordMetric.kindcurrentMaxReps로 "최고 반복수 12회"를 그린다. 배송본은 서버가 best·bestTable·fields를 준다는 전제로 화면을 다시 짰다. 배송본을 그대로 넣으면 그 필드가 없어 풀업·플랭크·러닝 상세의 최고 기록·그래프·PR 목록이 전부 "—"로 비고, 오너가 이미 정한 규칙(무게+시간은 무게별 표 보류, 유산소는 최장 거리가 대표)과도 어긋나며, 테스트 4파일이 깨진다. 배송 PR 초안도 "병합 시 판단 필요"라고 적어 놓았다.

2-2. 이미 랜딩된 수리 4건이 되돌아가 있었다

전부 PR #1049(#928 정리 묶음, base의 조상)가 넣은 코드: 드래그로 세트를 추가하다 브라우저가 포인터를 회수하면 리스너를 정리하는 처리, 그룹 목록 불러오기 실패 시 "다시 시도" 화면(코드+CSS), 달력 월을 넘길 때 그 달의 보드 표시를 불러오는 콜백. v84 반입과 같은 유형 — 디자인 환경이 #1049 이전 본문 위에 새 작업을 얹었다.

증거: git log -S"pointercancel" -- WorkoutRecord.tsx 등 → 도입 커밋 7fd716e6, git merge-base --is-ancestor 7fd716e6 5e5bca44 참.

2-3. 계약 마커 미등록 13건과 사라진 마커 1건

배송이 새로 붙인 data-lg-* 이름(복귀 바·강도 가이드·프로필 편집 카드·이름/아이디 모달·누적 모달 등)이 장부(designContract.ts)에 없었고, main의 prDetailCardioTable(테스트 단언 있음)은 배송이 뺐다.

2-4. 컨테이너 배선이 없는 콜백 3건

"운동하러 가기"(onOpenBoard), "프로필 사진 변경하기"(onChangePhoto), "이름 변경하기"(onSaveName)는 화면만 오고 컨테이너 쪽 동작이 없었다. 이름은 서버에 저장 경로 자체가 없었다(온보딩 완료 RPC만 표시명을 씀).

3. 해결 방안

원칙 (오너 결정 D1~D3, 2026-09-03)

  • D1 "D1은 권고. 디자인은 모양만 맞추면 돼" — 종목 상세 데이터 계약은 main(서버 연결 완료분) 유지, 디자인은 표현만 이식. 기각 = 배송 계약대로 서버·매퍼 재작업.
  • D2 무게+시간 종목(파머스 홀드 등)은 "가장 긴 버티기 + 그때 무게 병기", 무게별 표는 보류. 기각 = 배송의 무게별 표 모달.
  • D3 유산소 히어로 큰 숫자는 최장 거리, 거리별 최단 시간은 3열 표 팝업. 기각 = 최근 갱신된 기준 거리의 최단 시간을 대표로.

접근

대안판단
배송 파일 통째 복사기각 — main 전진분 10파일 소실(#1150 wfx-pctnote 1줄까지), 되돌림 4건 재발
3-way 병합(git merge-file) + 충돌 6곳 수동 해소 + 되돌림 main 복원채택 (Phase 1)
종목 상세: 배송 계약으로 서버·매퍼 재작업기각(D1) — #1100~#1103 되돌림, 규모 큼
종목 상세: 배송 recordMetric.ts를 main kind 입력 어댑터로 축소하고 화면은 main 데이터 필드 유지채택 (Phase 2)
거리별 표를 화면 안 인라인 표로 유지(main 방식)기각 — 디자인은 히어로 탭 → 표 모달. 표 부분을 RcDistanceTable로 분리해 테스트는 컴포넌트 직접 렌더

4. 적용한 내용

Phase 1 — 3-way 반입 + 되돌림 복원 + 마커 등록 (56489354)

  • 충돌 없는 10파일 배송 그대로, 충돌 6곳(SessionSheetBody 3·GroupScreen 2·ProfileScreen 1)은 main 전진분(#1151 복합 세트 한 줄 표기 formatCompositeSetLine·복합 반복수 ssReps, #922 신고 콜백 onReportComment, #1142 성별 설정 행) 보존 위에 디자인 델타 적용.
  • 되돌림 4건 main 기준 복원(2-2). GroupScreenonOpenInfo는 보드 화면이 더 이상 읽지 않아 계약 문서에 명시하고 제거(미사용 게이트).
  • designContract.ts 마커 11건(+Phase 2에서 2건), dead-css-baseline.json 동적 클래스 in-0…in-10·in-over(런타임 조합), eslint-baseline.json 억제 주석 문구 변경 동조.
  • 테스트 재조준 5파일: 세션 상세 합계 문구("총 무게 · 메인세트 n세트 (m회)", 스탯박스 "(n회)"), 라운지 라벨 삭제, 아이디 변경 행, 완료 화면 복귀 바 부활(구 "폐기" 단언 반전), 세트 값 추출 정규식에 강도 버튼 추가. 계약 단언(복합 횟수 합산·존 집계 서버 전용)은 유지.

Phase 2 — 종목 상세 이식 (5252c10c)

  • lib/recordMetric.ts = rcMetric(kind): one_rm | max_reps | max_hold | distance_time | none → 태그(1RM/MAX/HOLD)·값 분해·개선분·누적 라벨·세트 토큰·그래프 눈금·빈 상태 문구. 계산·판정 없음.
  • RecordsDetail.tsx: 종목명 아래 등급 행 rc-rank(등급 단위는 rank.unit), 최고 기록 아래 "YYYY.MM.DD 기록", 누적 숫자 탭 → 연도별 누적 모달(지표/세트수/반복수/훈련일수, yearStatsByYear 그대로), PR 행 태그·개선분, 훈련 기록 세트 토큰(12회 x2·1:30 x3·40kg 1:00 x1·5km 24:31 x1), 거리별 3열 표 모달 RcDistanceTable(export, hook prDetailCardioTable), 그래프는 m 기반(마지막 값 라벨 "현재 1RM 180kg" / "최고 12회").
  • 유지: 1RM 경로 표기·계산, 측정 1RM 계약 문구(measuredPrFrontendContract), 연도 목록 availableYears(#1110), RM 모달.
  • records-props.md §대표 기록 지표를 main 계약 기준으로 다시 씀(배송본 절은 서버가 주지 않는 필드를 적고 있었다).

Phase 3 — 컨테이너 배선 + e2e (fa0e0a5d)

  • groupStore.openGroupBoard(groupId): 목록에서 보드로 직행(라운지 데이터 동시 적재 → 뒤로가기가 빈 라운지에 떨어지지 않음).
  • 이름 변경: barbelicRepository.updateProfileDisplayName(profiles.display_name 직접 갱신, RLS profiles_update_own, 온보딩과 같은 공백 정리·1~24자) → authProfileDomainbarbelicApiprofileController.saveProfileName(remoteProfile 갱신·토스트) → mobileApp onSaveName. 사진 변경은 기존 uploadProfilePhoto 액션 연결.
  • e2e error-cases/CASE-028-finish-back-one-more-set-resave-round-trip: 종료 카드 저장(개정 1) → "한 세트만 더할게요" → 세트 추가 → 재저장(개정 2, 기존 줄 id 유지, 세트 3줄). externalAuthE2eConfig 목록 등재.
  • controllerBoundaries 프로필 가드 수 4 → 5(이름 저장 추가).

Phase 6 — 복귀 뒤 초안 저장 재개 (#1169)

  • 오너가 "에러가 될만한 표면은 없을까?"를 물어 점검하다 발견: 즉시 저장이 끝나면 초안 스냅샷 발행이 멈추는데(BUG-044 방지) 복귀 바로 돌아가도 풀리지 않아, 세트를 더 적다가 앱이 꺼지면 사라졌다.
  • 수리 = mobileReopenedDraftSnapshot: 복귀 뒤 발행되는 초안에 저장된 기록의 editSessionId·expectedRevisionexercisesDirty: true를 싣는다. 복원되면 기존 "완료 기록 수정" 경로(즉시 저장 없음·수정 저장·OCC)로 열려 기록이 두 개 생기지 않는다. 대안(멈춤만 해제 → 기록 중복 / 컨테이너 편집기 의도 전환 → 편집기 재마운트)은 기각.
  • 테스트 workoutFinishBackResumeDraft 신설 + 앵커 2파일 재조준. 상세 = BUG-066.

주요 결정과 그 근거

  • 배송의 metricKindOf(fields) 판정을 버리고 매퍼 kind를 받는 이유: 판정 정본은 src/react/recordMetric.ts·record-metric.md 한 곳이어야 하고(원칙 A), 화면이 다시 판정하면 두 규칙이 갈린다.
  • 거리별 표를 모달로 옮기면서 테스트는 RcDistanceTable 직접 렌더로: 정적 렌더는 탭 뒤 상태를 열 수 없다. 화면 단언은 "인라인 표 없음 + 히어로 탭 가능"으로.
  • records.css는 Phase 1 커밋에서 뺐다가 Phase 2에 넣었다: dead-css 게이트가 Phase 1 시점엔 미사용 클래스 5개를 잡기 때문(Phase 단위 게이트 원칙).

작업 중 드러난 것

  • 디자인 환경이 또 #1049 이전 본문 위에 작업했다(v84와 동일 PR). 배송 base가 #1049 뒤여도 파일 본문은 그 이전일 수 있다 — git log -S로 도입 커밋을 특정하는 헝크 감사가 이번에도 4건을 잡았다.
  • 배송 zip이 "완료 화면 진입 토스트 제거"를 후속으로 적었지만 실측상 그 토스트는 이미 없었다. 배송의 Codex 후속은 실측 판정부터(v83·v84 교훈 재확인).
  • workout.css는 main이 #1150 안내 문구 규칙 1줄을 더해 두어 통째 교체하면 사라진다 — 3-way 필수.
  • 로컬 환경 함정: npm run checkcheck:lint에서 "0 errors" 로 실패 = 억제 주석 문구 변경(베이스라인 문자열 비교). dead-css 허용 목록 추가는 --update로 재생성해야 통과. gh issue comment 본문의 작은따옴표 heredoc 안 $(…)는 그대로 문자열이 된다.
  • Browser 팬이 숨겨진 상태에서는 클릭이 30초 타임아웃 — javascript_tool.click() 호출이 우회. 스크린샷을 배치에 여러 장 넣으면 이미지가 누락된다(화면당 1장).
  • 유닛 tabSwitchLatencyProbe 1건은 main에서도 같은 실패(Windows 줄바꿈 한정, #1160 기록).

5. 적용 결과

항목결과
3-way 충돌19곳 → 0 (RecordsDetail 13은 Phase 2 이식으로 흡수)
되돌림(옛 본문이 지운 랜딩 수리)4건 → 0
미등록 마커13건 → 0(designContract 그린)
배송 그대로 넣을 때 빈 화면풀업·플랭크·러닝 상세 "—" → 값 표시(recordMetricPhase1/3 그린, 렌더 스모크 5종 NaN·undefined 0)
로컬 게이트npm run check 전 단계 + check-typescript-unused Phase 1·2·3 각각 통과(유닛 2,408건 중 1건 = main과 동일 실패)
브라우저 실렌더9화면 콘솔 오류 0(기존 svg height 경고 제외)
복귀 뒤 추가 세트 보존앱 강제 종료 시 소실 → 저장된 기록의 수정 초안으로 복원(Phase 6, 단위 테스트)
CIPR #1167 3차 그린(scope·verify 4m34s·migration-smoke 10m38s — 1차 그룹 보드 e2e 마커 충돌, 2차 CASE-028 줄 id 단언 정정)
미검증CASE-028 로컬 Supabase 실행(CI migration-smoke 레인 결과로 판정) · 다크 모드(앱 토큰 없음) · 실기기 safe-area는 사람 눈으로만 판단 가능

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

1RM이 없는 종목도 상세가 디자인대로 채워진다

풀업(회)·플랭크(초)·러닝(거리)이 같은 화면 문법(등급 행·최고 기록·변화 그래프·PR·훈련 기록)으로 보이고, 값은 서버가 이미 계산해 둔 것을 그대로 쓴다.

세트마다 "얼마나 힘들었는지"가 한눈에

세션 상세에 수행 강도 칼럼이 색 구간으로 정렬되고, 숫자를 누르면 뜻을 설명하는 팝업이 뜬다.

종료 → 저장 → 완료가 카드 하나로 이어진다

"저장됐나?"를 카드가 답하고, 세트를 빠뜨렸으면 완료 화면에서 바로 돌아가 이어 기록할 수 있다(재저장 경로는 CASE-028이 고정).

구조적으로 남는 것

  • 종목 상세 표기 규칙이 lib/recordMetric.ts 한 곳(매퍼 kind 기반) — 다음 디자인 배송은 이 계약을 읽는다.
  • 이름 변경 저장 경로(updateProfileDisplayName)와 그룹 보드 직행 함수(openGroupBoard).
  • 반입 절차 확인: 헝크 감사 + git log -S 도입 커밋 대조는 상시 절차(v65·v84·v85 세 번 연속 되돌림 검출).

남은 것

  • 버티기·유산소의 "총 누적 시간/거리"는 서버 연간 요약(yearStatsByYear)에 합계가 없어 "—". 서버가 durationSeconds·distanceMeters 합계를 주면 화면은 자동으로 채운다(별도 이슈).
  • 배송 노트의 종목군별 기준 거리 목록 확정 요청은 이미 cardio_reference_distances_v1(D4)로 정해져 있어 추가 작업 없음.
  • 데스크톱 종목 상세는 이번 표현 규칙 미적용(기존 잔여).