From a132c7eaf8c73860ae6f16ee5891dfe686338f98 Mon Sep 17 00:00:00 2001 From: htlee Date: Tue, 17 Feb 2026 06:22:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20useTheme=20=ED=9B=85=20+=20=ED=85=8C?= =?UTF-8?q?=EB=A7=88=20=ED=86=A0=EA=B8=80=20=EB=B2=84=ED=8A=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useTheme: localStorage 기반 다크/라이트 테마 전환 - data-theme 속성으로 CSS 변수 자동 전환 - Topbar에 Light/Dark 토글 버튼 추가 Co-Authored-By: Claude Opus 4.6 --- .../web/src/pages/dashboard/DashboardPage.tsx | 4 ++ apps/web/src/shared/hooks/index.ts | 1 + apps/web/src/shared/hooks/useTheme.ts | 47 +++++++++++++++++++ apps/web/src/widgets/topbar/Topbar.tsx | 13 ++++- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/shared/hooks/useTheme.ts diff --git a/apps/web/src/pages/dashboard/DashboardPage.tsx b/apps/web/src/pages/dashboard/DashboardPage.tsx index aa4cf77..ae15cae 100644 --- a/apps/web/src/pages/dashboard/DashboardPage.tsx +++ b/apps/web/src/pages/dashboard/DashboardPage.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo } from "react"; import { useAuth } from "../../shared/auth"; +import { useTheme } from "../../shared/hooks"; import { useAisTargetPolling } from "../../features/aisPolling/useAisTargetPolling"; import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types"; import { LEGACY_ALARM_KINDS } from "../../features/legacyDashboard/model/types"; @@ -58,6 +59,7 @@ function useLegacyIndex(data: LegacyVesselDataset | null) { export function DashboardPage() { const { user, logout } = useAuth(); + const { theme, toggleTheme } = useTheme(); const uid = user?.id ?? null; // ── Data fetching ── @@ -259,6 +261,8 @@ export function DashboardPage() { onLogoClick={onLogoClick} userName={user?.name} onLogout={logout} + theme={theme} + onToggleTheme={toggleTheme} /> (() => { + const t = readTheme(); + applyTheme(t); + return t; + }); + + useEffect(() => { + applyTheme(theme); + }, [theme]); + + const setTheme = useCallback((t: Theme) => { + setThemeState(t); + try { + localStorage.setItem(STORAGE_KEY, t); + } catch { + // quota exceeded or unavailable + } + }, []); + + const toggleTheme = useCallback(() => { + setTheme(theme === 'dark' ? 'light' : 'dark'); + }, [theme, setTheme]); + + return { theme, setTheme, toggleTheme } as const; +} diff --git a/apps/web/src/widgets/topbar/Topbar.tsx b/apps/web/src/widgets/topbar/Topbar.tsx index f100743..833f1ad 100644 --- a/apps/web/src/widgets/topbar/Topbar.tsx +++ b/apps/web/src/widgets/topbar/Topbar.tsx @@ -11,9 +11,11 @@ type Props = { onLogoClick?: () => void; userName?: string; onLogout?: () => void; + theme?: "dark" | "light"; + onToggleTheme?: () => void; }; -export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStatus, lastFetchMinutes, clock, adminMode, onLogoClick, userName, onLogout }: Props) { +export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStatus, lastFetchMinutes, clock, adminMode, onLogoClick, userName, onLogout, theme, onToggleTheme }: Props) { const statusColor = pollingStatus === "ready" ? "#22C55E" : pollingStatus === "loading" ? "#F59E0B" : pollingStatus === "error" ? "#EF4444" : "var(--muted)"; return ( @@ -55,6 +57,15 @@ export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStat
{clock}
+ {onToggleTheme && ( + + )} {userName && (
{userName}