diff --git a/apps/frontend/src/hook/quizZone/useQuizZone.tsx b/apps/frontend/src/hook/quizZone/useQuizZone.tsx index cb75cfabc..770f0075f 100644 --- a/apps/frontend/src/hook/quizZone/useQuizZone.tsx +++ b/apps/frontend/src/hook/quizZone/useQuizZone.tsx @@ -215,7 +215,11 @@ export const chatMessagesReducer: Reducer = (chatMess * @returns {Function} .playQuiz - 퀴즈 상태를 플레이 모드로 변경하는 함수 */ -const useQuizZone = (quizZoneId: string, handleReconnect?: () => void) => { +const useQuizZone = ( + quizZoneId: string, + handleReconnect?: () => void, + handleClose?: () => void, +) => { const initialQuizZoneState: QuizZone = { stage: 'LOBBY', currentPlayer: { @@ -249,6 +253,7 @@ const useQuizZone = (quizZoneId: string, handleReconnect?: () => void) => { const handleFinish = () => { dispatch({ type: 'leave', payload: undefined }); + handleClose?.(); }; const wsUrl = `${import.meta.env.VITE_WS_URL}/play`; diff --git a/apps/frontend/src/hook/useTimer.ts b/apps/frontend/src/hook/useTimer.ts index 13ba22dd7..3b0c41880 100644 --- a/apps/frontend/src/hook/useTimer.ts +++ b/apps/frontend/src/hook/useTimer.ts @@ -1,4 +1,5 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import TimerWorker from '@/workers/timer.worker?worker'; interface TimerConfig { initialTime: number; @@ -7,67 +8,85 @@ interface TimerConfig { } /** - * 카운트다운 타이머를 관리하는 커스텀 훅입니다. - * - * @description - * 이 훅은 시작 제어 기능을 갖춘 카운트다운 타이머 기능을 제공합니다. - * 초기 시간과 타이머 완료 시 실행될 선택적 콜백을 받습니다. - * - * @example - * ```typescript - * const { time, start } = useTimer({ - * initialTime: 60, - * onComplete: () => console.log('타이머 완료!'), - * }); - * - * // 타이머 시작 - * start(); - * ``` + * Web Worker를 활용한 정확한 카운트다운 타이머 커스텀 훅 * * @param {TimerConfig} config - 타이머 설정 객체 - * @param {number} config.initialTime - 카운트다운 초기 시간(초 단위) - * @param {() => void} [config.onComplete] - 타이머 완료 시 실행될 선택적 콜백 - * - * @returns {object} 현재 시간과 시작 함수를 포함하는 객체 - * @returns {number} returns.time - 카운트다운의 현재 시간 - * @returns {() => void} returns.start - 타이머를 시작하는 함수 + * @returns {object} 타이머 상태와 컨트롤 함수들 */ - export const useTimer = ({ initialTime, onComplete }: TimerConfig) => { const [time, setTime] = useState(initialTime); const [isRunning, setIsRunning] = useState(false); + const workerRef = useRef(null); useEffect(() => { - let timer: NodeJS.Timeout | null = null; - - if (isRunning) { - timer = setInterval(() => { - setTime((prev) => { - const nextTime = prev - 0.1; - if (nextTime <= 0) { - setIsRunning(false); - onComplete?.(); - return 0; - } - return nextTime; - }); - }, 100); + // Worker가 이미 존재하면 종료 + if (workerRef.current) { + workerRef.current.terminate(); } - return () => { - if (timer) { - clearInterval(timer); + // 새 Worker 생성 + workerRef.current = new TimerWorker(); + + // Worker 메시지 핸들러 + workerRef.current.onmessage = (event) => { + const { type, payload } = event.data; + // console.log('Received from worker:', type, payload); // 디버깅용 + + switch (type) { + case 'TICK': + setTime(payload.time); + break; + case 'COMPLETE': + setTime(0); + setIsRunning(false); + onComplete?.(); + break; } }; - }, [isRunning, onComplete]); + // Clean up + return () => { + workerRef.current?.terminate(); + }; + }, []); + + // 타이머 시작 const start = useCallback(() => { - if (isRunning) return; + if (isRunning || !workerRef.current) return; + + workerRef.current.postMessage({ + type: 'START', + payload: { + duration: initialTime, + serverTime: Date.now(), + }, + }); + setIsRunning(true); + }, [isRunning, initialTime]); + + // 타이머 정지 + const stop = useCallback(() => { + if (!isRunning || !workerRef.current) return; + + workerRef.current.postMessage({ type: 'STOP' }); + setIsRunning(false); }, [isRunning]); + // 타이머 리셋 + const reset = useCallback(() => { + if (!workerRef.current) return; + + workerRef.current.postMessage({ type: 'RESET' }); + setTime(initialTime); + setIsRunning(false); + }, [initialTime]); + return { time, + isRunning, start, + stop, + reset, }; }; diff --git a/apps/frontend/src/pages/QuizZonePage.tsx b/apps/frontend/src/pages/QuizZonePage.tsx index cdd673b73..edbc3c384 100644 --- a/apps/frontend/src/pages/QuizZonePage.tsx +++ b/apps/frontend/src/pages/QuizZonePage.tsx @@ -15,6 +15,7 @@ import CustomAlertDialogContent from '@/components/common/CustomAlertDialogConte const QuizZoneContent = () => { const [isLoading, setIsLoading] = useState(true); const [isDisconnection, setIsDisconnection] = useState(false); + const [isClose, setIsClose] = useState(false); const navigate = useNavigate(); const { quizZoneId } = useParams(); @@ -29,8 +30,12 @@ const QuizZoneContent = () => { setIsDisconnection(true); }; + const closeHandler = () => { + setIsClose(true); + }; + const { initQuizZoneData, quizZoneState, submitQuiz, startQuiz, playQuiz, exitQuiz, sendChat } = - useQuizZone(quizZoneId, reconnectHandler); + useQuizZone(quizZoneId, reconnectHandler, closeHandler); const initQuizZone = async () => { try { @@ -127,6 +132,17 @@ const QuizZoneContent = () => { handleConfirm={() => initQuizZone()} /> + + setIsClose(false)} + confirmText={'나가기'} + handleConfirm={() => navigate('/')} + /> + ); };