Skip to content

Commit 6b1e113

Browse files
authored
fix: 대기중, 게임중에 뒤로 가기 동작 처리 (#8)
1 parent 05cc262 commit 6b1e113

File tree

3 files changed

+397
-286
lines changed

3 files changed

+397
-286
lines changed

src/layout/game/GameLayout.js

Lines changed: 195 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,211 @@
1-
import {Outlet, useNavigate, useParams} from "react-router-dom";
2-
import Sidebar from "./Sidebar";
3-
import {useSetRecoilState} from "recoil";
1+
import { Outlet, useNavigate, useParams } from 'react-router-dom';
2+
import Sidebar from './Sidebar';
3+
import { useSetRecoilState, useRecoilValue } from 'recoil';
44
import {
5-
chatAtom, gameResultAtom,
6-
gameSettingAtom,
7-
loginUserAtom,
8-
playerListAtom,
9-
questionResultAtom,
10-
questionsAtom,
11-
questionStartAtom,
12-
rankUpdateAtom,
13-
roomSettingAtom,
14-
stompSendMessageAtom,
15-
systemNoticeAtom
16-
} from "../../state/atoms";
17-
import useStompClient from "../../hooks/useStompClient";
18-
import {useCallback, useEffect} from "react";
19-
import {useApiQuery} from "../../hooks/useApiQuery";
20-
import axios from "axios";
5+
chatAtom,
6+
gameResultAtom,
7+
gameSettingAtom,
8+
loginUserAtom,
9+
playerListAtom,
10+
questionResultAtom,
11+
questionsAtom,
12+
questionStartAtom,
13+
rankUpdateAtom,
14+
roomSettingAtom,
15+
stompSendMessageAtom,
16+
systemNoticeAtom,
17+
quizStartedAtom,
18+
} from '../../state/atoms';
19+
import useStompClient from '../../hooks/useStompClient';
20+
import { useCallback, useEffect, useRef, useLayoutEffect } from 'react';
21+
import { useApiQuery } from '../../hooks/useApiQuery';
22+
import axios from 'axios';
2123

2224
const PLAYER_COLORS = [
23-
"text-red-600",
24-
"text-blue-600",
25-
"text-red-500",
26-
"text-orange-500",
27-
"text-cyan-500",
28-
"text-green-600",
29-
"text-purple-600",
30-
"text-pink-600",
25+
'text-red-600',
26+
'text-blue-600',
27+
'text-red-500',
28+
'text-orange-500',
29+
'text-cyan-500',
30+
'text-green-600',
31+
'text-purple-600',
32+
'text-pink-600',
3133
];
3234

3335
const authMeRequest = async () => {
34-
const response = await axios.get(`/auth/me`);
35-
return response.data;
36+
const response = await axios.get(`/auth/me`);
37+
return response.data;
3638
};
3739

