Skip to content

Commit e1cae40

Browse files
committed
feat: 방 세팅, 방 채팅, 방 입장, 방 퇴장, 플레이어 리스트 구현
1 parent f678253 commit e1cae40

File tree

13 files changed

+375
-87
lines changed

13 files changed

+375
-87
lines changed

package-lock.json

Lines changed: 77 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@fortawesome/free-solid-svg-icons": "^6.7.2",
1414
"@fortawesome/react-fontawesome": "^0.2.2",
1515
"@hello-pangea/dnd": "^17.0.0",
16+
"@stomp/stompjs": "^7.1.1",
1617
"@tanstack/react-table": "^8.21.2",
1718
"@tosspayments/payment-sdk": "^1.9.1",
1819
"apexcharts": "^4.3.0",
@@ -52,6 +53,7 @@
5253
"recoil": "^0.7.7",
5354
"sass": "^1.81.0",
5455
"showdown": "^2.1.0",
56+
"sockjs-client": "^1.6.1",
5557
"styled-components": "^6.1.11",
5658
"swiper": "^11.2.1",
5759
"yet-another-react-lightbox": "^3.23.1"

server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const express = require('express');
22
const path = require('path');
33

44
const app = express();
5-
const PORT = process.env.PORT || 8080;
5+
const PORT = process.env.PORT || 3000;
66

77
// 정적 파일 제공 (build 디렉토리)
88
app.use(express.static(path.join(__dirname, 'build')));

src/hooks/useStompClient.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useEffect, useRef } from "react";
2+
import { Client } from "@stomp/stompjs";
3+
4+
export default function useStompClient(roomId, onMessage) {
5+
const stompClientRef = useRef(null);
6+
const hasSubscribedRef = useRef(false);
7+
useEffect(() => {
8+
const stompClient = new Client({
9+
brokerURL: `wss://brainrace.duckdns.org:7080/ws/game-room`,
10+
reconnectDelay: 5000,
11+
onConnect: () => {
12+
console.log("🔌 STOMP connected");
13+
// 구독
14+
if (!hasSubscribedRef.current) {
15+
hasSubscribedRef.current = true;
16+
stompClient.subscribe(`/sub/room/${roomId}`, (message) => {
17+
const payload = JSON.parse(message.body);
18+
console.log("📥 메시지 수신:", payload);
19+
20+
// 👉 외부에서 전달한 콜백 실행
21+
if (onMessage) onMessage(payload);
22+
});
23+
24+
// ✅ 자동으로 방 입장 메시지 퍼블리시
25+
stompClient.publish({
26+
destination: `/pub/room/initializeRoomSocket/${roomId}`,
27+
body: "", // 빈 body 명시적 전달
28+
});
29+
} //if (!hasSubscribedRef.current) { end
30+
}, //onConnect end
31+
onStompError: (frame) => {
32+
console.error("❌ STOMP error", frame);
33+
},
34+
});
35+
36+
stompClient.activate();
37+
stompClientRef.current = stompClient;
38+
39+
return () => {
40+
if (stompClientRef.current?.connected) {
41+
stompClientRef.current.deactivate();
42+
console.log("🔌 STOMP disconnected");
43+
}
44+
hasSubscribedRef.current = false;
45+
};
46+
}, [roomId, onMessage]);
47+
48+
// ✅ 메시지 전송 함수 반환
49+
const sendMessage = (destination, body) => {
50+
console.log("sendMessage: ", destination, body, stompClientRef.current?.connected)
51+
if (stompClientRef.current?.connected) {
52+
stompClientRef.current.publish({
53+
destination,
54+
body,
55+
});
56+
} else {
57+
console.warn("STOMP 연결되지 않음");
58+
}
59+
};
60+
61+
return {sendMessage};
62+
}

src/layout/game/GameLayout.js

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,84 @@
1-
import {Outlet} from "react-router-dom";
1+
import {Outlet, useNavigate, useParams} from "react-router-dom";
22
import Sidebar from "./Sidebar";
3+
import {useSetRecoilState} from "recoil";
4+
import {
5+
chatAtom,
6+
gameSettingAtom,
7+
playerListAtom,
8+
roomSettingAtom, stompSendMessageAtom, systemNoticeAtom
9+
} from "../../state/atoms";
10+
import useStompClient from "../../hooks/useStompClient";
11+
import {useCallback, useEffect} from "react";
12+
13+
const PLAYER_COLORS = [
14+
"text-red-600",
15+
"text-blue-600",
16+
"text-red-500",
17+
"text-orange-500",
18+
"text-cyan-500",
19+
"text-green-600",
20+
"text-purple-600",
21+
"text-pink-600",
22+
];
323

