Skip to content

디자인 배송 v84 — 종목 상세 두 숫자 재편 · 세션 상세 재설계 · 우측 슬라이드 오버레이 공용화 (2026-09-01)

  • 기간: 2026-09-01 (세션 1개, 오너 지시 원문 "이거 디자인 반영좀 해주라" + 배송 zip 바벨릭 (3).zip)
  • 랜딩: PR #1090 (f8af3164) — 마이그레이션 없음, Vercel 배포 O
  • 설계서: 없음(디자인 배송 — 정본은 zip 안 PR-DRAFT.md · src/react/ui/CHANGE-NOTES.md)
  • 정본: src/react/ui/mobile/lib/ListOverlay.tsx(우측 전체 보기 오버레이 공용 문법) · src/react/ui/mobile/lib/PrcTierBadge.tsx(PRC_TIER_DISPLAY 등급 표시 매핑) · src/react/ui/mobile/styles/base/tokens.css(--tabbar-h) · src/react/contracts/designContract.ts(마커 장부)
  • 도구: 반입 감사 = 서브에이전트 워크플로 2회(헝크 감사 7 + 후속 실측 6 + 게이트 1 / 테스트 판정 6 + 적대적 검증 12)
  • 게이트: designContract · dockGeometryContract + e2e/dock-geometry · check:dead-css · check:unused · mobileCssBoundaries · measuredPrFrontendContract · recordsDetailEmptyStateGate · recordsDetailEstimatedChart · statsCentralizationPhase2/4 · mobilePrcTierBadgeRelocation
  • 버그리포트: 없음(회귀가 사용자에게 도달하기 전에 반입 감사에서 차단 — 배포된 결함이 아니다)
  • 계약: 변경 없음. 단 마커 장부에 8종 추가

1. 배경

디자인 환경(Claude Design)이 모바일 종목 상세세션 상세를 재설계한 배송 delivery-20260901b 를 zip 으로 냈다. 이전 배송 v83(관리자 화면)과 달리 이번은 사용자가 매일 보는 두 화면을 통째로 갈아엎는 큰 턴이고, 셸 공통 값(탭바 높이)까지 건드린다.

배송 반입은 파일을 그대로 복사하는 일이 아니다. 디자인 환경은 본 저장소와 별도 사본이라, 최신 상태 재수령이 불완전하면 "새 디자인 + 옛 코드"가 한 파일에 섞여 들어온다. 2026-08-04 v65 에서 실제로 landed PR 여러 건이 되돌아갈 뻔했고, 그때부터 전 파일 헝크 감사가 상시 절차가 됐다([lift-guild-stale-delivery-audit-protocol] 메모리).

2. 문제 제기

2-1. 복합 종목 세트 수정이 옛 화면으로 되돌아가 있었다

유저 A가 "버피 + 풀업" 처럼 여러 동작이 묶인 종목의 세트를 나중에 고치려고 열면, 지금 앱은 동작별로 칸을 따로 열어준다(버피 10 / 풀업 5). 배송본에는 그 코드가 통째로 빠져 있어 합계 한 칸(15회)만 뜨는 옛 화면으로 돌아간다. 저장 로직(completedSessionWriteProjection.ts)은 동작별 값(scheme)을 정본으로 쓰기 때문에, 합계로 고친 숫자는 저장할 때 조용히 버려진다 — 이슈 #928 ⑦로 고쳤던 결함(PR #1049, 7fd716e6)이 그대로 재발한다.

증거: git log -S "compositeScheme" -- src/react/ui/mobile/screens/WorkoutRecordParts.tsx → 도입 커밋 7fd716e6 단독이고, 그 커밋은 배송이 선언한 base 71803de7 의 조상이다. 즉 디자이너 환경이 최신을 받고도 놓쳤다. 배송 노트의 최종 sync 줄은 "WorkoutRecordParts 차이 없음"이라고 적었는데 실제로는 main 이 그날 오전 이 파일을 +32줄 고쳤다.

2-2. "날짜 모름 · 과거 입력" 안내가 사라졌다

유저 B가 가입할 때 "예전에 스쿼트 150kg 했었다"고만 적은 값(날짜 없음)은, 정식으로 측정한 기록과 구분해 알려주게 돼 있다(d.current1RMIsBaseline). 새 히어로가 1RM 아래 날짜 줄 자리를 등급 배지로 바꾸면서 이 안내가 통째로 사라졌고, 데스크톱 상세는 그대로 표시 중이라 두 화면 표기가 어긋났다. 배송 노트에는 이 문구를 내린다는 말이 없다.

2-3. 화면이 사라진 자리를 겨누던 테스트 11건

