diff --git a/.pnp.cjs b/.pnp.cjs
index 0765d230..eb78e062 100755
--- a/.pnp.cjs
+++ b/.pnp.cjs
@@ -4255,6 +4255,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["@tanstack/query-devtools", [\
+ ["npm:5.61.4", {\
+ "packageLocation": "./.yarn/cache/@tanstack-query-devtools-npm-5.61.4-757435dc28-44886dbf92.zip/node_modules/@tanstack/query-devtools/",\
+ "packageDependencies": [\
+ ["@tanstack/query-devtools", "npm:5.61.4"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["@tanstack/react-query", [\
["npm:5.59.19", {\
"packageLocation": "./.yarn/cache/@tanstack-react-query-npm-5.59.19-d49be866ba-1640320168.zip/node_modules/@tanstack/react-query/",\
@@ -4278,6 +4287,33 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["@tanstack/react-query-devtools", [\
+ ["npm:5.62.0", {\
+ "packageLocation": "./.yarn/cache/@tanstack-react-query-devtools-npm-5.62.0-c120ddf122-b5f94368d8.zip/node_modules/@tanstack/react-query-devtools/",\
+ "packageDependencies": [\
+ ["@tanstack/react-query-devtools", "npm:5.62.0"]\
+ ],\
+ "linkType": "SOFT"\
+ }],\
+ ["virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:5.62.0", {\
+ "packageLocation": "./.yarn/__virtual__/@tanstack-react-query-devtools-virtual-d4e4f56ead/0/cache/@tanstack-react-query-devtools-npm-5.62.0-c120ddf122-b5f94368d8.zip/node_modules/@tanstack/react-query-devtools/",\
+ "packageDependencies": [\
+ ["@tanstack/react-query-devtools", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:5.62.0"],\
+ ["@tanstack/query-devtools", "npm:5.61.4"],\
+ ["@tanstack/react-query", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:5.59.19"],\
+ ["@types/react", "npm:18.3.12"],\
+ ["@types/tanstack__react-query", null],\
+ ["react", "npm:18.3.1"]\
+ ],\
+ "packagePeers": [\
+ "@tanstack/react-query",\
+ "@types/react",\
+ "@types/tanstack__react-query",\
+ "react"\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["@testing-library/dom", [\
["npm:10.4.0", {\
"packageLocation": "./.yarn/cache/@testing-library-dom-npm-10.4.0-a0d2ca848e-0352487720.zip/node_modules/@testing-library/dom/",\
@@ -6789,6 +6825,7 @@ const RAW_RUNTIME_STATE =
["@storybook/react-vite", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:8.4.1"],\
["@storybook/test", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:8.4.1"],\
["@tanstack/react-query", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:5.59.19"],\
+ ["@tanstack/react-query-devtools", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:5.62.0"],\
["@types/node", "npm:22.9.0"],\
["@types/react", "npm:18.3.12"],\
["@types/react-dom", "npm:18.3.1"],\
diff --git a/.yarn/cache/@tanstack-query-devtools-npm-5.61.4-757435dc28-44886dbf92.zip b/.yarn/cache/@tanstack-query-devtools-npm-5.61.4-757435dc28-44886dbf92.zip
new file mode 100644
index 00000000..935a1569
Binary files /dev/null and b/.yarn/cache/@tanstack-query-devtools-npm-5.61.4-757435dc28-44886dbf92.zip differ
diff --git a/.yarn/cache/@tanstack-react-query-devtools-npm-5.62.0-c120ddf122-b5f94368d8.zip b/.yarn/cache/@tanstack-react-query-devtools-npm-5.62.0-c120ddf122-b5f94368d8.zip
new file mode 100644
index 00000000..58a2d09b
Binary files /dev/null and b/.yarn/cache/@tanstack-react-query-devtools-npm-5.62.0-c120ddf122-b5f94368d8.zip differ
diff --git a/packages/client/package.json b/packages/client/package.json
index 43d19201..82da9a74 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"@tanstack/react-query": "^5.59.19",
+ "@tanstack/react-query-devtools": "^5.62.0",
"@youquiz/shared": "workspace:*",
"eslint": "^9.13.0",
"lucide-react": "^0.462.0",
diff --git a/packages/client/src/app/routes/Router.tsx b/packages/client/src/app/routes/Router.tsx
index bc25553b..b1066da4 100644
--- a/packages/client/src/app/routes/Router.tsx
+++ b/packages/client/src/app/routes/Router.tsx
@@ -35,7 +35,7 @@ export default function Router() {
} />
- } />
+ } />
} />
);
diff --git a/packages/client/src/main.tsx b/packages/client/src/main.tsx
index 6b8a8349..6b43e213 100644
--- a/packages/client/src/main.tsx
+++ b/packages/client/src/main.tsx
@@ -5,14 +5,24 @@ import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from '@/app';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
-const queryClient = new QueryClient();
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ refetchOnWindowFocus: false,
+ staleTime: 1000 * 60 * 10,
+ },
+ },
+});
createRoot(document.getElementById('root')!).render(
+
,
diff --git a/packages/client/src/pages/leaderboard/ConfettiBackground.tsx b/packages/client/src/pages/leaderboard/ConfettiBackground.tsx
new file mode 100644
index 00000000..5baaacd8
--- /dev/null
+++ b/packages/client/src/pages/leaderboard/ConfettiBackground.tsx
@@ -0,0 +1,21 @@
+export default function ConfettiBackground() {
+ return (
+
+ {Array.from({ length: 20 }).map((_, index) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/client/src/pages/leaderboard/PlayerList.tsx b/packages/client/src/pages/leaderboard/PlayerList.tsx
new file mode 100644
index 00000000..763a7dfa
--- /dev/null
+++ b/packages/client/src/pages/leaderboard/PlayerList.tsx
@@ -0,0 +1,46 @@
+import { Ranking } from '.';
+
+interface PlayerListProps {
+ players: Ranking[];
+}
+
+export default function PlayerList({ players }: PlayerListProps) {
+ return (
+
+ {players.map((player, index) => (
+
+ {/* Rank */}
+
+ {index + 4}
+
+
+ {/* Status & Name */}
+
+ {/*
*/}
+
{player.nickname}
+
+
+ {/* Stats */}
+
+ {/*
*/}
+ {/*
+
+ {player.correct}/{player.total}
+
+
+
+
+ {player.speed}s
+
*/}
+
{player.score}점
+
+
+ ))}
+
+ );
+}
diff --git a/packages/client/src/pages/leaderboard/TopPlayer.tsx b/packages/client/src/pages/leaderboard/TopPlayer.tsx
new file mode 100644
index 00000000..b9e05c62
--- /dev/null
+++ b/packages/client/src/pages/leaderboard/TopPlayer.tsx
@@ -0,0 +1,72 @@
+import { Ranking } from '.';
+import DogImage from '@/shared/assets/characters/강아지.png';
+import CatImage from '@/shared/assets/characters/고양이.png';
+import PigImage from '@/shared/assets/characters/돼지.png';
+import RabbitImage from '@/shared/assets/characters/토끼.png';
+import PenguinImage from '@/shared/assets/characters/펭귄.png';
+import HamsterImage from '@/shared/assets/characters/햄스터.png';
+
+interface TopPlayerProps {
+ players: Ranking[];
+}
+
+const characters = [DogImage, CatImage, PigImage, RabbitImage, PenguinImage, HamsterImage];
+const characterNames = ['강아지', '고양이', '돼지', '토끼', '펭귄', '햄스터'];
+
+const Nickname = ({ nickname }: { nickname: string }) => {
+ return (
+
+ {nickname}
+
+ );
+};
+export default function TopPlayer({ players }: TopPlayerProps) {
+ return (
+
+
+
+
+
+
+
+ {players[1]?.score}
+
+
+
+
+
+
+
+
+
+
+
+ {players[0]?.score}
+
+
+
+
+
+
+
+
+
+
+
+ {players[2]?.score}
+
+
+
+
+
+ );
+}
diff --git a/packages/client/src/pages/leaderboard/index.tsx b/packages/client/src/pages/leaderboard/index.tsx
index 674e7c31..9af0f0af 100644
--- a/packages/client/src/pages/leaderboard/index.tsx
+++ b/packages/client/src/pages/leaderboard/index.tsx
@@ -1,3 +1,58 @@
+import { useParams } from 'react-router-dom';
+
+import { getQuizSocket } from '@/shared/utils/socket';
+import TopPlayer from './TopPlayer';
+import PlayerList from './PlayerList';
+import ConfettiBackground from './ConfettiBackground';
+import { useLeaderboard } from './model/hooks/useLeaderboard';
+
+export interface Ranking {
+ nickname: string;
+ score: number;
+ character: number;
+}
+
export default function Leaderboard() {
- return Leaderboard
;
+ const { pinCode } = useParams();
+
+ const socket = getQuizSocket();
+ const { data, isLoading } = useLeaderboard(socket, pinCode ?? '');
+
+ if (isLoading) {
+ return Loading...
;
+ }
+
+ if (data) {
+ //TODO: 쿠키 삭제 로직
+ }
+
+ return (
+
+
+
+
+ 최종 결과 🎉
+
+
+ {/* 랭킹 리스트 */}
+
+
+
+
+
참가자
+
{data?.participantNumber}명
+
+
+
평균 점수
+
{data?.averageScore}점
+
+
+
+
+
+ );
}
diff --git a/packages/client/src/pages/leaderboard/model/hooks/useLeaderboard.ts b/packages/client/src/pages/leaderboard/model/hooks/useLeaderboard.ts
new file mode 100644
index 00000000..753f038f
--- /dev/null
+++ b/packages/client/src/pages/leaderboard/model/hooks/useLeaderboard.ts
@@ -0,0 +1,18 @@
+import { useQuery } from '@tanstack/react-query';
+import { Socket } from 'socket.io-client';
+
+import { Ranking } from '../..';
+import { emitEventWithAck } from '@/shared/utils/emitEventWithAck';
+
+interface LeaderboardData {
+ rankerData: Ranking[];
+ participantNumber: number;
+ averageScore: number;
+}
+
+export const useLeaderboard = (socket: Socket, pinCode: string) => {
+ return useQuery({
+ queryKey: ['leaderboard', pinCode],
+ queryFn: () => emitEventWithAck(socket, 'leaderboard', { pinCode }),
+ });
+};
diff --git a/packages/client/src/pages/quiz-master-session/index.tsx b/packages/client/src/pages/quiz-master-session/index.tsx
index d075116f..363b7a6e 100644
--- a/packages/client/src/pages/quiz-master-session/index.tsx
+++ b/packages/client/src/pages/quiz-master-session/index.tsx
@@ -50,7 +50,7 @@ export default function QuizMasterSession() {
const handleNextQuiz = () => {
if (isLastQuiz) {
socket.emit('end quiz', { pinCode, sid: getCookie('sid') });
- navigate('/quiz/session/end');
+ navigate(`/quiz/session/${pinCode}/end`);
return;
}
if (Math.floor(tick.remainingTime / 1000) === 0) {
diff --git a/packages/client/src/pages/quiz-session/index.tsx b/packages/client/src/pages/quiz-session/index.tsx
index c66f8b4f..3b30980c 100644
--- a/packages/client/src/pages/quiz-session/index.tsx
+++ b/packages/client/src/pages/quiz-session/index.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useParams } from 'react-router-dom';
import { getQuizSocket } from '@/shared/utils/socket';
import QuizBox from './ui/QuizBox';
@@ -15,6 +15,7 @@ export default function QuizSession() {
const socket = getQuizSocket();
// const toast = toastController();
const navigate = useNavigate();
+ const { pinCode } = useParams();
const [isLoading, setIsLoading] = useState(true);
const [isQuizEnd, setIsQuizEnd] = useState(false);
const [tick, setTick] = useState(INITIAL_TICK);
@@ -67,7 +68,7 @@ export default function QuizSession() {
};
const handleQuizEnd = () => {
- navigate('/quiz/session/end');
+ navigate(`/quiz/session/${pinCode}/end`);
};
socket.on('end quiz', handleQuizEnd);
diff --git a/packages/client/src/shared/utils/emitEventWithAck.ts b/packages/client/src/shared/utils/emitEventWithAck.ts
new file mode 100644
index 00000000..12984862
--- /dev/null
+++ b/packages/client/src/shared/utils/emitEventWithAck.ts
@@ -0,0 +1,13 @@
+import { Socket } from 'socket.io-client';
+
+export const emitEventWithAck = (socket: Socket, event: string, data: any) => {
+ return new Promise((resolve, reject) => {
+ socket.emit(event, data, (response: T) => {
+ if (response) {
+ resolve(response);
+ } else {
+ reject(new Error(`"${event}" event emit failed`));
+ }
+ });
+ });
+};
diff --git a/packages/client/tailwind.config.js b/packages/client/tailwind.config.js
index 157c8fa3..0bd8e23b 100644
--- a/packages/client/tailwind.config.js
+++ b/packages/client/tailwind.config.js
@@ -101,6 +101,10 @@ export default {
'50%': { transform: 'translateY(-20px) rotate(5deg)', opacity: 1 },
'100%': { transform: 'translateY(0) rotate(0deg)', opacity: 0.8 },
},
+ confetti: {
+ '0%': { transform: 'translateY(0) rotate(0deg)', opacity: 0 },
+ '100%': { transform: 'translateY(100vh) rotate(720deg)', opacity: 1 },
+ },
},
},
},
diff --git a/yarn.lock b/yarn.lock
index 95f02a0e..c01b918c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2546,6 +2546,25 @@ __metadata:
languageName: node
linkType: hard
+"@tanstack/query-devtools@npm:5.61.4":
+ version: 5.61.4
+ resolution: "@tanstack/query-devtools@npm:5.61.4"
+ checksum: 10c0/44886dbf92849d17729bf779a184a3fd304711d8ce8061c1deccc089a65d168ed8f0cf0da43b59a9a22a9afb7b25fba99de8af47fe44479ac3082d852dcaaf53
+ languageName: node
+ linkType: hard
+
+"@tanstack/react-query-devtools@npm:^5.62.0":
+ version: 5.62.0
+ resolution: "@tanstack/react-query-devtools@npm:5.62.0"
+ dependencies:
+ "@tanstack/query-devtools": "npm:5.61.4"
+ peerDependencies:
+ "@tanstack/react-query": ^5.62.0
+ react: ^18 || ^19
+ checksum: 10c0/b5f94368d83c3178fc0a53f917d801a5f09de91fd481a04efeb531e8390867f7abc5e63f96944a99269d8ca827bf802429c71c3e88a0286686e62d73497bf548
+ languageName: node
+ linkType: hard
+
"@tanstack/react-query@npm:^5.59.19":
version: 5.59.19
resolution: "@tanstack/react-query@npm:5.59.19"
@@ -4517,6 +4536,7 @@ __metadata:
"@storybook/react-vite": "npm:^8.4.1"
"@storybook/test": "npm:^8.4.1"
"@tanstack/react-query": "npm:^5.59.19"
+ "@tanstack/react-query-devtools": "npm:^5.62.0"
"@types/node": "npm:^22.9.0"
"@types/react": "npm:^18.3.12"
"@types/react-dom": "npm:^18.3.1"