diff --git a/src/page/TimerPage/components/TimeBasedTimer.tsx b/src/page/TimerPage/components/TimeBasedTimer.tsx index c5e79b9c..555a4fb0 100644 --- a/src/page/TimerPage/components/TimeBasedTimer.tsx +++ b/src/page/TimerPage/components/TimeBasedTimer.tsx @@ -12,13 +12,15 @@ type TimeBasedTimerInstance = { totalTimer: number | null; speakingTimer: number | null; isRunning: boolean; - startTimer: () => void; + startTimer: (isOpponentDone: boolean) => void; pauseTimer: () => void; resetCurrentTimer: () => void; + denominator: number; }; interface TimeBasedTimerProps { timeBasedTimerInstance: TimeBasedTimerInstance; + isOpponentDone: boolean; isSelected: boolean; onActivate?: () => void; prosCons: TimeBasedStance; @@ -28,6 +30,7 @@ interface TimeBasedTimerProps { export default function TimeBasedTimer({ timeBasedTimerInstance, + isOpponentDone, isSelected, prosCons, teamName, @@ -40,6 +43,7 @@ export default function TimeBasedTimer({ startTimer, pauseTimer, resetCurrentTimer, + denominator, } = timeBasedTimerInstance; const minute = Formatting.formatTwoDigits( @@ -55,30 +59,38 @@ export default function TimeBasedTimer({ ); const initRawProgress = (): number => { - if (speakingTimer === null) { - // 1회당 발언 시간 X일 때... - if (item.timePerTeam && totalTimer && item.timePerTeam > 0) { - // 팀당 발언 시간 타이머가 정상 동작 중이고 남은 시간이 있을 경우, 진행도를 계산 - if (totalTimer <= 0) { - return 100; - } - return ((item.timePerTeam - totalTimer) / item.timePerTeam) * 100; + if (isOpponentDone) { + if (speakingTimer === null) { + // 1회당 발언 시간 X일 때... + return ((denominator - (totalTimer ?? 0)) / denominator) * 100; } else { - // 팀당 발언 시간 타이머가 멈추거나 완료된 경우, - // 완료(100%)에 해당하는 진행도를 반환 - return 100; + // 1회당 발언 시간 O일 때... + return ((denominator - (speakingTimer ?? 0)) / denominator) * 100; } } else { - // 1회당 발언 시간 O일 때... - if (item.timePerSpeaking && speakingTimer && item.timePerSpeaking > 0) { - // 1회당 발언 시간 타이머가 정상 동작 중이고 남은 시간이 있을 경우, 진행도를 계산 - return ( - ((item.timePerSpeaking - speakingTimer) / item.timePerSpeaking) * 100 - ); + if (speakingTimer === null) { + // 1회당 발언 시간 X일 때... + if (item.timePerTeam && totalTimer && item.timePerTeam > 0) { + // 팀당 발언 시간 타이머가 정상 동작 중이고 남은 시간이 있을 경우, 진행도를 계산 + if (totalTimer <= 0) { + return 100; + } + return ((item.timePerTeam - totalTimer) / denominator) * 100; + } else { + // 팀당 발언 시간 타이머가 멈추거나 완료된 경우, + // 완료(100%)에 해당하는 진행도를 반환 + return 100; + } } else { - // 1회당 발언 시간 타이머가 멈추거나 완료된 경우, - // 완료(100%)에 해당하는 진행도를 반환 - return 100; + // 1회당 발언 시간 O일 때... + if (item.timePerSpeaking && speakingTimer && item.timePerSpeaking > 0) { + // 1회당 발언 시간 타이머가 정상 동작 중이고 남은 시간이 있을 경우, 진행도를 계산 + return ((item.timePerSpeaking - speakingTimer) / denominator) * 100; + } else { + // 1회당 발언 시간 타이머가 멈추거나 완료된 경우, + // 완료(100%)에 해당하는 진행도를 반환 + return 100; + } } } }; @@ -177,7 +189,7 @@ export default function TimeBasedTimer({ {/* 조작부 */} startTimer(isOpponentDone)} onPause={pauseTimer} onReset={resetCurrentTimer} stance={prosCons} diff --git a/src/page/TimerPage/components/TimerView.tsx b/src/page/TimerPage/components/TimerView.tsx index 933ee49d..478d3a8c 100644 --- a/src/page/TimerPage/components/TimerView.tsx +++ b/src/page/TimerPage/components/TimerView.tsx @@ -53,9 +53,11 @@ export default function TimerView({ state }: { state: TimerPageLogics }) { isRunning: timer1.isRunning, startTimer: timer1.startTimer, pauseTimer: timer1.pauseTimer, + denominator: timer1.denominator, resetCurrentTimer: () => timer1.resetCurrentTimer(timer2.isDone), }} item={data.table[index]} + isOpponentDone={timer2.isDone} isSelected={prosConsSelected === 'PROS'} onActivate={() => handleActivateTeam('PROS')} prosCons="PROS" @@ -79,9 +81,11 @@ export default function TimerView({ state }: { state: TimerPageLogics }) { isRunning: timer2.isRunning, startTimer: timer2.startTimer, pauseTimer: timer2.pauseTimer, + denominator: timer2.denominator, resetCurrentTimer: () => timer2.resetCurrentTimer(timer1.isDone), }} item={data.table[index]} + isOpponentDone={timer1.isDone} isSelected={prosConsSelected === 'CONS'} onActivate={() => handleActivateTeam('CONS')} prosCons="CONS" diff --git a/src/page/TimerPage/hooks/useTimeBasedTimer.ts b/src/page/TimerPage/hooks/useTimeBasedTimer.ts index d9957547..8f4c8641 100644 --- a/src/page/TimerPage/hooks/useTimeBasedTimer.ts +++ b/src/page/TimerPage/hooks/useTimeBasedTimer.ts @@ -38,6 +38,33 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { const targetTimeRef = useRef(null); const speakingTargetTimeRef = useRef(null); + // 진행률 계산용 변수 + const [denominator, setDenominator] = useState(1); + const updateDenominator = useCallback( + (isOpponentDone: boolean) => { + if (isOpponentDone) { + if (isSpeakingTimerAvailable) { + setDenominator(speakingTimer ?? 0); + } else { + setDenominator(totalTimer ?? 0); + } + } else { + if (isSpeakingTimerAvailable) { + setDenominator(defaultTime.defaultSpeakingTimer ?? 0); + } else { + setDenominator(defaultTime.defaultTotalTimer ?? 0); + } + } + }, + [ + defaultTime.defaultSpeakingTimer, + defaultTime.defaultTotalTimer, + isSpeakingTimerAvailable, + speakingTimer, + totalTimer, + ], + ); + /** * 타이머 시작을 위해 사용하는 저수준 함수 */ @@ -84,30 +111,37 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { * - isDone(완료) 상태일 땐 시작X * - 1초마다 totalTimer, speakingTimer(필요시) 감소 */ - const startTimer = useCallback(() => { - if (intervalRef.current !== null || totalTimer === null || isDone) { - return; - } + const startTimer = useCallback( + (isOpponentDone: boolean) => { + if (intervalRef.current !== null || totalTimer === null || isDone) { + return; + } - // 목표 시각을 실제 시각 기반으로 계산 - // 예를 들어, 현재 시각이 오후 13시 00분 30초인데, 1회당 발언 시간이 30초라면, - // 1회당 발언 시간이 모두 끝나는 시간은 13시 01분 00초이므로, - // 해당 시간을 목표 시간으로 두는 식임 - const startTime = Date.now(); - targetTimeRef.current = startTime + totalTimer * 1000; - if (isSpeakingTimerAvailable) { - speakingTargetTimeRef.current = startTime + speakingTimer * 1000; - } + // 목표 시각을 실제 시각 기반으로 계산 + // 예를 들어, 현재 시각이 오후 13시 00분 30초인데, 1회당 발언 시간이 30초라면, + // 1회당 발언 시간이 모두 끝나는 시간은 13시 01분 00초이므로, + // 해당 시간을 목표 시간으로 두는 식임 + const startTime = Date.now(); + targetTimeRef.current = startTime + totalTimer * 1000; + if (isSpeakingTimerAvailable) { + speakingTargetTimeRef.current = startTime + speakingTimer * 1000; + } - // 타이머 인터벌 시작 - setTimerInterval(); - }, [ - isDone, - isSpeakingTimerAvailable, - setTimerInterval, - speakingTimer, - totalTimer, - ]); + // 분모 갱신 + updateDenominator(isOpponentDone); + + // 타이머 인터벌 시작 + setTimerInterval(); + }, + [ + isDone, + isSpeakingTimerAvailable, + setTimerInterval, + speakingTimer, + totalTimer, + updateDenominator, + ], + ); /** * 타이머 일시정지 @@ -151,6 +185,9 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { } else { setSpeakingTimer(defaultTime.defaultSpeakingTimer); } + + // 분모 갱신 + updateDenominator(isOpponentDone); }, [ isSpeakingTimerAvailable, @@ -158,6 +195,7 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { defaultTime.defaultTotalTimer, totalTimer, pauseTimer, + updateDenominator, ], ); @@ -184,18 +222,25 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { // 계산한 시간을 1회당 발언 시간으로 설정 setSpeakingTimer(nextSpeakingTime); + updateDenominator(isOpponentDone); return nextSpeakingTime; } else { // # 1회당 발언 시간을 사용하지 않을 경우 // 전체 발언 시간 타이머는 초기값으로 리셋 + updateDenominator(isOpponentDone); if (totalTimer === 0 || totalTimer === null) { return 0; } return totalTimer; } }, - [defaultTime.defaultSpeakingTimer, isSpeakingTimerAvailable, totalTimer], + [ + defaultTime.defaultSpeakingTimer, + isSpeakingTimerAvailable, + totalTimer, + updateDenominator, + ], ); /** @@ -205,7 +250,6 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { */ const resetAndStartTimer = useCallback( (isOpponentDone: boolean) => { - console.log(`# resetAndStartTimer 호출`); const newTime = resetTimerForNextPhase(isOpponentDone); if (intervalRef.current !== null || totalTimer === null || isDone) { @@ -224,6 +268,9 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { // 타이머 인터벌 시작 setTimerInterval(); + + // 분모 갱신 + updateDenominator(isOpponentDone); }, [ resetTimerForNextPhase, @@ -231,6 +278,7 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { isDone, isSpeakingTimerAvailable, totalTimer, + updateDenominator, ], ); @@ -257,8 +305,9 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { setTotalTimer(null); setSpeakingTimer(null); setIsDone(false); + updateDenominator(false); intervalRef.current = null; - }, [pauseTimer]); + }, [pauseTimer, updateDenominator]); useEffect(() => () => pauseTimer(), [pauseTimer]); @@ -278,6 +327,7 @@ export function useTimeBasedTimer(): TimeBasedTimerLogics { setDefaultTime, setIsDone, clearTimer, + denominator, }; } @@ -291,7 +341,7 @@ export interface TimeBasedTimerLogics { defaultSpeakingTimer: number | null; }; isSpeakingTimerAvailable: boolean; - startTimer: () => void; + startTimer: (isOpponentDone: boolean) => void; pauseTimer: () => void; resetTimerForNextPhase: (isOpponentDone: boolean) => number; resetAndStartTimer: (isOpponentDone: boolean) => void; @@ -305,4 +355,5 @@ export interface TimeBasedTimerLogics { >; setIsDone: Dispatch>; clearTimer: () => void; + denominator: number; } diff --git a/src/page/TimerPage/hooks/useTimerHotkey.ts b/src/page/TimerPage/hooks/useTimerHotkey.ts index cf066907..169f706d 100644 --- a/src/page/TimerPage/hooks/useTimerHotkey.ts +++ b/src/page/TimerPage/hooks/useTimerHotkey.ts @@ -50,11 +50,19 @@ export function useTimerHotkey(state: TimerPageLogics) { } // 찬/반 타이머 토글 (시작/정지) - const toggleTimer = (timer: typeof timer1 | typeof timer2) => { - if (timer.isRunning) { - timer.pauseTimer(); - } else { - timer.startTimer(); + const toggleTimeBasedTimer = () => { + if (prosConsSelected === 'PROS') { + if (timer1.isRunning) { + timer1.pauseTimer(); + } else { + timer1.startTimer(timer2.isDone); + } + } else if (prosConsSelected === 'CONS') { + if (timer2.isRunning) { + timer2.pauseTimer(); + } else { + timer2.startTimer(timer1.isDone); + } } }; @@ -68,11 +76,7 @@ export function useTimerHotkey(state: TimerPageLogics) { normalTimer.startTimer(); } } else { - if (prosConsSelected === 'PROS') { - toggleTimer(timer1); - } else if (prosConsSelected === 'CONS') { - toggleTimer(timer2); - } + toggleTimeBasedTimer(); } break; case 'KeyR': diff --git a/src/page/TimerPage/stories/TimeBasedTimer.stories.tsx b/src/page/TimerPage/stories/TimeBasedTimer.stories.tsx index e6ae3cf5..690f6378 100644 --- a/src/page/TimerPage/stories/TimeBasedTimer.stories.tsx +++ b/src/page/TimerPage/stories/TimeBasedTimer.stories.tsx @@ -10,6 +10,7 @@ const mockTimerInstance = { isDone: false, defaultTime: { defaultTotalTimer: 150, defaultSpeakingTimer: 50 }, isSpeakingTimer: true, + denominator: 1, startTimer: () => {}, pauseTimer: () => {}, resetTimerForNextPhase: () => {},