diff --git a/src/commons/svgs/GameReadyButton.jsx b/src/commons/svgs/GameReadyButton.jsx index b13f06d3..671fe2af 100644 --- a/src/commons/svgs/GameReadyButton.jsx +++ b/src/commons/svgs/GameReadyButton.jsx @@ -1,15 +1,13 @@ -import game_ready_active from "assets/commons/game-ready-active.png"; -import game_ready_btn from "assets/commons/game-ready-btn.png"; -import game_ready_hover_btn from "assets/commons/game-ready-hover-btn.png"; -import { useState } from "react"; -import "styles/commons/svgs/GameStartButton.scss"; +import game_ready_active from 'assets/commons/game-ready-active.png'; +import game_ready_btn from 'assets/commons/game-ready-btn.png'; +import game_ready_hover_btn from 'assets/commons/game-ready-hover-btn.png'; +import { useState } from 'react'; +import 'styles/commons/svgs/GameStartButton.scss'; -const GameReadyButton = ({ onClick }) => { +const GameReadyButton = ({ onClick, isReady }) => { const [isHover, setIsHover] = useState(false); - const [isClicked, setClicked] = useState(false); const handleClick = (e) => { - setClicked(!isClicked); onClick?.(e); // 기존 클릭 핸들러 호출 }; @@ -30,13 +28,13 @@ const GameReadyButton = ({ onClick }) => { @@ -44,7 +42,7 @@ const GameReadyButton = ({ onClick }) => { { + const { showToast } = useToastStore.getState(); const [isHover, setIsHover] = useState(false); const [isClicked, setClicked] = useState(false); const handleClick = (e) => { - setClicked(!isClicked); - onClick?.(e); // 기존 클릭 핸들러 호출 + if (onClick(e)) { + setClicked(!isClicked); + return; + } + showToast('alert', '모든 플레이어가 준비되어야 합니다!'); }; return ( @@ -30,13 +35,13 @@ const GameStartButton = ({ onClick }) => { diff --git a/src/components/lobby/waiting/WaitingRoom.jsx b/src/components/lobby/waiting/WaitingRoom.jsx index d4ef4a73..552d19b4 100644 --- a/src/components/lobby/waiting/WaitingRoom.jsx +++ b/src/components/lobby/waiting/WaitingRoom.jsx @@ -1,24 +1,28 @@ -import CheckBox from "commons/svgs/CheckBox"; -import GameReadyButton from "commons/svgs/GameReadyButton"; -import GameStartButton from "commons/svgs/GameStartButton"; -import PlayerSlot from "components/lobby/waiting/PlayerSlot"; -import useWaitingRoomSocket from "hooks/waiting-room/useWaitingRoomSocket"; -import { useEffect, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; -import { getRoomDetailAPI } from "services/lobby/waiting/waitingRoom"; -import "styles/components/lobby/waiting/WaitingRoom.scss"; -import { getUserUuid } from "utils/user"; +import CheckBox from 'commons/svgs/CheckBox'; +import GameReadyButton from 'commons/svgs/GameReadyButton'; +import GameStartButton from 'commons/svgs/GameStartButton'; +import PlayerSlot from 'components/lobby/waiting/PlayerSlot'; +import useWaitingRoomSocket from 'hooks/waiting-room/useWaitingRoomSocket'; +import { useEffect, useRef, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { getRoomDetailAPI } from 'services/lobby/waiting/waitingRoom'; +import { useToastStore } from 'store/toast'; +import 'styles/components/lobby/waiting/WaitingRoom.scss'; +import { getUserUuid } from 'utils/user'; const WaitingRoom = () => { + const { showToast } = useToastStore.getState(); const navigate = useNavigate(); const location = useLocation(); const userUuid = getUserUuid(); const [roomId, setRoomId] = useState(null); // 방번호 const [roomInfo, setRoomInfo] = useState(null); // 방 전체 정보 - const [selectedDifficulty, setSelectedDifficulty] = useState("BASIC"); // 난이도 + const [selectedDifficulty, setSelectedDifficulty] = useState(null); // 난이도 const [isReady, setIsReady] = useState(false); // 준비 여부 const [isOwner, setIsOwner] = useState(false); + const ownerIsMe = userUuid === roomInfo?.roomInfo.ownerUuid; + const userInfos = roomInfo?.userInfos || []; const { startGame, @@ -35,7 +39,7 @@ const WaitingRoom = () => { // 최초 방정보 갱신 useEffect(() => { const params = new URLSearchParams(location.search); - const roomIdFromQuery = params.get("roomId"); + const roomIdFromQuery = params.get('roomId'); if (roomIdFromQuery) { setRoomId(roomIdFromQuery); @@ -54,13 +58,14 @@ const WaitingRoom = () => { } }; + const isFirstDifficultyInit = useRef(true); // 방정보 갱신 시 데이터 분배 useEffect(() => { if (!roomInfo) return; // 내 정보 조회 const myState = roomInfo.userInfos.find( - (user) => user.userUuid === userUuid + (user) => user.userUuid === userUuid, ); const isMeOwner = roomInfo.roomInfo.ownerUuid === userUuid; @@ -69,8 +74,20 @@ const WaitingRoom = () => { // 레디 여부 적용 setIsReady(myState?.ready); + + // 최초 난이도 세팅이면 토스트 스킵 + if (isFirstDifficultyInit.current) { + isFirstDifficultyInit.current = false; + setSelectedDifficulty(roomInfo.roomInfo.difficulty); + return; + } + + if (roomInfo.roomInfo.difficulty === selectedDifficulty) { + return; + } // 난이도 적용 setSelectedDifficulty(roomInfo.roomInfo.difficulty); + showToast('alert', '로봇 성능이 변경되었습니다!'); }, [roomInfo]); // 게임 시작 @@ -83,12 +100,14 @@ const WaitingRoom = () => { // 방 나가기 const quitRoomHandler = () => { quitRoom(roomId); - navigate("/lobby"); + navigate('/lobby'); }; // 게임 시작 요청 const gameStartHandler = () => { + if (!userInfos.every((user) => user.ready)) return false; startGame(roomId); + return true; }; // 게임 준비/취소 @@ -101,7 +120,10 @@ const WaitingRoom = () => { // 난이도 변경 const updateDifficultyHandler = (difficulty) => { - if (!isOwner) return; + if (!isOwner) { + showToast('alert', '방장만 로봇 성능을 변경할 수 있습니다!'); + return; + } if (difficulty === selectedDifficulty) return; setSelectedDifficulty(difficulty); @@ -150,24 +172,24 @@ const WaitingRoom = () => {
updateDifficultyHandler("BASIC")} + checked={selectedDifficulty === 'BASIC'} + onChange={() => updateDifficultyHandler('BASIC')} /> updateDifficultyHandler("ADVANCED")} + checked={selectedDifficulty === 'ADVANCED'} + onChange={() => updateDifficultyHandler('ADVANCED')} />
- {userUuid === roomInfo.roomInfo.ownerUuid ? ( + {ownerIsMe ? ( ) : ( )} diff --git a/src/components/scenes/game1/BattleScene.jsx b/src/components/scenes/game1/BattleScene.jsx index 754b67df..c56f1ad0 100644 --- a/src/components/scenes/game1/BattleScene.jsx +++ b/src/components/scenes/game1/BattleScene.jsx @@ -1,17 +1,17 @@ -import leftCloudImg from "assets/components/scenes/commons/left-cloud.png"; -import rightCloudImg from "assets/components/scenes/commons/right-cloud.png"; -import aiImg from "assets/components/scenes/game1/ai.png"; -import userImg from "assets/components/scenes/game1/player.png"; -import Timer from "commons/Timer"; -import { correctSFX, game1BattleBGM, incorrectSFX } from "constants/audio"; -import useAudio from "hooks/audio/useAudio"; -import useBattle from "hooks/game/game1/useBattle"; +import leftCloudImg from 'assets/components/scenes/commons/left-cloud.png'; +import rightCloudImg from 'assets/components/scenes/commons/right-cloud.png'; +import aiImg from 'assets/components/scenes/game1/ai.png'; +import userImg from 'assets/components/scenes/game1/player.png'; +import Timer from 'commons/Timer'; +import { correctSFX, game1BattleBGM, incorrectSFX } from 'constants/audio'; +import useAudio from 'hooks/audio/useAudio'; +import useBattle from 'hooks/game/game1/useBattle'; // import useEffectSound from "hooks/game/game1/useEffectSound"; -import { useEffect, useRef, useState } from "react"; -import { useToastStore } from "store/toast"; -import "styles/components/scenes/game1/BattleScene.scss"; -import { isPressEnterKey } from "utils/keyDown"; -import { isBlank } from "utils/validation"; +import { useEffect, useRef, useState } from 'react'; +import { useToastStore } from 'store/toast'; +import 'styles/components/scenes/game1/BattleScene.scss'; +import { isPressEnterKey } from 'utils/keyDown'; +import { isBlank } from 'utils/validation'; const BattleScene = ({ roomId, drawings }) => { // 배틀 씬 guess 상태 관리 훅 호출 @@ -29,7 +29,7 @@ const BattleScene = ({ roomId, drawings }) => { const { showToast } = useToastStore.getState(); const inputRef = useRef(null); - const [inputValue, setInputValue] = useState(""); + const [inputValue, setInputValue] = useState(''); const [aiSaying, setAiSaying] = useState(true); useAudio(game1BattleBGM); @@ -37,7 +37,7 @@ const BattleScene = ({ roomId, drawings }) => { // const { playEffect } = useEffectSound(); useEffect(() => { - showToast("gamealert", "AI가 맞출 차례입니다!"); + showToast('gamealert', 'AI가 맞출 차례입니다!'); }, []); useEffect(() => { @@ -50,7 +50,7 @@ const BattleScene = ({ roomId, drawings }) => { return () => clearTimeout(timer); } else { // ai 턴으로 이동 - setInputValue(""); + setInputValue(''); setAiSaying(true); } }, [isAiguessTurn]); @@ -66,11 +66,11 @@ const BattleScene = ({ roomId, drawings }) => { if (guessResult === null) return; if (guessResult) { - showToast("gameO"); + showToast('gameO'); playTrack(correctSFX); // playEffect("correct"); } else { - showToast("gameX"); + showToast('gameX'); playTrack(incorrectSFX); // playEffect("incorrect"); } @@ -78,11 +78,11 @@ const BattleScene = ({ roomId, drawings }) => { const handleKeyDown = (e) => { if (isSubmit) return; - if (isPressEnterKey(e)) { + if (isPressEnterKey(e) && e.nativeEvent.isComposing === false) { e.preventDefault(); if (!isBlank(inputValue)) { - inputRef.current?.blur(); sendGuess(inputValue); + inputRef.current?.blur(); } } }; diff --git a/src/hooks/game/game1/useBattle.js b/src/hooks/game/game1/useBattle.js index 34dbc3f9..f3d95f6b 100644 --- a/src/hooks/game/game1/useBattle.js +++ b/src/hooks/game/game1/useBattle.js @@ -1,8 +1,8 @@ -import { SOCKET_GAME_API } from "constants/api"; -import { useCallback, useEffect, useState } from "react"; -import { useGameSocketStore } from "store/socket"; -import { useToastStore } from "store/toast"; -import { getUserUuid } from "utils/user"; +import { SOCKET_GAME_API } from 'constants/api'; +import { useCallback, useEffect, useState } from 'react'; +import { useGameSocketStore } from 'store/socket'; +import { useToastStore } from 'store/toast'; +import { getUserUuid } from 'utils/user'; const useBattle = ({ roomId }) => { const userUuid = getUserUuid(); @@ -25,28 +25,29 @@ const useBattle = ({ roomId }) => { (message) => { const payload = JSON.parse(message.body); const { eventType: type, data } = payload; + const aiSay = JSON.parse(payload.aiSays)?.message; switch (type) { - case "GUESS_REQUEST": + case 'GUESS_REQUEST': setEndTime(data.guessEndTime); setGuessResult(null); setIsMyguessTurn(data.guesserUuid === userUuid); - setIsAiguessTurn(data.guesserUuid === "AI"); + setIsAiguessTurn(data.guesserUuid === 'AI'); setIsSubmit(false); break; - case "GUESS_SUBMIT": + case 'GUESS_SUBMIT': setGuessWord(data.guessWord); - setAiSays(payload.aiSays); + setAiSays(aiSay); break; - case "GUESS_RESULT": + case 'GUESS_RESULT': setGuessResult(data.correct); - setAiSays(payload.aiSays); + setAiSays(aiSay); setGuessWord(null); break; default: break; } - } + }, ); return () => sub.unsubscribe(); @@ -56,17 +57,17 @@ const useBattle = ({ roomId }) => { (guess) => { if (!isConnected) return; setIsSubmit(true); - showToast("alert", "제출 완료!"); + showToast('alert', '제출 완료!'); stompClient.publish({ destination: `/pub${SOCKET_GAME_API}/${roomId}`, body: JSON.stringify({ - eventType: "GUESS_SUBMIT", + eventType: 'GUESS_SUBMIT', data: { guessWord: guess }, }), }); }, - [isConnected, stompClient, roomId] + [isConnected, stompClient, roomId], ); return { diff --git a/src/hooks/waiting-room/useWaitingRoomSocket.js b/src/hooks/waiting-room/useWaitingRoomSocket.js index e9e730c6..c1a4dde8 100644 --- a/src/hooks/waiting-room/useWaitingRoomSocket.js +++ b/src/hooks/waiting-room/useWaitingRoomSocket.js @@ -1,8 +1,8 @@ -import { SOCKET_ROOM_API, SOCKET_ROOM_ERROR_API } from "constants/api"; -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router"; -import { useGameSocketStore } from "store/socket"; -import { useToastStore } from "store/toast"; +import { SOCKET_ROOM_API, SOCKET_ROOM_ERROR_API } from 'constants/api'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { useGameSocketStore } from 'store/socket'; +import { useToastStore } from 'store/toast'; /** * useWaitingRoom 커스텀 훅 @@ -10,19 +10,19 @@ import { useToastStore } from "store/toast"; */ const roomExitEX = { - eventType: "EXIT", + eventType: 'EXIT', }; const gameStartEX = { - eventType: "START", + eventType: 'START', }; const gameReadyEX = { - eventType: "READY", + eventType: 'READY', }; const gameUnreadyEX = { - eventType: "UNREADY", + eventType: 'UNREADY', }; const useWaitingRoomSocket = ({ roomId, userUuid, setRoomInfo }) => { @@ -30,18 +30,19 @@ const useWaitingRoomSocket = ({ roomId, userUuid, setRoomInfo }) => { const { stompClient, isConnected } = useGameSocketStore(); const [isGameStart, setIsGameStart] = useState(false); const [initGameInfo, setInitGameInfo] = useState(null); + const { showToast } = useToastStore.getState(); useEffect(() => { if (roomId === null) return; if (!isConnected) return; - console.log("방 입장 : " + roomId); + console.log('방 입장 : ' + roomId); const roomErrorSub = stompClient.subscribe( `${SOCKET_ROOM_ERROR_API}/${userUuid}`, (message) => { const payload = JSON.parse(message.body); - console.log("방 에러:", payload); - } + console.log('방 에러:', payload); + }, ); // 🔔 방 이벤트 여부 구독 @@ -49,57 +50,57 @@ const useWaitingRoomSocket = ({ roomId, userUuid, setRoomInfo }) => { `/sub${SOCKET_ROOM_API}/${roomId}`, (message) => { const payload = JSON.parse(message.body); - console.log("방 이벤트:", payload); + console.log('방 이벤트:', payload); const eventType = payload.type; const data = payload.data; switch (eventType) { - case "READY": - console.log("READY"); + case 'READY': + console.log('READY'); setRoomInfo((prev) => ({ ...prev, userInfos: prev.userInfos.map((user) => - user.userUuid === data ? { ...user, ready: true } : user + user.userUuid === data ? { ...user, ready: true } : user, ), })); break; - case "UNREADY": - console.log("UNREADY"); + case 'UNREADY': + console.log('UNREADY'); setRoomInfo((prev) => ({ ...prev, userInfos: prev.userInfos.map((user) => - user.userUuid === data ? { ...user, ready: false } : user + user.userUuid === data ? { ...user, ready: false } : user, ), })); break; - case "START": - console.log("START"); + case 'START': + console.log('START'); setIsGameStart(true); setInitGameInfo(data.gameData); break; - case "UPDATE": - console.log("UPDATE"); + case 'UPDATE': + console.log('UPDATE'); setRoomInfo(data); break; - case "JOIN": - console.log("JOIN"); + case 'JOIN': + console.log('JOIN'); setRoomInfo((prev) => ({ ...prev, userInfos: data, })); break; - case "EXIT": - console.log("EXIT"); + case 'EXIT': + console.log('EXIT'); setRoomInfo((prev) => ({ ...prev, userInfos: prev.userInfos.filter((user) => user.userUuid != data), })); break; - case "KICK": - console.log("KICK"); + case 'KICK': + console.log('KICK'); if (data == userUuid) { - navigate("/lobby"); - showToast("alert", "나~~가 ㅋㅋ"); + navigate('/lobby'); + showToast('alert', '나~~가 ㅋㅋ'); } setRoomInfo((prev) => ({ ...prev, @@ -110,7 +111,7 @@ const useWaitingRoomSocket = ({ roomId, userUuid, setRoomInfo }) => { default: break; } - } + }, ); // 🔕 대기방 이벤트 구독 해제 @@ -159,7 +160,7 @@ const useWaitingRoomSocket = ({ roomId, userUuid, setRoomInfo }) => { // body const roomUpdateEX = { - eventType: "UPDATE", + eventType: 'UPDATE', content: JSON.stringify(updateRoomDTO), }; diff --git a/src/pages/game/Game1CreatePage.jsx b/src/pages/game/Game1CreatePage.jsx index 0bc08fe2..351b4ba3 100644 --- a/src/pages/game/Game1CreatePage.jsx +++ b/src/pages/game/Game1CreatePage.jsx @@ -119,6 +119,7 @@ const Game1CreatePage = () => { type="text" placeholder="방 제목을 입력해주세요." value={title} + maxLength={50} onChange={(e) => setTitle(e.target.value)} /> {titleError && {titleError}}