From 969de889afdc3afb54ed1eada4f9d06dfbb54883 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Fri, 26 Jan 2024 11:11:16 +0900 Subject: [PATCH 01/33] =?UTF-8?q?refactor:=20RandomPick=20useMedia=20?= =?UTF-8?q?=ED=9B=85=20import=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Tools/RandomPick/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Tools/RandomPick/index.tsx b/src/pages/Tools/RandomPick/index.tsx index f1e00cc0..ea992b55 100644 --- a/src/pages/Tools/RandomPick/index.tsx +++ b/src/pages/Tools/RandomPick/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { AiOutlineUserAdd, AiFillSetting } from 'react-icons/ai'; import { css } from 'styled-components'; -import useMedia from '@Hooks/useMedia'; +import { useMedia } from '@Hooks/useMedia'; import useModal from '@Hooks/useModal'; import Button from '@Components/Button'; From d9770c62b49c31f4d45d1d12db1fc6d46dc1c0ab Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Fri, 26 Jan 2024 11:15:44 +0900 Subject: [PATCH 02/33] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Router.tsx | 2 ++ src/constant/routePath.ts | 18 +++++++++--------- src/pages/Tools/Timer/index.tsx | 5 +++++ 3 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 src/pages/Tools/Timer/index.tsx diff --git a/src/Router.tsx b/src/Router.tsx index 606ea95b..59c3fd79 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -13,6 +13,7 @@ import StudentManagement from '@Pages/StudentManagement'; import StudentInfo from '@Pages/StudentManagement/StudentInfo'; import StudentRegister from '@Pages/StudentManagement/StudentRegister'; import RandomPick from '@Pages/Tools/RandomPick'; +import Timer from '@Pages/Tools/Timer'; const router = createBrowserRouter([ { @@ -89,6 +90,7 @@ const router = createBrowserRouter([ children: [ { path: ROUTE_PATH.timer, + element: , }, { path: ROUTE_PATH.randomPick, diff --git a/src/constant/routePath.ts b/src/constant/routePath.ts index aa43bc42..17682da3 100644 --- a/src/constant/routePath.ts +++ b/src/constant/routePath.ts @@ -1,5 +1,5 @@ import { AiOutlineUsergroupAdd } from 'react-icons/ai'; -import { BiBowlRice } from 'react-icons/bi'; +import { BiBowlRice, BiTimer } from 'react-icons/bi'; import { // BsCalendarWeek, // BsNewspaper, @@ -90,7 +90,7 @@ export const CATEGORIES: Categories = [ category: 'main', name: '편의도구', path: 'tools', - firstChildPath: 'random-pick', + firstChildPath: 'timer', children: [ 'timer', 'randomPick', @@ -182,13 +182,13 @@ export const CATEGORIES: Categories = [ }, // tools - // { - // key: 'timer', - // category: 'middle', - // name: '타이머', - // path: 'timer', - // Icon: BiTimer, - // }, + { + key: 'timer', + category: 'middle', + name: '타이머', + path: 'timer', + Icon: BiTimer, + }, { key: 'randomPick', category: 'middle', diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx new file mode 100644 index 00000000..fdf77128 --- /dev/null +++ b/src/pages/Tools/Timer/index.tsx @@ -0,0 +1,5 @@ +function Timer() { + return
; +} + +export default Timer; From 9cde1ccff5860fe10bd729310ae2747e79c194b3 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Fri, 26 Jan 2024 12:17:14 +0900 Subject: [PATCH 03/33] =?UTF-8?q?feat:=20Timer=20=EB=A7=88=ED=81=AC?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Tools/Timer/index.tsx | 17 +++++++++- src/pages/Tools/Timer/style.ts | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/pages/Tools/Timer/style.ts diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index fdf77128..069db8e9 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -1,5 +1,20 @@ +import { FaPlay } from 'react-icons/fa6'; + +import * as S from './style'; + function Timer() { - return
; + return ( + + 쉬는시간 + + 03:20 + + + + + + + ); } export default Timer; diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts new file mode 100644 index 00000000..fd1ecfc9 --- /dev/null +++ b/src/pages/Tools/Timer/style.ts @@ -0,0 +1,57 @@ +import styled from 'styled-components'; + +import { flexCustom } from '@Styles/common'; +import theme from '@Styles/theme'; + +export const Layout = styled.div` + ${flexCustom('column', 'center', 'center')}; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +export const TimerMemo = styled.div` + padding: 20px 240px; + border-radius: 20px; + + background-color: ${theme.color.gray[200]}; + + font-weight: 600; + font-size: 4rem; +`; + +export const TimeContainer = styled.div` + ${flexCustom('column', 'center', 'center')}; +`; + +export const Time = styled.span` + font-size: 24vw; + font-weight: 600; +`; + +export const TimeBar = styled.div` + width: 100%; + height: 20px; + + border-radius: 4px; + + background-color: ${theme.color.primary[100]}; +`; + +export const TimerButton = styled.div` + ${flexCustom('row', 'center', 'center')} + + margin-top: 40px; + + width: 100px; + height: 100px; + border-radius: 50%; + + background-color: ${theme.color.primary[500]}; + color: ${theme.color.white}; + + font-size: 4rem; + + cursor: pointer; +`; From 7e0a9c1342b8f3d9fa62b649fb4669bf37306bea Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Fri, 26 Jan 2024 13:07:35 +0900 Subject: [PATCH 04/33] =?UTF-8?q?feat:=20Timer=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit memo 사이즈 크게 및 타이머 너버릭 스타일 적용 --- src/pages/Tools/Timer/style.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index fd1ecfc9..276dd54f 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -5,10 +5,13 @@ import theme from '@Styles/theme'; export const Layout = styled.div` ${flexCustom('column', 'center', 'center')}; + position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); + + min-width: 90%; `; export const TimerMemo = styled.div` @@ -18,21 +21,24 @@ export const TimerMemo = styled.div` background-color: ${theme.color.gray[200]}; font-weight: 600; - font-size: 4rem; + font-size: 6rem; `; export const TimeContainer = styled.div` ${flexCustom('column', 'center', 'center')}; + min-width: 100%; `; export const Time = styled.span` font-size: 24vw; - font-weight: 600; + font-weight: 700; + + font-variant-numeric: tabular-nums; `; export const TimeBar = styled.div` - width: 100%; - height: 20px; + min-width: 100%; + height: 24px; border-radius: 4px; From 6464fb79d4e7de3715cd47b942fa82e21cdf7643 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Fri, 26 Jan 2024 13:22:15 +0900 Subject: [PATCH 05/33] =?UTF-8?q?feat:=20Timer=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Tools/Timer/index.tsx | 40 ++++++++++++++++++++++++++++----- src/pages/Tools/Timer/style.ts | 9 ++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 069db8e9..6f77f09e 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -1,16 +1,46 @@ -import { FaPlay } from 'react-icons/fa6'; +import { useEffect, useState } from 'react'; +import { FaPause, FaPlay } from 'react-icons/fa6'; import * as S from './style'; function Timer() { + const [initTime, setInitTime] = useState(320); + const [time, setTime] = useState(320); + const [state, setState] = useState<'stop' | 'play'>('stop'); + + const minute = Math.floor(time / 60); + const second = time - minute * 60; + const displayMinute = minute < 10 ? '0' + String(minute) : minute; + const displaySecond = second < 10 ? '0' + String(second) : second; + const displayTime = `${displayMinute}:${displaySecond}`; + const timerButton = state === 'stop' ? : ; + const progress = ((initTime - time) / initTime) * 100; + + const handleClickTimerButton = () => { + if (state === 'play') setState('stop'); + else setState('play'); + }; + + useEffect(() => { + if (time === 0 || state === 'stop') return; + + const timer = setInterval(() => { + setTime((prev) => prev - 1); + }, 1000); + + return () => clearInterval(timer); + }, [time, state]); + return ( 쉬는시간 - 03:20 - - - + {displayTime} + + + + + {timerButton} diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index 276dd54f..c2c3c67e 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -40,11 +40,16 @@ export const TimeBar = styled.div` min-width: 100%; height: 24px; - border-radius: 4px; - background-color: ${theme.color.primary[100]}; `; +export const ProgressBar = styled.div<{ progress: number }>` + height: 100%; + width: ${({ progress }) => `${progress}%`}; + + background-color: ${theme.color.primary[500]}; +`; + export const TimerButton = styled.div` ${flexCustom('row', 'center', 'center')} From 18111795caf5d39cf85adc09802534d7f18d1587 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sat, 27 Jan 2024 17:56:44 +0900 Subject: [PATCH 06/33] =?UTF-8?q?feat:=20=EB=8B=A4=ED=81=AC=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Tools/Timer/style.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index c2c3c67e..2046e3cf 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -1,8 +1,10 @@ -import styled from 'styled-components'; +import styled, { RuleSet, css } from 'styled-components'; import { flexCustom } from '@Styles/common'; import theme from '@Styles/theme'; +import { ThemeName } from '@Types/style'; + export const Layout = styled.div` ${flexCustom('column', 'center', 'center')}; @@ -14,14 +16,26 @@ export const Layout = styled.div` min-width: 90%; `; +const TIMER_MEMO_THEME: Record = { + light: css` + background-color: ${theme.color.gray[200]}; + `, + + dark: css` + background-color: ${theme.color.gray[700]}; + `, +}; + export const TimerMemo = styled.div` padding: 20px 240px; border-radius: 20px; - background-color: ${theme.color.gray[200]}; - font-weight: 600; font-size: 6rem; + + ${({ theme }) => css` + ${TIMER_MEMO_THEME[theme.name]}; + `} `; export const TimeContainer = styled.div` From 6a6a5ee88c0944a4fee633f628768da7e6d2999a Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sat, 27 Jan 2024 18:24:31 +0900 Subject: [PATCH 07/33] =?UTF-8?q?refactor:=20useTimer=20=ED=9B=85=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/tools/timer/useTimer.ts | 28 ++++++++++++++++++++++++++++ src/pages/Tools/Timer/index.tsx | 30 +++++++----------------------- 2 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 src/hooks/tools/timer/useTimer.ts diff --git a/src/hooks/tools/timer/useTimer.ts b/src/hooks/tools/timer/useTimer.ts new file mode 100644 index 00000000..bd34430b --- /dev/null +++ b/src/hooks/tools/timer/useTimer.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; + +const useTimer = () => { + const [initTime, setInitTime] = useState(320); + const [time, setTime] = useState(320); + const [state, setState] = useState<'stop' | 'play'>('stop'); + + const progress = ((initTime - time) / initTime) * 100; + + const toggleState = () => { + if (state === 'play') setState('stop'); + else setState('play'); + }; + + useEffect(() => { + if (time === 0 || state === 'stop') return; + + const timer = setInterval(() => { + setTime((prev) => prev - 1); + }, 1000); + + return () => clearInterval(timer); + }, [time, state]); + + return { time, state, progress, toggleState }; +}; + +export default useTimer; diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 6f77f09e..4def747c 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -1,35 +1,21 @@ -import { useEffect, useState } from 'react'; import { FaPause, FaPlay } from 'react-icons/fa6'; +import useTimer from '@Hooks/tools/timer/useTimer'; + import * as S from './style'; function Timer() { - const [initTime, setInitTime] = useState(320); - const [time, setTime] = useState(320); - const [state, setState] = useState<'stop' | 'play'>('stop'); + const { state, time, progress, toggleState } = useTimer(); const minute = Math.floor(time / 60); const second = time - minute * 60; + const displayMinute = minute < 10 ? '0' + String(minute) : minute; const displaySecond = second < 10 ? '0' + String(second) : second; - const displayTime = `${displayMinute}:${displaySecond}`; - const timerButton = state === 'stop' ? : ; - const progress = ((initTime - time) / initTime) * 100; - const handleClickTimerButton = () => { - if (state === 'play') setState('stop'); - else setState('play'); - }; - - useEffect(() => { - if (time === 0 || state === 'stop') return; - - const timer = setInterval(() => { - setTime((prev) => prev - 1); - }, 1000); + const displayTime = `${displayMinute}:${displaySecond}`; - return () => clearInterval(timer); - }, [time, state]); + const timerButton = state === 'stop' ? : ; return ( @@ -39,9 +25,7 @@ function Timer() { - - {timerButton} - + {timerButton} ); From ca6f6f1f04e34497f8ab3ad1a12139bb94c26543 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 4 Feb 2024 12:44:29 +0900 Subject: [PATCH 08/33] =?UTF-8?q?refactor:=20useInterval=20=ED=9B=85=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/tools/timer/useTimer.ts | 15 +++++++-------- src/hooks/useInterval.ts | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 src/hooks/useInterval.ts diff --git a/src/hooks/tools/timer/useTimer.ts b/src/hooks/tools/timer/useTimer.ts index bd34430b..336facd6 100644 --- a/src/hooks/tools/timer/useTimer.ts +++ b/src/hooks/tools/timer/useTimer.ts @@ -1,5 +1,7 @@ import { useEffect, useState } from 'react'; +import useInterval from '@Hooks/useInterval'; + const useTimer = () => { const [initTime, setInitTime] = useState(320); const [time, setTime] = useState(320); @@ -12,15 +14,12 @@ const useTimer = () => { else setState('play'); }; - useEffect(() => { - if (time === 0 || state === 'stop') return; - - const timer = setInterval(() => { + useInterval( + () => { setTime((prev) => prev - 1); - }, 1000); - - return () => clearInterval(timer); - }, [time, state]); + }, + time === 0 || state === 'stop' ? null : 1000, + ); return { time, state, progress, toggleState }; }; diff --git a/src/hooks/useInterval.ts b/src/hooks/useInterval.ts new file mode 100644 index 00000000..f3c76c7a --- /dev/null +++ b/src/hooks/useInterval.ts @@ -0,0 +1,20 @@ +import { useEffect, useRef } from 'react'; + +const useInterval = (callback: () => void, delay: number | null) => { + const savedCallback = useRef<() => void>(); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + if (!savedCallback || !delay) return; + + const tick = () => savedCallback.current!(); + const id = setInterval(tick, delay); + + return () => clearInterval(id); + }, [delay]); +}; + +export default useInterval; From 93984e062fa4464982c1ece71acd91ce7f6cfcce Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 4 Feb 2024 20:08:44 +0900 Subject: [PATCH 09/33] =?UTF-8?q?refactor:=20useInterval=20=ED=9B=85=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20web=20worker=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/tools/timer/useTimer.ts | 36 +++++++++++++++++++++---------- src/pages/Tools/Timer/index.tsx | 2 +- src/workers/timerWorker.ts | 7 ++++++ tsconfig.json | 2 +- 4 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 src/workers/timerWorker.ts diff --git a/src/hooks/tools/timer/useTimer.ts b/src/hooks/tools/timer/useTimer.ts index 336facd6..e3e6ea22 100644 --- a/src/hooks/tools/timer/useTimer.ts +++ b/src/hooks/tools/timer/useTimer.ts @@ -1,25 +1,39 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; -import useInterval from '@Hooks/useInterval'; +const worker = new Worker('../../../src/workers/timerWorker.ts'); const useTimer = () => { - const [initTime, setInitTime] = useState(320); - const [time, setTime] = useState(320); - const [state, setState] = useState<'stop' | 'play'>('stop'); + const [initTime, setInitTime] = useState(20); + const [time, setTime] = useState(initTime); + const [state, setState] = useState<'pause' | 'play'>('pause'); const progress = ((initTime - time) / initTime) * 100; const toggleState = () => { - if (state === 'play') setState('stop'); + if (state === 'play') setState('pause'); else setState('play'); }; - useInterval( + const playTimer = useCallback(() => { () => { - setTime((prev) => prev - 1); - }, - time === 0 || state === 'stop' ? null : 1000, - ); + worker.postMessage(time); + worker.onmessage = (event: MessageEvent) => { + const leftTime = event.data; + + setTime(leftTime); + + if (leftTime === 0) setState('pause'); + }; + }; + }, [time]); + + useEffect(() => { + if (state === 'play') playTimer(); + + return () => { + worker.terminate(); + }; + }, [playTimer, state]); return { time, state, progress, toggleState }; }; diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 4def747c..97eb2397 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -15,7 +15,7 @@ function Timer() { const displayTime = `${displayMinute}:${displaySecond}`; - const timerButton = state === 'stop' ? : ; + const timerButton = state === 'pause' ? : ; return ( diff --git a/src/workers/timerWorker.ts b/src/workers/timerWorker.ts new file mode 100644 index 00000000..20cdc658 --- /dev/null +++ b/src/workers/timerWorker.ts @@ -0,0 +1,7 @@ +onmessage = (event: MessageEvent) => { + let time = event.data; + setInterval(() => { + time--; + postMessage(time); + }, 1000); +}; diff --git a/tsconfig.json b/tsconfig.json index 94fcd424..470f3a02 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], + "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], "allowJs": false, "skipLibCheck": true, "esModuleInterop": false, From 6434f89b8a34c82c78fa185568a549e8d178fa29 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 4 Feb 2024 20:41:49 +0900 Subject: [PATCH 10/33] =?UTF-8?q?feat:=20=EC=A0=95=EC=A7=80=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/tools/timer/useTimer.ts | 33 +++++++++++++++++-------------- src/pages/Tools/Timer/index.tsx | 21 +++++++++++++++----- src/pages/Tools/Timer/style.ts | 5 +++++ 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/hooks/tools/timer/useTimer.ts b/src/hooks/tools/timer/useTimer.ts index e3e6ea22..ae53d9e0 100644 --- a/src/hooks/tools/timer/useTimer.ts +++ b/src/hooks/tools/timer/useTimer.ts @@ -1,41 +1,44 @@ import { useCallback, useEffect, useState } from 'react'; -const worker = new Worker('../../../src/workers/timerWorker.ts'); - const useTimer = () => { - const [initTime, setInitTime] = useState(20); + const [initTime, setInitTime] = useState(5); const [time, setTime] = useState(initTime); - const [state, setState] = useState<'pause' | 'play'>('pause'); + const [isProceeding, setIsProceeding] = useState(false); const progress = ((initTime - time) / initTime) * 100; - const toggleState = () => { - if (state === 'play') setState('pause'); - else setState('play'); + const toggleState = () => setIsProceeding((prev) => !prev); + + const resetTimer = () => { + setTime(initTime); + setIsProceeding(false); }; - const playTimer = useCallback(() => { - () => { + const playTimer = useCallback( + (worker: Worker) => { worker.postMessage(time); worker.onmessage = (event: MessageEvent) => { const leftTime = event.data; setTime(leftTime); - if (leftTime === 0) setState('pause'); + if (leftTime === 0) setIsProceeding(false); }; - }; - }, [time]); + }, + [time], + ); useEffect(() => { - if (state === 'play') playTimer(); + const worker = new Worker('../../../src/workers/timerWorker.ts'); + + if (isProceeding) playTimer(worker); return () => { worker.terminate(); }; - }, [playTimer, state]); + }, [playTimer, isProceeding]); - return { time, state, progress, toggleState }; + return { time, isProceeding, progress, toggleState, resetTimer }; }; export default useTimer; diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 97eb2397..24327e06 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -1,11 +1,11 @@ -import { FaPause, FaPlay } from 'react-icons/fa6'; +import { FaPause, FaPlay, FaStop } from 'react-icons/fa6'; import useTimer from '@Hooks/tools/timer/useTimer'; import * as S from './style'; function Timer() { - const { state, time, progress, toggleState } = useTimer(); + const { isProceeding, time, progress, toggleState, resetTimer } = useTimer(); const minute = Math.floor(time / 60); const second = time - minute * 60; @@ -15,8 +15,6 @@ function Timer() { const displayTime = `${displayMinute}:${displaySecond}`; - const timerButton = state === 'pause' ? : ; - return ( 쉬는시간 @@ -25,7 +23,20 @@ function Timer() { - {timerButton} + + {isProceeding || progress === 100 ? ( + + + + ) : ( + + + + )} + + + + ); diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index 2046e3cf..e2feccca 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -64,6 +64,11 @@ export const ProgressBar = styled.div<{ progress: number }>` background-color: ${theme.color.primary[500]}; `; +export const TimerButtonWrapper = styled.div` + display: flex; + gap: 60px; +`; + export const TimerButton = styled.div` ${flexCustom('row', 'center', 'center')} From 96f764d5affe7569824760883e7f09bd92acde00 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 4 Feb 2024 23:57:43 +0900 Subject: [PATCH 11/33] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/tools/timer/useTimer.ts | 18 ++- .../TimeSettingModal/TimeSettingModal.tsx | 105 ++++++++++++++++++ .../Tools/Timer/TimeSettingModal/style.ts | 56 ++++++++++ src/pages/Tools/Timer/index.tsx | 22 +++- src/pages/Tools/Timer/style.ts | 31 ++++++ 5 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 src/pages/Tools/Timer/TimeSettingModal/TimeSettingModal.tsx create mode 100644 src/pages/Tools/Timer/TimeSettingModal/style.ts diff --git a/src/hooks/tools/timer/useTimer.ts b/src/hooks/tools/timer/useTimer.ts index ae53d9e0..0f7be298 100644 --- a/src/hooks/tools/timer/useTimer.ts +++ b/src/hooks/tools/timer/useTimer.ts @@ -1,14 +1,19 @@ import { useCallback, useEffect, useState } from 'react'; const useTimer = () => { - const [initTime, setInitTime] = useState(5); - const [time, setTime] = useState(initTime); + const [initTime, setInitTime] = useState(300); + const [time, setTime] = useState(300); const [isProceeding, setIsProceeding] = useState(false); const progress = ((initTime - time) / initTime) * 100; const toggleState = () => setIsProceeding((prev) => !prev); + const changeInitTime = (minute: number, second: number) => { + setInitTime(minute * 60 + second); + setTime(minute * 60 + second); + }; + const resetTimer = () => { setTime(initTime); setIsProceeding(false); @@ -38,7 +43,14 @@ const useTimer = () => { }; }, [playTimer, isProceeding]); - return { time, isProceeding, progress, toggleState, resetTimer }; + return { + time, + isProceeding, + progress, + toggleState, + resetTimer, + changeInitTime, + }; }; export default useTimer; diff --git a/src/pages/Tools/Timer/TimeSettingModal/TimeSettingModal.tsx b/src/pages/Tools/Timer/TimeSettingModal/TimeSettingModal.tsx new file mode 100644 index 00000000..76b9a0c7 --- /dev/null +++ b/src/pages/Tools/Timer/TimeSettingModal/TimeSettingModal.tsx @@ -0,0 +1,105 @@ +import { ChangeEventHandler, useState } from 'react'; + +import useModal from '@Hooks/useModal'; + +import Button from '@Components/Button'; +import Input from '@Components/Input'; + +import * as S from './style'; + +type Props = { + changeInitTime: (minute: number, second: number) => void; +}; + +function TimeSettingModal({ changeInitTime }: Props) { + const { closeModal } = useModal(); + + const [minute, setMinute] = useState(5); + const [second, setSecond] = useState(0); + + const handleChangeTime: ( + type: 'minute' | 'second', + ) => ChangeEventHandler = (type) => (event) => { + const value = Number(event.target.value); + + if (isNaN(value)) return; + + if (value > 60 && type === 'minute') { + setMinute(60); + return; + } + + if (value > 59 && type === 'second') { + setSecond(59); + return; + } + + if (type === 'minute') setMinute(value); + else setSecond(value); + }; + + const handleClickMinute = (minute: number) => { + setMinute(minute); + setSecond(0); + }; + + const handleClickConfirm = () => { + changeInitTime(minute, second); + closeModal(); + }; + + return ( + + 타이머 시간 설정하기 + + 타이머 시간을 설정할 수 있어요. +
+ 직접 설정할 수 있는 최대 시간은 60분에요. +
+ + 직접 설정하기 + + + + + + + + + + + + + 간편 설정하기 + + {[1, 3, 5, 10, 20, 30].map((minute) => ( + + ))} + + + + + + +
+ ); +} + +export default TimeSettingModal; diff --git a/src/pages/Tools/Timer/TimeSettingModal/style.ts b/src/pages/Tools/Timer/TimeSettingModal/style.ts new file mode 100644 index 00000000..d09df51d --- /dev/null +++ b/src/pages/Tools/Timer/TimeSettingModal/style.ts @@ -0,0 +1,56 @@ +import styled from 'styled-components'; + +import { flexCustom } from '@Styles/common'; +import theme from '@Styles/theme'; + +export const Layout = styled.div` + display: grid; + gap: 20px; +`; + +export const ModalTitle = styled.h2` + justify-self: center; + + font-size: 1.8rem; + font-weight: 600; + color: ${theme.color.primary[500]}; +`; + +export const ModalDescription = styled.p` + color: ${(props) => props.theme.grayText}; + line-height: 140%; +`; + +export const InputLabel = styled.label` + display: grid; + grid-template-columns: 120px 1fr; +`; + +export const InputLabelText = styled.span` + font-weight: 500; +`; + +export const InputContainer = styled.div` + display: flex; + gap: 10px; +`; + +export const InputWrapper = styled.div` + display: flex; + align-items: center; + gap: 5px; +`; + +export const TimerButtonContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 4px; +`; + +export const BottomButtonContainer = styled.div` + display: flex; + justify-content: flex-end; + gap: 20px; + + margin-top: 10px; +`; diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 24327e06..b2a804b3 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -1,11 +1,23 @@ import { FaPause, FaPlay, FaStop } from 'react-icons/fa6'; import useTimer from '@Hooks/tools/timer/useTimer'; +import useModal from '@Hooks/useModal'; +import TimeSettingModal from './TimeSettingModal/TimeSettingModal'; import * as S from './style'; function Timer() { - const { isProceeding, time, progress, toggleState, resetTimer } = useTimer(); + const { openModal } = useModal(); + const { + isProceeding, + time, + progress, + toggleState, + resetTimer, + changeInitTime, + } = useTimer(); + + console.log(time); const minute = Math.floor(time / 60); const second = time - minute * 60; @@ -19,7 +31,13 @@ function Timer() { 쉬는시간 - {displayTime} + + openModal() + } + > + {displayTime} + diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index e2feccca..48f289cb 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -43,11 +43,42 @@ export const TimeContainer = styled.div` min-width: 100%; `; +const TIMER_THEME: Record = { + light: css` + &:hover { + background-color: ${theme.color.gray[200]}; + } + `, + + dark: css` + &:hover { + background-color: ${theme.color.gray[700]}; + } + `, +}; + export const Time = styled.span` + width: 100%; + + margin: 40px 0px; + font-size: 24vw; font-weight: 700; + text-align: center; + + line-height: 100%; font-variant-numeric: tabular-nums; + transition: background-color 0.2s ease; + border-radius: 20px; + + &:hover { + cursor: pointer; + } + + ${({ theme }) => css` + ${TIMER_THEME[theme.name]}; + `} `; export const TimeBar = styled.div` From b777d388a8e2716c4d3caf9a6aa7aa28af96cf15 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 4 Feb 2024 23:58:17 +0900 Subject: [PATCH 12/33] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Tools/Timer/TimeSettingModal/style.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Tools/Timer/TimeSettingModal/style.ts b/src/pages/Tools/Timer/TimeSettingModal/style.ts index d09df51d..f8eae292 100644 --- a/src/pages/Tools/Timer/TimeSettingModal/style.ts +++ b/src/pages/Tools/Timer/TimeSettingModal/style.ts @@ -1,6 +1,5 @@ import styled from 'styled-components'; -import { flexCustom } from '@Styles/common'; import theme from '@Styles/theme'; export const Layout = styled.div` From 8dea14dea35b1d6de4a5d386615b13af815418ee Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Mon, 5 Feb 2024 07:39:47 +0900 Subject: [PATCH 13/33] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EB=A8=B8=20=EC=8B=9C=EA=B0=84=20=EA=B8=B0=EC=96=B5?= =?UTF-8?q?=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TimeSettingModal/TimeSettingModal.tsx | 19 ++++++++++++++----- .../Tools/Timer/TimeSettingModal/style.ts | 2 +- src/pages/Tools/Timer/index.tsx | 8 +++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pages/Tools/Timer/TimeSettingModal/TimeSettingModal.tsx b/src/pages/Tools/Timer/TimeSettingModal/TimeSettingModal.tsx index 76b9a0c7..1f7d6e42 100644 --- a/src/pages/Tools/Timer/TimeSettingModal/TimeSettingModal.tsx +++ b/src/pages/Tools/Timer/TimeSettingModal/TimeSettingModal.tsx @@ -8,14 +8,20 @@ import Input from '@Components/Input'; import * as S from './style'; type Props = { + minute: number; + second: number; changeInitTime: (minute: number, second: number) => void; }; -function TimeSettingModal({ changeInitTime }: Props) { +function TimeSettingModal({ + minute: prevMinute, + second: prevSecond, + changeInitTime, +}: Props) { const { closeModal } = useModal(); - const [minute, setMinute] = useState(5); - const [second, setSecond] = useState(0); + const [minute, setMinute] = useState(prevMinute); + const [second, setSecond] = useState(prevSecond); const handleChangeTime: ( type: 'minute' | 'second', @@ -54,7 +60,7 @@ function TimeSettingModal({ changeInitTime }: Props) { 타이머 시간을 설정할 수 있어요.
- 직접 설정할 수 있는 최대 시간은 60분에요. + 직접 설정할 수 있는 최대 시간은 60분이에요.
직접 설정하기 @@ -80,7 +86,7 @@ function TimeSettingModal({ changeInitTime }: Props) { 간편 설정하기 - {[1, 3, 5, 10, 20, 30].map((minute) => ( + {[3, 5, 10, 20, 30].map((minute) => ( @@ -100,6 +116,25 @@ function TimeSettingModal({ 최근 타이머 시간 + + {recentTimer.length !== 0 ? ( + recentTimer.map((time, index) => ( + + handleClickTimeButton(Math.floor(time / 60), time % 60) + } + > + {Math.floor(time / 60)}분 {time % 60}초 + + )) + ) : ( + + 최근에 사용한 타이머가 없어요. + + )} + + + +
+ ); }; export default TimerMemoModal; diff --git a/src/pages/Tools/Timer/TimerMemoModal/style.ts b/src/pages/Tools/Timer/TimerMemoModal/style.ts new file mode 100644 index 00000000..b673381c --- /dev/null +++ b/src/pages/Tools/Timer/TimerMemoModal/style.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components'; + +import theme from '@Styles/theme'; + +export const Layout = styled.div` + display: grid; + gap: 20px; +`; + +export const ModalTitle = styled.h2` + justify-self: center; + + font-size: 1.8rem; + font-weight: 600; + color: ${theme.color.primary[500]}; +`; + +export const ModalDescription = styled.p` + color: ${(props) => props.theme.grayText}; + line-height: 140%; +`; + +export const BottomButtonContainer = styled.div` + display: flex; + justify-content: flex-end; + gap: 20px; + + margin-top: 10px; +`; diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 92300296..e5880fcc 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { FaPause, FaPlay, FaStop } from 'react-icons/fa6'; import useTimer from '@Hooks/tools/timer/useTimer'; @@ -16,8 +16,6 @@ function Timer() { standard: 'width', }); - console.log(width); - const { openModal } = useModal(); const { isProceeding, @@ -28,15 +26,18 @@ function Timer() { changeInitTime, } = useTimer(); - const [memo, setMemo] = useState(); + const [memo, setMemo] = useState(''); const displayTime = format.time(time); - const timerFontSize = `${width / 3.3}px`; + const timerFontSize = `${width / 3.8}px`; + + const changeMemo = (memo: string) => setMemo(memo); const handleClickTime = () => openModal(); - const handleClickMessage = () => openModal(); + const handleClickMessage = () => + openModal(); return ( diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index 0fba3f19..b3c7b140 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -37,6 +37,8 @@ export const TimerMemo = styled.div` text-align: center; + cursor: pointer; + ${({ theme }) => css` ${TIMER_MEMO_THEME[theme.name]}; `} @@ -57,6 +59,8 @@ const TIMER_THEME: Record = { }; export const Time = styled.span<{ $fontSize: string }>` + position: relative; + display: flex; align-items: center; justify-content: center; @@ -69,7 +73,7 @@ export const Time = styled.span<{ $fontSize: string }>` text-align: center; letter-spacing: 20px; - line-height: 100%; + text-indent: 1; font-variant-numeric: tabular-nums; transition: background-color 0.2s ease; From 18458d6ee255143dd17d61d0b288918714ae051b Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 18 Feb 2024 16:25:44 +0900 Subject: [PATCH 21/33] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8B=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backdrop 스크롤 막기, ESC로 모달 끄기, 모달 애니메이션 적용 --- src/hooks/useKeydown.ts | 28 +++++++++ src/hooks/usePreventBodyScroll.ts | 13 +++++ src/providers/ModalProvider/index.tsx | 83 ++++++++++++++++++++++----- 3 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 src/hooks/useKeydown.ts create mode 100644 src/hooks/usePreventBodyScroll.ts diff --git a/src/hooks/useKeydown.ts b/src/hooks/useKeydown.ts new file mode 100644 index 00000000..20b75ad1 --- /dev/null +++ b/src/hooks/useKeydown.ts @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; + +const useKeydown = ( + char: string | string[], + callback: ([key, code]: string[]) => void, +) => { + useEffect(() => { + const handleKeydown = (event: KeyboardEvent) => { + const { key, code } = event; + if ( + (char instanceof Array && char.includes(key)) || + (char instanceof Array && char.includes(code)) || + key === char || + code === char + ) { + callback([key, code]); + } + }; + + window.addEventListener('keydown', handleKeydown); + + return () => { + window.removeEventListener('keydown', handleKeydown); + }; + }, [callback, char]); +}; + +export default useKeydown; diff --git a/src/hooks/usePreventBodyScroll.ts b/src/hooks/usePreventBodyScroll.ts new file mode 100644 index 00000000..f9f2137f --- /dev/null +++ b/src/hooks/usePreventBodyScroll.ts @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; + +const usePreventBodyScroll = () => { + useEffect(() => { + document.body.style.overflow = 'hidden'; + + return () => { + document.body.style.removeProperty('overflow'); + }; + }, []); +}; + +export default usePreventBodyScroll; diff --git a/src/providers/ModalProvider/index.tsx b/src/providers/ModalProvider/index.tsx index a025cca7..9164a388 100644 --- a/src/providers/ModalProvider/index.tsx +++ b/src/providers/ModalProvider/index.tsx @@ -5,12 +5,19 @@ import { createContext, useState, } from 'react'; -import styled, { css } from 'styled-components'; +import styled, { css, keyframes } from 'styled-components'; +import Keyframes from 'styled-components/dist/models/Keyframes'; + +import useKeydown from '@Hooks/useKeydown'; +import usePreventBodyScroll from '@Hooks/usePreventBodyScroll'; import { ThemeStyleSet } from '@Types/style'; +export type Animation = 'appear' | 'disAppear'; + type ModalContext = { isOpen: boolean; + animation: Animation; openModal: (model: ReactNode) => void; closeModal: () => void; } | null; @@ -19,14 +26,24 @@ export const ModalContext = createContext(null); const ModalProvider = ({ children }: PropsWithChildren) => { const [currentModal, setCurrentModal] = useState(null); + const [animation, setAnimation] = useState('appear'); const isOpen = !!currentModal; - const openModal = (modal: ReactNode) => setCurrentModal(modal); + const openModal = (modal: ReactNode) => { + setAnimation('appear'); + setCurrentModal(modal); + }; - const closeModal = () => setCurrentModal(null); + const closeModal = () => { + setAnimation('disAppear'); + setTimeout(() => { + setCurrentModal(null); + }, 200); + }; const value = { isOpen, + animation, openModal, closeModal, }; @@ -34,7 +51,11 @@ const ModalProvider = ({ children }: PropsWithChildren) => { return ( {children} - {isOpen && {currentModal}} + {isOpen && ( + + {currentModal} + + )} ); }; @@ -42,10 +63,19 @@ const ModalProvider = ({ children }: PropsWithChildren) => { export default ModalProvider; type ModalProps = { + animation: Animation; closeModal: () => void; }; -const Modal = ({ children, closeModal }: PropsWithChildren) => { +const Modal = ({ + animation, + children, + closeModal, +}: PropsWithChildren) => { + usePreventBodyScroll(); + + useKeydown('Escape', closeModal); + const onClickBackdrop = () => { closeModal(); }; @@ -55,13 +85,15 @@ const Modal = ({ children, closeModal }: PropsWithChildren) => { }; return ( - - {children} - + + + {children} + + ); }; -const MODAL_LAYOUT_THEME: ThemeStyleSet = { +const MODAL_BACKDROP_THEME: ThemeStyleSet = { light: css` background-color: rgba(0, 0, 0, 0.65); `, @@ -71,7 +103,7 @@ const MODAL_LAYOUT_THEME: ThemeStyleSet = { `, }; -const ModalLayout = styled.div` +const ModalBackdrop = styled.div` position: fixed; min-width: 100vw; min-height: 100vh; @@ -85,11 +117,35 @@ const ModalLayout = styled.div` z-index: 5; ${({ theme }) => css` - ${MODAL_LAYOUT_THEME[theme.name]} + ${MODAL_BACKDROP_THEME[theme.name]} `} `; -const ModalContainer = styled.div` +const modalAnimation: Record = { + appear: keyframes` + 0% { + transform: scale(0.9); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 1; + } +`, + + disAppear: keyframes` + 0% { + transform: scale(1); + opacity: 1; + } + 100% { + transform: scale(0.9); + opacity: 0; + } +`, +}; + +const ModalContainer = styled.div<{ animation: Animation }>` width: 480px; max-height: 500px; @@ -98,8 +154,9 @@ const ModalContainer = styled.div` padding: 20px; border-radius: 8px; - ${({ theme }) => css` + ${({ theme, animation }) => css` color: ${theme.text}; background-color: ${theme.mainBackground}; + animation: ${modalAnimation[animation]} 0.2s ease; `} `; From 94bf5f33c52e2c84fc3b27a6f08c645dc3756dae Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Sun, 25 Feb 2024 14:46:06 +0900 Subject: [PATCH 22/33] =?UTF-8?q?refactor:=20=ED=83=80=EC=96=B4=EB=AF=B8?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=20=EC=A1=B0=EA=B1=B4=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Tools/Timer/TimeSettingModal/index.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/pages/Tools/Timer/TimeSettingModal/index.tsx b/src/pages/Tools/Timer/TimeSettingModal/index.tsx index f2422c49..99efb484 100644 --- a/src/pages/Tools/Timer/TimeSettingModal/index.tsx +++ b/src/pages/Tools/Timer/TimeSettingModal/index.tsx @@ -33,17 +33,12 @@ function TimeSettingModal({ changeInitTime }: Props) { return; } - if (value === 60 && type === 'minute') { + if (value >= 60 && type === 'minute') { setMinute(60); setSecond(0); return; } - if (value > 60 && type === 'minute') { - setMinute(60); - return; - } - if (value > 59 && type === 'second') { setSecond(59); return; From 2303a39a21912c0125529a51c08903de82ea8f3e Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 28 Feb 2024 21:34:40 +0900 Subject: [PATCH 23/33] =?UTF-8?q?feat:=20Selector=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Icon/ArrowIcon/ArrowIcon.tsx | 42 ++++++ src/components/Select/Select.stories.tsx | 56 ++++++++ src/components/Select/index.tsx | 138 ++++++++++++++++++++ src/components/Select/style.ts | 102 +++++++++++++++ src/pages/Tools/.gitkeep | 0 5 files changed, 338 insertions(+) create mode 100644 src/components/Icon/ArrowIcon/ArrowIcon.tsx create mode 100644 src/components/Select/Select.stories.tsx create mode 100644 src/components/Select/index.tsx create mode 100644 src/components/Select/style.ts delete mode 100644 src/pages/Tools/.gitkeep diff --git a/src/components/Icon/ArrowIcon/ArrowIcon.tsx b/src/components/Icon/ArrowIcon/ArrowIcon.tsx new file mode 100644 index 00000000..168925f7 --- /dev/null +++ b/src/components/Icon/ArrowIcon/ArrowIcon.tsx @@ -0,0 +1,42 @@ +import { ComponentPropsWithoutRef } from 'react'; + +type Direction = 'down' | 'up' | 'left' | 'right'; + +type ArrowIconProps = { + color?: string; + direction: Direction; +}; + +const DIRECTION: Record = { + down: 'M11.6667 16.6667L20 25.0001L28.3333 16.6667', + up: 'M28.3333 23.3333L20 15L11.6667 23.3333', + left: 'M23.3333 11.6667L15 20.0001L23.3333 28.3334', + right: 'M16.6667 28.3334L25 20.0001L16.6667 11.6667', +}; + +const ArrowIcon = ({ + color = 'black', + direction, + ...rest +}: ArrowIconProps & ComponentPropsWithoutRef<'svg'>) => { + return ( + + + + ); +}; + +export default ArrowIcon; diff --git a/src/components/Select/Select.stories.tsx b/src/components/Select/Select.stories.tsx new file mode 100644 index 00000000..280587af --- /dev/null +++ b/src/components/Select/Select.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Select from '.'; + +type Story = StoryObj; + +/** + * `Select` 컴포넌트는 사용자가 텍스트를 입력하고 편집할 수 있는 컴포넌트입니다. + */ +const meta: Meta = { + title: 'INPUTS/Select', + component: Select, +}; + +export default meta; + +/** + * `DefaultSelect`는 가징 기본적인 `Select` 스토리입니다. + */ +export const DefaultSelect: Story = { + args: { + label: 'DEFAULT', + options: ['Option1', 'Option2', 'Option3', 'Option4', 'Option5'], + onChangeOption: (selected) => { + console.log(selected); + }, + }, +}; + +/** + * `HasDefaultOptionSelect`는 defaultOption이 존재하는 `Select` 스토리입니다. + */ +export const HasDefaultOptionSelect: Story = { + args: { + label: 'Has Default Option', + options: ['Option1', 'Option2', 'Option3', 'Option4', 'Option5'], + defaultOption: 'Option1', + onChangeOption: (selected) => { + console.log(selected); + }, + }, +}; + +/** + * `HasPlaceholderSelect`는 placeholder이 존재하는 `Select` 스토리입니다. + */ +export const HasPlaceholderSelect: Story = { + args: { + label: 'Has Placeholder', + options: ['Option1', 'Option2', 'Option3', 'Option4', 'Option5'], + placeholder: '커스튬 가능한 문구입니다.', + onChangeOption: (selected) => { + console.log(selected); + }, + }, +}; diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx new file mode 100644 index 00000000..812b7e24 --- /dev/null +++ b/src/components/Select/index.tsx @@ -0,0 +1,138 @@ +import { + MouseEventHandler, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; + +import useOutsideClick from '@Hooks/useOutsideClick'; + +import ArrowIcon from '@Components/Icon/ArrowIcon/ArrowIcon'; + +import * as S from './style'; + +type SelectProps = { + label?: string; + placeholder?: string; + defaultOption?: Options[number]; + options: Options; + onChangeOption?: (selected: Options[number]) => void; +}; + +const Select = ({ + label, + placeholder = '옵션을 선택해 주세요.', + defaultOption = '', + options, + onChangeOption, +}: SelectProps) => { + const optionsRef = useRef(null); + + const [selectedOption, setSelectedOption] = useState(defaultOption); + const [isOpenOptions, setIsOptionOptions] = useState(false); + + const ref = useOutsideClick(() => setIsOptionOptions(false)); + + const [offsetTop, setOffsetTop] = useState(ref?.current?.offsetTop || 0); + const [clientWidth, setClientWidth] = useState( + ref?.current?.clientWidth || 0, + ); + + const handleClickArrowIcon: MouseEventHandler = (event) => { + event.preventDefault(); + + setIsOptionOptions((prev) => !prev); + }; + + const handleChangeOption = (value: Options[number]) => { + setIsOptionOptions(false); + setSelectedOption(value); + + onChangeOption?.(value); + }; + + const setOptionsPositionWidth = useCallback(() => { + const offsetTop = ref?.current?.offsetTop; + const clientWidth = ref?.current?.clientWidth; + + if (!offsetTop || !clientWidth) return; + + setOffsetTop(offsetTop); + setClientWidth(clientWidth); + }, [ref]); + + useEffect(() => { + window.addEventListener('resize', setOptionsPositionWidth); + + return () => window.removeEventListener('resize', setOptionsPositionWidth); + }, [setOptionsPositionWidth]); + + useLayoutEffect(() => { + if (!isOpenOptions) return; + + setOptionsPositionWidth(); + }, [isOpenOptions, setOptionsPositionWidth]); + + // 스크롤 계산 + useEffect(() => { + if (!isOpenOptions || !optionsRef?.current) return; + + const { scrollHeight, offsetHeight, children } = optionsRef.current; + if (scrollHeight < offsetHeight) return; + + const options = Object.values(children) as HTMLLIElement[]; + + let top = 10; + for (let i = 0; i < options.length; i++) { + const { offsetHeight, innerText } = options[i]; + top += offsetHeight + 8; + + if (innerText === selectedOption) break; + } + + optionsRef.current.scrollTo({ + top: top - 160, + }); + }, [isOpenOptions, selectedOption]); + + return ( + + + {isOpenOptions && ( + + {options.map((option) => ( + handleChangeOption(option)} + $isSelected={selectedOption === option} + > + {option} + + ))} + + )} + + ); +}; + +export default Select; diff --git a/src/components/Select/style.ts b/src/components/Select/style.ts new file mode 100644 index 00000000..2470f704 --- /dev/null +++ b/src/components/Select/style.ts @@ -0,0 +1,102 @@ +import { css, styled } from 'styled-components'; + +import theme from '@Styles/theme'; + +export const Select = styled.div` + max-width: 100%; + + &label { + width: 100%; + } +`; + +export const LabelText = styled.p` + margin-bottom: 4px; + + font-size: 1.5rem; + color: ${theme.color.gray[500]}; +`; + +type InputLayoutProps = { + $isFocus: boolean; +}; + +export const InputLayout = styled.div` + display: flex; + align-items: center; + + padding: 10px 16px; + + border-radius: 12px; + + background-color: ${theme.color.gray[100]}; + + cursor: pointer; +`; + +export const Input = styled.input` + outline: none; + border: none; + + width: 100%; + + color: ${theme.color.gray[900]}; + background-color: ${theme.color.gray[100]}; + + font-size: 1.5rem; + + cursor: pointer; +`; + +type OptionsProps = { + top: string; + width: string; +}; + +export const Options = styled.ul` + position: absolute; + top: ${({ top }) => top}; + + display: flex; + flex-direction: column; + gap: 8px; + + width: ${({ width }) => width}; + max-height: 320px; + overflow-y: scroll; + + padding: 10px; + + border: 1px solid ${theme.color.gray[300]}; + border-radius: 8px; + + background-color: ${theme.color.white}; + + z-index: 15; +`; + +type OptionProps = { + $isSelected: boolean; +}; + +export const Option = styled.li` + padding: 10px 16px; + + border-radius: 8px; + + font-size: 1.5rem; + + cursor: pointer; + + ${({ $isSelected }) => + $isSelected + ? css` + background-color: ${theme.color.primary[500]}; + color: white; + ` + : css` + &:hover { + background-color: ${theme.color.gray[100]}; + } + `} +`; diff --git a/src/pages/Tools/.gitkeep b/src/pages/Tools/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 8709669102cbce24fcae1cacde3344bc60e64c45 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 28 Feb 2024 21:36:41 +0900 Subject: [PATCH 24/33] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20Selector=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F?= =?UTF-8?q?=20memo=EC=99=80=20=ED=95=A8=EA=BB=98=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.json | 2 +- src/constant/tools/timer.ts | 2 + src/hooks/tools/timer/useTimer.ts | 3 +- src/pages/App/index.tsx | 3 +- .../Tools/Timer/TimeSettingModal/index.tsx | 165 +++++++----------- .../Tools/Timer/TimeSettingModal/style.ts | 87 ++------- .../Tools/Timer/TimerMemoModal/index.tsx | 52 ------ src/pages/Tools/Timer/TimerMemoModal/style.ts | 29 --- src/pages/Tools/Timer/index.tsx | 14 +- src/pages/Tools/Timer/style.ts | 2 - src/providers/ModalProvider/index.tsx | 59 ++----- 11 files changed, 102 insertions(+), 316 deletions(-) create mode 100644 src/constant/tools/timer.ts delete mode 100644 src/pages/Tools/Timer/TimerMemoModal/index.tsx delete mode 100644 src/pages/Tools/Timer/TimerMemoModal/style.ts diff --git a/.eslintrc.json b/.eslintrc.json index 28eee73a..c0489b3b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -60,7 +60,7 @@ "position": "after" }, { - "pattern": "@Components/*", + "pattern": "@Components/**/*", "group": "internal", "position": "after" }, diff --git a/src/constant/tools/timer.ts b/src/constant/tools/timer.ts new file mode 100644 index 00000000..9cff13b6 --- /dev/null +++ b/src/constant/tools/timer.ts @@ -0,0 +1,2 @@ +export const EASY_SETTING_TIMER = [30, 60, 90, 180, 300, 600, 1200, 1800]; +export const DEFAULT_TIME = 300; diff --git a/src/hooks/tools/timer/useTimer.ts b/src/hooks/tools/timer/useTimer.ts index 14b5c4ee..16cbe926 100644 --- a/src/hooks/tools/timer/useTimer.ts +++ b/src/hooks/tools/timer/useTimer.ts @@ -1,10 +1,11 @@ +import { DEFAULT_TIME } from '@Constant/tools/timer'; import { useCallback, useEffect, useState } from 'react'; import localStorageHelper from '@Utils/localStorageHelper'; const useTimer = () => { const recentTimes = localStorageHelper.get('timer') ?? []; - const recentTime = recentTimes[0] ?? 300; + const recentTime = recentTimes[0] ?? DEFAULT_TIME; const [initTime, setInitTime] = useState(recentTime); const [time, setTime] = useState(recentTime); diff --git a/src/pages/App/index.tsx b/src/pages/App/index.tsx index ed53d991..1d91ce34 100644 --- a/src/pages/App/index.tsx +++ b/src/pages/App/index.tsx @@ -1,4 +1,3 @@ -import AuthErrorBoundary from '@Components/ErrorBoundary/AuthErrorBoundary'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useState } from 'react'; import { PiSunBold, PiMoonStarsBold } from 'react-icons/pi'; @@ -9,6 +8,8 @@ import { useMediaInit } from '@Hooks/useMedia'; import route from '@Utils/route'; +import AuthErrorBoundary from '@Components/ErrorBoundary/AuthErrorBoundary'; + import ModalProvider from '@Providers/ModalProvider'; import QueryProvider from '@Providers/QueryProvider'; import UserProvider from '@Providers/UserProvider'; diff --git a/src/pages/Tools/Timer/TimeSettingModal/index.tsx b/src/pages/Tools/Timer/TimeSettingModal/index.tsx index 99efb484..d798a73a 100644 --- a/src/pages/Tools/Timer/TimeSettingModal/index.tsx +++ b/src/pages/Tools/Timer/TimeSettingModal/index.tsx @@ -1,4 +1,5 @@ -import { ChangeEventHandler, useState } from 'react'; +import { DEFAULT_TIME } from '@Constant/tools/timer'; +import { useState } from 'react'; import useModal from '@Hooks/useModal'; @@ -6,135 +7,101 @@ import localStorageHelper from '@Utils/localStorageHelper'; import Button from '@Components/Button'; import Input from '@Components/Input'; +import Select from '@Components/Select'; import * as S from './style'; type Props = { + memo: string; changeInitTime: (minute: number, second: number) => void; + changeMemo: (memo: string) => void; }; -function TimeSettingModal({ changeInitTime }: Props) { +function TimeSettingModal({ memo, changeInitTime, changeMemo }: Props) { const { closeModal } = useModal(); const recentTimes = localStorageHelper.get('timer') ?? []; - const recentTime = recentTimes[0] ?? 300; + const recentTime = recentTimes[0] ?? DEFAULT_TIME; const [minute, setMinute] = useState(Math.floor(recentTime / 60)); const [second, setSecond] = useState(recentTime - minute * 60); - const handleChangeTime: ( - type: 'minute' | 'second', - ) => ChangeEventHandler = (type) => (event) => { - const value = Number(event.target.value); + const [newMemo, setNewMemo] = useState(memo); - if (isNaN(value)) return; - if (minute === 60 && type === 'second') { - setSecond(0); - return; - } - - if (value >= 60 && type === 'minute') { - setMinute(60); - setSecond(0); - return; - } - - if (value > 59 && type === 'second') { - setSecond(59); - return; - } + const handleClickTime = (selected: string) => { + const [minute, second] = selected + .replace(/[^0-9\s]/g, '') + .split(' ') + .map(Number); - if (type === 'minute') setMinute(value); - else setSecond(value); - }; - - const handleClickTimeButton = (minute: number, second: number = 0) => { setMinute(minute); setSecond(second); }; const setRecentTimer = () => { - localStorage.setItem( - 'timer', - JSON.stringify([ - minute * 60 + second, - ...(recentTimes.length === 6 ? recentTimes.slice(0, 5) : recentTimes), - ]), - ); + const recentTIme = minute * 60 + second; + + if (!recentTimes.includes(recentTIme)) + localStorage.setItem( + 'timer', + JSON.stringify([ + minute * 60 + second, + ...(recentTimes.length === 12 + ? recentTimes.slice(0, 11) + : recentTimes), + ]), + ); }; const handleClickConfirm = () => { + if (minute === 0 && second === 0) { + alert('유효한 시간을 설정해주세요.'); + + return; + } + setRecentTimer(); + changeMemo(newMemo); changeInitTime(minute, second); closeModal(); }; + const convertStringTime = (time: number) => + time < 10 ? `0${time}` : String(time); + return ( - 타이머 시간 설정하기 - - 타이머 시간을 설정할 수 있어요. -
- 직접 설정할 수 있는 최대 시간은 60분이에요. -
- - 직접 설정하기 - - - - - - - - - - - - - 간편 설정하기 - - {[3, 5, 10, 20, 30].map((minute) => ( - - ))} - - - - 최근 타이머 시간 - - {recentTimes.length !== 0 ? ( - recentTimes.map((time, index) => ( - - handleClickTimeButton(Math.floor(time / 60), time % 60) - } - > - {Math.floor(time / 60)}분 {time % 60}초 - - )) - ) : ( - - 최근에 사용한 타이머가 없어요. - - )} - - + 타이머 설정 + + 시간 직접 설정하기 + + + convertStringTime(index), + )} + defaultOption={String(second)} + onChangeOption={(selected) => setSecond(Number(selected))} + /> + + + + 메모 설정하기 + setNewMemo(event.target.value)} + /> + - - -
- ); -}; - -export default TimerMemoModal; diff --git a/src/pages/Tools/Timer/TimerMemoModal/style.ts b/src/pages/Tools/Timer/TimerMemoModal/style.ts deleted file mode 100644 index b673381c..00000000 --- a/src/pages/Tools/Timer/TimerMemoModal/style.ts +++ /dev/null @@ -1,29 +0,0 @@ -import styled from 'styled-components'; - -import theme from '@Styles/theme'; - -export const Layout = styled.div` - display: grid; - gap: 20px; -`; - -export const ModalTitle = styled.h2` - justify-self: center; - - font-size: 1.8rem; - font-weight: 600; - color: ${theme.color.primary[500]}; -`; - -export const ModalDescription = styled.p` - color: ${(props) => props.theme.grayText}; - line-height: 140%; -`; - -export const BottomButtonContainer = styled.div` - display: flex; - justify-content: flex-end; - gap: 20px; - - margin-top: 10px; -`; diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index e5880fcc..f2835638 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -8,7 +8,6 @@ import useModal from '@Hooks/useModal'; import format from '@Utils/format'; import TimeSettingModal from './TimeSettingModal'; -import TimerMemoModal from './TimerMemoModal'; import * as S from './style'; function Timer() { @@ -34,14 +33,17 @@ function Timer() { const changeMemo = (memo: string) => setMemo(memo); const handleClickTime = () => - openModal(); - - const handleClickMessage = () => - openModal(); + openModal( + , + ); return ( - {memo} + {memo ? {memo} :
} {displayTime} diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index b3c7b140..51664e00 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -37,8 +37,6 @@ export const TimerMemo = styled.div` text-align: center; - cursor: pointer; - ${({ theme }) => css` ${TIMER_MEMO_THEME[theme.name]}; `} diff --git a/src/providers/ModalProvider/index.tsx b/src/providers/ModalProvider/index.tsx index 9164a388..36a6f312 100644 --- a/src/providers/ModalProvider/index.tsx +++ b/src/providers/ModalProvider/index.tsx @@ -6,18 +6,14 @@ import { useState, } from 'react'; import styled, { css, keyframes } from 'styled-components'; -import Keyframes from 'styled-components/dist/models/Keyframes'; import useKeydown from '@Hooks/useKeydown'; import usePreventBodyScroll from '@Hooks/usePreventBodyScroll'; import { ThemeStyleSet } from '@Types/style'; -export type Animation = 'appear' | 'disAppear'; - type ModalContext = { isOpen: boolean; - animation: Animation; openModal: (model: ReactNode) => void; closeModal: () => void; } | null; @@ -26,24 +22,14 @@ export const ModalContext = createContext(null); const ModalProvider = ({ children }: PropsWithChildren) => { const [currentModal, setCurrentModal] = useState(null); - const [animation, setAnimation] = useState('appear'); const isOpen = !!currentModal; - const openModal = (modal: ReactNode) => { - setAnimation('appear'); - setCurrentModal(modal); - }; + const openModal = (modal: ReactNode) => setCurrentModal(modal); - const closeModal = () => { - setAnimation('disAppear'); - setTimeout(() => { - setCurrentModal(null); - }, 200); - }; + const closeModal = () => setCurrentModal(null); const value = { isOpen, - animation, openModal, closeModal, }; @@ -51,11 +37,7 @@ const ModalProvider = ({ children }: PropsWithChildren) => { return ( {children} - {isOpen && ( - - {currentModal} - - )} + {isOpen && {currentModal}} ); }; @@ -63,15 +45,10 @@ const ModalProvider = ({ children }: PropsWithChildren) => { export default ModalProvider; type ModalProps = { - animation: Animation; closeModal: () => void; }; -const Modal = ({ - animation, - children, - closeModal, -}: PropsWithChildren) => { +const Modal = ({ children, closeModal }: PropsWithChildren) => { usePreventBodyScroll(); useKeydown('Escape', closeModal); @@ -86,9 +63,7 @@ const Modal = ({ return ( - - {children} - + {children} ); }; @@ -121,8 +96,7 @@ const ModalBackdrop = styled.div` `} `; -const modalAnimation: Record = { - appear: keyframes` +const modalAnimation = keyframes` 0% { transform: scale(0.9); opacity: 0; @@ -131,21 +105,9 @@ const modalAnimation: Record = { transform: scale(1); opacity: 1; } -`, - - disAppear: keyframes` - 0% { - transform: scale(1); - opacity: 1; - } - 100% { - transform: scale(0.9); - opacity: 0; - } -`, -}; +`; -const ModalContainer = styled.div<{ animation: Animation }>` +const ModalContainer = styled.div` width: 480px; max-height: 500px; @@ -154,9 +116,10 @@ const ModalContainer = styled.div<{ animation: Animation }>` padding: 20px; border-radius: 8px; - ${({ theme, animation }) => css` + animation: ${modalAnimation} 0.2s ease; + + ${({ theme }) => css` color: ${theme.text}; background-color: ${theme.mainBackground}; - animation: ${modalAnimation[animation]} 0.2s ease; `} `; From db47bdc8fa6b2c12bd4538b617ffcf234c354f05 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 28 Feb 2024 22:33:43 +0900 Subject: [PATCH 25/33] =?UTF-8?q?feat:=20=EC=B5=9C=EA=B7=BC=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EB=A8=B8=20=EC=8B=9C=EA=B0=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 및 메모 로컬 스토리지에 저장 --- src/components/Select/index.tsx | 4 + src/hooks/tools/timer/useTimer.ts | 9 +++ .../PagiNationButton/index.tsx | 2 +- .../PagiNationButton/style.ts | 6 +- .../index.tsx | 73 ++++++++++--------- .../style.ts | 8 +- src/pages/Tools/Timer/index.tsx | 20 +++-- src/pages/Tools/Timer/style.ts | 22 +----- 8 files changed, 76 insertions(+), 68 deletions(-) rename src/pages/Tools/Timer/{TimeSettingModal => SettingModal}/index.tsx (61%) rename src/pages/Tools/Timer/{TimeSettingModal => SettingModal}/style.ts (86%) diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index 812b7e24..01682d6f 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -75,6 +75,10 @@ const Select = ({ setOptionsPositionWidth(); }, [isOpenOptions, setOptionsPositionWidth]); + useEffect(() => { + if (defaultOption) setSelectedOption(defaultOption); + }, [defaultOption]); + // 스크롤 계산 useEffect(() => { if (!isOpenOptions || !optionsRef?.current) return; diff --git a/src/hooks/tools/timer/useTimer.ts b/src/hooks/tools/timer/useTimer.ts index 16cbe926..63c08932 100644 --- a/src/hooks/tools/timer/useTimer.ts +++ b/src/hooks/tools/timer/useTimer.ts @@ -18,6 +18,15 @@ const useTimer = () => { const changeInitTime = (minute: number, second: number) => { setInitTime(minute * 60 + second); setTime(minute * 60 + second); + + if (!recentTimes.includes(recentTime)) + localStorage.setItem( + 'timer', + JSON.stringify([ + minute * 60 + second, + ...(recentTimes.length === 6 ? recentTimes.slice(0, 5) : recentTimes), + ]), + ); }; const resetTimer = () => { diff --git a/src/pages/ClassManagement/LunchMenu/RegisterSchoolModal/PagiNationButton/index.tsx b/src/pages/ClassManagement/LunchMenu/RegisterSchoolModal/PagiNationButton/index.tsx index d0874db2..e943a0c9 100644 --- a/src/pages/ClassManagement/LunchMenu/RegisterSchoolModal/PagiNationButton/index.tsx +++ b/src/pages/ClassManagement/LunchMenu/RegisterSchoolModal/PagiNationButton/index.tsx @@ -27,7 +27,7 @@ function PagiNationButton({ }; return ( - + handleClickPagiNationButton('prev')} /> handleClickPagiNationButton('next')} /> diff --git a/src/pages/ClassManagement/LunchMenu/RegisterSchoolModal/PagiNationButton/style.ts b/src/pages/ClassManagement/LunchMenu/RegisterSchoolModal/PagiNationButton/style.ts index f6af4153..e937572c 100644 --- a/src/pages/ClassManagement/LunchMenu/RegisterSchoolModal/PagiNationButton/style.ts +++ b/src/pages/ClassManagement/LunchMenu/RegisterSchoolModal/PagiNationButton/style.ts @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { flexCustom } from '@Styles/common'; type LayoutProps = { - hasPage: boolean; + $hasPage: boolean; }; export const Layout = styled.div` @@ -11,9 +11,9 @@ export const Layout = styled.div` gap: 10px; margin-top: 10px; - color: ${(props) => (props.hasPage ? props.theme.grayText : 'transparent')}; + color: ${(props) => (props.$hasPage ? props.theme.grayText : 'transparent')}; svg { - cursor: ${(props) => props.hasPage && 'pointer'}; + cursor: ${(props) => props.$hasPage && 'pointer'}; } `; diff --git a/src/pages/Tools/Timer/TimeSettingModal/index.tsx b/src/pages/Tools/Timer/SettingModal/index.tsx similarity index 61% rename from src/pages/Tools/Timer/TimeSettingModal/index.tsx rename to src/pages/Tools/Timer/SettingModal/index.tsx index d798a73a..06531354 100644 --- a/src/pages/Tools/Timer/TimeSettingModal/index.tsx +++ b/src/pages/Tools/Timer/SettingModal/index.tsx @@ -1,5 +1,6 @@ import { DEFAULT_TIME } from '@Constant/tools/timer'; import { useState } from 'react'; +import { css } from 'styled-components'; import useModal from '@Hooks/useModal'; @@ -17,58 +18,39 @@ type Props = { changeMemo: (memo: string) => void; }; -function TimeSettingModal({ memo, changeInitTime, changeMemo }: Props) { +function SettingModal({ memo, changeInitTime, changeMemo }: Props) { + const convertStringTime = (time: number) => + time < 10 ? `0${time}` : String(time); + const { closeModal } = useModal(); const recentTimes = localStorageHelper.get('timer') ?? []; const recentTime = recentTimes[0] ?? DEFAULT_TIME; - const [minute, setMinute] = useState(Math.floor(recentTime / 60)); - const [second, setSecond] = useState(recentTime - minute * 60); + const [minute, setMinute] = useState( + convertStringTime(Math.floor(recentTime / 60)), + ); + const [second, setSecond] = useState(convertStringTime(recentTime % 60)); const [newMemo, setNewMemo] = useState(memo); - const handleClickTime = (selected: string) => { - const [minute, second] = selected - .replace(/[^0-9\s]/g, '') - .split(' ') - .map(Number); - - setMinute(minute); - setSecond(second); - }; - - const setRecentTimer = () => { - const recentTIme = minute * 60 + second; - - if (!recentTimes.includes(recentTIme)) - localStorage.setItem( - 'timer', - JSON.stringify([ - minute * 60 + second, - ...(recentTimes.length === 12 - ? recentTimes.slice(0, 11) - : recentTimes), - ]), - ); + const handleRecentClickTime = (time: number) => { + setMinute(convertStringTime(Math.floor(time / 60))); + setSecond(convertStringTime(time % 60)); }; const handleClickConfirm = () => { - if (minute === 0 && second === 0) { + if (minute === '00' && second === '00') { alert('유효한 시간을 설정해주세요.'); return; } - setRecentTimer(); changeMemo(newMemo); - changeInitTime(minute, second); + changeInitTime(Number(minute), Number(second)); closeModal(); }; - const convertStringTime = (time: number) => - time < 10 ? `0${time}` : String(time); - return ( 타이머 설정 @@ -81,7 +63,7 @@ function TimeSettingModal({ memo, changeInitTime, changeMemo }: Props) { convertStringTime(index), )} defaultOption={String(minute)} - onChangeOption={(selected) => setMinute(Number(selected))} + onChangeOption={(selected) => setMinute(selected)} /> ('memo') ?? ''; + + const [memo, setMemo] = useState(recentMemo); + + const changeMemo = (memo: string) => { + setMemo(memo); + localStorage.setItem('memo', JSON.stringify(memo)); + }; + const [ref, width] = useLength({ standard: 'width', }); @@ -25,19 +35,15 @@ function Timer() { changeInitTime, } = useTimer(); - const [memo, setMemo] = useState(''); - const displayTime = format.time(time); const timerFontSize = `${width / 3.8}px`; - const changeMemo = (memo: string) => setMemo(memo); - const handleClickTime = () => openModal( - , ); diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index 51664e00..04b16fc9 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -42,20 +42,6 @@ export const TimerMemo = styled.div` `} `; -const TIMER_THEME: Record = { - light: css` - &:hover { - background-color: ${theme.color.gray[200]}; - } - `, - - dark: css` - &:hover { - background-color: ${theme.color.gray[700]}; - } - `, -}; - export const Time = styled.span<{ $fontSize: string }>` position: relative; @@ -77,14 +63,8 @@ export const Time = styled.span<{ $fontSize: string }>` transition: background-color 0.2s ease; border-radius: 20px; - &:hover { - cursor: pointer; - } - - ${({ theme, $fontSize }) => css` + ${({ $fontSize }) => css` font-size: ${$fontSize}; - - ${TIMER_THEME[theme.name]}; `} `; From e9435cd8d1b0826e88b8c1d538bee158573b504c Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 28 Feb 2024 22:35:23 +0900 Subject: [PATCH 26/33] =?UTF-8?q?fix:=20import=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/SideSection/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/Home/SideSection/index.tsx b/src/pages/Home/SideSection/index.tsx index 0fe478a2..c64a066c 100644 --- a/src/pages/Home/SideSection/index.tsx +++ b/src/pages/Home/SideSection/index.tsx @@ -1,8 +1,9 @@ -import SkeletonSummaryList from '@Components/SummaryList/SkeletonSummaryList'; import { Suspense } from 'react'; import useUserInfo from '@Hooks/useUserInfo'; +import SkeletonSummaryList from '@Components/SummaryList/SkeletonSummaryList'; + import * as S from './style'; import { SideSectionProps } from './type'; import HomeMemo from '../HomeMemo'; From 9f0d182e42e1754ce3717d74f2737c7319e524ee Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Thu, 29 Feb 2024 14:46:26 +0900 Subject: [PATCH 27/33] =?UTF-8?q?feat:=20setting=20icon=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Tools/Timer/index.tsx | 20 +++++++++++++------- src/pages/Tools/Timer/style.ts | 24 +++++++++++++++++++++++- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 06ca187a..411cbc83 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { FaPause, FaPlay, FaStop } from 'react-icons/fa6'; +import { IoSettings } from 'react-icons/io5'; import useTimer from '@Hooks/tools/timer/useTimer'; import useLength from '@Hooks/useLength'; @@ -36,9 +37,10 @@ function Timer() { } = useTimer(); const displayTime = format.time(time); - const timerFontSize = `${width / 3.8}px`; + const timerFontSize = `${width / 4}px`; - const handleClickTime = () => + const setTimer = () => { + resetTimer(); openModal( , ); + }; return ( {memo ? {memo} :
} - - {displayTime} - + {displayTime} - + {isProceeding || progress === 100 ? ( @@ -69,7 +70,12 @@ function Timer() { - + + + + + +
); } diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index 04b16fc9..424d8116 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -6,6 +6,7 @@ import theme from '@Styles/theme'; import { ThemeName } from '@Types/style'; export const Layout = styled.div` + position: relative; display: grid; grid-template-rows: auto 1fr auto auto; gap: 20px; @@ -82,7 +83,7 @@ export const ProgressBar = styled.div<{ progress: number }>` background-color: ${theme.color.primary[500]}; `; -export const TimerButtonWrapper = styled.div` +export const TimerButtons = styled.div` display: flex; justify-content: center; gap: 60px; @@ -104,3 +105,24 @@ export const TimerButton = styled.div` cursor: pointer; `; + +export const TimerSideButtons = styled.div` + position: absolute; + top: 0; + right: 0; +`; + +export const SideButton = styled.div` + ${flexCustom('row', 'center', 'center')} + + width: 40px; + height: 40px; + border-radius: 20px; + + background-color: ${theme.color.primary[500]}; + color: ${theme.color.white}; + + font-size: 2rem; + + cursor: pointer; +`; From ee1fb7295162467daeac42c83692bb3a85d3e12e Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 6 Mar 2024 20:32:15 +0900 Subject: [PATCH 28/33] =?UTF-8?q?feat:=20EasySettingTimes=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constant/tools/timer.ts | 31 ++++++++++++++++++++++++++++++- src/pages/Tools/Timer/index.tsx | 21 ++++++++++++++++++++- src/pages/Tools/Timer/style.ts | 14 +++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/constant/tools/timer.ts b/src/constant/tools/timer.ts index 9cff13b6..77df235d 100644 --- a/src/constant/tools/timer.ts +++ b/src/constant/tools/timer.ts @@ -1,2 +1,31 @@ -export const EASY_SETTING_TIMER = [30, 60, 90, 180, 300, 600, 1200, 1800]; +export const EASY_SETTING_TIMER = [ + { + minute: 0, + second: 30, + }, + { + minute: 1, + second: 0, + }, + { + minute: 3, + second: 0, + }, + { + minute: 5, + second: 0, + }, + { + minute: 10, + second: 0, + }, + { + minute: 20, + second: 0, + }, + { + minute: 30, + second: 0, + }, +]; export const DEFAULT_TIME = 300; diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 411cbc83..7b4460df 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -1,6 +1,8 @@ +import { EASY_SETTING_TIMER } from '@Constant/tools/timer'; import { useState } from 'react'; import { FaPause, FaPlay, FaStop } from 'react-icons/fa6'; import { IoSettings } from 'react-icons/io5'; +import { css } from 'styled-components'; import useTimer from '@Hooks/tools/timer/useTimer'; import useLength from '@Hooks/useLength'; @@ -9,6 +11,8 @@ import useModal from '@Hooks/useModal'; import format from '@Utils/format'; import localStorageHelper from '@Utils/localStorageHelper'; +import Button from '@Components/Button'; + import SettingModal from './SettingModal'; import * as S from './style'; @@ -73,8 +77,23 @@ function Timer() { - + + + {EASY_SETTING_TIMER.map(({ minute, second }, index) => ( + + ))} +
); diff --git a/src/pages/Tools/Timer/style.ts b/src/pages/Tools/Timer/style.ts index 424d8116..78ed52cd 100644 --- a/src/pages/Tools/Timer/style.ts +++ b/src/pages/Tools/Timer/style.ts @@ -110,6 +110,10 @@ export const TimerSideButtons = styled.div` position: absolute; top: 0; right: 0; + + display: flex; + flex-direction: column; + align-items: flex-end; `; export const SideButton = styled.div` @@ -120,9 +124,17 @@ export const SideButton = styled.div` border-radius: 20px; background-color: ${theme.color.primary[500]}; - color: ${theme.color.white}; + color: ${({ theme }) => theme.pageBackground}; font-size: 2rem; cursor: pointer; `; + +export const EasySettingTimes = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + + margin-top: 10px; +`; From 11c6abf3675b8ec845f2e22fde717d713a33082d Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 6 Mar 2024 20:49:21 +0900 Subject: [PATCH 29/33] =?UTF-8?q?feat:=20Selector=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=8B=A4=ED=81=AC=EB=AA=A8=EB=93=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Select/style.ts | 77 ++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/src/components/Select/style.ts b/src/components/Select/style.ts index 2470f704..ffe3680e 100644 --- a/src/components/Select/style.ts +++ b/src/components/Select/style.ts @@ -2,6 +2,8 @@ import { css, styled } from 'styled-components'; import theme from '@Styles/theme'; +import { ThemeStyleSet } from '@Types/style'; + export const Select = styled.div` max-width: 100%; @@ -10,17 +12,38 @@ export const Select = styled.div` } `; +const LABEL_TEXT_THEME: ThemeStyleSet = { + light: css` + color: ${theme.color.gray[500]}; + `, + + dark: css` + color: ${theme.color.gray[300]}; + `, +}; + export const LabelText = styled.p` margin-bottom: 4px; font-size: 1.5rem; - color: ${theme.color.gray[500]}; + + ${({ theme }) => LABEL_TEXT_THEME[theme.name]} `; type InputLayoutProps = { $isFocus: boolean; }; +const INPUT_LAYOUT_THEME: ThemeStyleSet = { + light: css` + background-color: ${theme.color.gray[100]}; + `, + + dark: css` + background-color: ${theme.color.gray[700]}; + `, +}; + export const InputLayout = styled.div` display: flex; align-items: center; @@ -29,11 +52,23 @@ export const InputLayout = styled.div` border-radius: 12px; - background-color: ${theme.color.gray[100]}; - cursor: pointer; + + ${({ theme }) => INPUT_LAYOUT_THEME[theme.name]} `; +const INPUT_THEME: ThemeStyleSet = { + light: css` + color: ${theme.color.gray[900]}; + background-color: ${theme.color.gray[100]}; + `, + + dark: css` + color: ${theme.color.gray[100]}; + background-color: ${theme.color.gray[700]}; + `, +}; + export const Input = styled.input` outline: none; border: none; @@ -46,6 +81,8 @@ export const Input = styled.input` font-size: 1.5rem; cursor: pointer; + + ${({ theme }) => INPUT_THEME[theme.name]} `; type OptionsProps = { @@ -53,6 +90,22 @@ type OptionsProps = { width: string; }; +const OPTIONS_THEME: ThemeStyleSet = { + light: css` + border: 1px solid ${theme.color.gray[300]}; + background-color: ${theme.color.white}; + + color: ${theme.color.black}; + `, + + dark: css` + border: 1px solid ${theme.color.gray[700]}; + background-color: ${theme.color.gray[900]}; + + color: ${theme.color.white}; + `, +}; + export const Options = styled.ul` position: absolute; top: ${({ top }) => top}; @@ -70,15 +123,25 @@ export const Options = styled.ul` border: 1px solid ${theme.color.gray[300]}; border-radius: 8px; - background-color: ${theme.color.white}; - z-index: 15; + + ${({ theme }) => OPTIONS_THEME[theme.name]} `; type OptionProps = { $isSelected: boolean; }; +const OPTION_THEME: ThemeStyleSet = { + light: css` + background-color: ${theme.color.gray[100]}; + `, + + dark: css` + background-color: ${theme.color.gray[800]}; + `, +}; + export const Option = styled.li` padding: 10px 16px; @@ -88,7 +151,7 @@ export const Option = styled.li` cursor: pointer; - ${({ $isSelected }) => + ${({ $isSelected, theme }) => $isSelected ? css` background-color: ${theme.color.primary[500]}; @@ -96,7 +159,7 @@ export const Option = styled.li` ` : css` &:hover { - background-color: ${theme.color.gray[100]}; + ${OPTION_THEME[theme.name]} } `} `; From b70f36617f931c2583f7d2d8ada249cf183e90d8 Mon Sep 17 00:00:00 2001 From: nlom0218 Date: Wed, 6 Mar 2024 21:29:58 +0900 Subject: [PATCH 30/33] =?UTF-8?q?refactor:=20Easy=20Setting=20Button=20con?= =?UTF-8?q?cept=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Tools/Timer/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Tools/Timer/index.tsx b/src/pages/Tools/Timer/index.tsx index 7b4460df..19748fd4 100644 --- a/src/pages/Tools/Timer/index.tsx +++ b/src/pages/Tools/Timer/index.tsx @@ -82,6 +82,7 @@ function Timer() { {EASY_SETTING_TIMER.map(({ minute, second }, index) => ( ))}