From 8f977cacf9f7fdcc9a512041313c1e543819c96d Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sat, 29 Nov 2025 21:16:19 +0900 Subject: [PATCH 01/22] =?UTF-8?q?design:=20=EB=B3=BC=EB=A5=A8=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/icons/Icon.stories.tsx | 13 +++++++++++++ src/components/icons/Volume.tsx | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/components/icons/Volume.tsx diff --git a/src/components/icons/Icon.stories.tsx b/src/components/icons/Icon.stories.tsx index ddfa56bf..aa1a5668 100644 --- a/src/components/icons/Icon.stories.tsx +++ b/src/components/icons/Icon.stories.tsx @@ -17,6 +17,7 @@ import DTReset from './Reset'; import DTShare from './Share'; import DTExchange from './Exchange'; import DTBell from './Bell'; +import DTVolume from './Volume'; const meta: Meta = { title: 'Design System/Icons', @@ -256,3 +257,15 @@ export const OnBell: Story = { ), }; + +export const OnVolume: Story = { + args: { + color: '#FECD4C', + }, + render: (args) => ( +
+ +

볼륨 조절

+
+ ), +}; diff --git a/src/components/icons/Volume.tsx b/src/components/icons/Volume.tsx new file mode 100644 index 00000000..b9a86ec7 --- /dev/null +++ b/src/components/icons/Volume.tsx @@ -0,0 +1,22 @@ +import { IconProps } from './IconProps'; + +export default function DTVolume({ + color = 'currentColor', + className = '', + ...props +}: IconProps) { + return ( + + + + ); +} From 62f71737d58bd7038831e12f2381804e747d2e6d Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sat, 29 Nov 2025 23:16:17 +0900 Subject: [PATCH 02/22] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=ED=9B=85?= =?UTF-8?q?=EC=97=90=20=EB=B2=A8=20=EC=86=8C=EB=A6=AC=20=EB=B3=BC=EB=A5=A8?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/TimerPage/hooks/useBellSound.ts | 9 ++++-- src/page/TimerPage/hooks/useTimerPageState.ts | 30 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/page/TimerPage/hooks/useBellSound.ts b/src/page/TimerPage/hooks/useBellSound.ts index a9e96588..1391c33c 100644 --- a/src/page/TimerPage/hooks/useBellSound.ts +++ b/src/page/TimerPage/hooks/useBellSound.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { NormalTimerLogics } from './useNormalTimer'; import { BellConfig } from '../../../type/type'; @@ -8,9 +8,12 @@ interface UseBellSoundProps { } export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { + const [volume, setVolume] = useState(1.0); + // 종소리 여러 번 - 새로운 Audio로 재생 function playBell(count: number) { const audio = new Audio(`/sounds/bell-${count}.mp3`); + audio.volume = volume; audio.play().catch((err) => { console.warn('audio.play() 실패:', err); }); @@ -39,5 +42,7 @@ export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { playBell(bell.count); } }); - }, [normalTimer.timer, bells, normalTimer.defaultTimer]); + }, [normalTimer.timer, bells, normalTimer.defaultTimer, volume]); + + return { volume, setVolume }; } diff --git a/src/page/TimerPage/hooks/useTimerPageState.ts b/src/page/TimerPage/hooks/useTimerPageState.ts index 813150d7..4f2a9c46 100644 --- a/src/page/TimerPage/hooks/useTimerPageState.ts +++ b/src/page/TimerPage/hooks/useTimerPageState.ts @@ -18,6 +18,8 @@ import { import { useTimerBackground } from './useTimerBackground'; import useFullscreen from '../../../hooks/useFullscreen'; +const VOLUME_SCALE = 10; + /** * 타이머 페이지의 상태(타이머, 라운드, 벨 등) 전반을 관리하는 커스텀 훅 */ @@ -55,11 +57,29 @@ export function useTimerPageState(tableId: number): TimerPageLogics { useState('PROS'); // 벨 사운드 관련 훅 - useBellSound({ + const { volume: rawVolume, setVolume: setRawVolume } = useBellSound({ normalTimer, bells: data?.table[index].bell, }); + // 볼륨 값과 조절 함수 + // - React 내부적으로는 0.0 ~ 1.0 사이 값 사용 + // - 아래 값과 함수를 통해 사용자에게는 0 ~ 10 사이 값으로 인식되게 값을 변형 + const volume = Math.round(rawVolume * VOLUME_SCALE); + const setVolume = (value: number) => { + if (value < 0 || value > VOLUME_SCALE) { + return; + } + + setRawVolume(value / VOLUME_SCALE); + }; + + // 벨 볼륨 관련 + const [isVolumeBarOpen, setIsVolumeBarOpen] = useState(false); + const toggleVolumeBar = () => { + setIsVolumeBarOpen((prev) => !prev); + }; + const { bg, setBg } = useTimerBackground({ timer1, timer2, @@ -262,6 +282,10 @@ export function useTimerPageState(tableId: number): TimerPageLogics { isFullscreen, toggleFullscreen, setFullscreen, + volume, + setVolume, + isVolumeBarOpen, + toggleVolumeBar, }; } @@ -287,4 +311,8 @@ export interface TimerPageLogics { isFullscreen: boolean; toggleFullscreen: () => void; setFullscreen: (value: boolean) => void; + volume: number; + setVolume: (value: number) => void; + toggleVolumeBar: () => void; + isVolumeBarOpen: boolean; } From 4258ddf20d8b4288b1d4526c2dfd90b801c7aca8 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sun, 30 Nov 2025 17:31:04 +0900 Subject: [PATCH 03/22] =?UTF-8?q?refactor:=20=ED=97=A4=EB=8D=94=EC=97=90?= =?UTF-8?q?=20z-index=20=EB=B6=80=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layout/components/header/StickyTriSectionHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layout/components/header/StickyTriSectionHeader.tsx b/src/layout/components/header/StickyTriSectionHeader.tsx index 8ef669a3..498f60ba 100644 --- a/src/layout/components/header/StickyTriSectionHeader.tsx +++ b/src/layout/components/header/StickyTriSectionHeader.tsx @@ -20,7 +20,7 @@ function StickyTriSectionHeader(props: PropsWithChildren) { const { children } = props; return ( -
+
{children}
From 75191a0a7c4f3a01cf70a95cd846ed1a0beb6e03 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sun, 30 Nov 2025 17:37:43 +0900 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=8D=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomRangeSlider/CustomRangeSlider.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/components/CustomRangeSlider/CustomRangeSlider.tsx diff --git a/src/components/CustomRangeSlider/CustomRangeSlider.tsx b/src/components/CustomRangeSlider/CustomRangeSlider.tsx new file mode 100644 index 00000000..a6a8eee5 --- /dev/null +++ b/src/components/CustomRangeSlider/CustomRangeSlider.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; + +interface CustomRangeSliderProps { + // Controlled props + value: number; + onValueChange: (value: number) => void; + + // Configuration + min?: number; + max?: number; + step?: number; +} + +export default function CustomRangeSlider({ + value, + onValueChange, + min = 0, + max = 100, + step = 1, +}: CustomRangeSliderProps) { + const [showTooltip, setShowTooltip] = useState(false); + const percentage = Math.min( + 100, + Math.max(0, ((value - min) / (max - min)) * 100), + ); + + return ( +
+ {/* 스타일링 레이어 */} + {/* 1. 전체 트랙 (배경) */} +
+ + {/* 2. 현재 진행도 트랙 */} +
+ + {/* 3. Thumb (원형 드래그 핸들) */} +
+ {/* 볼륨 수치로 나타내는 말풍선 */} + {showTooltip && ( +
+
+ {value} + {/* 말풍선 꼬리 */} +
+
+
+ )} +
+ + {/* 실제 상호작용 레이어 */} + onValueChange(Number(e.target.value))} + onMouseEnter={() => setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" + /> +
+ ); +} From 55384e51041c8ca39e7950fe2f22835ed667f2a2 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sun, 30 Nov 2025 17:37:50 +0900 Subject: [PATCH 05/22] =?UTF-8?q?feat:=20=EB=B3=BC=EB=A5=A8=20=EB=B0=94=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 --- .../VolumeBar/VolumeBar.stories.tsx | 19 ++++ src/components/VolumeBar/VolumeBar.tsx | 95 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/components/VolumeBar/VolumeBar.stories.tsx create mode 100644 src/components/VolumeBar/VolumeBar.tsx diff --git a/src/components/VolumeBar/VolumeBar.stories.tsx b/src/components/VolumeBar/VolumeBar.stories.tsx new file mode 100644 index 00000000..a5bf7fbe --- /dev/null +++ b/src/components/VolumeBar/VolumeBar.stories.tsx @@ -0,0 +1,19 @@ +import { Meta, StoryObj } from '@storybook/react'; +import VolumeBar from './VolumeBar'; + +const meta: Meta = { + title: 'components/VolumeBar', + component: VolumeBar, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + volume: 0, + onVolumeChange: (delta: number) => console.log(delta), + }, +}; diff --git a/src/components/VolumeBar/VolumeBar.tsx b/src/components/VolumeBar/VolumeBar.tsx new file mode 100644 index 00000000..e55f3aa6 --- /dev/null +++ b/src/components/VolumeBar/VolumeBar.tsx @@ -0,0 +1,95 @@ +// Integer 1-10, step = 1 +// Mute button available +import CustomRangeSlider from '../CustomRangeSlider/CustomRangeSlider'; +import DTVolume from '../icons/Volume'; + +interface VolumeBarProps { + volume: number; + onVolumeChange: (volume: number) => void; + className?: string; +} + +export default function VolumeBar({ + volume, + onVolumeChange, + className = '', +}: VolumeBarProps) { + return ( +
+ {/* SVG Layer */} + + + + + + + + + + + + + + + + + + + + + + + {/* Content Layer */} +
+
+ + +
+
+
+ ); +} From 7d396327665e48139ec0b6373973c7cff39ba5ad Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sun, 30 Nov 2025 17:37:59 +0900 Subject: [PATCH 06/22] =?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=EC=97=90=20=EB=B3=BC=EB=A5=A8=20?= =?UTF-8?q?=EB=B0=94=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/TimerPage/TimerPage.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/page/TimerPage/TimerPage.tsx b/src/page/TimerPage/TimerPage.tsx index 86fe87f2..4e481a2e 100644 --- a/src/page/TimerPage/TimerPage.tsx +++ b/src/page/TimerPage/TimerPage.tsx @@ -15,6 +15,8 @@ import clsx from 'clsx'; import ErrorIndicator from '../../components/ErrorIndicator/ErrorIndicator'; import LoadingIndicator from '../../components/LoadingIndicator/LoadingIndicator'; import { RiFullscreenFill, RiFullscreenExitFill } from 'react-icons/ri'; +import DTVolume from '../../components/icons/Volume'; +import VolumeBar from '../../components/VolumeBar/VolumeBar'; export default function TimerPage() { const pathParams = useParams(); @@ -39,6 +41,10 @@ export default function TimerPage() { isLoading, isError, refetch, + isVolumeBarOpen, + toggleVolumeBar, + volume, + setVolume, isFullscreen, setFullscreen, toggleFullscreen, @@ -103,6 +109,26 @@ export default function TimerPage() { )} + +
+ + + {isVolumeBarOpen && ( +
+ setVolume(value)} + /> +
+ )} +
From b798e5ace8f1175ae946736ef163b352cdf74876 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sun, 30 Nov 2025 17:53:03 +0900 Subject: [PATCH 07/22] =?UTF-8?q?test:=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=8D=94=20Storybook=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomRangeSlider.stories.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/components/CustomRangeSlider/CustomRangeSlider.stories.tsx diff --git a/src/components/CustomRangeSlider/CustomRangeSlider.stories.tsx b/src/components/CustomRangeSlider/CustomRangeSlider.stories.tsx new file mode 100644 index 00000000..9df65729 --- /dev/null +++ b/src/components/CustomRangeSlider/CustomRangeSlider.stories.tsx @@ -0,0 +1,24 @@ +// Storybook 코드 +import { Meta, StoryObj } from '@storybook/react'; +import CustomRangeSlider from './CustomRangeSlider'; + +const meta: Meta = { + title: 'Components/CustomRangeSlider', + component: CustomRangeSlider, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + value: 5, + max: 10, + min: 0, + onValueChange: (value: number) => { + console.log(value); + }, + }, +}; From a32bd106dd07d0db6f32e7316e28fe87194809d8 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sun, 30 Nov 2025 17:53:17 +0900 Subject: [PATCH 08/22] =?UTF-8?q?refactor:=20=EC=B5=9C=EC=86=8C,=20?= =?UTF-8?q?=EC=B5=9C=EB=8C=80=20=EB=B3=BC=EB=A5=A8=20=EB=B0=8F=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/VolumeBar/VolumeBar.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/VolumeBar/VolumeBar.tsx b/src/components/VolumeBar/VolumeBar.tsx index e55f3aa6..38808328 100644 --- a/src/components/VolumeBar/VolumeBar.tsx +++ b/src/components/VolumeBar/VolumeBar.tsx @@ -9,6 +9,10 @@ interface VolumeBarProps { className?: string; } +const MIN_VOLUME = 0; +const MAX_VOLUME = 10; +const STEP_VOLUME = 1; + export default function VolumeBar({ volume, onVolumeChange, @@ -84,9 +88,9 @@ export default function VolumeBar({
From 2ca7770f9e2ab6688826fbcefdf7939ae08a85ea Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sun, 30 Nov 2025 18:04:19 +0900 Subject: [PATCH 09/22] =?UTF-8?q?feat:=20=EC=9D=8C=EC=86=8C=EA=B1=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/VolumeBar/VolumeBar.tsx | 35 ++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/VolumeBar/VolumeBar.tsx b/src/components/VolumeBar/VolumeBar.tsx index 38808328..2b5a80aa 100644 --- a/src/components/VolumeBar/VolumeBar.tsx +++ b/src/components/VolumeBar/VolumeBar.tsx @@ -1,7 +1,9 @@ // Integer 1-10, step = 1 // Mute button available +import { useState } from 'react'; import CustomRangeSlider from '../CustomRangeSlider/CustomRangeSlider'; import DTVolume from '../icons/Volume'; +import clsx from 'clsx'; interface VolumeBarProps { volume: number; @@ -18,6 +20,21 @@ export default function VolumeBar({ onVolumeChange, className = '', }: VolumeBarProps) { + // 음소거 해제 시 가장 마지막의 볼륨 값을 복원하기 위함 + const [lastVolume, setLastVolume] = useState(5); + + // 음소거 로직 + const handleMute = () => { + if (volume === 0) { + onVolumeChange(lastVolume === 0 ? 1 : lastVolume); + } else { + onVolumeChange(0); + } + }; + + // 음소거 버튼은 오직 볼륨이 0일 때에만 흐리게 강조됨 + const isMuteButtonHighlighted = volume > 0; + return (
{/* SVG Layer */} @@ -84,10 +101,24 @@ export default function VolumeBar({ {/* Content Layer */}
- + { + onVolumeChange(value); + + // 마지막 볼륨이 0으로 저장되면, 음소거를 해제해도 음소거가 유지되는 버그를 피하기 위함 + if (value > 0) { + setLastVolume(value); + } + }} min={MIN_VOLUME} max={MAX_VOLUME} step={STEP_VOLUME} From b873bab29ceb0ad92b1ea60231f0a03949b318e3 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Sun, 30 Nov 2025 18:38:13 +0900 Subject: [PATCH 10/22] =?UTF-8?q?feat:=20=EB=B3=BC=EB=A5=A8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=B4=20=EB=A1=9C=EC=BB=AC=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=EC=97=90=20=EC=A0=80=EC=9E=A5=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/TimerPage/hooks/useBellSound.ts | 31 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/page/TimerPage/hooks/useBellSound.ts b/src/page/TimerPage/hooks/useBellSound.ts index 1391c33c..74558138 100644 --- a/src/page/TimerPage/hooks/useBellSound.ts +++ b/src/page/TimerPage/hooks/useBellSound.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { NormalTimerLogics } from './useNormalTimer'; import { BellConfig } from '../../../type/type'; @@ -7,18 +7,41 @@ interface UseBellSoundProps { bells?: BellConfig[] | null; } +const STORAGE_KEY = 'timer-volume'; + export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { - const [volume, setVolume] = useState(1.0); + const [volume, setVolume] = useState(() => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem(STORAGE_KEY); + return saved !== null ? Number(saved) : 1.0; + } + return 1.0; + }); + const volumeRef = useRef(volume); + + useEffect(() => { + volumeRef.current = volume; + }, [volume]); // 종소리 여러 번 - 새로운 Audio로 재생 function playBell(count: number) { const audio = new Audio(`/sounds/bell-${count}.mp3`); - audio.volume = volume; + audio.volume = volumeRef.current; audio.play().catch((err) => { console.warn('audio.play() 실패:', err); }); } + // 볼륨 변경 시 최신 값을 로컬 저장소에 저장 + // 500 ms 디바운싱 적용하여 성능 문제 예방 + useEffect(() => { + const timerId = setTimeout(() => { + localStorage.setItem(STORAGE_KEY, volume.toString()); + }, 500); + + return () => clearTimeout(timerId); + }, [volume]); + useEffect(() => { const timerVal = normalTimer.timer; const defaultTime = normalTimer.defaultTimer; @@ -42,7 +65,7 @@ export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { playBell(bell.count); } }); - }, [normalTimer.timer, bells, normalTimer.defaultTimer, volume]); + }, [normalTimer.timer, bells, normalTimer.defaultTimer]); return { volume, setVolume }; } From 954e6d49f8c32cf95c58b74d45e171e7125f7bd3 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Mon, 1 Dec 2025 23:15:58 +0900 Subject: [PATCH 11/22] =?UTF-8?q?fix:=20CodeRabbit=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 오류 위주 --- src/components/CustomRangeSlider/CustomRangeSlider.tsx | 7 +++---- src/components/VolumeBar/VolumeBar.stories.tsx | 4 ++-- src/components/VolumeBar/VolumeBar.tsx | 3 ++- src/page/TimerPage/hooks/useBellSound.ts | 7 ++++++- src/page/TimerPage/hooks/useTimerPageState.ts | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/CustomRangeSlider/CustomRangeSlider.tsx b/src/components/CustomRangeSlider/CustomRangeSlider.tsx index a6a8eee5..78001b32 100644 --- a/src/components/CustomRangeSlider/CustomRangeSlider.tsx +++ b/src/components/CustomRangeSlider/CustomRangeSlider.tsx @@ -19,10 +19,9 @@ export default function CustomRangeSlider({ step = 1, }: CustomRangeSliderProps) { const [showTooltip, setShowTooltip] = useState(false); - const percentage = Math.min( - 100, - Math.max(0, ((value - min) / (max - min)) * 100), - ); + const range = max - min; + const percentage = + range <= 0 ? 0 : Math.min(100, Math.max(0, ((value - min) / range) * 100)); return (
diff --git a/src/components/VolumeBar/VolumeBar.stories.tsx b/src/components/VolumeBar/VolumeBar.stories.tsx index a5bf7fbe..7ebd5ce3 100644 --- a/src/components/VolumeBar/VolumeBar.stories.tsx +++ b/src/components/VolumeBar/VolumeBar.stories.tsx @@ -2,7 +2,7 @@ import { Meta, StoryObj } from '@storybook/react'; import VolumeBar from './VolumeBar'; const meta: Meta = { - title: 'components/VolumeBar', + title: 'Components/VolumeBar', component: VolumeBar, tags: ['autodocs'], }; @@ -14,6 +14,6 @@ type Story = StoryObj; export const Default: Story = { args: { volume: 0, - onVolumeChange: (delta: number) => console.log(delta), + onVolumeChange: (volume: number) => console.log(volume), }, }; diff --git a/src/components/VolumeBar/VolumeBar.tsx b/src/components/VolumeBar/VolumeBar.tsx index 2b5a80aa..adbd74cf 100644 --- a/src/components/VolumeBar/VolumeBar.tsx +++ b/src/components/VolumeBar/VolumeBar.tsx @@ -21,13 +21,14 @@ export default function VolumeBar({ className = '', }: VolumeBarProps) { // 음소거 해제 시 가장 마지막의 볼륨 값을 복원하기 위함 - const [lastVolume, setLastVolume] = useState(5); + const [lastVolume, setLastVolume] = useState(volume > 0 ? volume : 5); // 음소거 로직 const handleMute = () => { if (volume === 0) { onVolumeChange(lastVolume === 0 ? 1 : lastVolume); } else { + setLastVolume(volume); onVolumeChange(0); } }; diff --git a/src/page/TimerPage/hooks/useBellSound.ts b/src/page/TimerPage/hooks/useBellSound.ts index 74558138..16489e18 100644 --- a/src/page/TimerPage/hooks/useBellSound.ts +++ b/src/page/TimerPage/hooks/useBellSound.ts @@ -13,7 +13,12 @@ export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { const [volume, setVolume] = useState(() => { if (typeof window !== 'undefined') { const saved = localStorage.getItem(STORAGE_KEY); - return saved !== null ? Number(saved) : 1.0; + if (saved !== null) { + // NaN 등의 손상된 값 검증 + const parsed = Number(saved); + return Number.isFinite(parsed) ? parsed : 1.0; + } + return 1.0; } return 1.0; }); diff --git a/src/page/TimerPage/hooks/useTimerPageState.ts b/src/page/TimerPage/hooks/useTimerPageState.ts index 4f2a9c46..1d85fb0f 100644 --- a/src/page/TimerPage/hooks/useTimerPageState.ts +++ b/src/page/TimerPage/hooks/useTimerPageState.ts @@ -59,7 +59,7 @@ export function useTimerPageState(tableId: number): TimerPageLogics { // 벨 사운드 관련 훅 const { volume: rawVolume, setVolume: setRawVolume } = useBellSound({ normalTimer, - bells: data?.table[index].bell, + bells: data?.table[index]?.bell, }); // 볼륨 값과 조절 함수 From 5abd7649da64a5922f5416e6e5b68f67df59989f Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Mon, 1 Dec 2025 23:20:27 +0900 Subject: [PATCH 12/22] =?UTF-8?q?refactor:=20=EC=A0=91=EA=B7=BC=EC=84=B1?= =?UTF-8?q?=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomRangeSlider/CustomRangeSlider.tsx | 2 ++ src/components/VolumeBar/VolumeBar.tsx | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/CustomRangeSlider/CustomRangeSlider.tsx b/src/components/CustomRangeSlider/CustomRangeSlider.tsx index 78001b32..2fe91682 100644 --- a/src/components/CustomRangeSlider/CustomRangeSlider.tsx +++ b/src/components/CustomRangeSlider/CustomRangeSlider.tsx @@ -63,6 +63,8 @@ export default function CustomRangeSlider({ onChange={(e) => onValueChange(Number(e.target.value))} onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} + onFocus={() => setShowTooltip(true)} + onBlur={() => setShowTooltip(false)} className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" />
diff --git a/src/components/VolumeBar/VolumeBar.tsx b/src/components/VolumeBar/VolumeBar.tsx index adbd74cf..722040b9 100644 --- a/src/components/VolumeBar/VolumeBar.tsx +++ b/src/components/VolumeBar/VolumeBar.tsx @@ -34,7 +34,7 @@ export default function VolumeBar({ }; // 음소거 버튼은 오직 볼륨이 0일 때에만 흐리게 강조됨 - const isMuteButtonHighlighted = volume > 0; + const isNotMute = volume > 0; return (
@@ -102,11 +102,16 @@ export default function VolumeBar({ {/* Content Layer */}
- From abc28ed9941a035c1e159b64fb6a05069522d032 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Mon, 1 Dec 2025 23:29:15 +0900 Subject: [PATCH 13/22] =?UTF-8?q?refactor:=20=EC=99=B8=EB=B6=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B3=BC=EB=A5=A8=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C?= =?UTF-8?q?=20=EB=8F=99=EA=B8=B0=ED=99=94=ED=95=98=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/VolumeBar/VolumeBar.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/VolumeBar/VolumeBar.tsx b/src/components/VolumeBar/VolumeBar.tsx index 722040b9..d59f03d2 100644 --- a/src/components/VolumeBar/VolumeBar.tsx +++ b/src/components/VolumeBar/VolumeBar.tsx @@ -1,6 +1,6 @@ // Integer 1-10, step = 1 // Mute button available -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import CustomRangeSlider from '../CustomRangeSlider/CustomRangeSlider'; import DTVolume from '../icons/Volume'; import clsx from 'clsx'; @@ -36,6 +36,13 @@ export default function VolumeBar({ // 음소거 버튼은 오직 볼륨이 0일 때에만 흐리게 강조됨 const isNotMute = volume > 0; + // 외부에서 볼륨이 변경될 경우, 값을 관측하여 동기화 + useEffect(() => { + if (volume > 0) { + setLastVolume(volume); + } + }, [volume]); + return (
{/* SVG Layer */} From 3deed5ed8558f5fa7a85602690fdb84498a57e3a Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Mon, 1 Dec 2025 23:39:15 +0900 Subject: [PATCH 14/22] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/VolumeBar/VolumeBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/VolumeBar/VolumeBar.tsx b/src/components/VolumeBar/VolumeBar.tsx index d59f03d2..6c9a076c 100644 --- a/src/components/VolumeBar/VolumeBar.tsx +++ b/src/components/VolumeBar/VolumeBar.tsx @@ -1,4 +1,4 @@ -// Integer 1-10, step = 1 +// Integer 0-10, step = 1 // Mute button available import { useEffect, useState } from 'react'; import CustomRangeSlider from '../CustomRangeSlider/CustomRangeSlider'; @@ -51,7 +51,7 @@ export default function VolumeBar({ viewBox="0 0 234 76" fill="none" xmlns="http://www.w3.org/2000/svg" - preserveAspectRatio="none" // Ensures SVG stretches if container resizes + preserveAspectRatio="none" > From bbf487f7d7127751ddead9bf1ba3bad109a3feee Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Tue, 2 Dec 2025 18:03:55 +0900 Subject: [PATCH 15/22] =?UTF-8?q?fix:=20z-index=20=EC=9D=BC=EB=B6=80=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/hooks/useModal.tsx | 2 +- src/layout/components/header/StickyTriSectionHeader.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useModal.tsx b/src/hooks/useModal.tsx index dcdb1beb..b2d0e73c 100644 --- a/src/hooks/useModal.tsx +++ b/src/hooks/useModal.tsx @@ -62,7 +62,7 @@ export function useModal(options: UseModalOptions = {}) { return (
diff --git a/src/layout/components/header/StickyTriSectionHeader.tsx b/src/layout/components/header/StickyTriSectionHeader.tsx index 498f60ba..caf3ece9 100644 --- a/src/layout/components/header/StickyTriSectionHeader.tsx +++ b/src/layout/components/header/StickyTriSectionHeader.tsx @@ -20,7 +20,7 @@ function StickyTriSectionHeader(props: PropsWithChildren) { const { children } = props; return ( -
+
{children}
From a468f3f105306c849a32c6f96474d48cee14fc53 Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Tue, 23 Dec 2025 23:30:45 +0900 Subject: [PATCH 16/22] =?UTF-8?q?refactor:=20=EB=B3=BC=EB=A5=A8=20?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=EC=95=88=EC=A0=84=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=ED=95=98=EB=8A=94=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/TimerPage/hooks/useBellSound.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/page/TimerPage/hooks/useBellSound.ts b/src/page/TimerPage/hooks/useBellSound.ts index 16489e18..4d9401bc 100644 --- a/src/page/TimerPage/hooks/useBellSound.ts +++ b/src/page/TimerPage/hooks/useBellSound.ts @@ -24,6 +24,20 @@ export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { }); const volumeRef = useRef(volume); + // 안전하게 볼륨 값을 갱신하는 함수 + const updateVolume = (value: number) => { + if ( + value === null || + Number.isNaN(value) || + !Number.isFinite(value) || + value < 0 || + value > 1 + ) { + setVolume(0.5); + } + setVolume(value); + }; + useEffect(() => { volumeRef.current = volume; }, [volume]); @@ -72,5 +86,5 @@ export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { }); }, [normalTimer.timer, bells, normalTimer.defaultTimer]); - return { volume, setVolume }; + return { volume, updateVolume }; } From 6ef17e6a1235a328b615cd94d2197a3c6ac9fc4a Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Tue, 23 Dec 2025 23:30:53 +0900 Subject: [PATCH 17/22] =?UTF-8?q?refactor:=20=EB=B3=BC=EB=A5=A8=20?= =?UTF-8?q?=EA=B0=92=20=EC=B4=88=EA=B8=B0=ED=99=94=20=ED=95=A8=EC=88=98=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/page/TimerPage/hooks/useBellSound.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/page/TimerPage/hooks/useBellSound.ts b/src/page/TimerPage/hooks/useBellSound.ts index 4d9401bc..fd01b6ae 100644 --- a/src/page/TimerPage/hooks/useBellSound.ts +++ b/src/page/TimerPage/hooks/useBellSound.ts @@ -10,7 +10,8 @@ interface UseBellSoundProps { const STORAGE_KEY = 'timer-volume'; export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { - const [volume, setVolume] = useState(() => { + // 볼륨 초기화 함수 + const getAndInitVolume = () => { if (typeof window !== 'undefined') { const saved = localStorage.getItem(STORAGE_KEY); if (saved !== null) { @@ -21,7 +22,9 @@ export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { return 1.0; } return 1.0; - }); + }; + + const [volume, setVolume] = useState(() => getAndInitVolume()); const volumeRef = useRef(volume); // 안전하게 볼륨 값을 갱신하는 함수 From df049c4b0073b1662647a575840bf85e0356e23a Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Tue, 23 Dec 2025 23:33:36 +0900 Subject: [PATCH 18/22] =?UTF-8?q?chore:=20=EC=9D=B4=ED=95=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/TimerPage/hooks/useBellSound.ts | 9 ++++++--- src/page/TimerPage/hooks/useTimerPageState.ts | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/page/TimerPage/hooks/useBellSound.ts b/src/page/TimerPage/hooks/useBellSound.ts index fd01b6ae..db7deef3 100644 --- a/src/page/TimerPage/hooks/useBellSound.ts +++ b/src/page/TimerPage/hooks/useBellSound.ts @@ -27,14 +27,17 @@ export function useBellSound({ normalTimer, bells }: UseBellSoundProps) { const [volume, setVolume] = useState(() => getAndInitVolume()); const volumeRef = useRef(volume); - // 안전하게 볼륨 값을 갱신하는 함수 + /** 안전하게 볼륨 값을 갱신하는 함수. + * 값이 정상적이지 않을 경우, 일괄 0.5로 초기화합니다. + * @param value 갱신할 볼륨 값 (실수 0.0부터 1.0 사이) + */ const updateVolume = (value: number) => { if ( value === null || Number.isNaN(value) || !Number.isFinite(value) || - value < 0 || - value > 1 + value < 0.0 || + value > 1.0 ) { setVolume(0.5); } diff --git a/src/page/TimerPage/hooks/useTimerPageState.ts b/src/page/TimerPage/hooks/useTimerPageState.ts index 1d85fb0f..f410433b 100644 --- a/src/page/TimerPage/hooks/useTimerPageState.ts +++ b/src/page/TimerPage/hooks/useTimerPageState.ts @@ -71,7 +71,8 @@ export function useTimerPageState(tableId: number): TimerPageLogics { return; } - setRawVolume(value / VOLUME_SCALE); + // UI 상의 0 ~ 10 볼륨을 React 내부 로직의 0.0 ~ 1.0으로 바꾸어서 갱신 + updateVolume(value / VOLUME_SCALE); }; // 벨 볼륨 관련 From 37204f8a9a14d61220a7719127b2c2e9a1b26fdb Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Tue, 23 Dec 2025 23:38:52 +0900 Subject: [PATCH 19/22] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/TimerPage/hooks/useTimerPageState.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/page/TimerPage/hooks/useTimerPageState.ts b/src/page/TimerPage/hooks/useTimerPageState.ts index f410433b..cf73cc25 100644 --- a/src/page/TimerPage/hooks/useTimerPageState.ts +++ b/src/page/TimerPage/hooks/useTimerPageState.ts @@ -57,7 +57,7 @@ export function useTimerPageState(tableId: number): TimerPageLogics { useState('PROS'); // 벨 사운드 관련 훅 - const { volume: rawVolume, setVolume: setRawVolume } = useBellSound({ + const { volume: rawVolume, updateVolume: updateRawVolume } = useBellSound({ normalTimer, bells: data?.table[index]?.bell, }); @@ -66,13 +66,13 @@ export function useTimerPageState(tableId: number): TimerPageLogics { // - React 내부적으로는 0.0 ~ 1.0 사이 값 사용 // - 아래 값과 함수를 통해 사용자에게는 0 ~ 10 사이 값으로 인식되게 값을 변형 const volume = Math.round(rawVolume * VOLUME_SCALE); - const setVolume = (value: number) => { + const updateVolume = (value: number) => { if (value < 0 || value > VOLUME_SCALE) { return; } // UI 상의 0 ~ 10 볼륨을 React 내부 로직의 0.0 ~ 1.0으로 바꾸어서 갱신 - updateVolume(value / VOLUME_SCALE); + updateRawVolume(value / VOLUME_SCALE); }; // 벨 볼륨 관련 @@ -284,7 +284,7 @@ export function useTimerPageState(tableId: number): TimerPageLogics { toggleFullscreen, setFullscreen, volume, - setVolume, + setVolume: updateVolume, isVolumeBarOpen, toggleVolumeBar, }; From 1543f453cc33dc517ab72b715228873b452b79da Mon Sep 17 00:00:00 2001 From: Shawn Kang Date: Tue, 23 Dec 2025 23:45:47 +0900 Subject: [PATCH 20/22] =?UTF-8?q?feat:=20=EB=B3=BC=EB=A5=A8=20=EB=B0=94=20?= =?UTF-8?q?=EB=B0=94=EA=B9=A5=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EB=8B=AB?= =?UTF-8?q?=ED=9E=88=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/page/TimerPage/TimerPage.tsx | 3 ++- src/page/TimerPage/hooks/useTimerPageState.ts | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/page/TimerPage/TimerPage.tsx b/src/page/TimerPage/TimerPage.tsx index 4e481a2e..c5efdfa5 100644 --- a/src/page/TimerPage/TimerPage.tsx +++ b/src/page/TimerPage/TimerPage.tsx @@ -48,6 +48,7 @@ export default function TimerPage() { isFullscreen, setFullscreen, toggleFullscreen, + volumeRef, } = state; // If error, print error message and let user be able to retry @@ -110,7 +111,7 @@ export default function TimerPage() { )} -
+