Skip to content

Commit e542d8b

Browse files
committed
.env파일 추가, 관리자 페이지 추가, sse통신을 위한 hook추가
1 parent 65e508b commit e542d8b

File tree

11 files changed

+216
-16
lines changed

11 files changed

+216
-16
lines changed

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
REACT_APP_API_BASE_URL=https://api-brainrace.duckdns.org
2+
REACT_APP_WS_BASE_URL=wss://api-brainrace.duckdns.org

src/hooks/useRoomSseClient.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
3+
let eventSource = null;
4+
5+
export default function useRoomSseClient(onRoomEvent) {
6+
const onRoomEventRef = useRef(onRoomEvent);
7+
const [connected, setConnected] = useState(false);
8+
9+
// 최신 콜백 유지
10+
useEffect(() => {
11+
onRoomEventRef.current = onRoomEvent;
12+
}, [onRoomEvent]);
13+
14+
useEffect(() => {
15+
if (eventSource) return;
16+
17+
eventSource = new EventSource(`${process.env.REACT_APP_API_BASE_URL}/sub/room.list`);
18+
19+
eventSource.onopen = () => {
20+
console.log('✅ SSE 연결됨');
21+
setConnected(true);
22+
};
23+
24+
eventSource.onmessage = (event) => {
25+
try {
26+
const payload = JSON.parse(event.data);
27+
onRoomEventRef.current?.(payload); // 예: { type: 'ROOM_CREATED', data: { ... } }
28+
} catch (e) {
29+
console.error('❌ SSE 메시지 파싱 실패', e);
30+
}
31+
};
32+
33+
eventSource.onerror = (err) => {
34+
console.error('❌ SSE 연결 오류:', err);
35+
eventSource.close();
36+
eventSource = null;
37+
setConnected(false);
38+
};
39+
40+
return () => {
41+
eventSource?.close();
42+
eventSource = null;
43+
setConnected(false);
44+
};
45+
}, []);
46+
47+
return {
48+
connected,
49+
};
50+
}

