Skip to content

Commit 803909f

Browse files
authored
Merge pull request #245 from boostcampwm-2024/develop
[Deploy] 방장 종료 구현 및 웹 워커 타이머 구현
2 parents 5085d12 + 9495fed commit 803909f

File tree

3 files changed

+85
-45
lines changed

3 files changed

+85
-45
lines changed

apps/frontend/src/hook/quizZone/useQuizZone.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,11 @@ export const chatMessagesReducer: Reducer<ChatMessage[], chatAction> = (chatMess
215215
* @returns {Function} .playQuiz - 퀴즈 상태를 플레이 모드로 변경하는 함수
216216
*/
217217

218-
const useQuizZone = (quizZoneId: string, handleReconnect?: () => void) => {
218+
const useQuizZone = (
219+
quizZoneId: string,
220+
handleReconnect?: () => void,
221+
handleClose?: () => void,
222+
) => {
219223
const initialQuizZoneState: QuizZone = {
220224
stage: 'LOBBY',
221225
currentPlayer: {
@@ -249,6 +253,7 @@ const useQuizZone = (quizZoneId: string, handleReconnect?: () => void) => {
249253

250254
const handleFinish = () => {
251255
dispatch({ type: 'leave', payload: undefined });
256+
handleClose?.();
252257
};
253258

254259
const wsUrl = `${import.meta.env.VITE_WS_URL}/play`;

apps/frontend/src/hook/useTimer.ts

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useState, useEffect, useCallback } from 'react';
1+
import { useCallback, useEffect, useRef, useState } from 'react';
2+
import TimerWorker from '@/workers/timer.worker?worker';
23

34
interface TimerConfig {
45
initialTime: number;
@@ -7,67 +8,85 @@ interface TimerConfig {
78
}
89

910
/**
10-
* 카운트다운 타이머를 관리하는 커스텀 훅입니다.
11-
*
12-
* @description
13-
* 이 훅은 시작 제어 기능을 갖춘 카운트다운 타이머 기능을 제공합니다.
14-
* 초기 시간과 타이머 완료 시 실행될 선택적 콜백을 받습니다.
15-
*
16-
* @example
17-
* ```typescript
18-
* const { time, start } = useTimer({
19-
* initialTime: 60,
20-
* onComplete: () => console.log('타이머 완료!'),
21-
* });
22-
*
23-
* // 타이머 시작
24-
* start();
25-
* ```
11+
* Web Worker를 활용한 정확한 카운트다운 타이머 커스텀 훅
2612
*
2713
* @param {TimerConfig} config - 타이머 설정 객체
28-
* @param {number} config.initialTime - 카운트다운 초기 시간(초 단위)
29-
* @param {() => void} [config.onComplete] - 타이머 완료 시 실행될 선택적 콜백
30-
*
31-
* @returns {object} 현재 시간과 시작 함수를 포함하는 객체
32-
* @returns {number} returns.time - 카운트다운의 현재 시간
33-
* @returns {() => void} returns.start - 타이머를 시작하는 함수
14+
* @returns {object} 타이머 상태와 컨트롤 함수들
3415
*/
35-
3616
export const useTimer = ({ initialTime, onComplete }: TimerConfig) => {
3717
const [time, setTime] = useState(initialTime);
3818
const [isRunning, setIsRunning] = useState(false);
19+
const workerRef = useRef<Worker | null>(null);
3920

4021
useEffect(() => {
41-
let timer: NodeJS.Timeout | null = null;
42-
43-
if (isRunning) {
44-
timer = setInterval(() => {
45-
setTime((prev) => {
46-
const nextTime = prev - 0.1;
47-
if (nextTime <= 0) {
48-
setIsRunning(false);
49-
onComplete?.();
50-
return 0;
51-
}
52-
return nextTime;
53-
});
54-
}, 100);
22+
// Worker가 이미 존재하면 종료
23+
if (workerRef.current) {
24+
workerRef.current.terminate();
5525
}
5626

57-
return () => {
58-
if (timer) {
59-
clearInterval(timer);
27+
// 새 Worker 생성
28+
workerRef.current = new TimerWorker();
29+
30+
// Worker 메시지 핸들러
31+
workerRef.current.onmessage = (event) => {
32+
const { type, payload } = event.data;
33+
// console.log('Received from worker:', type, payload); // 디버깅용
34+
35+
switch (type) {
36+
case 'TICK':
37+
setTime(payload.time);
38+
break;
39+
case 'COMPLETE':
40+
setTime(0);
41+
setIsRunning(false);
42+
onComplete?.();
43+
break;
6044
}
6145
};
62-
}, [isRunning, onComplete]);
6346

47+
// Clean up
48+
return () => {
49+
workerRef.current?.terminate();
50+
};
51+
}, []);
52+
53+
// 타이머 시작
6454
const start = useCallback(() => {
65-
if (isRunning) return;
55+
if (isRunning || !workerRef.current) return;
56+
57+
workerRef.current.postMessage({
58+
type: 'START',
59+
payload: {
60+
duration: initialTime,
61+
serverTime: Date.now(),
62+
},
63+
});
64+
6665
setIsRunning(true);
66+
}, [isRunning, initialTime]);
67+
68+
// 타이머 정지
69+
const stop = useCallback(() => {
70+
if (!isRunning || !workerRef.current) return;
71+
72+
workerRef.current.postMessage({ type: 'STOP' });
73+
setIsRunning(false);
6774
}, [isRunning]);
6875

76+
// 타이머 리셋
77+
const reset = useCallback(() => {
78+
if (!workerRef.current) return;
79+
80+
workerRef.current.postMessage({ type: 'RESET' });
81+
setTime(initialTime);
82+
setIsRunning(false);
83+
}, [initialTime]);
84+
6985
return {
7086
time,
87+
isRunning,
7188
start,
89+
stop,
90+
reset,
7291
};
7392
};

apps/frontend/src/pages/QuizZonePage.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import CustomAlertDialogContent from '@/components/common/CustomAlertDialogConte
1515
const QuizZoneContent = () => {
1616
const [isLoading, setIsLoading] = useState(true);
1717
const [isDisconnection, setIsDisconnection] = useState(false);
18+
const [isClose, setIsClose] = useState(false);
1819

1920
const navigate = useNavigate();
2021
const { quizZoneId } = useParams();
@@ -29,8 +30,12 @@ const QuizZoneContent = () => {
2930
setIsDisconnection(true);
3031
};
3132

33+
const closeHandler = () => {
34+
setIsClose(true);
35+
};
36+
3237
const { initQuizZoneData, quizZoneState, submitQuiz, startQuiz, playQuiz, exitQuiz, sendChat } =
33-
useQuizZone(quizZoneId, reconnectHandler);
38+
useQuizZone(quizZoneId, reconnectHandler, closeHandler);
3439

3540
const initQuizZone = async () => {
3641
try {
@@ -127,6 +132,17 @@ const QuizZoneContent = () => {
127132
handleConfirm={() => initQuizZone()}
128133
/>
129134
</AlertDialog>
135+
<AlertDialog open={isClose}>
136+
<CustomAlertDialogContent
137+
title={'퀴즈존 종료'}
138+
description={'방장이 퀴즈존을 떠나 퀴즈존이 삭제되었습니다.'}
139+
type={'info'}
140+
cancelText={'취소'}
141+
handleCancel={() => setIsClose(false)}
142+
confirmText={'나가기'}
143+
handleConfirm={() => navigate('/')}
144+
/>
145+
</AlertDialog>
130146
</div>
131147
);
132148
};

0 commit comments

Comments
 (0)