Skip to content

Commit

Permalink
Feat: 채팅 확인 및 coupon 모달
Browse files Browse the repository at this point in the history
  • Loading branch information
kangsinbeom committed Jun 21, 2024
1 parent d0c153e commit a3dc2c6
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 109 deletions.
2 changes: 1 addition & 1 deletion src/apis/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
20 changes: 20 additions & 0 deletions src/components/CouponPortal.tsx
Original file line number Diff line number Diff line change
@@ -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(<div>{open && <CouponModal close={close} />}</div>, $portal_root)
: null}
</>
);
};

export default CouponPortal;
10 changes: 9 additions & 1 deletion src/components/NotificationPortal.tsx
Original file line number Diff line number Diff line change
@@ -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(<Notification />, $portal_root) : null}</>;
return (
<>
{$portal_root
? createPortal(<div>{isLogin && <Notification />}</div>, $portal_root)
: null}
</>
);
};

export default NotificationPortal;
11 changes: 8 additions & 3 deletions src/hooks/useTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
80 changes: 36 additions & 44 deletions src/pages/chatModal/components/chat/ChatMenu.tsx
Original file line number Diff line number Diff line change
@@ -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<ChatMessageResponse[]>([]);
const [newMessage, setNewMessage] = useState<string>('');
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<HTMLDivElement>(null);
Expand All @@ -20,52 +32,35 @@ 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('');
};

// 메세지 추가될 때
useEffect(() => {
scrollToBottom();
}, [messages]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewMessage(e.target.value);
};

Expand All @@ -79,15 +74,12 @@ const ChatMenu = ({ chatRoomId }: { chatRoomId: number }) => {
profileImage={'default'}
content={msg.content}
/>
// 작가 메세지 구분 추후 추가
))}
</S.ContentBox>
<S.InputBox>
<S.Input
placeholder="채팅 입력"
value={newMessage}
onChange={handleInputChange}
/>
<Icon value="send" onClick={sendMessage} />
<S.Input placeholder="채팅 입력" value={newMessage} onChange={onInputChange} />
<Icon value="send" onClick={postMessage} />
</S.InputBox>
</S.Container>
);
Expand Down
88 changes: 88 additions & 0 deletions src/pages/chatModal/hooks/useStomp.ts
Original file line number Diff line number Diff line change
@@ -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<Client | null>(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;
59 changes: 0 additions & 59 deletions src/pages/chatModal/hooks/useWebSocket.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/pages/payment/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
7 changes: 6 additions & 1 deletion src/pages/payment/components/sDiscount/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import CouponPortal from '@/components/CouponPortal';

import * as S from './styles';
import { couponStore } from '@/stores/modal';

const DiscountBox = () => {
const open = couponStore((state) => state.open);
return (
<S.Container>
<S.Title typography="t5" bold="bold">
할인 혜택
</S.Title>
<S.Box>
<S.CouponBlock>현재 적용한 쿠폰이 없습니다.</S.CouponBlock>
<S.Button>사용</S.Button>
<S.Button onClick={open}>사용</S.Button>
</S.Box>
<CouponPortal />
</S.Container>
);
};
Expand Down

0 comments on commit a3dc2c6

Please sign in to comment.