diff --git a/FE/package-lock.json b/FE/package-lock.json index 1cf5226..f15ee83 100644 --- a/FE/package-lock.json +++ b/FE/package-lock.json @@ -1054,9 +1054,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2885,9 +2885,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/FE/src/App.tsx b/FE/src/App.tsx index f5dd228..c5f0177 100644 --- a/FE/src/App.tsx +++ b/FE/src/App.tsx @@ -6,6 +6,7 @@ import { QuizSetupPage } from './pages/QuizSetupPage'; import { GameLobbyPage } from './pages/GameLobbyPage'; import { LoginPage } from './pages/LoginPage'; import { MyPage } from './pages/MyPage'; +import { PinPage } from './pages/PinPage'; function App() { return ( @@ -17,6 +18,7 @@ function App() { } /> } /> } /> + } /> } /> not found} /> diff --git a/FE/src/api/rest/authApi.ts b/FE/src/api/rest/authApi.ts index 11bf43a..e8023f7 100644 --- a/FE/src/api/rest/authApi.ts +++ b/FE/src/api/rest/authApi.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import axiosInstance from './instance'; type LoginResponse = { - result: string; + acess_token: string; }; export async function login(email: string, password: string): Promise { @@ -14,13 +14,7 @@ export async function login(email: string, password: string): Promise { + if (axios.isAxiosError(error)) { + console.error('Axios Error:', error.response?.data || error.message); + } else { + console.error('Unexpected Error:', error); + } + return null; +}; diff --git a/FE/src/api/rest/quizTypes.ts b/FE/src/api/rest/quizTypes.ts index fb91cc8..5034a0d 100644 --- a/FE/src/api/rest/quizTypes.ts +++ b/FE/src/api/rest/quizTypes.ts @@ -24,7 +24,7 @@ export type QuizChoiceInput = { export type QuizInput = { quiz: string; limitTime: number; - choiceList: QuizChoiceInput[]; + choices: QuizChoiceInput[]; }; export type CreateQuizSetPayload = { diff --git a/FE/src/api/socket/socket.ts b/FE/src/api/socket/socket.ts index 7ba1ec2..ad6e1b5 100644 --- a/FE/src/api/socket/socket.ts +++ b/FE/src/api/socket/socket.ts @@ -32,6 +32,9 @@ class SocketService { private socket: SocketInterface; private url: string; private handlers: (() => void)[]; + private handlerMap: Partial< + Record void)[]> + > = {}; constructor(url: string) { this.socket = io() as SocketInterface; @@ -46,17 +49,21 @@ class SocketService { this.socket.on('connect', () => resolve()); this.socket.on('error', () => reject()); }); - this.handlers.forEach((h) => h()); - this.socket.onAny((eventName, ...args) => { - console.log(`SOCKET[${eventName}]`, ...args); - }); + this.initHandler(); return; } async connectMock(gameId: keyof typeof mockMap) { if (this.isActive()) return; this.socket = new mockMap[gameId]() as SocketInterface; + this.initHandler(); + } + + initHandler() { this.handlers.forEach((h) => h()); + Object.entries(this.handlerMap).forEach(([event, handlers]) => + handlers.forEach((h) => this.socket.on(event, h)) + ); this.socket.onAny((eventName, ...args) => { console.log(`SOCKET[${eventName}]`, ...args); }); @@ -74,6 +81,7 @@ class SocketService { return this.socket.id; } + // deprecated onPermanently( event: T, callback: (data: SocketDataMap[T]['response']) => void @@ -85,10 +93,14 @@ class SocketService { on(event: T, callback: (data: SocketDataMap[T]['response']) => void) { if (this.isActive()) this.socket.on(event, callback); + if (!this.handlerMap[event]) this.handlerMap[event] = []; + this.handlerMap[event].push(callback); } off(event: T, callback: (data: SocketDataMap[T]['response']) => void) { + if (!this.handlerMap[event]) return; if (this.isActive()) this.socket.off(event, callback); + this.handlerMap[event] = this.handlerMap[event].filter((e) => e !== callback); } emit(event: T, data: SocketDataMap[T]['request']) { diff --git a/FE/src/components/Input.tsx b/FE/src/components/Input.tsx deleted file mode 100644 index f120d1b..0000000 --- a/FE/src/components/Input.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -type InputProps = { - placeholder: string; - error: string; - focus?: boolean; -}; - -export const Input = (props: InputProps) => { - const [inputValue, setInputValue] = useState(''); - const inputRef = useRef(null); - - const handleInputChange = (e: React.ChangeEvent) => { - setInputValue(e.target.value); - }; - - useEffect(() => { - if (props.focus && inputRef.current) { - inputRef.current.focus(); - } - }, [props.focus, inputRef]); - - return ( -
- -

{props.error}

-
- ); -}; diff --git a/FE/src/components/QuizOptionBoard.tsx b/FE/src/components/QuizOptionBoard.tsx index 3405f42..f8a0b23 100644 --- a/FE/src/components/QuizOptionBoard.tsx +++ b/FE/src/components/QuizOptionBoard.tsx @@ -33,8 +33,8 @@ export const QuizOptionBoard = () => { const handleClick: React.MouseEventHandler = (e) => { const { pageX, pageY } = e; const { width, height, top, left } = e.currentTarget.getBoundingClientRect(); - const x = (pageX - left) / width; - const y = (pageY - top) / height; + const x = (pageX - left - window.scrollX) / width; + const y = (pageY - top - window.scrollY) / height; if (x > 1 || y > 1) return; socketService.emit('updatePosition', { gameId, newPosition: [y, x] }); const option = Math.round(x) + Math.floor(y * Math.ceil(choiceList.length / 2)) * 2; diff --git a/FE/src/components/QuizSetSearchList.tsx b/FE/src/components/QuizSetSearchList.tsx index 2d954d7..dde895d 100644 --- a/FE/src/components/QuizSetSearchList.tsx +++ b/FE/src/components/QuizSetSearchList.tsx @@ -1,6 +1,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useRef, useState } from 'react'; import { QuizPreview } from './QuizPreview'; +import { getQuizSetList } from '@/api/rest/quizApi'; // type Quiz = { // id: string; @@ -27,20 +28,16 @@ type Params = { const SEARCH_COUNT = 10; const QuizSetSearchList = ({ onClick, search }: Params) => { - const fetchPosts = async ({ pageParam = 1 }) => { - const res = await fetch( - '/api/quizset?' + - new URLSearchParams([ - ['search', search], - ['offset', String(pageParam * SEARCH_COUNT)], - ['size', String(SEARCH_COUNT)] - ]) - ); - const data: { quizSetList: QuizSet[] } = await res.json(); + // api로 수정시 + const fetchPosts = async ({ pageParam = '' }) => { + const data = await getQuizSetList('', pageParam, SEARCH_COUNT, search); + if (!data) { + throw new Error('Failed to fetch quiz set list'); + } return { data: data.quizSetList, - nextPage: pageParam + 1, - hasMore: data.quizSetList.length > 0 + nextPage: data.paging.nextCursor || '', + hasMore: data.paging.hasNextPage }; }; @@ -49,7 +46,7 @@ const QuizSetSearchList = ({ onClick, search }: Params) => { useInfiniteQuery({ queryKey: [search], queryFn: fetchPosts, - initialPageParam: 0, + initialPageParam: '', getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextPage : undefined) }); diff --git a/FE/src/components/TextInput.tsx b/FE/src/components/TextInput.tsx new file mode 100644 index 0000000..22d9400 --- /dev/null +++ b/FE/src/components/TextInput.tsx @@ -0,0 +1,45 @@ +import { TextField } from '@mui/material'; +import { ReactNode, useEffect, useRef } from 'react'; + +type InputProps = { + type?: string; + label: string; + value: string; + onChange: React.ChangeEventHandler; + error?: string; + className?: string; + children?: ReactNode; +}; + +export const TextInput = (props: InputProps) => { + const inputRef = useRef(null); + + useEffect(() => { + if (props.error && inputRef.current) { + inputRef.current.querySelector('input')?.focus(); + } + }, [props.error, inputRef]); + + return ( +
+
+ + {props.children} +
+

{props.error}

+
+ ); +}; diff --git a/FE/src/pages/GameLobbyPage.tsx b/FE/src/pages/GameLobbyPage.tsx index 0d08a14..66049f8 100644 --- a/FE/src/pages/GameLobbyPage.tsx +++ b/FE/src/pages/GameLobbyPage.tsx @@ -1,67 +1,70 @@ import { HeaderBar } from '@/components/HeaderBar'; import { LobbyList } from '@/components/LobbyList'; +import { useState, useEffect, useCallback } from 'react'; +import { getRoomList } from '@/api/rest/roomApi'; -// api 코드 분리 폴더 구조 논의 -// const fetchLobbyRooms = async () => { -// const response = await fetch("/api/lobbies"); -// const data = await response.json(); -// return data; -// }; +type Room = { + title: string; + gameMode: string; + maxPlayerCount: number; + currentPlayerCount: number; + quizSetTitle: string; + gameId: string; +}; -const rooms = [ - { - title: 'Fun Quiz Night', - gameMode: 'Classic', - maxPlayerCount: 10, - currentPlayerCount: 7, - quizSetTitle: 'General Knowledge', - gameId: 'room1' - }, - { - title: 'Fast Fingers Challenge', - gameMode: 'Speed Round', - maxPlayerCount: 8, - currentPlayerCount: 5, - quizSetTitle: 'Science Trivia', - gameId: 'room2' - }, - { - title: 'Trivia Titans', - gameMode: 'Battle Mode', - maxPlayerCount: 12, - currentPlayerCount: 9, - quizSetTitle: 'History and Geography', - gameId: 'room3' - }, - { - title: 'Casual Fun', - gameMode: 'Relaxed', - maxPlayerCount: 6, - currentPlayerCount: 3, - quizSetTitle: 'Pop Culture', - gameId: 'room4' - }, - { - title: 'Quick Thinkers', - gameMode: 'Fast Play', - maxPlayerCount: 10, - currentPlayerCount: 6, - quizSetTitle: 'Sports Trivia', - gameId: 'room5' - } -]; +type Paging = { + nextCursor: string; + hasNextPage: boolean; +}; export const GameLobbyPage = () => { - // const [rooms, setRooms] = useState([]); + const [rooms, setRooms] = useState([]); + const [paging, setPaging] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const loadRooms = useCallback( + async (cursor: string | null, take: number = 10) => { + if (isLoading) return; + + setIsLoading(true); + const response = await getRoomList(cursor ?? '', take); + + if (response) { + setRooms((prevRooms) => [...prevRooms, ...response.roomList]); + setPaging(response.paging); + } + + setIsLoading(false); + }, + [isLoading] + ); + + useEffect(() => { + loadRooms(null); + }, [loadRooms]); + + const handleScroll = useCallback(() => { + if (!paging?.hasNextPage) return; + + const { scrollTop, scrollHeight, clientHeight } = document.documentElement; + if (scrollHeight - scrollTop <= clientHeight + 200) { + loadRooms(paging?.nextCursor); // 스크롤 시 추가 데이터 요청 + } + }, [paging, loadRooms]); + + useEffect(() => { + window.addEventListener('scroll', handleScroll); - // useEffect(() => { - // fetchLobbyRooms().then(setRooms); - // }, []); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [handleScroll]); return (
+ {isLoading &&
Loading...
}
); }; diff --git a/FE/src/pages/GamePage.tsx b/FE/src/pages/GamePage.tsx index 9762bf0..3ccdd2b 100644 --- a/FE/src/pages/GamePage.tsx +++ b/FE/src/pages/GamePage.tsx @@ -22,6 +22,8 @@ export const GamePage = () => { const setGameState = useRoomStore((state) => state.setGameState); const resetScore = usePlayerStore((state) => state.resetScore); const [isModalOpen, setIsModalOpen] = useState(true); + const [isErrorModalOpen, setIsErrorModalOpen] = useState(false); + const [errorModalTitle, setErrorModalTitle] = useState(''); const [isResultOpen, setIsResultOpen] = useState(false); useEffect(() => { diff --git a/FE/src/pages/GameSetupPage.tsx b/FE/src/pages/GameSetupPage.tsx index ed33bb8..0d2ccc1 100644 --- a/FE/src/pages/GameSetupPage.tsx +++ b/FE/src/pages/GameSetupPage.tsx @@ -6,8 +6,7 @@ import { FormLabel, RadioGroup, FormControlLabel, - Radio, - TextField + Radio } from '@mui/material'; import { useEffect, useState } from 'react'; import { socketService } from '@/api/socket'; @@ -15,10 +14,12 @@ import RoomConfig from '@/constants/roomConfig'; import { useNavigate } from 'react-router-dom'; import { useRoomStore } from '@/store/useRoomStore'; import { usePlayerStore } from '@/store/usePlayerStore'; +import { TextInput } from '@/components/TextInput'; export const GameSetupPage = () => { const { gameId, updateRoom } = useRoomStore((state) => state); const setIsHost = usePlayerStore((state) => state.setIsHost); const [title, setTitle] = useState(''); + const [titleError, setTitleError] = useState(''); const [maxPlayerCount, setMaxPlayerCount] = useState(RoomConfig.DEFAULT_PLAYERS); const [gameMode, setGameMode] = useState<'SURVIVAL' | 'RANKING'>('RANKING'); const [roomPublic, setRoomPublic] = useState(true); @@ -28,12 +29,21 @@ export const GameSetupPage = () => { if (gameId) navigate(`/game/${gameId}`); }, [gameId, navigate]); + const handleTitleChange: React.ChangeEventHandler = (e) => { + setTitle(e.target.value); + setTitleError(''); + }; + const handleModeChange = (e: React.ChangeEvent) => { const value = e.target.value === 'RANKING' ? 'RANKING' : 'SURVIVAL'; setGameMode(value); }; const handleSubmit = async () => { + if (!title.trim()) { + setTitleError('제목을 입력해 주세요'); + return; + } const roomData = { title, maxPlayerCount, @@ -55,18 +65,11 @@ export const GameSetupPage = () => { > {'<'} 뒤로가기 - setTitle(e.target.value)} - className="w-full mb-4" - InputLabelProps={{ - style: { color: '#1E40AF' } // 파란색 라벨 텍스트 - }} - InputProps={{ - style: { color: '#1E40AF' } // 파란색 입력 텍스트 - }} + onChange={handleTitleChange} + error={titleError} />
최대인원 diff --git a/FE/src/pages/LoginPage.tsx b/FE/src/pages/LoginPage.tsx index df3f747..0ddefac 100644 --- a/FE/src/pages/LoginPage.tsx +++ b/FE/src/pages/LoginPage.tsx @@ -1,13 +1,21 @@ import { HeaderBar } from '@/components/HeaderBar'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { login, signUp } from '@/api/rest/authApi'; export const LoginPage = () => { const [isSignUp, setIsSignUp] = useState(false); const navigate = useNavigate(); - const handleLogin = () => { - navigate('/mypage'); + const handleLogin = async (email: string, password: string) => { + const response = await login(email, password); + if (response) { + localStorage.setItem('accessToken', response.acess_token); + // 로그인 성공 시 메인 페이지로 이동 + navigate('/'); + } else { + alert('로그인에 실패했습니다. 이메일과 비밀번호를 확인해주세요.'); + } }; return ( @@ -47,7 +55,11 @@ export const LoginPage = () => { className="bg-gray-800 rounded-md p-8 transition-all duration-500" style={{ minHeight: '400px' }} > - {isSignUp ? : } + {isSignUp ? ( + + ) : ( + + )}
@@ -56,66 +68,115 @@ export const LoginPage = () => { }; type LoginFormProps = { - handleLogin: () => void; + handleLogin: (email: string, password: string) => void; }; -const LoginForm: React.FC = ({ handleLogin }) => ( -
-

로그인

-
- -
-
- -
- -

- - 비밀번호를 잊으셨나요? - -

-
-); +const LoginForm: React.FC = ({ handleLogin }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); -const SignUpForm = () => ( -
-

회원가입

-
- -
-
- + const onSubmit = () => { + if (!email || !password) { + alert('이메일과 비밀번호를 입력해주세요.'); + return; + } + handleLogin(email, password); + }; + + return ( +
+

로그인

+
+ setEmail(e.target.value)} + className="w-full px-4 py-3 text-sm text-gray-300 bg-gray-700 rounded-md focus:ring-2 focus:ring-yellow-400 focus:outline-none" + /> +
+
+ setPassword(e.target.value)} + className="w-full px-4 py-3 text-sm text-gray-300 bg-gray-700 rounded-md focus:ring-2 focus:ring-yellow-400 focus:outline-none" + /> +
+ +

+ + 비밀번호를 잊으셨나요? + +

-
- + ); +}; + +type SignUpFormProps = { + setIsSignUp: (value: boolean) => void; // props 타입 정의 +}; + +const SignUpForm: React.FC = ({ setIsSignUp }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [nickname, setNickname] = useState(''); + + const handleSignUp = async () => { + if (!email || !password || !nickname) { + alert('모든 필드를 입력해주세요.'); + return; + } + + const result = await signUp({ email, password, nickname }); + + if (result) { + alert('회원가입에 성공했습니다!'); + setIsSignUp(false); + } else { + alert('회원가입에 실패했습니다. 다시 시도해주세요.'); + } + }; + + return ( +
+

회원가입

+
+ setNickname(e.target.value)} + /> +
+
+ setEmail(e.target.value)} + /> +
+
+ setPassword(e.target.value)} + /> +
+
- -
-); + ); +}; diff --git a/FE/src/pages/MainPage.tsx b/FE/src/pages/MainPage.tsx index f580f2e..d23e8d5 100644 --- a/FE/src/pages/MainPage.tsx +++ b/FE/src/pages/MainPage.tsx @@ -19,7 +19,9 @@ export const MainPage = () => { - + diff --git a/FE/src/pages/PinPage.tsx b/FE/src/pages/PinPage.tsx new file mode 100644 index 0000000..2fb7cd5 --- /dev/null +++ b/FE/src/pages/PinPage.tsx @@ -0,0 +1,74 @@ +import { socketService } from '@/api/socket'; +import { HeaderBar } from '@/components/HeaderBar'; +import { TextInput } from '@/components/TextInput'; +import { getRandomNickname } from '@/utils/nickname'; +import { useEffect, useState } from 'react'; + +export const PinPage = () => { + const [nickname, setNickname] = useState(''); + const [pin, setPin] = useState(''); + const [errors, setErrors] = useState({ nickname: '', pin: '' }); + + useEffect(() => { + setNickname(getRandomNickname()); + }, []); + + const handleJoin = () => { + const newErrors = { nickname: '', pin: '' }; + let hasError = false; + + if (!nickname.trim()) { + newErrors.nickname = '닉네임을 입력해주세요'; + hasError = true; + } + + if (!pin.trim()) { + newErrors.pin = '핀번호를 입력해주세요'; + hasError = true; + } + + setErrors(newErrors); + + if (hasError) return; + + socketService.joinRoom(pin, nickname); + }; + + return ( + <> + +
+
+

방 들어가기

+ + { + setNickname(e.target.value); + if (errors.nickname) setErrors((prev) => ({ ...prev, nickname: '' })); + }} + error={errors.nickname} + /> + + { + setPin(e.target.value); + if (errors.pin) setErrors((prev) => ({ ...prev, pin: '' })); + }} + error={errors.pin} + /> + + +
+
+ + ); +}; diff --git a/FE/src/pages/QuizSetupPage.tsx b/FE/src/pages/QuizSetupPage.tsx index fe608ba..210536e 100644 --- a/FE/src/pages/QuizSetupPage.tsx +++ b/FE/src/pages/QuizSetupPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { TextField, Button, @@ -9,6 +9,9 @@ import { SelectChangeEvent } from '@mui/material'; import { HeaderBar } from '@/components/HeaderBar'; +import { TextInput } from '@/components/TextInput'; +import { createQuizSet } from '@/api/rest/quizApi'; +import { CreateQuizSetPayload } from '@/api/rest/quizTypes'; /* { title: string, // 퀴즈셋의 제목 @@ -43,29 +46,44 @@ type QuizData = { }; export const QuizSetupPage: React.FC = () => { - const [title, setTitle] = useState(''); - const [category, setCategory] = useState(''); - const [quizSet, setQuizSet] = useState([ - { quiz: '', limitTime: 0, choices: [{ content: '', order: 1, isAnswer: false }] } - ]); + const [title, setTitle] = useState(''); + const [titleError, setTitleError] = useState(''); + const [category, setCategory] = useState(''); + const [quizSet, setQuizSet] = useState([]); + const [quizErrorIndex, setQuizErrorIndex] = useState(null); + const [choiceErrorIndex, setChoiceErrorIndex] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // 빌드가 되기 위해 변수 사용 + console.log(isSubmitting); + + const handleTitleChange: React.ChangeEventHandler = (e) => { + setTitle(e.target.value); + setTitleError(''); + }; - const handleTitleChange = (e: React.ChangeEvent) => setTitle(e.target.value); const handleCategoryChange = (e: SelectChangeEvent) => { setCategory(e.target.value); }; + // 퀴즈 이름 변경 const handleQuizChange = (index: number, value: string) => { const updatedQuizSet = [...quizSet]; updatedQuizSet[index].quiz = value; setQuizSet(updatedQuizSet); + if (index === quizErrorIndex) setQuizErrorIndex(null); }; + // 제한 시간 변경 const handleLimitTimeChange = (index: number, value: string) => { const updatedQuizSet = [...quizSet]; - updatedQuizSet[index].limitTime = parseInt(value, 10); + let newLimitTime = parseInt(value, 10); + if (newLimitTime > 99) newLimitTime %= 10; + updatedQuizSet[index].limitTime = Math.min(60, Math.max(1, newLimitTime)); setQuizSet(updatedQuizSet); }; + // 질문지 변경 const handleChoiceChange = ( quizIndex: number, choiceIndex: number, @@ -74,33 +92,112 @@ export const QuizSetupPage: React.FC = () => { ) => { const updatedQuizSet = [...quizSet]; if (field === 'isAnswer') { - (updatedQuizSet[quizIndex].choices[choiceIndex][field] as boolean) = value === 'true'; + // 정답인지를 수정한 경우 + updatedQuizSet[quizIndex].choices = updatedQuizSet[quizIndex].choices.map((c, i) => ({ + ...c, + isAnswer: choiceIndex === i + })); } else { + // 질문지 인풋을 수정한 경우 (updatedQuizSet[quizIndex].choices[choiceIndex][field] as string) = value; + if ( + choiceErrorIndex && + quizIndex === choiceErrorIndex[0] && + choiceIndex === choiceErrorIndex[1] + ) + setChoiceErrorIndex(null); } setQuizSet(updatedQuizSet); }; - const addQuiz = () => { + + // 퀴즈 추가 + const addQuiz = useCallback(() => { setQuizSet([ ...quizSet, - { quiz: '', limitTime: 0, choices: [{ content: '', order: 1, isAnswer: false }] } + { quiz: '', limitTime: 10, choices: [{ content: '', order: 1, isAnswer: true }] } ]); + }, [quizSet]); + + const removeQuiz = (quizIndex: number) => { + setQuizSet(quizSet.filter((_, i) => i !== quizIndex)); }; + //선택지 삭제 + const removeChoice = (quizIndex: number, choiceIndex: number) => { + const updatedQuizSet = [...quizSet]; + updatedQuizSet[quizIndex].choices = updatedQuizSet[quizIndex].choices.filter( + (_, i) => i !== choiceIndex + ); + setQuizSet(updatedQuizSet); + }; + + //선택지 추가 const addChoice = (quizIndex: number) => { + if (quizSet[quizIndex].choices.length > 5) return; const updatedQuizSet = [...quizSet]; const newChoiceOrder = updatedQuizSet[quizIndex].choices.length + 1; updatedQuizSet[quizIndex].choices.push({ content: '', order: newChoiceOrder, isAnswer: false }); setQuizSet(updatedQuizSet); }; - const handleSubmit = () => { + const handleSubmit = async () => { + // 제목 비어있는지 검사 + if (!title.trim()) { + setTitleError('제목을 입력해주세요'); + return; + } + + // 퀴즈 이름 비어있는지 검사 + const emptyQuizIndex = quizSet.findIndex((quiz) => !quiz.quiz.trim()); + if (emptyQuizIndex >= 0) { + setQuizErrorIndex(emptyQuizIndex); + return; + } + + //선택지 비어있는지 검사 + const emptyQuizChoiceIndex = quizSet.findIndex((quiz) => + quiz.choices.find((choice) => !choice.content.trim()) + ); + if (emptyQuizChoiceIndex >= 0) { + const emptyChoiceIndex = quizSet[emptyQuizChoiceIndex].choices.findIndex( + (choice) => !choice.content.trim() + ); + setChoiceErrorIndex([emptyQuizChoiceIndex, emptyChoiceIndex]); + return; + } + const quizData: QuizData = { title, category, quizSet }; + const payload: CreateQuizSetPayload = { + title: quizData.title, + category: quizData.category, + quizList: quizData.quizSet // 이름 변경 + }; console.log('Quiz Data:', quizData); - // POST 요청 - // fetch "/api/quizset" + + try { + setIsSubmitting(true); // 로딩 시작 + const response = await createQuizSet(payload); + if (response) { + alert('퀴즈셋이 성공적으로 생성되었습니다!'); + // 성공적으로 생성되면 상태 초기화 + setTitle(''); + setCategory(''); + setQuizSet([]); + } else { + alert('퀴즈셋 생성에 실패했습니다. 다시 시도해주세요.'); + } + } catch (error) { + console.error('Error submitting quiz data:', error); + alert('퀴즈셋 생성 중 오류가 발생했습니다.'); + } finally { + setIsSubmitting(false); // 로딩 종료 + } }; + useEffect(() => { + if (quizSet.length === 0) addQuiz(); + }, [quizSet, addQuiz]); + return ( <> @@ -109,14 +206,7 @@ export const QuizSetupPage: React.FC = () => { 퀴즈셋 생성하기 - + - handleChoiceChange(quizIndex, choiceIndex, 'isAnswer', e.target.value) + error={ + choiceErrorIndex && + quizIndex === choiceErrorIndex[0] && + choiceIndex === choiceErrorIndex[1] + ? '선택지를 입력해주세요' + : '' } - className="w-28" > - 정답아님 - 정답 - - - ))} + + + + ))} + diff --git a/FE/src/store/useChatStore.ts b/FE/src/store/useChatStore.ts index 82e771a..0c67d32 100644 --- a/FE/src/store/useChatStore.ts +++ b/FE/src/store/useChatStore.ts @@ -20,6 +20,6 @@ export const useChatStore = create((set) => ({ } })); -socketService.onPermanently('chatMessage', (data) => { +socketService.on('chatMessage', (data) => { useChatStore.getState().addMessage(data); }); diff --git a/FE/src/store/usePlayerStore.ts b/FE/src/store/usePlayerStore.ts index 63d4283..8f51b88 100644 --- a/FE/src/store/usePlayerStore.ts +++ b/FE/src/store/usePlayerStore.ts @@ -92,7 +92,7 @@ export const usePlayerStore = create((set) => ({ } })); -socketService.onPermanently('joinRoom', (data) => { +socketService.on('joinRoom', (data) => { const { addPlayers, setCurrentPlayerId } = usePlayerStore.getState(); const newPlayers = data.players.map((player) => ({ ...player, @@ -107,11 +107,11 @@ socketService.onPermanently('joinRoom', (data) => { } }); -socketService.onPermanently('updatePosition', (data) => { +socketService.on('updatePosition', (data) => { usePlayerStore.getState().updatePlayerPosition(data.playerId, data.playerPosition); }); -socketService.onPermanently('endQuizTime', (data) => { +socketService.on('endQuizTime', (data) => { const { players, setPlayers } = usePlayerStore.getState(); const { gameMode } = useRoomStore.getState(); @@ -146,10 +146,10 @@ socketService.onPermanently('endQuizTime', (data) => { } }); -socketService.onPermanently('endGame', (data) => { +socketService.on('endGame', (data) => { usePlayerStore.getState().setIsHost(data.hostId === socketService.getSocketId()); }); -socketService.onPermanently('exitRoom', (data) => { +socketService.on('exitRoom', (data) => { usePlayerStore.getState().removePlayer(data.playerId); }); diff --git a/FE/src/store/useQuizStore.ts b/FE/src/store/useQuizStore.ts index d11feae..137e3e9 100644 --- a/FE/src/store/useQuizStore.ts +++ b/FE/src/store/useQuizStore.ts @@ -64,15 +64,15 @@ export const useQuizeStore = create((set) => ({ })); // 진행 중인 퀴즈 설정 -socketService.onPermanently('startQuizTime', (data) => { +socketService.on('startQuizTime', (data) => { useQuizeStore.getState().setQuizState(QuizState.START); useQuizeStore.getState().setCurrentQuiz(data); }); -socketService.onPermanently('endQuizTime', (data) => { +socketService.on('endQuizTime', (data) => { useQuizeStore.getState().setQuizState(QuizState.END); useQuizeStore.getState().setCurrentAnswer(Number(data.answer)); }); -socketService.onPermanently('endGame', () => { +socketService.on('endGame', () => { useQuizeStore.getState().resetQuiz(); }); diff --git a/FE/src/store/useRoomStore.ts b/FE/src/store/useRoomStore.ts index 19b081c..fbcbc88 100644 --- a/FE/src/store/useRoomStore.ts +++ b/FE/src/store/useRoomStore.ts @@ -36,18 +36,18 @@ export const useRoomStore = create((set) => ({ } })); -socketService.onPermanently('createRoom', (data) => { +socketService.on('createRoom', (data) => { useRoomStore.getState().updateRoom({ gameId: data.gameId }); }); -socketService.onPermanently('updateRoomOption', (data) => { +socketService.on('updateRoomOption', (data) => { useRoomStore.getState().updateRoom(data); }); -socketService.onPermanently('startGame', () => { +socketService.on('startGame', () => { useRoomStore.getState().setGameState(GameState.PROGRESS); }); -socketService.onPermanently('endGame', () => { +socketService.on('endGame', () => { useRoomStore.getState().setGameState(GameState.END); }); diff --git a/FE/src/utils/nickname.ts b/FE/src/utils/nickname.ts new file mode 100644 index 0000000..b3a9b61 --- /dev/null +++ b/FE/src/utils/nickname.ts @@ -0,0 +1,211 @@ +const adjectives = [ + '예쁜', + '멋진', + '아름다운', + '귀여운', + '친절한', + '상냥한', + '착한', + '즐거운', + '행복한', + '사랑스러운', + '빠른', + '느린', + '좋은', + '나쁜', + '큰', + '작은', + '넓은', + '좁은', + '높은', + '낮은', + '시원한', + '따뜻한', + '추운', + '더운', + '매운', + '짠', + '단', + '신', + '쓴', + '부드러운', + '단단한', + '무거운', + '가벼운', + '깨끗한', + '더러운', + '배고픈', + '배부른', + '건강한', + '아픈', + '젊은', + '늙은', + '똑똑한', + '멍청한', + '재미있는', + '지루한', + '깊은', + '얕은', + '화난', + '기쁜', + '슬픈', + '피곤한', + '힘든', + '쉬운', + '어려운', + '달콤한', + '상쾌한', + '차가운', + '따사로운', + '건조한', + '축축한', + '유명한', + '평범한', + '독특한', + '섬세한', + '강한', + '약한', + '단순한', + '복잡한', + '예민한', + '둔감한', + '부유한', + '가난한', + '화려한', + '소박한', + '튼튼한', + '낡은', + '새로운', + '기분 좋은', + '기분 나쁜', + '불안한', + '만족스러운', + '불만족스러운', + '차분한', + '흥분한', + '용감한', + '겁 많은', + '성실한', + '게으른', + '밝은', + '어두운', + '조용한', + '시끄러운', + '차가운', + '따스한', + '풍요로운', + '고요한', + '서늘한', + '아늑한', + '허전한', + '뿌듯한' +]; + +const animals = [ + '강아지', + '고양이', + '호랑이', + '사자', + '코끼리', + '기린', + '하마', + '원숭이', + '코뿔소', + '늑대', + '여우', + '곰', + '판다', + '돼지', + '토끼', + '햄스터', + '고슴도치', + '다람쥐', + '수달', + '악어', + '독수리', + '매', + '참새', + '비둘기', + '펭귄', + '갈매기', + '오리', + '백조', + '닭', + '공작새', + '올빼미', + '부엉이', + '까마귀', + '까치', + '앵무새', + '돌고래', + '상어', + '고래', + '해파리', + '문어', + '오징어', + '거북이', + '말', + '소', + '양', + '염소', + '치타', + '표범', + '재규어', + '알파카', + '라마', + '사슴', + '순록', + '고라니', + '바다사자', + '바다표범', + '물개', + '개미', + '벌', + '나비', + '잠자리', + '메뚜기', + '베짱이', + '거미', + '달팽이', + '지렁이', + '개구리', + '두꺼비', + '뱀', + '이구아나', + '카멜레온', + '도마뱀', + '참치', + '연어', + '고등어', + '갈치', + '정어리', + '붕어', + '잉어', + '메기', + '장어', + '송어', + '별똥별', + '흰수염고래', + '불가사리', + '해마', + '가재', + '게', + '랍스터', + '청개구리', + '코알라', + '캥거루', + '웜뱃', + '미어캣', + '스컹크', + '너구리', + '담비', + '족제비', + '밍크', + '퓨마' +]; + +export const getRandomNickname = () => { + const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)]; + const randomAnimal = animals[Math.floor(Math.random() * animals.length)]; + return randomAdjective + ' ' + randomAnimal; +}; diff --git a/FE/src/utils/serverTime.ts b/FE/src/utils/serverTime.ts index 96e07e4..25b5e0a 100644 --- a/FE/src/utils/serverTime.ts +++ b/FE/src/utils/serverTime.ts @@ -20,7 +20,8 @@ const syncServerTimestamp = () => { }); }; -syncServerTimestamp(); +//1초 후 초기화 +setTimeout(syncServerTimestamp, 1000); export const getServerTimestamp = () => { syncServerTimestamp(); diff --git a/FE/vite.config.ts b/FE/vite.config.ts index 0604559..9e47479 100644 --- a/FE/vite.config.ts +++ b/FE/vite.config.ts @@ -10,5 +10,8 @@ export default defineConfig({ '@': path.resolve(process.cwd(), 'src') } }, - publicDir: 'public' + publicDir: 'public', + define: { + 'process.env.SOCKET_PORT': 3333 + } });