Skip to content

폴드·태블릿 터치 화면 수리 — 데스크톱 루트 오배정·브라우저 강제 다크 (2026-08-22)

  • 기간: 2026-08-22 (세션 1개, 오너 보고 → 계획 → "권고안대로 Phase 0~4 무중단" 지시)
  • 랜딩: PR #583(Phase 1~3 — main.tsx 루트 선택 쿼리, app-host.css 터치/마우스 분기, index.html·admin/index.html color-scheme 메타, 계약 테스트 2파일). 마이그레이션·엣지 없음.
  • 정본: 루트 선택 = src/react/vite/main.tsx DESKTOP_VIEWPORT_QUERY, 호스트 폭 규칙 = src/react/vite/app-host.css .gym-app-device 미디어 규칙, 테마 선언 = index.html 메타 + app-host.css html, body
  • 실측 도구: 세션 scratchpad의 Playwright 스크립트(레포 밖) — 로컬 supabase 일회용 유저 + 뷰포트 5종 + Chromium 강제 다크 A/B. 재현 절차는 §4에 적어 두었다.

1. 배경

오너가 갤럭시 폴드(웹)에서 앱을 연 스크린샷 4장을 가져왔다 — 펼친 화면은 데스크톱 레이아웃이 아주 작게 가운데 떠 있고, 폰 다크 모드에서는 라이트 전용 화면이 검게 뒤집혀 있었다. "비율 다 깨지고 배경 검정색". 원인은 서로 독립인 둘이었고, 둘 다 폴드 전용 버그가 아니라 설계 구멍이다.

펼친 화면이 "데스크톱"으로 분류된다

폴드 내부 화면의 CSS 뷰포트는 약 984×1092(Fold 7, DPR 2.0). main.tsxmatchMedia("(min-width: 900px)") 하나로 모바일/데스크톱 루트를 골랐으므로 터치 기기도 900을 넘기면 데스크톱 루트다. 데스크톱 루트는 1913×1063 고정 캔버스를 zoom = min(w/1913, h/1063)로 통째 축소하는 구조라 폴드에선 zoom 0.514 — 글자·탭 타깃 전부 절반, 세로 레터박스, body 배경(좌반 흰색/우반 하늘색 그라데이션)이 반반으로 드러난다. .dk-appmin-width: 1180px이라 터치 폭 900~1200 구간은 애초에 설계 밖이었다. "900px 이상 = 항상 1913 캔버스"는 아카이브된 계약 (docs/archive/desktop-scroll-ownership-handoff.md)의 산물이고 현행 규범은 아니었다.

접으면(<900) 모바일 루트라 정상이었고, 접었다 펼칠 때마다 루트가 통째로 교체됐다.

앱이 color-scheme를 선언하지 않는다

레포 전체에 prefers-color-scheme/color-scheme 0건. 앱은 라이트 단일 테마인데 삼성 인터넷 "다크 모드"(갤럭시 기본 브라우저) 또는 Chrome Android 자동 다크가 색을 알고리즘으로 뒤집은 것이다. Chrome의 공식 opt-out은 <meta name="color-scheme" content="only light">뿐이고, light만 쓰면 무시된다.

2. 결정

오너가 계획의 권고안을 그대로 채택했다.

  • (a) 터치 기기는 폭 무관 모바일 루트. 데스크톱 캔버스 조건을 (min-width: 900px) and (pointer: fine)로. 데스크톱 CSS 무접촉, 접힘↔펼침에서 루트 교체 없이 리플로우만(운동 중 상태 유지), 아이패드 웹도 함께 해결. 마우스 데스크톱·DeX·터치 노트북(primary pointer=fine)은 무변화.
  • (b) 전환점만 900→1200으로 올리는 안은 비권고 — 펼침(984)은 모바일이 되지만 ≥520px "390×844 폰 목업 프레임"이 가로 회전에서 잘리므로 결국 (a)의 규칙이 필요하고, 창 줄인 노트북이 모바일로 떨어지는 부작용이 있다.
  • (c) 데스크톱 캔버스 반응형화(1913 폐기)는 Design 트랙 대형 작업 — 장기 과제로 기록만.
  • 터치 넓은 화면의 모바일 컬럼 폭은 실측 3변형(기본 430 / 600 / 전폭)을 보고 600px 상한으로 정했다. 전폭(984)은 하단 탭 5개·히어로 수치가 과하게 벌어지고, 430은 화면 절반을 버린다.

