Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 채팅을 웹소켓으로 교체한다 #138

Merged
merged 1 commit into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@stomp/stompjs": "^7.0.0",
"@tanstack/react-query": "^5.32.0",
"axios": "^1.6.8",
"event-source-polyfill": "^1.0.31",
Expand Down
8 changes: 4 additions & 4 deletions src/axios/http.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSuspenseQuery } from '@tanstack/react-query';

import {
Chat,
ChatArray,
ChatRequest,
DeadResult,
GameExist,
Expand Down Expand Up @@ -45,8 +45,8 @@ export const useChatsQuery = () => {
const { data: chats, ...rest } = useSuspenseQuery({
queryKey: ['chats', localStorage.getItem('auth')],
queryFn: () => getChats(),
refetchInterval: 500,
staleTime: 500,
// refetchInterval: 500,
// staleTime: 500,
});
return {
chats,
Expand Down Expand Up @@ -85,7 +85,7 @@ export const existGame = () => {
};

export const getChats = () => {
return http.get<Chat[]>(`/chat`);
return http.get<ChatArray>(`/v2/chat`);
};

export const postChats = (payload: ChatRequest) => {
Expand Down
3 changes: 2 additions & 1 deletion src/axios/instances.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

export const BASE_URL = 'https://dev.mafia-together.com/api';
export const DOMAIN = 'dev.mafia-together.com';
export const BASE_URL = `https://${DOMAIN}/api`;

export const axiosInstance = axios.create({
baseURL: BASE_URL,
Expand Down
45 changes: 45 additions & 0 deletions src/components/chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import { useRecoilState } from 'recoil';

import { roomInfoState } from '../../recoil/roominfo/atom';
import { VariablesCSS } from '../../styles/VariablesCSS';
import { ChatArray } from '../../type';
import { ChatForm } from './ChatForm';
import { Chats } from './Chats';

type PropsType = {
publishChat: (content: string) => void;
chats: ChatArray;
setChats: React.Dispatch<React.SetStateAction<ChatArray>>;
};

export const Chat = ({ publishChat, chats, setChats }: PropsType) => {
/* 방 정보 */
const [roomInfo] = useRecoilState(roomInfoState);

// 내가 살아있는지
const isAlive = roomInfo?.isAlive;

return (
<>
{/* 채팅목록 */}
<div css={middle}>
<Chats chats={chats} setChats={setChats} />
</div>

{/* 살아있는 경우에만 input창이 보인다. */}
{isAlive && <ChatForm publishChat={publishChat} />}
</>
);
};

const middle = css`
height: calc(100% - ${VariablesCSS.top} - 55px - 20px);
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
`;
10 changes: 7 additions & 3 deletions src/components/chat/ChatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@
import { css } from '@emotion/react';
import { useState } from 'react';

import { postChats } from '../../axios/http';
import { VariablesCSS } from '../../styles/VariablesCSS';

function isInvalidInputChat(inputChat: string) {
inputChat = inputChat.trim();
return inputChat.length === 0;
}

export const ChatForm = () => {
type PropsType = {
publishChat: (content: string) => void;
};

export const ChatForm = ({ publishChat }: PropsType) => {
const [inputChat, setInputChat] = useState<string>('');
return (
<form
css={chatForm}
onSubmit={event => {
event.preventDefault();
postChats({ contents: inputChat });
publishChat(inputChat);
// postChats({ content: inputChat });
setInputChat('');
}}
>
Expand Down
8 changes: 6 additions & 2 deletions src/components/chat/ChatGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ export default forwardRef(function ChatGroup(props: PropsType, ref: any) {
<PlayerChat job={chats[0].job} />
<div css={right(props)}>
<p css={nameText}>{chats[0].name}</p>
{chats.map(chat => (
<ChatMessage contents={chat.contents} isOwner={chat.isOwner} key={`${chat.timestamp}`} />
{chats.map((chat, idx) => (
<ChatMessage
contents={chat.content}
isOwner={chat.isOwner}
key={`${chat.timeStamp} ${idx}`}
/>
))}
</div>
</div>
Expand Down
11 changes: 7 additions & 4 deletions src/components/chat/Chats.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useEffect, useRef } from 'react';

import { useChatsQuery } from '../../axios/http';
import { Chat } from '../../type';
import { Chat, ChatArray } from '../../type';
import ChatGroup from './ChatGroup';

export const Chats = () => {
type PropsType = {
chats: ChatArray;
setChats: React.Dispatch<React.SetStateAction<ChatArray>>;
};

export const Chats = ({ chats }: PropsType) => {
const chatRef = useRef<HTMLDivElement | null>(null);

const { chats } = useChatsQuery();
useEffect(() => {
if (!chatRef.current) return;
chatRef.current.scrollIntoView({ block: 'end' });
Expand Down
27 changes: 7 additions & 20 deletions src/pages/Day.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { css } from '@emotion/react';
import { Suspense, useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';

import { ChatForm } from '../components/chat/ChatForm';
import { Chats } from '../components/chat/Chats';
import { Chat } from '../components/chat/Chat';
import { Loading } from '../components/etc/Loading';
import AppContainerCSS from '../components/layout/AppContainerCSS';
import ModalContainer from '../components/modal/ModalContainer';
Expand All @@ -16,13 +15,16 @@ import VoteResult from '../components/modal/VoteResult';
import TopDay from '../components/top/TopDay';
import { gameRound, roomInfoState } from '../recoil/roominfo/atom';
import { VariablesCSS } from '../styles/VariablesCSS';
import { Status } from '../type';
import { ChatArray, Status } from '../type';

type PropsType = {
statusType: Status;
publishChat: (content: string) => void;
chats: ChatArray;
setChats: React.Dispatch<React.SetStateAction<ChatArray>>;
};

export default function Day({ statusType }: PropsType) {
export default function Day({ statusType, publishChat, chats, setChats }: PropsType) {
// 라운드 (몇일차)
const [gameRoundState] = useRecoilState(gameRound);

Expand Down Expand Up @@ -64,12 +66,7 @@ export default function Day({ statusType }: PropsType) {
statusType={statusType}
/>

<div css={middle}>
<Chats />
</div>

{/* 살아있는 경우에만 input창이 보인다. */}
{isAlive && <ChatForm />}
<Chat publishChat={publishChat} chats={chats} setChats={setChats} />

{/* 공지 모달 TIME*/}
<ModalContainer isOpen={statusType === 'NOTICE'}>
Expand Down Expand Up @@ -131,13 +128,3 @@ const gameMessage = css`
color: ${VariablesCSS.day};
animation: smoothshow 0.8s;
`;

const middle = css`
height: calc(100% - ${VariablesCSS.top} - 55px - 20px);
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
`;
97 changes: 90 additions & 7 deletions src/pages/Game.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import * as StompJs from '@stomp/stompjs';
import { EventListener, EventSourcePolyfill } from 'event-source-polyfill';
import { useEffect, useRef, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { useRecoilState, useSetRecoilState } from 'recoil';

import { getGamesInfo } from '../axios/http';
import { getChats, getGamesInfo } from '../axios/http';
import { BASE_URL } from '../axios/instances';
import { gameRound, roomInfoState } from '../recoil/roominfo/atom';
import { ChatArray, ChatResponse, GameStatus } from '../type';
import Day from './Day';
import Night from './Night';
import Result from './Result';
import WaitingRoom from './WaitingRoom';

export default function Game() {
const auth = localStorage.getItem('auth');
const [chats, setChats] = useState<ChatArray>([]);
const socketClientState = useRef<StompJs.Client | null>(null);
const [chatSubscribeId, setChatSubscribeId] = useState<StompJs.StompSubscription | null>(null);
const [roomsInfoState, setRoomsInfoState] = useRecoilState(roomInfoState); // 방 정보
const setGameRoundState = useSetRecoilState(gameRound);

// 방 상태 불러오기
const [gamesStatus, setGameStatus] = useState({ statusType: 'WAIT' });
const [gamesStatus, setGameStatus] = useState<GameStatus>({ statusType: 'WAIT' });

// SSE
const eventSource = useRef<EventSourcePolyfill | null>(null);
Expand All @@ -37,10 +46,77 @@ export default function Game() {
};
}, []);

// 방 정보 저장 (방 상태가 바뀔때만 작동?)
const setRoomsInfoState = useSetRecoilState(roomInfoState); // 방 정보
// WebSocket
const connect = () => {
const socket = new StompJs.Client({
brokerURL: `wss://dev.mafia-together.com/api/stomp`,
reconnectDelay: 10000,
});

const setGameRoundState = useSetRecoilState(gameRound);
if (!socket.active) {
socket.activate();
}

socketClientState.current = socket;
};

// 채팅구독
const subscribeChat = () => {
if (!socketClientState.current?.connected) return;

const chatSubscribeId = socketClientState.current.subscribe(`/sub/chat/${auth}`, response => {
const msg: ChatResponse = JSON.parse(response.body);

const isOwner = msg.name == roomsInfoState.myName;
setChats(chats => [...chats, { ...msg, isOwner: isOwner }]);
});

setChatSubscribeId(chatSubscribeId);
};

// 채팅구독끊기
const unsubscribeChat = () => {
if (!socketClientState.current?.connected) return;
chatSubscribeId?.unsubscribe();
};

// 채팅보내기
const publishChat = (content: string) => {
if (!socketClientState.current?.connected) return;

socketClientState.current.publish({
destination: `/pub/chat/${auth}`,
body: JSON.stringify({ content: content }),
});
};

const disConnect = () => {
socketClientState.current?.deactivate();
};

// 웹소켓 연결
useEffect(() => {
connect();
return () => disConnect();
}, []);

// 채팅구독
useEffect(() => {
if (gamesStatus.statusType === 'DAY') {
subscribeChat();
}
return () => unsubscribeChat();
}, [gamesStatus.statusType]);

// 본래 채팅불러오기
useEffect(() => {
(async () => {
const response = await getChats();
setChats(response);
})();
}, []);

// 방 정보 저장 (방 상태가 바뀔때만 작동?)
useEffect(() => {
// 방 정보 불러오기
(async () => {
Expand All @@ -63,7 +139,14 @@ export default function Game() {
gamesStatus.statusType === 'NOTICE' ||
gamesStatus.statusType === 'DAY' ||
gamesStatus.statusType === 'VOTE' ||
gamesStatus.statusType === 'VOTE_RESULT') && <Day statusType={gamesStatus.statusType} />}
gamesStatus.statusType === 'VOTE_RESULT') && (
<Day
statusType={gamesStatus.statusType}
publishChat={publishChat}
chats={chats}
setChats={setChats}
/>
)}
{(gamesStatus.statusType === 'NIGHT_INTRO' || gamesStatus.statusType === 'NIGHT') && (
<Night statusType={gamesStatus.statusType} />
)}
Expand Down
3 changes: 2 additions & 1 deletion src/pages/WaitingRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Toaster } from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';

import { getRoomsCode, startGame, useGamesInfoQuery } from '../axios/http';
import { DOMAIN } from '../axios/instances';
import BigButton from '../components/button/BigButton';
import { Loading } from '../components/etc/Loading';
import AppContainerCSS from '../components/layout/AppContainerCSS';
Expand Down Expand Up @@ -66,7 +67,7 @@ export default function WaitingRoom() {

const onShareLink = async () => {
// 링크 공유s
const inviteLink = 'https://dev.mafia-together.com/api' + '/#/participate?code=' + code;
const inviteLink = DOMAIN + '/#/participate?code=' + code;
const shareData = {
url: inviteLink,
};
Expand Down
2 changes: 1 addition & 1 deletion src/recoil/roominfo/atom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { atom } from 'recoil';

import { Job, GameInfo } from '../../type';
import { GameInfo, Job } from '../../type';

export const gameRound = atom({
key: 'gameRound',
Expand Down
Loading
Loading