Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/frontend/src/hook/quizZone/useQuizZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,11 @@ export const chatMessagesReducer: Reducer<ChatMessage[], chatAction> = (chatMess
* @returns {Function} .playQuiz - 퀴즈 상태를 플레이 모드로 변경하는 함수
*/

const useQuizZone = (quizZoneId: string, handleReconnect?: () => void) => {
const useQuizZone = (
quizZoneId: string,
handleReconnect?: () => void,
handleClose?: () => void,
) => {
const initialQuizZoneState: QuizZone = {
stage: 'LOBBY',
currentPlayer: {
Expand Down Expand Up @@ -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`;
Expand Down
105 changes: 62 additions & 43 deletions apps/frontend/src/hook/useTimer.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Worker | null>(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,
};
};
18 changes: 17 additions & 1 deletion apps/frontend/src/pages/QuizZonePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 {
Expand Down Expand Up @@ -127,6 +132,17 @@ const QuizZoneContent = () => {
handleConfirm={() => initQuizZone()}
/>
</AlertDialog>
<AlertDialog open={isClose}>
<CustomAlertDialogContent
title={'퀴즈존 종료'}
description={'방장이 퀴즈존을 떠나 퀴즈존이 삭제되었습니다.'}
type={'info'}
cancelText={'취소'}
handleCancel={() => setIsClose(false)}
confirmText={'나가기'}
handleConfirm={() => navigate('/')}
/>
</AlertDialog>
</div>
);
};
Expand Down
Loading