디자인 배송 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-h55 → 50, 홈 프로필 행 + XP바 단일 탭 영역
되돌린 것 3건 (배송 미신고)
| 파일 | 조치 |
|---|---|
WorkoutRecordParts.tsx | 복합 종목 동작별 횟수 입력 복원(main 기준). 테스트 무수정으로 issue928Cleanup 10/10 |
RecordsDetail.tsx | "날짜 모름 · 과거 입력"을 RM별 기록 모달 캡션으로 복원 — 계약 단언이 원문 그대로 통과 |
HomeScreen.tsx | #1076 검색 엔진 재사용 근거 주석 1줄 복원 |
게이트 동반 수정
- 마커 장부에 8종 등록:
prDetailHeroprDetailRmOpenprDetailRmModalprDetailFeedAllLinkprDetailFeedModalprTapeRm1ToggleworkoutHeaderEditDialogpr.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건.
prTierLadder·prDetailWindowSelect·prChart마커 등록 해제 — 데스크톱(DesktopPrBoardParts.tsx:355,DesktopPrDetailParts.tsx:451·463)이 여전히 렌더한다. 해제했으면 마커 계약의 역방향 단언이 깨진다. 디자이너는 모바일만 보고 판단한 것으로 보인다.- 등급 이름을 어휘 원본(
contracts/gamificationPolicy.ts)에서 "…바벨"로 개정 — 그 이름 배열은 표시용이 아니라 DB가 내려주는 등급 데이터와 대조하는 열쇠다(cutlines의{"tier":"아이언"}↔BARBELIC_TIER_LABELS.indexOf). 원본을 바꾸면 전 종목의 등급 판정이 어긋난다. 배송본이 실제로 택한 방식(원본은 두고 표시 매핑만 추가)이 맞고, 그 매핑을 지키는 테스트를 새로 만들었다.
테스트 재조준 — 계약은 하나도 약화하지 않았다.
| 테스트 | 무엇이 깨졌나 | 어떻게 옮겼나 |
|---|---|---|
recordsDetailEstimatedChart 3건 | 추정 1RM 그래프 폐지 | 실측 1RM 그래프 기준으로. "추정치를 실측으로 부르지 않는다" 단언 신설 |
measuredPrFrontendContract | RM 5셀 폐지 | RM 모달 1~10 그리드로. "없는 칸을 계산해 채우지 않는다"를 모달·히어로 양쪽에 고정(단언 1 → 3) |
recordsDetailEmptyStateGate | 히어로 마크업 변경 | 새 마크업으로. 단위 미표기까지 추가 단언 |
statsCentralizationPhase2 | 도넛 비노출 | "안 그린다" + D1 계약(강도 존 = 서버 세트별 강도, 목적 존 = 서버 stimulus_class, 프론트 e1RM 추정 금지)을 소스 단언으로 잠금 |
statsCentralizationPhase4 | 연간 스탯 4칸 폐지 | 모바일이 pr_records 를 재집계해 폐지값을 되살리는 것 금지 단언 추가 |
mobileCssBoundaries | 감시 선택자 소멸 | rc-dhero → rc-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 규칙을 지우고 남긴 주석에 클래스 이름을 그대로 쓰면 안 된다.
mobileCssBoundaries의commonCss.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 · 무게 칼럼 고정폭으로 × 세로선 일치 |
| 탭바 높이 | 55px | 50px (소비처 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미공급 상태라 날짜 줄만 나온다(배송 설계상 정상 폴백).
남은 것
- 세션 상세 "올해 n번째 운동" — 과거 세션의 순번을 정확히 내려면 서버 순번 공급이 필요하다. 기존 헬퍼(
buildMobileWorkoutYearStats)는 과거 세션에 대해 일부러 계산을 거부한다(범위가 잘린 연간 집계로는 정확한 순번을 못 낸다) — 틀린 숫자를 보여주느니 날짜만 두는 쪽을 택했다. nRM PR칩이 최고 중량 그룹 추정 — 서버가 PR 세트 식별 필드(session_exercise_id·source_load·target_reps)를 이미 내려주므로 정확 대조로 교체 가능.- 등급 표시명 규칙이 두 곳에 중복 — 홈 화면 인라인(
main부터 있던 것)과 새PRC_TIER_DISPLAY. 데스크톱은 여전히 옛 표기(실버)이고 이는 이 배송 이전부터 그랬다. 정리하려면 표시 매핑을 공용 경계로 올리는 것이 맞다(어휘 원본 개정은 위 이유로 금지). - 홈 오버레이 탭바 클리어런스 — 배송 노트는
hm-lsov로 탭바를 피한다고 적었으나 실제 구현(.lsov)은 탭바를 덮는다. 클래스hm-lsov는 저장소 어디에도 없다. 문구 정정 또는 규칙 신설 중 택1(디자이너 확인). rc-rmodal은 이름뿐인 클래스 — RM 모달에 붙지만 CSS 규칙이 없다(실제 모양은rc-gmodal이 만든다).