src/hooks/useStompClient.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function useStompClient(roomId, onMessage) {
2525
isConnecting = true;
2626

2727
stompClient = new Client({
28-
brokerURL: 'wss://brainrace.duckdns.org:7080/ws/game-room',
28+
brokerURL: `${process.env.REACT_APP_WS_BASE_URL}/ws/game-room`,
2929
reconnectDelay: 5000,
3030
// debug: (msg) => console.log('[STOMP]', msg),
3131
onConnect: () => {

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {RecoilRoot} from "recoil";
77
import {QueryClient, QueryClientProvider} from "react-query";
88
import axios from "axios";
99

10-
axios.defaults.baseURL = process.env.REACT_APP_API_URL || "https://brainrace.duckdns.org:7080";
10+
axios.defaults.baseURL = process.env.REACT_APP_API_BASE_URL || "https://api-brainrace.duckdns.org";
1111
// react-qeury사용을 위해 선언
1212
const queryClient = new QueryClient({
1313
defaultOptions: {

src/layout/main/Layout.js

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,17 @@ const Layout = () => {
185185
Quiz
186186
</NavLink>
187187

188-
<NavLink
189-
to="/room"
188+
{data && data.role === 'ROLE_USER' && <NavLink
189+
to="/room"
190190
style={({isActive}) => ({
191191
...f1Styles.navLink,
192-
...(isActive ? f1Styles.navLinkActive : {})
192+
...(isActive ? f1Styles.navLinkActive
193+
: {})
193194
})}
194195
onMouseEnter={(e) => {
195196
if (!isActive('/room')) {
196-
Object.assign(e.target.style, f1Styles.navLinkHover);
197+
Object.assign(e.target.style,
198+
f1Styles.navLinkHover);
197199
}
198200
}}
199201
onMouseLeave={(e) => {
@@ -204,7 +206,7 @@ const Layout = () => {
204206
}}
205207
>
206208
게임하기
207-
</NavLink>
209+
</NavLink>}
208210

209211
<NavLink
210212
to="/rank"
@@ -227,15 +229,17 @@ const Layout = () => {
227229
전체 랭킹
228230
</NavLink>
229231

230-
<NavLink
231-
to="/mypage"
232+
{data && data.role === 'ROLE_USER' &&<NavLink
233+
to="/mypage"
232234
style={({isActive}) => ({
233235
...f1Styles.navLink,
234-
...(isActive ? f1Styles.navLinkActive : {})
236+
...(isActive ? f1Styles.navLinkActive
237+
: {})
235238
})}
236239
onMouseEnter={(e) => {
237240
if (!isActive('/mypage')) {
238-
Object.assign(e.target.style, f1Styles.navLinkHover);
241+
Object.assign(e.target.style,
242+
f1Styles.navLinkHover);
239243
}
240244
}}
241245
onMouseLeave={(e) => {
@@ -246,7 +250,30 @@ const Layout = () => {
246250
}}
247251
>
248252
마이페이지
249-
</NavLink>
253+
</NavLink>}
254+
255+
{data && data.role === 'ROLE_ADMIN' &&<NavLink
256+
to="/users"
257+
style={({isActive}) => ({
258+
...f1Styles.navLink,
259+
...(isActive ? f1Styles.navLinkActive
260+
: {})
261+
})}
262+
onMouseEnter={(e) => {
263+
if (!isActive('/users')) {
264+
Object.assign(e.target.style,
265+
f1Styles.navLinkHover);
266+
}
267+
}}
268+
onMouseLeave={(e) => {
269+
if (!isActive('/users')) {
270+
e.target.style.backgroundColor = 'transparent';
271+
e.target.style.transform = 'translateY(0)';
272+
}
273+
}}
274+
>
275+
관리자 메뉴
276+
</NavLink>}
250277

251278
<button
252279
style={f1Styles.logoutButton}

src/pages/admin/UserList.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {useEffect, useState} from "react";
2+
import PaginationNavigator from "../../layout/PaginationNavigator";
3+
import { X } from "lucide-react"
4+
import styles from "../rank/rank.module.scss";
5+
import FlexibleTable from "../../shared/table/FlexibleTable";
6+
import TableBackGroundCard from "../../shared/TableBackGroundCard";
7+
import {useApiQuery} from "../../hooks/useApiQuery";
8+
import {useQueryParam} from "../../hooks/QueryParam";
9+
import axios from "axios";
10+
11+
const initColumns = [
12+
{ accessorKey: "id", header: "순번" },
13+
{ accessorKey: "nickname", header: "닉네임" },
14+
{ accessorKey: "lastLogin", header: "최근 로그인" },
15+
{ accessorKey: "createdAt", header: "가입 날짜" },
16+
];
17+
18+
const usersRequest = async (params) => {
19+
const response = await axios.get(`/admin/users`, {params});
20+
return response.data;
21+
};
22+
23+
const UserList = () => {
24+
const [searchTerm, setSearchTerm] = useState("");
25+
const [params, setParams] = useQueryParam();
26+
const [tableRows, setTableRows] = useState([]);
27+
const { data } = useApiQuery(
28+
['/admin/users', params], // queryKey에 params 포함
29+
() => usersRequest(params)
30+
);
31+
32+
useEffect(() => {
33+
if (data) {
34+
setTableRows(data.users);
35+
}
36+
}, [data])
37+
38+
const handleSearch = (e) => {
39+
e.preventDefault();
40+
console.log("검색어:", searchTerm)
41+
setParams({
42+
...params,
43+
page: 1,
44+
nickname: searchTerm.trim()
45+
})
46+
}
47+
48+
const handleClearSearch = () => {
49+
setParams({
50+
...params,
51+
nickname: ''
52+
})
53+
setSearchTerm("")
54+
}
55+
56+
return (
57+
<div className="min-h-screen bg-gray-100 p-8">
58+
<div className="max-w-6xl mx-auto bg-white rounded-lg shadow-md border border-gray-200">
59+
{/* 헤더 및 검색 섹션 */}
60+
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
61+
<h1 className="text-xl font-bold text-gray-900">[ 유저 관리 페이지 ]</h1>
62+
<form className="flex items-center space-x-2" onSubmit={handleSearch}>
63+
<div className="relative w-64">
64+
<input
65+
type="text"
66+
placeholder="유저 검색 ..."
67+
value={searchTerm}
68+
onChange={(e) => setSearchTerm(e.target.value)}
69+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-gray-900"
70+
/>
71+
{searchTerm && (
72+
<button
73+
type="button"
74+
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0 text-gray-500 hover:bg-gray-100 rounded-md flex items-center justify-center"
75+
onClick={handleClearSearch}
76+
>
77+
<X className="h-4 w-4" />
78+
</button>
79+
)}
80+
</div>
81+
<button
82+
type="submit"
83+
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-gray-800 text-white hover:bg-gray-900 h-10 px-4 py-2"
84+
>
85+
검색
86+
</button>
87+
</form>
88+
</div>
89+
<TableBackGroundCard>
90+
<FlexibleTable initColumns={initColumns} data={tableRows} />
91+
{/* Pagination */}
92+
<PaginationNavigator currentPage={data?.currentPage} totalPages={data?.totalPages}
93+
onPageChange={(page) => setParams({
94+
...params
95+
,page: page,
96+
})}/>
97+
</TableBackGroundCard>
98+
</div>
99+
</div>
100+
);
101+
}
102+
103+
export default UserList;

src/pages/game/HostPageHeader.js

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

55
const HostPageHeader = ({ isHost, handleExitRoomClick }) => {
66
const roomSetting = useRecoilValue(roomSettingAtom);
7+
const playerList = useRecoilValue(playerListAtom);
78

89
return (
910
<header className="bg-gradient-to-r from-red-600 to-red-500 text-white py-4 px-6 shadow-lg">
@@ -22,7 +23,7 @@ const HostPageHeader = ({ isHost, handleExitRoomClick }) => {
2223
<div className="flex items-center space-x-4">
2324
<div className="flex items-center space-x-2">
2425
<Users className="w-5 h-5" />
25-
<span className="font-semibold">{roomSetting?.currentUserCount}/{roomSetting?.maxUserCount} 플레이어</span>
26+
<span className="font-semibold">{playerList?.length}/{roomSetting?.maxUserCount} 플레이어</span>
2627
</div>
2728
{isHost && <div className="bg-yellow-500/20 px-3 py-1 rounded-full">
2829
<span className="text-sm font-medium">👑 방장</span>

src/pages/login/Login.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const Login = () => {
1313
</div>
1414
<button className={styles.loginButton}
1515
onClick={() => {
16-
window.location.href = "https://brainrace.duckdns.org:7080/oauth2/authorization/kakao";
16+
window.location.href = `${process.env.REACT_APP_API_BASE_URL}/oauth2/authorization/kakao`;
1717
}}>
1818
뇌피셜 입장하기
1919
</button>

src/pages/rank/Rank.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const Rank = () => {
2929
['/stats/rankings', params], // queryKey에 params 포함
3030
() => rankRequest(params)
3131
);
32-
console.log(data);
3332

3433
useEffect(() => {
3534
if (data) {

src/pages/room/RoomList.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {useApiQuery} from "../../hooks/useApiQuery";
1111
import axios from "axios";
1212
import {useApiMutation} from "../../hooks/useApiMutation";
1313
import {useNavigate} from "react-router-dom";
14+
import useRoomSseClient from "../../hooks/useRoomSseClient";
1415

1516
const roomsRequest = async () => {
1617
const response = await axios.get(`/rooms`);
@@ -45,6 +46,19 @@ const RoomList = () => {
4546
() => roomsRequest(),
4647
);
4748

49+
useRoomSseClient((event) => {
50+
const { type, payload } = event;
51+
console.log(type, payload)
52+
setRooms((prev) => {
53+
switch (type) {
54+
case 'CREATE':
55+
case 'UPDATE':
56+
case 'DELETE':
57+
default:
58+
}
59+
});
60+
});
61+
4862
const { mutate: createRoomMutate } = useApiMutation(createRoomRequest, {
4963
onSuccess: (data) => {
5064
navigate(`/room/${data.roomId}`);

0 commit comments

Comments
 (0)