diff --git a/config b/config index 36f81ff28..af2375238 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 36f81ff2864e6c3a7ef5af1fd752fab31e9b4edc +Subproject commit af23752382cdb750ff865dedcd9d8a238f1b724d diff --git a/frontend/src/Cabinet/assets/images/moonIcon.svg b/frontend/src/Cabinet/assets/images/moonIcon.svg new file mode 100644 index 000000000..3c8a7af6d --- /dev/null +++ b/frontend/src/Cabinet/assets/images/moonIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/Cabinet/assets/images/sunIcon.svg b/frontend/src/Cabinet/assets/images/sunIcon.svg new file mode 100644 index 000000000..c3a482e73 --- /dev/null +++ b/frontend/src/Cabinet/assets/images/sunIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/Cabinet/components/AdminInfo/Chart/CoinUseLineChart.tsx b/frontend/src/Cabinet/components/AdminInfo/Chart/CoinUseLineChart.tsx index 4860da99c..b4a4f9555 100644 --- a/frontend/src/Cabinet/components/AdminInfo/Chart/CoinUseLineChart.tsx +++ b/frontend/src/Cabinet/components/AdminInfo/Chart/CoinUseLineChart.tsx @@ -15,6 +15,7 @@ const CoinUseLineChart = ({ if (totalCoinUseData === undefined) { return null; } + const formattedData = [ { id: "issuedCoin", @@ -39,7 +40,7 @@ const CoinUseLineChart = ({ (data) => data.id === coinToggleType ); - const yMin = Math.min(...filteredData[0].data.map((d) => d.y)); + const yMin = 0; const yMax = Math.max(...filteredData[0].data.map((d) => d.y)); // NOTE : y축 scale을 log로 표현하기 위해 Y scale을 설정 diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container.tsx b/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container.tsx index 472e4251f..cd54e2466 100644 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container.tsx +++ b/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container.tsx @@ -1,86 +1,51 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; +import { useRecoilValue } from "recoil"; +import { displayStyleState } from "@/Cabinet/recoil/atoms"; import DisplayStyleCard from "@/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard"; import { DisplayStyleToggleType, DisplayStyleType, } from "@/Cabinet/types/enum/displayStyle.type.enum"; +import useDisplayStyleToggle from "@/Cabinet/hooks/useDisplayStyleToggle"; +import { + isDeviceDarkMode, + updateBodyDisplayStyle, +} from "@/Cabinet/utils/displayStyleUtils"; // 로컬스토리지의 display-style-toggle 값에 따라 DisplayStyleType 반환 export const getInitialDisplayStyle = ( - savedDisplayStyleToggle: DisplayStyleToggleType, + displayStyleToggle: DisplayStyleToggleType, darkModeQuery: MediaQueryList -) => { +): DisplayStyleType => { // 라이트 / 다크 버튼 - if (savedDisplayStyleToggle === DisplayStyleToggleType.LIGHT) + if (displayStyleToggle === DisplayStyleToggleType.LIGHT) { return DisplayStyleType.LIGHT; - else if (savedDisplayStyleToggle === DisplayStyleToggleType.DARK) - return DisplayStyleType.DARK; - // 디바이스 버튼 - if (darkModeQuery.matches) { + } else if (displayStyleToggle === DisplayStyleToggleType.DARK) { return DisplayStyleType.DARK; + // 디바이스 버튼 + } else { + return darkModeQuery.matches + ? DisplayStyleType.DARK + : DisplayStyleType.LIGHT; } - return DisplayStyleType.LIGHT; }; const DisplayStyleCardContainer = () => { - const savedDisplayStyleToggle = - (localStorage.getItem("display-style-toggle") as DisplayStyleToggleType) || - DisplayStyleToggleType.DEVICE; - var darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); - const initialDisplayStyle = getInitialDisplayStyle( - savedDisplayStyleToggle, - darkModeQuery - ); - const [darkMode, setDarkMode] = useState( - initialDisplayStyle as DisplayStyleType - ); - const [toggleType, setToggleType] = useState( - savedDisplayStyleToggle - ); - - const setColorsAndLocalStorage = (toggleType: DisplayStyleToggleType) => { - setToggleType(toggleType); - localStorage.setItem("display-style-toggle", toggleType); - }; - - const handleDisplayStyleButtonClick = (displayStyleToggleType: string) => { - if (toggleType === displayStyleToggleType) return; - setToggleType( - displayStyleToggleType as React.SetStateAction - ); - setColorsAndLocalStorage(displayStyleToggleType as DisplayStyleToggleType); - }; + const darkModeQuery = isDeviceDarkMode(); + const toggleType = useRecoilValue(displayStyleState); + const { addDarkModeListener } = useDisplayStyleToggle(); useEffect(() => { - darkModeQuery.addEventListener("change", (event) => - setDarkMode( - event.matches ? DisplayStyleType.DARK : DisplayStyleType.LIGHT - ) - ); - }, []); + const applyDisplayStyle = () => { + const newDarkMode = getInitialDisplayStyle(toggleType, darkModeQuery); + updateBodyDisplayStyle(newDarkMode); + }; - useEffect(() => { - document.body.setAttribute("display-style", darkMode); - }, [darkMode]); - - useEffect(() => { - if (toggleType === DisplayStyleToggleType.LIGHT) { - setDarkMode(DisplayStyleType.LIGHT); - } else if (toggleType === DisplayStyleToggleType.DARK) { - setDarkMode(DisplayStyleType.DARK); - } else { - setDarkMode( - darkModeQuery.matches ? DisplayStyleType.DARK : DisplayStyleType.LIGHT - ); - } + applyDisplayStyle(); + addDarkModeListener(darkModeQuery, applyDisplayStyle); }, [toggleType]); - return ( - - ); + return ; }; export default DisplayStyleCardContainer; diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.tsx b/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.tsx index 59685d886..0c3d1d1f9 100644 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.tsx +++ b/frontend/src/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.tsx @@ -1,19 +1,18 @@ +import React from "react"; +import { useRecoilState } from "recoil"; import styled from "styled-components"; +import { displayStyleState } from "@/Cabinet/recoil/atoms"; import Card from "@/Cabinet/components/Card/Card"; import { CardContentWrapper } from "@/Cabinet/components/Card/CardStyles"; import { ReactComponent as MonitorMobileIcon } from "@/Cabinet/assets/images/monitorMobile.svg"; import { ReactComponent as MoonIcon } from "@/Cabinet/assets/images/moon.svg"; import { ReactComponent as SunIcon } from "@/Cabinet/assets/images/sun.svg"; import { DisplayStyleToggleType } from "@/Cabinet/types/enum/displayStyle.type.enum"; - -interface DisplayStyleProps { - displayStyleToggle: DisplayStyleToggleType; - handleDisplayStyleButtonClick: (DisplayStyleToggleType: string) => void; -} +import useDisplayStyleToggle from "@/Cabinet/hooks/useDisplayStyleToggle"; interface IToggleItemSeparated { name: string; - key: string; + key: DisplayStyleToggleType; icon: React.ComponentType>; } @@ -35,42 +34,48 @@ const toggleList: IToggleItemSeparated[] = [ }, ]; -const DisplayStyleCard = ({ - displayStyleToggle, - handleDisplayStyleButtonClick, -}: DisplayStyleProps) => { +export const updateLocalStorageDisplayStyleToggle = ( + toggleType: DisplayStyleToggleType +) => { + localStorage.setItem("display-style-toggle", toggleType); +}; + +const DisplayStyleCard = () => { + const [toggleType, setToggleType] = useRecoilState(displayStyleState); + const { updateToggleType } = useDisplayStyleToggle(); + const handleButtonClick = (key: DisplayStyleToggleType) => { + if (toggleType === key) return; + updateToggleType(key); + }; + return ( - <> - - - <> - - - {toggleList.map((item) => { - const DisplayStyleIcon = item.icon; - return ( - handleDisplayStyleButtonClick(item.key)} - > - {DisplayStyleIcon && } - {item.name} - - ); - })} - - - - - - + + + + + {toggleList.map((item) => { + const DisplayStyleIcon = item.icon; + return ( + handleButtonClick(item.key)} + > + + {item.name} + + ); + })} + + + + ); }; diff --git a/frontend/src/Cabinet/components/Card/DisplayStyleCard/displayStyleInitializer.ts b/frontend/src/Cabinet/components/Card/DisplayStyleCard/displayStyleInitializer.ts index 3302a8b48..d4e4b4112 100644 --- a/frontend/src/Cabinet/components/Card/DisplayStyleCard/displayStyleInitializer.ts +++ b/frontend/src/Cabinet/components/Card/DisplayStyleCard/displayStyleInitializer.ts @@ -3,19 +3,19 @@ import { DisplayStyleToggleType, DisplayStyleType, } from "@/Cabinet/types/enum/displayStyle.type.enum"; +import { + getDisplayStyleFromLocalStorage, + isDeviceDarkMode, + updateBodyDisplayStyle, +} from "@/Cabinet/utils/displayStyleUtils"; (function () { const isClient = typeof window !== "undefined"; if (isClient) { - const savedDisplayStyleToggle = - (localStorage.getItem( - "display-style-toggle" - ) as DisplayStyleToggleType) || DisplayStyleToggleType.DEVICE; - - const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const darkModeQuery = isDeviceDarkMode(); const colorMode = getInitialDisplayStyle( - savedDisplayStyleToggle, + getDisplayStyleFromLocalStorage(), darkModeQuery ); @@ -27,7 +27,7 @@ import { // 이 코드가 실행중일땐 전역변수가 아직 정의가 안된 상태라 전역변수 대신 hex code 사용 document.addEventListener("DOMContentLoaded", function () { - document.body.setAttribute("display-style", colorMode); + updateBodyDisplayStyle(colorMode); }); } })(); diff --git a/frontend/src/Cabinet/components/Common/DarkModeToggleSwitch.tsx b/frontend/src/Cabinet/components/Common/DarkModeToggleSwitch.tsx new file mode 100644 index 000000000..5eb8f52c5 --- /dev/null +++ b/frontend/src/Cabinet/components/Common/DarkModeToggleSwitch.tsx @@ -0,0 +1,158 @@ +import { useCallback, useEffect, useState } from "react"; +import { useRecoilState } from "recoil"; +import styled from "styled-components"; +import { displayStyleState } from "@/Cabinet/recoil/atoms"; +import { getInitialDisplayStyle } from "@/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard.container"; +import { ReactComponent as MoonIcon } from "@/Cabinet/assets/images/moonIcon.svg"; +import { ReactComponent as SunIcon } from "@/Cabinet/assets/images/sunIcon.svg"; +import { + DisplayStyleToggleType, + DisplayStyleType, +} from "@/Cabinet/types/enum/displayStyle.type.enum"; +import useDisplayStyleToggle from "@/Cabinet/hooks/useDisplayStyleToggle"; +import { + getDisplayStyleFromLocalStorage, + isDeviceDarkMode, + updateBodyDisplayStyle, +} from "@/Cabinet/utils/displayStyleUtils"; + +const DarkModeToggleSwitch = ({ id }: { id: string }) => { + const [toggleType, setToggleType] = useRecoilState(displayStyleState); + const darkModeQuery = isDeviceDarkMode(); + const [displayStyleType, setDisplayStyleType] = useState( + () => { + return getInitialDisplayStyle( + getDisplayStyleFromLocalStorage(), + darkModeQuery + ); + } + ); + + const isDarkMode = displayStyleType === DisplayStyleType.DARK; + const { updateToggleType, addDarkModeListener } = useDisplayStyleToggle(); + + useEffect(() => { + setToggleType(getDisplayStyleFromLocalStorage()); + }, []); + + useEffect(() => { + const updateDisplayStyleType = () => { + const newDisplayStyleType = getInitialDisplayStyle( + toggleType, + darkModeQuery + ); + setDisplayStyleType(newDisplayStyleType); + }; + + updateDisplayStyleType(); + addDarkModeListener(darkModeQuery, updateDisplayStyleType); + }, [toggleType]); + + useEffect(() => { + updateBodyDisplayStyle(displayStyleType); + }, [displayStyleType]); + + const handleToggleChange = useCallback(() => { + let newToggleType; + if (toggleType === DisplayStyleToggleType.DEVICE) { + newToggleType = + displayStyleType === DisplayStyleType.LIGHT + ? DisplayStyleToggleType.DARK + : DisplayStyleToggleType.LIGHT; + } else { + newToggleType = + toggleType === DisplayStyleToggleType.LIGHT + ? DisplayStyleToggleType.DARK + : DisplayStyleToggleType.LIGHT; + } + + updateToggleType(newToggleType); + }, [displayStyleType]); + + return ( + + + + + + + + + + + + + ); +}; + +const ToggleWrapperStyled = styled.div` + display: inline-block; + position: relative; + margin-right: 10px; +`; + +const CheckboxStyled = styled.input.attrs({ type: "checkbox" })` + opacity: 0; + position: absolute; + width: 0; + height: 0; +`; + +const ToggleSwitchStyled = styled.label<{ isChecked: boolean }>` + cursor: pointer; + display: inline-block; + position: relative; + background: ${(props) => + props.isChecked ? "var(--sys-main-color)" : "var(--line-color)"}; + width: 46px; + height: 24px; + border-radius: 50px; + transition: background-color 0.2s ease; +`; + +const ToggleKnobStyled = styled.span<{ isChecked: boolean }>` + position: absolute; + top: 50%; + transform: translateY(-50%); + left: ${(props) => (props.isChecked ? "calc(100% - 21px)" : "3px")}; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--white-text-with-bg-color); + transition: left 0.2s; +`; + +const MoonIconWrapperStyled = styled.div` + position: absolute; + left: 5px; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + + & > svg { + width: 100%; + height: 100%; + fill: var(--ref-gray-900); + } +`; + +const SunIconWrapperStyled = styled.div` + position: absolute; + right: 5px; + top: 50%; + transform: translateY(-50%); + width: 18px; + height: 18px; + + & > svg { + width: 100%; + height: 100%; + } +`; + +export default DarkModeToggleSwitch; diff --git a/frontend/src/Cabinet/components/LentLog/LogTable/AdminCabinetLogTable.tsx b/frontend/src/Cabinet/components/LentLog/LogTable/AdminCabinetLogTable.tsx index f4c09b9c5..f330b294d 100644 --- a/frontend/src/Cabinet/components/LentLog/LogTable/AdminCabinetLogTable.tsx +++ b/frontend/src/Cabinet/components/LentLog/LogTable/AdminCabinetLogTable.tsx @@ -111,3 +111,4 @@ const EmptyLogStyled = styled.div` `; export default AdminCabinetLogTable; + \ No newline at end of file diff --git a/frontend/src/Cabinet/components/TopNav/TopNavDomainGroup/TopNavDomainGroup.tsx b/frontend/src/Cabinet/components/TopNav/TopNavDomainGroup/TopNavDomainGroup.tsx index 737ff86ae..600730b67 100644 --- a/frontend/src/Cabinet/components/TopNav/TopNavDomainGroup/TopNavDomainGroup.tsx +++ b/frontend/src/Cabinet/components/TopNav/TopNavDomainGroup/TopNavDomainGroup.tsx @@ -1,5 +1,6 @@ import { useLocation, useNavigate } from "react-router-dom"; import styled from "styled-components"; +import DarkModeToggleSwitch from "@/Cabinet/components/Common/DarkModeToggleSwitch"; import { ReactComponent as CabiLogo } from "@/Cabinet/assets/images/logo.svg"; import { ReactComponent as PresentationLogo } from "@/Presentation/assets/images/logo.svg"; @@ -58,6 +59,9 @@ const TopNavDomainGroup = ({ isAdmin = false }: { isAdmin?: boolean }) => { {index < domains.length - 1 && } ))} + + + ); }; @@ -127,4 +131,11 @@ const DomainSeparatorStyled = styled.div` background-color: var(--service-man-title-border-btm-color); `; +const ToggleWrapperStyled = styled.div` + position: absolute; + right: 18px; + display: flex; + align-items: center; +`; + export default TopNavDomainGroup; diff --git a/frontend/src/Cabinet/hooks/useDisplayStyleToggle.ts b/frontend/src/Cabinet/hooks/useDisplayStyleToggle.ts new file mode 100644 index 000000000..6d6df1356 --- /dev/null +++ b/frontend/src/Cabinet/hooks/useDisplayStyleToggle.ts @@ -0,0 +1,28 @@ +import { useRecoilState } from "recoil"; +import { displayStyleState } from "@/Cabinet/recoil/atoms"; +import { updateLocalStorageDisplayStyleToggle } from "@/Cabinet/components/Card/DisplayStyleCard/DisplayStyleCard"; +import { DisplayStyleToggleType } from "@/Cabinet/types/enum/displayStyle.type.enum"; + +export const useDisplayStyleToggle = () => { + const [toggleType, setToggleType] = useRecoilState(displayStyleState); + + const updateToggleType = (newToggleType: DisplayStyleToggleType) => { + setToggleType(newToggleType); + updateLocalStorageDisplayStyleToggle(newToggleType); + }; + + const addDarkModeListener = ( + darkModeQuery: MediaQueryList, + callback: () => void + ) => { + if (toggleType === DisplayStyleToggleType.DEVICE) { + darkModeQuery.addEventListener("change", callback); + return () => { + darkModeQuery.removeEventListener("change", callback); + }; + } + }; + return { updateToggleType, addDarkModeListener }; +}; + +export default useDisplayStyleToggle; diff --git a/frontend/src/Cabinet/pages/admin/AdminSlackAlarmPage.tsx b/frontend/src/Cabinet/pages/admin/AdminSlackAlarmPage.tsx index 94a2df9f0..030a49fcc 100644 --- a/frontend/src/Cabinet/pages/admin/AdminSlackAlarmPage.tsx +++ b/frontend/src/Cabinet/pages/admin/AdminSlackAlarmPage.tsx @@ -275,6 +275,7 @@ const FormSubTitleStyled = styled.h3` `; const FormTextareaStyled = styled.textarea` + color: var(--normal-text-color); box-sizing: border-box; width: 100%; min-height: 200px; diff --git a/frontend/src/Cabinet/recoil/atoms.ts b/frontend/src/Cabinet/recoil/atoms.ts index cfc0711a8..e87eaa663 100644 --- a/frontend/src/Cabinet/recoil/atoms.ts +++ b/frontend/src/Cabinet/recoil/atoms.ts @@ -19,6 +19,7 @@ import { import { ClubUserDto } from "@/Cabinet/types/dto/lent.dto"; import { UserDto, UserInfo } from "@/Cabinet/types/dto/user.dto"; import CabinetDetailAreaType from "@/Cabinet/types/enum/cabinetDetailArea.type.enum"; +import { DisplayStyleToggleType } from "@/Cabinet/types/enum/displayStyle.type.enum"; const { persistAtom } = recoilPersist(); @@ -213,3 +214,8 @@ export const currentFloorSectionNamesState = atom({ key: "currentFloorSectionNames", default: [], }); + +export const displayStyleState = atom({ + key: "displayStyle", + default: DisplayStyleToggleType.DEVICE, +}); diff --git a/frontend/src/Cabinet/utils/displayStyleUtils.ts b/frontend/src/Cabinet/utils/displayStyleUtils.ts new file mode 100644 index 000000000..04ea15343 --- /dev/null +++ b/frontend/src/Cabinet/utils/displayStyleUtils.ts @@ -0,0 +1,21 @@ +import { + DisplayStyleToggleType, + DisplayStyleType, +} from "@/Cabinet/types/enum/displayStyle.type.enum"; + +export const getDisplayStyleFromLocalStorage: () => DisplayStyleToggleType = + () => { + return ( + (localStorage.getItem( + "display-style-toggle" + ) as DisplayStyleToggleType) || DisplayStyleToggleType.DEVICE + ); + }; + +export const updateBodyDisplayStyle = (style: DisplayStyleType) => { + document.body.setAttribute("display-style", style); +}; + +export const isDeviceDarkMode = () => { + return window.matchMedia("(prefers-color-scheme: dark)"); +};