-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FE] - 마스터, 참가자 퀴즈 세션 새로고침 반영 (#157)
* feat: pinCode Check API 구현 * feat: Nickname 페이지 커스텀 훅 구현 * feat: 메인 페이지에서 핀코드 유효성(방 정원 초과 여부) 검사 * fix: session 이벤트 수정 * fix: 커스텀 훅 적용 및 lazy 페이지로 변경 * fix: import 경로 변경 * fix: Prevent Router 적용 * refactor: quiz session 로직 변경 * refactor: quiz-session, master-session 코드 리팩토링 * feat: usePersistState 커스텀 훅 생성 * feat: clearLocalStorage 유틸 함수 구현 * refactor: participant quiz-session 구조 개선 * feat: 로컬 스토리지를 활용한 훅 usePersistState 추가하여 새로고침 시 마스터 통계값 저장 * fix: 사용하지 않는 변수 및 함수 삭제 * feat: 남은 시간 로컬 스토리지 추가 및 그래프 색상, 텍스트 변경 * feat: 서버 코드 변경 * feat: 퀴즈 시작 시 clearLocalStoarge 추가 --------- Co-authored-by: dooohun <[email protected]>
- Loading branch information
1 parent
95505c1
commit aa3ae79
Showing
17 changed files
with
551 additions
and
340 deletions.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
packages/client/src/pages/quiz-master-session/index.lazy.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { useState } from 'react'; | ||
import { useNavigate, useParams } from 'react-router-dom'; | ||
|
||
import { getCookie } from '@/shared/utils/cookie'; | ||
import { getQuizSocket } from '@/shared/utils/socket'; | ||
|
||
import { useQuizSession } from '../quiz-session/model/hooks/useQuizSession'; | ||
import QuizMasterHeader from './ui/QuizMasterHeader'; | ||
import Statistics from './ui/Statistics'; | ||
import { clearLocalStorage } from '@/shared/utils/clearLocalStorage'; | ||
|
||
const LOCAL_STORAGE_KEYS = ['masterStatistics', 'reactionStats', 'history', 'remainingTime']; | ||
|
||
export default function QuizMasterSessionLazyPage() { | ||
const { pinCode, id } = useParams(); | ||
const navigate = useNavigate(); | ||
const socket = getQuizSocket(); | ||
const [initializeStates, setInitializeStates] = useState(false); | ||
|
||
const { data: quiz, refetch } = useQuizSession({ socket, pinCode: pinCode as string }); | ||
|
||
const handleNextQuiz = () => { | ||
if (quiz.isLast) { | ||
socket.emit('end quiz', { pinCode, sid: getCookie('sid') }); | ||
clearLocalStorage(LOCAL_STORAGE_KEYS); | ||
navigate(`/quiz/session/${pinCode}/end`); | ||
return; | ||
} | ||
|
||
socket.emitWithAck('start quiz', { pinCode, sid: getCookie('sid') }).then(() => { | ||
clearLocalStorage(LOCAL_STORAGE_KEYS); | ||
navigate(`/quiz/session/host/${pinCode}/${parseInt(id as string) + 1}`); | ||
refetch(); | ||
setInitializeStates(true); | ||
}); | ||
}; | ||
|
||
return ( | ||
<div className="w-screen min-h-screen"> | ||
<QuizMasterHeader | ||
quizData={quiz.currentQuizData} | ||
startTime={quiz.startTime} | ||
timeLimit={quiz.currentQuizData.timeLimit} | ||
handleNextQuiz={handleNextQuiz} | ||
pinCode={pinCode as string} | ||
socket={socket} | ||
/> | ||
<Statistics | ||
quizData={quiz.currentQuizData} | ||
initializeStates={initializeStates} | ||
setInitializeStates={setInitializeStates} | ||
/> | ||
</div> | ||
); | ||
} |
141 changes: 5 additions & 136 deletions
141
packages/client/src/pages/quiz-master-session/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,141 +1,10 @@ | ||
import { useNavigate, useParams } from 'react-router-dom'; | ||
import { useEffect, useState } from 'react'; | ||
|
||
import { getCookie } from '@/shared/utils/cookie'; | ||
import AnswerGraph from '@/pages/quiz-master-session/ui/AnswerChart'; | ||
import RecentSubmittedAnswers from './ui/RecentSubmittedAnswers'; | ||
import { getQuizSocket } from '@/shared/utils/socket'; | ||
import StatisticsGroup from './ui/StatisticsGroup'; | ||
import { QuizData } from '@youquiz/shared/interfaces/utils/quizdata.interface'; | ||
import { | ||
TimerTickResponse, | ||
MasterStatisticsResponse, | ||
ShowQuizResponse, | ||
} from '@youquiz/shared/interfaces/response'; | ||
import { | ||
INITIAL_QUIZ_DATA, | ||
INITIAL_TICK, | ||
INITIAL_MASTER_STATISTICS, | ||
INITIAL_EMOJI, | ||
} from '@/shared/constants/initialState'; | ||
import EmojiChart from './ui/EmojiChart'; | ||
|
||
interface HistoryItem { | ||
user: string; | ||
submitTime: number; | ||
elapsedTime: number; | ||
displayTime: string; | ||
} | ||
import AsyncBoundary from '@/shared/boundary/AsyncBoundary'; | ||
import QuizMasterSessionLazyPage from './index.lazy'; | ||
|
||
export default function QuizMasterSession() { | ||
const { pinCode } = useParams(); | ||
const navigate = useNavigate(); | ||
const socket = getQuizSocket(); | ||
const [masterStatistics, setMasterStatistics] = | ||
useState<MasterStatisticsResponse>(INITIAL_MASTER_STATISTICS); | ||
const [quizData, setQuizData] = useState<QuizData>(INITIAL_QUIZ_DATA); | ||
const [tick, setTick] = useState<TimerTickResponse>(INITIAL_TICK); | ||
const [quizIndex, setQuizIndex] = useState(0); | ||
const [isLastQuiz, setIsLastQuiz] = useState(false); | ||
const [reactionStats, setReactionStats] = useState(INITIAL_EMOJI); | ||
const [history, setHistory] = useState<HistoryItem[]>([]); | ||
|
||
const initQuizData = () => { | ||
setQuizData(INITIAL_QUIZ_DATA); | ||
setMasterStatistics(INITIAL_MASTER_STATISTICS); | ||
setTick(INITIAL_TICK); | ||
setHistory([]); | ||
setReactionStats(INITIAL_EMOJI); | ||
}; | ||
|
||
const handleNextQuiz = () => { | ||
if (isLastQuiz) { | ||
socket.emit('end quiz', { pinCode, sid: getCookie('sid') }); | ||
navigate(`/quiz/session/${pinCode}/end`); | ||
return; | ||
} | ||
if (Math.floor(tick.remainingTime / 1000) === 0) { | ||
initQuizData(); | ||
setQuizIndex((prev) => prev + 1); | ||
socket.emit('show quiz', { pinCode }); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
socket.emit('show quiz', { pinCode }); | ||
|
||
const handleShowQuiz = (response: ShowQuizResponse) => { | ||
const { currentQuizData, isLast } = response; | ||
setQuizData(currentQuizData); | ||
setIsLastQuiz(isLast); | ||
}; | ||
const handleMasterStatistics = (response: MasterStatisticsResponse) => { | ||
setMasterStatistics(response); | ||
}; | ||
const handleTimerTick = (response: TimerTickResponse) => { | ||
setTick(response); | ||
}; | ||
|
||
const handleReactionUpdate = (response: { easy: number; hard: number }) => { | ||
setReactionStats(response); | ||
}; | ||
|
||
socket.on('show quiz', handleShowQuiz); | ||
socket.on('master statistics', handleMasterStatistics); | ||
socket.on('timer tick', handleTimerTick); | ||
socket.on('emoji', handleReactionUpdate); | ||
|
||
return () => { | ||
socket.off('show quiz', handleShowQuiz); | ||
socket.off('master statistics', handleMasterStatistics); | ||
socket.off('timer tick', handleTimerTick); | ||
}; | ||
}, []); | ||
return ( | ||
<div className="w-screen min-h-screen"> | ||
<div className="p-5"> | ||
<div className="flex justify-between"> | ||
<div> | ||
<h1 className="text-xl font-bold mb-2">실시간 통계</h1> | ||
<p className="text-2xl font-bold mb-2"> | ||
Q{quizIndex + 1}. {quizData.content} | ||
</p> | ||
</div> | ||
<div> | ||
<p className="font-bold text-gray-500 mb-2"> | ||
제한 시간{' '} | ||
{Math.floor(tick.remainingTime / 1000) === 0 | ||
? '종료' | ||
: Math.floor(tick.remainingTime / 1000)} | ||
</p> | ||
<div className="mb-2"> | ||
<button | ||
className={`bg-blue-500 text-white px-4 py-2 rounded-md disabled:bg-blue-300 disabled:cursor-not-allowed disabled:opacity-50`} | ||
onClick={handleNextQuiz} | ||
disabled={Math.floor(tick.remainingTime / 1000) !== 0} | ||
> | ||
다음 퀴즈 | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<StatisticsGroup participantStatistics={masterStatistics} /> | ||
<div className="grid grid-cols-[3fr_1fr] gap-4 mx-5 h-[calc(100vh-300px)]"> | ||
<AnswerGraph | ||
answerStats={masterStatistics.choiceStatus} | ||
participantCount={masterStatistics.participantLength} | ||
quizData={quizData} | ||
/> | ||
<div> | ||
<RecentSubmittedAnswers | ||
userSubmitHistory={masterStatistics.submitHistory} | ||
history={history} | ||
setHistory={setHistory} | ||
/> | ||
<EmojiChart reactionStats={reactionStats} /> | ||
</div> | ||
</div> | ||
</div> | ||
<AsyncBoundary> | ||
<QuizMasterSessionLazyPage /> | ||
</AsyncBoundary> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
packages/client/src/pages/quiz-master-session/ui/QuizMasterHeader.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { usePersistState } from '@/shared/hooks/usePersistState'; | ||
import { useEffect } from 'react'; | ||
|
||
import { Socket } from 'socket.io-client'; | ||
|
||
interface QuizMasterHeaderProps { | ||
quizData: QuizData; | ||
startTime: number; | ||
timeLimit: number; | ||
handleNextQuiz: () => void; | ||
pinCode: string; | ||
socket: Socket; | ||
} | ||
|
||
export default function QuizMasterHeader({ | ||
quizData, | ||
startTime, | ||
timeLimit, | ||
handleNextQuiz, | ||
pinCode, | ||
socket, | ||
}: QuizMasterHeaderProps) { | ||
const [remainingTime, setRemainingTime] = usePersistState('remainingTime', timeLimit); | ||
|
||
useEffect(() => { | ||
const intervalId = setInterval(() => { | ||
const timeLeft = timeLimit - Math.floor((Date.now() - startTime) / 1000); | ||
setRemainingTime(timeLeft); | ||
}, 1000); | ||
|
||
return () => { | ||
clearInterval(intervalId); | ||
}; | ||
}, [startTime]); | ||
|
||
useEffect(() => { | ||
if (remainingTime === 0) { | ||
socket.emit('time end', { pinCode: pinCode }); | ||
} | ||
}, [remainingTime]); | ||
|
||
return ( | ||
<div className="p-5"> | ||
<div className="flex justify-between"> | ||
<div> | ||
<h1 className="text-xl font-bold mb-2">실시간 통계</h1> | ||
<p className="text-2xl font-bold mb-2"> | ||
Q{quizData.position + 1}. {quizData.content} | ||
</p> | ||
</div> | ||
<div> | ||
<p className="font-bold text-gray-500 mb-2"> | ||
제한 시간 {remainingTime <= 0 ? '종료' : remainingTime} | ||
</p> | ||
<div className="mb-2"> | ||
<button | ||
className={`bg-blue-500 text-white px-4 py-2 rounded-md disabled:bg-blue-300 disabled:cursor-not-allowed disabled:opacity-50`} | ||
onClick={handleNextQuiz} | ||
disabled={remainingTime > 0} | ||
> | ||
다음 퀴즈 | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.