배송이 오너 승인 하에 폐지한 블록(RM 5셀 · 등급 래더 · 추정 1RM 그래프 · 연간 스탯 4칸 · 분포 도넛)을 "이 블록이 있어야 한다"고 단언하던 테스트들이 함께 깨졌다. 그중 일부는 단순 UI 존재 단언이지만, 일부는 제품 계약을 지키고 있었다 — "없는 값을 공식으로 계산해 채우지 않는다", "서버가 계산한 값을 클라이언트가 재집계하지 않는다".

2-4. npm run check 가 안 도는 CI 전용 게이트

check:unused(TypeScript 미사용 래칫)는 check 체인 밖의 독립 CI 스텝이다. 도넛 게이트 교체로 고아가 된 변수 zN 을 잡는데, 로컬 전체 게이트를 통과시켜도 이 실패는 안 보인다.

3. 해결 방안

원칙 (오너 결정, 2026-09-01)

  • D1 — "이거 디자인 반영좀 해주라": 배송을 그대로 반입한다. 디자인 판단은 되돌리지 않는다.
  • D2 — 훈련 기록 행의 리뷰 말풍선: 배송 노트("유지")와 코드(비노출)가 어긋났고, 오너 재확인 결과 비노출이 맞다. 코드 그대로 두고 근거만 주석에 남긴다.

접근

대안판단
파일을 그대로 복사하고 게이트만 통과시킨다기각 — v65 사고의 재현. 이번에도 실제로 되돌리기 2건이 섞여 있었다
전 파일 헝크 감사 후, 신고 델타는 유지 / 미신고 되돌리기는 main 기준 복원채택
깨진 테스트를 삭제하거나 느슨하게 만든다기각 — 계약 단언과 UI 존재 단언을 구분하지 못하면 다음 회귀를 놓친다
UI 존재 단언만 새 화면으로 조준을 옮기고, 계약 단언은 유지하거나 강화채택

각 판정에 적대적 검증 에이전트를 붙여 "이 수리안이 계약을 슬쩍 약화시키는가"를 반증하게 했다. 12건 중 3건이 반증돼 수정했다.

4. 적용한 내용

