Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b5a1af2
feat: TypeScript ์ปดํŒŒ์ผ๋Ÿฌ ์„ค์ •์— ์›น ์›Œ์ปค ์ง€์› ์ถ”๊ฐ€
krokerdile Dec 5, 2024
a78f6f9
feat: Vite ์›น ์›Œ์ปค ์„ค์ • ์ถ”๊ฐ€
krokerdile Dec 5, 2024
031bdd4
feat: ํƒ€์ด๋จธ ์›น ์›Œ์ปค ๊ตฌํ˜„
krokerdile Dec 5, 2024
ba84644
feat: ํƒ€์ด๋จธ ์›น ์›Œ์ปค ๋ฉ”์‹œ์ง€ ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€
krokerdile Dec 5, 2024
3c19988
refactor: ํƒ€์ด๋จธ ์ปค์Šคํ…€ ํ›… ์›น ์›Œ์ปค ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐœ์„ 
krokerdile Dec 5, 2024
212222d
refactor: ํ€ด์ฆˆ ์ง„ํ–‰ ์ปดํฌ๋„ŒํŠธ์— ํƒ€์ด๋จธ ํ›… ์ ์šฉ
krokerdile Dec 5, 2024
4e6415c
refactor: ProgressBar ์ปดํฌ๋„ŒํŠธ ๋‹จ์ˆœํ™” ๋ฐ ํƒ€์ด๋จธ ์—ฐ๋™
krokerdile Dec 5, 2024
eb11ffd
feat: quizZone Controller respsonse serverTime ์ถ”๊ฐ€
krokerdile Dec 5, 2024
721ffd7
feat: QuizZone Type serverTime, offset ์ถ”๊ฐ€
krokerdile Dec 5, 2024
1170efb
feat: init ํ•  ์‹œ์— serverTime ๊ธฐ๋ฐ˜ offset ๊ณ„์‚ฐ ๋กœ์ง ์ถ”๊ฐ€
krokerdile Dec 5, 2024
9ebc249
feat: offset ๊ณ„์‚ฐ์„ ์œ„ํ•œ now(ํ˜„์žฌ ์‹œ๊ฐ) ์ „๋‹ฌ
krokerdile Dec 5, 2024
2a52cca
Merge branch 'develop' into feature/web-worker-time-sync
krokerdile Dec 5, 2024
ed9f55c
feat: quizZoneInfo await ์ ์šฉ
krokerdile Dec 5, 2024
4fd47a6
feat: NotFound Page ์ถ”๊ฐ€
krokerdile Dec 5, 2024
7072fe6
fix: ProgressBar ์Šคํ† ๋ฆฌ ์ˆ˜์ •
krokerdile Dec 5, 2024
97e466d
fix: QuizZoneInProgress undefined ํ•ด๊ฒฐ
krokerdile Dec 5, 2024
f3eb8da
fix: e2e ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ •
codemario318 Dec 5, 2024
46029dd
Merge remote-tracking branch 'origin/feature/web-worker-time-sync' inโ€ฆ
codemario318 Dec 5, 2024
be09b68
remove: useTimer.test ์‚ญ์ œ
krokerdile Dec 5, 2024
613bd91
test: useValidInput ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€
krokerdile Dec 5, 2024
b4ea3c5
test: useQuizZone test ์ถ”๊ฐ€
krokerdile Dec 5, 2024
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
3 changes: 3 additions & 0 deletions apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions apps/backend/src/quiz-zone/quiz-zone.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,16 @@ export class QuizZoneController {
@Session() session: Record<string, any>,
@Param('quizZoneId') quizZoneId: string,
) {
const quizZoneInfo = this.quizZoneService.getQuizZoneInfo(
const serverTime = Date.now();
const quizZoneInfo = await this.quizZoneService.getQuizZoneInfo(
session.id,
quizZoneId,
session.quizZoneId,
);
session['quizZoneId'] = quizZoneId;
return quizZoneInfo;
return {
...quizZoneInfo,
serverTime,
};
}
}
19 changes: 16 additions & 3 deletions apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
Expand All @@ -25,7 +38,7 @@ const QuizInProgress = ({ currentQuiz, submitAnswer }: QuizInProgressProps) => {

return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<ProgressBar deadlineTime={currentQuiz.deadlineTime} onTimeEnd={() => {}} />
<ProgressBar time={time} playTime={playTime} onTimeEnd={() => {}} />
<ContentBox className="w-full flex flex-col gap-4 bg-white shadow-lg">
<Typography
text={`Q. ${currentQuiz.question}`}
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const QuizZoneInProgress = ({ quizZoneState, submitAnswer, playQuiz }: QuizZoneI
return (
<QuizInProgress
playTime={playTime!}
currentQuiz={currentQuiz}
currentQuiz={currentQuiz!}
submitAnswer={submitAnswer}
/>
);
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/components/common/ProgressBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ type Story = StoryObj<typeof ProgressBar>;

export const Default: Story = {
args: {
deadlineTime: Date.now() + 30000,
playTime: 30000,
time: Date.now(),
onTimeEnd: () => {
alert('time end');
},
Expand Down
74 changes: 15 additions & 59 deletions apps/frontend/src/components/common/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -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
* <ProgressBar
* deadlineTime={new Date(Date.now() + 30000)}
* playTime={30}
* time={25}
* onTimeEnd={() => 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<number>(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 (
<div className="w-full flex justify-center flex-col items-center gap-2">
<Progress value={progress} max={100} className="max-w-[60%]" />
<Typography
text={`๋‚จ์€์‹œ๊ฐ„ ${Math.max(0, remainingSeconds).toFixed(0)}์ดˆ`}
text={`๋‚จ์€์‹œ๊ฐ„ ${Math.max(0, time).toFixed(0)}์ดˆ`}
size="xl"
color="black"
bold={true}
Expand Down
102 changes: 102 additions & 0 deletions apps/frontend/src/hook/quizZone/useQuizZone.test.ts
Original file line number Diff line number Diff line change
@@ -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<QuizZone> = {
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');
});
});
});
33 changes: 16 additions & 17 deletions apps/frontend/src/hook/quizZone/useQuizZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useReducer } from 'react';
import useWebSocket from '@/hook/useWebSocket.tsx';
import {
ChatMessage,
InitQuizZoneResponse,
NextQuizResponse,
Player,
QuizZone,
Expand All @@ -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 }
Expand All @@ -39,23 +40,19 @@ const quizZoneReducer: Reducer<QuizZone, QuizZoneAction> = (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':
Expand Down Expand Up @@ -121,8 +118,8 @@ const quizZoneReducer: Reducer<QuizZone, QuizZoneAction> = (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: {
Expand Down Expand Up @@ -162,7 +159,7 @@ const quizZoneReducer: Reducer<QuizZone, QuizZoneAction> = (state, action) => {
submits: payload.submits,
quizzes: payload.quizzes,
ranks: payload.ranks,
endSocketTime: payload.endSocketTime,
endSocketTime: payload.endSocketTime - state.offset,
};
case 'chat':
return {
Expand Down Expand Up @@ -241,6 +238,8 @@ const useQuizZone = (quizZoneId: string, handleReconnect?: () => void) => {
quizzes: [],
chatMessages: [],
maxPlayers: 0,
offset: 0,
serverTime: 0,
};

const [quizZoneState, dispatch] = useReducer(quizZoneReducer, initialQuizZoneState);
Expand All @@ -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 });
};
Expand Down
Loading
Loading