diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 936f66a..87d21e8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import { Routes, Route, useLocation } from 'react-router-dom'; import { AnimatePresence, motion } from 'framer-motion'; import DeviceLayout, { useIsMobile } from './components/DeviceLayout'; import Sidebar from './components/Sidebar'; +import { ThemeProvider } from './contexts/ThemeContext'; import ServerDetail from './pages/ServerDetail'; import WorldsPage from './pages/WorldsPage'; import PlayersPage from './pages/PlayersPage'; @@ -46,6 +47,7 @@ function App() { // 인증 페이지는 사이드바 없이 렌더링 if (isAuthPage) { return ( +
@@ -58,12 +60,14 @@ function App() {
+
); } // 관리자/프로필 페이지는 별도 레이아웃 if (isStandalonePage) { return ( +
@@ -75,10 +79,12 @@ function App() {
+
); } return ( +
@@ -105,6 +111,7 @@ function App() {
+
); } diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index ea42f84..1eab4ee 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,8 +1,9 @@ import React, { useState, useRef, useEffect } from 'react'; import { NavLink, useLocation, Link, useNavigate } from 'react-router-dom'; -import { Home, Globe, Users, Menu, X, Gamepad2, Map, Shield, LogIn, LogOut, User, Settings, BarChart2, Package } from 'lucide-react'; +import { Home, Globe, Users, Menu, X, Gamepad2, Map, Shield, LogIn, LogOut, User, Settings, BarChart2, Package, Sun, Moon, Monitor } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useAuth } from '../contexts/AuthContext'; +import { useTheme } from '../contexts/ThemeContext'; import { io } from 'socket.io-client'; // 사이드바 네비게이션 컴포넌트 @@ -16,6 +17,7 @@ const Sidebar = ({ isMobile = false }) => { const location = useLocation(); const navigate = useNavigate(); const { isLoggedIn, isAdmin, user, logout, checkAuth } = useAuth(); + const { theme, cycleTheme, isDark } = useTheme(); const profileMenuRef = useRef(null); const menuItems = [ @@ -259,18 +261,18 @@ const Sidebar = ({ isMobile = false }) => { initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.9 }} - className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 bg-zinc-900 border border-zinc-700 rounded-2xl p-6 z-[101] shadow-2xl" + className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-2xl p-6 z-[101] shadow-2xl" >
-

로그아웃

-

정말 로그아웃 하시겠습니까?

+

로그아웃

+

정말 로그아웃 하시겠습니까?

@@ -288,6 +290,32 @@ const Sidebar = ({ isMobile = false }) => { ); + // 테마 토글 버튼 컴포넌트 + const ThemeToggle = () => { + const getThemeIcon = () => { + if (theme === 'system') return ; + if (theme === 'dark') return ; + return ; + }; + + const getThemeLabel = () => { + if (theme === 'system') return '시스템'; + if (theme === 'dark') return '다크'; + return '라이트'; + }; + + return ( + + ); + }; + // 프로필 영역 (클릭 가능) const ProfileSection = () => (
@@ -578,6 +606,9 @@ const Sidebar = ({ isMobile = false }) => { })} + {/* 테마 토글 */} + + {/* 하단: 로그인/유저 정보 */} {isLoggedIn ? ( diff --git a/frontend/src/contexts/ThemeContext.jsx b/frontend/src/contexts/ThemeContext.jsx new file mode 100644 index 0000000..0d45151 --- /dev/null +++ b/frontend/src/contexts/ThemeContext.jsx @@ -0,0 +1,81 @@ +import React, { createContext, useContext, useState, useEffect } from "react"; + +// 테마 타입: 'system' | 'dark' | 'light' +const ThemeContext = createContext(); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; + +export const ThemeProvider = ({ children }) => { + // localStorage에서 저장된 테마 가져오기 (기본값: system) + const [theme, setTheme] = useState(() => { + const saved = localStorage.getItem("theme"); + return saved || "system"; + }); + + // 실제 적용되는 테마 (시스템 설정 반영) + const [resolvedTheme, setResolvedTheme] = useState("dark"); + + // 시스템 테마 감지 및 적용 + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + const updateResolvedTheme = () => { + if (theme === "system") { + setResolvedTheme(mediaQuery.matches ? "dark" : "light"); + } else { + setResolvedTheme(theme); + } + }; + + updateResolvedTheme(); + + // 시스템 테마 변경 감지 + const handler = () => updateResolvedTheme(); + mediaQuery.addEventListener("change", handler); + + return () => mediaQuery.removeEventListener("change", handler); + }, [theme]); + + // 테마 클래스를 에 적용 + useEffect(() => { + const root = document.documentElement; + root.classList.remove("light", "dark"); + root.classList.add(resolvedTheme); + }, [resolvedTheme]); + + // 테마 변경 및 localStorage 저장 + const setThemeWithStorage = (newTheme) => { + setTheme(newTheme); + localStorage.setItem("theme", newTheme); + }; + + // 테마 순환: system -> dark -> light -> system + const cycleTheme = () => { + const order = ["system", "dark", "light"]; + const currentIndex = order.indexOf(theme); + const nextIndex = (currentIndex + 1) % order.length; + setThemeWithStorage(order[nextIndex]); + }; + + return ( + + {children} + + ); +}; + +export default ThemeContext; diff --git a/frontend/src/index.css b/frontend/src/index.css index 7d6936d..f72c753 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,14 +2,55 @@ @tailwind components; @tailwind utilities; -/* 기본 html, body 스타일 - 다크 배경 & 스크롤바 레이아웃 고정 */ +/* 테마 CSS 변수 */ +:root { + /* 다크 테마 (기본) */ + --bg-primary: #141414; + --bg-secondary: #1c1c1c; + --bg-tertiary: #252525; + --bg-card: #1c1c1c; + --bg-hover: rgba(255, 255, 255, 0.05); + + --text-primary: #ffffff; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + --text-muted: #52525b; + + --border-color: rgba(255, 255, 255, 0.1); + --border-subtle: rgba(255, 255, 255, 0.05); + + --shadow-color: rgba(0, 0, 0, 0.3); +} + +/* 라이트 테마 */ +.light { + --bg-primary: #f8fafc; + --bg-secondary: #ffffff; + --bg-tertiary: #f1f5f9; + --bg-card: #ffffff; + --bg-hover: rgba(0, 0, 0, 0.05); + + --text-primary: #18181b; + --text-secondary: #52525b; + --text-tertiary: #71717a; + --text-muted: #a1a1aa; + + --border-color: rgba(0, 0, 0, 0.1); + --border-subtle: rgba(0, 0, 0, 0.05); + + --shadow-color: rgba(0, 0, 0, 0.1); +} + +/* 기본 html, body 스타일 */ html { scrollbar-gutter: stable; /* 스크롤바 공간 예약 (세로 스크롤바) */ } body { - background: #141414; + background: var(--bg-primary); + color: var(--text-primary); min-height: 100vh; + transition: background-color 0.3s ease, color 0.3s ease; } /* PC 레이아웃 - min-width 적용 */ diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 9ccb6db..c2bb725 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,6 +1,7 @@ /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + darkMode: "class", // 클래스 기반 다크 모드 theme: { extend: { fontFamily: {