Skip to content

Commit 5ad29be

Browse files
committed
feat: 미구현 웹소켓 메시지 추가, 퀴즈 변경 기능 구현, 새로고침시 퍼블리싱 url 분기
1 parent f99c7a5 commit 5ad29be

File tree

8 files changed

+200
-49
lines changed

8 files changed

+200
-49
lines changed

src/hooks/useStompClient.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,22 @@ export default function useStompClient(roomId, onMessage) {
8888
// ✅ 최초 1회 initializeRoomSocket 퍼블리시
8989
useEffect(() => {
9090
if (!isConnected || !stompClient || !roomId || hasInitializedRoomRef.current === true) return;
91-
92-
stompClient.publish({
93-
destination: `/pub/room/initializeRoomSocket/${roomId}`,
94-
body: '',
95-
});
91+
const localKey = `enteredRoom_${roomId}`;
92+
const hasEnteredBefore = localStorage.getItem(localKey);
93+
if (hasEnteredBefore) {
94+
// 재접속 또는 새로고침
95+
stompClient.publish({
96+
destination: `/pub/room/reconnect/${roomId}`,
97+
body: '',
98+
});
99+
} else {
100+
// 첫 입장
101+
stompClient.publish({
102+
destination: `/pub/room/initializeRoomSocket/${roomId}`,
103+
body: '',
104+
});
105+
localStorage.setItem(localKey, 'true'); //방 목록에서 제거됨
106+
}
96107

97108
hasInitializedRoomRef.current = true;
98109
console.log(`🚀 초기화 메시지 전송됨: /pub/room/initializeRoomSocket/${roomId}`);

src/layout/game/GameLayout.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ import {Outlet, useNavigate, useParams} from "react-router-dom";
22
import Sidebar from "./Sidebar";
33
import {useSetRecoilState} from "recoil";
44
import {
5-
chatAtom,
6-
gameSettingAtom, loginUserAtom,
5+
chatAtom, gameResultAtom,
6+
gameSettingAtom,
7+
loginUserAtom,
78
playerListAtom,
8-
roomSettingAtom, stompSendMessageAtom, systemNoticeAtom
9+
questionResultAtom,
10+
questionsAtom,
11+
questionStartAtom,
12+
rankUpdateAtom,
13+
roomSettingAtom,
14+
stompSendMessageAtom,
15+
systemNoticeAtom
916
} from "../../state/atoms";
1017
import useStompClient from "../../hooks/useStompClient";
1118
import {useCallback, useEffect} from "react";
@@ -34,9 +41,15 @@ const GameLayout = () => {
3441
const setRoomSetting = useSetRecoilState(roomSettingAtom);
3542
const setGameSetting = useSetRecoilState(gameSettingAtom);
3643
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);
3749
const setSystemNotice = useSetRecoilState(systemNoticeAtom);
3850
const setSendMessage = useSetRecoilState(stompSendMessageAtom);
3951
const setLoginUser = useSetRecoilState(loginUserAtom);
52+
const navigate = useNavigate();
4053

4154
const { isLoading, data } = useApiQuery(
4255
["authme"],
@@ -51,6 +64,7 @@ const GameLayout = () => {
5164

5265
// 메시지를 처리하는 콜백
5366
const handleStompMessage = useCallback((payload) => {
67+
console.log('receive message: ', payload)
5468
switch (payload.type) {
5569
case "PLAYER_LIST":
5670
const { host, players } = payload.message;
@@ -81,6 +95,24 @@ const GameLayout = () => {
8195
case "CHAT":
8296
setChat(payload.message);
8397
break;
98+
case "GAME_START":
99+
setQuestions(payload.message);
100+
break;
101+
case "QUESTION_START":
102+
setQuestionStart(payload.message);
103+
break;
104+
case "QUESTION_RESULT":
105+
setQuestionResult(payload.message);
106+
break;
107+
case "RANK_UPDATE":
108+
setRankUpdate(payload.message.rank);
109+
break;
110+
case "GAME_RESULT":
111+
setGameResult(payload.message.result);
112+
break;
113+
case "EXIT_SUCCESS":
114+
navigate("/room");
115+
break;
84116
default:
85117
console.warn("알 수 없는 메시지", payload);
86118
}

src/layout/game/components/GameSettings.js

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { useState } from "react"
2-
import { useNavigate } from "react-router-dom"
3-
import { Settings } from "lucide-react"
1+
import {useState} from "react"
2+
import {useNavigate} from "react-router-dom"
3+
import {Settings} from "lucide-react"
44
import QuizSelectModal from "../../../pages/game/QuizSelectModal";
55
import {useRecoilValue} from "recoil";
66
import {stompSendMessageAtom} from "../../../state/atoms";
77

8-
function GameSettings({ roomId }) {
9-
const [timePerQuestion, setTimePerQuestion] = useState("30초");
10-
const [questionCount, setQuestionCount] = useState(25);
8+
function GameSettings({ roomId, gameSetting, allReady }) {
9+
const [timePerQuestion, setTimePerQuestion] = useState(60);
10+
const [questionCount, setQuestionCount] = useState(gameSetting?.quiz.numberOfQuestion);
1111
const [quizSelectModalOpen, setQuizSelectModalOpen] = useState(false);
12-
const [selectedQuiz, setSelectedQuiz] = useState(null);
1312
const navigate = useNavigate();
1413
const sendMessage = useRecoilValue(stompSendMessageAtom);
1514

@@ -19,9 +18,36 @@ function GameSettings({ roomId }) {
1918
}
2019

2120
const handleQuizSelect = (quiz) => {
22-
setSelectedQuiz(quiz);
21+
const quizChangeMessage = {
22+
"message" : {
23+
"quizId" : quiz.quizId,
24+
}
25+
}
26+
sendMessage(`/room/quiz/${roomId}`, quizChangeMessage);
2327
}
2428

29+
const handleTimePerQuestionChange = (e) => {
30+
console.log('handleTimePerQuestionChange: ', e.target.value)
31+
setTimePerQuestion(e.target.value);
32+
const timePerQuestionMessage = {
33+
"message" : {
34+
"timeLimit" : e.target.value
35+
}
36+
}
37+
sendMessage(`/room/time-limit/${roomId}`, timePerQuestionMessage);
38+
};
39+
40+
const handleQuestionCountChange = (e) => {
41+
console.log('handleQuestionCountChange: ', e.target.value)
42+
setQuestionCount(e.target.value);
43+
const questionCountMessage = {
44+
"message" : {
45+
"round" : e.target.value
46+
}
47+
}
48+
sendMessage(`/room/round/${roomId}`, questionCountMessage);
49+
};
50+
2551
return (
2652
<>
2753
<div className="bg-white rounded-2xl shadow-lg p-6 mb-6">
@@ -44,38 +70,47 @@ function GameSettings({ roomId }) {
4470
<label className="block text-sm font-medium text-gray-700 mb-2">제한 시간</label>
4571
<select
4672
value={timePerQuestion}
47-
onChange={(e) => setTimePerQuestion(e.target.value)}
73+
onChange={handleTimePerQuestionChange}
4874
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 appearance-none bg-white bg-no-repeat bg-right pr-8"
4975
style={{
5076
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' strokeLinecap='round' strokeLinejoin='round' strokeWidth='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e")`,
5177
backgroundPosition: "right 0.5rem center",
5278
backgroundSize: "1.5em 1.5em",
5379
}}
5480
>
55-
<option>15초</option>
56-
<option>30초</option>
57-
<option>45초</option>
58-
<option>60초</option>
81+
<option value={15}>15초</option>
82+
<option value={30}>30초</option>
83+
<option value={45}>45초</option>
84+
<option value={60}>60초</option>
5985
</select>
6086
</div>
6187
<div>
62-
<label className="block text-sm font-medium text-gray-700 mb-2">문제 수</label>
63-
<input
64-
type="number"
65-
min="10"
66-
max="80"
88+
<label className="block text-sm font-medium text-gray-700 mb-2">라운드</label>
89+
<select
6790
value={questionCount}
68-
onChange={(e) => setQuestionCount(Number.parseInt(e.target.value) || 30)}
69-
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
70-
placeholder="문제 수를 입력하세요 (10-80)"
71-
/>
91+
onChange={handleQuestionCountChange}
92+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 appearance-none bg-white bg-no-repeat bg-right pr-8"
93+
style={{
94+
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' strokeLinecap='round' strokeLinejoin='round' strokeWidth='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e")`,
95+
backgroundPosition: "right 0.5rem center",
96+
backgroundSize: "1.5em 1.5em",
97+
}}
98+
>
99+
{Array.from({ length: gameSetting?.quiz.numberOfQuestion - 9 }, (_, i) => {
100+
const value = i + 10;
101+
return (
102+
<option key={value} value={value}>
103+
{value}
104+
</option>
105+
);
106+
})}
107+
</select>
72108
</div>
73109
</div>
74110
<div className="pt-4">
75-
<button
76-
onClick={handleStartGame}
77-
disabled={!selectedQuiz}
78-
className="w-full px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-semibold disabled:bg-gray-400 disabled:cursor-not-allowed"
111+
<button onClick={handleStartGame}
112+
disabled={!allReady}
113+
className="w-full px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-semibold disabled:bg-gray-400 disabled:cursor-not-allowed"
79114
>
80115
게임 시작
81116
</button>

src/layout/game/components/QuizInfoCard.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { MessageCircleQuestion, Clock, List } from "lucide-react"
22
import {useRecoilValue} from "recoil";
33
import {gameSettingAtom} from "../../../state/atoms";
44

5-
function QuizInfoCard() {
6-
const gameSetting = useRecoilValue(gameSettingAtom);
5+
function QuizInfoCard({ gameSetting }) {
76
return (
87
<div className="bg-white rounded-2xl shadow-lg p-8 mb-6">
98
<div className="flex items-center mb-6">
@@ -49,9 +48,9 @@ function QuizInfoCard() {
4948
<div className="bg-purple-50 rounded-lg p-3">
5049
<div className="flex items-center space-x-2">
5150
<List className="w-4 h-4 text-purple-600" />
52-
<span className="text-sm font-medium text-purple-900">문제 수</span>
51+
<span className="text-sm font-medium text-purple-900">라운드</span>
5352
</div>
54-
<div className="text-lg font-bold text-purple-700">30개</div>
53+
<div className="text-lg font-bold text-purple-700">{gameSetting?.round}</div>
5554
</div>
5655
</div>
5756
</div>

src/pages/game/HostPage.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,66 @@ import {useNavigate, useParams} from "react-router-dom"
22
import QuizInfoCard from "../../layout/game/components/QuizInfoCard"
33
import GameSettings from "../../layout/game/components/GameSettings"
44
import {useRecoilValue} from "recoil";
5-
import {stompSendMessageAtom} from "../../state/atoms";
5+
import {
6+
gameSettingAtom,
7+
loginUserAtom,
8+
playerListAtom,
9+
roomSettingAtom,
10+
stompSendMessageAtom
11+
} from "../../state/atoms";
612
import HostPageHeader from "./HostPageHeader";
13+
import {useEffect, useState} from "react";
14+
import clsx from "clsx";
715

816
function HostPage() {
917
const { id: roomId } = useParams();
1018
const navigate = useNavigate();
19+
const [isReady, setReady] = useState(false);
1120
const sendMessage = useRecoilValue(stompSendMessageAtom);
21+
const gameSetting = useRecoilValue(gameSettingAtom);
22+
console.log(gameSetting);
23+
24+
//현재 로그인 유저를 찾고 방장인지 확인
25+
const playerList = useRecoilValue(playerListAtom);
26+
const loginUser = useRecoilValue(loginUserAtom);
27+
const matchingPlayers = playerList.filter(player => player.nickname === loginUser.name);
28+
const isHost = matchingPlayers.some(player => player.status === "host");
29+
const allReady = playerList.every(player => player.ready);
30+
31+
useEffect(() => {
32+
if (playerList) {
33+
setReady(matchingPlayers.ready);
34+
}
35+
}, [playerList])
1236

1337
const handleExitRoomClick = () => {
1438
sendMessage(`/pub/room/exit/${roomId}`, "");
15-
navigate("/room");
1639
};
1740

41+
const handleReadyGame = () => {
42+
// setReady(prev => !prev);
43+
sendMessage(`/pub/room/ready/${roomId}`, "");
44+
}
45+
1846
return (
1947
<div className="flex flex-col h-full" style={{ height: "90vh" }}>
20-
<HostPageHeader handleExitRoomClick={handleExitRoomClick}/>
48+
<HostPageHeader isHost={isHost} handleExitRoomClick={handleExitRoomClick}/>
2149
{/* Body */}
2250
<div className="flex-1 p-8">
2351
<div className="max-w-6xl mx-auto">
24-
<QuizInfoCard />
25-
<GameSettings isHost={true} roomId={roomId} />
52+
<QuizInfoCard gameSetting={gameSetting} />
53+
{isHost ? <GameSettings roomId={roomId} gameSetting={gameSetting} allReady={allReady}/> :
54+
<button onClick={handleReadyGame}
55+
className={clsx(
56+
"w-full px-6 py-3 text-white rounded-lg transition-colors font-semibold disabled:bg-gray-400 disabled:cursor-not-allowed",
57+
{
58+
"bg-green-600 hover:bg-green-700": !isReady,
59+
"bg-red-600 hover:bg-red-700": isReady
60+
}
61+
)}>
62+
{isReady ? '준비 완료 상태입니다.다시 클릭하면 준비 해제됩니다.' : '준비'}
63+
</button>
64+
}
2665
</div>
2766
</div>
2867
</div>

src/pages/game/HostPageHeader.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Flag, Lock, LogOut, Users} from "lucide-react";
22
import {useRecoilValue} from "recoil";
33
import {roomSettingAtom} from "../../state/atoms";
44

5-
const HostPageHeader = ({ handleExitRoomClick }) => {
5+
const HostPageHeader = ({ isHost, handleExitRoomClick }) => {
66
const roomSetting = useRecoilValue(roomSettingAtom);
77

88
return (
@@ -13,19 +13,20 @@ const HostPageHeader = ({ handleExitRoomClick }) => {
1313
<Flag className="w-6 h-6" />
1414
<h1 className="text-xl font-bold">{roomSetting?.roomName}</h1>
1515
</div>
16-
<div className="flex items-center space-x-2 bg-white/20 px-3 py-1 rounded-full">
17-
<Lock className="w-4 h-4" />
16+
{roomSetting?.locked && <div
17+
className="flex items-center space-x-2 bg-white/20 px-3 py-1 rounded-full">
18+
<Lock className="w-4 h-4"/>
1819
<span className="text-sm">비공개 방</span>
19-
</div>
20+
</div>}
2021
</div>
2122
<div className="flex items-center space-x-4">
2223
<div className="flex items-center space-x-2">
2324
<Users className="w-5 h-5" />
2425
<span className="font-semibold">{roomSetting?.currentUserCount}/{roomSetting?.maxUserCount} 플레이어</span>
2526
</div>
26-
<div className="bg-yellow-500/20 px-3 py-1 rounded-full">
27+
{isHost && <div className="bg-yellow-500/20 px-3 py-1 rounded-full">
2728
<span className="text-sm font-medium">👑 방장</span>
28-
</div>
29+
</div>}
2930
<button className="px-4 py-2 bg-white/20 text-white rounded-lg hover:bg-white/30 transition-colors"
3031
onClick={handleExitRoomClick}>
3132
<LogOut className="w-4 h-4 mr-2 inline" />방 나가기

src/pages/room/RoomList.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ const RoomList = () => {
6767
}
6868
}, [data])
6969

70+
useEffect(() => {
71+
// prefix로 저장된 방 관련 키들 삭제
72+
Object.keys(localStorage).forEach((key) => {
73+
if (key.startsWith("enteredRoom_")) {
74+
localStorage.removeItem(key);
75+
}
76+
});
77+
}, []);
78+
7079
const handleSearch = () => {
7180
const filtered = rooms.filter((room) => room.roomName.toLowerCase().includes(searchTerm.toLowerCase()));
7281
setFilteredRooms(filtered);

src/state/atoms.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,28 @@ export const systemNoticeAtom = atom({
5656
key: 'systemNotice',
5757
default: null,
5858
});
59+
60+
export const questionsAtom = atom({
61+
key: 'questions',
62+
default: null,
63+
});
64+
65+
export const questionStartAtom = atom({
66+
key: 'questionStart',
67+
default: null,
68+
});
69+
70+
export const questionResultAtom = atom({
71+
key: 'questionResult',
72+
default: null,
73+
});
74+
75+
export const rankUpdateAtom = atom({
76+
key: 'rankUpdate',
77+
default: null,
78+
});
79+
80+
export const gameResultAtom = atom({
81+
key: 'gameResult',
82+
default: null,
83+
});

0 commit comments

Comments
 (0)