424
const GameLayout = () => {
25+
const { id: roomId } = useParams();
26+
const setPlayerList = useSetRecoilState(playerListAtom);
27+
const setRoomSetting = useSetRecoilState(roomSettingAtom);
28+
const setGameSetting = useSetRecoilState(gameSettingAtom);
29+
const setChat = useSetRecoilState(chatAtom);
30+
const setSystemNotice = useSetRecoilState(systemNoticeAtom);
31+
const setSendMessage = useSetRecoilState(stompSendMessageAtom);
32+
const navigate = useNavigate();
33+
34+
// 메시지를 처리하는 콜백
35+
const handleStompMessage = useCallback((payload) => {
36+
switch (payload.type) {
37+
case "PLAYER_LIST":
38+
const { host, players } = payload.message;
39+
const processedPlayers = players.map((player, index) => {
40+
let status = "waiting";
41+
if (player.nickname === host) {
42+
status = "host";
43+
} else if (player.ready) {
44+
status = "ready";
45+
}
46+
return {
47+
...player,
48+
status,
49+
color: PLAYER_COLORS[index] || "text-gray-500",
50+
};
51+
});
52+
setPlayerList(processedPlayers);
53+
break;
54+
case "ROOM_SETTING":
55+
setRoomSetting(payload.message);
56+
break;
57+
case "GAME_SETTING":
58+
setGameSetting(payload.message);
59+
break;
60+
case "SYSTEM_NOTICE":
61+
setSystemNotice(payload.message);
62+
break;
63+
case "CHAT":
64+
setChat(payload.message);
65+
break;
66+
default:
67+
console.warn("알 수 없는 메시지", payload);
68+
}
69+
}, [setPlayerList, setRoomSetting, setGameSetting, setChat]);
70+
71+
const { sendMessage } = useStompClient(roomId, handleStompMessage);
72+
useEffect(() => {
73+
setSendMessage(() => sendMessage);
74+
}, [sendMessage]);
75+
76+
useEffect(() => {
77+
if (sendMessage) {
78+
sendMessage(`/pub/room/initializeRoomSocket/${roomId}`, "");
79+
}
80+
}, [sendMessage, roomId]);
81+
582
return (
683
<div className="d-flex flex-column vh-100">
784
<div className="d-flex flex-grow-1">

src/layout/game/Sidebar.js

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,16 @@ const leaderboardData = [
1212
{ rank: 6, name: "Fernando Alonso", score: 7, color: "text-green-600" },
1313
{ rank: 7, name: "Oscar Piastri", score: 6, color: "text-purple-600" },
1414
{ rank: 8, name: "Carlos Sainz", score: 5, color: "text-pink-600" },
15-
]
16-
17-
const participants = [
18-
{ id: "1", name: "Lewis Hamilton", status: "host", color: "text-red-600" },
19-
{ id: "2", name: "Max Verstappen", status: "ready", color: "text-blue-600" },
20-
{ id: "3", name: "Charles Leclerc", status: "ready", color: "text-red-500" },
21-
{ id: "4", name: "Lando Norris", status: "waiting", color: "text-orange-500" },
22-
{ id: "5", name: "George Russell", status: "ready", color: "text-cyan-500" },
23-
{ id: "6", name: "Fernando Alonso", status: "ready", color: "text-green-600" },
24-
{ id: "7", name: "Oscar Piastri", status: "waiting", color: "text-purple-600" },
25-
{ id: "8", name: "Carlos Sainz", status: "ready", color: "text-pink-600" },
26-
]
27-
28-
const chatMessages = [
29-
{ id: "1", user: "Lewis Hamilton", message: "Great question! 🏎️", color: "text-red-600" },
30-
{ id: "2", user: "Max Verstappen", message: "This is getting intense!", color: "text-blue-600" },
31-
{ id: "3", user: "Charles Leclerc", message: "Close race for first place", color: "text-red-500" },
32-
{ id: "4", user: "Lando Norris", message: "Good luck everyone! 🏁", color: "text-orange-500" },
33-
]
15+
];
3416

3517
function Sidebar() {
36-
const location = useLocation()
37-
const isGamePlay = location.pathname.includes("/play")
18+
const location = useLocation();
19+
const isGamePlay = location.pathname.includes("/play");
3820

3921
return (
4022
<aside className="w-96 bg-white border-r border-gray-200 flex flex-col">
41-
{isGamePlay ? <LiveLeaderboard entries={leaderboardData} /> : <ParticipantsList participants={participants} />}
42-
<ChatSection messages={chatMessages} />
23+
{isGamePlay ? <LiveLeaderboard entries={leaderboardData} /> : <ParticipantsList />}
24+
<ChatSection />
4325
</aside>
4426
)
4527
}

0 commit comments

Comments
 (0)