From a3dc2c699b874ff9730377e2d5f5e46eeeece37a Mon Sep 17 00:00:00 2001 From: kangsinbeom Date: Fri, 21 Jun 2024 10:03:41 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=B1=84=ED=8C=85=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=20=EB=B0=8F=20coupon=20=EB=AA=A8=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/chat.ts | 2 +- src/components/CouponPortal.tsx | 20 +++++ src/components/NotificationPortal.tsx | 10 ++- src/hooks/useTimer.ts | 11 ++- .../chatModal/components/chat/ChatMenu.tsx | 80 ++++++++--------- src/pages/chatModal/hooks/useStomp.ts | 88 +++++++++++++++++++ src/pages/chatModal/hooks/useWebSocket.ts | 59 ------------- src/pages/payment/components/index.ts | 1 + .../payment/components/sDiscount/index.tsx | 7 +- 9 files changed, 169 insertions(+), 109 deletions(-) create mode 100644 src/components/CouponPortal.tsx create mode 100644 src/pages/chatModal/hooks/useStomp.ts delete mode 100644 src/pages/chatModal/hooks/useWebSocket.ts diff --git a/src/apis/chat.ts b/src/apis/chat.ts index cecc846..6b883ea 100644 --- a/src/apis/chat.ts +++ b/src/apis/chat.ts @@ -2,7 +2,7 @@ import instance from './instance'; // 채팅 메시지 전체 조회 export const getChatMessage = async (chatRoomId: number) => { - const response = await instance.get(`/${chatRoomId}/chatmessages`); + const response = await instance.get(`/${chatRoomId}/chat-messages`); return response?.data; }; diff --git a/src/components/CouponPortal.tsx b/src/components/CouponPortal.tsx new file mode 100644 index 0000000..971d4f8 --- /dev/null +++ b/src/components/CouponPortal.tsx @@ -0,0 +1,20 @@ +import { CouponModal } from '@/pages/payment/components'; +import { couponStore } from '@/stores/modal'; +import { createPortal } from 'react-dom'; + +const CouponPortal = () => { + const { + close, + couponValue: { open }, + } = couponStore(); + const $portal_root = document.getElementById('content-portal'); + return ( + <> + {$portal_root + ? createPortal(
{open && }
, $portal_root) + : null} + + ); +}; + +export default CouponPortal; diff --git a/src/components/NotificationPortal.tsx b/src/components/NotificationPortal.tsx index 42e2c1b..fc7b042 100644 --- a/src/components/NotificationPortal.tsx +++ b/src/components/NotificationPortal.tsx @@ -1,9 +1,17 @@ import { createPortal } from 'react-dom'; import { Notification } from '.'; +import { memberStore } from '@/stores/member'; const NotificationPortal = () => { + const isLogin = !!memberStore((state) => state.accessToken); const $portal_root = document.getElementById('notification-portal'); - return <>{$portal_root ? createPortal(, $portal_root) : null}; + return ( + <> + {$portal_root + ? createPortal(
{isLogin && }
, $portal_root) + : null} + + ); }; export default NotificationPortal; diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts index 1c5d451..9052195 100644 --- a/src/hooks/useTimer.ts +++ b/src/hooks/useTimer.ts @@ -11,16 +11,21 @@ const useTimer = ({ time = 300 }: { time?: number }) => { }, [count]); const getSeconds = () => { - const seconds = Number(count % 60); + const seconds = Number(Math.floor(count % 60)); if (seconds >= 10) return String(seconds); return `0${seconds}`; }; const getMinutes = () => { - const minutes = Number(Math.floor(count / 60)); + const minutes = Number(Math.floor((count % 3600) / 60)); if (minutes >= 10) return String(minutes); return `0${minutes}`; }; - return { minutes: getMinutes(), seconds: getSeconds() }; + const getHours = () => { + const hours = Number(Math.floor(count / 3600)); + if (hours >= 10) return String(hours); + return `0${hours}`; + }; + return { hours: getHours(), minutes: getMinutes(), seconds: getSeconds() }; }; export default useTimer; diff --git a/src/pages/chatModal/components/chat/ChatMenu.tsx b/src/pages/chatModal/components/chat/ChatMenu.tsx index 568c9f0..0ff716f 100644 --- a/src/pages/chatModal/components/chat/ChatMenu.tsx +++ b/src/pages/chatModal/components/chat/ChatMenu.tsx @@ -1,16 +1,28 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import Icon from '@/components/icon'; import Message from './Message'; -import { ChatMessageRequest, ChatMessageResponse } from '@/types/chat'; +import { ChatMessageResponse } from '@/types/chat'; import { useGetMessages } from '../../hooks/useGetMessages'; import { memberStore } from '@/stores/member'; import * as S from './styles'; -import useWebSocket from '../../hooks/useWebSocket'; +import useStomp from '../../hooks/useStomp'; const ChatMenu = ({ chatRoomId }: { chatRoomId: number }) => { - const { nickname } = memberStore((state) => state.auth); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); + const { accessToken } = memberStore.getState(); + + // 메세지 받는 callback 함수 + const callback = useCallback((message: ChatMessageResponse) => { + const messageData = JSON.parse(message.content); + setMessages((prevMessages) => [...prevMessages, messageData]); + }, []); + + const { connect, disconnect, sendMessage } = useStomp( + chatRoomId, + accessToken as string, + callback, + ); // 스크롤 아래로 이동 const ref = useRef(null); @@ -20,44 +32,27 @@ const ChatMenu = ({ chatRoomId }: { chatRoomId: number }) => { } }; - // 과거 채팅 - // const { data } = useGetMessages(chatRoomId); - // useEffect(() => { - // if (data) { - // setMessages(data); - // } - // scrollToBottom(); - // }, [data]); + // 과거 채팅 불러오기, WebSocket 연결 + const { data } = useGetMessages(chatRoomId); + useEffect(() => { + if (data) { + setMessages(data); + } + scrollToBottom(); - // 새로운 메시지 수신 처리 - const handleIncomingMessage = useCallback((msg: ChatMessageResponse) => { - setMessages((prevMessages) => [...prevMessages, msg]); - }, []); + connect(); - // WebSocket 연결 - const stompClient = useWebSocket(chatRoomId, handleIncomingMessage); + // 언마운트시 연결 종료 + return () => { + disconnect(); + }; + }, [data]); // 새로운 채팅 보내기 - const sendMessage = () => { - if (stompClient && newMessage) { - const chatMessage: ChatMessageRequest = { - content: newMessage, - }; - stompClient.publish({ - destination: `/pub/ws/${chatRoomId}/chat-messages`, - body: JSON.stringify(chatMessage), - }); - setMessages((prevMessages) => [ - ...prevMessages, - { - sender: nickname, - content: newMessage, - createAt: new Date(), - }, - ]); - - setNewMessage(''); - } + const postMessage = () => { + const messageBody = { content: newMessage }; + sendMessage(`/pub/ws/${chatRoomId}/chat-messages`, messageBody); + setNewMessage(''); }; // 메세지 추가될 때 @@ -65,7 +60,7 @@ const ChatMenu = ({ chatRoomId }: { chatRoomId: number }) => { scrollToBottom(); }, [messages]); - const handleInputChange = (e: React.ChangeEvent) => { + const onInputChange = (e: React.ChangeEvent) => { setNewMessage(e.target.value); }; @@ -79,15 +74,12 @@ const ChatMenu = ({ chatRoomId }: { chatRoomId: number }) => { profileImage={'default'} content={msg.content} /> + // 작가 메세지 구분 추후 추가 ))} - - + + ); diff --git a/src/pages/chatModal/hooks/useStomp.ts b/src/pages/chatModal/hooks/useStomp.ts new file mode 100644 index 0000000..124c2c8 --- /dev/null +++ b/src/pages/chatModal/hooks/useStomp.ts @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import SockJS from 'sockjs-client'; +import { Client, IMessage } from '@stomp/stompjs'; +import { ChatMessageRequest, ChatMessageResponse } from '@/types/chat'; + +export const useStomp = ( + chatRoomId: number, + accessToken: string, + callback: (msg: ChatMessageResponse) => void, +) => { + const [client, setClient] = useState(null); + + const connect = () => { + const socket = new SockJS(import.meta.env.VITE_SOCKET_URL); + + const client = new Client({ + webSocketFactory: () => socket, + connectHeaders: { + Authorization: `Bearer ${accessToken}`, + }, + reconnectDelay: 5000, + debug: (str) => { + console.log(str); + }, + onConnect: () => { + console.log('Websocket 연결'); + subscribe(); + setClient(client); + }, + }); + + // 에러 메세지 + client.onStompError = (frame) => { + console.error('Stomp error: ' + frame.headers['message']); + console.error('Additional details: ' + frame.body); + }; + + client.onWebSocketError = (event) => { + console.error('WebSocket Error:', event); + }; + + client.onWebSocketClose = (event) => { + console.error('WebSocket Closed:', event); + }; + + client.activate(); + }; + + // callback 함수 수정 + const subscribe = () => { + client?.subscribe( + `/sub/ws/${chatRoomId}`, + (message: IMessage) => { + if (message.body) { + try { + const messageData: ChatMessageResponse = JSON.parse(message.body); + callback(messageData); + } catch (error) { + console.error('Error:', error); + } + } + }, + { + Authorization: `Bearer ${accessToken}`, + }, + ); + console.log('socket 구독'); + }; + + const disconnect = () => { + client?.deactivate(); + setClient(null); + console.log('WebSocket 연결 종료'); + }; + + const sendMessage = (destination: string, content: ChatMessageRequest) => { + if (client && client.connected) { + client.publish({ destination, body: JSON.stringify(content) }); + console.log('메세지 전송'); + } else { + console.error('WebSocket 연결 애러'); + } + }; + + return { connect, disconnect, sendMessage }; +}; + +export default useStomp; diff --git a/src/pages/chatModal/hooks/useWebSocket.ts b/src/pages/chatModal/hooks/useWebSocket.ts deleted file mode 100644 index 94b8ff1..0000000 --- a/src/pages/chatModal/hooks/useWebSocket.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Client, Message, StompConfig } from '@stomp/stompjs'; -import { ChatMessageResponse } from '@/types/chat'; -import { memberStore } from '@/stores/member'; -import SockJS from 'sockjs-client'; - -const useWebSocket = ( - chatRoomId: number, - onMessage: (msg: ChatMessageResponse) => void, -) => { - const [stompClient, setStompClient] = useState(null); - const { accessToken } = memberStore.getState(); - - useEffect(() => { - if (!accessToken) { - console.error('No access token available'); - return; - } - const soket = new SockJS(`${import.meta.env.VITE_SOCKET_URL}`); - const client = new Client({ - webSocketFactory: () => soket, - - reconnectDelay: 5000, - onConnect: () => { - console.log('Connected to WebSocket'); - client.subscribe(`/sub/ws/${chatRoomId}`, (message: Message) => { - console.log(`Received: ${message.body}`); - const msg: ChatMessageResponse = JSON.parse(message.body); - onMessage(msg); - }); - }, - //에러 메세지 - onStompError: (frame) => { - console.error('Broker reported error: ' + frame.headers['message']); - console.error('Additional details: ' + frame.body); - }, - onWebSocketError: (event) => { - console.error('WebSocket Error:', event.target); - }, - onWebSocketClose: (event) => { - console.error('WebSocket Closed:', event); - }, - } as StompConfig); - - client.activate(); - setStompClient(client); - - return () => { - if (client) { - client.deactivate(); - setStompClient(null); - } - }; - }, [accessToken, chatRoomId, onMessage]); - - return stompClient; -}; - -export default useWebSocket; diff --git a/src/pages/payment/components/index.ts b/src/pages/payment/components/index.ts index 0fa2f4c..4f0ca2f 100644 --- a/src/pages/payment/components/index.ts +++ b/src/pages/payment/components/index.ts @@ -2,3 +2,4 @@ export { default as OrderBox } from './sOrder'; export { default as DiscountBox } from './sDiscount'; export { default as PaymentTypeBox } from './sPaymentType'; export { default as TotalCostBox } from './sTotalCost'; +export { default as CouponModal } from './couponModal'; diff --git a/src/pages/payment/components/sDiscount/index.tsx b/src/pages/payment/components/sDiscount/index.tsx index ae22eb8..dfc9fd8 100644 --- a/src/pages/payment/components/sDiscount/index.tsx +++ b/src/pages/payment/components/sDiscount/index.tsx @@ -1,6 +1,10 @@ +import CouponPortal from '@/components/CouponPortal'; + import * as S from './styles'; +import { couponStore } from '@/stores/modal'; const DiscountBox = () => { + const open = couponStore((state) => state.open); return ( @@ -8,8 +12,9 @@ const DiscountBox = () => { 현재 적용한 쿠폰이 없습니다. - 사용 + 사용 + ); };