Skip to content

Commit

Permalink
[FE] - 퀴즈 진행 관련 에러 해결 (#163)
Browse files Browse the repository at this point in the history
* feat: 리더보드 버튼 추가 및 쿠키 삭제

* feat: api 코드 추가

* fix: 퀴즈 풀이 화면 수정

* feat: 핀코드 입력 시 진행 중인 게임으로 이동하는 로직 추가

* fix: 0보다 작은 경우 quizEnd true로 변경

* feat: show quiz 리턴값에 participantLength 추가

* feat: 퀴즈 참가 인원 데이터를 UI에 추가

* feat: 퀴즈 로딩 fallback 변경

* fix: 퀴즈 평균 점수 소수점 한 자리로 변경

* feat: 퀴즈 헤더 삭제

* fix: height 수정

* feat: 평균 점수 소수점 변경

* feat: my info 이벤트 추가 및 UI 변경

* style: dvh로 변경

* feat: 퀴즈 세션 전용 프로그래스바 추가

* fix: 서버 position 에러 해결 및 my info 이벤트 추가

---------

Co-authored-by: byeong <[email protected]>
  • Loading branch information
dooohun and chan-byeong authored Dec 4, 2024
1 parent 4b09cbc commit b6c8aa3
Show file tree
Hide file tree
Showing 19 changed files with 266 additions and 77 deletions.
4 changes: 1 addition & 3 deletions packages/client/src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,14 @@ export default function Router() {
</Route>
<Route element={<GuestLayout />}>
<Route element={<PreventGuestRouter />}>
<Route path="/quiz/session/:pinCode/:id" element={<QuizSession />} />
<Route path="/quiz/wait/:pinCode" element={<QuizWaitPage />} />
<Route path="/guest/questions" element={<GuestQnA />} />
</Route>
<Route path="/nickname/:pinCode" element={<Nickname />} />
</Route>
<Route path="/quiz/question" element={<QuizQuestion />} />

<Route path="/quiz/session/:pinCode/:id" element={<QuizSession />} />
<Route path="/quiz/session/host/:pinCode/:id" element={<QuizMasterSession />} />

<Route path="/quiz/session/:pinCode/end" element={<Leaderboard />} />
<Route path={'*'} element={<NotFound />} />
</Routes>
Expand Down
36 changes: 29 additions & 7 deletions packages/client/src/pages/leaderboard/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,22 +20,39 @@ export default function Leaderboard() {

const socket = getQuizSocket();
const { data, isLoading } = useLeaderboard(socket, pinCode ?? '');
const navigate = useNavigate();

if (isLoading) {
return <div>Loading...</div>;
return (
<div className="flex items-center justify-center w-screen h-screen">
<LoadingSpinner className="animate-spin w-10 h-10" />
</div>
);
}

if (data) {
//TODO: 쿠키 삭제 로직
deleteCookie('sid');
}

return (
<section className="relative w-dvw min-h-dvh h-fit bg-gradient-to-br from-blue-100 via-white to-purple-100">
<ConfettiBackground />
<div className="w-full max-w-3xl mx-auto p-6 bg-gradient-to-br from-blue-50 via-white to-purple-50 rounded-2xl shadow-xl">
<span className="text-2xl font-bold mb-4 bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
최종 결과 🎉
</span>
<div className="flex items-center justify-between">
<span className="text-2xl font-bold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
최종 결과 🎉
</span>
<button
className="flex items-center px-4 py-2 text-sm font-medium text-gray-600
bg-white border border-gray-200 rounded-full shadow-sm hover:bg-gray-50
hover:text-gray-900 hover:border-gray-300 transition-all duration-200 ease-in-out
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300"
onClick={() => navigate('/')}
>
<LogOut className="h-4 w-4 mr-2" />
나가기
</button>
</div>
<div className="flex justify-center ">
<div className="w-full">
<TopPlayer players={data?.rankerData.slice(0, 3) ?? []} />
Expand All @@ -48,7 +68,9 @@ export default function Leaderboard() {
</div>
<div className="p-2 border-x border-gray-100">
<div className="text-sm text-gray-500 mb-1">평균 점수</div>
<div className="font-bold text-gray-800">{data?.averageScore}</div>
<div className="font-bold text-gray-800">
{Number(data?.averageScore.toFixed(1))}
</div>
</div>
</div>
</div>
Expand Down
40 changes: 34 additions & 6 deletions packages/client/src/pages/main/index.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('');
Expand All @@ -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;
}
Expand Down
1 change: 0 additions & 1 deletion packages/client/src/pages/quiz-list/ui/ClassItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function QuizMasterSessionLazyPage() {
quizData={quiz.currentQuizData}
initializeStates={initializeStates}
setInitializeStates={setInitializeStates}
totalParticipants={quiz.participantLength}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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 (
<ResponsiveContainer width="100%" height="100%">
Expand Down Expand Up @@ -64,7 +64,7 @@ export default function AnswerGraph({ answerStats, quizData, participantCount }:
axisLine={false}
tickLine={false}
tickCount={tickCount}
domain={[0, participantCount]}
domain={[0, totalParticipants]}
/>
<Tooltip formatter={(value: number) => [`${value}명`, '참여자 수']} />
<Legend formatter={() => '참여자 수'} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface StatisticsProps {
quizData: QuizData;
initializeStates: boolean;
setInitializeStates: React.Dispatch<React.SetStateAction<boolean>>;
totalParticipants: number;
}

interface HistoryItem {
Expand All @@ -26,6 +27,7 @@ export default function Statistics({
quizData,
initializeStates,
setInitializeStates,
totalParticipants,
}: StatisticsProps) {
const socket = getQuizSocket();
const [masterStatistics, setMasterStatistics] = usePersistState<MasterStatisticsResponse>(
Expand Down Expand Up @@ -72,7 +74,7 @@ export default function Statistics({
<div className="grid grid-cols-[3fr_1fr] gap-4 mx-5 h-[calc(100vh-300px)]">
<AnswerGraph
answerStats={masterStatistics.choiceStatus}
participantCount={masterStatistics.participantLength}
totalParticipants={totalParticipants}
quizData={quizData}
/>
<div>
Expand Down
8 changes: 6 additions & 2 deletions packages/client/src/pages/quiz-session/index.lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
Expand All @@ -21,8 +20,13 @@ export default function QuizSessionLazyPage() {
startTime={quiz.startTime}
timeLimit={quiz.currentQuizData.timeLimit}
setQuizEnd={setIsQuizEnd}
totalParticipants={quiz.participantLength}
/>
<QuizBox
quiz={quiz.currentQuizData}
startTime={quiz.startTime}
quizMaxNum={quiz.quizMaxNum}
/>
<QuizBox quiz={quiz.currentQuizData} startTime={quiz.startTime} />
</div>
)}
{isQuizEnd && (
Expand Down
17 changes: 17 additions & 0 deletions packages/client/src/pages/quiz-session/model/hooks/useGetMyInfo.ts
Original file line number Diff line number Diff line change
@@ -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<any>(socket, 'my info', { sid: getCookie('sid') });
},
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface ShowQuizResponse {
currentQuizData: QuizData;
isLast: boolean;
startTime: number;
participantLength: number;
}

export const useQuizSession = ({ socket, pinCode }: UseQuizSessionProps) => {
Expand Down
64 changes: 64 additions & 0 deletions packages/client/src/pages/quiz-session/ui/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="group" onAnimationEnd={handleAnimationEnd}>
<div className="h-[6px] w-full bg-transparent">
<div
className={`h-[6px] ${progressBarColor} ${progressBarShape} ${
pauseOnHover && 'group-hover:[animation-play-state:paused]'
}`}
style={{
width: `${progress}%`,
animation: `progress ${animationDuration}s linear forwards`,
}}
/>
</div>
</section>
);
};

export default ProgressBar;
12 changes: 4 additions & 8 deletions packages/client/src/pages/quiz-session/ui/QuizBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number[]>([]);
const [hasSubmitted, setHasSubmitted] = usePersistState('hasSubmitted', false);
Expand Down Expand Up @@ -99,13 +100,8 @@ export default function QuizBox({ quiz, startTime }: QuizBoxProps) {
<div className="relative z-10 p-6 max-w-4xl mx-auto mb-8">
<div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg p-6 mb-12">
<div className="flex items-center justify-between mb-6">
<span className="text-sm text-gray-500">Question {quiz.position}/10</span>
<span className="text-sm text-gray-500">
난이도
<div className="flex items-center">
<div className="w-12 h-2 bg-gradient-to-t from-blue-200 to-blue-100 rounded-base" />
<div className="w-12 h-2 bg-gradient-to-t from-blue-200 to-blue-100 rounded-base" />
</div>
Question {quiz.position + 1}/{quizMaxNum + 1}
</span>
</div>
{/* 문제 */}
Expand Down Expand Up @@ -159,7 +155,7 @@ export default function QuizBox({ quiz, startTime }: QuizBoxProps) {
<button
ref={hardButtonRef}
onClick={() => handleReaction('hard')}
className={`relative w-[160px] px-4 py-2 rounded-full transition-all flex items-center justify-center space-x-2 ${
className={`relative w-[170px] px-4 py-2 rounded-full transition-all flex items-center justify-center space-x-2 ${
reactionStats.hard > 0
? 'bg-red-500/90 text-white'
: 'bg-white border hover:border-red-400'
Expand Down
5 changes: 2 additions & 3 deletions packages/client/src/pages/quiz-session/ui/QuizEnd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const LOCAL_STORAGE_KEYS = [
'participantStatistics',
'hasSubmitted',
'submitOrder',
'remianingTime',
'ramainingTime',
];

export default function QuizEnd({ refetch, setQuizEnd }: QuizEndProps) {
Expand All @@ -34,7 +34,6 @@ export default function QuizEnd({ refetch, setQuizEnd }: QuizEndProps) {

const { data: ranking } = useShowRanking({ socket, pinCode: pinCode as string });
console.log(ranking);
// TODO: localStorage 삭제하기
useEffect(() => {
const handleStartQuiz = () => {
clearLocalStorage(LOCAL_STORAGE_KEYS);
Expand All @@ -58,7 +57,7 @@ export default function QuizEnd({ refetch, setQuizEnd }: QuizEndProps) {
}, []);

return (
<div className="h-[calc(100vh-78px)] bg-gradient-to-b from-blue-100 to-white p-4">
<div className="h-dvh bg-gradient-to-b from-blue-100 to-white p-4">
<div className="max-w-2xl mx-auto mt-12 p-16 ">
<div className="text-center mb-8">
<span className="text-4xl font-semibold text-gray-600">🏆 중 간 점 검</span>
Expand Down
Loading

0 comments on commit b6c8aa3

Please sign in to comment.