하단 크롬 여백 계약 (bottom chrome clearance)
2026-09-04 이슈 #1250(하단 탭바가 내용을 가리는 문제 원천 차단, 오너 결정 D1=b·D2=a·D3=a) 산출물. 이슈 #855 §2-1(크롬 상태 채널)이
.screen에만 적용하던 "여백은 채널에서 유도한다"를 하단에서 끝나는 모든 스크롤 컨테이너로 확장하고, 탭바 표시/숨김을 CSS 감지(:has)에서 완전히 떼어 채널 하나로 모은다. 구조 변경은 이 계약 개정이 선행한다.
0. 한 줄 원칙
하단 크롬(탭바·시작/복귀 바·친구 칩 바·PR 복귀 바·뒤로 바·운동 도크·세트 편집 행)의 존재와 높이는 채널 한 곳이 게시하고, 스크롤 컨테이너의 바닥 여백은 그 채널에서만 유도한다. 화면별 손 여백은 금지.
iOS 네이티브가 탭바 높이를 스크롤 뷰의 아래 여백(contentInset)에 자동 반영하는 것과 같은 모델이다. 유저가 어느 화면에서든 끝까지 내리면 마지막 요소가 하단 크롬 위에 온전히 보여야 한다.
1. 용어
| 용어 | 뜻 |
|---|---|
| 하단 크롬 | 화면 바닥에 떠 있어 내용을 가릴 수 있는 것. 탭바, 탭바 위 시작/복귀 바, 친구 칩 바(fsb), PR 복귀 바, 세션 상세 뒤로 바, 운동 도크, 세트 편집 행. (완료 단계의 종료 바는 in-flow 버튼이라 크롬이 아니다) |
| 채널 | .app[data-bottom-chrome="<값>"] 속성. 지금 보이는 하단 크롬이 무엇인지 열거값 하나로 게시 |
| 명목 높이 | 채널 값 → --bottom-chrome-h 사상표(nav.css). safe-bottom 제외 |
| 숨(hush) | 크롬 윗변과 내용 마지막 요소 사이의 여유. --chrome-hush, 기본 14px. 화면 취향이라 CSS가 소유 |
| 유도식 | --bottom-clearance: calc(var(--bottom-chrome-h, var(--tabbar-h)) + var(--chrome-hush, 14px) + var(--safe-bottom)) — core.css 한 곳에서 .screen·.pg-overlay 자신에게 선언(커스텀 속성은 선언한 요소에서 계산되므로 .app에 두면 숨이 굳는다) |
| 신고(claim) | 하단 크롬을 만들거나 덮는 컴포넌트가 "나 여기 있다"를 React 훅으로 알리는 것. 채널 값은 신고들에서 계산된다 |
2. 현황 (전 — 2026-09-04, 코드 대조)
"탭바가 보이는가"와 "바닥 여백"이 세 곳(채널·:has 감지·클래스별 z-index / 손 여백)에서 따로 결정되던 상태. ⚠ = 어긋남 또는 잠재 결함.
| 스크롤 컨테이너 | 탭바 | 바닥 여백(전) | 결정 위치 | 비고 |
|---|---|---|---|---|
.screen 탭 화면 5종·기록 도구 레이어 | 채널 tabbar / tabbar-start / tabbar-resume | 유도식(50 또는 104 + 숨 + safe) | core.css 52 | #855 Phase 6 정본 |
.screen 친구 판(홈·피드·리포트·일지 달력) | 채널 fsb(숨김) | 76 + safe, 화면별 4규칙 | home.css 144 · feed.css 30 · report.css 13 · session.css 10 | fsb에 사상표 값이 없어 폴백 뒤 화면 규칙이 덮음 |
.screen PR 복귀 바 | :has(.pr-return-bar) 숨김 | 71 + safe | session.css 88·89 | 바 59 + 숨 12. 채널은 tabbar라 실제와 어긋남 ⚠ |
.screen 운동 플로우 기본(시작 단계 등) | 채널 dock(숨김) | 16 (safe 없음) | core.css 110 | |
.screen 운동 종료 바 있는 단계 | 〃 | 74 + safe | core.css 111 | 바 50 + 인셋 12 + 숨 12 |
.screen 완료 단계(종료 바 흐름 안) | 〃 | 16 + safe | workout.css 742 | |
.screen 세트 진행 / 휴식 도크 | 〃 | --dock-h + 56 + safe | workout.css 956 · 716 | 도크 채널 유도 ✓, 형식만 다름 |
.screen 종목 완료 검토 도크 | 〃 | --dock-h + 66 + safe (+ 통계 패널) | workout.css 903 · 904 | 〃 |
.pg-overlay 기본(홈 리포트 오버레이) | 보임(z 30 < 40) | --tabbar-h + 14 + safe | primitives.css 564 | 홈은 시작 바(104)인데 50만 확보 ⚠ |
.gp-full.pg-overlay 그룹 보드·커피라운지 | 보임 | --tabbar-h + 14 + safe | group.css 22 | 그룹 시작 바(104) 미반영 ⚠ |
.gp-sess.pg-overlay 그룹 세션 상세 | :has(.pr-return-bar) 숨김 | 0 (복귀 바 sticky in-flow) | group.css 153 | |
.sj-mo.pg-overlay 일지 하루·월 상세 | 보임 | 14 + safe | session.css 58 | 이번 결함: 탭바 50 미반영 → 마지막 버튼 가림 ⚠ |
.sj-mo.pg-overlay 세션 상세 풀페이지(일지·피드) | :has(session.detail) 숨김 | 14 + safe | session.css 58 · nav.css 123 | |
.rc-det.pg-overlay 종목 상세 | :has(pr.detail) 숨김 | 14 + safe | records.css 58 · nav.css 124 | |
.lsov.pg-overlay PR 기록 목록 | z 46으로 덮음(가정) | 14 + safe | primitives.css 567 | 덮임이 z-index에 의존. iOS WKWebView에서 .screen이 합성 레이어가 되면 탭바가 위로 올라올 수 있음(미검증) ⚠ |
.sheet-body 드로어 전체 | 시트 z 60이 덮음 | 16 / 닫기 바 64 + 12 / 14 + safe 등 | overlays.css 398 · 509, feed.css 95 · 101, workout.css 581, home.css 90 | 시트는 크롬을 덮는 쪽. 안쪽 여백은 시트 자기 몫(닫기 바·safe) |
.fd-apbody 친구 추가 모달 안 목록 | 모달 | 0 | feed.css 37 | 모달 카드가 인셋 소유 |
.lgd-scroll 법적 문서 | 앱 밖 전면 | 32 + safe | legal-doc.css 13 | 탭바 없음 |
댓글 시트 .fd-cmsheet | :has 숨김(#775) | 시트 내부 | nav.css 128 | 채널은 tabbar라 어긋남 ⚠ |
탭바 숨김 결정 주체(전): 채널 값 3개(dock·none·fsb) + :has 감지 4건(세션 상세 풀페이지·종목 상세·댓글 시트·PR 복귀 바). 손 여백(전): 위 표의 .screen 화면 규칙 9 + 오버레이 7 + 폴백 1 = 17곳 (시트·모달·법적 문서 제외).
3. 채널 값 사상표 (후)
값을 추가할 때는 이 표와 nav.css 사상표를 함께 고친다. 신고자 = 그 크롬을 그리는 컴포넌트.
| 값 | 신고자 | --bottom-chrome-h | 탭바 | 우선순위 |
|---|---|---|---|---|
tabbar | 탭 리전(기본) | --tabbar-h(50) | 보임 | 10 |
tabbar-start / tabbar-resume | 탭 리전 | --tabbar-h + 54 | 보임 + 바 | 10 |
fsb | 친구 페이지 스택 셸(UiStackScreen chrome="fsb", 이슈 #1393 Phase 4 — 종전 탭 리전 게시) | 66 (칩 바 54 + 인셋 12) | 숨김 | 20 |
return-bar | PR 복귀 바(SessionSheetBody) | 59 | 숨김 | 30 |
back-bar | 세션 상세 풀페이지 뒤로 바(UiSessionDetail, 복귀 문맥 아님) | 64 | 숨김 | 30 |
dock | 운동 도크(세트 진행·휴식·검토) | --dock-h | 숨김 | 30 |
edit-row | 세트 편집 저장/취소 행(WorkoutRecord, 편집 중) | 76 (패딩 12 + 버튼 48 + 인셋 고정분 16) | 숨김 | 30 |
none | 하단 크롬을 전부 덮는 것: 전체 화면 스택 셸(세션 상세·종목 상세·PR 기록 목록·일 요약 — UiStackScreen chrome="none", 이슈 #1393), 댓글 시트, 운동 플로우 기본(도크 없는 단계·완료 단계 — 완료 단계의 종료 바는 in-flow라 크롬이 아니다) | 0 | 숨김 | 40 |
- 여러 신고가 동시에 있으면 우선순위가 높은 값이 게시된다(덮는 것 > 국소 바 > 친구 칩 바 > 탭바). 같은 우선순위면 나중 신고.
- 탭바 표시/숨김은 이 채널만 결정한다.
nav.css에서.tabbar를 숨기는 선택자는.app[data-bottom-chrome=…]형태만 허용. 화면 표식 감지(:has)로 탭바를 숨기는 규칙은 금지 — 덮임을 z-index(페인트 순서)에 기대지 않고 탭바를 실제로 없앤다(#775와 같은 원칙). - 게시 방식은 #855 그대로:
.app속성을 레이아웃 이펙트로 직접 쓴다. 신고 저장소는 컨텍스트의 안정 객체(ref)이고 값 변화는 루트 재렌더 없이 속성만 바꾼다(렌더 입자성 규칙 유지).
4. 유도식 소비자 (후)
하단에서 끝나는 스크롤 컨테이너는 바닥 여백을 var(--bottom-clearance)로만 쓴다(.screen·.pg-overlay는 core.css/primitives.css의 공용 규칙이 이미 쓰고 있으므로 새 화면은 아무것도 적지 않는다). 오버레이는 --chrome-hush: 14px로 시작해 뒤 지면의 숨을 상속하지 않는다.
| 컨테이너 | 여백 | 숨(--chrome-hush) |
|---|---|---|
.screen (탭 화면·레이어·친구 판·운동 플로우 전부) | --bottom-clearance | 기본 14 · 기록 도구 12 · 풀블리드(rc-full·rp-full·sj-fullcal) 0 · 친구 판 10 · PR 복귀 바 12 · 뒤로 바 8 · 운동 플로우(도크 없는 단계·완료 단계) 16 · 세트 진행/휴식/완료 대기 56 · 검토 66(+통계 패널) · 세트 편집 164(구 240 유지) |
.pg-overlay 전부(sj-mo·gp-full·rc-det·lsov·홈 리포트) | --bottom-clearance | 기본 14 |
.gp-sess.pg-overlay 그룹 세션 상세 | 유예 padding-bottom: 0 | 복귀 바가 in-flow sticky로 내용 끝에 붙어 있어 여백을 두면 바 아래 빈 띠가 생긴다. 탭바 숨김은 채널 return-bar 신고 |
.fscope 안 .screen | .screen 규칙 그대로 | 10 |
숨 값은 화면 CSS가 --chrome-hush로만 표현한다. padding-bottom을 직접 쓰지 않는다.
채널 밖(유예, 정적 검사 허용 목록): 시트 .sheet-body 계열(시트가 크롬을 덮으므로 안쪽 인셋은 닫기 바·safe 자기 몫), 모달 안 목록(.fd-apbody), 법적 문서(.lgd-scroll, 앱 밖), 탭바 없는 온보딩 셸(.screen:has(.ob-flow) 0 — 채널 미게시라 폴백 탭바 몫을 상쇄), in-flow 바가 safe를 소유하는 화면(운동 시작 단계 .screen:has(.wf2-ctas) 0, 그룹 세션 상세), 그리고 스크롤 컨테이너가 아닌 하단 바 자체(탭바 패딩·온보딩/로그인 도크·댓글 입력 행·운동 CTA 행·운동 도크·편집 행·픽커 발·PR 복귀 바). 유예는 tests/react/bottomChromeClearanceContract.test.mjs의 목록에 파일:선택자와 이유를 적는다.
5. 게이트
| 게이트 | 무엇을 보나 | 어디서 |
|---|---|---|
계약 테스트 bottomChromeClearanceContract | 모바일 스타일에서 padding-bottom/padding 축약에 --tabbar-h·--safe-bottom·--bottom-chrome-h·고정 px가 든 선언은 유도식 정의 1곳 + 유예 목록 외 금지. .tabbar를 숨기는 선택자는 채널 속성만. 사상표의 값 집합 = 이 문서 §3 = 신고 훅의 타입 | 전 PR(npm run check) |
| e2e 채널 등가(#855) | 채널 값 == 실제 보이는 크롬, --bottom-chrome-h 해석값 | full-ci(viewport 잡) |
| e2e 도달 게이트(#1250 Phase 4) | 16표면 × safe 0/34: 끝까지 스크롤 → 마지막 누를 수 있는 요소 아랫변 ≤ 보이는 크롬 윗변, 그 점의 elementFromPoint가 그 요소 | full-ci(viewport 잡) |
도달 게이트 표면(D2=a 전수): 홈 · 홈 리포트 오버레이 · 일지 달력 · 일지 하루 상세 · 일지 월 상세 · 세션 상세 풀페이지 · 피드 · 피드 세션 상세 · 그룹 목록 · 그룹 보드 · 커피라운지 · 리포트 · 기록 도구(스택 화면 .pg-overlay.prt-stack, 이슈 #1393 Phase 5) · 종목 상세 · PR 기록 목록 · 친구 일지(fsb). 새 표면(오버레이·스택 화면)을 만들면 이 목록에 추가한다.
6. 새 화면·오버레이를 만들 때
- 탭바를 덮거나 자기 하단 바를 가지면
useBottomChromeClaim("<값>")을 호출한다(값은 §3에서 고른다. 없으면 이 문서·nav.css 사상표·훅 타입에 함께 추가). - 스크롤 컨테이너의 바닥 여백은
padding-bottom: var(--bottom-clearance)한 줄. 여유를 바꾸고 싶으면--chrome-hush만 바꾼다. - §5 도달 게이트 표면 목록에 추가한다.
이 세 줄을 빠뜨리면 계약 테스트(1·2)와 도달 게이트(3)가 PR에서 잡는다.
7. Phase 2에서 달라진 값
전(2026-09-04 Phase 2 이전)과 달라진 값: 운동 플로우의 도크 없는 단계·완료 단계는 16 → 16 + safe-bottom(홈 인디케이터 위로), 세션 상세 뒤로 바 화면은 72 → 72 + safe-bottom, 세트 편집 중 240 → 240 + safe-bottom. 나머지는 값 동일(손 여백을 숨으로 옮긴 것). 이번 결함(하루·월 상세)은 14 + safe → 탭바 50 + 14 + safe.
2026-09-10 운동 편집 중 보존된 탭의 신고 (#1554)
같은 .app 안에서 탭과 운동 화면은 각각 BottomChromeProvider를 가지며 현재 모드의 제공자만 DOM 채널을 게시한다. 탭을 편집 중 숨겨도 신고를 잃지 않고, 비활성 탭의 비동기 갱신은 운동의 none·도크·편집 행을 덮지 않는다. 운동을 닫으면 보존된 탭·상세의 신고가 다시 게시된다. 화면별 여백 계산과 우선순위는 유지한다.
그룹 작성기처럼 같은 제공자 안에서 아래 화면만 숨길 때는 BottomChromeScope active=false로 해당 화면의 신고를 잠시 해제한다. 화면의 DOM·입력 상태는 유지하며 다시 활성화하면 현재 신고를 부모 채널에 복원한다. 별도 .app이나 새로운 우선순위 규칙은 만들지 않는다.