Skip to content

Commit

Permalink
[FE] - Session, Quiz-wait 페이지 이벤트 로직 변경 (#155)
Browse files Browse the repository at this point in the history
* feat: pinCode Check API 구현

* feat: Nickname 페이지 커스텀 훅 구현

* feat: 메인 페이지에서 핀코드 유효성(방 정원 초과 여부) 검사

* fix: session 이벤트 수정

* fix: 커스텀 훅 적용 및 lazy 페이지로 변경

* fix: import 경로 변경

* fix: Prevent Router 적용
  • Loading branch information
chan-byeong authored Dec 2, 2024
1 parent 1481bff commit 19d4ee4
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 149 deletions.
12 changes: 7 additions & 5 deletions packages/client/src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import QuizCreatePage from '@/pages/quiz-create';
import GuestLayout from '@/app/layouts/GuestLayout';
import NotFound from '@/app/routes/NotFound';
import QuizSession from '@/pages/quiz-session';
import QuizWait from '@/pages/quiz-wait';
import Nickname from '@/pages/nickname';
import QuizQuestion from '@/pages/quiz-question';
import QnA from '@/pages/qna';
import GuestQnA from '@/pages/guest-qna';
import QuizMasterSession from '@/pages/quiz-master-session';
import Leaderboard from '@/pages/leaderboard';
import QuizListPage from '@/pages/quiz-list';
import QuizWaitPage from '@/pages/quiz-wait';
import PreventGuestRouter from './PreventGuestRouter';

export default function Router() {
return (
Expand All @@ -25,10 +26,11 @@ export default function Router() {
<Route path="/questions" element={<QnA />} />
</Route>
<Route element={<GuestLayout />}>
<Route path="/quiz/session/:pinCode/:id" element={<QuizSession />} />
<Route path="/quiz/wait/:pinCode" element={<QuizWait />} />
<Route path="/guest/questions" element={<GuestQnA />} />

<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 />} />
Expand Down
11 changes: 9 additions & 2 deletions packages/client/src/pages/main/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toastController } from '@/features/toast/model/toastController';
import { getPincodeExist } from '@/shared/api/games';
import { getPincodeExist, checkPincodePossible } from '@/shared/api/games';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import FloatingSquare from './ui/FloatingSquare';
Expand All @@ -16,8 +16,15 @@ export default function MainPage() {
return;
}
const response = await getPincodeExist(pinCode);

if (response.isExist) {
navigate(`/nickname/${pinCode}`);
const checkResponse = await checkPincodePossible(pinCode);
console.log(checkResponse);
if (checkResponse.isPossible) {
navigate(`/nickname/${pinCode}`);
} else {
toast.warning('방이 가득 찼습니다.');
}
return;
}
toast.error('잘못된 코드입니다.');
Expand Down
33 changes: 18 additions & 15 deletions packages/client/src/pages/nickname/index.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';

import TrophyIcon from '@/shared/assets/icons/tropyhy.svg?react';
import AvatarIcon from '@/shared/assets/icons/avatar.svg?react';

import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { getCookie, setCookie } from '@/shared/utils/cookie';
import { setCookie } from '@/shared/utils/cookie';
import { getQuizSocket } from '@/shared/utils/socket';
import { emitEventWithAck } from '@/shared/utils/emitEventWithAck';
import { toastController } from '@/features/toast/model/toastController';

const MAX_NICKNAME_LENGTH = 12;

export default function Nickname() {
const { pinCode } = useParams();
const [nickname, setNickname] = useState('');
const navigate = useNavigate();
const toast = toastController();
const [nickname, setNickname] = useState('');

const handleNicknameSubmit = (nickname: string) => {
const handleNicknameSubmit = async (nickname: string) => {
const socket = getQuizSocket();
const sid = getCookie('sid');
if (sid) {
socket.emit('participant re-entry', { pinCode: pinCode, nickname: nickname, sid: sid });
navigate(`/quiz/wait/${pinCode}`);
return;
}
socket.emit('participant entry', { pinCode: pinCode, nickname: nickname });

socket.on('session', (response) => {
setCookie('sid', response);
const sid = await emitEventWithAck<string>(socket, 'session', {
pinCode: pinCode,
nickname: nickname,
});
if (!sid) {
toast.warning('방이 가득 찼습니다.');
navigate(`/`);
}
setCookie('sid', sid);
socket.emit('participant notice', { pinCode: pinCode });
navigate(`/quiz/wait/${pinCode}`);
};

Expand Down
122 changes: 122 additions & 0 deletions packages/client/src/pages/quiz-wait/index.lazy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Copy, Users, PlayCircle } from 'lucide-react';

import { getQuizSocket } from '@/shared/utils/socket';
import { getCookie } from '@/shared/utils/cookie';
import { toastController } from '@/features/toast/model/toastController';
import { apiClient } from '@/shared/api';
import UserGridView from './ui/UserGridView';
import { useNickname } from './model/hooks/useNickname';

export interface Guest {
nickname: string;
character: number;
position: number;
}

export default function QuizWaitLazyPage() {
const socket = getQuizSocket();
const { pinCode } = useParams();
const {
data: { participantList, myPosition },
refetch,
} = useNickname(socket, pinCode ?? '', getCookie('sid') ?? '');
const [userType, setUserType] = useState<string>('');

const guestLink = `${import.meta.env.VITE_CLIENT_URL}/nickname/${pinCode}`;
const toast = toastController();

const navigate = useNavigate();

useEffect(() => {
const handleParticipantNotice = () => {
refetch();
};
socket.on('participant notice', handleParticipantNotice);

const fetchUserType = async () => {
const response = await apiClient.get(`/games/${pinCode}/sid/${getCookie('sid')}`);
setUserType(response.type);
};

socket.on('start quiz', (response) => {
console.log('start quiz', response);
navigate(`/quiz/session/${pinCode}/1`);
});

fetchUserType();

return () => {
socket.off('participant notice', handleParticipantNotice);
};
}, []);

const handleCopyLink = () => {
try {
navigator.clipboard.writeText(guestLink);
toast.success('링크가 복사되었습니다.');
} catch (error) {
if (error instanceof Error) {
const errorMessage = error.message;
toast.error(errorMessage);
}
}
};

const handleQuizStart = () => {
socket.emit('start quiz', { sid: getCookie('sid'), pinCode });
navigate(`/quiz/session/host/${pinCode}/1`);
};

return (
<div className="flex justify-center gap-6 pt-8">
<div className="flex flex-col gap-6 justify-center items-center">
<div className="w-full bg-white rounded-xl shadow-md p-6">
<div className="relative flex items-center justify-between mb-4 gap-2">
<div className="flex items-center gap-2">
<div className="text-lg font-bold px-4 py-2 rounded-xl bg-gray-100">
<span className="text-gray-500">Pin : </span>
{pinCode}
</div>
<div
className="px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 flex items-center gap-2 cursor-pointer"
onClick={handleCopyLink}
>
<Copy className="w-4 h-4" />
링크 복사하기
</div>
</div>

<p className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-fit flex items-center gap-2 justify-center font-medium text-md text-gray-700">
<span className="text-blue-500 font-bold"> / </span>입력 시 채팅을 칠 수 있어요.
</p>

<p className="flex items-center gap-6 font-bold text-lg">
<div className="flex items-center gap-3 bg-gray-100 px-4 py-2 rounded-2xl">
<Users className="w-5 h-5 text-gray-600" />
<span className="font-medium text-gray-700">{participantList.length}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
<span className="text-gray-500 font-medium">참가 대기중</span>
</div>
</p>
</div>
<UserGridView guests={participantList} myPosition={myPosition} />
</div>
{userType === 'master' && (
<div className="flex justify-end min-w-full">
<button
className="px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 flex items-center gap-2 cursor-pointer"
onClick={handleQuizStart}
>
<PlayCircle className="w-6 h-6 text-white" />
퀴즈 시작하기
</button>
</div>
)}
</div>
</div>
);
}
131 changes: 6 additions & 125 deletions packages/client/src/pages/quiz-wait/index.tsx
Original file line number Diff line number Diff line change
@@ -1,129 +1,10 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Copy, Users, PlayCircle } from 'lucide-react';

import { getQuizSocket } from '@/shared/utils/socket';
import { getCookie } from '@/shared/utils/cookie';
import { toastController } from '@/features/toast/model/toastController';
import { apiClient } from '@/shared/api';
import UserGridView from './ui/UserGridView';

export interface Guest {
nickname: string;
character: number;
position: number;
}

export default function QuizWait() {
const { pinCode } = useParams();
const [userType, setUserType] = useState<string>('');
const [guests, setGuests] = useState<Guest[]>([]);
const [myPosition, setMyPosition] = useState<number>(-1);

const guestLink = `${import.meta.env.VITE_CLIENT_URL}/nickname/${pinCode}`;
const socket = getQuizSocket();
const toast = toastController();

const navigate = useNavigate();

useEffect(() => {
const handleMyPosition = (response: any) => {
const { participantList, myPosition } = response;
setGuests(participantList);
setMyPosition(myPosition);
};

socket.on('my position', handleMyPosition);

const handleNickname = (response: any) => {
const { participantList } = response;
setGuests(participantList);
};

socket.on('nickname', handleNickname);

const fetchUserType = async () => {
const response = await apiClient.get(`/games/${pinCode}/sid/${getCookie('sid')}`);
setUserType(response.type);
};

socket.on('start quiz', (response) => {
console.log('start quiz', response);
navigate(`/quiz/session/${pinCode}/1`);
});

fetchUserType();

return () => {
socket.off('nickname', handleNickname);
};
}, []);

const handleCopyLink = () => {
try {
navigator.clipboard.writeText(guestLink);
toast.success('링크가 복사되었습니다.');
} catch (error) {
if (error instanceof Error) {
const errorMessage = error.message;
toast.error(errorMessage);
}
}
};

const handleQuizStart = () => {
socket.emit('start quiz', { sid: getCookie('sid'), pinCode });
navigate(`/quiz/session/host/${pinCode}/1`);
};
import AsyncBoundary from '@/shared/boundary/AsyncBoundary';
import QuizWaitLazyPage from './index.lazy';

export default function QuizWaitPage() {
return (
<div className="flex justify-center gap-6 pt-8">
<div className="flex flex-col gap-6 justify-center items-center">
<div className="w-full bg-white rounded-xl shadow-md p-6">
<div className="relative flex items-center justify-between mb-4 gap-2">
<div className="flex items-center gap-2">
<div className="text-lg font-bold px-4 py-2 rounded-xl bg-gray-100">
<span className="text-gray-500">Pin : </span>
{pinCode}
</div>
<div
className="px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 flex items-center gap-2 cursor-pointer"
onClick={handleCopyLink}
>
<Copy className="w-4 h-4" />
링크 복사하기
</div>
</div>

<p className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-fit flex items-center gap-2 justify-center font-medium text-md text-gray-700">
<span className="text-blue-500 font-bold"> / </span>입력 시 채팅을 칠 수 있어요.
</p>

<p className="flex items-center gap-6 font-bold text-lg">
<div className="flex items-center gap-3 bg-gray-100 px-4 py-2 rounded-2xl">
<Users className="w-5 h-5 text-gray-600" />
<span className="font-medium text-gray-700">{guests.length}</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
<span className="text-gray-500 font-medium">참가 대기중</span>
</div>
</p>
</div>
<UserGridView guests={guests} myPosition={myPosition} />
</div>
{userType === 'master' && (
<div className="flex justify-end min-w-full">
<button
className="px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 flex items-center gap-2 cursor-pointer"
onClick={handleQuizStart}
>
<PlayCircle className="w-6 h-6 text-white" />
퀴즈 시작하기
</button>
</div>
)}
</div>
</div>
<AsyncBoundary>
<QuizWaitLazyPage />
</AsyncBoundary>
);
}
17 changes: 17 additions & 0 deletions packages/client/src/pages/quiz-wait/model/hooks/useNickname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useSuspenseQuery } from '@tanstack/react-query';

import { emitEventWithAck } from '@/shared/utils/emitEventWithAck';
import { Socket } from 'socket.io-client';
import { Guest } from '../../index.lazy';

interface NicknameResponse {
participantList: Guest[];
myPosition: number;
}

export const useNickname = (socket: Socket, pinCode: string, sid: string) => {
return useSuspenseQuery({
queryKey: ['participant info', pinCode],
queryFn: () => emitEventWithAck<NicknameResponse>(socket, 'participant info', { pinCode, sid }),
});
};
2 changes: 1 addition & 1 deletion packages/client/src/pages/quiz-wait/ui/UserGridItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';

import { Guest } from '../index';
import { Guest } from '../index.lazy';
import { getQuizSocket } from '@/shared/utils/socket';

import DogImage from '@/shared/assets/characters/강아지.png';
Expand Down
Loading

0 comments on commit 19d4ee4

Please sign in to comment.