Skip to content

Commit 8cea7e6

Browse files
authored
fix: roomsearch page(landmark get)
1 parent 0302a28 commit 8cea7e6

File tree

20 files changed

+618
-134
lines changed

20 files changed

+618
-134
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,21 @@
1313
"check-all": "yarn check:types && yarn check:format && yarn check:unused"
1414
},
1515
"dependencies": {
16+
"@stomp/stompjs": "^7.2.1",
1617
"axios": "^1.13.2",
1718
"jotai": "^2.16.2",
1819
"react": "^19.1.0",
1920
"react-dom": "^19.1.0",
2021
"react-icons": "^5.5.0",
21-
"react-router-dom": "^7.12.0"
22+
"react-router-dom": "^7.12.0",
23+
"sockjs-client": "^1.6.1"
2224
},
2325
"devDependencies": {
2426
"@biomejs/biome": "1.9.4",
2527
"@types/node": "^22.15.29",
2628
"@types/react": "^19.1.2",
2729
"@types/react-dom": "^19.1.2",
30+
"@types/sockjs-client": "^1.5.4",
2831
"@vitejs/plugin-react-swc": "^3.9.0",
2932
"knip": "^5.59.1",
3033
"typescript": "~5.8.3",

src/App.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { useSetAtom } from 'jotai';
2-
import { useEffect } from 'react';
2+
import { useEffect, useState } from 'react';
33
import { type User, getMe } from './api/auth';
44
import {
55
emailAtom,
66
isLoggedInAtom,
77
nicknameAtom,
88
profileImageAtom,
9+
userIdAtom,
910
} from './common/user';
1011
import Router from './router/Router';
1112

1213
const App = () => {
14+
const [loading, setLoading] = useState(true);
1315
const setIsLoggedIn = useSetAtom(isLoggedInAtom);
16+
const setUserId = useSetAtom(userIdAtom);
1417
const setEmail = useSetAtom(emailAtom);
1518
const setNickname = useSetAtom(nicknameAtom);
1619
const setProfileImage = useSetAtom(profileImageAtom);
@@ -20,15 +23,22 @@ const App = () => {
2023
try {
2124
const user: User = await getMe();
2225
setIsLoggedIn(true);
26+
setUserId(user.id);
2327
setEmail(user.email);
2428
setNickname(user.username);
2529
setProfileImage(user.profileImageUrl);
2630
} catch (_error) {
2731
setIsLoggedIn(false);
32+
} finally {
33+
setLoading(false);
2834
}
2935
};
3036
checkUser();
31-
}, [setIsLoggedIn, setEmail, setNickname, setProfileImage]);
37+
}, [setIsLoggedIn, setUserId, setEmail, setNickname, setProfileImage]);
38+
39+
if (loading) {
40+
return <div>Loading...</div>;
41+
}
3242

3343
return <Router />;
3444
};

src/api/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import apiClient from './index';
22

