Skip to content

Commit 53d603c

Browse files
committed
(#261) 채팅방식 웹소켓으로 변경
1 parent 47d2cf4 commit 53d603c

File tree

3 files changed

+121
-129
lines changed

3 files changed

+121
-129
lines changed

src/app/(main)/chat/room/page.tsx

Lines changed: 105 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@ import { useAuth } from 'react-oidc-context'
77
import Header from '@/components/commons/header'
88
import KebabIcon from '@/assets/icon_kebab.svg'
99
import { isSameMinute } from '@/utils/dataFormatting'
10-
import {
11-
useReadChatMessages,
12-
useDeleteChatRoom,
13-
useGetChatTicket,
14-
} from '@/hooks/api/useChatApi'
10+
import { useReadChatMessages, useDeleteChatRoom, useGetChatTicket } from '@/hooks/api/useChatApi'
1511
import { useChatSocket } from '@/hooks/useChatSocket'
1612
import type { ChatContent, SocketChatContent } from '@/types/chat'
13+
import type { ChatSocketResponse } from '@/types/chat'
1714
import { logAnalyticsEvent } from '@/lib/firebase/analytics'
1815

1916
import KebabModal from './_components/kebabModal'
@@ -25,115 +22,147 @@ import ChatInput from './_components/chatInput'
2522
import styles from './room.module.css'
2623

2724
export default function ChatRoom() {
28-
// Hooks
2925
const { user } = useAuth()
3026
const router = useRouter()
3127
const searchParams = useSearchParams()
3228

33-
// URL 파라미터에서 채팅방 정보 추출
3429
const chatRoomId = Number(searchParams.get('chatRoomId'))
3530
const nickName = searchParams.get('nickName')
3631
const memberId = searchParams.get('memberId')
3732

38-
// State
3933
const [isMenuOpen, setIsMenuOpen] = useState(false)
4034
const [visibleDate, setVisibleDate] = useState<string | null>(null)
4135
const [isDateVisible, setIsDateVisible] = useState(false)
4236
const [ticket, setTicket] = useState<string | null>(null)
37+
const [isOtherUserLeft, setIsOtherUserLeft] = useState(false)
38+
const [isBlocked, setIsBlocked] = useState(false)
4339

44-
// Refs
4540
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null)
4641
const messageContainerRef = useRef<HTMLDivElement>(null)
4742
const messageEndRef = useRef<HTMLDivElement>(null)
4843
const prevScrollHeightRef = useRef<number | null>(null)
4944

50-
// API Hooks
51-
const {
52-
messages,
53-
hasNextPage,
54-
opponentActive,
55-
blockActive,
56-
setSize,
57-
mutate,
58-
isFetchingPrevMessages,
59-
} = useReadChatMessages(chatRoomId)
45+
const { messages, hasNextPage, opponentActive, blockActive, setSize, mutate, isFetchingPrevMessages } = useReadChatMessages(chatRoomId)
6046
const { trigger: getTicket } = useGetChatTicket()
6147
const { trigger: leaveChatRoom } = useDeleteChatRoom(chatRoomId)
62-
// const otherUserLeft = !opponentActive
63-
// const isBlockActive = blockActive || newMessagesData?.blockActive || false
48+
49+
// 초기 API 로드 시 상대방 활성화/차단 상태 동기화
50+
useEffect(() => {
51+
setIsOtherUserLeft(!opponentActive)
52+
setIsBlocked(blockActive)
53+
}, [opponentActive, blockActive])
6454

6555
// 티켓 발급
6656
useEffect(() => {
6757
const fetchTicket = async () => {
6858
if (!chatRoomId) return
69-
7059
try {
7160
const res = await getTicket({ chatroomId: chatRoomId })
7261
setTicket(res.data.chatTicket)
73-
} catch {}
62+
} catch (err) {
63+
console.error('티켓 발급 실패:', err)
64+
}
7465
}
75-
7666
fetchTicket()
7767
}, [chatRoomId, getTicket])
7868

