뷰포트 계층 계약 (Viewport tiers)
Barbelic 웹·iOS 앱(Capacitor, 같은 번들)이 화면 폭과 입력 장치에 따라 어떤 루트·컬럼으로 그려지는지의 정본. 분기점은 tests/react/viewportTiers.test.mjs와 실제 브라우저의 e2e/viewport-matrix.spec.mjs에서 동작으로 검증한다.
v0.18.0 U03 변경(2026-09-10, U06 대조 2026-09-11): 아래의 고정1913×1063 캔버스·최소폭1180·zoom하한0.7 설명은 이전 버전의 기록이다. 현재 데스크톱은 실제 창 폭을 사용하고 CSS zoom은1이며 문서 스크롤과 본문 재배치로 대응한다. 모바일 루트 선택·520/600 폭·safe area는 유지한다. 현행 데스크톱 정본은 shell frames, 변경 근거는 U03 결과다. U06의 D1/D3 검사는 현재 배율1·창 폭·문서 가로 넘침0을 검증한다.
- 배경: 2026-08-22 폴드·터치 수리(#583), 2026-08-23 화면 대응 트랙 Phase 1(#600, BUG-006). 기록은
docs/updates/2026-08-23-viewport-tiers.md. - 오너 결정(2026-08-23): D1 폰 구간 상한 없음 · D2 터치 ≥520 600px 유지 · D3 데스크톱 축소 창 zoom 하한 + 스크롤 · D4 아이폰 세로 잠금(iPad 전 방향, 웹은 거터 색만).
1. 두 축 — 폭과 포인터
루트 선택은 폭 한 점(900px)과 포인터 축 둘로 정한다. 모바일 CSS는 폭 기준 미디어쿼리가 없고(vw·clamp·rem 0건) "컬럼 안에서만 유동"이므로, 컬럼 폭 규칙 하나가 곧 전 기기 대응이다.
| 정하는 것 | 소유 파일 | 값 |
|---|---|---|
| 루트(모바일/데스크톱) | src/react/vite/main.tsx DESKTOP_VIEWPORT_QUERY | (min-width: 900px) and (pointer: fine) |
| 모바일 컬럼 폭 | src/react/vite/app-host.css .gym-app-device + 미디어 규칙 2개 | 기본 전폭 / 터치 ≥520 max-width: 600px / 마우스 ≥520 390×844 목업 |
| 데스크톱 캔버스 | src/react/vite/desktopRoot.tsx + app-host.css .gym-app-host.desktop | 1913×1063 고정, zoom = max(0.7, min(w/1913, h/1063)) — 하한에 걸리면 호스트 스크롤 |
| 데스크톱 최소 폭 | src/react/ui/desktop/styles/desktop.css .dk-app | min-width: 1180px |
| 바닥 | src/react/vite/vite-scaffold.css body | min-width: 320px |
| 안전영역 | src/react/ui/mobile/styles/base/tokens.css :root | --safe-top/--safe-bottom/--safe-left/--safe-right = env(safe-area-inset-*, 0px) |
2. 계층
CSS 픽셀 폭 기준. "검증 뷰포트"는 실측·CI 매트릭스(e2e/viewport-matrix.spec.mjs, Phase 5)에서 쓰는 대표값.
| 계층 | 폭 × 포인터 | 루트 | 컬럼 | 대표 기기 | 검증 뷰포트 |
|---|---|---|---|---|---|
| compact | 320~359, 터치 | 모바일 | 전폭 | iPhone SE 1세대 | 320×568 (가로 넘침만 본다 — 설계 바닥) |
| phone | 360~519, 터치 | 모바일 | 전폭, 상한 없음 | 갤럭시 S(360) · iPhone SE 2/3(375) · iPhone 12~14(390, Design 기준 프레임) · iPhone 16/17(402) · Pixel(412) · 14/15 Plus·Pro Max(430) · 16/17 Plus·Pro Max(440) | 360×780 · 375×667 · 390×844 · 402×874 · 440×956 |
| wide-touch | ≥520, 터치 | 모바일 | max-width: 600px, 거터 = 모바일 지면색 | iPad mini/10세대 세로(744/820) · 갤럭시 폴드 내부(984) · 폰 가로(932) · iPad 가로(1180) | 820×1180 · 984×1092 · 932×440 |
| narrow-pointer | <520, 마우스 | 모바일 | 전폭 | 데스크톱 브라우저 창을 폰 폭까지 줄임 | 400×800 (마우스) |
| pointer-mockup | 520~899, 마우스 | 모바일 | 390×844 폰 목업 프레임(상태바 47·홈 34px 여백 고정), 거터 = 새벽 글로우 한 겹 | 데스크톱 브라우저 창 중간 폭 | 800×900 (마우스) |
| desktop | ≥900, 마우스 | 데스크톱 | 1913×1063 캔버스 zoom, .dk-app 1180 최소 | 노트북·모니터 | 1440×900 · 1280×720 |
읽는 법:
- phone 계층에는 상한이 없다. 폰 폭은 컬럼 하나라 상한의 이득이 없고, 숫자 상한(과거 430)은 다음 폰에서 재발한다(BUG-006).
- 600 상한은 wide-touch에만 있다. 전폭(984)은 하단 탭 5개·히어로 수치가 과하게 벌어지고, 430은 화면 절반을 버린다 (2026-08-22 3변형 실측). 값은
app-host.css한 줄 — 터치 폭 분포가 바뀔 때만 재검토. - 터치 기기는 폭이 900을 넘어도 모바일 루트(접힘↔펼침에서 루트 교체 없이 리플로우만). 데스크톱 캔버스는 마우스 환경에서만.
- 900~1179 마우스는 데스크톱 루트이지만
.dk-app최소 폭(1180) 아래라 zoom < 1로 축소된다. zoom 하한 0.7(D3, Phase 4): 1366×768(0.714)은 그대로, 1280×720(0.669)·932×440(0.414)은 0.7로 고정되고 호스트가overflow: auto·place-items: start로 바뀌어 캔버스를 스크롤한다(--lg-desktop-root-overflow/--lg-desktop-root-align). 관리자 독립 셸(AdminStandaloneRoot)도 같은 규칙.
3. 지면(배경)과 거터
- body·
.gym-app-host·.gym-app-device는 모바일 지면 리터럴oklch(0.972 0.004 240)(.app글로우 베이스와 동일). 컬럼이 뷰포트보다 좁은 wide-touch 구간의 거터는 화면과 같은 지면으로 이어진다. - 데스크톱 "좌백/우새벽" 분할 거터는
.gym-app-host.desktop만 그린다. pointer-mockup 호스트(@media (min-width:520px) and (pointer:fine) .gym-app-host)는 새벽 글로우 한 겹(--lg-desktop-dawn-bg)만 — 390px 목업 뒤의 반 분할은 폰이 이음새 위에 놓인 것처럼 보였다(이슈 #1381). 같은 규칙이 목업 장치에--safe-top: 47px·--safe-bottom: 34px(iPhone 14 계열 실제 env 값)를 주어 브라우저(env 0)에서도 콘텐츠가 둥근 모서리 안으로 들어가지 않는다. - 함정 — 호스트 바깥에서
var(--bg)금지.--bg는.app/.lg-shell-scope·.dk-app/.adm스코프에만 있다. 스코프 밖에서background: #eef5f8; background: var(--bg);처럼 폴백 + var()를 겹쳐 써도 두 번째 선언은 invalid at computed-value time이라 폴백으로 돌아가지 않고 투명이 된다(BUG-006 두 번째 원인). 호스트·body는 리터럴만.
4. 안전영역·높이
--safe-top/--safe-bottom은.app .screen상단 패딩·탭바 하단에서 소비(40곳).--safe-left/--safe-right는.app의 좌우 패딩 — 세로에서는 0이고, 웹 가로(Safari는 잠글 수 없다)에서 노치 밑으로 콘텐츠가 들어가는 것을 막는다.100vh는 iOS Safari 주소창 변동에 취약하므로 모바일 화면 CSS에서 쓰지 않는다(100dvh또는 컨테이너100%). 상한이 있는min(46vh, 380px)(workout.css)은 허용.
5. 방향(D4)
- iOS 앱:
ios/App/App/Info.plistUISupportedInterfaceOrientations(iPhone) = Portrait만,~ipad는 전 방향 유지. - 설치형 웹앱:
public/manifest.webmanifest"orientation": "portrait"— Android 설치형(PWA)에 적용되고 iOS Safari는 무시한다. 브라우저 탭(웹)은 잠글 수 없으므로 가로에서도 깨지지 않게 wide-touch 규칙·거터 색·--safe-left/right가 받는다. - 세트 기록·타이머는 한 손 세로 사용이 전부이고, 가로는 600 컬럼 양옆 166px 거터를 만들 뿐이다.
6. 새 기기가 나왔을 때
- phone 계층은 상한이 없으므로 조치 없음. 새 폰 폭(450·460…)에 코드 변경 0.
- 터치 넓은 화면의 600 상한 재검토는 터치 폭 분포가 바뀔 때만(
app-host.css한 줄 + 이 표). - 데스크톱 축소 창은 zoom 하한 0.7 + 스크롤(
DESKTOP_ZOOM_FLOOR, 두 루트 동일). - Design 전달본은 390×844 프레임이 기준이지 상한이 아니다 — 360·440·600에서 깨지면 계약 위반 (
docs/process/claude-free-design-contract.md"Viewport width").
7. 검증
- 계약 테스트(scope 레인):
tests/react/backgroundTokens.test.mjs(컬럼 규칙·거터·var(--bg)금지·폰 구간 상한 부재),tests/react/viewportTiers.test.mjs(이 문서의 숫자 = 코드 상수, 안전영역 토큰 4종,100vh부재, Info.plist·manifest 방향). - 뷰포트 매트릭스(full-ci 레인):
e2e/viewport-matrix.spec.mjs(npm run test:e2e-viewport,playwright.viewport.config.mjs) — 계층별 검증 뷰포트 × 탭 5개에서 가로 넘침 0 · 컬럼 = 규칙값 · 호스트 배경 ≠ 투명, 데스크톱 zoom/overflow 규칙. 픽셀 스냅샷은 두지 않는다. 스크린샷은 CI 아티팩트viewport-matrix-screenshots. 실행 절차는docs/testing/browser-full-stack-e2e.md.