Skip to content

Commit

Permalink
[FE] 게임 중 새로고침 처리, 탈주 및 탈락 표시 (#217)
Browse files Browse the repository at this point in the history
* feat: 탈주, 탈락자 표시 기능 구현

* chore: 불필요한 코드 제거

* feat: 게임중 키보드 새로고침 동작 방지 기능 추가

* feat: 게임중 입장 불가 Alert 띄우기

제대로 동작 안 하는 상태 이후 추가적인 수정 필요

* docs: README.md 업데이트

* chore: 토스트 메시지 수정
  • Loading branch information
studioOwol authored Dec 2, 2024
1 parent a8fdf52 commit e463405
Show file tree
Hide file tree
Showing 14 changed files with 137 additions and 33 deletions.
10 changes: 10 additions & 0 deletions fe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,13 @@
{...props}
/>
```

### 새로고침 지옥에서 꺼내줘

- 개발 시작 단계부터 날 괴롭게 했던 새로고침.. 재입장 처리로 어떻게 넘어갔었는데, 게임 중일 때는 막아야 함
- 키보드 동작은 막을 수 있는데 브라우저 새로고침 버튼 클릭은 막을 수 없음. Alert 띄우는 게 최선인데 이 Alert도 메시지 수정 불가.
- 채점 중에서 안 넘어가는 문제가 해결되지 않았다.
- 계속 테스트해 보는데 음성 데이터 전달 중에 새로고침 하면 채점이 안 되고 결과를 못 받아와서 그런 것 같다.
- `beforeunload` 이벤트의 브라우저 기본 alert보다 먼저 혹은 동시에 CustomAlertDialog을 띄우는 것은 불가능함
- 강퇴처럼 방 목록 페이지에 왔을 때 알림을 띄우기로 함
- 이게 왜 잘 안되는 건지 모르겠다.. 강퇴랑 별다를 게 없는 거 같은데..🤯 나중에 고쳐보는 걸로
7 changes: 6 additions & 1 deletion fe/src/components/common/CustomAlertDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface CustomAlertDialogProps {
title: string;
description?: string;
actionText?: string;
handleClick?: () => void;
}

const CustomAlertDialog = ({
Expand All @@ -22,6 +23,7 @@ const CustomAlertDialog = ({
title,
description,
actionText = '확인',
handleClick,
}: CustomAlertDialogProps) => {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
Expand All @@ -33,7 +35,10 @@ const CustomAlertDialog = ({
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction className="bg-primary hover:bg-primary/90">
<AlertDialogAction
className="bg-primary hover:bg-primary/90"
onClick={handleClick}
>
{actionText}
</AlertDialogAction>
</AlertDialogFooter>
Expand Down
1 change: 0 additions & 1 deletion fe/src/hooks/useBackExit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';

export const useBackExit = ({ setShowExitDialog }) => {
const popStateListenerRef = useRef<(() => void) | null>(null);
Expand Down
34 changes: 34 additions & 0 deletions fe/src/hooks/usePreventRefresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect } from 'react';
import { toast } from 'react-toastify';

export const usePreventRefresh = (isPlaying: boolean) => {
useEffect(() => {
if (!isPlaying) return;

const preventKeyboardRefresh = (e: KeyboardEvent) => {
if (
e.key === 'F5' ||
(e.ctrlKey && e.key === 'r') ||
(e.metaKey && e.key === 'r')
) {
e.preventDefault();

toast.error('게임 중에는 새로고침 할 수 없습니다!', {
position: 'top-left',
autoClose: 1000,
style: {
fontFamily: 'Galmuri11, monospace',
width: '25rem',
minWidth: '25rem',
},
});
}
};

document.addEventListener('keydown', preventKeyboardRefresh);

return () => {
document.removeEventListener('keydown', preventKeyboardRefresh);
};
}, [isPlaying]);
};
13 changes: 10 additions & 3 deletions fe/src/hooks/useReconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useAudioPermission } from './useAudioPermission';
import { useAudioManager } from './useAudioManager';
import useGameStore from '@/stores/zustand/useGameStore';

export const useReconnect = ({ currentRoom }) => {
const { roomId } = useParams();
Expand All @@ -14,6 +15,7 @@ export const useReconnect = ({ currentRoom }) => {
const { data: room } = getCurrentRoomQuery(roomId);
const { requestPermission } = useAudioPermission();
const audioManager = useAudioManager();
const { setGameInProgressError } = useGameStore();

useEffect(() => {
const handleReconnect = async () => {
Expand Down Expand Up @@ -42,19 +44,24 @@ export const useReconnect = ({ currentRoom }) => {
signalingSocket.setupLocalStream(stream);

// 5. 방 참가
gameSocket.joinRoom(roomId, nickname);
signalingSocket.joinRoom(room, nickname);
await gameSocket.joinRoom(roomId, nickname);
await signalingSocket.joinRoom(room, nickname);
}
} catch (error) {
console.error('Reconnection failed:', error);
// 실패 시 audioManager 제거
signalingSocket.setAudioManager(null);

if (error === 'GameAlreadyInProgress') {
setGameInProgressError(true);

window.location.href = '/';
}
}
};

handleReconnect();

// cleanup
return () => {
signalingSocket.setAudioManager(null);
};
Expand Down
24 changes: 19 additions & 5 deletions fe/src/pages/GamePage/GameScreen/EndScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,29 @@ import podiumAnimation from '@/assets/lottie/podium.json';
import useGameStore from '@/stores/zustand/useGameStore';
import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { useParams } from 'react-router-dom';
import { getCurrentRoomQuery } from '@/stores/queries/getCurrentRoomQuery';
import useRoomStore from '@/stores/zustand/useRoomStore';

const EndScreen = () => {
const rank = useGameStore((state) => state.rank);
const resetGame = useGameStore((state) => state.resetGame);
const { roomId } = useParams();
const { refetch } = getCurrentRoomQuery(roomId);
const { setCurrentRoom } = useRoomStore();

const handleGameEnd = () => {
resetGame();
const handleGameEnd = async () => {
try {
resetGame();
// room 정보 다시 가져오기
const { data } = await refetch();
// 새로운 room 정보로 상태 업데이트
if (data) {
setCurrentRoom(data);
}
} catch (error) {
console.error('Failed to refresh room data:', error);
}
};

const positions = {
Expand Down Expand Up @@ -43,7 +59,7 @@ const EndScreen = () => {
<div className="absolute inset-0 pointer-events-none">
{rank.slice(0, 3).map((playerName, index) => (
<motion.div
key={playerName}
key={`rank-${index}-${playerName}`}
className="absolute"
style={{
top: positions[index as keyof typeof positions].top,
Expand Down Expand Up @@ -76,7 +92,6 @@ const EndScreen = () => {
))}
</div>

{/* 최종 순위 리스트 */}
<motion.div
className="absolute top-4 right-4 bg-white/90 p-4 rounded-lg shadow-lg"
initial={{ opacity: 0, y: -20 }}
Expand All @@ -97,7 +112,6 @@ const EndScreen = () => {
</div>
</motion.div>

{/* 게임 종료 버튼 */}
<motion.div
className="absolute bottom-4 right-4 z-10"
initial={{ opacity: 0, y: 20 }}
Expand Down
2 changes: 1 addition & 1 deletion fe/src/pages/GamePage/GameScreen/GameScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const GameScreen = () => {
setCurrentPlayer(nickname);
}
}
}, [currentPlayer, setCurrentPlayer]);
}, [currentPlayer]);

return turnData ? <PlayScreen /> : <ReadyScreen />;
};
Expand Down
14 changes: 9 additions & 5 deletions fe/src/pages/GamePage/GameScreen/PlayScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import EndScreen from './EndScreen';
import ReadyScreen from './ReadyScreen';
import PitchVisualizer from '@/components/game/PitchVisualizer';
import { useParams } from 'react-router-dom';
import { usePreventRefresh } from '@/hooks/usePreventRefresh';

type GamePhase = 'intro' | 'gameplay' | 'grading' | 'result';

Expand All @@ -23,11 +24,14 @@ const PlayScreen = () => {
const rank = useGameStore((state) => state.rank);
const [gamePhase, setGamePhase] = useState<GamePhase>('intro');
const [timeLeft, setTimeLeft] = useState(0);
const { roomId: roomIdParam } = useParams();
const { roomId } = useParams();

const INTRO_TIME = 2000;
const RESULT_TIME = 3000;

// 새로고침 방지
usePreventRefresh(Boolean(turnData && rank.length === 0));

// 턴 데이터 변경 시 게임 초기화
useEffect(() => {
if (!turnData) return;
Expand All @@ -36,16 +40,16 @@ const PlayScreen = () => {
setGamePhase('intro');
setGameResult(null);

const introTimer = setTimeout(() => {
const introTimer = setTimeout(async () => {
setGamePhase('gameplay');
setTimeLeft(turnData.timeLimit); // gameplay 페이즈로 전환될 때 시간 설정

// 현재 플레이어 차례이고 게임 참여 가능한 경우에만 녹음 시작
if (currentPlayer === turnData.playerNickname) {
voiceSocket
await voiceSocket
.startRecording(
signalingSocket.getLocalStream(),
roomIdParam,
roomId,
currentPlayer
)
.catch(console.error);
Expand Down Expand Up @@ -73,7 +77,7 @@ const PlayScreen = () => {
}, 1000);

return () => clearInterval(timer);
}, [gamePhase, currentPlayer, turnData]);
}, [gamePhase, turnData]);

// 채점 중 -> 결과 화면 전환
useEffect(() => {
Expand Down
31 changes: 27 additions & 4 deletions fe/src/pages/GamePage/PlayerList/Player.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Card, CardContent } from '@/components/ui/card';
import { FaCrown, FaMicrophone, FaMicrophoneSlash } from 'react-icons/fa6';
import { FaCrown, FaMicrophoneSlash, FaRegFaceSmile } from 'react-icons/fa6';
import VolumeBar from './VolumeBar';
import { PlayerProps } from '@/types/roomTypes';
import { isHost } from '@/utils/playerUtils';
Expand All @@ -12,7 +12,7 @@ import { gameSocket } from '@/services/gameSocket';
import MikeButton from '@/components/common/MikeButton';
import useGameStore from '@/stores/zustand/useGameStore';

const Player = ({ playerNickname, isReady }: PlayerProps) => {
const Player = ({ playerNickname, isReady, isDead, isLeft }: PlayerProps) => {
const { currentRoom, currentPlayer } = useRoomStore();
// 본인이 방장인지
const isCurrentPlayerHost = currentPlayer === currentRoom?.hostNickname;
Expand Down Expand Up @@ -54,11 +54,34 @@ const Player = ({ playerNickname, isReady }: PlayerProps) => {

return (
<Card className={`h-full ${!isPlayerHost && isReady ? 'bg-cyan-50' : ''}`}>
<CardContent className="flex h-[4.7rem] items-center justify-between p-4">
<CardContent className="relative flex h-[4.7rem] items-center justify-between p-4">
<div className="flex items-center gap-2">
{isPlayerHost ? <FaCrown className="text-yellow-500" /> : ''}
{isPlayerHost ? (
<FaCrown className="text-yellow-500 mr-1" />
) : (
<FaRegFaceSmile className="mr-1" />
)}
<span className="font-galmuri">{playerNickname}</span>
</div>

<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
{isLeft ? (
<img
className="w-16 h-10 md:w-20 md:h-12 lg:w-[6.875rem] lg:h-[3.625rem]"
src="https://i.imgur.com/JCNlJnB.png"
alt="탈주"
/>
) : isDead ? (
<img
className="w-16 h-10 md:w-20 md:h-12 lg:w-[6.875rem] lg:h-[3.625rem]"
src="https://i.imgur.com/kcsoaeY.png"
alt="탈락"
/>
) : (
''
)}
</div>

<div className="flex items-center gap-4">
{isCurrentPlayer ? (
<MikeButton isMuted={isCurrentPlayerMuted} onToggle={toggleMute} />
Expand Down
11 changes: 7 additions & 4 deletions fe/src/pages/GamePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ import JoinDialog from '../RoomListPage/RoomDialog/JoinDialog';
const GamePage = () => {
const [showJoinDialog, setShowJoinDialog] = useState(false);
const [showExitDialog, setShowExitDialog] = useState(false);
const { currentRoom, kickedPlayer, setKickedPlayer } = useRoomStore();
const { kickedPlayer, setKickedPlayer } = useRoomStore();
const currentRoom = useRoomStore((state) => state.currentRoom);
const audioManager = useAudioManager();
const { roomId } = useParams();
const { data: room } = getCurrentRoomQuery(roomId);
const nickname = sessionStorage.getItem('user_nickname');

useReconnect({ currentRoom });
useBackExit({ setShowExitDialog });

useEffect(() => {
if (room && !currentRoom) {
if (!nickname) {
Expand All @@ -31,9 +35,6 @@ const GamePage = () => {
}
}, [room, currentRoom, nickname]);

useReconnect({ currentRoom });
useBackExit({ setShowExitDialog });

// 오디오 매니저 설정
useEffect(() => {
signalingSocket.setAudioManager(audioManager);
Expand Down Expand Up @@ -113,6 +114,8 @@ const GamePage = () => {
players={currentRoom.players.map((player) => ({
playerNickname: player.playerNickname,
isReady: player.isReady,
isDead: player.isDead,
isLeft: player.isLeft,
}))}
/>
</div>
Expand Down
1 change: 0 additions & 1 deletion fe/src/pages/RoomListPage/RoomList/RoomList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const RoomList = () => {
const [isJoinDialogOpen, setIsJoinDialogOpen] = useState(false);
const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
const [showPagination, setShowPagination] = useState(false);
const isEmpty = rooms.length === 0;

useEffect(() => {
if (pagination?.totalPages > 1) {
Expand Down
6 changes: 6 additions & 0 deletions fe/src/pages/RoomListPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { useEffect, useState } from 'react';
import CustomAlertDialog from '@/components/common/CustomAlertDialog';
import useRoomStore from '@/stores/zustand/useRoomStore';
import { useRoomsSSE } from '@/hooks/useRoomsSSE';
import useGameStore from '@/stores/zustand/useGameStore';

const RoomListPage = () => {
const [showAlert, setShowAlert] = useState(false);
const [kickedRoomName, setKickedRoomName] = useState('');
const { rooms } = useRoomStore();
const isEmpty = rooms.length === 0;
const { setGameInProgressError } = useGameStore();

useRoomsSSE();

Expand All @@ -24,6 +26,10 @@ const RoomListPage = () => {
}
}, []);

const handleGameInProgressError = () => {
setGameInProgressError(false);
};

return (
<div className="min-h-screen flex flex-col pb-16 relative">
<div className="flex-1">
Expand Down
Loading

0 comments on commit e463405

Please sign in to comment.