3. 변경

Phase내용파일
Phase 0재현·실측 — 폴드 커버/내부 세로/내부 가로/iPad/데스크톱 5뷰포트 + 강제 다크 A/Bscratchpad Playwright(레포 밖)
Phase 1<meta name="color-scheme" content="only light"> 2곳 + html, body { color-scheme: only light }index.html, admin/index.html, src/react/vite/app-host.css
Phase 2분기 기준 (a) 채택
Phase 3DESKTOP_VIEWPORT_QUERY 상수, 폰 목업 프레임 pointer: fine 한정, 터치 max-width: 600px, 계약 테스트src/react/vite/main.tsx, app-host.css, tests/react/backgroundTokens.test.mjs, tests/react/barbelicWebMetadata.test.mjs
Phase 4오너 잔여 판단(아래)

4. 실측

Playwright(headless Chromium) + 로컬 supabase(service role 일회용 유저·세션 5개·통계 드레인) + 앱에 세션 주입. 변경 전(main)과 변경 후(브랜치)를 같은 스크립트로 찍었다.

뷰포트변경 전변경 후
폴드 커버 412×915 터치모바일모바일(무변화)
폴드 내부 세로 984×1092 터치데스크톱 zoom 0.514모바일, 600px 컬럼 전높이
폴드 내부 가로 1092×984 터치데스크톱 zoom 0.571모바일, 600px 컬럼 전높이
iPad 가로 1180×820 터치데스크톱 zoom 0.617모바일, 600px 컬럼 전높이
데스크톱 1440×900 마우스데스크톱 zoom 0.753데스크톱 zoom 0.753(무변화)
  • 전 뷰포트 documentElement.scrollWidth == clientWidth(가로 스크롤 0).
  • 강제 다크 A/B: Chromium을 --blink-settings=forceDarkModeEnabled=true로 띄우면 변경 전은 오너 스크린샷과 동일하게 반전되고, 변경 후는 라이트를 유지한다. (--enable-features=WebContentsForceDark 만으로는 headless에서 반전이 일어나지 않는다 — 재현 시 blink-settings 플래그가 필요.)
  • 게이트: check:types·check:lint·check:dead-css·check:test-manifest 통과, 접촉 테스트 37/37.

5. 남는 것 (Phase 4, 오너)

  • 삼성 인터넷 강제 다크가 only light를 존중하는지 기기 실측. 삼성 문서상 강제 다크는 사이트가 light dark + 실제 다크 CSS를 갖고 Labs "웹사이트 다크 테마 사용"이 켜진 경우에만 비켜 간다. 무시하면 두 갈래: 진짜 다크 테마 도입(Design 트랙, light dark + 다크 토큰 세트) vs 현행 유지 + 사용자 안내.
  • 터치 컬럼 상한 600px은 권고값 — 취향 조정 가능(한 줄).
  • (c) 데스크톱 캔버스 반응형화는 별도 트랙.

6. 작업 중 드러난 것

  • perl -pi가 Windows에서 .bak를 남긴다 — 첫 커밋에 .bak 5개가 섞여 들어가 후속 커밋으로 제거했다. 다중행 치환은 EOL(CRLF)까지 감안한 Node 스크립트로 하는 편이 안전하다(이 레포 소스는 CRLF·LF가 섞여 있어 \n 앵커 패턴이 조용히 실패한다).
  • Playwright 브라우저가 이 머신에 없었다(npx playwright install chromium). 전체 chromium은 spawn UNKNOWN으로 뜨지 않고 headless shell만 뜬다 — 강제 다크 재현은 headless shell + blink-settings로.
  • 계획 단계에서 약속한 "e2e Fold 스모크 1건"은 번호 케이스 체계(README 5절·USER-JOURNEY·case.json· Red/Green CI 증빙)가 이 변경 규모에 비해 과해 넣지 않았다. 회귀 방어는 계약 테스트(루트 선택 쿼리 상수·미디어 규칙·메타)와 이 문서의 실측 절차로 대신한다.