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/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/components/Participation/Participation.style.ts b/src/components/Participation/Participation.style.ts index a30de665..e4119373 100644 --- a/src/components/Participation/Participation.style.ts +++ b/src/components/Participation/Participation.style.ts @@ -1,5 +1,7 @@ import styled from '@emotion/styled'; +import { Text } from '@components/shared/Text'; + export const ManageContainer = styled.div` ${({ theme }) => theme.STYLES.LAYOUT} min-height: 100dvh; @@ -15,3 +17,7 @@ export const AllowCardGroup = styled.div` flex-wrap: wrap; gap: 0.75rem; `; + +export const InformText = styled(Text)` + padding: 16px; +`; diff --git a/src/components/Participation/Participation.tsx b/src/components/Participation/Participation.tsx index 79a10c55..e061666a 100644 --- a/src/components/Participation/Participation.tsx +++ b/src/components/Participation/Participation.tsx @@ -4,12 +4,15 @@ import { useNavigate } from 'react-router-dom'; import { useLoginInfoStore } from '@/stores/loginInfo.store'; import { AllowCard } from '@components/Participation/components/AllowCard'; +import { Flex } from '@components/shared/Flex'; + +import { theme } from '@styles/theme'; import { Member } from '@type/models'; import { PATH_NAME } from '@consts/pathName'; -import { AllowCardGroup, Main } from './Participation.style'; +import { AllowCardGroup, InformText, Main } from './Participation.style'; type ParticipationProps = { id: Member['id']; @@ -36,7 +39,7 @@ export const Participation = ({ navigate(PATH_NAME.LOGIN); return; } - }, [id, navigate]); + }, [id, loginInfo, navigate]); const moveToProfile = (memberId: number) => { navigate(PATH_NAME.GET_PROFILE_PATH(String(memberId))); @@ -44,17 +47,25 @@ export const Participation = ({ return (
- - {waitingMembers.map(({ ...props }: Member) => ( - moveToProfile(props.id)} - onClickAllowButton={() => handleGuestAction(props.id, '확정')} - onClickDisallowButton={() => handleGuestAction(props.id, '거절')} - /> - ))} - {' '} + {waitingMembers.length > 0 ? ( + + {waitingMembers.map(({ ...props }: Member) => ( + moveToProfile(props.id)} + onClickAllowButton={() => handleGuestAction(props.id, '확정')} + onClickDisallowButton={() => handleGuestAction(props.id, '거절')} + /> + ))} + + ) : ( + + + 수락 대기중인 인원이 없습니다 + + + )}
); }; diff --git a/src/components/Participation/components/AllowCard/AllowCard.tsx b/src/components/Participation/components/AllowCard/AllowCard.tsx index 000e017c..f0f1da60 100644 --- a/src/components/Participation/components/AllowCard/AllowCard.tsx +++ b/src/components/Participation/components/AllowCard/AllowCard.tsx @@ -37,6 +37,7 @@ export const AllowCard = ({ size={40} onClick={onClickProfile} /> +   { + 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/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 77% rename from src/pages/ChatRoomListPage/useChatRoomListPage.ts rename to src/pages/ChatRoomListPage/useChatRoomList.ts index c66fcdf0..36cf5838 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 { LoginRequireError } from '@routes/LoginRequireBoundary'; @@ -9,7 +10,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); @@ -17,9 +18,9 @@ export const useChatRoomListPage = () => { throw new LoginRequireError(); } - const { chatRoomTab, setChatRoomTab } = useChatRoomTabStore(); + const { chatRoomTab } = useChatRoomTabStore(); - const { data: selectedTabChatRoomList, refetch: refetchChatList } = + const { data: selectedTabChatRoomList, refetch: refetchChatRoomList } = useAllChatRoomListQuery({ type: chatRoomTab, }); @@ -28,17 +29,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/pages/CrewsDetailPage/CrewsDetailPage.tsx b/src/pages/CrewsDetailPage/CrewsDetailPage.tsx index dbf8efd8..4e9f7a2d 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,18 +138,34 @@ export const CrewsDetailPage = () => { )} {renderParticipateButton && ( + } + onError={() => toast.error('크루 가입여부를 불러올 수 없습니다')} + > + + participateMutate( + { crewId: crew.id }, + { + onSuccess: () => { + toast('가입 신청되었습니다'); + }, + } + ) + } + /> + + )} + {loginInfo === null && ( )} 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.styles.ts b/src/pages/GamesDetailPage/GamesDetailPage.styles.ts index 35e6aabf..b151f87d 100644 --- a/src/pages/GamesDetailPage/GamesDetailPage.styles.ts +++ b/src/pages/GamesDetailPage/GamesDetailPage.styles.ts @@ -75,3 +75,21 @@ export const ButtonWrapper = styled.div` bottom: 70px; left: 0; `; + +export const PositionItemBox = styled.div` + border: ${({ theme }) => `1px solid ${theme.PALETTE.GRAY_400}`}; + box-sizing: border-box; + width: 45px; + height: 45px; + line-height: 45px; + text-align: center; + border-radius: 8px; + color: ${({ theme }) => theme.PALETTE.GRAY_900}; + font-size: ${({ theme }) => theme.FONT_SIZE.XS}; + overflow: hidden; + cursor: pointer; +`; + +export const ModalItem = styled(Flex)` + padding: 16px; +`; diff --git a/src/pages/GamesDetailPage/GamesDetailPage.tsx b/src/pages/GamesDetailPage/GamesDetailPage.tsx index d09f12c7..c01bfdbb 100644 --- a/src/pages/GamesDetailPage/GamesDetailPage.tsx +++ b/src/pages/GamesDetailPage/GamesDetailPage.tsx @@ -1,9 +1,14 @@ -import { useNavigate, useParams } from 'react-router-dom'; -import { useQueryClient } from '@tanstack/react-query'; +import { useState } from 'react'; + +import { ErrorBoundary } from 'react-error-boundary'; +import toast from 'react-hot-toast'; + +import { useNavigate, useParams } from 'react-router-dom'; import { Avatar } from '@components/Avatar'; import { Header } from '@components/Header'; +import { Modal } from '@components/Modal'; import { Button } from '@components/shared/Button'; import { Flex } from '@components/shared/Flex'; import { Image } from '@components/shared/Image'; @@ -11,12 +16,15 @@ import { Text } from '@components/shared/Text'; import { useGameParticipateCreateMutation } from '@hooks/mutations/useGameParticipateCreateMutation'; import { useGameDetailQuery } from '@hooks/queries/useGameDetailQuery'; +import { usePositionsQuery } from '@hooks/queries/usePositionsQuery'; import { useChatOnButtonClick } from '@hooks/useChatOnButtonClick'; import { theme } from '@styles/theme'; import { useLoginInfoStore } from '@stores/loginInfo.store'; +import { Position, PositionInfo } from '@type/models/Position'; + import { PATH_NAME } from '@consts/pathName'; import { WEEKDAY } from '@consts/weekday'; @@ -33,11 +41,16 @@ import { Guests, GuestsContainer, InfoItem, + ModalItem, PageContent, PageLayout, + PositionItemBox, TextContainer, UserDataWrapper, } from './GamesDetailPage.styles'; +import { ParticipateButton } from './ParticipateButton'; + +Modal; export const GamesDetailPage = () => { const { id } = useParams(); @@ -48,7 +61,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,16 +76,15 @@ export const GamesDetailPage = () => { const isEnded = isGameEnded(startDate, match.playTimeMinutes); const { mutate: participateMutate } = useGameParticipateCreateMutation(); - const onParticipateSuccess = () => { - queryClient.invalidateQueries({ - queryKey: ['game-detail', gameId], - }); - }; + const [isPositionModalOpen, setIsPositionModalOpen] = useState(false); + const { data: positions } = usePositionsQuery(); const [year, month, day] = match.playDate.split('-'); const [hour, min] = match.playStartTime.split(':'); const date = new Date(Number(year), Number(month) - 1, Number(day)); const weekday = WEEKDAY[date.getDay()]; + const [clickedPositionInfo, setClickedPositionInfo] = + useState(null); const handleClickMemberProfile = (id: number | string) => navigate(PATH_NAME.GET_PROFILE_PATH(String(id))); @@ -84,6 +96,26 @@ export const GamesDetailPage = () => { myId: loginInfo?.id ?? null, }); + const handleClickPosition = (myPosition: Position) => { + const positionInfo = positions.find( + (position) => position.acronym === myPosition + ); + + if (!positionInfo) { + return; + } + setClickedPositionInfo(positionInfo); + setIsPositionModalOpen(true); + }; + + const togglePositionModal = () => { + setIsPositionModalOpen((prev) => !prev); + }; + + const formatCost = (cost: number) => { + return String(cost).replace(/\B(?
@@ -157,12 +189,37 @@ export const GamesDetailPage = () => { }h)`} + + 선호 포지션 + + + {match.positions.map((position) => ( + handleClickPosition(position)} + > + {position} + + ))} + + + {clickedPositionInfo && ( + + + + {clickedPositionInfo.name} + + {clickedPositionInfo.description} + + + )} + 참가비 money - {`${match.cost}원`} + {`${formatCost(match.cost)}원`} 현재원 @@ -201,23 +258,28 @@ export const GamesDetailPage = () => { ))} - {loginInfo && !isStarted && canParticipate && ( - - )} + {loginInfo && !isStarted && canParticipate && ( + } + onError={() => toast.error('경기 참여여부를 불러올 수 없습니다')} + > + + participateMutate( + { gameId }, + { + onSuccess: () => { + toast('참여 신청되었습니다'); + }, + } + ) + } + /> + + )} {loginInfo && !isStarted && isMyMatch && ( )} + {loginInfo === null && ( + + )} diff --git a/src/pages/GamesDetailPage/ParticipateButton.tsx b/src/pages/GamesDetailPage/ParticipateButton.tsx new file mode 100644 index 00000000..9607ca0b --- /dev/null +++ b/src/pages/GamesDetailPage/ParticipateButton.tsx @@ -0,0 +1,34 @@ +import { Button } from '@components/shared/Button'; + +import { useGameRegistrationStatusQuery } from '@hooks/queries/useGameRegistrationStatusQuery'; + +import { theme } from '@styles/theme'; + +export const ParticipateButton = ({ + memberId, + gameId, + onClick, +}: { + memberId: number; + gameId: number; + onClick: VoidFunction; +}) => { + const { + data: { registrationStatus }, + } = useGameRegistrationStatusQuery({ memberId, gameId }); + + if (registrationStatus) { + return null; + } + + return ( + + ); +}; 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 = () => {
- +