168 lines
3.8 KiB
TypeScript
168 lines
3.8 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { createContext, useContext, useState, useEffect } from "react";
|
||
|
|
|
||
|
|
export type ThemeMode = "light" | "dark" | "system";
|
||
|
|
export type ColorScheme = "orange" | "blue" | "green" | "purple" | "red";
|
||
|
|
|
||
|
|
export interface ThemeConfig {
|
||
|
|
mode: ThemeMode;
|
||
|
|
colorScheme: ColorScheme;
|
||
|
|
customColors?: {
|
||
|
|
primary?: string;
|
||
|
|
secondary?: string;
|
||
|
|
accent?: string;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ThemeContextType {
|
||
|
|
theme: ThemeConfig;
|
||
|
|
setTheme: (theme: Partial<ThemeConfig>) => void;
|
||
|
|
toggleMode: () => void;
|
||
|
|
isDark: boolean;
|
||
|
|
colors: {
|
||
|
|
primary: string;
|
||
|
|
secondary: string;
|
||
|
|
accent: string;
|
||
|
|
background: string;
|
||
|
|
surface: string;
|
||
|
|
text: string;
|
||
|
|
textSecondary: string;
|
||
|
|
border: string;
|
||
|
|
hover: string;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||
|
|
|
||
|
|
const colorSchemes = {
|
||
|
|
orange: {
|
||
|
|
primary: "#f97316",
|
||
|
|
secondary: "#ea580c",
|
||
|
|
accent: "#fb923c",
|
||
|
|
},
|
||
|
|
blue: {
|
||
|
|
primary: "#3b82f6",
|
||
|
|
secondary: "#2563eb",
|
||
|
|
accent: "#60a5fa",
|
||
|
|
},
|
||
|
|
green: {
|
||
|
|
primary: "#10b981",
|
||
|
|
secondary: "#059669",
|
||
|
|
accent: "#34d399",
|
||
|
|
},
|
||
|
|
purple: {
|
||
|
|
primary: "#8b5cf6",
|
||
|
|
secondary: "#7c3aed",
|
||
|
|
accent: "#a78bfa",
|
||
|
|
},
|
||
|
|
red: {
|
||
|
|
primary: "#ef4444",
|
||
|
|
secondary: "#dc2626",
|
||
|
|
accent: "#f87171",
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
const lightColors = {
|
||
|
|
background: "#ffffff",
|
||
|
|
surface: "#f8fafc",
|
||
|
|
text: "#1f2937",
|
||
|
|
textSecondary: "#6b7280",
|
||
|
|
border: "#e5e7eb",
|
||
|
|
hover: "#f3f4f6",
|
||
|
|
};
|
||
|
|
|
||
|
|
const darkColors = {
|
||
|
|
background: "#0f172a",
|
||
|
|
surface: "#1e293b",
|
||
|
|
text: "#f1f5f9",
|
||
|
|
textSecondary: "#94a3b8",
|
||
|
|
border: "#334155",
|
||
|
|
hover: "#334155",
|
||
|
|
};
|
||
|
|
|
||
|
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||
|
|
const [theme, setThemeState] = useState<ThemeConfig>({
|
||
|
|
mode: "system",
|
||
|
|
colorScheme: "orange",
|
||
|
|
});
|
||
|
|
|
||
|
|
const [isDark, setIsDark] = useState(false);
|
||
|
|
|
||
|
|
// 시스템 테마 감지
|
||
|
|
useEffect(() => {
|
||
|
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||
|
|
const handleChange = () => {
|
||
|
|
if (theme.mode === "system") {
|
||
|
|
setIsDark(mediaQuery.matches);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
handleChange();
|
||
|
|
mediaQuery.addEventListener("change", handleChange);
|
||
|
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||
|
|
}, [theme.mode]);
|
||
|
|
|
||
|
|
// 테마 모드에 따른 다크모드 설정
|
||
|
|
useEffect(() => {
|
||
|
|
switch (theme.mode) {
|
||
|
|
case "light":
|
||
|
|
setIsDark(false);
|
||
|
|
break;
|
||
|
|
case "dark":
|
||
|
|
setIsDark(true);
|
||
|
|
break;
|
||
|
|
case "system":
|
||
|
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||
|
|
setIsDark(mediaQuery.matches);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}, [theme.mode]);
|
||
|
|
|
||
|
|
const setTheme = (newTheme: Partial<ThemeConfig>) => {
|
||
|
|
setThemeState(prev => ({ ...prev, ...newTheme }));
|
||
|
|
};
|
||
|
|
|
||
|
|
const toggleMode = () => {
|
||
|
|
setThemeState(prev => ({
|
||
|
|
...prev,
|
||
|
|
mode: prev.mode === "light" ? "dark" : "light"
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
const baseColors = colorSchemes[theme.colorScheme];
|
||
|
|
const themeColors = isDark ? darkColors : lightColors;
|
||
|
|
|
||
|
|
const colors = {
|
||
|
|
primary: theme.customColors?.primary || baseColors.primary,
|
||
|
|
secondary: theme.customColors?.secondary || baseColors.secondary,
|
||
|
|
accent: theme.customColors?.accent || baseColors.accent,
|
||
|
|
background: themeColors.background,
|
||
|
|
surface: themeColors.surface,
|
||
|
|
text: themeColors.text,
|
||
|
|
textSecondary: themeColors.textSecondary,
|
||
|
|
border: themeColors.border,
|
||
|
|
hover: themeColors.hover,
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ThemeContext.Provider value={{
|
||
|
|
theme,
|
||
|
|
setTheme,
|
||
|
|
toggleMode,
|
||
|
|
isDark,
|
||
|
|
colors,
|
||
|
|
}}>
|
||
|
|
{children}
|
||
|
|
</ThemeContext.Provider>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const useTheme = () => {
|
||
|
|
const context = useContext(ThemeContext);
|
||
|
|
if (context === undefined) {
|
||
|
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||
|
|
}
|
||
|
|
return context;
|
||
|
|
};
|