export type AppTheme = "light" | "dark"; export const THEME_KEY = "vignette.theme"; const themeListeners = new Set<() => void>(); export function readInitialTheme(): AppTheme { try { const saved = localStorage.getItem(THEME_KEY); if (saved === "light" || saved === "dark") return saved; } catch { /* localStorage 접근 불가 환경에서는 기본값 사용 */ } // 이번 시각 검증은 어두운 훈련 화면을 기본 표면으로 삼는다. 사용자가 바꾸면 저장값을 우선한다. return "dark"; } export function applyTheme(theme: AppTheme) { const changed = document.documentElement.getAttribute("data-theme") !== theme; document.documentElement.setAttribute("data-theme", theme); try { localStorage.setItem(THEME_KEY, theme); } catch { /* 저장 실패는 렌더링을 막지 않는다. */ } if (changed) themeListeners.forEach((listener) => listener()); } /** React와 비 React 진입점이 함께 쓰는 현재 적용 테마 snapshot. */ export function readAppliedTheme(): AppTheme { const applied = document.documentElement.getAttribute("data-theme"); return applied === "light" || applied === "dark" ? applied : readInitialTheme(); } /** 앱 전역 테마 store 구독. 테마 상태의 유일한 알림 경로다. */ export function subscribeTheme(listener: () => void) { themeListeners.add(listener); return () => themeListeners.delete(listener); } export function initTheme() { applyTheme(readInitialTheme()); }