diff --git a/packages/admin/src/components/chat/RealTimeChatting.tsx b/packages/admin/src/components/chat/RealTimeChatting.tsx index bca9f4c7..ea3d9b82 100644 --- a/packages/admin/src/components/chat/RealTimeChatting.tsx +++ b/packages/admin/src/components/chat/RealTimeChatting.tsx @@ -2,8 +2,8 @@ import { ChatList } from '@softeer/common/components'; import { useRef } from 'react'; import { AdminSocketReturnType } from 'src/hooks/socket/index.ts'; import { useAlert } from 'src/store/provider/AlertProvider.tsx'; -import { Button } from '../ui/button.js'; -import { Input } from '../ui/input.js'; +import { Button } from '../ui/button.tsx'; +import { Input } from '../ui/input.tsx'; import Chat from './Chat.js'; /** 실시간 기대평 섹션 */ @@ -13,6 +13,7 @@ function RealTimeChatting({ }: Pick) { const { openAlert, addAlertCallback } = useAlert(); const noticeInputRef = useRef(null); + const handleSend: React.FormEventHandler = (event) => { event.preventDefault(); const { value } = noticeInputRef!.current!; diff --git a/packages/admin/src/hooks/socket/index.ts b/packages/admin/src/hooks/socket/index.ts index 1a8b43f5..ee48604b 100644 --- a/packages/admin/src/hooks/socket/index.ts +++ b/packages/admin/src/hooks/socket/index.ts @@ -9,7 +9,15 @@ export type AdminSocketReturnType = ReturnType; export default function useSocket() { const accessToken = Cookie.getCookie(ACCESS_TOKEN_KEY) ?? ''; const chatSocket = useChatSocket(); - const { onReceiveMessage, onReceiveBlock, onReceiveNotice, ...chatSocketProps } = chatSocket; + + const { + onReceiveMessage, + onReceiveBlock, + onReceiveNotice, + onReceiveMessageHistory, + ...chatSocketProps + } = chatSocket; + useEffect(() => { if (accessToken !== '') { socketManager.connectSocketClient({ @@ -17,9 +25,10 @@ export default function useSocket() { onReceiveMessage, onReceiveBlock, onReceiveNotice, + onReceiveMessageHistory, }); } - }, [socketManager, accessToken]); + }, []); return { chatSocket: chatSocketProps }; } diff --git a/packages/admin/src/hooks/socket/useChatSocket.ts b/packages/admin/src/hooks/socket/useChatSocket.ts index 19998dfb..47175bc6 100644 --- a/packages/admin/src/hooks/socket/useChatSocket.ts +++ b/packages/admin/src/hooks/socket/useChatSocket.ts @@ -1,7 +1,9 @@ import { ChatProps } from '@softeer/common/components'; import { CHAT_SOCKET_ENDPOINTS } from '@softeer/common/constants'; -import { SocketSubscribeCallbackType } from '@softeer/common/utils'; -import { useCallback, useState } from 'react'; +import { Socket, SocketSubscribeCallbackType } from '@softeer/common/utils'; +import { useCallback, useEffect, useState } from 'react'; +import { eventBus } from 'src/services/eventBus.ts'; + import socketManager from 'src/services/socket.ts'; import { useAlert } from 'src/store/provider/AlertProvider.tsx'; @@ -9,12 +11,20 @@ export type AdminChatSocketReturnType = ReturnType; export default function useChatSocket() { const { openAlert } = useAlert(); - - const socketClient = socketManager.getSocketClient(); - + const [socketClient, setSocketClient] = useState(null); + const [isValid, setIsValid] = useState(false); const [chatMessages, setChatMessages] = useState([]); const [notice, setNotice] = useState(''); + const updateSocket = () => { + setSocketClient(socketManager.getSocketClient()); + setIsValid(true); + }; + + useEffect(() => { + eventBus.on('socket_connected', updateSocket); + }, []); + const handleIncomingMessage: SocketSubscribeCallbackType = useCallback((data: unknown) => { setChatMessages((prevMessages) => [...prevMessages, data] as ChatProps[]); }, []); @@ -42,7 +52,15 @@ export default function useChatSocket() { const { content } = data as { content: string }; setNotice(content); }, - [socketClient], + [chatMessages], + ); + + const handleIncomingMessageHistory: SocketSubscribeCallbackType = useCallback( + (data: unknown) => { + const parsedDataList = data as Omit[]; + setChatMessages((prevMessages) => [...parsedDataList, ...prevMessages] as ChatProps[]); + }, + [chatMessages], ); const handleBlock = useCallback( @@ -51,10 +69,14 @@ export default function useChatSocket() { blockId: id, }; try { - socketClient.sendMessages({ - destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_BLOCK, - body: blockId, - }); + if (socketClient) { + socketClient.sendMessages({ + destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_BLOCK, + body: blockId, + }); + } else { + openAlert('소켓이 연결되지 않았습니다.', 'alert'); + } } catch (error) { const errorMessage = (error as Error).message; openAlert(errorMessage.length > 0 ? errorMessage : '문제가 발생했습니다.', 'alert'); @@ -66,11 +88,15 @@ export default function useChatSocket() { const handleSendNotice = useCallback( (content: string) => { try { - const chatMessage = { content }; - socketClient.sendMessages({ - destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_NOTICE, - body: chatMessage, - }); + if (socketClient) { + const chatMessage = { content }; + socketClient.sendMessages({ + destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_NOTICE, + body: chatMessage, + }); + } else { + openAlert('소켓이 연결되지 않았습니다.', 'alert'); + } } catch (error) { const errorMessage = (error as Error).message; openAlert(errorMessage.length > 0 ? errorMessage : '문제가 발생했습니다.', 'alert'); @@ -79,13 +105,32 @@ export default function useChatSocket() { [socketClient], ); + const handleRequestMessageHistory = useCallback(() => { + try { + if (socketClient) { + socketClient.sendMessages({ + destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_MESSAGE_HISTORY, + body: {}, + }); + } else { + openAlert('소켓이 연결되지 않았습니다.', 'alert'); + } + } catch (error) { + const errorMessage = (error as Error).message; + openAlert(errorMessage.length > 0 ? errorMessage : '문제가 발생했습니다.', 'alert'); + } + }, [socketClient]); + return { onReceiveMessage: handleIncomingMessage, onReceiveBlock: handleIncomintBlock, onReceiveNotice: handleIncomingNotice, + onReceiveMessageHistory: handleIncomingMessageHistory, onBlock: handleBlock, onNotice: handleSendNotice, + onRequestMessageHistory: handleRequestMessageHistory, messages: chatMessages, notice, + isValid, }; } diff --git a/packages/admin/src/pages/review/Review.tsx b/packages/admin/src/pages/review/Review.tsx index e6b6b563..0f111ad2 100644 --- a/packages/admin/src/pages/review/Review.tsx +++ b/packages/admin/src/pages/review/Review.tsx @@ -1,8 +1,14 @@ +import { useEffect } from 'react'; import RealTimeChatting from 'src/components/chat/RealTimeChatting.tsx'; import useSocket from 'src/hooks/socket/index.ts'; function Review() { const { chatSocket } = useSocket(); + useEffect(() => { + if (chatSocket.isValid) { + chatSocket.onRequestMessageHistory(); + } + }, [chatSocket.isValid]); return ; } export default Review; diff --git a/packages/admin/src/services/eventBus.ts b/packages/admin/src/services/eventBus.ts new file mode 100644 index 00000000..ede570e3 --- /dev/null +++ b/packages/admin/src/services/eventBus.ts @@ -0,0 +1,18 @@ +class EventBus { + private eventTarget = new EventTarget(); + + public on(event: string, callback: (event: Event) => void): void { + this.eventTarget.addEventListener(event, callback); + } + + public off(event: string, callback: (event: Event) => void): void { + this.eventTarget.removeEventListener(event, callback); + } + + public emit(event: string, detail: any): void { + const customEvent = new CustomEvent(event, { detail }); + this.eventTarget.dispatchEvent(customEvent); + } +} + +export const eventBus = new EventBus(); diff --git a/packages/admin/src/services/socket.ts b/packages/admin/src/services/socket.ts index 7b395e56..d485abda 100644 --- a/packages/admin/src/services/socket.ts +++ b/packages/admin/src/services/socket.ts @@ -1,6 +1,8 @@ -import { ACCESS_TOKEN_KEY, CHAT_SOCKET_ENDPOINTS } from '@softeer/common/constants'; -import { Cookie, Socket, SocketSubscribeCallbackType } from '@softeer/common/utils'; +import { CHAT_SOCKET_ENDPOINTS } from '@softeer/common/constants'; +import { Socket, SocketSubscribeCallbackType } from '@softeer/common/utils'; import { SOCKET_BASE_URL } from 'src/constants/environments.ts'; +import { eventBus } from './eventBus.ts'; + // import CustomError from 'src/utils/error.ts'; class SocketManager { @@ -12,14 +14,18 @@ class SocketManager { private onReceiveNotice: SocketSubscribeCallbackType | null = null; - constructor(token: string | null) { - this.initializeSocketClient(token); - } + private onReceiveMessageHistory: SocketSubscribeCallbackType | null = null; + + private isConnected: boolean = false; public getSocketClient() { return this.socketClient!; } + public getIsConnected() { + return this.isConnected; + } + private initializeSocketClient(token?: string | null) { if (token) { this.socketClient = new Socket(SOCKET_BASE_URL, token); @@ -31,11 +37,13 @@ class SocketManager { onReceiveMessage, onReceiveBlock, onReceiveNotice, + onReceiveMessageHistory, }: { token: string | null | undefined; onReceiveMessage: SocketSubscribeCallbackType; onReceiveBlock: SocketSubscribeCallbackType; onReceiveNotice: SocketSubscribeCallbackType; + onReceiveMessageHistory: SocketSubscribeCallbackType; }) { if (this.socketClient) { await this.socketClient.disconnect(); @@ -46,10 +54,13 @@ class SocketManager { this.onReceiveMessage = onReceiveMessage; this.onReceiveBlock = onReceiveBlock; this.onReceiveNotice = onReceiveNotice; + this.onReceiveMessageHistory = onReceiveMessageHistory; try { await this.socketClient!.connect(); this.subscribeToTopics(); + this.isConnected = true; + eventBus.emit('socket_connected', {}); } catch (error) { throw new Error('서버에서 데이터를 불러오는 데 실패했습니다.'); } @@ -61,6 +72,7 @@ class SocketManager { onReceiveBlock: this.onReceiveBlock!, onReceiveMessage: this.onReceiveMessage!, onReceiveNotice: this.onReceiveNotice!, + onReceiveMessageHistory: this.onReceiveMessageHistory!, }); } @@ -68,7 +80,7 @@ class SocketManager { if (this.socketClient) { if (this.onReceiveMessage) { this.socketClient.subscribe({ - destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_CHAT, + destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_MESSAGE, callback: this.onReceiveMessage, }); } @@ -85,9 +97,16 @@ class SocketManager { callback: this.onReceiveNotice, }); } + + if (this.onReceiveMessageHistory) { + this.socketClient.subscribe({ + destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_MESSAGE_HISTORY, + callback: this.onReceiveMessageHistory, + }); + } } } } +const socketManager = new SocketManager(); -const socketManager = new SocketManager(Cookie.getCookie(ACCESS_TOKEN_KEY)); export default socketManager; diff --git a/packages/common/src/constants/socket.ts b/packages/common/src/constants/socket.ts index dd2b444b..7484829a 100644 --- a/packages/common/src/constants/socket.ts +++ b/packages/common/src/constants/socket.ts @@ -1,16 +1,15 @@ import { Category, SocketCategory } from 'src/types/category.ts'; export const CHAT_SOCKET_ENDPOINTS = { - SUBSCRIBE_CHAT: '/topic/chat', + SUBSCRIBE_MESSAGE: '/topic/chat', PUBLISH_CHAT: '/app/chat.sendMessage', SUBSCRIBE_BLOCK: '/topic/block', PUBLISH_BLOCK: '/app/chat.sendBlock', SUBSCRIBE_NOTICE: '/topic/notice', PUBLISH_NOTICE: '/app/chat.sendNotice', - SUBSCRIBE_CHAT_LIST: '/user/queue/chatHistory', - PUBLISH_CHAT_LIST: '/app/chat.getHistory', + SUBSCRIBE_MESSAGE_HISTORY: '/user/queue/chatHistory', + PUBLISH_MESSAGE_HISTORY: '/app/chat.getHistory', SUBSCRIBE_ERROR: '/user/queue/errors', - } as const; export const RACING_SOCKET_ENDPOINTS = { diff --git a/packages/user/src/services/socket.ts b/packages/user/src/services/socket.ts index 0a9cb54f..d7dd739f 100644 --- a/packages/user/src/services/socket.ts +++ b/packages/user/src/services/socket.ts @@ -79,11 +79,11 @@ class SocketManager { if (this.onReceiveChatList) { await this.socketClient.subscribe({ - destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_CHAT_LIST, + destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_MESSAGE_HISTORY, callback: this.onReceiveChatList, }); this.socketClient.sendMessages({ - destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_CHAT_LIST, + destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_MESSAGE_HISTORY, body: {}, requiresAuth: false, }); @@ -91,7 +91,7 @@ class SocketManager { if (this.onReceiveMessage) { this.socketClient.subscribe({ - destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_CHAT, + destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_MESSAGE, callback: this.onReceiveMessage, }); }