/* ============================================================================= * device_context.ts * 기기 상태 한 곳(요구 193~195 · PLAN_기기대응 2-1) — `` 에 지금 상태를 늘 적음. * * - data-size = compact(<600) · medium(<840) · expanded(<1200) · large ← 배치는 이것만 봄 * - data-pointer = 주 입력 coarse · fine(미디어 `pointer`) * - data-input = 마지막 누름의 pointerType(mouse · touch · pen) * - data-touch-mode = 사용자 스위치(auto · on · off · 이 기기 브라우저에만 저장) * - data-touch = on · off — 누를 자리 크기를 키울지(auto 면 주 입력 coarse 일 때) * - data-burger = on · off — 공개 쪽에서 ☰ 서랍이 메뉴를 맡는지(폭 900 이하 · 터치) · 로그인 뒤 쪽(B · M)은 늘 ☰(7-1) * 바뀔 때만 window 에 `app:context` 이벤트(detail = 지금 상태). * 경계 표는 여기 한 벌 — CSS 는 위 표시(data-size)로 가르고, 숫자가 필요하면 --bp-* 변수. * 첫 화면 깜빡임 — 화면은 모두 JS 가 그리므로 main.ts 가 맨 먼저 import 하면 그리기 전에 적힘. * ========================================================================== */ import "./device_context.css"; export const SIZE_BREAKS = { medium: 600, expanded: 840, large: 1200 } as const; /** 위 메뉴가 숨는 폭(app_shell `@media (max-width: 900px)` 과 짝) — 이하면 ☰ 서랍 */ export const BURGER_MAX = 900; export type SizeClass = "compact" | "medium" | "expanded" | "large"; export type TouchMode = "auto" | "on" | "off"; export interface DeviceContext { size: SizeClass; pointer: "coarse" | "fine"; input: string; touchMode: TouchMode; touch: boolean; burger: boolean; } const TOUCH_MODE_KEY = "frd_touch_mode"; export function sizeClassOf(width: number): SizeClass { if (width < SIZE_BREAKS.medium) return "compact"; if (width < SIZE_BREAKS.expanded) return "medium"; if (width < SIZE_BREAKS.large) return "expanded"; return "large"; } let modeInMemory: TouchMode | null = null; // 저장이 막힌 브라우저용 export function readTouchMode(): TouchMode { if (modeInMemory) return modeInMemory; try { const v = localStorage.getItem(TOUCH_MODE_KEY); return v === "on" || v === "off" ? v : "auto"; } catch { return "auto"; } } const coarseQuery = window.matchMedia("(pointer: coarse)"); let current: DeviceContext | null = null; let lastInput = ""; function read(): DeviceContext { const pointer = coarseQuery.matches ? "coarse" : "fine"; const touchMode = readTouchMode(); const touch = touchMode === "on" || (touchMode === "auto" && pointer === "coarse"); return { size: sizeClassOf(window.innerWidth), pointer, input: lastInput || (pointer === "coarse" ? "touch" : "mouse"), touchMode, touch, burger: touch || window.innerWidth <= BURGER_MAX, }; } function apply(): void { const next = read(); if (current && JSON.stringify(current) === JSON.stringify(next)) return; const html = document.documentElement; html.dataset.size = next.size; html.dataset.pointer = next.pointer; html.dataset.input = next.input; html.dataset.touchMode = next.touchMode; html.dataset.touch = next.touch ? "on" : "off"; html.dataset.burger = next.burger ? "on" : "off"; const first = !current; current = next; if (!first) window.dispatchEvent(new CustomEvent("app:context", { detail: next })); } export function deviceContext(): DeviceContext { return current ?? read(); } /** 터치 모드 스위치(☰ 서랍) — 이 기기 브라우저에만 저장 */ export function setTouchMode(mode: TouchMode): void { modeInMemory = mode; try { if (mode === "auto") localStorage.removeItem(TOUCH_MODE_KEY); else localStorage.setItem(TOUCH_MODE_KEY, mode); } catch { /* 저장 막힌 브라우저 — 이번 화면만 */ } apply(); } function init(): void { const html = document.documentElement; for (const [name, px] of Object.entries(SIZE_BREAKS)) html.style.setProperty(`--bp-${name}`, `${px}px`); apply(); coarseQuery.addEventListener("change", apply); window.addEventListener("resize", apply); window.visualViewport?.addEventListener("resize", apply); screen.orientation?.addEventListener("change", apply); window.addEventListener( "pointerdown", (e) => { if (e.pointerType === lastInput) return; lastInput = e.pointerType; apply(); }, { capture: true, passive: true }, ); } init();