From 7f4b6e06189e5ff3062ab7ce521334610a410d1e Mon Sep 17 00:00:00 2001
From: ByeongChan Choi <77400298+chan-byeong@users.noreply.github.com>
Date: Tue, 19 Nov 2024 20:22:48 +0900
Subject: [PATCH] =?UTF-8?q?[FE=20-=20#73]=20=EC=B0=B8=EC=97=AC=EC=9E=90=20?=
=?UTF-8?q?=ED=80=B4=EC=A6=88=20=ED=92=80=EC=9D=B4=20=ED=8E=98=EC=9D=B4?=
=?UTF-8?q?=EC=A7=80=20UI=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=86=8C?=
=?UTF-8?q?=EC=BC=93=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#83)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: floatUp 키프레임 추가
* fix: Header UI 수정
* feat: Quiz 풀이 페이지 배경 컴포넌트
* fix: 기존 코드 제거
* feat: QuizLoading 페이지 UI 구현
- mock data 바꿔야 함
- UI 일부 수정 필요
* feat: Quiz 풀이 페이지 헤더
- 제출자 , 남은 시간 표기
* feat: Quiz 박스 UI 구현
- 문제, 선지, 제출 버튼
* feat: QuizSession UI 구현
* fix: 에러 메세지 토스트 추가
* fix: ReactionData로 타입 통일
---
.../client/src/pages/quiz-session/index.tsx | 134 +++++++--------
.../client/src/pages/quiz-session/ui/Quiz.tsx | 51 ------
.../pages/quiz-session/ui/QuizBackground.tsx | 18 ++
.../src/pages/quiz-session/ui/QuizBox.tsx | 155 ++++++++++++++++++
.../src/pages/quiz-session/ui/QuizForm.tsx | 42 -----
.../src/pages/quiz-session/ui/QuizHeader.tsx | 34 ++++
.../src/pages/quiz-session/ui/QuizLoading.tsx | 31 ++++
.../client/src/shared/ui/header/Header.tsx | 2 +-
packages/client/tailwind.config.js | 10 ++
9 files changed, 311 insertions(+), 166 deletions(-)
delete mode 100644 packages/client/src/pages/quiz-session/ui/Quiz.tsx
create mode 100644 packages/client/src/pages/quiz-session/ui/QuizBackground.tsx
create mode 100644 packages/client/src/pages/quiz-session/ui/QuizBox.tsx
delete mode 100644 packages/client/src/pages/quiz-session/ui/QuizForm.tsx
create mode 100644 packages/client/src/pages/quiz-session/ui/QuizHeader.tsx
create mode 100644 packages/client/src/pages/quiz-session/ui/QuizLoading.tsx
diff --git a/packages/client/src/pages/quiz-session/index.tsx b/packages/client/src/pages/quiz-session/index.tsx
index 78e7ec9c..c6d5d628 100644
--- a/packages/client/src/pages/quiz-session/index.tsx
+++ b/packages/client/src/pages/quiz-session/index.tsx
@@ -1,84 +1,74 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
-import Quiz from './ui/Quiz';
-import { useNavigate } from 'react-router-dom';
+import { getQuizSocket } from '@/shared/utils/socket';
+import QuizBackground from './ui/QuizBackground';
+import QuizBox from './ui/QuizBox';
+import QuizHeader from './ui/QuizHeader';
+import QuizLoading from './ui/QuizLoading';
+import { toastController } from '@/features/toast/model/toastController';
-interface Quizes {
- title: string;
- choices: {
- content: string;
- isAnswer: boolean;
- }[];
-}
+export default function QuizSession() {
+ const socket = getQuizSocket();
+ const toast = toastController();
+ const [isLoading, setIsLoading] = useState(true);
+ const [reactionStats, setReactionStats] = useState({
+ easy: 0,
+ hard: 0,
+ });
+ const [quiz, setQuiz] = useState(null);
-const mockQuizData: Quizes[] = [
- {
- title: '임시 퀴즈 문제1',
- choices: [
- {
- content: '천마총',
- isAnswer: false,
- },
- {
- content: '왕릉',
- isAnswer: false,
- },
- {
- content: '석굴암',
- isAnswer: true,
- },
- {
- content: '불국사',
- isAnswer: false,
- },
- ],
- },
- {
- title: '임시 퀴즈 문제2',
- choices: [
- {
- content: '천마총',
- isAnswer: false,
- },
- {
- content: '왕릉',
- isAnswer: false,
- },
- {
- content: '석굴암',
- isAnswer: true,
- },
- {
- content: '불국사',
- isAnswer: false,
- },
- ],
- },
-];
+ const totalReactions = reactionStats.easy + reactionStats.hard;
+ const easyPercentage = totalReactions ? (reactionStats.easy / totalReactions) * 100 : 50;
-export default function QuizSession() {
- //TODO: 퀴즈 정보는 React-query를 활용해서 브라우저 캐시에서 가져온다.
+ useEffect(() => {
+ const quizPromise = new Promise((resolve, reject) => {
+ const handleShowQuiz = (data: any) => {
+ setQuiz(data);
+ resolve(data);
+ };
+
+ socket.on('show quiz', handleShowQuiz);
+
+ const timer = setTimeout(() => {
+ reject(new Error('Timeout'));
+ }, 2000);
+
+ return () => {
+ socket.off('show quiz', handleShowQuiz);
+ clearTimeout(timer);
+ };
+ });
+
+ const timerPromise = new Promise((resolve) => {
+ setTimeout(resolve, 2000);
+ });
- const [currentQuizIndex, setCurrentQuizIndex] = useState(0);
- const navigate = useNavigate();
+ Promise.all([quizPromise, timerPromise])
+ .then(() => {
+ setIsLoading(false);
+ })
+ .catch(() => {
+ toast.error('문제 로딩에 실패했습니다.');
+ setIsLoading(false);
+ });
- const handleAnimationEnd = () => {
- // TODO: 타이머 종료 시 다음 퀴즈 페이지로 이동하는 이벤트 emit
- setCurrentQuizIndex((pre) => pre + 1);
- if (currentQuizIndex === mockQuizData.length - 1) {
- // TODO: Host 여부에 따라 페이지 변경
- // HOST - navigate('/questions)
- navigate('/quiz/question');
- }
- };
+ socket.emit('timeout', (response: any) => {
+ console.log(response);
+ });
+ }, []);
+ console.log(quiz);
return (
<>
-
+ {isLoading ? (
+
+ ) : (
+
+
+
+
+
+ )}
>
);
}
diff --git a/packages/client/src/pages/quiz-session/ui/Quiz.tsx b/packages/client/src/pages/quiz-session/ui/Quiz.tsx
deleted file mode 100644
index 01174ce8..00000000
--- a/packages/client/src/pages/quiz-session/ui/Quiz.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useState } from 'react';
-
-import ProgressBar from '@/shared/ui/progress-bar/ProgressBar';
-import CustomButton from '@/shared/ui/buttons/CustomButton';
-import QuizForm from './QuizForm';
-
-// TODO: 제출하기 버튼 API 연동
-// TODO: 타이머 종료 시 다음 퀴즈 페이지로 이동
-
-type Choice = {
- content: string;
- isAnswer: boolean;
-};
-
-interface QuizProps {
- quizData: {
- title: string;
- choices: Choice[];
- };
- handleAnimationEnd: () => void;
-}
-
-export default function Quiz({ quizData, handleAnimationEnd }: QuizProps) {
- const [selectedOptions, setSelectedOptions] = useState([]);
-
- const handleToggle = (index: number) => {
- setSelectedOptions((prev) => {
- if (prev.includes(index)) {
- return prev.filter((item) => item !== index);
- }
- return [...prev, index];
- });
- };
-
- return (
-
-
handleAnimationEnd()}
- />
-
-
- );
-}
diff --git a/packages/client/src/pages/quiz-session/ui/QuizBackground.tsx b/packages/client/src/pages/quiz-session/ui/QuizBackground.tsx
new file mode 100644
index 00000000..1d96a3cf
--- /dev/null
+++ b/packages/client/src/pages/quiz-session/ui/QuizBackground.tsx
@@ -0,0 +1,18 @@
+interface QuizBackgroundProps {
+ easyPercentage: number;
+}
+
+export default function QuizBackground({ easyPercentage }: QuizBackgroundProps) {
+ return (
+
+ );
+}
diff --git a/packages/client/src/pages/quiz-session/ui/QuizBox.tsx b/packages/client/src/pages/quiz-session/ui/QuizBox.tsx
new file mode 100644
index 00000000..0c643400
--- /dev/null
+++ b/packages/client/src/pages/quiz-session/ui/QuizBox.tsx
@@ -0,0 +1,155 @@
+import { Dispatch, SetStateAction, useState, useRef, useEffect, useCallback } from 'react';
+
+import { getQuizSocket } from '@/shared/utils/socket';
+interface ReactionData {
+ easy: number;
+ hard: number;
+}
+
+interface QuizBoxProps {
+ reactionStats: ReactionData;
+ setReactionStats: Dispatch>;
+}
+
+export default function QuizBox({ reactionStats, setReactionStats }: QuizBoxProps) {
+ const [selectedAnswer, setSelectedAnswer] = useState([]);
+ const [hasSubmitted, setHasSubmitted] = useState(false);
+ const easyButtonRef = useRef(null);
+ const hardButtonRef = useRef(null);
+ const socket = getQuizSocket();
+
+ const handleSelectAnswer = (idx: number) => {
+ setSelectedAnswer((prev) => {
+ if (prev.includes(idx)) {
+ return prev.filter((i) => i !== idx);
+ }
+ return [...prev, idx];
+ });
+ };
+
+ const handleSubmit = () => {
+ socket.emit('submit answer', { selectAnswer: selectedAnswer });
+ console.log(selectedAnswer);
+ setHasSubmitted(true);
+ };
+
+ const handleReaction = (reaction: 'easy' | 'hard') => {
+ setReactionStats({ ...reactionStats, [reaction]: reactionStats[reaction] + 1 });
+ handleFloatUp(reaction);
+ socket.emit('emoji', { reaction });
+ };
+
+ const handleFloatUp = (reaction: 'easy' | 'hard') => {
+ const buttonRef = reaction === 'easy' ? easyButtonRef : hardButtonRef;
+
+ const emoji = document.createElement('div');
+ emoji.textContent = reaction === 'easy' ? '😊' : '🤔';
+ emoji.className = 'fixed left-4 text-2xl absolute animate-[floatUp_1s_ease-in-out_forwards]';
+ buttonRef.current?.appendChild(emoji);
+
+ setTimeout(() => {
+ emoji.remove();
+ }, 1000);
+ };
+
+ const handleReactionUpdate = useCallback((data: ReactionData) => {
+ setReactionStats(data);
+ }, []);
+
+ const handleSubmitUpdate = useCallback(() => {
+ // 제출자에게 제출 완료에 대한 피드백 보여주기
+ }, []);
+
+ useEffect(() => {
+ socket.on('emoji', handleReactionUpdate);
+
+ socket.on('submit answer', handleSubmitUpdate);
+
+ return () => {
+ socket.off('emoji', handleReactionUpdate);
+ };
+ }, []);
+
+ return (
+ <>
+
+
+
+ Question 1/10
+ 난이도
+
+ {/* 문제 */}
+
+
+ Python의 기본 자료형에 대한 설명으로 올바른 것은?
+
+
+ 다음 중 Python의 기본 자료형(Data Type)에 대한 설명으로 가장 적절한 것을 고르시오.
+
+
+ {/* 선택지 */}
+
+ {[
+ '문자열(string)은 변경 가능한(mutable) 자료형이다.',
+ '튜플(tuple)은 변경 불가능한(immutable) 자료형이다.',
+ '리스트(list)는 변경 불가능한(immutable) 자료형이다.',
+ '딕셔너리(dictionary)는 정렬된 자료형이다.',
+ ].map((answer, idx) => (
+
+ ))}
+
+
+
+ {/* 제출 버튼 */}
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/packages/client/src/pages/quiz-session/ui/QuizForm.tsx b/packages/client/src/pages/quiz-session/ui/QuizForm.tsx
deleted file mode 100644
index bf4a7c93..00000000
--- a/packages/client/src/pages/quiz-session/ui/QuizForm.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import ToggleButton from '@/shared/ui/buttons/ToggleButton';
-
-type Choice = {
- content: string;
- isAnswer: boolean;
-};
-
-interface QuizProps {
- title: string;
- choices: Choice[];
-}
-
-interface QuizFormProps {
- selectedOptions: number[];
- onToggle: (index: number) => void;
- quizData: QuizProps;
-}
-
-export default function QuizForm({ selectedOptions, onToggle, quizData }: QuizFormProps) {
- return (
-
-
{quizData.title}
-
- {quizData.choices.map((option, index) => (
-
onToggle(index)}
- >
-
- {option.content}
-
- ))}
-
-
- );
-}
diff --git a/packages/client/src/pages/quiz-session/ui/QuizHeader.tsx b/packages/client/src/pages/quiz-session/ui/QuizHeader.tsx
new file mode 100644
index 00000000..83849401
--- /dev/null
+++ b/packages/client/src/pages/quiz-session/ui/QuizHeader.tsx
@@ -0,0 +1,34 @@
+import { useEffect, useState } from 'react';
+
+import { getQuizSocket } from '@/shared/utils/socket';
+
+export default function QuizHeader() {
+ const socket = getQuizSocket();
+ const [submitStatus, setSubmitStatus] = useState<{ count: number; total: number }>({
+ count: 0,
+ total: 0,
+ });
+
+ const handleSubmitStatus = (status: { count: number; total: number }) => {
+ setSubmitStatus(status);
+ };
+
+ useEffect(() => {
+ socket.on('submit status', handleSubmitStatus);
+
+ return () => {
+ socket.off('submit status', handleSubmitStatus);
+ };
+ }, []);
+
+ return (
+
+
+
+ {submitStatus.count} / {submitStatus.total}명 제출
+
+
남은 시간
+
+
+ );
+}
diff --git a/packages/client/src/pages/quiz-session/ui/QuizLoading.tsx b/packages/client/src/pages/quiz-session/ui/QuizLoading.tsx
new file mode 100644
index 00000000..789c0c59
--- /dev/null
+++ b/packages/client/src/pages/quiz-session/ui/QuizLoading.tsx
@@ -0,0 +1,31 @@
+export default function QuizLoading() {
+ return (
+
+
+
+ Leader Board
+
+
+
+
+
+ 나는 몇 등?
+ #10
+
+
+
+ );
+}
diff --git a/packages/client/src/shared/ui/header/Header.tsx b/packages/client/src/shared/ui/header/Header.tsx
index 831ac09b..0dc33682 100644
--- a/packages/client/src/shared/ui/header/Header.tsx
+++ b/packages/client/src/shared/ui/header/Header.tsx
@@ -9,7 +9,7 @@ interface HeaderProps {
export default function Header({ classTitle }: HeaderProps) {
//TODO: 로그인 상태 관리
return (
-
+
diff --git a/packages/client/tailwind.config.js b/packages/client/tailwind.config.js
index 689d7263..0ac3ce6b 100644
--- a/packages/client/tailwind.config.js
+++ b/packages/client/tailwind.config.js
@@ -65,6 +65,16 @@ export default {
opacity: 0,
},
},
+ floatUp: {
+ '0%': {
+ transform: 'translateY(0)',
+ opacity: 1,
+ },
+ '100%': {
+ transform: 'translateY(-100px)',
+ opacity: 0,
+ },
+ },
},
},
},