'use client'; import { useCallback } from 'react'; import { DEFAULT_THEME_PREFERENCE, THEME_ATTRIBUTE_NAME, THEME_PREFERENCE_CYCLE, THEME_STORAGE_KEY, type ThemePreference, } from '@/lib/constants/theme'; // design이 globals.css에서 이 속성이 존재하는 동안만 색상 전환에 트랜지션을 건다(design 계획 // §8.3). prefers-reduced-motion 분기는 그 CSS 쪽 셀렉터가 처리하므로 이 훅은 조건 없이 // 부여/제거만 한다. const THEME_TRANSITION_ATTRIBUTE_NAME = 'data-theme-transition'; const THEME_TRANSITION_DURATION_MS = 200; export interface ThemePreferenceState { cycleThemePreference: () => void; } /** ``에서 현재 선택을 읽는다. 속성이 없거나 알 수 없는 값이면 system이다. */ function readThemePreference(): ThemePreference { const value = document.documentElement.getAttribute(THEME_ATTRIBUTE_NAME); return value === 'light' || value === 'dark' ? value : DEFAULT_THEME_PREFERENCE; } /** 순환 순서(THEME_PREFERENCE_CYCLE)에서 다음 값을 계산한다. */ function getNextThemePreference(current: ThemePreference): ThemePreference { const currentIndex = THEME_PREFERENCE_CYCLE.indexOf(current); const nextIndex = (currentIndex + 1) % THEME_PREFERENCE_CYCLE.length; return THEME_PREFERENCE_CYCLE[nextIndex]; } /** 선택값을 기기에 영속화한다. system은 저장값을 지워 "선택 없음" 상태로 되돌린다. */ function persistThemePreference(next: ThemePreference): void { try { if (next === 'system') { localStorage.removeItem(THEME_STORAGE_KEY); } else { localStorage.setItem(THEME_STORAGE_KEY, next); } } catch { // localStorage 접근이 차단된 환경(시크릿 모드 등)에서도 조용히 무시한다 — 만 // 으로 현재 세션 동안은 정상 동작하고, 다음 방문 시 다시 system으로 fail-safe한다. } } /** ``에 선택값을 반영한다. system은 속성 부재로 표현한다(§4-1 규격). */ function applyThemePreference(next: ThemePreference): void { const root = document.documentElement; if (next === 'system') { root.removeAttribute(THEME_ATTRIBUTE_NAME); } else { root.setAttribute(THEME_ATTRIBUTE_NAME, next); } persistThemePreference(next); } /** * 헤더 테마 토글 버튼의 화면 로컬 로직(ViewModel 대응) — `` 자체가 SSOT이므로 * React state를 두지 않는다(§4-3). 클릭 시점에 DOM에서 현재값을 읽고 다음 값을 계산해 * 속성/localStorage에 쓴다. `useState`/`useEffect`가 없어 hydration 불일치 표면적이 0이다. */ export function useThemePreference(): ThemePreferenceState { const cycleThemePreference = useCallback(() => { const root = document.documentElement; const next = getNextThemePreference(readThemePreference()); // 트랜지션 부여 → 같은 tick에 data-theme 변경 → 200ms 후 트랜지션 속성 제거. root.setAttribute(THEME_TRANSITION_ATTRIBUTE_NAME, ''); applyThemePreference(next); window.setTimeout(() => { root.removeAttribute(THEME_TRANSITION_ATTRIBUTE_NAME); }, THEME_TRANSITION_DURATION_MS); }, []); return { cycleThemePreference }; }