@@ -7,13 +7,10 @@ import { useAuth } from 'react-oidc-context'
77import Header from '@/components/commons/header'
88import KebabIcon from '@/assets/icon_kebab.svg'
99import { 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'
1511import { useChatSocket } from '@/hooks/useChatSocket'
1612import type { ChatContent , SocketChatContent } from '@/types/chat'
13+ import type { ChatSocketResponse } from '@/types/chat'
1714import { logAnalyticsEvent } from '@/lib/firebase/analytics'
1815
1916import KebabModal from './_components/kebabModal'
@@ -25,115 +22,147 @@ import ChatInput from './_components/chatInput'
2522import styles from './room.module.css'
2623
2724export 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
0 commit comments