폴드·태블릿 터치 화면 수리 — 데스크톱 루트 오배정·브라우저 강제 다크 (2026-08-22)
- 기간: 2026-08-22 (세션 1개, 오너 보고 → 계획 → "권고안대로 Phase 0~4 무중단" 지시)
- 랜딩: PR #583(Phase 1~3 —
main.tsx루트 선택 쿼리,app-host.css터치/마우스 분기,index.html·admin/index.htmlcolor-scheme 메타, 계약 테스트 2파일). 마이그레이션·엣지 없음. - 정본: 루트 선택 =
src/react/vite/main.tsxDESKTOP_VIEWPORT_QUERY, 호스트 폭 규칙 =src/react/vite/app-host.css.gym-app-device미디어 규칙, 테마 선언 =index.html메타 +app-host.csshtml, body - 실측 도구: 세션 scratchpad의 Playwright 스크립트(레포 밖) — 로컬 supabase 일회용 유저 + 뷰포트 5종 + Chromium 강제 다크 A/B. 재현 절차는 §4에 적어 두었다.
1. 배경
오너가 갤럭시 폴드(웹)에서 앱을 연 스크린샷 4장을 가져왔다 — 펼친 화면은 데스크톱 레이아웃이 아주 작게 가운데 떠 있고, 폰 다크 모드에서는 라이트 전용 화면이 검게 뒤집혀 있었다. "비율 다 깨지고 배경 검정색". 원인은 서로 독립인 둘이었고, 둘 다 폴드 전용 버그가 아니라 설계 구멍이다.
펼친 화면이 "데스크톱"으로 분류된다
폴드 내부 화면의 CSS 뷰포트는 약 984×1092(Fold 7, DPR 2.0). main.tsx는 matchMedia("(min-width: 900px)") 하나로 모바일/데스크톱 루트를 골랐으므로 터치 기기도 900을 넘기면 데스크톱 루트다. 데스크톱 루트는 1913×1063 고정 캔버스를 zoom = min(w/1913, h/1063)로 통째 축소하는 구조라 폴드에선 zoom 0.514 — 글자·탭 타깃 전부 절반, 세로 레터박스, body 배경(좌반 흰색/우반 하늘색 그라데이션)이 반반으로 드러난다. .dk-app는 min-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/B | scratchpad 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 3 | DESKTOP_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를 남긴다 — 첫 커밋에.bak5개가 섞여 들어가 후속 커밋으로 제거했다. 다중행 치환은 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 증빙)가 이 변경 규모에 비해 과해 넣지 않았다. 회귀 방어는 계약 테스트(루트 선택 쿼리 상수·미디어 규칙·메타)와 이 문서의 실측 절차로 대신한다.