반입 (PR #1090 06d1dca4)

배송 계약 v2 검사 4종 통과: base 가 main 의 조상 O · base 이후 main 의 src/react/ui 변경 0건(3-way 병합 불필요) · zip 파일 목록 == manifest(17/17) · 범위 src/react/ui/** 이탈 0.

화면

  • 종목 상세: 2-up 히어로(최고 기록 1RM · 총 누적 볼륨) → 1RM 실측 선그래프(노드 탭 툴팁) → 훈련 발자취(연도 스텝퍼) → PR 기록(최근 5) → 훈련 기록(최근 10). 등급표 = 센터 모달, 1RM 숫자 탭 = RM별 기록 모달(1~10RM), 전체 보기 = 우측 슬라이드 오버레이
  • lib/ListOverlay.tsx 신설 — 홈 PR 전광판(구 바텀 시트)도 같은 문법으로 교체, PR 행 규칙(rc-prw2/pr2)은 primitives.css 공용 승격
  • 세션 상세: 분포 도넛 3종·세트 타입 구분자 비노출 게이트, 세트 행 타이포 축소 + 행간 확대, 무게 칼럼 고정폭으로 × 세로선 일치, 스탯 밴드 2행 그리드, 통계 말풍선, RPE 히트 컬러 복원
  • 운동 플로우: 세션 정보 수정 모달(제목·시작·종료, 시·분 분리 입력 + 범위 검증)
  • 셸: --tabbar-h 55 → 50, 홈 프로필 행 + XP바 단일 탭 영역

되돌린 것 3건 (배송 미신고)

파일조치
WorkoutRecordParts.tsx복합 종목 동작별 횟수 입력 복원(main 기준). 테스트 무수정으로 issue928Cleanup 10/10
RecordsDetail.tsx"날짜 모름 · 과거 입력"을 RM별 기록 모달 캡션으로 복원 — 계약 단언이 원문 그대로 통과
HomeScreen.tsx#1076 검색 엔진 재사용 근거 주석 1줄 복원

게이트 동반 수정

  • 마커 장부에 8종 등록: prDetailHero prDetailRmOpen prDetailRmModal prDetailFeedAllLink prDetailFeedModal prTapeRm1Toggle workoutHeaderEditDialog pr.chart.node
  • 기하 계약: 탭바 50 기준으로 dockGeometryContract(토큰) + e2e/dock-geometry(크롬 104/50, 클리어런스 118/50/64/50)
  • dead-CSS: 런타임 조합 클래스 rpe-c1~4정확 이름 예외로 등록, 재편으로 고아가 된 primitives.css 규칙 2건 삭제
  • 테스트 재조준 11건(아래 표)

후속 (130f61af · 6412a5aa · 74ab9298 · fbfde037)

  • 130f61af — CI 전용 check:unused 가 잡은 고아 변수 zN 제거
  • 6412a5aa — 리뷰 말풍선 비노출에 대한 오너 재확인(D2)을 주석에 기록(동작 변경 없음)

주요 결정과 그 근거

배송이 요청했지만 하면 안 됐던 것 2건.

  1. prTierLadder · prDetailWindowSelect · prChart 마커 등록 해제 — 데스크톱(DesktopPrBoardParts.tsx:355, DesktopPrDetailParts.tsx:451·463)이 여전히 렌더한다. 해제했으면 마커 계약의 역방향 단언이 깨진다. 디자이너는 모바일만 보고 판단한 것으로 보인다.
  2. 등급 이름을 어휘 원본(contracts/gamificationPolicy.ts)에서 "…바벨"로 개정 — 그 이름 배열은 표시용이 아니라 DB가 내려주는 등급 데이터와 대조하는 열쇠다(cutlines{"tier":"아이언"}BARBELIC_TIER_LABELS.indexOf). 원본을 바꾸면 전 종목의 등급 판정이 어긋난다. 배송본이 실제로 택한 방식(원본은 두고 표시 매핑만 추가)이 맞고, 그 매핑을 지키는 테스트를 새로 만들었다.

테스트 재조준 — 계약은 하나도 약화하지 않았다.

테스트무엇이 깨졌나어떻게 옮겼나
recordsDetailEstimatedChart 3건추정 1RM 그래프 폐지실측 1RM 그래프 기준으로. "추정치를 실측으로 부르지 않는다" 단언 신설
measuredPrFrontendContractRM 5셀 폐지RM 모달 1~10 그리드로. "없는 칸을 계산해 채우지 않는다"를 모달·히어로 양쪽에 고정(단언 1 → 3)
recordsDetailEmptyStateGate히어로 마크업 변경새 마크업으로. 단위 미표기까지 추가 단언
statsCentralizationPhase2도넛 비노출"안 그린다" + D1 계약(강도 존 = 서버 세트별 강도, 목적 존 = 서버 stimulus_class, 프론트 e1RM 추정 금지)을 소스 단언으로 잠금
statsCentralizationPhase4연간 스탯 4칸 폐지모바일이 pr_records 를 재집계해 폐지값을 되살리는 것 금지 단언 추가
mobileCssBoundaries감시 선택자 소멸rc-dherorc-duo, 공용 사례를 rc-prw2(홈·상세 공용)로
mobilePrcTierBadgeRelocation 2건표시 매핑 신설 + sm 수식어소유권 단언 유지 + 오너 표기 9종 리터럴 고정 테스트 신설

작업 중 드러난 것

  • npm run check 는 CI 전부가 아니다. check:unused · check:policy-snapshots · npm run build 는 CI 워크플로의 독립 스텝이라 로컬 전체 게이트가 초록이어도 CI 가 빨간불이 될 수 있다. 랜딩 전 node scripts/check-typescript-unused.mjs 를 따로 돌 것.
  • dead-CSS 동적 예외는 하이픈으로 끝나는 이름만 프리픽스로 받는다. rpe-c 처럼 하이픈 없이 숫자가 붙는 조합은 dynamicClasses(정확 이름) 목록에 넣어야 한다 — 프리픽스로 넣으면 deadCssGate.test.mjs 가 "동적 프리픽스는 하이픈으로 끝나야 한다"로 거부한다(넓게 먹는 예외를 막는 규칙).
  • CSS 규칙을 지우고 남긴 주석에 클래스 이름을 그대로 쓰면 안 된다. mobileCssBoundariescommonCss.includes(".rp-big")삭제 주석 문자열에 걸려 거짓 통과했다. 적대적 검증 에이전트가 잡았다. 주석에서 점(.)을 빼는 것으로 해결.
  • 서브에이전트가 만든 임시 테스트 파일이 전체 스위트에 섞인다. 감사 에이전트가 tests/react/__probeRcEst.test.mjs 를 만들어 돌리고 지웠는데, 그 사이 내 npm run check 가 집어 실패 2건으로 보고했다. 유령 실패를 만나면 git status·파일 실존부터 확인할 것.
  • 탭바 높이 같은 기하 값은 로컬 게이트로 끝나지 않는다. 실렌더 단언(TABBAR_OWNERSHIP_ENFORCED — 브라우저에서 잰 탭바 높이 == 토큰)은 npm run test:e2e-viewport 에 있고, 이건 CI 의 migration-smoke 잡(policy-contract.yml:251, full 레인)에서만 돈다. 로컬 npm run check 는 텍스트 계약(dockGeometryContract)까지만 본다.
  • 배송 zip 의 "Codex 후속"은 구현 전 실측 판정부터. 6건 중 2건은 이미 충족(yearPrCount 서버 공급 확인), 2건은 하면 안 되는 요청(위 참조), 2건만 진짜 미결이었다.

5. 적용 결과

항목
종목 상세 첫 화면 정보RM 5셀 + 등급 래더 + 추정 그래프 + 연간 4칸두 숫자(최고 기록 1RM · 총 누적 볼륨) 중심
종목 상세 진입/퇴장탭 전환우→좌 슬라이드 + 좌→우 스와이프 퇴장
PR 전체 보기바텀 시트 2종(홈·상세 각각)우측 슬라이드 오버레이 1종(ListOverlay 공용)
세션 상세 세트 행값 16px · 행간 10px · × 자릿수 따라 흔들림값 14px · 행간 17px · 무게 칼럼 고정폭으로 × 세로선 일치
탭바 높이55px50px (소비처 12곳 자동 동조)
복합 종목 세트 수정동작별 칸 (#1049)동일 — 배송이 되돌린 것을 복원
"날짜 모름 · 과거 입력"히어로에 항상 표시RM별 기록 모달 캡션(한 번 눌러야 보임)
마커 장부미등록 8종전부 등록 (양방향 계약 그린)
로컬 게이트유닛 2,291 pass / 0 fail · 타입 0 · 린트 0

미검증: 실기기 safe-area(탭바 50 + safe-bottom) · 친구 세션 스코프에서의 세션 상세.

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

  • 종목 상세가 "이 종목에서 내가 얼마나 세졌나"를 두 숫자로 먼저 답한다. 이전에는 RM 5칸·등급 래더·추정 그래프·연간 4칸이 한 화면에 겹쳐 있어 무엇을 봐야 하는지가 불명확했다.
  • 1RM 그래프가 추정치에서 실측 PR 기록으로 바뀌었다. 계산해 만든 선이 아니라 실제로 든 무게의 선이다. 대신 실측 1RM PR 이 없는 유저는 빈 안내를 더 자주 보게 된다(의도된 결과).
  • 전체 보기 화면이 앱 전체에서 한 문법으로 통일됐다(ListOverlay). 홈·상세가 각각 다른 시트를 쓰던 것을 하나로 합쳤고, PR 행 스타일도 공용 CSS 로 올라가 홈 첫 진입에서 무스타일로 그려질 위험이 사라졌다.
  • 반입 감사가 실제로 회귀 2건을 막았다. 하나는 저장 데이터가 조용히 버려지는 결함, 하나는 사용자에게 알려야 할 데이터 단서의 소실이다.
  • 계약 테스트가 새 화면 위에서 더 촘촘해졌다 — 없는 값 계산 금지가 두 곳에 고정됐고, 추정치를 실측으로 부르지 않기·서버 값 재집계 금지·등급 표기 정본이 새로 잠겼다.

부작용·리스크

  • "날짜 모름 · 과거 입력"이 한 번 눌러야 보이게 됐다 — 히어로 2-up 재편으로 항상 보이던 자리가 없어졌다. 항상 노출하려면 디자인 결정이 필요하다.
  • 세션 상세 통계 말풍선은 yearStats 미공급 상태라 날짜 줄만 나온다(배송 설계상 정상 폴백).

남은 것

  1. 세션 상세 "올해 n번째 운동" — 과거 세션의 순번을 정확히 내려면 서버 순번 공급이 필요하다. 기존 헬퍼(buildMobileWorkoutYearStats)는 과거 세션에 대해 일부러 계산을 거부한다(범위가 잘린 연간 집계로는 정확한 순번을 못 낸다) — 틀린 숫자를 보여주느니 날짜만 두는 쪽을 택했다.
  2. nRM PR 칩이 최고 중량 그룹 추정 — 서버가 PR 세트 식별 필드(session_exercise_id · source_load · target_reps)를 이미 내려주므로 정확 대조로 교체 가능.
  3. 등급 표시명 규칙이 두 곳에 중복 — 홈 화면 인라인(main 부터 있던 것)과 새 PRC_TIER_DISPLAY. 데스크톱은 여전히 옛 표기(실버)이고 이는 이 배송 이전부터 그랬다. 정리하려면 표시 매핑을 공용 경계로 올리는 것이 맞다(어휘 원본 개정은 위 이유로 금지).
  4. 홈 오버레이 탭바 클리어런스 — 배송 노트는 hm-lsov 로 탭바를 피한다고 적었으나 실제 구현(.lsov)은 탭바를 덮는다. 클래스 hm-lsov 는 저장소 어디에도 없다. 문구 정정 또는 규칙 신설 중 택1(디자이너 확인).
  5. rc-rmodal 은 이름뿐인 클래스 — RM 모달에 붙지만 CSS 규칙이 없다(실제 모양은 rc-gmodal 이 만든다).