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' ;
44import {
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
2224const 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
3335const 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
3840const 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