diff --git a/FE/src/api/socket/mocks/SocketMock.ts b/FE/src/api/socket/mocks/SocketMock.ts index 866b73a1..902b247b 100644 --- a/FE/src/api/socket/mocks/SocketMock.ts +++ b/FE/src/api/socket/mocks/SocketMock.ts @@ -6,8 +6,27 @@ type SocketEvent = keyof SocketDataMap; export class SocketMock { private listenerSet: Record void)[]> = {}; private onAnyListenerList: ((event: string, ...args: unknown[]) => void)[] = []; + initialrized: Promise; constructor() { - console.log(`Mock WebSocket 연결`); + console.log(`%c Mock WebSocket 연결`, 'color:yellow; font-weight:bold;'); + this.initialrized = new Promise((resolve) => { + this.delay(0).then(() => { + resolve(); + }); + }); + this.initialrized.then(() => { + const currentPlayer = { + playerId: this.id, + playerName: 'Me', + playerPosition: [0.5, 0.5] as [number, number] + }; + this.emitServer('joinRoom', { players: [currentPlayer] }); + this.addPlayers([currentPlayer]); + this.emitServer('getSelfId', { playerId: this.id }); + this.emitServer('setPlayerName', { playerId: this.id, playerName: 'Me' }); + }); + + // } } /** @@ -28,12 +47,10 @@ export class SocketMock { this.onAnyListenerList.push(listener); } emit(event: T, data: SocketDataMap[T]['request']) { - //여기서 서버에 데이터 전송 + console.log(`%c SERVER_SOCKET[${event}]`, 'background:blue; color:white', data); switch (event) { case SocketEvents.CHAT_MESSAGE: return this.handleChat(data as SocketDataMap[typeof SocketEvents.CHAT_MESSAGE]['request']); - case SocketEvents.JOIN_ROOM: - return this.handleJoin(data as SocketDataMap[typeof SocketEvents.JOIN_ROOM]['request']); case SocketEvents.UPDATE_POSITION: return this.handlePosition( data as SocketDataMap[typeof SocketEvents.UPDATE_POSITION]['request'] @@ -119,18 +136,6 @@ export class SocketMock { await this.delay(0.1); this.chatMessage(this.id, data.message); } - private async handleJoin(data: SocketDataMap[typeof SocketEvents.JOIN_ROOM]['request']) { - if (this.getPlayer(this.id)) return; - await this.delay(0.1); - const currentPlayer: (typeof this.players)[string] = { - playerId: this.id, - playerName: data.playerName, - playerPosition: [0.5, 0.5] - }; - this.emitServer(SocketEvents.JOIN_ROOM, { players: this.getPlayerList() }); - this.emitServer(SocketEvents.JOIN_ROOM, { players: [currentPlayer] }); - this.addPlayers([currentPlayer]); - } private async handlePosition( data: SocketDataMap[typeof SocketEvents.UPDATE_POSITION]['request'] ) { @@ -172,6 +177,7 @@ export class SocketMock { } addPlayers(players: Array) { players.forEach((p) => (this.players[p.playerId] = p)); + this.emitServer('joinRoom', { players }); } setQuiz(quiz: string, quizSecond: number, choiceList: string[]) { const COUNT_DOWN_TIME = 3000; @@ -225,7 +231,8 @@ export class SocketMock { }); } - createDummyPlayer(count: number) { + async createDummyPlayer(count: number) { + await this.initialrized; const playerCount = Object.keys(this.players).length; this.addPlayers( Array(count) @@ -239,6 +246,7 @@ export class SocketMock { } async chatRandom(testSec: number, chatPerSecPerPlyaer: number = 1) { + await this.initialrized; const playerCount = this.getPlayerList().length; for (let j = 0; j < testSec; j++) { for (const player of this.getPlayerList()) { @@ -250,6 +258,7 @@ export class SocketMock { } async moveRandom(testSec: number, movePerSecPerPlyaer: number = 1) { + await this.initialrized; const playerCount = this.getPlayerList().length; for (let j = 0; j < testSec; j++) { for (const player of this.getPlayerList()) { diff --git a/FE/src/api/socket/socket.ts b/FE/src/api/socket/socket.ts index f06eca89..5d8db204 100644 --- a/FE/src/api/socket/socket.ts +++ b/FE/src/api/socket/socket.ts @@ -42,20 +42,20 @@ class SocketService { this.handlers = []; } - async connect() { + async connect(header: { 'create-room'?: string; 'game-id'?: string }) { if (this.isActive()) return; - this.socket = io(this.url) as SocketInterface; - await new Promise((resolve, reject) => { - this.socket.on('connect', () => resolve()); - this.socket.on('error', () => reject()); - }); - this.initHandler(); - return; - } - - async connectMock(gameId: keyof typeof mockMap) { - if (this.isActive()) return; - this.socket = new mockMap[gameId]() as SocketInterface; + const gameId = header['game-id']; + if (gameId && gameId in mockMap) { + // mock과 연결 + this.socket = new mockMap[gameId as keyof typeof mockMap]() as SocketInterface; + } else { + // 소켓 연결 + this.socket = io(this.url, { query: header }) as SocketInterface; + await new Promise((resolve, reject) => { + this.socket.on('connect', () => resolve()); + this.socket.on('error', () => reject()); + }); + } this.initHandler(); } @@ -70,27 +70,13 @@ class SocketService { } disconnect() { - this.socket.disconnect(); + if (this.isActive()) this.socket.disconnect(); } isActive() { return this.socket && this.socket.connected; } - getSocketId() { - return this.socket.id; - } - - // deprecated - onPermanently( - event: T, - callback: (data: SocketDataMap[T]['response']) => void - ) { - const handler = () => this.socket.on(event, callback); - this.handlers.push(handler); - if (this.isActive()) handler(); - } - on(event: T, callback: (data: SocketDataMap[T]['response']) => void) { if (this.isActive()) this.socket.on(event, callback); if (!this.handlerMap[event]) this.handlerMap[event] = []; @@ -107,15 +93,22 @@ class SocketService { this.socket.emit(event, data); } - async createRoom(payload: SocketDataMap['createRoom']['request']) { - await this.connect(); - this.socket.emit(SocketEvents.CREATE_ROOM, payload); + async createRoom(option: { + title: string; + gameMode: 'RANKING' | 'SURVIVAL'; + maxPlayerCount: number; + isPublic: boolean; + }) { + this.disconnect(); + await this.connect({ + 'create-room': Object.entries(option) + .map(([key, value]) => key + '=' + value) + .join(';') + }); } - async joinRoom(gameId: string, playerName: string) { - if (gameId in mockMap) this.connectMock(gameId as keyof typeof mockMap); - else if (!this.isActive()) await this.connect(); - this.socket.emit(SocketEvents.JOIN_ROOM, { gameId, playerName }); + async joinRoom(gameId: string) { + await this.connect({ 'game-id': gameId }); } kickRoom(gameId: string, kickPlayerId: string) { diff --git a/FE/src/api/socket/socketEventTypes.ts b/FE/src/api/socket/socketEventTypes.ts index e91d7726..409b4462 100644 --- a/FE/src/api/socket/socketEventTypes.ts +++ b/FE/src/api/socket/socketEventTypes.ts @@ -22,14 +22,6 @@ type UpdatePositionResponse = { playerPosition: [number, number]; }; -// 게임방 생성 타입 -type CreateRoomRequest = { - title: string; - gameMode: 'RANKING' | 'SURVIVAL'; - maxPlayerCount: number; - isPublic: boolean; -}; - type CreateRoomResponse = { gameId: string; // PIN }; @@ -62,12 +54,6 @@ type UpdateRoomQuizsetResponse = { quizCount: number; }; -// 게임방 입장 타입 -type JoinRoomRequest = { - gameId: string; - playerName: string; -}; - type JoinRoomResponse = { players: Array<{ playerId: string; // socketId @@ -76,22 +62,26 @@ type JoinRoomResponse = { }>; }; -// 게임 시작 타입 -type StartGameRequest = { - gameId: string; +type getSelfIdResponse = { + playerId: string; }; -type StartGameResponse = Record; // 빈 객체 +type setPlayerNameRequest = { + playerName: string; +}; -// 게임 정지 타입 -type StopGameRequest = { - gameId: string; +type setPlayerNameResponse = { + playerId: string; + playerName: string; }; -type StopGameResponse = { - status: string; +// 게임 시작 타입 +type StartGameRequest = { + gameId: string; }; +type StartGameResponse = Record; // 빈 객체 + type EndGameRequest = { gameId: string; }; @@ -113,11 +103,6 @@ type StartQuizTimeEvent = { startTime: number; //timestamp }; -// 게임 점수 업데이트 타입 -type UpdateScoreEvent = { - scores: Map; // Map -}; - // 게임방 퇴장 타입 type ExitRoomEvent = { playerId: string; @@ -127,6 +112,7 @@ type KickRoomRequest = { gameId: string; kickPlayerId: string; }; + type KickRoomResponse = { playerId: string; }; @@ -142,7 +128,7 @@ export type SocketDataMap = { response: UpdatePositionResponse; }; createRoom: { - request: CreateRoomRequest; + request: null; response: CreateRoomResponse; }; updateRoomOption: { @@ -154,17 +140,21 @@ export type SocketDataMap = { response: UpdateRoomQuizsetResponse; }; joinRoom: { - request: JoinRoomRequest; + request: null; response: JoinRoomResponse; }; + getSelfId: { + request: null; + response: getSelfIdResponse; + }; + setPlayerName: { + request: setPlayerNameRequest; + response: setPlayerNameResponse; + }; startGame: { request: StartGameRequest; response: StartGameResponse; }; - stopGame: { - request: StopGameRequest; - response: StopGameResponse; - }; endQuizTime: { request: null; response: EndQuizTimeEvent; @@ -173,10 +163,6 @@ export type SocketDataMap = { request: null; response: StartQuizTimeEvent; }; - updateScore: { - request: null; - response: UpdateScoreEvent; - }; exitRoom: { request: null; response: ExitRoomEvent; diff --git a/FE/src/features/game/components/AnswerModal.tsx b/FE/src/features/game/components/AnswerModal.tsx index 0e4d6fd5..86f9203c 100644 --- a/FE/src/features/game/components/AnswerModal.tsx +++ b/FE/src/features/game/components/AnswerModal.tsx @@ -1,33 +1,31 @@ import Lottie from 'lottie-react'; import AnswerBg from '@/assets/lottie/answer_background.json'; -// import { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; type AnswerModalProps = { isOpen: boolean; - // onClose: () => void; answer: number; }; const AnswerModal: React.FC = ({ isOpen, answer }) => { - // const [countdown, setCountdown] = useState(3); + const [countdown, setCountdown] = useState(3); - // useEffect(() => { - // if (isOpen) { - // setCountdown(3); // 모달이 열릴 때 카운트다운을 초기화 - // const interval = setInterval(() => { - // setCountdown((prev) => { - // if (prev === 1) { - // clearInterval(interval); - // onClose(); // 0에 도달하면 모달 닫기 - // } - // return prev - 1; - // }); - // }, 1000); - // return () => clearInterval(interval); // 모달이 닫히거나 언마운트될 때 타이머 정리 - // } - // }, [isOpen, onClose]); + useEffect(() => { + if (isOpen) { + setCountdown(3); + const interval = setInterval(() => { + setCountdown((prev: number) => { + if (prev === 1) { + clearInterval(interval); + } + return prev - 1; + }); + }, 1000); + return () => clearInterval(interval); + } + }, [isOpen]); - if (!isOpen) return null; + if (!isOpen || countdown <= 0) return null; return (
= ({ isOpen, answer }) => {

{answer}

- {/* */}
); diff --git a/FE/src/features/game/components/GameHeader.tsx b/FE/src/features/game/components/GameHeader.tsx index 6a8ae317..561599ce 100644 --- a/FE/src/features/game/components/GameHeader.tsx +++ b/FE/src/features/game/components/GameHeader.tsx @@ -1,5 +1,4 @@ import { ClipboardCopy } from '../../../components/ClipboardCopy'; -import Card from '@mui/material/Card'; import { QuizPreview } from '../../../components/QuizPreview'; import { useParams } from 'react-router-dom'; import { useRoomStore } from '@/features/game/data/store/useRoomStore'; @@ -23,7 +22,7 @@ export const GameHeader = React.memo(() => { }; // 예시 return ( - +
@@ -31,9 +30,13 @@ export const GameHeader = React.memo(() => {
{gameTitle}
- +
+
+ +
+
{isHost && ( -
+
); }); diff --git a/FE/src/features/game/components/Modal.tsx b/FE/src/features/game/components/NicknameModal.tsx similarity index 96% rename from FE/src/features/game/components/Modal.tsx rename to FE/src/features/game/components/NicknameModal.tsx index bd75eec6..a065a4fd 100644 --- a/FE/src/features/game/components/Modal.tsx +++ b/FE/src/features/game/components/NicknameModal.tsx @@ -8,7 +8,7 @@ type ModalProps = { onSubmit: (value: string) => void; }; -export const Modal: React.FC = ({ +export const NicknameModal: React.FC = ({ isOpen, title, placeholder, diff --git a/FE/src/features/game/components/ParticipantDisplay.tsx b/FE/src/features/game/components/ParticipantDisplay.tsx index eed350f4..ae427bbc 100644 --- a/FE/src/features/game/components/ParticipantDisplay.tsx +++ b/FE/src/features/game/components/ParticipantDisplay.tsx @@ -3,6 +3,7 @@ import GameState from '@/constants/gameState'; import { usePlayerStore } from '@/features/game/data/store/usePlayerStore'; import { useRoomStore } from '@/features/game/data/store/useRoomStore'; import { motion } from 'framer-motion'; +import { useCallback, useMemo } from 'react'; type ParticipantDisplayProps = { gameState: keyof typeof GameState; @@ -10,105 +11,122 @@ type ParticipantDisplayProps = { const ParticipantDisplay: React.FC = ({ gameState }) => { const players = usePlayerStore((state) => state.players); - const playerCount = usePlayerStore((state) => state.players.size); const maxPlayerCount = useRoomStore((state) => state.maxPlayerCount); const currentPlayerId = usePlayerStore((state) => state.currentPlayerId); const isHost = usePlayerStore((state) => state.isHost); const gameMode = useRoomStore((state) => state.gameMode); const gameId = useRoomStore((state) => state.gameId); - const handleKick = (playerId: string) => { - socketService.kickRoom(gameId, playerId); - }; - - // 대기 모드일 때 참가자 목록 표시 - const renderWaitingMode = () => ( -
- {Array.from(players).map(([, player], i) => ( -
-
{i + 1 + '. ' + player.playerName}
- {isHost && currentPlayerId !== player.playerId && ( - - )} -
- ))} -
+ const handleKick = useCallback( + (playerId: string) => { + socketService.kickRoom(gameId, playerId); + }, + [gameId] ); - // 진행 모드일 때 랭킹 현황 표시 - const renderProgressRankingMode = () => ( -
- {Array.from(players) - .sort(([, a], [, b]) => b.playerScore - a.playerScore) // 점수 내림차순 - .map(([, player], i) => ( - ( +
+ {Array.from(players).map(([, player]) => ( +
-
{i + 1 + '. ' + player.playerName}
+
{player.emoji + ' ' + player.playerName}
+ {isHost && currentPlayerId !== player.playerId && ( + + )} +
+ ))} +
+ ), + [players, currentPlayerId, handleKick, isHost] + ); + + // 진행 모드일 때 랭킹 현황 표시 + const renderProgressRankingMode = useCallback( + () => ( +
+ {Array.from(players) + .sort(([, a], [, b]) => b.playerScore - a.playerScore) // 점수 내림차순 + .map(([, player]) => ( - {player.emoji + ' ' + player.playerName}
+ - {player.playerScore} - + + {player.playerScore} + +
- - ))} -
+ ))} +
+ ), + [players] ); // 진행 모드일 때 생존자 표시 - const renderProgressSurvivalMode = () => ( -
- {Array.from(players) - .filter(([, player]) => player.isAlive) - .map(([, player], i) => ( - -
- {i + 1 + '. ' + player.playerName} -
-
- ))} -
+ const renderProgressSurvivalMode = useCallback( + () => ( +
+ {Array.from(players) + .filter(([, player]) => player.isAlive) + .map(([, player]) => ( + +
+ {player.emoji + ' ' + player.playerName} +
+
+ ))} +
+ ), + [players] + ); + + const playerList = useMemo( + () => + gameState === GameState.WAIT + ? renderWaitingMode() + : gameMode === 'SURVIVAL' + ? renderProgressSurvivalMode() + : renderProgressRankingMode(), + [gameState, gameMode, renderProgressRankingMode, renderProgressSurvivalMode, renderWaitingMode] ); return (
{gameState === GameState.WAIT - ? `참가자 [${playerCount}/${maxPlayerCount}]` + ? `참가자 [${players.size}/${maxPlayerCount}]` : gameMode === 'SURVIVAL' ? '생존자' : `랭킹 현황`}
- {gameState === GameState.WAIT - ? renderWaitingMode() - : gameMode === 'SURVIVAL' - ? renderProgressSurvivalMode() - : renderProgressRankingMode()} + {playerList}
); }; diff --git a/FE/src/features/game/components/Player.tsx b/FE/src/features/game/components/Player.tsx index a0acfbbc..1d0ccd85 100644 --- a/FE/src/features/game/components/Player.tsx +++ b/FE/src/features/game/components/Player.tsx @@ -89,7 +89,13 @@ export const Player = ({ playerId, boardSize, isCurrent }: Props) => { }} onClick={(e) => e.preventDefault()} > -
+
{/* 정답 시 정답 이펙트 5초 켜졌다가 사라짐 */} {showEffect && (
{ /> )} {/*
*/} -
{quizState === 'end' && !player.isAnswer ? '😭' : '😃'}
+
+ {quizState === 'end' && !player.isAnswer ? '👻' : player.emoji} +
diff --git a/FE/src/features/game/components/QuizHeader.tsx b/FE/src/features/game/components/QuizHeader.tsx index c9acfcc9..283f3777 100644 --- a/FE/src/features/game/components/QuizHeader.tsx +++ b/FE/src/features/game/components/QuizHeader.tsx @@ -33,7 +33,7 @@ export const QuizHeader = () => { if (!currentQuiz) return ( -
+
곧 퀴즈가 시작됩니다
@@ -41,13 +41,13 @@ export const QuizHeader = () => { if (currentQuiz.startTime > getServerTimestamp()) return ( -
+
{Math.ceil((currentQuiz.startTime - getServerTimestamp()) / 1000)}
); return ( -
+
{ {seconds <= 0 ? '종료' : seconds.toFixed(2)}
-
0.2 ? 'green' : 'brown' - }} - >
+ {seconds > 0 && ( +
0.2 ? 'green' : 'brown' + }} + >
+ )}
diff --git a/FE/src/features/game/components/QuizSetSearchList.tsx b/FE/src/features/game/components/QuizSetSearchList.tsx index d002c307..6390adaa 100644 --- a/FE/src/features/game/components/QuizSetSearchList.tsx +++ b/FE/src/features/game/components/QuizSetSearchList.tsx @@ -65,8 +65,22 @@ const QuizSetSearchList = ({ onClick, search }: Params) => { return () => observer.disconnect(); }, [onIntersect]); - if (isLoading) return

Loading...

; - if (isError) return

Error fetching data.

; + if (isLoading) return; +
+

Loading...

+
; + if (isError) + return ( +
+

Error fetching data.

+
+ ); + if (data?.pages[0].data.length === 0) + return ( +
+ {search}와(과) 일치하는 검색결과가 없습니다 +
+ ); return ( <> diff --git a/FE/src/features/game/components/QuizSettingModal.tsx b/FE/src/features/game/components/QuizSettingModal.tsx index 20083822..6bfbf0dd 100644 --- a/FE/src/features/game/components/QuizSettingModal.tsx +++ b/FE/src/features/game/components/QuizSettingModal.tsx @@ -84,10 +84,8 @@ export const QuizSettingModal = ({ isOpen, onClose }: Props) => { ✕
-
- {searchParam && ( - - )} +
+
diff --git a/FE/src/features/game/components/ResultModal.tsx b/FE/src/features/game/components/ResultModal.tsx index 47eee371..f285c228 100644 --- a/FE/src/features/game/components/ResultModal.tsx +++ b/FE/src/features/game/components/ResultModal.tsx @@ -47,7 +47,7 @@ export const ResultModal: React.FC = ({ ) : ( 탈락 )}{' '} - {player.playerName} + {player.emoji + ' ' + player.playerName} {gameMode === 'RANKING' && ( {player.playerScore}점 diff --git a/FE/src/features/game/data/socketListener.ts b/FE/src/features/game/data/socketListener.ts index e4336a6a..fedcb189 100644 --- a/FE/src/features/game/data/socketListener.ts +++ b/FE/src/features/game/data/socketListener.ts @@ -6,30 +6,24 @@ import { useRoomStore } from './store/useRoomStore'; import GameState from '@/constants/gameState'; import QuizState from '@/constants/quizState'; import { getQuizSetDetail } from '@/api/rest/quizApi'; +import { getEmoji } from '../utils/emoji'; // chat socketService.on('chatMessage', (data) => { useChatStore.getState().addMessage(data); }); -socketService.on('disconnect', () => { - useChatStore.getState().reset(); -}); - // player socketService.on('joinRoom', (data) => { - const { addPlayers, setCurrentPlayerId } = usePlayerStore.getState(); + const { addPlayers } = usePlayerStore.getState(); const newPlayers = data.players.map((player) => ({ ...player, playerScore: 0, isAlive: true, - isAnswer: true + isAnswer: true, + emoji: getEmoji() })); addPlayers(newPlayers); - const socketId = socketService.getSocketId(); - if (newPlayers.length > 0 && newPlayers[0].playerId === socketId) { - setCurrentPlayerId(socketId); - } }); socketService.on('updatePosition', (data) => { @@ -49,7 +43,8 @@ socketService.on('endQuizTime', (data) => { playerPosition: _p?.playerPosition || [0, 0], playerScore: p.score, isAnswer: p.isAnswer, - isAlive: _p?.isAlive || false + isAlive: _p?.isAlive || false, + emoji: _p?.emoji || 'o' }; }) ); @@ -72,15 +67,24 @@ socketService.on('endQuizTime', (data) => { }); socketService.on('endGame', (data) => { - usePlayerStore.getState().setIsHost(data.hostId === socketService.getSocketId()); + usePlayerStore.getState().setIsHost(data.hostId === usePlayerStore.getState().currentPlayerId); }); socketService.on('exitRoom', (data) => { usePlayerStore.getState().removePlayer(data.playerId); }); -socketService.on('disconnect', () => { - usePlayerStore.getState().reset(); +socketService.on('getSelfId', (data) => { + const playerName = usePlayerStore.getState().players.get(data.playerId); + usePlayerStore.getState().setCurrentPlayerId(data.playerId); + usePlayerStore.getState().setCurrentPlayerName(String(playerName)); +}); + +socketService.on('setPlayerName', (data) => { + usePlayerStore.getState().setPlayerName(data.playerId, data.playerName); + if (data.playerId === usePlayerStore.getState().currentPlayerId) { + usePlayerStore.getState().setCurrentPlayerName(data.playerName); + } }); // Quiz @@ -105,10 +109,6 @@ socketService.on('updateRoomQuizset', async (data) => { useQuizStore.getState().setQuizSet(String(res?.title), String(res?.category)); }); -socketService.on('disconnect', () => { - useQuizStore.getState().reset(); -}); - // Room socketService.on('createRoom', (data) => { @@ -132,6 +132,11 @@ socketService.on('kickRoom', () => { // 메인페이지 or 로비로 이동시키기? }); +// 소켓 연결 해제시 초기화 + socketService.on('disconnect', () => { useRoomStore.getState().reset(); + usePlayerStore.getState().reset(); + useChatStore.getState().reset(); + useQuizStore.getState().reset(); }); diff --git a/FE/src/features/game/data/store/usePlayerStore.ts b/FE/src/features/game/data/store/usePlayerStore.ts index 4b43857f..e03d2460 100644 --- a/FE/src/features/game/data/store/usePlayerStore.ts +++ b/FE/src/features/game/data/store/usePlayerStore.ts @@ -7,6 +7,7 @@ type Player = { playerScore: number; isAnswer: boolean; isAlive: boolean; + emoji: string; }; type PlayerStore = { @@ -22,6 +23,7 @@ type PlayerStore = { setIsHost: (isHost: boolean) => void; setPlayers: (players: Player[]) => void; resetScore: () => void; + setPlayerName: (playerId: string, playerName: string) => void; reset: () => void; }; @@ -83,5 +85,13 @@ export const usePlayerStore = create((set) => ({ }); }, + setPlayerName: (playerId, playerName) => { + set((state) => { + const targetPlayer = state.players.get(playerId); + if (targetPlayer) state.players.set(playerId, { ...targetPlayer, playerName }); + return { players: state.players }; + }); + }, + reset: () => set(initialPlayerState) })); diff --git a/FE/src/features/game/pages/GamePage.tsx b/FE/src/features/game/pages/GamePage.tsx index 3220f1bb..b3dcfcf3 100644 --- a/FE/src/features/game/pages/GamePage.tsx +++ b/FE/src/features/game/pages/GamePage.tsx @@ -1,7 +1,7 @@ import Chat from '@/features/game/components/Chat'; import ParticipantDisplay from '@/features/game/components/ParticipantDisplay'; import { QuizOptionBoard } from '@/features/game/components/QuizOptionBoard'; -import { Modal } from '../components/Modal'; +import { NicknameModal } from '../components/NicknameModal'; import { useState, useEffect } from 'react'; import { GameHeader } from '@/features/game/components/GameHeader'; import { HeaderBar } from '@/components/HeaderBar'; @@ -15,16 +15,17 @@ import { ResultModal } from '@/features/game/components/ResultModal'; import { ErrorModal } from '@/components/ErrorModal'; import { useNavigate } from 'react-router-dom'; import { getRandomNickname } from '@/features/game/utils/nickname'; +import { resetEmojiPool } from '../utils/emoji'; export const GamePage = () => { const { gameId } = useParams<{ gameId: string }>(); const updateRoom = useRoomStore((state) => state.updateRoom); const gameState = useRoomStore((state) => state.gameState); const currentPlayerName = usePlayerStore((state) => state.currentPlayerName); - const setCurrentPlayerName = usePlayerStore((state) => state.setCurrentPlayerName); + // const setCurrentPlayerName = usePlayerStore((state) => state.setCurrentPlayerName); const setGameState = useRoomStore((state) => state.setGameState); const resetScore = usePlayerStore((state) => state.resetScore); - const [isModalOpen, setIsModalOpen] = useState(true); + // const [isModalOpen, setIsModalOpen] = useState(true); const [isErrorModalOpen, setIsErrorModalOpen] = useState(false); const [errorModalTitle, setErrorModalTitle] = useState(''); const [isResultOpen, setIsResultOpen] = useState(false); @@ -40,14 +41,16 @@ export const GamePage = () => { // }, []); useEffect(() => { - updateRoom({ gameId }); - }, [gameId, updateRoom]); + if (gameId) resetEmojiPool(gameId); + }, [gameId]); useEffect(() => { - if (gameId && currentPlayerName) { - socketService.joinRoom(gameId, currentPlayerName); - } - }, [gameId, currentPlayerName]); + if (gameId) socketService.joinRoom(gameId); + }, [gameId]); + + useEffect(() => { + updateRoom({ gameId }); + }, [gameId, updateRoom]); useEffect(() => { if (gameState === GameState.END) setIsResultOpen(true); @@ -58,25 +61,24 @@ export const GamePage = () => { setIsErrorModalOpen(true); }); - const handleNameSubmit = (name: string) => { - setCurrentPlayerName(name); - setIsModalOpen(false); // 이름이 설정되면 모달 닫기 - }; - const handleEndGame = () => { setGameState(GameState.WAIT); resetScore(); setIsResultOpen(false); }; + const handleSubmitNickname = (name: string) => { + socketService.emit('setPlayerName', { playerName: name }); + }; + return ( <>
-
+
{gameState === GameState.WAIT ? : }
-
+
@@ -93,12 +95,12 @@ export const GamePage = () => { onClose={handleEndGame} currentPlayerName={currentPlayerName} /> - { const { updateRoom } = useRoomStore((state) => state); const setIsHost = usePlayerStore((state) => state.setIsHost); @@ -29,10 +30,6 @@ export const GameSetupPage = () => { navigate(`/game/${data.gameId}`); }); - useEffect(() => { - socketService.disconnect(); - }, []); - const handleTitleChange: React.ChangeEventHandler = (e) => { setTitle(e.target.value); setTitleError(''); diff --git a/FE/src/features/game/pages/PinPage.tsx b/FE/src/features/game/pages/PinPage.tsx index 639bf7ce..7f6de8e9 100644 --- a/FE/src/features/game/pages/PinPage.tsx +++ b/FE/src/features/game/pages/PinPage.tsx @@ -1,27 +1,22 @@ -import { socketService } from '@/api/socket'; +import { socketService, useSocketEvent } from '@/api/socket'; import { HeaderBar } from '@/components/HeaderBar'; import { TextInput } from '@/components/TextInput'; -import { getRandomNickname } from '@/features/game/utils/nickname'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; export const PinPage = () => { - const [nickname, setNickname] = useState(''); const [pin, setPin] = useState(''); const [errors, setErrors] = useState({ nickname: '', pin: '' }); + const navigate = useNavigate(); - useEffect(() => { - setNickname(getRandomNickname()); - }, []); + useSocketEvent('joinRoom', () => { + navigate(`/game/${pin}`); + }); const handleJoin = () => { const newErrors = { nickname: '', pin: '' }; let hasError = false; - if (!nickname.trim()) { - newErrors.nickname = '닉네임을 입력해주세요'; - hasError = true; - } - if (!pin.trim()) { newErrors.pin = '핀번호를 입력해주세요'; hasError = true; @@ -31,7 +26,7 @@ export const PinPage = () => { if (hasError) return; - socketService.joinRoom(pin, nickname); + socketService.joinRoom(pin); }; return ( @@ -41,16 +36,6 @@ export const PinPage = () => {

방 들어가기

- { - setNickname(e.target.value); - if (errors.nickname) setErrors((prev) => ({ ...prev, nickname: '' })); - }} - error={errors.nickname} - /> - getEmojiByNumber(i)) + .sort(() => seededRandom() - 0.5); +} + +export function getEmoji() { + return emojiPool[front++ % emojiPool.length]; +} +function getEmojiByNumber(n: number) { + const base = 0x1f600; // 😀 시작점 + const emojiCode = base + n; // n번째 이모지 + const emoji = String.fromCodePoint(emojiCode); + return emoji; +} + +let seed = 0; +function initialSeed(str: string) { + seed = 0; + for (let i = 0; i < str.length; i++) { + seed = (seed * 31 + str.charCodeAt(i)) & 0xffffffff; + } +} + +function seededRandom() { + const m = 0x80000000; + const a = 1103515245; + const c = 12345; + + seed = (a * seed + c) % m; + return seed / m; +} diff --git a/FE/src/index.css b/FE/src/index.css index ca11f3a7..c1a6024b 100644 --- a/FE/src/index.css +++ b/FE/src/index.css @@ -1,4 +1,10 @@ -@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css'); +@font-face { + font-family: 'NPSfontBold'; + src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2310@1.0/NPSfontBold.woff2') + format('woff2'); + font-weight: 700; + font-style: normal; +} @tailwind base; @tailwind components; @@ -6,7 +12,7 @@ @layer base { html { - font-family: 'Pretendard', ui-sans-serif, system-ui; + font-family: 'NPSfontBold', ui-sans-serif, system-ui; box-sizing: border-box; color: #5f6e76; } diff --git a/FE/tailwind.config.js b/FE/tailwind.config.js index f0462dd8..377b0f98 100644 --- a/FE/tailwind.config.js +++ b/FE/tailwind.config.js @@ -33,7 +33,7 @@ export default { m: '1rem', s: '0.5rem' }, - dropShadow: { + boxShadow: { default: '0 4px 2px rgba(20, 33, 43, 0.02)' } } @@ -50,7 +50,7 @@ export default { '.component-popup': { borderRadius: theme('borderRadius.m'), backgroundColor: theme('backgroundColor.surface.default'), - dropShadow: theme('dropShadow.default') + boxShadow: theme('boxShadow.default') }, '.center': { display: 'flex',