From ba87e0b2b25725564c273830d3d1a384e9c5ebe8 Mon Sep 17 00:00:00 2001 From: mario Date: Wed, 4 Dec 2024 11:13:47 +0900 Subject: [PATCH 01/38] =?UTF-8?q?fix:=20=ED=98=B8=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9D=98=20=EB=8C=80=EA=B8=B0=EC=8B=A4=20=ED=87=B4=EC=9E=A5=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EA=B4=80=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/play/play.gateway.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/backend/src/play/play.gateway.ts b/apps/backend/src/play/play.gateway.ts index d83309b3e..5cf7a7c39 100644 --- a/apps/backend/src/play/play.gateway.ts +++ b/apps/backend/src/play/play.gateway.ts @@ -15,11 +15,10 @@ import { SendEventMessage } from './entities/send-event.entity'; import { ClientInfo } from './entities/client-info.entity'; import { WebSocketWithSession } from '../core/SessionWsAdapter'; import { RuntimeException } from '@nestjs/core/errors/exceptions'; -import { CLOSE_CODE } from '../common/constants'; import { SubmitResponseDto } from './dto/submit-response.dto'; -import { clearTimeout } from 'node:timers'; import { ChatMessage } from 'src/chat/entities/chat-message.entity'; -import { ChatService } from '../chat/chat.service'; // 경로 수정 +import { ChatService } from '../chat/chat.service'; +import { CLOSE_CODE } from '../common/constants'; // 경로 수정 /** * 퀴즈 게임에 대한 WebSocket 연결을 관리하는 Gateway입니다. @@ -248,7 +247,7 @@ export class PlayGateway implements OnGatewayInit { const clientsIds = summaries.map(({ id }) => id); - this.clearQuizZone(clientsIds, quizZoneId, endSocketTime); + this.clearQuizZone(clientsIds, quizZoneId, endSocketTime - Date.now()); } /** @@ -258,16 +257,16 @@ export class PlayGateway implements OnGatewayInit { * - 일반 플레이어가 나가면 퀴즈 존에서 나가고 다른 플레이어에게 나갔다고 알립니다. * @param clientIds - 퀴즈존에 참여하고 있는 클라이언트 id 리스트 * @param quizZoneId - 퀴즈가 끝난 퀴즈존 id - * @param endSocketTime - 소켓 연결 종료 시간 종료 시간 + * @param time - 소켓 연결 종료 시간 종료 시간 */ - private clearQuizZone(clientIds: string[], quizZoneId: string, endSocketTime: number) { + private clearQuizZone(clientIds: string[], quizZoneId: string, time: number) { setTimeout(() => { clientIds.forEach((id) => { this.clearClient(id, 'finish'); }); this.playService.clearQuizZone(quizZoneId); this.chatService.delete(quizZoneId); - }, endSocketTime - Date.now()); + }, time); } /** @@ -286,7 +285,7 @@ export class PlayGateway implements OnGatewayInit { if (isHost) { this.broadcast(playerIds, 'close'); - playerIds.forEach((id) => this.clearClient(id, 'Host leave.')); + this.clearQuizZone(playerIds, quizZoneId, 0); } else { this.broadcast(playerIds, 'someone_leave', clientId); this.clearClient(clientId, 'Client leave'); From ffb313d61a6f00186f29db9cbd283f545fe43ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=ED=98=84=EB=AF=BC?= <77275989+joyjhm@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:35:11 +0900 Subject: [PATCH 02/38] =?UTF-8?q?fix:=20=EA=B2=B0=EA=B3=BC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=ED=95=B4=EB=8F=84=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=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 --- apps/backend/src/play/entities/quiz-summary.entity.ts | 6 ++++++ apps/backend/src/play/entities/rank.entity.ts | 6 ++++++ apps/backend/src/play/play.service.ts | 6 +++++- apps/backend/src/quiz-zone/dto/find-quiz-zone.dto.ts | 8 ++++++++ .../src/quiz-zone/entities/quiz-zone.entity.ts | 2 ++ apps/backend/src/quiz-zone/quiz-zone.service.ts | 10 +++++++--- apps/frontend/src/hook/quizZone/useQuizZone.tsx | 11 ++--------- 7 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 apps/backend/src/play/entities/quiz-summary.entity.ts create mode 100644 apps/backend/src/play/entities/rank.entity.ts diff --git a/apps/backend/src/play/entities/quiz-summary.entity.ts b/apps/backend/src/play/entities/quiz-summary.entity.ts new file mode 100644 index 000000000..1edc0de4d --- /dev/null +++ b/apps/backend/src/play/entities/quiz-summary.entity.ts @@ -0,0 +1,6 @@ +import { Rank } from './rank.entity'; + +export interface QuizSummary { + readonly ranks: Rank[]; + readonly endSocketTime?: number; +} \ No newline at end of file diff --git a/apps/backend/src/play/entities/rank.entity.ts b/apps/backend/src/play/entities/rank.entity.ts new file mode 100644 index 000000000..7854840df --- /dev/null +++ b/apps/backend/src/play/entities/rank.entity.ts @@ -0,0 +1,6 @@ +export interface Rank { + readonly id: string; + readonly nickname: string; + readonly score: number; + readonly ranking: number; +} \ No newline at end of file diff --git a/apps/backend/src/play/play.service.ts b/apps/backend/src/play/play.service.ts index 2badf6b3a..fd033c338 100644 --- a/apps/backend/src/play/play.service.ts +++ b/apps/backend/src/play/play.service.ts @@ -304,7 +304,8 @@ export class PlayService { const now = Date.now(); const endSocketTime = now + socketConnectTime; - return [...players.values()].map(({ id, score, submits }) => ({ + + const summaries = [...players.values()].map(({ id, score, submits }) => ({ id, score, submits, @@ -312,6 +313,9 @@ export class PlayService { ranks, endSocketTime })); + + quizZone.summaries = {ranks, endSocketTime}; + return summaries; } public clearQuizZone(quizZoneId: string) { diff --git a/apps/backend/src/quiz-zone/dto/find-quiz-zone.dto.ts b/apps/backend/src/quiz-zone/dto/find-quiz-zone.dto.ts index c5f708f85..37e3b3da1 100644 --- a/apps/backend/src/quiz-zone/dto/find-quiz-zone.dto.ts +++ b/apps/backend/src/quiz-zone/dto/find-quiz-zone.dto.ts @@ -2,6 +2,8 @@ import { PLAYER_STATE, QUIZ_ZONE_STAGE } from '../../common/constants'; import { CurrentQuizDto } from '../../play/dto/current-quiz.dto'; import { SubmittedQuiz } from '../entities/submitted-quiz.entity'; import { ChatMessage } from 'src/chat/entities/chat-message.entity'; +import { Rank } from '../../play/entities/rank.entity'; +import { Quiz } from '../entities/quiz.entity'; /** * 퀴즈 게임에 참여하는 플레이어 엔티티 @@ -42,4 +44,10 @@ export interface FindQuizZoneDto { readonly currentQuiz?: CurrentQuizDto; readonly maxPlayers?: number; readonly chatMessages?: ChatMessage[]; + + readonly ranks?: Rank[]; + readonly endSocketTime?: number; + readonly score?: number; + readonly quizzes?: Quiz[]; + readonly submits?: SubmittedQuiz[]; } diff --git a/apps/backend/src/quiz-zone/entities/quiz-zone.entity.ts b/apps/backend/src/quiz-zone/entities/quiz-zone.entity.ts index 59c762e5e..4e6bbbffc 100644 --- a/apps/backend/src/quiz-zone/entities/quiz-zone.entity.ts +++ b/apps/backend/src/quiz-zone/entities/quiz-zone.entity.ts @@ -1,6 +1,7 @@ import { Quiz } from './quiz.entity'; import { Player } from './player.entity'; import { QUIZ_ZONE_STAGE } from '../../common/constants'; +import { QuizSummary } from '../../play/entities/quiz-summary.entity'; /** * 퀴즈 게임을 진행하는 공간을 나타내는 퀴즈존 인터페이스 * @@ -28,4 +29,5 @@ export interface QuizZone { currentQuizStartTime: number; currentQuizDeadlineTime: number; intervalTime: number; + summaries?: QuizSummary; } diff --git a/apps/backend/src/quiz-zone/quiz-zone.service.ts b/apps/backend/src/quiz-zone/quiz-zone.service.ts index 135b6914f..d2aeda3e2 100644 --- a/apps/backend/src/quiz-zone/quiz-zone.service.ts +++ b/apps/backend/src/quiz-zone/quiz-zone.service.ts @@ -172,7 +172,7 @@ export class QuizZoneService { } private async getResultInfo(clientId: string, quizZoneId: string): Promise { - const { players, stage, title, description, hostId, quizzes, maxPlayers } = + const { players, stage, title, description, hostId, quizzes, summaries } = await this.findOne(quizZoneId); const { id, nickname, state, submits, score } = players.get(clientId); const chatMessages = await this.chatService.get(quizZoneId); @@ -181,11 +181,15 @@ export class QuizZoneService { currentPlayer: { id, nickname, state, score, submits }, title, description, - maxPlayers: maxPlayers, quizCount: quizzes.length, stage: stage, hostId, - chatMessages: chatMessages, + chatMessages, + ranks: summaries.ranks, + endSocketTime: summaries.endSocketTime, + quizzes, + score, + submits }; } diff --git a/apps/frontend/src/hook/quizZone/useQuizZone.tsx b/apps/frontend/src/hook/quizZone/useQuizZone.tsx index 3a18a2a34..a475197e9 100644 --- a/apps/frontend/src/hook/quizZone/useQuizZone.tsx +++ b/apps/frontend/src/hook/quizZone/useQuizZone.tsx @@ -39,15 +39,10 @@ const quizZoneReducer: Reducer = (state, action) => { switch (type) { case 'init': + console.log(payload); return { ...state, - stage: payload.stage, - title: payload.title, - description: payload.description, - quizCount: payload.quizCount, - hostId: payload.hostId, - currentPlayer: payload.currentPlayer, - chatMessages: payload.chatMessages, + ...payload, currentQuiz: payload.currentQuiz !== undefined ? { @@ -55,8 +50,6 @@ const quizZoneReducer: Reducer = (state, action) => { question: atob(payload.currentQuiz?.question ?? ''), } : undefined, - maxPlayers: payload.maxPlayers, - players: [], }; case 'join': return { ...state, players: payload }; From fd98cf112222687a02b8aa4a27af180336dfce18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=ED=98=84=EB=AF=BC?= <77275989+joyjhm@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:31:41 +0900 Subject: [PATCH 03/38] =?UTF-8?q?test:=20result=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EA=B0=92=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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/quiz-zone/quiz-zone.service.spec.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts b/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts index a2e8b9281..847367989 100644 --- a/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts +++ b/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts @@ -6,7 +6,6 @@ import { IQuizZoneRepository } from './repository/quiz-zone.repository.interface import { Quiz } from './entities/quiz.entity'; import { PLAYER_STATE, QUIZ_TYPE, QUIZ_ZONE_STAGE } from '../common/constants'; import { QuizService } from '../quiz/quiz.service'; -import { max } from 'class-validator'; import { ChatService } from '../chat/chat.service'; const nickNames: string[] = [ @@ -571,6 +570,13 @@ describe('QuizZoneService', () => { currentQuizStartTime: Date.now(), currentQuizDeadlineTime: Date.now() + playTime, intervalTime: 5000, + summaries: { + ranks: [ + {id: "player1", nickname: "미친투사", score: 0, ranking: 1}, + {id: "player2", nickname: "미친투사", score: 0, ranking: 1} + ], + endSocketTime: Date.now(), + } }; mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); @@ -579,7 +585,7 @@ describe('QuizZoneService', () => { const result = await service.getQuizZoneInfo(clientId, quizZoneId); // then - expect(result).toEqual({ + expect(result).toMatchObject({ currentPlayer: { id: clientId, nickname: '닉네임', @@ -591,8 +597,12 @@ describe('QuizZoneService', () => { description: '테스트 퀴즈입니다', quizCount: quizzes.length, stage: QUIZ_ZONE_STAGE.RESULT, - maxPlayers: 10, hostId: 'adminId', + ranks: [ + {id: "player1", nickname: "미친투사", score: 0, ranking: 1}, + {id: "player2", nickname: "미친투사", score: 0, ranking: 1} + ], + endSocketTime: Date.now(), }); }); From 8b42ef4c3b3bcc9139002a9ad5dd1682e2157b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=A0=EB=B9=88?= Date: Wed, 4 Dec 2024 19:45:11 +0900 Subject: [PATCH 04/38] =?UTF-8?q?fix:=20=ED=80=B4=EC=A6=88=EC=A1=B4=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=B5=9C=EB=8C=80=20=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=EB=A5=BC=20500=EC=9E=90=EC=97=90=EC=84=9C=20300=EC=9E=90?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx | 4 ++-- apps/frontend/src/components/common/Input.tsx | 2 +- apps/frontend/src/hook/quizZone/useQuizZone.tsx | 1 - apps/frontend/src/utils/validators.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx b/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx index b433f259d..8df873058 100644 --- a/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx +++ b/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx @@ -129,9 +129,9 @@ const CreateQuizZoneBasic = ({ onChange={(e) => handleChangeQuizZoneBasic(e, 'DESC')} isBorder={true} placeholder="퀴즈존 설명을 입력하세요" - error={validationError == '500자 이하로 입력해주세요.' ? validationError : ''} + error={validationError == '300자 이하로 입력해주세요.' ? validationError : ''} isShowCount={true} - max={500} + max={300} /> diff --git a/apps/frontend/src/components/common/Input.tsx b/apps/frontend/src/components/common/Input.tsx index a9e0e137e..7c96b645d 100644 --- a/apps/frontend/src/components/common/Input.tsx +++ b/apps/frontend/src/components/common/Input.tsx @@ -148,7 +148,7 @@ const Input = forwardRef( )} {typeof value === 'string' && isShowCount && ( -
+
= (state, action) => { switch (type) { case 'init': - console.log(payload); return { ...state, ...payload, diff --git a/apps/frontend/src/utils/validators.ts b/apps/frontend/src/utils/validators.ts index fd7e4724f..8397f4c72 100644 --- a/apps/frontend/src/utils/validators.ts +++ b/apps/frontend/src/utils/validators.ts @@ -32,7 +32,7 @@ export const validateQuizZoneSetName = (name: string) => { //퀴즈존 설명 유효성 검사 export const validateQuizZoneSetDescription = (description: string) => { - if (description.length > 500) return '500자 이하로 입력해주세요.'; + if (description.length > 300) return '300자 이하로 입력해주세요.'; }; //퀴즈존 입장 코드 유효성 검사 From d338537e89730b133e4ab668915be694b0236898 Mon Sep 17 00:00:00 2001 From: mario Date: Thu, 5 Dec 2024 10:41:12 +0900 Subject: [PATCH 05/38] =?UTF-8?q?fix:=20quizZoneReducer=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/hook/quizZone/useQuizZone.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/apps/frontend/src/hook/quizZone/useQuizZone.tsx b/apps/frontend/src/hook/quizZone/useQuizZone.tsx index d15f14429..530bc1be0 100644 --- a/apps/frontend/src/hook/quizZone/useQuizZone.tsx +++ b/apps/frontend/src/hook/quizZone/useQuizZone.tsx @@ -76,13 +76,11 @@ const quizZoneReducer: Reducer = (state, action) => { ...state.currentPlayer, state: 'SUBMIT', }, - chatMessages: payload.chatMessages, currentQuizResult: { + ...payload, fastestPlayers: payload.fastestPlayerIds .map((id) => state.players?.find((p) => p.id === id)) .filter((p) => !!p), - submittedCount: payload.submittedCount, - totalPlayerCount: payload.totalPlayerCount, }, }; case 'someone_submit': @@ -110,11 +108,8 @@ const quizZoneReducer: Reducer = (state, action) => { }, currentQuiz: { ...state.currentQuiz, + ...nextQuiz, question: atob(nextQuiz.question), - currentIndex: nextQuiz.currentIndex, - playTime: nextQuiz.playTime, - startTime: nextQuiz.startTime, - deadlineTime: nextQuiz.deadlineTime, quizType: 'SHORT', }, currentQuizResult: { @@ -149,12 +144,8 @@ const quizZoneReducer: Reducer = (state, action) => { case 'summary': return { ...state, + ...payload, stage: 'RESULT', - score: payload.score, - submits: payload.submits, - quizzes: payload.quizzes, - ranks: payload.ranks, - endSocketTime: payload.endSocketTime, }; case 'chat': return { From b5a1af2641e8523a854b99c0f155738ae36e1504 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 13:29:41 +0900 Subject: [PATCH 06/38] =?UTF-8?q?feat:=20TypeScript=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=9F=AC=20=EC=84=A4=EC=A0=95=EC=97=90=20=EC=9B=B9=20?= =?UTF-8?q?=EC=9B=8C=EC=BB=A4=20=EC=A7=80=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compilerOptions에 lib 배열 추가 - webworker: 웹 워커 API 타입 지원 - es2015: ES2015(ES6) 기능 지원 --- apps/frontend/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json index 75ade8bf1..5bd43f4db 100644 --- a/apps/frontend/tsconfig.json +++ b/apps/frontend/tsconfig.json @@ -6,7 +6,8 @@ "paths": { "@/*": ["./src/*"] }, - "types": ["vitest/globals", "@testing-library/jest-dom"] + "types": ["vitest/globals", "@testing-library/jest-dom"], + "lib": ["webworker", "es2015"] }, "files": [], "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] From a78f6f9591bc6aa8a5aa27a1256142336d7c31f0 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 13:30:20 +0900 Subject: [PATCH 07/38] =?UTF-8?q?feat:=20Vite=20=EC=9B=B9=20=EC=9B=8C?= =?UTF-8?q?=EC=BB=A4=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - worker 설정 추가 - format을 'es'로 지정하여 ES 모듈 형식의 웹 워커 지원 --- apps/frontend/vite.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 887d59784..d3d322823 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -7,6 +7,9 @@ export default defineConfig(({ mode }) => { return { plugins: [react()], + worker: { + format: 'es', + }, resolve: { alias: { '@': path.resolve(__dirname, './src'), From 031bdd4aa254745dc69cad1354772d167a3b2483 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 13:32:07 +0900 Subject: [PATCH 08/38] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=9B=B9=20=EC=9B=8C=EC=BB=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타이머 기능을 웹 워커로 구현하여 메인 스레드와 분리 - 주요 기능: - 타이머 시작/정지/리셋 기능 구현 - 서버 시간 동기화 지원 - 타이머 일시정지 및 재개 기능 - 0.1초 단위 정확도로 시간 계산 --- apps/frontend/src/workers/timer.worker.ts | 94 +++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 apps/frontend/src/workers/timer.worker.ts diff --git a/apps/frontend/src/workers/timer.worker.ts b/apps/frontend/src/workers/timer.worker.ts new file mode 100644 index 000000000..32ede9bcb --- /dev/null +++ b/apps/frontend/src/workers/timer.worker.ts @@ -0,0 +1,94 @@ +import { TimerMessage, TimerResponse } from '@/types/timer.types'; + +class TimerWorker { + private timerId: ReturnType | null = null; + private startTime: number | null = null; + private duration: number | null = null; + private timeOffset: number = 0; + private pausedTimeRemaining: number | null = null; + + constructor() { + self.onmessage = this.handleMessage.bind(this); + } + + private handleMessage(event: MessageEvent) { + const { type, payload } = event.data; + + switch (type) { + case 'START': + if (!payload?.duration) return; + + if (this.pausedTimeRemaining !== null) { + this.startTimer(this.pausedTimeRemaining); + this.pausedTimeRemaining = null; + } else { + if (payload.serverTime) { + this.timeOffset = Date.now() - payload.serverTime; + } + this.startTimer(payload.duration); + } + break; + + case 'STOP': + if (this.timerId !== null && this.startTime !== null && this.duration !== null) { + const currentTime = Date.now() - this.timeOffset; + const elapsed = (currentTime - this.startTime) / 1000; + this.pausedTimeRemaining = Math.max(0, this.duration - elapsed); + } + this.stopTimer(); + break; + + case 'RESET': + this.resetTimer(); + break; + } + } + + private startTimer(duration: number) { + this.stopTimer(); // 기존 타이머가 있다면 정리 + + this.duration = duration; + this.startTime = Date.now() - this.timeOffset; + + // setInterval 직접 사용 + this.timerId = setInterval(() => { + if (!this.startTime || !this.duration) return; + + const currentTime = Date.now() - this.timeOffset; + const elapsed = (currentTime - this.startTime) / 1000; + const remaining = Math.max(0, this.duration - elapsed); + const roundedRemaining = Math.round(remaining * 10) / 10; + + if (roundedRemaining <= 0) { + this.postMessage({ type: 'COMPLETE' }); + this.stopTimer(); + } else { + this.postMessage({ + type: 'TICK', + payload: { time: roundedRemaining }, + }); + } + }, 100); + } + + private stopTimer() { + if (this.timerId !== null) { + clearInterval(this.timerId); + this.timerId = null; + } + } + + private resetTimer() { + this.stopTimer(); + this.startTime = null; + this.duration = null; + this.timeOffset = 0; + this.pausedTimeRemaining = null; + } + + private postMessage(message: TimerResponse) { + self.postMessage(message); + } +} + +new TimerWorker(); From ba846447af5a01dc3600859efef71680000c843f Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 13:32:27 +0900 Subject: [PATCH 09/38] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=9B=B9=20=EC=9B=8C=EC=BB=A4=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TimerMessage 인터페이스 추가 - START, STOP, RESET 메시지 타입 정의 - duration, serverTime 페이로드 정의 - TimerResponse 인터페이스 추가 - TICK, COMPLETE 응답 타입 정의 - time 페이로드 정의 --- apps/frontend/src/types/timer.types.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/frontend/src/types/timer.types.ts diff --git a/apps/frontend/src/types/timer.types.ts b/apps/frontend/src/types/timer.types.ts new file mode 100644 index 000000000..63ad825b3 --- /dev/null +++ b/apps/frontend/src/types/timer.types.ts @@ -0,0 +1,14 @@ +export interface TimerMessage { + type: 'START' | 'STOP' | 'RESET'; + payload?: { + duration: number; + serverTime?: number; + }; +} + +export interface TimerResponse { + type: 'TICK' | 'COMPLETE'; + payload?: { + time: number; + }; +} From 3c19988738b55902030a1d4fc9e757b3f3923616 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 13:36:17 +0900 Subject: [PATCH 10/38] =?UTF-8?q?refactor:=20=ED=83=80=EC=9D=B4=EB=A8=B8?= =?UTF-8?q?=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=9B=B9=20?= =?UTF-8?q?=EC=9B=8C=EC=BB=A4=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setInterval 방식에서 웹 워커 기반으로 변경하여 정확도 개선 - 새로운 기능 추가: - 정지(stop) 기능 추가 - 리셋(reset) 기능 추가 - isRunning 상태 제공 - 서버 시간 동기화 지원 - 웹 워커 생명주기 관리 로직 추가 - 컴포넌트 언마운트 시 워커 정리 - 중복 워커 생성 방지 --- apps/frontend/src/hook/useTimer.ts | 106 +++++++++++++++++------------ 1 file changed, 63 insertions(+), 43 deletions(-) diff --git a/apps/frontend/src/hook/useTimer.ts b/apps/frontend/src/hook/useTimer.ts index 13ba22dd7..4f7e248a3 100644 --- a/apps/frontend/src/hook/useTimer.ts +++ b/apps/frontend/src/hook/useTimer.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; interface TimerConfig { initialTime: number; @@ -7,67 +7,87 @@ 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 Worker(new URL('../workers/timer.worker.ts', import.meta.url), { + type: 'module', + }); + + // 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, }; }; From 212222d214f96742795225eda4632b237b57d787 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 13:37:21 +0900 Subject: [PATCH 11/38] =?UTF-8?q?refactor:=20=ED=80=B4=EC=A6=88=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=ED=83=80=EC=9D=B4=EB=A8=B8=20=ED=9B=85=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useTimer 훅을 사용하여 타이머 기능 구현 - 컴포넌트 타입 개선 - currentQuiz 타입을 CurrentQuiz로 명시 - 불필요한 playTime prop 제거 - ProgressBar 컴포넌트 props 변경 - deadlineTime을 time과 playTime으로 분리 - 타이머 자동 시작 기능 추가 --- .../src/blocks/QuizZone/QuizInProgress.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx b/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx index 1d0ab98ba..7c5d726a1 100644 --- a/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx +++ b/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx @@ -3,11 +3,13 @@ import ContentBox from '@/components/common/ContentBox'; import Input from '@/components/common/Input'; import ProgressBar from '@/components/common/ProgressBar'; import Typography from '@/components/common/Typogrpahy'; -import { useState } from 'react'; +import { useTimer } from '@/hook/useTimer'; +import { CurrentQuiz } from '@/types/quizZone.types'; +import { useEffect, useState } from 'react'; interface QuizInProgressProps { playTime: number | null; - currentQuiz: any; + currentQuiz: CurrentQuiz; submitAnswer: (e: any) => void; } @@ -17,6 +19,17 @@ const QuizInProgress = ({ currentQuiz, submitAnswer }: QuizInProgressProps) => { const MAX_TEXT_LENGTH = 100; const MIN_TEXT_LENGTH = 1; + const playTime = currentQuiz.playTime; + + const { start, time } = useTimer({ + initialTime: playTime / 1000, + onComplete: () => {}, + }); + + useEffect(() => { + start(); + }, []); + const handleSubmitAnswer = () => { if (answer.length >= MIN_TEXT_LENGTH && answer.length <= MAX_TEXT_LENGTH) { submitAnswer(answer); @@ -25,7 +38,7 @@ const QuizInProgress = ({ currentQuiz, submitAnswer }: QuizInProgressProps) => { return (
- {}} /> + {}} /> Date: Thu, 5 Dec 2024 13:38:08 +0900 Subject: [PATCH 12/38] =?UTF-8?q?refactor:=20ProgressBar=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=8B=A8=EC=88=9C=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=ED=83=80=EC=9D=B4=EB=A8=B8=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컴포넌트 인터페이스 변경 - deadlineTime prop을 playTime과 time으로 분리 - 타이머 훅과 직접 연동되도록 개선 - 내부 로직 단순화 - 상태 관리 로직 제거 (타이머 훅으로 이관) - setInterval 로직 제거 - 불필요한 refs 제거 - 진행도 계산 로직 개선 - 밀리초 단위 변환 적용 - 최소/최대값 범위 보정 --- .../src/components/common/ProgressBar.tsx | 74 ++++--------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/apps/frontend/src/components/common/ProgressBar.tsx b/apps/frontend/src/components/common/ProgressBar.tsx index 6833349b7..6d5e002ee 100644 --- a/apps/frontend/src/components/common/ProgressBar.tsx +++ b/apps/frontend/src/components/common/ProgressBar.tsx @@ -1,83 +1,39 @@ -import { Progress } from '../ui/progress'; -import { useState, useEffect, useRef } from 'react'; +import { Progress } from '@/components/ui/progress'; import Typography from './Typogrpahy'; export interface ProgressBarProps { - deadlineTime: number; // ISO string or Date object + playTime: number; + time: number; onTimeEnd?: () => void; } /** * @description - * ProgressBar 컴포넌트는 주어진 마감 시간까지 남은 시간을 시각적으로 표시합니다. - * 현재 시간과 마감 시간 사이의 진행도를 보여주며, 시간이 다 되면 콜백을 실행합니다. + * ProgressBar 컴포넌트는 주어진 플레이 시간과 현재 시간을 기반으로 진행도를 시각적으로 표시합니다. * * @example * console.log('Time ended!')} * /> */ -const ProgressBar = ({ deadlineTime, onTimeEnd }: ProgressBarProps) => { - const totalDurationRef = useRef(deadlineTime - Date.now()); +const ProgressBar = ({ playTime, time, onTimeEnd }: ProgressBarProps) => { + // 진행도 계산 (남은 시간 / 전체 시간 * 100) + const progress = Math.max(0, Math.min(100, (time / playTime) * 100000)); - const calculateInitialProgress = () => { - const now = Date.now(); - const totalDuration = totalDurationRef.current; - const remaining = deadlineTime - now; - const progress = Math.max(0, (remaining / totalDuration) * 100); - return progress; - }; + // 시간이 다 되었을 때 콜백 실행 + if (time <= 0) { + onTimeEnd?.(); + } - const calculateInitialSeconds = () => { - const now = Date.now(); - return Math.max(0, (deadlineTime - now) / 1000); - }; - - const [progress, setProgress] = useState(calculateInitialProgress()); - const [remainingSeconds, setRemainingSeconds] = useState(calculateInitialSeconds()); - - useEffect(() => { - const startTime = Date.now(); - const totalDuration = totalDurationRef.current; - - // deadlineTime이 이미 지난 경우 - if (deadlineTime <= startTime) { - setProgress(0); - setRemainingSeconds(0); - onTimeEnd?.(); - return; - } - - const updateProgress = () => { - const currentTime = Date.now(); - const remaining = deadlineTime - currentTime; - - if (remaining <= 0) { - setProgress(0); - setRemainingSeconds(0); - onTimeEnd?.(); - return false; - } - - // totalDurationRef.current를 사용하여 일정한 비율로 감소 - const newProgress = (remaining / totalDuration) * 100; - setProgress(Math.max(0, Math.min(100, newProgress))); - setRemainingSeconds(remaining / 1000); - - return true; - }; - - const interval = setInterval(updateProgress, 100); - - return () => clearInterval(interval); - }, [deadlineTime, onTimeEnd]); + console.log(progress); return (
Date: Thu, 5 Dec 2024 14:52:56 +0900 Subject: [PATCH 13/38] =?UTF-8?q?feat:=20quizZone=20Controller=20respsonse?= =?UTF-8?q?=20serverTime=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/quiz-zone/quiz-zone.controller.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/quiz-zone/quiz-zone.controller.ts b/apps/backend/src/quiz-zone/quiz-zone.controller.ts index 415ea4e91..74be4d91d 100644 --- a/apps/backend/src/quiz-zone/quiz-zone.controller.ts +++ b/apps/backend/src/quiz-zone/quiz-zone.controller.ts @@ -68,12 +68,16 @@ export class QuizZoneController { @Session() session: Record, @Param('quizZoneId') quizZoneId: string, ) { + const serverTime = Date.now(); const quizZoneInfo = this.quizZoneService.getQuizZoneInfo( session.id, quizZoneId, session.quizZoneId, ); session['quizZoneId'] = quizZoneId; - return quizZoneInfo; + return { + ...quizZoneInfo, + serverTime, + }; } } From 721ffd73be837e7c88c21657c634ed92070253d3 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 14:53:20 +0900 Subject: [PATCH 14/38] =?UTF-8?q?feat:=20QuizZone=20Type=20serverTime,=20o?= =?UTF-8?q?ffset=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/types/quizZone.types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/frontend/src/types/quizZone.types.ts b/apps/frontend/src/types/quizZone.types.ts index aeb13ec32..c55c2c08a 100644 --- a/apps/frontend/src/types/quizZone.types.ts +++ b/apps/frontend/src/types/quizZone.types.ts @@ -27,6 +27,8 @@ export interface QuizZone { chatMessages?: ChatMessage[]; isQuizZoneEnd?: boolean; endSocketTime?: number; + serverTime: number; + offset: number; } export interface Rank { @@ -119,3 +121,8 @@ export interface ChatMessage { nickname: string; message: string; } + +export interface InitQuizZoneResponse { + quizZone: QuizZone; + now: number; +} From 1170efbca5812d5819d5502beb4607cfc3614d5b Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 14:54:28 +0900 Subject: [PATCH 15/38] =?UTF-8?q?feat:=20init=20=ED=95=A0=20=EC=8B=9C?= =?UTF-8?q?=EC=97=90=20serverTime=20=EA=B8=B0=EB=B0=98=20offset=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=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/hook/quizZone/useQuizZone.tsx | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/apps/frontend/src/hook/quizZone/useQuizZone.tsx b/apps/frontend/src/hook/quizZone/useQuizZone.tsx index 3a18a2a34..7451ea345 100644 --- a/apps/frontend/src/hook/quizZone/useQuizZone.tsx +++ b/apps/frontend/src/hook/quizZone/useQuizZone.tsx @@ -2,6 +2,7 @@ import { useReducer } from 'react'; import useWebSocket from '@/hook/useWebSocket.tsx'; import { ChatMessage, + InitQuizZoneResponse, NextQuizResponse, Player, QuizZone, @@ -12,7 +13,7 @@ import { import atob from '@/utils/atob'; export type QuizZoneAction = - | { type: 'init'; payload: QuizZone } + | { type: 'init'; payload: InitQuizZoneResponse } | { type: 'join'; payload: Player[] } | { type: 'someone_join'; payload: Player } | { type: 'someone_leave'; payload: string } @@ -39,23 +40,19 @@ const quizZoneReducer: Reducer = (state, action) => { switch (type) { case 'init': + const { quizZone, now } = payload; + const receiveTime = new Date().getTime(); return { ...state, - stage: payload.stage, - title: payload.title, - description: payload.description, - quizCount: payload.quizCount, - hostId: payload.hostId, - currentPlayer: payload.currentPlayer, - chatMessages: payload.chatMessages, + ...quizZone, currentQuiz: - payload.currentQuiz !== undefined + quizZone.currentQuiz !== undefined ? { - ...payload.currentQuiz, - question: atob(payload.currentQuiz?.question ?? ''), + ...quizZone.currentQuiz, + question: atob(quizZone.currentQuiz?.question ?? ''), } : undefined, - maxPlayers: payload.maxPlayers, + offset: quizZone.serverTime - (now + receiveTime) / 2, players: [], }; case 'join': @@ -121,8 +118,8 @@ const quizZoneReducer: Reducer = (state, action) => { question: atob(nextQuiz.question), currentIndex: nextQuiz.currentIndex, playTime: nextQuiz.playTime, - startTime: nextQuiz.startTime, - deadlineTime: nextQuiz.deadlineTime, + startTime: nextQuiz.startTime - state.offset, + deadlineTime: nextQuiz.deadlineTime - state.offset, quizType: 'SHORT', }, currentQuizResult: { @@ -162,7 +159,7 @@ const quizZoneReducer: Reducer = (state, action) => { submits: payload.submits, quizzes: payload.quizzes, ranks: payload.ranks, - endSocketTime: payload.endSocketTime, + endSocketTime: payload.endSocketTime - state.offset, }; case 'chat': return { @@ -241,6 +238,8 @@ const useQuizZone = (quizZoneId: string, handleReconnect?: () => void) => { quizzes: [], chatMessages: [], maxPlayers: 0, + offset: 0, + serverTime: 0, }; const [quizZoneState, dispatch] = useReducer(quizZoneReducer, initialQuizZoneState); @@ -267,8 +266,8 @@ const useQuizZone = (quizZoneId: string, handleReconnect?: () => void) => { }); //initialize QuizZOne - const initQuizZoneData = async (quizZone: QuizZone) => { - dispatch({ type: 'init', payload: quizZone }); + const initQuizZoneData = async (quizZone: QuizZone, now: number) => { + dispatch({ type: 'init', payload: { quizZone, now } }); beginConnection(); joinQuizZone({ quizZoneId }); }; From 9ebc249f689f16410cd3a453f0f5fa7654d421a8 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 14:55:35 +0900 Subject: [PATCH 16/38] =?UTF-8?q?feat:=20offset=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20now(=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=81)=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/pages/QuizZonePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/pages/QuizZonePage.tsx b/apps/frontend/src/pages/QuizZonePage.tsx index 1bf3a30c2..cdd673b73 100644 --- a/apps/frontend/src/pages/QuizZonePage.tsx +++ b/apps/frontend/src/pages/QuizZonePage.tsx @@ -37,8 +37,8 @@ const QuizZoneContent = () => { setIsLoading(true); const quizZone = await requestQuizZone(quizZoneId); - - await initQuizZoneData(quizZone); + const now = new Date().getTime(); + await initQuizZoneData(quizZone, now); setIsLoading(false); setIsDisconnection(false); From ed9f55c87e9b4d9371c2e2aa754cb7823cae591a Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 15:39:42 +0900 Subject: [PATCH 17/38] =?UTF-8?q?feat:=20quizZoneInfo=20await=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/quiz-zone/quiz-zone.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/quiz-zone/quiz-zone.controller.ts b/apps/backend/src/quiz-zone/quiz-zone.controller.ts index 74be4d91d..ab8d6da79 100644 --- a/apps/backend/src/quiz-zone/quiz-zone.controller.ts +++ b/apps/backend/src/quiz-zone/quiz-zone.controller.ts @@ -69,7 +69,7 @@ export class QuizZoneController { @Param('quizZoneId') quizZoneId: string, ) { const serverTime = Date.now(); - const quizZoneInfo = this.quizZoneService.getQuizZoneInfo( + const quizZoneInfo = await this.quizZoneService.getQuizZoneInfo( session.id, quizZoneId, session.quizZoneId, From 4fd47a673d48e07c5a853688c9027d98b10e9030 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 15:39:55 +0900 Subject: [PATCH 18/38] =?UTF-8?q?feat:=20NotFound=20Page=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/pages/NotFoundPage.tsx | 50 ++++++++++++++++++++++++ apps/frontend/src/router/router.tsx | 2 + 2 files changed, 52 insertions(+) create mode 100644 apps/frontend/src/pages/NotFoundPage.tsx diff --git a/apps/frontend/src/pages/NotFoundPage.tsx b/apps/frontend/src/pages/NotFoundPage.tsx new file mode 100644 index 000000000..705b0a020 --- /dev/null +++ b/apps/frontend/src/pages/NotFoundPage.tsx @@ -0,0 +1,50 @@ +import CommonButton from '@/components/common/CommonButton'; +import ContentBox from '@/components/common/ContentBox'; +import Typography from '@/components/common/Typogrpahy'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; +import { useNavigate } from 'react-router-dom'; +import Logo from '@/components/common/Logo'; + +const NotFound = () => { + const navigate = useNavigate(); + const handleMoveMainPage = () => { + navigate(`/`); + }; + + return ( +
+ + + + + + + + + + + + + +
+ ); +}; + +export default NotFound; diff --git a/apps/frontend/src/router/router.tsx b/apps/frontend/src/router/router.tsx index d6c3453d8..81bddd9ce 100644 --- a/apps/frontend/src/router/router.tsx +++ b/apps/frontend/src/router/router.tsx @@ -3,6 +3,7 @@ import MainPage from '@/pages/MainPage'; import RootLayout from '../pages/RootLayout'; import QuizZonePage from '@/pages/QuizZonePage'; import CreateQuizZonePage from '@/pages/CreateQuizZonePage.tsx'; +import NotFound from '@/pages/NotFoundPage'; function Router() { return ( @@ -11,6 +12,7 @@ function Router() { } /> } /> } /> + } /> ); From 7072fe69b8e5fa18a3a45a9d8c9df0024cacb25a Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 15:45:36 +0900 Subject: [PATCH 19/38] =?UTF-8?q?fix:=20ProgressBar=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/components/common/ProgressBar.stories.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/components/common/ProgressBar.stories.tsx b/apps/frontend/src/components/common/ProgressBar.stories.tsx index 7f2f5849a..efc4e6515 100644 --- a/apps/frontend/src/components/common/ProgressBar.stories.tsx +++ b/apps/frontend/src/components/common/ProgressBar.stories.tsx @@ -13,7 +13,8 @@ type Story = StoryObj; export const Default: Story = { args: { - deadlineTime: Date.now() + 30000, + playTime: 30000, + time: Date.now(), onTimeEnd: () => { alert('time end'); }, From 97e466d145b1eb4d9789756072cb83a98a40188e Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 15:47:57 +0900 Subject: [PATCH 20/38] =?UTF-8?q?fix:=20QuizZoneInProgress=20undefined=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx b/apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx index cab5fd8ae..6a4ba687b 100644 --- a/apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx +++ b/apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx @@ -27,7 +27,7 @@ const QuizZoneInProgress = ({ quizZoneState, submitAnswer, playQuiz }: QuizZoneI return ( ); From f3eb8da436c31f98a62be9c06b534a984a7cd5ce Mon Sep 17 00:00:00 2001 From: mario Date: Thu, 5 Dec 2024 15:52:50 +0900 Subject: [PATCH 21/38] =?UTF-8?q?fix:=20e2e=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts b/apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts index b46f80cf0..0f307e34b 100644 --- a/apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts +++ b/apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts @@ -81,8 +81,11 @@ describe('QuizZoneController', () => { stage: 'LOBBY', playerCount: 1, maxPlayers: 8, + serverTime: 0, }; + Date.now = jest.fn().mockReturnValue(0); + it('퀴즈존 정보를 성공적으로 조회한다', async () => { const session = { id: 'sessionId' }; mockQuizZoneService.getQuizZoneInfo.mockResolvedValue(mockQuizZoneInfo); From be09b6859bb4915dd8ae2f1c63928ed9af963a3f Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 15:59:34 +0900 Subject: [PATCH 22/38] =?UTF-8?q?remove:=20useTimer.test=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/hook/useTimer.test.ts | 82 ------------------------- 1 file changed, 82 deletions(-) delete mode 100644 apps/frontend/src/hook/useTimer.test.ts diff --git a/apps/frontend/src/hook/useTimer.test.ts b/apps/frontend/src/hook/useTimer.test.ts deleted file mode 100644 index 371442aa6..000000000 --- a/apps/frontend/src/hook/useTimer.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -// useTimer.test.ts -import { renderHook, act } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { useTimer } from './useTimer'; - -describe('useTimer', () => { - beforeEach(() => { - // 가짜 타이머 설정 - vi.useFakeTimers(); - }); - - afterEach(() => { - // 테스트 간 타이머 정리 - vi.clearAllTimers(); - vi.restoreAllMocks(); - }); - - it('초기 시간이 올바르게 설정되어야 한다', () => { - const { result } = renderHook(() => - useTimer({ - initialTime: 10, - onComplete: () => {}, - }), - ); - - expect(result.current.time).toBe(10); - }); - - it('start 호출 시 카운트다운이 시작되어야 한다', () => { - const { result } = renderHook(() => - useTimer({ - initialTime: 10, - onComplete: () => {}, - }), - ); - - act(() => { - result.current.start(); - }); - - act(() => { - vi.advanceTimersByTime(100); - }); - - expect(result.current.time).toBe(9.9); - }); - - it('타이머가 0에 도달하면 onComplete가 한 번만 호출되어야 한다', () => { - const onComplete = vi.fn(); - const { result } = renderHook(() => - useTimer({ - initialTime: 0.2, - onComplete, - }), - ); - - act(() => { - result.current.start(); - }); - - // 타이머가 0에 도달할 때까지 시간 진행 - act(() => { - vi.advanceTimersByTime(200); - }); - - // onComplete가 정확히 한 번만 호출되었는지 확인 - expect(onComplete).toHaveBeenCalledTimes(1); - }); - - it('컴포넌트 언마운트 시 모든 타이머가 정리되어야 한다', () => { - const { unmount } = renderHook(() => - useTimer({ - initialTime: 10, - onComplete: () => {}, - }), - ); - - unmount(); - - expect(vi.getTimerCount()).toBe(0); - }); -}); From 613bd91114ad88aee5a1b7ce063246fa760abbfa Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 16:07:09 +0900 Subject: [PATCH 23/38] =?UTF-8?q?test:=20useValidInput=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/hook/useValidInput.test.ts | 124 +++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 apps/frontend/src/hook/useValidInput.test.ts diff --git a/apps/frontend/src/hook/useValidInput.test.ts b/apps/frontend/src/hook/useValidInput.test.ts new file mode 100644 index 000000000..6b69f63d8 --- /dev/null +++ b/apps/frontend/src/hook/useValidInput.test.ts @@ -0,0 +1,124 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; +import useValidState from './useValidInput'; + +describe('useValidState', () => { + test('초기값이 유효한 경우', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(10, validator)); + const [state, errorMessage, _, isInvalid] = result.current; + + expect(state).toBe(10); + expect(errorMessage).toBe(''); + expect(isInvalid).toBe(false); + }); + + test('초기값이 유효하지 않은 경우', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(-5, validator)); + const [state, errorMessage, _, isInvalid] = result.current; + + expect(state).toBe(-5); + expect(errorMessage).toBe('음수는 입력할 수 없습니다.'); + expect(isInvalid).toBe(true); + }); + + test('유효한 값으로 상태 업데이트', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(0, validator)); + + act(() => { + const [, , setValue] = result.current; + setValue(5); + }); + + const [state, errorMessage, _, isInvalid] = result.current; + expect(state).toBe(5); + expect(errorMessage).toBe(''); + expect(isInvalid).toBe(false); + }); + + test('유효하지 않은 값으로 상태 업데이트', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(0, validator)); + + act(() => { + const [, , setValue] = result.current; + setValue(-10); + }); + + const [state, errorMessage, _, isInvalid] = result.current; + expect(state).toBe(-10); + expect(errorMessage).toBe('음수는 입력할 수 없습니다.'); + expect(isInvalid).toBe(true); + }); + + test('다중 조건 검증', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + if (value > 100) return '100보다 큰 숫자는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(0, validator)); + + // 유효한 값 테스트 + act(() => { + const [, , setValue] = result.current; + setValue(50); + }); + + expect(result.current[0]).toBe(50); + expect(result.current[1]).toBe(''); + expect(result.current[3]).toBe(false); + + // 범위를 초과하는 값 테스트 + act(() => { + const [, , setValue] = result.current; + setValue(150); + }); + + expect(result.current[0]).toBe(150); + expect(result.current[1]).toBe('100보다 큰 숫자는 입력할 수 없습니다.'); + expect(result.current[3]).toBe(true); + }); + + test('문자열 유효성 검사', () => { + const validator = (value: string) => { + if (value.length < 3) return '최소 3글자 이상이어야 합니다.'; + }; + + const { result } = renderHook(() => useValidState('', validator)); + + // 유효하지 않은 값 테스트 + act(() => { + const [, , setValue] = result.current; + setValue('ab'); + }); + + expect(result.current[0]).toBe('ab'); + expect(result.current[1]).toBe('최소 3글자 이상이어야 합니다.'); + expect(result.current[3]).toBe(true); + + // 유효한 값 테스트 + act(() => { + const [, , setValue] = result.current; + setValue('abc'); + }); + + expect(result.current[0]).toBe('abc'); + expect(result.current[1]).toBe(''); + expect(result.current[3]).toBe(false); + }); +}); From b4ea3c52c2cca5220758318639faf20ca7416899 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 16:14:53 +0900 Subject: [PATCH 24/38] =?UTF-8?q?test:=20useQuizZone=20test=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hook/quizZone/useQuizZone.test.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 apps/frontend/src/hook/quizZone/useQuizZone.test.ts diff --git a/apps/frontend/src/hook/quizZone/useQuizZone.test.ts b/apps/frontend/src/hook/quizZone/useQuizZone.test.ts new file mode 100644 index 000000000..b9b5cde37 --- /dev/null +++ b/apps/frontend/src/hook/quizZone/useQuizZone.test.ts @@ -0,0 +1,102 @@ +import { renderHook, act } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { QuizZone } from '@/types/quizZone.types'; +import useQuizZone from './useQuizZone'; + +// useWebSocket 모킹 +vi.mock('@/hook/useWebSocket', () => ({ + default: () => ({ + beginConnection: vi.fn(), + sendMessage: vi.fn(), + closeConnection: vi.fn(), + messageHandler: vi.fn(), + }), +})); + +// env 모킹 +vi.mock('@/utils/atob', () => ({ + default: vi.fn((str) => str), +})); + +describe('useQuizZone', () => { + const mockQuizZoneId = 'test-quiz-zone-id'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('초기 상태가 올바르게 설정되어야 합니다', () => { + const { result } = renderHook(() => useQuizZone(mockQuizZoneId)); + + expect(result.current.quizZoneState).toEqual({ + stage: 'LOBBY', + currentPlayer: { + id: '', + nickname: '', + }, + title: '', + description: '', + hostId: '', + quizCount: 0, + players: [], + score: 0, + submits: [], + quizzes: [], + chatMessages: [], + maxPlayers: 0, + offset: 0, + serverTime: 0, + }); + }); + + it('playQuiz 액션이 상태를 올바르게 업데이트해야 합니다', () => { + const { result } = renderHook(() => useQuizZone(mockQuizZoneId)); + + act(() => { + result.current.playQuiz(); + }); + + expect(result.current.quizZoneState.stage).toBe('IN_PROGRESS'); + expect(result.current.quizZoneState.currentPlayer.state).toBe('PLAY'); + }); + + it('init 액션이 상태를 올바르게 업데이트해야 합니다', () => { + const { result } = renderHook(() => useQuizZone(mockQuizZoneId)); + + const mockQuizZone: Partial = { + stage: 'LOBBY', + currentPlayer: { + id: 'player1', + nickname: 'Player 1', + state: 'WAIT', + }, + title: 'Test Quiz', + description: 'Test Description', + hostId: 'host1', + quizCount: 5, + serverTime: Date.now(), + }; + + act(() => { + result.current.initQuizZoneData(mockQuizZone as QuizZone, Date.now()); + }); + + expect(result.current.quizZoneState.title).toBe('Test Quiz'); + expect(result.current.quizZoneState.currentPlayer.id).toBe('player1'); + expect(result.current.quizZoneState.quizCount).toBe(5); + }); + + describe('state transitions', () => { + it('LOBBY에서 IN_PROGRESS로 상태 전환이 올바르게 되어야 합니다', () => { + const { result } = renderHook(() => useQuizZone(mockQuizZoneId)); + + expect(result.current.quizZoneState.stage).toBe('LOBBY'); + + act(() => { + result.current.playQuiz(); + }); + + expect(result.current.quizZoneState.stage).toBe('IN_PROGRESS'); + }); + }); +}); From 3321d2d1031d5d7b48acbb6a2433e80d5e68cb8d Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 17:01:59 +0900 Subject: [PATCH 25/38] =?UTF-8?q?chore:=20vite-env=20web=20Worker=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/vite-env.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/frontend/src/vite-env.d.ts b/apps/frontend/src/vite-env.d.ts index 11f02fe2a..ce0977e1c 100644 --- a/apps/frontend/src/vite-env.d.ts +++ b/apps/frontend/src/vite-env.d.ts @@ -1 +1,7 @@ /// +declare module '*?worker' { + const workerConstructor: { + new (): Worker; + }; + export default workerConstructor; +} From 8c7b13633059bff0cf46dd1a294a6a05658d101c Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 17:02:56 +0900 Subject: [PATCH 26/38] =?UTF-8?q?feat:=20vite.config=20=EB=A1=A4=EC=97=85?= =?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 - web worker 빌드 옵션 추가 - hash 값 추가 --- apps/frontend/vite.config.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index d3d322823..ca2fd34a6 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -25,6 +25,24 @@ export default defineConfig(({ mode }) => { }, }, }, + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('timer.worker')) { + return 'worker'; + } + }, + // 캐시 무효화를 위한 더 안전한 방법 + entryFileNames: `assets/[name].[hash].js`, + chunkFileNames: `assets/[name].[hash].js`, + assetFileNames: `assets/[name].[hash].[ext]`, + }, + }, + // 캐시 설정 + manifest: true, // manifest 파일 생성 + sourcemap: true, + }, test: { environment: 'jsdom', globals: true, From 81c9163607e5a878f1d290c018641ce3686d7596 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 17:03:07 +0900 Subject: [PATCH 27/38] =?UTF-8?q?remove:=20console=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/components/common/ProgressBar.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/frontend/src/components/common/ProgressBar.tsx b/apps/frontend/src/components/common/ProgressBar.tsx index 6d5e002ee..e3acab8d7 100644 --- a/apps/frontend/src/components/common/ProgressBar.tsx +++ b/apps/frontend/src/components/common/ProgressBar.tsx @@ -27,8 +27,6 @@ const ProgressBar = ({ playTime, time, onTimeEnd }: ProgressBarProps) => { onTimeEnd?.(); } - console.log(progress); - return (
From d65144bb8e6bb02c410f19801a57702851dc8456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=ED=98=84=EB=AF=BC?= <77275989+joyjhm@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:15:25 +0900 Subject: [PATCH 28/38] =?UTF-8?q?fix:=20=ED=80=B4=EC=A6=88=EC=85=8B=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=8B=9C=20=EC=B6=94=EC=B2=9C=20=ED=80=B4?= =?UTF-8?q?=EC=A6=88=EA=B0=80=20=EC=95=9E=EC=9C=BC=EB=A1=9C=20=EC=98=A4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/quiz/quiz.service.ts | 14 +------------- .../src/quiz/repository/quiz-set.repository.ts | 15 +-------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/apps/backend/src/quiz/quiz.service.ts b/apps/backend/src/quiz/quiz.service.ts index a191163cc..a04abb4ac 100644 --- a/apps/backend/src/quiz/quiz.service.ts +++ b/apps/backend/src/quiz/quiz.service.ts @@ -54,7 +54,7 @@ export class QuizService { } async deleteQuizSet(quizSetId: number) { - const quiz = await this.findQuizSet(quizSetId); + const quizSet = await this.findQuizSet(quizSetId); await this.quizSetRepository.delete({ id: quizSetId }); } @@ -80,10 +80,6 @@ export class QuizService { async searchQuizSet(searchQuery: SearchQuizSetRequestDTO) { const { name, page, size } = searchQuery; - if (!name) { - return this.findDefaultQuizSet(page, size); - } - const [quizSets, count] = await Promise.all([ this.quizSetRepository.searchByName(name, page, size), this.quizSetRepository.countByName(name), @@ -93,12 +89,4 @@ export class QuizService { return { quizSetDetails, total: count, currentPage: page }; } - private async findDefaultQuizSet(page: number, size: number) { - const [quizSets, count] = await Promise.all([ - this.quizSetRepository.findByRecommend(page, size), - this.quizSetRepository.countByRecommend(), - ]); - const quizSetDetails = quizSets.map(QuizSetDetails.from); - return { quizSetDetails, total: count, currentPage: page }; - } } diff --git a/apps/backend/src/quiz/repository/quiz-set.repository.ts b/apps/backend/src/quiz/repository/quiz-set.repository.ts index 66ee5425f..4275e55fc 100644 --- a/apps/backend/src/quiz/repository/quiz-set.repository.ts +++ b/apps/backend/src/quiz/repository/quiz-set.repository.ts @@ -11,7 +11,7 @@ export class QuizSetRepository extends Repository { searchByName(name: string, page: number, pageSize: number) { return this.find({ where: {name: ILike(`${name}%`)}, - order: {createAt: 'desc'}, + order: {recommended: 'DESC', createAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, }); @@ -20,17 +20,4 @@ export class QuizSetRepository extends Repository { countByName(name: string) { return this.count({ where: { name: ILike(`${name}%`) } }); } - - countByRecommend() { - return this.count({where: {recommended: true}}); - } - - findByRecommend(page: number, pageSize: number) { - return this.find({ - where: {recommended: true}, - order: {createAt: 'desc'}, - skip: (page - 1) * pageSize, - take: pageSize, - }) - } } From 14990ccd0b127517fa25a212c19f250a0d135e3d Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 17:46:17 +0900 Subject: [PATCH 29/38] =?UTF-8?q?chore:=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EA=B2=BD=EB=A1=9C=20clean=20=EC=98=B5=EC=85=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 --- apps/frontend/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index ca2fd34a6..86c411032 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -26,6 +26,7 @@ export default defineConfig(({ mode }) => { }, }, build: { + clean: true, // 빌드 전에 outDir을 청소합니다 rollupOptions: { output: { manualChunks(id) { From 6107b731a7b460023d27430ad91a7858c1f793e2 Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 17:59:15 +0900 Subject: [PATCH 30/38] =?UTF-8?q?feat:=20webWorker=20=EC=BF=BC=EB=A6=AC=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 --- apps/frontend/src/hook/useTimer.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/hook/useTimer.ts b/apps/frontend/src/hook/useTimer.ts index 4f7e248a3..ded5984bd 100644 --- a/apps/frontend/src/hook/useTimer.ts +++ b/apps/frontend/src/hook/useTimer.ts @@ -24,9 +24,12 @@ export const useTimer = ({ initialTime, onComplete }: TimerConfig) => { } // 새 Worker 생성 - workerRef.current = new Worker(new URL('../workers/timer.worker.ts', import.meta.url), { - type: 'module', - }); + workerRef.current = new Worker( + new URL('../workers/timer.worker.ts?worker', import.meta.url), + { + type: 'module', + }, + ); // Worker 메시지 핸들러 workerRef.current.onmessage = (event) => { From 559a408f90c87241dc82db163abcff6f6431f164 Mon Sep 17 00:00:00 2001 From: mario Date: Thu, 5 Dec 2024 18:44:20 +0900 Subject: [PATCH 31/38] =?UTF-8?q?fix:=20worker=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/hook/useTimer.ts | 10 +- apps/frontend/src/workers/timer.worker.ts | 161 ++++++++++------------ 2 files changed, 79 insertions(+), 92 deletions(-) diff --git a/apps/frontend/src/hook/useTimer.ts b/apps/frontend/src/hook/useTimer.ts index ded5984bd..3b0c41880 100644 --- a/apps/frontend/src/hook/useTimer.ts +++ b/apps/frontend/src/hook/useTimer.ts @@ -1,4 +1,5 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import TimerWorker from '@/workers/timer.worker?worker'; interface TimerConfig { initialTime: number; @@ -24,12 +25,7 @@ export const useTimer = ({ initialTime, onComplete }: TimerConfig) => { } // 새 Worker 생성 - workerRef.current = new Worker( - new URL('../workers/timer.worker.ts?worker', import.meta.url), - { - type: 'module', - }, - ); + workerRef.current = new TimerWorker(); // Worker 메시지 핸들러 workerRef.current.onmessage = (event) => { diff --git a/apps/frontend/src/workers/timer.worker.ts b/apps/frontend/src/workers/timer.worker.ts index 32ede9bcb..c1940c45e 100644 --- a/apps/frontend/src/workers/timer.worker.ts +++ b/apps/frontend/src/workers/timer.worker.ts @@ -1,94 +1,85 @@ -import { TimerMessage, TimerResponse } from '@/types/timer.types'; - -class TimerWorker { - private timerId: ReturnType | null = null; - private startTime: number | null = null; - private duration: number | null = null; - private timeOffset: number = 0; - private pausedTimeRemaining: number | null = null; - - constructor() { - self.onmessage = this.handleMessage.bind(this); - } - - private handleMessage(event: MessageEvent) { - const { type, payload } = event.data; - - switch (type) { - case 'START': - if (!payload?.duration) return; - - if (this.pausedTimeRemaining !== null) { - this.startTimer(this.pausedTimeRemaining); - this.pausedTimeRemaining = null; - } else { - if (payload.serverTime) { - this.timeOffset = Date.now() - payload.serverTime; - } - this.startTimer(payload.duration); - } - break; - - case 'STOP': - if (this.timerId !== null && this.startTime !== null && this.duration !== null) { - const currentTime = Date.now() - this.timeOffset; - const elapsed = (currentTime - this.startTime) / 1000; - this.pausedTimeRemaining = Math.max(0, this.duration - elapsed); - } - this.stopTimer(); - break; - - case 'RESET': - this.resetTimer(); - break; - } - } - - private startTimer(duration: number) { - this.stopTimer(); // 기존 타이머가 있다면 정리 - - this.duration = duration; - this.startTime = Date.now() - this.timeOffset; - - // setInterval 직접 사용 - this.timerId = setInterval(() => { - if (!this.startTime || !this.duration) return; - - const currentTime = Date.now() - this.timeOffset; - const elapsed = (currentTime - this.startTime) / 1000; - const remaining = Math.max(0, this.duration - elapsed); - const roundedRemaining = Math.round(remaining * 10) / 10; - - if (roundedRemaining <= 0) { - this.postMessage({ type: 'COMPLETE' }); - this.stopTimer(); +let timerId: ReturnType | null = null; +let startTime: number | null = null; +let duration: number | null = null; +let timeOffset: number = 0; +let pausedTimeRemaining: number | null = null; + +self.onmessage = (event: MessageEvent) => { + const { type, payload } = event.data; + + switch (type) { + case 'START': + if (!payload?.duration) return; + + if (pausedTimeRemaining !== null) { + startTimer(pausedTimeRemaining, self.postMessage); + pausedTimeRemaining = null; } else { - this.postMessage({ - type: 'TICK', - payload: { time: roundedRemaining }, - }); + if (payload.serverTime) { + timeOffset = Date.now() - payload.serverTime; + } + startTimer(payload.duration, self.postMessage); } - }, 100); - } + break; - private stopTimer() { - if (this.timerId !== null) { - clearInterval(this.timerId); - this.timerId = null; - } - } + case 'STOP': + if (timerId !== null && startTime !== null && duration !== null) { + const currentTime = Date.now() - timeOffset; + const elapsed = (currentTime - startTime) / 1000; + pausedTimeRemaining = Math.max(0, duration - elapsed); + } + stopTimer(); + break; - private resetTimer() { - this.stopTimer(); - this.startTime = null; - this.duration = null; - this.timeOffset = 0; - this.pausedTimeRemaining = null; + case 'RESET': + resetTimer(); + break; } +}; + +function startTimer( + newDuration: number, + postMessage: { + (message: any, targetOrigin: string, transfer?: Transferable[]): void; + (message: any, options?: WindowPostMessageOptions): void; + }, +) { + stopTimer(); // 기존 타이머가 있다면 정리 + + duration = newDuration; + startTime = Date.now() - timeOffset; + + timerId = setInterval(() => { + if (!startTime || !duration) return; + + const currentTime = Date.now() - timeOffset; + const elapsed = (currentTime - startTime) / 1000; + const remaining = Math.max(0, duration - elapsed); + const roundedRemaining = Math.round(remaining * 10) / 10; + + if (roundedRemaining <= 0) { + postMessage({ type: 'COMPLETE' }); + stopTimer(); + } else { + postMessage({ + type: 'TICK', + payload: { time: roundedRemaining }, + }); + } + }, 100); +} - private postMessage(message: TimerResponse) { - self.postMessage(message); +function stopTimer() { + if (timerId !== null) { + clearInterval(timerId); + timerId = null; } } -new TimerWorker(); +function resetTimer() { + stopTimer(); + startTime = null; + duration = null; + timeOffset = 0; + pausedTimeRemaining = null; +} From 0447498bb7fd341a9edf8c11f7f6ee39e9824cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=ED=98=84=EB=AF=BC?= <77275989+joyjhm@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:39:45 +0900 Subject: [PATCH 32/38] =?UTF-8?q?docs:=20readme=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 167863de2..389f6a542 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,13 @@ ## 🛠 기술 스택 -| 영역 | 기술 스택 | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **공통** | ![TypeScript](https://img.shields.io/badge/TypeScript-5.6.3-3178C6?style=flat-square&logo=typescript&logoColor=white) ![WebSocket](https://img.shields.io/badge/WebSocket-8.18.0-010101?style=flat-square&logo=socket.io&logoColor=white) ![TSDoc](https://img.shields.io/badge/TSDoc-0.26.11-3178C6?style=flat-square&logo=typescript&logoColor=white) | -| **Frontend** | ![React](https://img.shields.io/badge/React-18.3.1-61DAFB?style=flat-square&logo=react&logoColor=white) ![Vite](https://img.shields.io/badge/Vite-5.4.10-646CFF?style=flat-square&logo=vite&logoColor=white) ![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-3.4-38B2AC?style=flat-square&logo=tailwind-css&logoColor=white) ![shadcn/ui](https://img.shields.io/badge/shadcn%2Fui-latest-000000?style=flat-square) ![Vitest](https://img.shields.io/badge/Vitest-latest-6E9F18?style=flat-square&logo=vitest&logoColor=white) ![Testing Library](https://img.shields.io/badge/Testing%20Library-latest-E33332?style=flat-square&logo=testing-library&logoColor=white) ![Storybook](https://img.shields.io/badge/Storybook-8.4.2-FF4785?style=flat-square&logo=storybook&logoColor=white) | -| **Backend** | ![NestJS](https://img.shields.io/badge/NestJS-10.4.7-E0234E?style=flat-square&logo=nestjs&logoColor=white) ![MySQL](https://img.shields.io/badge/MySQL-2-4479A1?style=flat-square&logo=mysql&logoColor=white) ![SQLite](https://img.shields.io/badge/SQLite-3-003B57?style=flat-square&logo=sqlite&logoColor=white) ![TypeORM](https://img.shields.io/badge/TypeORM-0.3.20-E93524?style=flat-square&logo=typeorm&logoColor=white) ![Swagger](https://img.shields.io/badge/Swagger-8.0.5-85EA2D?style=flat-square&logo=swagger&logoColor=black) ![Jest](https://img.shields.io/badge/Jest-Testing-C21325?style=flat-square&logo=jest&logoColor=white) ![SuperTest](https://img.shields.io/badge/SuperTest-Testing-009688?style=flat-square&logo=testing-library&logoColor=white) ![Artillery](https://img.shields.io/badge/Artillery-2.0.21-CA2B2B?style=flat-square&logoColor=white) | -| **인프라** | ![NCP](https://img.shields.io/badge/Naver%20Cloud%20Platform-latest-03C75A?style=flat-square&logo=naver&logoColor=white) ![Nginx](https://img.shields.io/badge/Nginx-1.24.0-009639?style=flat-square&logo=nginx&logoColor=white) ![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-3.0-2088FF?style=flat-square&logo=github-actions&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-24.0.7-2496ED?style=flat-square&logo=docker&logoColor=white) ![Linux](https://img.shields.io/badge/Linux-Ubuntu%2022.04-FCC624?style=flat-square&logo=linux&logoColor=black) | -| **협업 도구** | ![Notion](https://img.shields.io/badge/Notion-2.0.41-000000?style=flat-square&logo=notion&logoColor=white) ![Figma](https://img.shields.io/badge/Figma-latest-F24E1E?style=flat-square&logo=figma&logoColor=white) ![Excalidraw](https://img.shields.io/badge/Excalidraw-latest-6965DB?style=flat-square&logo=excalidraw&logoColor=white) ![Zoom](https://img.shields.io/badge/Zoom-5.17.0-2D8CFF?style=flat-square&logo=zoom&logoColor=white) ![Git](https://img.shields.io/badge/Git-2.42.0-F05032?style=flat-square&logo=git&logoColor=white) ![GitHub](https://img.shields.io/badge/GitHub-latest-181717?style=flat-square&logo=github&logoColor=white) ![GitHub Projects](https://img.shields.io/badge/GitHub%20Projects-latest-181717?style=flat-square&logo=github&logoColor=white) | +| 영역 | 기술 스택 | +| ------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **공통** | ![TypeScript](https://img.shields.io/badge/TypeScript-5.6.3-3178C6?style=flat-square&logo=typescript&logoColor=white) ![WebSocket](https://img.shields.io/badge/WebSocket-8.18.0-010101?style=flat-square&logo=socket.io&logoColor=white) ![TSDoc](https://img.shields.io/badge/TSDoc-0.26.11-3178C6?style=flat-square&logo=typescript&logoColor=white) | +| **Frontend** | ![React](https://img.shields.io/badge/React-18.3.1-61DAFB?style=flat-square&logo=react&logoColor=white) ![Vite](https://img.shields.io/badge/Vite-5.4.10-646CFF?style=flat-square&logo=vite&logoColor=white) ![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-3.4-38B2AC?style=flat-square&logo=tailwind-css&logoColor=white) ![shadcn/ui](https://img.shields.io/badge/shadcn%2Fui-latest-000000?style=flat-square) ![Vitest](https://img.shields.io/badge/Vitest-latest-6E9F18?style=flat-square&logo=vitest&logoColor=white) ![Testing Library](https://img.shields.io/badge/Testing%20Library-latest-E33332?style=flat-square&logo=testing-library&logoColor=white) ![Storybook](https://img.shields.io/badge/Storybook-8.4.2-FF4785?style=flat-square&logo=storybook&logoColor=white) | +| **Backend** | ![NestJS](https://img.shields.io/badge/NestJS-10.4.7-E0234E?style=flat-square&logo=nestjs&logoColor=white) ![MySQL](https://img.shields.io/badge/MySQL-2-4479A1?style=flat-square&logo=mysql&logoColor=white) ![SQLite](https://img.shields.io/badge/SQLite-3-003B57?style=flat-square&logo=sqlite&logoColor=white) ![TypeORM](https://img.shields.io/badge/TypeORM-0.3.20-E93524?style=flat-square&logo=typeorm&logoColor=white) ![Swagger](https://img.shields.io/badge/Swagger-8.0.5-85EA2D?style=flat-square&logo=swagger&logoColor=black) ![Jest](https://img.shields.io/badge/Jest-Testing-C21325?style=flat-square&logo=jest&logoColor=white) ![SuperTest](https://img.shields.io/badge/SuperTest-Testing-009688?style=flat-square&logo=testing-library&logoColor=white) | +| **인프라** | ![NCP](https://img.shields.io/badge/Naver%20Cloud%20Platform-latest-03C75A?style=flat-square&logo=naver&logoColor=white) ![Nginx](https://img.shields.io/badge/Nginx-1.24.0-009639?style=flat-square&logo=nginx&logoColor=white) ![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-3.0-2088FF?style=flat-square&logo=github-actions&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-24.0.7-2496ED?style=flat-square&logo=docker&logoColor=white) ![Linux](https://img.shields.io/badge/Linux-Ubuntu%2022.04-FCC624?style=flat-square&logo=linux&logoColor=black) ![Grafana](https://img.shields.io/badge/Grafana-10.2.3-F46800?style=flat-square&logo=grafana&logoColor=white) ![Prometheus](https://img.shields.io/badge/Prometheus-2.43.0-E6522C?style=flat-square&logo=prometheus&logoColor=white) ![k6](https://img.shields.io/badge/k6-0.55.0-7D64FF?style=flat-square&logo=k6&logoColor=white)| +| **협업 도구** | ![Notion](https://img.shields.io/badge/Notion-2.0.41-000000?style=flat-square&logo=notion&logoColor=white) ![Figma](https://img.shields.io/badge/Figma-latest-F24E1E?style=flat-square&logo=figma&logoColor=white) ![Excalidraw](https://img.shields.io/badge/Excalidraw-latest-6965DB?style=flat-square&logo=excalidraw&logoColor=white) ![Zoom](https://img.shields.io/badge/Zoom-5.17.0-2D8CFF?style=flat-square&logo=zoom&logoColor=white) ![Git](https://img.shields.io/badge/Git-2.42.0-F05032?style=flat-square&logo=git&logoColor=white) ![GitHub](https://img.shields.io/badge/GitHub-latest-181717?style=flat-square&logo=github&logoColor=white) ![GitHub Projects](https://img.shields.io/badge/GitHub%20Projects-latest-181717?style=flat-square&logo=github&logoColor=white) | ## 🏗 시스템 아키텍처 @@ -456,7 +456,7 @@ pnpm run start └── ... ``` -## 팀 소개(표인데 얘는 수정하지 말아주세요) +## 팀 소개 | [J004 강준현](https://github.com/JunhyunKang) | [J074 김현우](https://github.com/krokerdile) | [J086 도선빈](https://github.com/typingmistake) | [J175 이동현](https://github.com/codemario318) | [J217 전현민](https://github.com/joyjhm) | | --------------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------- | From 0c8a28508b118120032f3b4c4cff0f1de5780f1a Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 21:46:52 +0900 Subject: [PATCH 33/38] =?UTF-8?q?fix:=20webWorker=20=EC=9D=B4=EC=8A=88?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20=EA=B8=B0=EC=A1=B4=20useTimer?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/hook/useTimer.ts | 105 ++++++++++++----------------- 1 file changed, 43 insertions(+), 62 deletions(-) diff --git a/apps/frontend/src/hook/useTimer.ts b/apps/frontend/src/hook/useTimer.ts index 3b0c41880..13ba22dd7 100644 --- a/apps/frontend/src/hook/useTimer.ts +++ b/apps/frontend/src/hook/useTimer.ts @@ -1,5 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import TimerWorker from '@/workers/timer.worker?worker'; +import { useState, useEffect, useCallback } from 'react'; interface TimerConfig { initialTime: number; @@ -8,85 +7,67 @@ interface TimerConfig { } /** - * Web Worker를 활용한 정확한 카운트다운 타이머 커스텀 훅 + * 카운트다운 타이머를 관리하는 커스텀 훅입니다. + * + * @description + * 이 훅은 시작 제어 기능을 갖춘 카운트다운 타이머 기능을 제공합니다. + * 초기 시간과 타이머 완료 시 실행될 선택적 콜백을 받습니다. + * + * @example + * ```typescript + * const { time, start } = useTimer({ + * initialTime: 60, + * onComplete: () => console.log('타이머 완료!'), + * }); + * + * // 타이머 시작 + * start(); + * ``` * * @param {TimerConfig} config - 타이머 설정 객체 - * @returns {object} 타이머 상태와 컨트롤 함수들 + * @param {number} config.initialTime - 카운트다운 초기 시간(초 단위) + * @param {() => void} [config.onComplete] - 타이머 완료 시 실행될 선택적 콜백 + * + * @returns {object} 현재 시간과 시작 함수를 포함하는 객체 + * @returns {number} returns.time - 카운트다운의 현재 시간 + * @returns {() => void} returns.start - 타이머를 시작하는 함수 */ + export const useTimer = ({ initialTime, onComplete }: TimerConfig) => { const [time, setTime] = useState(initialTime); const [isRunning, setIsRunning] = useState(false); - const workerRef = useRef(null); useEffect(() => { - // Worker가 이미 존재하면 종료 - if (workerRef.current) { - workerRef.current.terminate(); - } - - // 새 Worker 생성 - workerRef.current = new TimerWorker(); + let timer: NodeJS.Timeout | null = null; - // 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; - } - }; + if (isRunning) { + timer = setInterval(() => { + setTime((prev) => { + const nextTime = prev - 0.1; + if (nextTime <= 0) { + setIsRunning(false); + onComplete?.(); + return 0; + } + return nextTime; + }); + }, 100); + } - // Clean up return () => { - workerRef.current?.terminate(); + if (timer) { + clearInterval(timer); + } }; - }, []); + }, [isRunning, onComplete]); - // 타이머 시작 const start = useCallback(() => { - if (isRunning || !workerRef.current) return; - - workerRef.current.postMessage({ - type: 'START', - payload: { - duration: initialTime, - serverTime: Date.now(), - }, - }); - + if (isRunning) return; 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, }; }; From 073105f7ecef89c7334a2363dc0f652258b7f27b Mon Sep 17 00:00:00 2001 From: krokerdile Date: Thu, 5 Dec 2024 21:47:19 +0900 Subject: [PATCH 34/38] =?UTF-8?q?fix:=20progress=20=EB=B0=94=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=8B=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx b/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx index 7c5d726a1..0c327c48b 100644 --- a/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx +++ b/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx @@ -19,10 +19,11 @@ const QuizInProgress = ({ currentQuiz, submitAnswer }: QuizInProgressProps) => { const MAX_TEXT_LENGTH = 100; const MIN_TEXT_LENGTH = 1; - const playTime = currentQuiz.playTime; + const now = new Date().getTime(); + const { playTime, deadlineTime } = currentQuiz; const { start, time } = useTimer({ - initialTime: playTime / 1000, + initialTime: (deadlineTime - now) / 1000, onComplete: () => {}, }); From 0109647421ef393fc598a2ea8ace208750fbf2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=A0=EB=B9=88?= Date: Thu, 5 Dec 2024 21:59:29 +0900 Subject: [PATCH 35/38] =?UTF-8?q?feat:=20=ED=80=B4=EC=A6=88=EC=A1=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=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/quiz-zone/dto/create-quiz-zone.dto.ts | 2 +- .../CreateQuizZone/CreateQuizZoneBasic.tsx | 10 +++++---- apps/frontend/src/components/common/Input.tsx | 21 +++++++++++++++++++ apps/frontend/src/utils/validators.ts | 3 ++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts b/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts index 008433d71..bb257971a 100644 --- a/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts +++ b/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts @@ -24,7 +24,7 @@ export class CreateQuizZoneDto { @IsInt({ message: '최대 플레이어 수가 없습니다.' }) @Min(1, { message: '최소 1명 이상이어야 합니다.' }) - // @Max(300, { message: '최대 300명까지 가능합니다.' }) + @Max(300, { message: '최대 300명까지 가능합니다.' }) readonly limitPlayerCount: number; @IsNotEmpty({ message: '퀴즈존을 선택해주세요.' }) diff --git a/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx b/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx index 8df873058..ed11e7c6b 100644 --- a/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx +++ b/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx @@ -80,10 +80,11 @@ const CreateQuizZoneBasic = ({ onChange={(e) => handleChangeQuizZoneBasic(e, 'QUIZ_ZONE_ID')} isBorder={true} className="rounded-md w-full" - placeholder="퀴즈존 코드를 입력하세요" + placeholder="숫자와 영문자 5자 이상 10자 이하" error={ validationError == '5자 이상 입력해주세요.' || - validationError == '10자 이하로 입력해주세요.' + validationError == '10자 이하로 입력해주세요.' || + validationError == '숫자와 알파벳 조합만 가능합니다.' ? validationError : '' } @@ -95,12 +96,13 @@ const CreateQuizZoneBasic = ({ label="최대 인원" name="quiz-zone-limit-player-count" step={10} - min={1} - max={300} value={limitPlayerCount} onChange={(e) => handleChangeQuizZoneBasic(e, 'LIMIT')} isBorder={true} className="w-20" + isShowCount={true} + min={1} + max={300} />
( />
)} + {typeof value === 'number' && isShowCount && ( +
+ +
+ )}
); diff --git a/apps/frontend/src/utils/validators.ts b/apps/frontend/src/utils/validators.ts index 4ab4fbe18..a4fa943fa 100644 --- a/apps/frontend/src/utils/validators.ts +++ b/apps/frontend/src/utils/validators.ts @@ -39,10 +39,11 @@ export const validateQuizZoneSetDescription = (description: string) => { export const validateQuizZoneSetCode = (code: string) => { if (code.length < 5) return '5자 이상 입력해주세요.'; if (code.length > 10) return '10자 이하로 입력해주세요.'; + if (!/^[a-zA-Z0-9]*$/g.test(code)) return '숫자와 알파벳 조합만 가능합니다.'; }; //입장 인원 제한 유효성 검사 export const validateQuizZoneSetLimit = (limit: number) => { if (limit < 1) return '최소 1명 이상 지정해주세요.'; - // if (limit > 300) return '최대 인원은 300명입니다.'; + if (limit > 300) return '최대 인원은 300명입니다.'; }; From c2f5df3c7c4b32e187fc6bf797bff14e7b325f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=ED=98=84=EB=AF=BC?= <77275989+joyjhm@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:24:33 +0900 Subject: [PATCH 36/38] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/quiz-zone/quiz-zone.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts b/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts index 847367989..b6a8e13de 100644 --- a/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts +++ b/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts @@ -602,7 +602,7 @@ describe('QuizZoneService', () => { {id: "player1", nickname: "미친투사", score: 0, ranking: 1}, {id: "player2", nickname: "미친투사", score: 0, ranking: 1} ], - endSocketTime: Date.now(), + endSocketTime: expect.any(Number), }); }); From 1e55b1a5832fcbe6f6885fa59eef31d791d2cbbd Mon Sep 17 00:00:00 2001 From: mario Date: Thu, 5 Dec 2024 23:42:01 +0900 Subject: [PATCH 37/38] =?UTF-8?q?fix:=20=EB=B6=80=ED=95=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts | 2 +- apps/frontend/src/utils/validators.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts b/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts index 008433d71..bb257971a 100644 --- a/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts +++ b/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts @@ -24,7 +24,7 @@ export class CreateQuizZoneDto { @IsInt({ message: '최대 플레이어 수가 없습니다.' }) @Min(1, { message: '최소 1명 이상이어야 합니다.' }) - // @Max(300, { message: '최대 300명까지 가능합니다.' }) + @Max(300, { message: '최대 300명까지 가능합니다.' }) readonly limitPlayerCount: number; @IsNotEmpty({ message: '퀴즈존을 선택해주세요.' }) diff --git a/apps/frontend/src/utils/validators.ts b/apps/frontend/src/utils/validators.ts index 4ab4fbe18..8397f4c72 100644 --- a/apps/frontend/src/utils/validators.ts +++ b/apps/frontend/src/utils/validators.ts @@ -44,5 +44,5 @@ export const validateQuizZoneSetCode = (code: string) => { //입장 인원 제한 유효성 검사 export const validateQuizZoneSetLimit = (limit: number) => { if (limit < 1) return '최소 1명 이상 지정해주세요.'; - // if (limit > 300) return '최대 인원은 300명입니다.'; + if (limit > 300) return '최대 인원은 300명입니다.'; }; From b90d67256c0c48beb32f14f9e2bb012ed9a410f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=84=ED=98=84=EB=AF=BC?= <77275989+joyjhm@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:52:47 +0900 Subject: [PATCH 38/38] =?UTF-8?q?chore:=20action=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-backend.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 648dd9520..1968888a1 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -1,8 +1,8 @@ name: backend test on: - push: - pull_request: [develop, main] + pull_request: + branches: [develop, main] paths: - apps/backend/**