- frontend/ 폴더로 프론트엔드 전체 이관 - signal-batch API 연동 (한국 선박 위치 데이터) - Tailwind CSS 4 + CSS 변수 테마 토큰 (dark/light) - i18next 다국어 (ko/en) 인프라 + 28개 컴포넌트 적용 - 레이어 패널 트리 구조 재설계 (카테고리별 온/오프, 범례) - Google OAuth 로그인 화면 + DEV LOGIN 우회 - 외부 API CORS 프록시 전환 (Airplanes.live, OpenSky, CelesTrak) - ShipLayer 이미지 탭 전환 (signal-batch / MarineTraffic) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
48 lines
1.0 KiB
TypeScript
48 lines
1.0 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react';
|
|
|
|
type Theme = 'dark' | 'light';
|
|
|
|
const STORAGE_KEY = 'kcg:theme';
|
|
const DEFAULT_THEME: Theme = 'dark';
|
|
|
|
function readStoredTheme(): Theme {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored === 'dark' || stored === 'light') return stored;
|
|
} catch {
|
|
// localStorage unavailable
|
|
}
|
|
return DEFAULT_THEME;
|
|
}
|
|
|
|
function applyTheme(theme: Theme) {
|
|
document.documentElement.dataset.theme = theme;
|
|
}
|
|
|
|
export function useTheme() {
|
|
const [theme, setThemeState] = useState<Theme>(() => {
|
|
const t = readStoredTheme();
|
|
applyTheme(t);
|
|
return t;
|
|
});
|
|
|
|
useEffect(() => {
|
|
applyTheme(theme);
|
|
}, [theme]);
|
|
|
|
const setTheme = useCallback((t: Theme) => {
|
|
setThemeState(t);
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, t);
|
|
} catch {
|
|
// localStorage unavailable
|
|
}
|
|
}, []);
|
|
|
|
const toggleTheme = useCallback(() => {
|
|
setTheme(theme === 'dark' ? 'light' : 'dark');
|
|
}, [theme, setTheme]);
|
|
|
|
return { theme, setTheme, toggleTheme } as const;
|
|
}
|