33
export interface User {
4+
// id: number;
45
email: string;
56
username: string;
67
profileImageUrl: string | null;

src/api/map.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import api from '.';
2+
3+
// eslint-disable-next-line import/prefer-default-export
4+
export const getLandmarks = async () => {
5+
const response = await api.get('/maps/landmarks');
6+
return response.data;
7+
};

src/api/room.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ export interface Pot {
2626
status: 'RECRUITING' | 'WAITING' | 'DEPARTED' | 'COMPLETED' | 'CANCELLED';
2727
}
2828

29+
export interface Message {
30+
id: number;
31+
potId: number;
32+
senderId: number;
33+
text: string;
34+
datetimeSendAt: string;
35+
}
36+
37+
// export interface GetMessagesResponse {
38+
// items: Message[];
39+
// nextCursor: number;
40+
// hasNext: boolean;
41+
// }
42+
2943
export const createRoom = async (
3044
roomDetails: RoomCreationRequest
3145
): Promise<RoomCreationResponse> => {
@@ -41,10 +55,20 @@ export const getCurrentPot = async (): Promise<Pot> => {
4155
return response.data;
4256
};
4357

44-
// export const deleteRoom = async (roomId: number): Promise<void> => {
45-
// await apiClient.delete(`/rooms/${roomId}`);
46-
// };
47-
48-
export const leaveRoom = async (roomId: number): Promise<void> => {
49-
await apiClient.post(`/rooms/${roomId}/leave`);
58+
export const deleteRoom = async (roomId: number): Promise<void> => {
59+
await apiClient.delete(`/rooms/${roomId}`);
5060
};
61+
62+
// export const getMessages = async (
63+
// roomId: number,
64+
// cursor: number,
65+
// size = 20
66+
// ): Promise<GetMessagesResponse> => {
67+
// const response = await apiClient.get<GetMessagesResponse>(
68+
// `/rooms/${roomId}/messages`,
69+
// {
70+
// params: { cursor, size },
71+
// }
72+
// );
73+
// return response.data;
74+
// };

src/api/user.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ interface UsernameUpdateRequest {
66

77
export const updateProfilePicture = async (file: File) => {
88
const formData = new FormData();
9-
formData.append('picture', file);
9+
formData.append('image', file);
1010

1111
const response = await apiClient.post('/user/profile/picture', formData, {
1212
headers: {

src/api/websocket.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Client } from '@stomp/stompjs';
2+
import SockJS from 'sockjs-client';
3+
4+
const WEBSOCKET_URL = 'https://snuxi.com/ws';
5+
6+
export const createStompClient = () => {
7+
const client = new Client({
8+
webSocketFactory: () => new SockJS(WEBSOCKET_URL),
9+
reconnectDelay: 5000,
10+
heartbeatIncoming: 4000,
11+
heartbeatOutgoing: 4000,
12+
});
13+
14+
return client;
15+
};

src/common/user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { atom } from 'jotai';
22

33
export const isLoggedInAtom = atom(false);
4+
export const userIdAtom = atom<number | null>(null);
45
export const emailAtom = atom<string | null>(null);
56
export const nicknameAtom = atom('학부생');
67
export const profileImageAtom = atom<string | null>(null);

src/pages/ChatRoom/ChatRoom.css

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
.chat-room-container {
2+
display: flex;
3+
flex-direction: column;
4+
height: 100vh;
5+
max-width: 600px;
6+
margin: 0 auto;
7+
border: 1px solid #ccc;
8+
}
9+
10+
.messages-container {
11+
flex-grow: 1;
12+
overflow-y: auto;
13+
padding: 1rem;
14+
display: flex;
15+
flex-direction: column-reverse;
16+
}
17+
18+
.message-bubble {
19+
padding: 0.5rem 1rem;
20+
border-radius: 1rem;
21+
margin-bottom: 0.5rem;
22+
max-width: 70%;
23+
}
24+
25+
.my-message {
26+
align-self: flex-end;
27+
background-color: #007bff;
28+
color: white;
29+
}
30+
31+
.other-message {
32+
align-self: flex-start;
33+
background-color: #f1f0f0;
34+
}
35+
36+
.message-text {
37+
margin: 0;
38+
}
39+
40+
.message-time {
41+
font-size: 0.75rem;
42+
color: #888;
43+
display: block;
44+
text-align: right;
45+
margin-top: 0.25rem;
46+
}
47+
48+
.my-message .message-time {
49+
color: #f1f0f0;
50+
}
51+
52+
.message-input-container {
53+
display: flex;
54+
padding: 1rem;
55+
border-top: 1px solid #ccc;
56+
}
57+
58+
.message-input-container input {
59+
flex-grow: 1;
60+
border: 1px solid #ccc;
61+
border-radius: 1rem;
62+
padding: 0.5rem 1rem;
63+
margin-right: 1rem;
64+
}
65+
66+
.message-input-container button {
67+
border: none;
68+
background-color: #007bff;
69+
color: white;
70+
padding: 0.5rem 1rem;
71+
border-radius: 1rem;
72+
cursor: pointer;
73+
}

src/pages/ChatRoom/index.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { useAtom } from 'jotai';
2+
import { useCallback, useEffect, useRef, useState } from 'react';
3+
import { useNavigate, useParams } from 'react-router-dom';
4+
import type { Message } from '../../api/room';
5+
import { getMessages } from '../../api/room';
6+
import { createStompClient } from '../../api/websocket';
7+
import { isLoggedInAtom, userIdAtom } from '../../common/user';
8+
import './ChatRoom.css';
9+
import type { Client } from '@stomp/stompjs';
10+
11+
const ChatRoom = () => {
12+
const { roomId } = useParams<{ roomId: string }>();
13+
const navigate = useNavigate();
14+
const [messages, setMessages] = useState<Message[]>([]);
15+
const [_loading, setLoading] = useState(true);
16+
const [hasNext, setHasNext] = useState(true);
17+
const [cursor, setCursor] = useState(Date.now());
18+
const [isLoggedIn] = useAtom(isLoggedInAtom);
19+
const [userId] = useAtom(userIdAtom);
20+
const [newMessage, setNewMessage] = useState('');
21+
const clientRef = useRef<Client | null>(null);
22+
23+
useEffect(() => {
24+
if (!isLoggedIn) {
25+
alert('로그인이 필요합니다.');
26+
navigate('/');
27+
}
28+
}, [isLoggedIn, navigate]);
29+
30+
const fetchMessages = useCallback(async () => {
31+
if (!roomId || !hasNext) return;
32+
33+
setLoading(true);
34+
try {
35+
const {
36+
items,
37+
nextCursor,
38+
hasNext: newHasNext,
39+
} = await getMessages(parseInt(roomId, 10), cursor);
40+
setMessages((prev) => [...items, ...prev]); // Prepend old messages
41+
setCursor(nextCursor);
42+
setHasNext(newHasNext);
43+
} catch (error) {
44+
console.error('Error fetching messages:', error);
45+
} finally {
46+
setLoading(false);
47+
}
48+
}, [roomId, cursor, hasNext]);
49+
50+
useEffect(() => {
51+
if (isLoggedIn) {
52+
fetchMessages();
53+
}
54+
}, [isLoggedIn, fetchMessages]);
55+
56+
useEffect(() => {
57+
if (!roomId || !userId) return;
58+
59+
const client = createStompClient();
60+
clientRef.current = client;
61+
62+
client.onConnect = () => {
63+
client.subscribe(`/sub/rooms/${roomId}`, (message) => {
64+
const receivedMessage = JSON.parse(message.body);
65+
setMessages((prevMessages) => [receivedMessage, ...prevMessages]);
66+
});
67+
};
68+
69+
client.activate();
70+
71+
return () => {
72+
client.deactivate();
73+
};
74+
}, [roomId, userId]);
75+
76+
const sendMessage = () => {
77+
if (clientRef.current && newMessage.trim() !== '' && roomId && userId) {
78+
const messageToSend = {
79+
text: newMessage,
80+
};
81+
clientRef.current.publish({
82+
destination: `/pub/rooms/${roomId}`,
83+
body: JSON.stringify(messageToSend),
84+
});
85+
setNewMessage('');
86+
}
87+
};
88+
89+
return (
90+
<div className="chat-room-container">
91+
<div className="messages-container">
92+
{messages.map((msg, index) => {
93+
const isMyMessage = msg.senderId === userId;
94+
return (
95+
<div
96+
key={msg.id || `msg-${index}`}
97+
className={`message-bubble ${
98+
isMyMessage ? 'my-message' : 'other-message'
99+
}`}
100+
>
101+
<p className="message-text">{msg.text}</p>
102+
<span className="message-time">
103+
{new Date(msg.datetimeSendAt).toLocaleTimeString()}
104+
</span>
105+
</div>
106+
);
107+
})}
108+
</div>
109+
<div className="message-input-container">
110+
<input
111+
type="text"
112+
placeholder="Type a message..."
113+
value={newMessage}
114+
onChange={(e) => setNewMessage(e.target.value)}
115+
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
116+
/>
117+
<button onClick={sendMessage}>Send</button>
118+
</div>
119+
</div>
120+
);
121+
};
122+
123+
export default ChatRoom;

0 commit comments

Comments
 (0)