Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 다크모드 기능 추가 #8

Merged
merged 8 commits into from
Jul 12, 2023
27 changes: 27 additions & 0 deletions components/commons/headers/headerBtns/darkModeToggle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, {useContext} from 'react';
import {ThemeContext} from '@/context/themeContext';
import styled from '@emotion/styled';
import {lightTheme, ColorTheme} from '@/styles/theme';

interface ToggleProps {
colorTheme: ColorTheme;
}

const DarkModeToggle = () => {
const {colorTheme, toggleColorTheme} = useContext(ThemeContext);

return (
<ToggleButton onClick={toggleColorTheme} colorTheme={colorTheme}>
{colorTheme === lightTheme ? '다크 모드' : '라이트 모드'}
</ToggleButton>
);
}

const ToggleButton = styled('button')<ToggleProps>`
display: flex;
background: ${({colorTheme}) => colorTheme.background};
color: ${({colorTheme}) => colorTheme.main};
cursor: pointer;
`;

export default DarkModeToggle;
25 changes: 25 additions & 0 deletions context/themeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {createContext} from 'react';
import {useDarkMode} from "@/hooks/useDarkMode";
import {lightTheme, ColorTheme} from "@/styles/theme";

interface ContextProps {
colorTheme: ColorTheme;
toggleColorTheme: () => void;
}

export const ThemeContext = createContext<ContextProps>({
colorTheme: lightTheme,
toggleColorTheme: () => {
return null;
}
})

export const ThemeProvider = ({children}) => {
const {colorTheme, toggleColorTheme} = useDarkMode();

return (
<ThemeContext.Provider value={{colorTheme, toggleColorTheme}}>
{children}
</ThemeContext.Provider>
)
}
35 changes: 35 additions & 0 deletions hooks/useDarkMode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import LocalStorage from "@/utils/localStorageUtil";
import {useEffect, useState} from 'react';
import {lightTheme, darkTheme, ColorTheme} from "@/styles/theme";

export const useDarkMode = () => {
const [colorTheme, setColorTheme] = useState<ColorTheme>(lightTheme);
const localStorage = new LocalStorage();

const setMode = (mode: ColorTheme) => {
mode === lightTheme
? localStorage.setItem('travel-theme-frontend-v1', 'light')
: localStorage.setItem('travel-theme-frontend-v1', 'dark');
setColorTheme(mode);
}

const toggleColorTheme = () => {
colorTheme === lightTheme ? setMode(darkTheme) : setMode(lightTheme);
}

useEffect(() => {
const localTheme = localStorage.getItem('travel-theme-frontend-v1');

if (localTheme !== null) {
if (localTheme === 'dark') {
setColorTheme(darkTheme);
document.body.dataset.theme = 'dark';
} else {
setColorTheme(lightTheme);
document.body.dataset.theme = 'light';
}
}
}, [colorTheme]);

return {colorTheme, toggleColorTheme}
}
2 changes: 2 additions & 0 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import {AppProps} from 'next/app';
import {Global, ThemeProvider} from '@emotion/react';
import {globalStyles} from '../styles/globalStyle';
Expand All @@ -11,3 +12,4 @@ export default function App({Component, pageProps}: AppProps) {
</ThemeProvider>
);
}

15 changes: 12 additions & 3 deletions styles/globalStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,18 @@ export const globalStyles = css`

body {
line-height: 1;
background-color: white;
}


body[data-theme="light"] {
background: #fff;
color: #222;
}

body[data-theme="dark"] {
background: #222;
color: #fff;
}

ol,
ul,
li {
Expand All @@ -109,7 +118,7 @@ export const globalStyles = css`
a {
text-decoration: none;
}

@font-face {
font-family: 'NanumSquareNeo';
font-weight: 300;
Expand Down
13 changes: 13 additions & 0 deletions styles/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const lightTheme = {
main: '#222',
sub: '#fff',
background: '#fff',
};

export const darkTheme = {
main: 'white',
sub: 'black',
background: '#222',
};

export type ColorTheme = typeof lightTheme;
106 changes: 106 additions & 0 deletions utils/localStorageUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
export default class LocalStorage {
private data: { [key: string]: { value: string; expiry: string } } = {};

constructor() {
if (!this.isLocalStorageSupported()) {
console.error('로컬 스토리지를 지원하지 않습니다.');
return;
}
this.loadData();
}

private isLocalStorageSupported(): boolean {
try {
const localStorage = typeof window !== 'undefined' ? window.localStorage : null;
if (!localStorage) {
return false;
}

localStorage?.setItem('test', 'test');
localStorage?.removeItem('test');
return true;
} catch (error) {
return false;
}
}

private loadData() {
const localStorage = typeof window !== 'undefined' ? window.localStorage : null;
if (localStorage) {
const dataStr = localStorage.getItem('data');
this.data = dataStr ? JSON.parse(dataStr) : {};
}
}

private saveData() {
const localStorage = typeof window !== 'undefined' ? window.localStorage : null;
try {
if (localStorage) {
localStorage.setItem('data', JSON.stringify(this.data));
}
} catch (error) {
console.error('데이터 저장 중 오류가 발생했습니다.', error);
}
}

public getItem(key: string): string | null {
try {
const item = this.data[key];

if (!item) return null;

const now = new Date();
const expiryDate = new Date(item.expiry);

if (now > expiryDate) {
this.removeItem(key);
this.saveData();
throw new Error(`키가 만료되었습니다: ${key}`);
}

return item.value;
} catch (error) {
console.error('항목을 가져오는 중 오류가 발생했습니다.', error);
return null;
}
}

public setItem(key: string, value: string, ttl?: number) {
try {
const now = new Date();
let expiryDate: Date | undefined = undefined;

if (ttl) {
expiryDate = new Date(now.getTime() + ttl * 3600000);
}

const item = {
value: value,
expiry: expiryDate ? expiryDate.toISOString() : ''
};

this.data[key] = item;
this.saveData();
} catch (error) {
console.error('항목을 설정하는 중 오류가 발생했습니다.', error);
}
}

public removeItem(key: string) {
const newData: { [key: string]: { value: string; expiry: string } } = {};

for (const prop in this.data) {
if (prop !== key) {
newData[prop] = this.data[prop];
}
}

this.data = newData;
this.saveData();
}

public clear() {
this.data = {};
this.saveData();
}
}