Skip to content

뷰포트 계층 계약 (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.desktop1913×1063 고정, zoom = max(0.7, min(w/1913, h/1063)) — 하한에 걸리면 호스트 스크롤
데스크톱 최소 폭src/react/ui/desktop/styles/desktop.css .dk-appmin-width: 1180px
바닥src/react/vite/vite-scaffold.css bodymin-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)에서 쓰는 대표값.

계층폭 × 포인터루트컬럼대표 기기검증 뷰포트
compact320~359, 터치모바일전폭iPhone SE 1세대320×568 (가로 넘침만 본다 — 설계 바닥)
phone360~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-mockup520~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.plist UISupportedInterfaceOrientations(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.