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 1/6] =?UTF-8?q?=EC=B1=84=ED=8C=85=20qa=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#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; }; From 31fbc2da79d4281c87a30246b111a1bcfa752ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A7=84=EC=9A=B1?= Date: Mon, 27 Nov 2023 16:07:31 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=ED=81=AC=EB=A3=A8=20=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=EC=97=AC=EB=B6=80,=20=EA=B2=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=AA=A8=EC=A7=91=20=EC=B0=B8=EC=97=AC=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=EC=97=AC=EB=B6=80=20=EA=B4=80=EB=A0=A8=20api=20?= =?UTF-8?q?=EB=B0=8F=20ui=20=EC=9E=91=EC=97=85=20(#312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 크루 참여신청 여부 관련 api 및 ui 반영 * feat: 게스트 모집 참여신청 여부 관련 api 및 ui 반영 * feat: 참여 신청 완료 시 토스트 기능 --- src/api/member/getCrewRegistrationStatus.ts | 17 +++++++ src/api/member/getGameRegistrationStatus.ts | 17 +++++++ .../useCrewParticipateCreateMutation.ts | 16 +++++- .../useGameParticipateCreateMutation.ts | 16 +++++- .../queries/useCrewRegistrationStatusQuery.ts | 15 ++++++ .../queries/useGameRegistrationStatusQuery.ts | 15 ++++++ src/pages/CrewsDetailPage/CrewsDetailPage.tsx | 42 ++++++++-------- .../CrewsDetailPage/ParticipateButton.tsx | 34 +++++++++++++ src/pages/GamesDetailPage/GamesDetailPage.tsx | 49 ++++++++++--------- .../GamesDetailPage/ParticipateButton.tsx | 34 +++++++++++++ src/type/api/member.ts | 14 ++++++ 11 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 src/api/member/getCrewRegistrationStatus.ts create mode 100644 src/api/member/getGameRegistrationStatus.ts create mode 100644 src/hooks/queries/useCrewRegistrationStatusQuery.ts create mode 100644 src/hooks/queries/useGameRegistrationStatusQuery.ts create mode 100644 src/pages/CrewsDetailPage/ParticipateButton.tsx create mode 100644 src/pages/GamesDetailPage/ParticipateButton.tsx diff --git a/src/api/member/getCrewRegistrationStatus.ts b/src/api/member/getCrewRegistrationStatus.ts new file mode 100644 index 00000000..0176b71f --- /dev/null +++ b/src/api/member/getCrewRegistrationStatus.ts @@ -0,0 +1,17 @@ +import { axiosInstance } from '@api/axiosInstance'; + +import { + GetCrewRegistrationStatusRequest, + GetRegistrationStatusResponse, +} from '@type/api/member'; + +export const getCrewRegistrationStatus = async ({ + memberId, + crewId, +}: GetCrewRegistrationStatusRequest) => { + const { data } = await axiosInstance.get( + `/members/${memberId}/crews/${crewId}/registration-status` + ); + + return data; +}; diff --git a/src/api/member/getGameRegistrationStatus.ts b/src/api/member/getGameRegistrationStatus.ts new file mode 100644 index 00000000..453ca3e2 --- /dev/null +++ b/src/api/member/getGameRegistrationStatus.ts @@ -0,0 +1,17 @@ +import { axiosInstance } from '@api/axiosInstance'; + +import { + GetGameRegistrationStatusRequest, + GetRegistrationStatusResponse, +} from '@type/api/member'; + +export const getGameRegistrationStatus = async ({ + memberId, + gameId, +}: GetGameRegistrationStatusRequest) => { + const { data } = await axiosInstance.get( + `/members/${memberId}/games/${gameId}/registration-status` + ); + + return data; +}; diff --git a/src/hooks/mutations/useCrewParticipateCreateMutation.ts b/src/hooks/mutations/useCrewParticipateCreateMutation.ts index 00bd452c..f394c7dd 100644 --- a/src/hooks/mutations/useCrewParticipateCreateMutation.ts +++ b/src/hooks/mutations/useCrewParticipateCreateMutation.ts @@ -1,9 +1,23 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { postCrewParticipate } from '@api/crews/postCrewParticipate'; +import { useLoginInfoStore } from '@stores/loginInfo.store'; + export const useCrewParticipateCreateMutation = () => { + const queryClient = useQueryClient(); + const id = useLoginInfoStore((state) => state.loginInfo?.id); + return useMutation({ mutationFn: postCrewParticipate, + onSuccess: (_, { crewId }) => { + queryClient.invalidateQueries({ + queryKey: ['crew-detail', crewId], + }); + id && + queryClient.invalidateQueries({ + queryKey: ['crew-registration', id, crewId], + }); + }, }); }; diff --git a/src/hooks/mutations/useGameParticipateCreateMutation.ts b/src/hooks/mutations/useGameParticipateCreateMutation.ts index 0dfa78f1..ea7e8914 100644 --- a/src/hooks/mutations/useGameParticipateCreateMutation.ts +++ b/src/hooks/mutations/useGameParticipateCreateMutation.ts @@ -1,9 +1,23 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { postGameParticipate } from '@api/games/postGameParticipate'; +import { useLoginInfoStore } from '@stores/loginInfo.store'; + export const useGameParticipateCreateMutation = () => { + const queryClient = useQueryClient(); + const id = useLoginInfoStore((state) => state.loginInfo?.id); + return useMutation({ mutationFn: postGameParticipate, + onSuccess: (_, { gameId }) => { + queryClient.invalidateQueries({ + queryKey: ['game-detail', gameId], + }); + id && + queryClient.invalidateQueries({ + queryKey: ['game-registration', id, gameId], + }); + }, }); }; diff --git a/src/hooks/queries/useCrewRegistrationStatusQuery.ts b/src/hooks/queries/useCrewRegistrationStatusQuery.ts new file mode 100644 index 00000000..759b4bd8 --- /dev/null +++ b/src/hooks/queries/useCrewRegistrationStatusQuery.ts @@ -0,0 +1,15 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { getCrewRegistrationStatus } from '@api/member/getCrewRegistrationStatus'; + +import { GetCrewRegistrationStatusRequest } from '@type/api/member'; + +export const useCrewRegistrationStatusQuery = ({ + memberId, + crewId, +}: GetCrewRegistrationStatusRequest) => { + return useSuspenseQuery({ + queryKey: ['crew-registration', memberId, crewId], + queryFn: () => getCrewRegistrationStatus({ memberId, crewId }), + }); +}; diff --git a/src/hooks/queries/useGameRegistrationStatusQuery.ts b/src/hooks/queries/useGameRegistrationStatusQuery.ts new file mode 100644 index 00000000..109e64ad --- /dev/null +++ b/src/hooks/queries/useGameRegistrationStatusQuery.ts @@ -0,0 +1,15 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { getGameRegistrationStatus } from '@api/member/getGameRegistrationStatus'; + +import { GetGameRegistrationStatusRequest } from '@type/api/member'; + +export const useGameRegistrationStatusQuery = ({ + memberId, + gameId, +}: GetGameRegistrationStatusRequest) => { + return useSuspenseQuery({ + queryKey: ['game-registration', memberId, gameId], + queryFn: () => getGameRegistrationStatus({ memberId, gameId }), + }); +}; diff --git a/src/pages/CrewsDetailPage/CrewsDetailPage.tsx b/src/pages/CrewsDetailPage/CrewsDetailPage.tsx index dbf8efd8..261e26f5 100644 --- a/src/pages/CrewsDetailPage/CrewsDetailPage.tsx +++ b/src/pages/CrewsDetailPage/CrewsDetailPage.tsx @@ -1,7 +1,7 @@ +import { ErrorBoundary } from 'react-error-boundary'; +import toast from 'react-hot-toast'; import { useNavigate, useParams } from 'react-router-dom'; -import { useQueryClient } from '@tanstack/react-query'; - import { Avatar } from '@components/Avatar'; import { Header } from '@components/Header'; import { InfoItem } from '@components/InfoItem'; @@ -35,6 +35,7 @@ import { PageWrapper, ProfileImage, } from './CrewsDetailPage.styles'; +import { ParticipateButton } from './ParticipateButton'; export const CrewsDetailPage = () => { const { id } = useParams(); @@ -43,7 +44,6 @@ export const CrewsDetailPage = () => { } const loginInfo = useLoginInfoStore((state) => state.loginInfo); - const queryClient = useQueryClient(); const { data: crew } = useCrewDetailQuery({ crewId: Number(id) }); const { mutate: participateMutate } = useCrewParticipateCreateMutation(); const navigate = useNavigate(); @@ -60,12 +60,6 @@ export const CrewsDetailPage = () => { crew.leader.id !== loginInfo.id && crew.members.every((member) => member.id !== loginInfo.id); - const onParticipateSuccess = () => { - queryClient.invalidateQueries({ - queryKey: ['crew-detail', crew.id], - }); - }; - return (
@@ -144,19 +138,25 @@ export const CrewsDetailPage = () => { )} {renderParticipateButton && ( - + + participateMutate( + { crewId: crew.id }, + { + onSuccess: () => { + toast('가입 신청되었습니다'); + }, + } + ) + } + /> + )} diff --git a/src/pages/CrewsDetailPage/ParticipateButton.tsx b/src/pages/CrewsDetailPage/ParticipateButton.tsx new file mode 100644 index 00000000..b818459a --- /dev/null +++ b/src/pages/CrewsDetailPage/ParticipateButton.tsx @@ -0,0 +1,34 @@ +import { Button } from '@components/shared/Button'; + +import { useCrewRegistrationStatusQuery } from '@hooks/queries/useCrewRegistrationStatusQuery'; + +import { theme } from '@styles/theme'; + +export const ParticipateButton = ({ + memberId, + crewId, + onClick, +}: { + memberId: number; + crewId: number; + onClick: VoidFunction; +}) => { + const { + data: { registrationStatus }, + } = useCrewRegistrationStatusQuery({ memberId, crewId }); + + if (registrationStatus) { + return null; + } + + return ( + + ); +}; diff --git a/src/pages/GamesDetailPage/GamesDetailPage.tsx b/src/pages/GamesDetailPage/GamesDetailPage.tsx index d09f12c7..82f82c46 100644 --- a/src/pages/GamesDetailPage/GamesDetailPage.tsx +++ b/src/pages/GamesDetailPage/GamesDetailPage.tsx @@ -1,7 +1,7 @@ +import { ErrorBoundary } from 'react-error-boundary'; +import toast from 'react-hot-toast'; import { useNavigate, useParams } from 'react-router-dom'; -import { useQueryClient } from '@tanstack/react-query'; - import { Avatar } from '@components/Avatar'; import { Header } from '@components/Header'; import { Button } from '@components/shared/Button'; @@ -38,6 +38,7 @@ import { TextContainer, UserDataWrapper, } from './GamesDetailPage.styles'; +import { ParticipateButton } from './ParticipateButton'; export const GamesDetailPage = () => { const { id } = useParams(); @@ -48,7 +49,7 @@ export const GamesDetailPage = () => { const loginInfo = useLoginInfoStore((state) => state.loginInfo); const navigate = useNavigate(); - const queryClient = useQueryClient(); + const { data: match } = useGameDetailQuery(gameId); const isMyMatch = match.host.id === loginInfo?.id; @@ -63,11 +64,6 @@ export const GamesDetailPage = () => { const isEnded = isGameEnded(startDate, match.playTimeMinutes); const { mutate: participateMutate } = useGameParticipateCreateMutation(); - const onParticipateSuccess = () => { - queryClient.invalidateQueries({ - queryKey: ['game-detail', gameId], - }); - }; const [year, month, day] = match.playDate.split('-'); const [hour, min] = match.playStartTime.split(':'); @@ -201,23 +197,28 @@ export const GamesDetailPage = () => { ))} - {loginInfo && !isStarted && canParticipate && ( - - )} + {loginInfo && !isStarted && canParticipate && ( + } + onError={() => toast.error('경기 참여여부를 불러올 수 없습니다')} + > + + participateMutate( + { gameId }, + { + onSuccess: () => { + toast('참여 신청되었습니다'); + }, + } + ) + } + /> + + )} {loginInfo && !isStarted && isMyMatch && ( + ); +}; diff --git a/src/type/api/member.ts b/src/type/api/member.ts index 2ad74e0b..f5b4ebb5 100644 --- a/src/type/api/member.ts +++ b/src/type/api/member.ts @@ -49,3 +49,17 @@ export type GetJoinedCrewsResponse = CrewProfile[]; export type GetCreatedCrewsRequest = { memberId: Member['id'] }; export type GetCreatedCrewsResponse = CrewProfile[]; + +export type GetCrewRegistrationStatusRequest = { + memberId: Member['id']; + crewId: CrewProfile['id']; +}; + +export type GetGameRegistrationStatusRequest = { + memberId: Member['id']; + gameId: Game['id']; +}; + +export type GetRegistrationStatusResponse = { + registrationStatus: boolean; +}; From 0b62cf455191dd890021deff19a16d0cac62b3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A7=84=EC=9A=B1?= Date: Mon, 27 Nov 2023 16:09:00 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=ED=81=AC=EB=A3=A8,=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=9C=A0=EB=8F=84=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=9B=84=20?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20=EC=8B=A0=EC=B2=AD=ED=95=98=EA=B8=B0)=20(#?= =?UTF-8?q?330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 크루, 게임 상세 페이지 로그인 유도 버튼 추가 * fix: 크루 상세 페이지 참여 -> 가입으로 문구 수정 --- src/pages/CrewsDetailPage/CrewsDetailPage.tsx | 10 ++++++++++ src/pages/GamesDetailPage/GamesDetailPage.tsx | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/pages/CrewsDetailPage/CrewsDetailPage.tsx b/src/pages/CrewsDetailPage/CrewsDetailPage.tsx index 261e26f5..4e9f7a2d 100644 --- a/src/pages/CrewsDetailPage/CrewsDetailPage.tsx +++ b/src/pages/CrewsDetailPage/CrewsDetailPage.tsx @@ -158,6 +158,16 @@ export const CrewsDetailPage = () => { /> )} + {loginInfo === null && ( + + )} diff --git a/src/pages/GamesDetailPage/GamesDetailPage.tsx b/src/pages/GamesDetailPage/GamesDetailPage.tsx index 82f82c46..25eb3eeb 100644 --- a/src/pages/GamesDetailPage/GamesDetailPage.tsx +++ b/src/pages/GamesDetailPage/GamesDetailPage.tsx @@ -243,6 +243,16 @@ export const GamesDetailPage = () => { 리뷰 남기기 )} + {loginInfo === null && ( + + )} From c331de2da19c7fdb840f6d99736609a41be48374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A7=84=EC=9A=B1?= Date: Mon, 27 Nov 2023 16:09:17 +0900 Subject: [PATCH 4/6] =?UTF-8?q?style:=20=EB=82=B4=20=EA=B7=BC=EC=B2=98?= =?UTF-8?q?=EC=9D=98=20=EA=B2=BD=EA=B8=B0=20=EB=AC=B8=EC=9E=90=EC=97=B4?= =?UTF-8?q?=EC=9D=84=20=EB=82=B4=20=EA=B7=BC=EC=B2=98=20=EA=B2=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A7=A4=EC=B9=98=EB=A1=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#332)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MainPage/MainPage.loading.tsx | 2 +- src/pages/MainPage/MainPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/MainPage/MainPage.loading.tsx b/src/pages/MainPage/MainPage.loading.tsx index 3724f71f..038b3d45 100644 --- a/src/pages/MainPage/MainPage.loading.tsx +++ b/src/pages/MainPage/MainPage.loading.tsx @@ -12,7 +12,7 @@ export const MainPageLoading = () => {
- +