From 962ea2aba5a4dd7ae5822452d156238685b0f453 Mon Sep 17 00:00:00 2001 From: 1g2g <87280835+1g2g@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:46:23 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B1=84=ED=8C=85=20qa=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?(#336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: navigate 전 모달 닫기 * fix: 채팅 목록 탭 전환 시 깜빡거리는 버그 해결 * feat: 채팅 목록 스켈레톤 ui 적용 * feat: 채팅방 나가기 validation 구현 * fix: api 수정사항 반영 --- src/api/chat/getPersonalChatRoomExisted.ts | 2 +- src/hooks/useChatOnButtonClick.ts | 113 +++++++----------- src/pages/ChatRoomListPage/ChatRoomList.tsx | 37 ++++++ .../ChatRoomListPage/ChatRoomListPage.tsx | 38 ++---- .../ChatRoomListPage/SkeletonChatRoomList.tsx | 31 +++++ ...ChatRoomListPage.ts => useChatRoomList.ts} | 16 ++- src/pages/ChattingPage/quitChatCondition.ts | 80 +++++++++++++ src/pages/ChattingPage/useChattingPage.ts | 19 ++- src/type/api/chat.ts | 2 +- 9 files changed, 228 insertions(+), 110 deletions(-) create mode 100644 src/pages/ChatRoomListPage/ChatRoomList.tsx create mode 100644 src/pages/ChatRoomListPage/SkeletonChatRoomList.tsx rename src/pages/ChatRoomListPage/{useChatRoomListPage.ts => useChatRoomList.ts} (76%) create mode 100644 src/pages/ChattingPage/quitChatCondition.ts diff --git a/src/api/chat/getPersonalChatRoomExisted.ts b/src/api/chat/getPersonalChatRoomExisted.ts index f75147a3..908e42bc 100644 --- a/src/api/chat/getPersonalChatRoomExisted.ts +++ b/src/api/chat/getPersonalChatRoomExisted.ts @@ -9,7 +9,7 @@ export const getPersonalChatRoomExisted = async ({ receiverId, }: GetPersonalChatRoomExistedRequest) => { const { data } = await axiosInstance.get( - '/rooms/personal/existed', + '/rooms/personal', { params: { receiver: receiverId } } ); diff --git a/src/hooks/useChatOnButtonClick.ts b/src/hooks/useChatOnButtonClick.ts index c31df0db..f88b0a56 100644 --- a/src/hooks/useChatOnButtonClick.ts +++ b/src/hooks/useChatOnButtonClick.ts @@ -1,8 +1,8 @@ import { useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; import SockJS from 'sockjs-client'; import Stomp from 'stompjs'; -import { getAllChatRoomList } from '@api/chat/getAllChatRoomList'; import { getPersonalChatRoomExisted } from '@api/chat/getPersonalChatRoomExisted'; import { @@ -19,7 +19,6 @@ import { Member } from '@type/models'; import { ChatMessage } from '@type/models/ChatMessage'; import { ChatRoom } from '@type/models/ChatRoom'; -import { CHAT_ROOM_TAB_TITLE } from '@consts/chatRoomTabTitle'; import { PATH_NAME } from '@consts/pathName'; type useChatOnButtonClickProps = { @@ -31,7 +30,6 @@ type useChatOnButtonClickProps = { export const useChatOnButtonClick = ({ targetId, - targetNickname, myId = null, navigate: moveToPage, }: useChatOnButtonClickProps) => { @@ -41,60 +39,38 @@ export const useChatOnButtonClick = ({ enabled: false, }); - const { refetch: fetchAllChatRoomList } = useQuery({ - queryKey: ['all-chat-room-list', CHAT_ROOM_TAB_TITLE.INDIVIDUAL], - queryFn: () => getAllChatRoomList({ type: CHAT_ROOM_TAB_TITLE.INDIVIDUAL }), - enabled: false, - }); - const { mutateAsync } = useCreatePersonalChatRoomMutation(); - const handleExistingRoom = async ( - individualRooms: ChatRoom[], - isSenderActive: boolean - ) => { - const { id: roomId } = - individualRooms.find( - ({ roomName }: { roomName: string }) => roomName === targetNickname - ) || {}; - if (!roomId) { - return; - } - - if (isSenderActive) { - moveToPage(PATH_NAME.GET_CHAT_PATH(String(roomId))); - } else { - const sock = new SockJS(stompConfig.webSocketEndpoint); - const stompClient = Stomp.over(sock); - - connect({ - stompClient, - connectEvent: () => { - subscribe({ - stompClient, - roomId, - subscribeEvent: (received: ChatMessage) => { - const { - type, - sender: { id: senderId }, - } = received; - - if (type === '입장' && senderId === myId) { - moveToPage(PATH_NAME.GET_CHAT_PATH(String(received.roomId))); - sock.close(); - } - }, - }); - - const sendData: SendMessageRequest = { - senderId: myId!, - content: null, - }; - - enter({ stompClient, roomId, sendData }); - }, - }); - } + const enterChatRoom = async (roomId: ChatRoom['id']) => { + const sock = new SockJS(stompConfig.webSocketEndpoint); + const stompClient = Stomp.over(sock); + + connect({ + stompClient, + connectEvent: () => { + subscribe({ + stompClient, + roomId, + subscribeEvent: (received: ChatMessage) => { + const { + type, + sender: { id: senderId }, + } = received; + + if (type === '입장' && senderId === myId) { + sock.close(); + } + }, + }); + + const sendData: SendMessageRequest = { + senderId: myId!, + content: null, + }; + + enter({ stompClient, roomId, sendData }); + }, + }); }; const handleClickChattingButton = async () => { @@ -103,21 +79,24 @@ export const useChatOnButtonClick = ({ return; } - const { data } = await fetchPersonalChatRoomExisted(); - if (!data) return; + const { data, error } = await fetchPersonalChatRoomExisted(); - const { isRoomExisted, isSenderActive } = data; + if (data) { + const { roomId } = data; + + await enterChatRoom(roomId); - if (isRoomExisted) { - const { data: individualRooms } = await fetchAllChatRoomList(); - if (individualRooms) { - handleExistingRoom(individualRooms, isSenderActive); - } - } else { - const { id: roomId } = await mutateAsync({ - receiverId: targetId, - }); moveToPage(PATH_NAME.GET_CHAT_PATH(String(roomId))); + } else { + if (error instanceof AxiosError) { + if (error.response?.data.code === 'CHT-003') { + const { id: roomId } = await mutateAsync({ + receiverId: targetId, + }); + + moveToPage(PATH_NAME.GET_CHAT_PATH(String(roomId))); + } + } } }; diff --git a/src/pages/ChatRoomListPage/ChatRoomList.tsx b/src/pages/ChatRoomListPage/ChatRoomList.tsx new file mode 100644 index 00000000..ebf57a67 --- /dev/null +++ b/src/pages/ChatRoomListPage/ChatRoomList.tsx @@ -0,0 +1,37 @@ +import { Flex } from '@components/shared/Flex'; + +import { theme } from '@styles/theme.ts'; + +import { ChatRoom } from '@type/models/ChatRoom.ts'; + +import { PATH_NAME } from '@consts/pathName.ts'; + +import { ChatRoomItem } from './ChatRoomItem.tsx'; +import { InformText } from './ChatRoomListPage.style.ts'; +import { useChatRoomList } from './useChatRoomList.ts'; + +export const ChatRoomList = () => { + const { selectedTabChatRoomList, moveToPage } = useChatRoomList(); + + return ( + <> + {selectedTabChatRoomList.length !== 0 ? ( + selectedTabChatRoomList.map((chatRoomItem: ChatRoom) => ( + + moveToPage(PATH_NAME.GET_CHAT_PATH(String(chatRoomItem.id))) + } + /> + )) + ) : ( + + + 채팅 내역이 없습니다. + + + )} + + ); +}; diff --git a/src/pages/ChatRoomListPage/ChatRoomListPage.tsx b/src/pages/ChatRoomListPage/ChatRoomListPage.tsx index a8573f9d..1a6935b4 100644 --- a/src/pages/ChatRoomListPage/ChatRoomListPage.tsx +++ b/src/pages/ChatRoomListPage/ChatRoomListPage.tsx @@ -1,26 +1,22 @@ -import { Header } from '@components/Header'; -import { Flex } from '@components/shared/Flex'; +import { Suspense } from 'react'; -import { theme } from '@styles/theme.ts'; +import { Header } from '@components/Header'; -import { ChatRoom } from '@type/models/ChatRoom.ts'; +import { useChatRoomTabStore } from '@stores/chatRoomTab.store.ts'; import { CHAT_ROOM_TAB_TITLE } from '@consts/chatRoomTabTitle.ts'; -import { PATH_NAME } from '@consts/pathName.ts'; -import { ChatRoomItem } from './ChatRoomItem.tsx'; +import { ChatRoomList } from './ChatRoomList.tsx'; import { - InformText, Main, MessagePageContainer, TabBar, TabBarButton, } from './ChatRoomListPage.style.ts'; -import { useChatRoomListPage } from './useChatRoomListPage.ts'; +import { SkeletonChatRoomList } from './SkeletonChatRoomList.tsx'; export const ChatRoomListPage = () => { - const { selectedTabChatRoomList, chatRoomTab, moveToPage, handleClickTab } = - useChatRoomListPage(); + const { chatRoomTab, setChatRoomTab } = useChatRoomTabStore(); return ( @@ -29,7 +25,7 @@ export const ChatRoomListPage = () => { {Object.values(CHAT_ROOM_TAB_TITLE).map((tab) => ( handleClickTab(tab)} + onClick={() => setChatRoomTab(tab)} isSelected={chatRoomTab === tab} key={tab} > @@ -37,23 +33,9 @@ export const ChatRoomListPage = () => { ))} - {selectedTabChatRoomList.length !== 0 ? ( - selectedTabChatRoomList.map((chatRoomItem: ChatRoom) => ( - - moveToPage(PATH_NAME.GET_CHAT_PATH(String(chatRoomItem.id))) - } - /> - )) - ) : ( - - - 채팅 내역이 없습니다. - - - )} + }> + + ); diff --git a/src/pages/ChatRoomListPage/SkeletonChatRoomList.tsx b/src/pages/ChatRoomListPage/SkeletonChatRoomList.tsx new file mode 100644 index 00000000..69d5037c --- /dev/null +++ b/src/pages/ChatRoomListPage/SkeletonChatRoomList.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from '@components/Skeleton'; +import { Flex } from '@components/shared/Flex'; + +import { theme } from '@styles/theme'; + +export const SkeletonChatRoomList = () => ( + + + {Array(5) + .fill(null) + .map((_, index) => ( + + + + + + + + + + + ))} + + +); diff --git a/src/pages/ChatRoomListPage/useChatRoomListPage.ts b/src/pages/ChatRoomListPage/useChatRoomList.ts similarity index 76% rename from src/pages/ChatRoomListPage/useChatRoomListPage.ts rename to src/pages/ChatRoomListPage/useChatRoomList.ts index 6991b999..3e810228 100644 --- a/src/pages/ChatRoomListPage/useChatRoomListPage.ts +++ b/src/pages/ChatRoomListPage/useChatRoomList.ts @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAllChatRoomListQuery } from '@hooks/queries/useAllChatRoomListQuery.ts'; @@ -7,7 +8,7 @@ import { useLoginInfoStore } from '@stores/loginInfo.store'; import { ChatRoom } from '@type/models/ChatRoom.ts'; -export const useChatRoomListPage = () => { +export const useChatRoomList = () => { const navigate = useNavigate(); const loginInfo = useLoginInfoStore((state) => state.loginInfo); @@ -15,9 +16,9 @@ export const useChatRoomListPage = () => { throw new Error('로그인이 필요한 서비스입니다.'); } - const { chatRoomTab, setChatRoomTab } = useChatRoomTabStore(); + const { chatRoomTab } = useChatRoomTabStore(); - const { data: selectedTabChatRoomList, refetch: refetchChatList } = + const { data: selectedTabChatRoomList, refetch: refetchChatRoomList } = useAllChatRoomListQuery({ type: chatRoomTab, }); @@ -26,17 +27,14 @@ export const useChatRoomListPage = () => { navigate(pathName); }; - const handleClickTab = (tab: ChatRoom['type']) => { - setChatRoomTab(tab); - - refetchChatList(); - }; + useEffect(() => { + refetchChatRoomList(); + }, [refetchChatRoomList]); return { selectedTabChatRoomList: sortDate(selectedTabChatRoomList), chatRoomTab, moveToPage, - handleClickTab, }; }; diff --git a/src/pages/ChattingPage/quitChatCondition.ts b/src/pages/ChattingPage/quitChatCondition.ts new file mode 100644 index 00000000..c02ee261 --- /dev/null +++ b/src/pages/ChattingPage/quitChatCondition.ts @@ -0,0 +1,80 @@ +import toast from 'react-hot-toast'; + +import { useQuery } from '@tanstack/react-query'; + +import { getConfirmedGames } from '@api/member/getConfirmedGames'; +import { getJoinedCrews } from '@api/member/getJoinedCrews'; + +import { Crew, Game, Member } from '@type/models'; +import { ChatRoom } from '@type/models/ChatRoom'; + +import { getGameStartDate, isGameEnded } from '@utils/domain'; + +type UseQuitConditionProps = { + myId: Member['id']; + type: ChatRoom['type']; + domainId: Game['id'] | Crew['id']; +}; + +export const useQuitCondition = ({ + myId, + type, + domainId, +}: UseQuitConditionProps) => { + const { refetch: fetchJoinedCrews } = useQuery({ + queryKey: ['joined-crews', myId, '확정'], + queryFn: () => getJoinedCrews({ memberId: myId, status: '확정' }), + enabled: false, + }); + + const { refetch: fetchConfirmedGames } = useQuery({ + queryKey: ['confirmed-games', myId], + queryFn: () => getConfirmedGames({ memberId: myId }), + enabled: false, + }); + + const isChatroomExitAllowed = async () => { + if (type === '크루') { + const { data: crews } = await fetchJoinedCrews(); + if (!crews) { + toast.error('크루 정보를 불러오는 데 실패했습니다. 다시 시도해주세요.'); + return false; + } + + const belongToCrew = crews.some((crew) => crew.id === domainId); + + if (belongToCrew) { + toast.error( + '크루에 속한 크루원은 채팅방을 나갈 수 없습니다. 크루를 탈퇴하고 다시 시도해주세요.' + ); + return false; + } + + return !belongToCrew; + } else if (type === '게스트') { + const { data: confirmedGames } = await fetchConfirmedGames(); + if (!confirmedGames) { + toast.error('게임 정보를 불러오는 데 실패했습니다. 다시 시도해주세요.'); + return false; + } + + const game = confirmedGames.find((crew) => crew.id === domainId); + if (!game) { + return true; + } + + const startTime = getGameStartDate(game.playDate, game.playStartTime); + + const allowToQuit = isGameEnded(startTime, game.playTimeMinutes); + + if (!allowToQuit) { + toast.error('게임이 종료된 이후에 채팅방을 나갈 수 있습니다.'); + } + + return allowToQuit; + } + + return true; + }; + return { isChatroomExitAllowed }; +}; diff --git a/src/pages/ChattingPage/useChattingPage.ts b/src/pages/ChattingPage/useChattingPage.ts index c283df24..a5271765 100644 --- a/src/pages/ChattingPage/useChattingPage.ts +++ b/src/pages/ChattingPage/useChattingPage.ts @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { flushSync } from 'react-dom'; import { useNavigate, useParams } from 'react-router-dom'; import SockJS from 'sockjs-client'; @@ -17,6 +18,7 @@ import { PATH_NAME } from '@consts/pathName'; import { convertUTCToKoreanTime } from '@utils/convertUTCToKoreanTime'; +import { useQuitCondition } from './quitChatCondition'; import { connect, leave, send, stompConfig, subscribe } from './stompApi'; export const useChattingPage = () => { @@ -33,12 +35,17 @@ export const useChattingPage = () => { const { data: prevChatMessages } = useAllChatMessagesQuery({ roomId: Number(roomId), }); - const myId = useLoginInfoStore((state) => state.loginInfo?.id); if (!myId || !roomDetails.members.find(({ id }) => id === myId)) { throw new Error('채팅방의 멤버가 아닙니다.'); } + const { isChatroomExitAllowed } = useQuitCondition({ + myId, + type: roomDetails.type, + domainId: roomDetails.domainId, + }); + const [sock, setSock] = useState(null); const [stompClient, setStompClient] = useState(null); const [chatMessages, setChatMessages] = useState(prevChatMessages); @@ -139,7 +146,7 @@ export const useChattingPage = () => { inputRef.current.value = ''; }; - const quitChatting = () => { + const quitChatting = async () => { if (!stompClient) { return; } @@ -149,9 +156,13 @@ export const useChattingPage = () => { content: null, }; - leave({ stompClient, roomId, sendData }); + const exitCondition = await isChatroomExitAllowed(); - navigate(PATH_NAME.CHAT); + if (exitCondition) { + leave({ stompClient, roomId, sendData }); + flushSync(() => setIsModalOpen(false)); + navigate(PATH_NAME.CHAT, { replace: true }); + } }; const handleClickChattingMenu = () => { diff --git a/src/type/api/chat.ts b/src/type/api/chat.ts index c3d882fb..6a4cd119 100644 --- a/src/type/api/chat.ts +++ b/src/type/api/chat.ts @@ -12,7 +12,7 @@ export type GetPersonalChatRoomExistedRequest = { }; export type GetPersonalChatRoomExistedResponse = { - isRoomExisted: boolean; + roomId: number; isSenderActive: boolean; };