79-
// 새 메시지 수신 시 처리 로직
80-
const handleNewMessage = useCallback(
81-
(newChat: SocketChatContent) => {
82-
mutate((currentData) => {
83-
if (!currentData) return []
69+
const handleSocketEvent = useCallback(
70+
(response: ChatSocketResponse) => {
71+
const { type, data, message } = response
8472

85-
const newData = [...currentData]
86-
const isDuplicate = newData.some((page) =>
87-
page.content.some((chat) => chat.chatId === newChat.chatId),
88-
)
73+
if (type === 'TALK' && data) {
74+
const socketData = data as SocketChatContent
75+
const isMine = socketData.senderId === user?.profile?.sub
8976

90-
if (isDuplicate) return currentData
91-
92-
newData[0] = {
93-
...newData[0],
94-
content: [...newData[0].content, newChat as ChatContent],
77+
if (!isMine && isOtherUserLeft) {
78+
setIsOtherUserLeft(false)
79+
setIsBlocked(false)
9580
}
96-
return newData
97-
}, false)
81+
82+
mutate((currentData) => {
83+
if (!currentData) return []
84+
85+
const newData = [...currentData]
86+
const isDuplicate = newData.some((page) => page.content.some((chat) => chat.chatId === socketData.chatId))
87+
88+
if (isDuplicate) return currentData
89+
90+
const newChatEntry: ChatContent = {
91+
...socketData,
92+
mine: isMine,
93+
}
94+
95+
newData[0] = {
96+
...newData[0],
97+
content: [...newData[0].content, newChatEntry],
98+
}
99+
return newData
100+
}, false)
101+
}
102+
if (message === null) return
103+
if (type === 'SYSTEM_LEAVE' || type === 'SYSTEM_BANNED') {
104+
if (type === 'SYSTEM_LEAVE') setIsOtherUserLeft(true)
105+
if (type === 'SYSTEM_BANNED') setIsBlocked(true)
106+
107+
mutate((currentData) => {
108+
if (!currentData) return []
109+
const newData = [...currentData]
110+
const systemEntry: ChatContent = {
111+
chatId: Date.now(),
112+
chatContent: message,
113+
sentAt: new Date().toISOString(),
114+
mine: false,
115+
chatType: 'SYSTEM',
116+
senderId: '',
117+
senderNickname: '',
118+
senderProfileUrl: '',
119+
senderThumbnailUrl: '',
120+
}
121+
newData[0] = {
122+
...newData[0],
123+
content: [...newData[0].content, systemEntry],
124+
}
125+
return newData
126+
}, false)
127+
}
128+
129+
if (type === 'ERROR') {
130+
console.log(message || '메시지 전송에 실패했습니다.')
131+
}
98132
},
99-
[mutate],
133+
[mutate, user?.profile?.sub],
100134
)
101135

102136
// 웹소켓 연결
103-
const { sendMessage: sendMessageBySocket } = useChatSocket(
104-
chatRoomId,
105-
ticket,
106-
handleNewMessage,
107-
)
137+
const { sendMessage: sendMessageBySocket } = useChatSocket(chatRoomId, ticket, handleSocketEvent)
108138

109139
// 메세지 전송 핸들러
110140

111141
const handleSendMessage = (message: string) => {
112142
if (!message.trim() || !user?.profile) return
113-
const tempChatId = Date.now()
114-
const tempMessage: ChatContent = {
115-
chatId: tempChatId,
116-
chatContent: message,
117-
sentAt: new Date().toISOString(),
118-
mine: true,
119-
chatType: 'USER',
120-
senderId: user.profile.sub,
121-
senderNickname: '',
122-
senderProfileUrl: '',
123-
senderThumbnailUrl: '',
124-
}
125143

126-
mutate((currentData) => {
127-
if (!currentData) return []
128-
const newData = [...currentData]
129-
newData[0] = {
130-
...newData[0],
131-
content: [...newData[0].content, tempMessage],
132-
}
133-
return newData
134-
}, false)
144+
// 추후 응답속도가 느려 낙관적 업데이트가 필요할 시 재사용
145+
// const tempMessage: ChatContent = {
146+
// chatId: Date.now(),
147+
// chatContent: message,
148+
// sentAt: new Date().toISOString(),
149+
// mine: true,
150+
// chatType: 'USER',
151+
// senderId: user.profile.sub,
152+
// senderNickname: '',
153+
// senderProfileUrl: '',
154+
// senderThumbnailUrl: '',
155+
// }
156+
// mutate((currentData) => {
157+
// if (!currentData) return []
158+
// const newData = [...currentData]
159+
// newData[0] = {
160+
// ...newData[0],
161+
// content: [...newData[0].content, tempMessage],
162+
// }
163+
// return newData
164+
// }, false)
135165

136-
// 실제 웹소켓으로 전송
137166
sendMessageBySocket(message)
138167

139168
logAnalyticsEvent('chat_sent', {
@@ -206,7 +235,7 @@ export default function ChatRoom() {
206235
}
207236
}, [handleScroll])
208237

209-
// 스크롤 위치 조정 로직
238+
// 새 메시지 수신 시 하단 스크롤 유지
210239
useEffect(() => {
211240
const container = messageContainerRef.current
212241
if (!container) return
@@ -238,10 +267,7 @@ export default function ChatRoom() {
238267
return (
239268
<div className={styles['container']}>
240269
{/* 헤더 영역 */}
241-
<Header
242-
rightIcon={<KebabIcon />}
243-
onClick={() => setIsMenuOpen(!isMenuOpen)}
244-
>
270+
<Header rightIcon={<KebabIcon />} onClick={() => setIsMenuOpen(!isMenuOpen)}>
245271
{nickName}
246272
</Header>
247273

@@ -251,22 +277,9 @@ export default function ChatRoom() {
251277
<div className={styles['message-container']} ref={messageContainerRef}>
252278
{messages.map((chat, index) => {
253279
const prevChat = index > 0 ? messages[index - 1] : null
254-
const nextChat =
255-
index < messages.length - 1 ? messages[index + 1] : null
256-
257-
const isContinuous = !!(
258-
prevChat &&
259-
prevChat.senderId === chat.senderId &&
260-
prevChat.chatType === 'USER' &&
261-
chat.chatType === 'USER' &&
262-
isSameMinute(prevChat.sentAt, chat.sentAt)
263-
)
264-
265-
const shouldShowTime =
266-
!nextChat ||
267-
nextChat.senderId !== chat.senderId ||
268-
nextChat.chatType !== 'USER' ||
269-
!isSameMinute(nextChat.sentAt, chat.sentAt)
280+
const nextChat = index < messages.length - 1 ? messages[index + 1] : null
281+
const isContinuous = !!(prevChat && prevChat.senderId === chat.senderId && prevChat.chatType === 'USER' && chat.chatType === 'USER' && isSameMinute(prevChat.sentAt, chat.sentAt))
282+
const shouldShowTime = !nextChat || nextChat.senderId !== chat.senderId || nextChat.chatType !== 'USER' || !isSameMinute(nextChat.sentAt, chat.sentAt)
270283

271284
if (chat.chatType === 'SYSTEM') {
272285
return (
@@ -278,20 +291,11 @@ export default function ChatRoom() {
278291

279292
return chat.mine ? (
280293
<div key={chat.chatId} data-date={chat.sentAt}>
281-
<MyMessage
282-
chat={chat}
283-
isContinuous={isContinuous}
284-
shouldShowTime={shouldShowTime}
285-
/>
294+
<MyMessage chat={chat} isContinuous={isContinuous} shouldShowTime={shouldShowTime} />
286295
</div>
287296
) : (
288297
<div key={chat.chatId} data-date={chat.sentAt}>
289-
<OtherMessage
290-
chat={chat}
291-
isContinuous={isContinuous}
292-
shouldShowTime={shouldShowTime}
293-
memberId={memberId}
294-
/>
298+
<OtherMessage chat={chat} isContinuous={isContinuous} shouldShowTime={shouldShowTime} memberId={memberId} />
295299
</div>
296300
)
297301
})}
@@ -300,11 +304,7 @@ export default function ChatRoom() {
300304
</div>
301305

302306
{/* 채팅 입력 영역 */}
303-
<ChatInput
304-
onSend={handleSendMessage}
305-
disabled={!opponentActive}
306-
blocked={blockActive}
307-
/>
307+
<ChatInput onSend={handleSendMessage} disabled={isOtherUserLeft} blocked={isBlocked} />
308308

309309
{isMenuOpen && (
310310
<KebabModal

src/hooks/useChatSocket.ts

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,45 @@
11
import { useEffect, useRef, useState, useCallback } from 'react'
2-
import type { SocketChatContent } from '@/types/chat'
2+
import type { ChatSocketResponse } from '@/types/chat'
33

4-
type WebSocketMessage = {
5-
webSocketType: 'TALK' | 'PING' | 'PONG'
6-
data?: SocketChatContent | null
7-
}
8-
9-
export const useChatSocket = (
10-
chatroomId: number,
11-
ticket: string | null,
12-
onMessageReceived: (message: SocketChatContent) => void,
13-
) => {
4+
export const useChatSocket = (chatroomId: number, ticket: string | null, onEvent: (response: ChatSocketResponse) => void) => {
145
const socketRef = useRef<WebSocket | null>(null)
156
const [isConnected, setIsConnected] = useState(false)
167
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null)
178

189
const connect = useCallback(() => {
1910
if (!ticket || !chatroomId) return
2011

21-
const socket = new WebSocket(
22-
`${process.env.NEXT_PUBLIC_CHAT_SOCKET_DOMAIN}?chatTicket=${ticket}`,
23-
)
12+
const socket = new WebSocket(`${process.env.NEXT_PUBLIC_CHAT_SOCKET_DOMAIN}?chatTicket=${ticket}`)
2413

2514
socket.onopen = () => {
2615
setIsConnected(true)
2716
pingIntervalRef.current = setInterval(() => {
2817
if (socket.readyState === WebSocket.OPEN) {
29-
socket.send(JSON.stringify({ webSocketType: 'PING' }))
18+
socket.send(JSON.stringify({ type: 'PING' }))
3019
}
3120
}, 30000)
3221
}
3322

3423
socket.onmessage = (event) => {
35-
const response: WebSocketMessage = JSON.parse(event.data)
36-
if (response.webSocketType === 'TALK' && response.data) {
37-
onMessageReceived(response.data)
38-
}
24+
const response: ChatSocketResponse = JSON.parse(event.data)
25+
onEvent(response)
3926
}
4027

4128
socket.onclose = (event) => {
42-
console.log(
43-
`WS Disconnected (Code: ${event.code}, Reason: ${event.reason})`,
44-
)
29+
console.log(`WS Disconnected (Code: ${event.code})`)
4530
setIsConnected(false)
4631
if (pingIntervalRef.current) clearInterval(pingIntervalRef.current)
4732
socketRef.current = null
4833
}
4934

5035
socketRef.current = socket
51-
}, [ticket, chatroomId, onMessageReceived])
36+
}, [ticket, chatroomId, onEvent])
5237

5338
const sendMessage = useCallback((content: string) => {
5439
if (socketRef.current?.readyState === WebSocket.OPEN) {
5540
socketRef.current.send(
5641
JSON.stringify({
57-
webSocketType: 'TALK',
42+
type: 'TALK',
5843
content: content,
5944
}),
6045
)

src/types/chat.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,10 @@ export type SocketChatContent = ChatId & {
5656
chatType: string
5757
sentAt: string
5858
}
59+
60+
export type ChatSocketResponse = {
61+
succeed: boolean
62+
type: 'TALK' | 'PONG' | 'ERROR' | 'SYSTEM_BANNED' | 'SYSTEM_LEAVE'
63+
message: string | null
64+
data: SocketChatContent | string | null
65+
}

0 commit comments

Comments
 (0)