diff --git a/src/frontend/apps/web/app/stock/layout.tsx b/src/frontend/apps/web/app/stock/layout.tsx index e9d10bb9..f3b9a17a 100644 --- a/src/frontend/apps/web/app/stock/layout.tsx +++ b/src/frontend/apps/web/app/stock/layout.tsx @@ -1,3 +1,4 @@ +import { ToastAlarm } from '@/src/features/alarm'; import AuthWrapper from '@/src/features/auth/ui/auth-wrapper'; import { getAccessToken } from '@/src/features/stock'; import { ProfilePopover } from '@/src/features/user'; @@ -8,6 +9,7 @@ import { WebSocketProvider, } from '@/src/shared'; import { getUserIdFromCookie } from '@/src/shared/services/lib'; +import { Toaster } from '@workspace/ui/components'; import '@workspace/ui/globals.css'; @@ -30,6 +32,8 @@ export default async function RootLayout({ {children} + + diff --git a/src/frontend/apps/web/src/features/alarm/index.ts b/src/frontend/apps/web/src/features/alarm/index.ts new file mode 100644 index 00000000..19996a78 --- /dev/null +++ b/src/frontend/apps/web/src/features/alarm/index.ts @@ -0,0 +1 @@ +export { ToastAlarm } from './model/toast-alarm'; diff --git a/src/frontend/apps/web/src/features/alarm/model/toast-alarm.tsx b/src/frontend/apps/web/src/features/alarm/model/toast-alarm.tsx new file mode 100644 index 00000000..4005ac0d --- /dev/null +++ b/src/frontend/apps/web/src/features/alarm/model/toast-alarm.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { useEffect } from 'react'; +import { webSocketEvent } from '@/src/shared/providers/stomp-websocket-provider'; +import { useToast } from '@workspace/ui/hooks/Toast/use-toast'; +import { ToastAction } from '@workspace/ui/components'; + +export type Alarm = { + userNickname: string; + channelName: string; + text: string; +}; + +export const ToastAlarm = () => { + const { toast } = useToast(); + + useEffect(() => { + const handleMessage = (message: Alarm) => { + toast({ + title: `${message.userNickname} #${message.channelName}`, + description: `${message.text}`, + }); + }; + + webSocketEvent.on('alarmReceived', handleMessage); + + return () => { + webSocketEvent.off('alarmReceived', handleMessage); + }; + }, [toast]); + + return null; +}; diff --git a/src/frontend/apps/web/src/shared/providers/stomp-websocket-provider.tsx b/src/frontend/apps/web/src/shared/providers/stomp-websocket-provider.tsx index 3e858d3e..9c20b290 100644 --- a/src/frontend/apps/web/src/shared/providers/stomp-websocket-provider.tsx +++ b/src/frontend/apps/web/src/shared/providers/stomp-websocket-provider.tsx @@ -12,10 +12,14 @@ import { import * as StompJs from '@stomp/stompjs'; import SockJS from 'sockjs-client'; +import EventEmitter from 'events'; + +export const webSocketEvent = new EventEmitter(); type WebSocketContextProps = { client: StompJs.Client | null; isConnected: boolean; + sessionId: string; }; type WebSocketProviderProps = { @@ -31,8 +35,7 @@ export const StompWebSocketProvider = ({ }: WebSocketProviderProps) => { const client = useRef(null); const [isConnected, setIsConnected] = useState(false); - - // const BASE_URL = `http://${process.env.NEXT_PUBLIC_BASE_URL}:${process.env.NEXT_PUBLIC_CHAT_SERVER1_PORT}`; + const sessionId = useRef(null); const BASE_URL = `${process.env.NEXT_PUBLIC_REAL_BASE_URL}`; const connect = useCallback(() => { @@ -50,6 +53,23 @@ export const StompWebSocketProvider = ({ onConnect: () => { console.log('✅ WebSocket Connected'); setIsConnected(true); + + const socketUrl = client.current.webSocket['_transport'].ws.url; + sessionId.current = socketUrl.split('/').slice(-2, -1)[0]; + + client.current.subscribe( + `/subscribe/notification.${sessionId.current}`, + (message) => { + try { + const payload = JSON.parse(message.body); + console.log('📩 Received:', payload); + + webSocketEvent.emit('alarmReceived', payload); + } catch (error) { + console.error('❌ 메시지 파싱 실패:', error); + } + }, + ); }, onStompError: (frame) => { console.error('❌ Broker error:', frame.headers['message'], frame.body); @@ -73,7 +93,11 @@ export const StompWebSocketProvider = ({ return ( {children}