diff --git a/packages/client/src/app/routes/Router.tsx b/packages/client/src/app/routes/Router.tsx index 0007b46d..0f4920e6 100644 --- a/packages/client/src/app/routes/Router.tsx +++ b/packages/client/src/app/routes/Router.tsx @@ -27,16 +27,14 @@ export default function Router() { }> }> - } /> } /> } /> } /> } /> - + } /> } /> - } /> } /> diff --git a/packages/client/src/pages/leaderboard/index.tsx b/packages/client/src/pages/leaderboard/index.tsx index 9af0f0af..0458424b 100644 --- a/packages/client/src/pages/leaderboard/index.tsx +++ b/packages/client/src/pages/leaderboard/index.tsx @@ -1,10 +1,13 @@ -import { useParams } from 'react-router-dom'; +import { useNavigate, 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'; +import { deleteCookie } from '@/shared/utils/cookie'; +import { LogOut } from 'lucide-react'; +import LoadingSpinner from '@/shared/assets/icons/loading-alt-loop.svg?react'; export interface Ranking { nickname: string; @@ -17,22 +20,39 @@ export default function Leaderboard() { const socket = getQuizSocket(); const { data, isLoading } = useLeaderboard(socket, pinCode ?? ''); + const navigate = useNavigate(); if (isLoading) { - return
Loading...
; + return ( +
+ +
+ ); } if (data) { - //TODO: 쿠키 삭제 로직 + deleteCookie('sid'); } return (
- - 최종 결과 🎉 - +
+ + 최종 결과 🎉 + + +
@@ -48,7 +68,9 @@ export default function Leaderboard() {
평균 점수
-
{data?.averageScore}점
+
+ {Number(data?.averageScore.toFixed(1))}점 +
diff --git a/packages/client/src/pages/main/index.tsx b/packages/client/src/pages/main/index.tsx index d575008b..fd99475a 100644 --- a/packages/client/src/pages/main/index.tsx +++ b/packages/client/src/pages/main/index.tsx @@ -1,9 +1,25 @@ import { toastController } from '@/features/toast/model/toastController'; -import { getPincodeExist, checkPincodePossible } from '@/shared/api/games'; +import { getPincodeExist, checkPincodePossible, checkPincodeStatus } from '@/shared/api/games'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import FloatingSquare from './ui/FloatingSquare'; import FloatingQuestion from './ui/FloatingQuestion'; +import { getCookie } from '@/shared/utils/cookie'; + +const mappingGameStatus = (status: string, pinCode: string) => { + switch (status) { + case 'WAITING': + return `/quiz/wait/${pinCode}`; + case 'IN PROGRESS': + return `/quiz/session/${pinCode}/1`; + case 'LEADERBOARD': + return `/quiz/session/${pinCode}/end`; + case 'END': + return `/quiz/session/${pinCode}/end`; + default: + return '/'; + } +}; export default function MainPage() { const [pinCode, setPinCode] = useState(''); @@ -18,12 +34,24 @@ export default function MainPage() { const response = await getPincodeExist(pinCode); if (response.isExist) { - const checkResponse = await checkPincodePossible(pinCode); - console.log(checkResponse); - if (checkResponse.isPossible) { - navigate(`/nickname/${pinCode}`); + const sid = getCookie('sid'); + if (sid) { + const response = await checkPincodeStatus(pinCode, sid); + if (response.isPossible) { + const status = response.gameStatus; + const path = mappingGameStatus(status, pinCode); + navigate(path); + } else { + toast.info('게임이 종료되었습니다.'); + } } else { - toast.warning('방이 가득 찼습니다.'); + const checkResponse = await checkPincodePossible(pinCode); + console.log(checkResponse); + if (checkResponse.isPossible) { + navigate(`/nickname/${pinCode}`); + } else { + toast.warning('방이 가득 찼습니다.'); + } } return; } diff --git a/packages/client/src/pages/quiz-list/ui/ClassItem.tsx b/packages/client/src/pages/quiz-list/ui/ClassItem.tsx index 7495e053..d8e4064b 100644 --- a/packages/client/src/pages/quiz-list/ui/ClassItem.tsx +++ b/packages/client/src/pages/quiz-list/ui/ClassItem.tsx @@ -41,7 +41,6 @@ export default function ClassItem({ index, quizList }: ClassItemProps) { setCookie('sid', sid); const pinCode = await waitForSocketEvent('pincode', socket); - setCookie('pinCode', pinCode); navigate(`/quiz/wait/${pinCode}`); }; diff --git a/packages/client/src/pages/quiz-master-session/index.lazy.tsx b/packages/client/src/pages/quiz-master-session/index.lazy.tsx index 0d5cbc67..cdadbc29 100644 --- a/packages/client/src/pages/quiz-master-session/index.lazy.tsx +++ b/packages/client/src/pages/quiz-master-session/index.lazy.tsx @@ -49,6 +49,7 @@ export default function QuizMasterSessionLazyPage() { quizData={quiz.currentQuizData} initializeStates={initializeStates} setInitializeStates={setInitializeStates} + totalParticipants={quiz.participantLength} /> ); diff --git a/packages/client/src/pages/quiz-master-session/ui/AnswerChart.tsx b/packages/client/src/pages/quiz-master-session/ui/AnswerChart.tsx index 724b6140..35b5ef23 100644 --- a/packages/client/src/pages/quiz-master-session/ui/AnswerChart.tsx +++ b/packages/client/src/pages/quiz-master-session/ui/AnswerChart.tsx @@ -16,7 +16,7 @@ import { QuizData } from '@youquiz/shared/interfaces/utils/quizdata.interface'; interface AnswerStatProps { answerStats: MasterStatisticsResponse['choiceStatus']; quizData: QuizData; - participantCount: number; + totalParticipants: number; } const calculateTickCount = (maxValue: number): number => { @@ -28,13 +28,13 @@ const calculateTickCount = (maxValue: number): number => { return Math.max(divisors[Math.min(divisors.length - 1, maxTicks - 1)], 2); }; -export default function AnswerGraph({ answerStats, quizData, participantCount }: AnswerStatProps) { +export default function AnswerGraph({ answerStats, quizData, totalParticipants }: AnswerStatProps) { const answerStatsArray = quizData.choices.map((choice, index) => ({ answer: `${index + 1}번: ${choice.content} ${choice.isCorrect ? '(정답)' : ''}`, count: answerStats[index] || 0, isCorrect: choice.isCorrect, })); - const tickCount = calculateTickCount(participantCount); + const tickCount = calculateTickCount(totalParticipants); return ( @@ -64,7 +64,7 @@ export default function AnswerGraph({ answerStats, quizData, participantCount }: axisLine={false} tickLine={false} tickCount={tickCount} - domain={[0, participantCount]} + domain={[0, totalParticipants]} /> [`${value}명`, '참여자 수']} /> '참여자 수'} /> diff --git a/packages/client/src/pages/quiz-master-session/ui/Statistics.tsx b/packages/client/src/pages/quiz-master-session/ui/Statistics.tsx index 016db9ed..234d25ca 100644 --- a/packages/client/src/pages/quiz-master-session/ui/Statistics.tsx +++ b/packages/client/src/pages/quiz-master-session/ui/Statistics.tsx @@ -13,6 +13,7 @@ interface StatisticsProps { quizData: QuizData; initializeStates: boolean; setInitializeStates: React.Dispatch>; + totalParticipants: number; } interface HistoryItem { @@ -26,6 +27,7 @@ export default function Statistics({ quizData, initializeStates, setInitializeStates, + totalParticipants, }: StatisticsProps) { const socket = getQuizSocket(); const [masterStatistics, setMasterStatistics] = usePersistState( @@ -72,7 +74,7 @@ export default function Statistics({
diff --git a/packages/client/src/pages/quiz-session/index.lazy.tsx b/packages/client/src/pages/quiz-session/index.lazy.tsx index 463ec74d..58c7d9de 100644 --- a/packages/client/src/pages/quiz-session/index.lazy.tsx +++ b/packages/client/src/pages/quiz-session/index.lazy.tsx @@ -12,7 +12,6 @@ export default function QuizSessionLazyPage() { const { pinCode } = useParams(); const [isQuizEnd, setIsQuizEnd] = usePersistState('isQuizEnd', false); const { data: quiz, refetch } = useQuizSession({ socket, pinCode: pinCode as string }); - return ( <> {!isQuizEnd && ( @@ -21,8 +20,13 @@ export default function QuizSessionLazyPage() { startTime={quiz.startTime} timeLimit={quiz.currentQuizData.timeLimit} setQuizEnd={setIsQuizEnd} + totalParticipants={quiz.participantLength} + /> + -
)} {isQuizEnd && ( diff --git a/packages/client/src/pages/quiz-session/model/hooks/useGetMyInfo.ts b/packages/client/src/pages/quiz-session/model/hooks/useGetMyInfo.ts new file mode 100644 index 00000000..e55deeb9 --- /dev/null +++ b/packages/client/src/pages/quiz-session/model/hooks/useGetMyInfo.ts @@ -0,0 +1,17 @@ +import { getCookie } from '@/shared/utils/cookie'; +import { emitEventWithAck } from '@/shared/utils/emitEventWithAck'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { Socket } from 'socket.io-client'; + +interface UseGetMyInfoProps { + socket: Socket; +} + +export const useGetMyInfo = ({ socket }: UseGetMyInfoProps) => { + return useSuspenseQuery({ + queryKey: ['myInfo'], + queryFn: () => { + return emitEventWithAck(socket, 'my info', { sid: getCookie('sid') }); + }, + }); +}; diff --git a/packages/client/src/pages/quiz-session/model/hooks/useQuizSession.ts b/packages/client/src/pages/quiz-session/model/hooks/useQuizSession.ts index 0e80a3fe..c799896d 100644 --- a/packages/client/src/pages/quiz-session/model/hooks/useQuizSession.ts +++ b/packages/client/src/pages/quiz-session/model/hooks/useQuizSession.ts @@ -12,6 +12,7 @@ interface ShowQuizResponse { currentQuizData: QuizData; isLast: boolean; startTime: number; + participantLength: number; } export const useQuizSession = ({ socket, pinCode }: UseQuizSessionProps) => { diff --git a/packages/client/src/pages/quiz-session/ui/ProgressBar.tsx b/packages/client/src/pages/quiz-session/ui/ProgressBar.tsx new file mode 100644 index 00000000..51df29ef --- /dev/null +++ b/packages/client/src/pages/quiz-session/ui/ProgressBar.tsx @@ -0,0 +1,64 @@ +interface ProgressBarProps { + /** 전체 시간 설정 */ + time: number; + /** 프로그래스바 타입 */ + type: 'success' | 'warning' | 'error' | 'info' | 'gradient'; + /** 프로그래스바 모양 */ + barShape?: 'rounded' | 'square'; + /** 마우스 호버 시 정지 여부 */ + pauseOnHover?: boolean; + /** 애니메이션 종료 시 콜백 함수 */ + handleAnimationEnd?: () => void; + /** 현재 진행 시간 */ + currentTime?: number; +} + +const progressBarColors = { + success: 'bg-secondary', + warning: 'bg-yellow-500', + error: 'bg-red-500', + info: 'bg-blue-500', + gradient: 'bg-gradient-to-r from-red-500 to-orange-500', +}; + +const progressBarShapes = { + rounded: 'rounded-r-base', + square: 'rounded-none', +}; + +const ProgressBar = ({ + time = 5, + type = 'success', + barShape = 'rounded', + pauseOnHover, + handleAnimationEnd, + currentTime = 0, +}: ProgressBarProps) => { + const progressBarColor = progressBarColors[type]; + const progressBarShape = progressBarShapes[barShape]; + + // 현재 시간에 따른 진행률 계산 + const progress = Math.min(Math.max((currentTime / time) * 100, 0), 100); + // 남은 시간 비율 계산 + const remainingTimeRatio = 1 - currentTime / time; + // 애니메이션 지속 시간 계산 (초 단위) + const animationDuration = time * remainingTimeRatio; + + return ( +
+
+
+
+
+ ); +}; + +export default ProgressBar; diff --git a/packages/client/src/pages/quiz-session/ui/QuizBox.tsx b/packages/client/src/pages/quiz-session/ui/QuizBox.tsx index 18509a2d..5ea41c94 100644 --- a/packages/client/src/pages/quiz-session/ui/QuizBox.tsx +++ b/packages/client/src/pages/quiz-session/ui/QuizBox.tsx @@ -11,9 +11,10 @@ import { usePersistState } from '@/shared/hooks/usePersistState'; interface QuizBoxProps { quiz: QuizData; startTime: number; + quizMaxNum: number; } -export default function QuizBox({ quiz, startTime }: QuizBoxProps) { +export default function QuizBox({ quiz, startTime, quizMaxNum }: QuizBoxProps) { const { pinCode } = useParams(); const [selectedAnswer, setSelectedAnswer] = useState([]); const [hasSubmitted, setHasSubmitted] = usePersistState('hasSubmitted', false); @@ -99,13 +100,8 @@ export default function QuizBox({ quiz, startTime }: QuizBoxProps) {
- Question {quiz.position}/10 - 난이도 -
-
-
-
+ Question {quiz.position + 1}/{quizMaxNum + 1}
{/* 문제 */} @@ -159,7 +155,7 @@ export default function QuizBox({ quiz, startTime }: QuizBoxProps) {