File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'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;
}
/** `<html data-theme>`에서 현재 선택을 읽는다. 속성이 없거나 알 수 없는 값이면 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 접근이 차단된 환경(시크릿 모드 등)에서도 조용히 무시한다 — <html data-theme>만
// 으로 현재 세션 동안은 정상 동작하고, 다음 방문 시 다시 system으로 fail-safe한다.
}
}
/** `<html>`에 선택값을 반영한다. 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 대응) — `<html data-theme>` 자체가 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 };
}