3840
const GameLayout = () => {
39-
const { id: roomId } = useParams();
40-
const setPlayerList = useSetRecoilState(playerListAtom);
41-
const setRoomSetting = useSetRecoilState(roomSettingAtom);
42-
const setGameSetting = useSetRecoilState(gameSettingAtom);
43-
const setChat = useSetRecoilState(chatAtom);
44-
const setQuestions = useSetRecoilState(questionsAtom);
45-
const setQuestionStart = useSetRecoilState(questionStartAtom);
46-
const setQuestionResult = useSetRecoilState(questionResultAtom);
47-
const setRankUpdate = useSetRecoilState(rankUpdateAtom);
48-
const setGameResult = useSetRecoilState(gameResultAtom);
49-
const setSystemNotice = useSetRecoilState(systemNoticeAtom);
50-
const setSendMessage = useSetRecoilState(stompSendMessageAtom);
51-
const setLoginUser = useSetRecoilState(loginUserAtom);
52-
const navigate = useNavigate();
53-
54-
const { isLoading, data } = useApiQuery(
55-
["authme"],
56-
() => authMeRequest(),
57-
);
58-
59-
useEffect(() => {
60-
if (data) {
61-
setLoginUser(data);
62-
}
63-
}, [data])
64-
65-
// 메시지를 처리하는 콜백
66-
const handleStompMessage = useCallback((payload) => {
67-
console.log('receive message: ', payload)
68-
switch (payload.type) {
69-
case "PLAYER_LIST":
70-
const { host, players } = payload.message;
71-
const processedPlayers = players.map((player, index) => {
72-
let status = "waiting";
73-
if (player.nickname === host) {
74-
status = "host";
75-
} else if (player.ready) {
76-
status = "ready";
77-
}
78-
return {
79-
...player,
80-
status,
81-
color: PLAYER_COLORS[index] || "text-gray-500",
82-
};
83-
});
84-
setPlayerList(processedPlayers);
85-
break;
86-
case "ROOM_SETTING":
87-
setRoomSetting(payload.message);
88-
break;
89-
case "GAME_SETTING":
90-
setGameSetting(payload.message);
91-
break;
92-
case "SYSTEM_NOTICE":
93-
setSystemNotice(payload.message);
94-
break;
95-
case "CHAT":
96-
setChat(payload.message);
97-
break;
98-
case "GAME_START":
99-
setQuestions(payload.message.questions);
100-
navigate("play");
101-
break;
102-
case "QUESTION_START":
103-
setQuestionStart(payload.message);
104-
break;
105-
case "QUESTION_RESULT":
106-
setQuestionResult(payload.message);
107-
break;
108-
case "RANK_UPDATE":
109-
setRankUpdate(payload.message.rank);
110-
break;
111-
case "GAME_RESULT":
112-
setGameResult(payload.message.result);
113-
break;
114-
case "EXIT_SUCCESS":
115-
disconnect();
116-
navigate("/room");
117-
break;
118-
default:
119-
console.warn("알 수 없는 메시지", payload);
120-
}
121-
}, [setPlayerList, setRoomSetting, setGameSetting, setSystemNotice, setChat]);
41+
const { id: roomId } = useParams();
42+
const setPlayerList = useSetRecoilState(playerListAtom);
43+
const setRoomSetting = useSetRecoilState(roomSettingAtom);
44+
const setGameSetting = useSetRecoilState(gameSettingAtom);
45+
const setChat = useSetRecoilState(chatAtom);
46+
const setQuestions = useSetRecoilState(questionsAtom);
47+
const setQuestionStart = useSetRecoilState(questionStartAtom);
48+
const setQuestionResult = useSetRecoilState(questionResultAtom);
49+
const setRankUpdate = useSetRecoilState(rankUpdateAtom);
50+
const setGameResult = useSetRecoilState(gameResultAtom);
51+
const setSystemNotice = useSetRecoilState(systemNoticeAtom);
52+
const setSendMessage = useSetRecoilState(stompSendMessageAtom);
53+
const setLoginUser = useSetRecoilState(loginUserAtom);
54+
const navigate = useNavigate();
55+
56+
const isQuizStarted = useRecoilValue(quizStartedAtom);
57+
58+
const { isLoading, data } = useApiQuery(['authme'], () => authMeRequest());
59+
60+
useEffect(() => {
61+
if (data) {
62+
setLoginUser(data);
63+
}
64+
}, [data, setLoginUser]);
65+
66+
const disconnectRef = useRef(null);
67+
const ignorePopState = useRef(false);
68+
69+
const handleStompMessage = useCallback(
70+
(payload) => {
71+
console.log('receive message: ', payload);
72+
switch (payload.type) {
73+
case 'PLAYER_LIST':
74+
const { host, players } = payload.message;
75+
const processedPlayers = players.map((player, index) => {
76+
let status = 'waiting';
77+
if (player.nickname === host) {
78+
status = 'host';
79+
} else if (player.ready) {
80+
status = 'ready';
81+
}
82+
return {
83+
...player,
84+
status,
85+
color: PLAYER_COLORS[index] || 'text-gray-500',
86+
};
87+
});
88+
setPlayerList(processedPlayers);
89+
break;
90+
case 'ROOM_SETTING':
91+
setRoomSetting(payload.message);
92+
break;
93+
case 'GAME_SETTING':
94+
setGameSetting(payload.message);
95+
break;
96+
case 'SYSTEM_NOTICE':
97+
setSystemNotice(payload.message);
98+
break;
99+
case 'CHAT':
100+
setChat(payload.message);
101+
break;
102+
case 'GAME_START':
103+
setQuestions(payload.message.questions);
104+
navigate('play');
105+
break;
106+
case 'QUESTION_START':
107+
setQuestionStart(payload.message);
108+
break;
109+
case 'QUESTION_RESULT':
110+
setQuestionResult(payload.message);
111+
break;
112+
case 'RANK_UPDATE':
113+
setRankUpdate(payload.message.rank);
114+
break;
115+
case 'GAME_RESULT':
116+
setGameResult(payload.message.result);
117+
break;
118+
case 'EXIT_SUCCESS':
119+
// 서버로부터 퇴장 성공 메시지를 받은 후 소켓 연결을 끊고 페이지 이동
120+
if (disconnectRef.current) {
121+
disconnectRef.current();
122+
}
123+
navigate('/room');
124+
break;
125+
default:
126+
console.warn('알 수 없는 메시지', payload);
127+
}
128+
},
129+
[
130+
setPlayerList,
131+
setRoomSetting,
132+
setGameSetting,
133+
setSystemNotice,
134+
setChat,
135+
setQuestions,
136+
setQuestionStart,
137+
setQuestionResult,
138+
setRankUpdate,
139+
setGameResult,
140+
navigate,
141+
],
142+
);
122143

123-
const { sendMessage, disconnect } = useStompClient(roomId, handleStompMessage);
144+
const { sendMessage, disconnect } = useStompClient(
145+
roomId,
146+
handleStompMessage,
147+
);
124148

125-
useEffect(() => {
149+
useEffect(() => {
150+
disconnectRef.current = disconnect;
151+
}, [disconnect]);
152+
153+
useEffect(() => {
154+
if (sendMessage) {
155+
setSendMessage(() => sendMessage);
156+
}
157+
}, [sendMessage, setSendMessage]);
158+
159+
// **뒤로가기 이벤트를 감지하고 라우팅을 방지하는 로직**
160+
useLayoutEffect(() => {
161+
const handlePopState = (event) => {
162+
// ignorePopState 플래그가 true이면 이벤트를 무시하고 플래그를 false로 되돌립니다.
163+
if (ignorePopState.current) {
164+
ignorePopState.current = false;
165+
return;
166+
}
167+
168+
if (isQuizStarted) {
169+
const userConfirmed = window.confirm(
170+
'지금 나가시면 게임 결과가 반영되지 않습니다. 정말 나가시겠습니까?',
171+
);
172+
if (userConfirmed) {
173+
// '확인'을 누르면 서버에 퇴장 메시지를 보냅니다.
174+
if (disconnectRef.current) {
175+
disconnectRef.current();
176+
navigate('/room');
177+
}
178+
} else {
179+
// '취소'를 누르면 뒤로가기 동작을 무효화하고 현재 페이지에 머무릅니다.
180+
// history.go(1)을 통해 뒤로가기 기록을 앞으로 이동시키고,
181+
// ignorePopState 플래그를 설정하여 불필요한 이벤트 중복을 막습니다.
182+
ignorePopState.current = true;
183+
window.history.go(1);
184+
}
185+
} else {
186+
// 게임 시작 전에는 경고 없이 바로 퇴장 메시지를 보냅니다.
126187
if (sendMessage) {
127-
setSendMessage(() => sendMessage); // Recoil 전역 등록
188+
sendMessage(`/pub/room/exit/${roomId}`, '');
128189
}
129-
}, [sendMessage]);
190+
}
191+
};
192+
193+
window.history.pushState(null, null, window.location.href);
194+
window.addEventListener('popstate', handlePopState);
130195

131-
return (
132-
<div className="flex h-screen">
133-
<Sidebar />
134-
<main className="flex-1 overflow-y-auto">
135-
<Outlet />
136-
</main>
137-
</div>
196+
return () => {
197+
window.removeEventListener('popstate', handlePopState);
198+
};
199+
}, [sendMessage, roomId, isQuizStarted, disconnectRef]);
138200

139-
);
140-
}
201+
return (
202+
<div className='flex h-screen'>
203+
<Sidebar />
204+
<main className='flex-1 overflow-y-auto'>
205+
<Outlet />
206+
</main>
207+
</div>
208+
);
209+
};
141210

142-
export default GameLayout;
211+
export default GameLayout;

0 commit comments

Comments
 (0)