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 ( +
+
+
+ {`${characterNames[players[1]?.character]}character`} +
+
+ + {players[1]?.score} + +
+ +
+ +
+
+ {`${characterNames[players[0]?.character]}character`} +
+
+ + {players[0]?.score} + +
+ +
+ +
+
+ {`${characterNames[players[2]?.character]}character`} +
+
+ + {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"