diff --git a/src/RoomSetting/tabs/MemberTab.tsx b/src/RoomSetting/tabs/MemberTab.tsx deleted file mode 100644 index 6c27be28..00000000 --- a/src/RoomSetting/tabs/MemberTab.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const MemberTab = () => { - return <>Member 탭입니다; -}; - -export default MemberTab; diff --git a/src/RoomSetting/tabs/MemberTab/DelegationButton.tsx b/src/RoomSetting/tabs/MemberTab/DelegationButton.tsx new file mode 100644 index 00000000..ace28883 --- /dev/null +++ b/src/RoomSetting/tabs/MemberTab/DelegationButton.tsx @@ -0,0 +1,77 @@ +import { useMutation } from '@tanstack/react-query'; +import roomAPI from '@/core/api/functions/roomAPI'; +import { useMoveRoute } from '@/core/hooks'; +import { ModalHeadingStyle, descriptionStyle, errorStyle } from './styles'; +import { BottomSheet, useBottomSheet } from '@/shared/BottomSheet'; +import { LoadingSpinner } from '@/shared/LoadingSpinner'; + +interface DelegationButtonProps { + roomId: string; + memberId: string; + nickname: string; +} + +const DelegationButton = ({ + roomId, + memberId, + nickname +}: DelegationButtonProps) => { + const { bottomSheetProps, open } = useBottomSheet(); + + const { mutate, isPending, error } = useMutation({ + mutationFn: roomAPI.putDelegateMaster + }); + + const moveTo = useMoveRoute(); + + const handleDelegation = () => { + mutate( + { roomId, memberId }, + { + onSuccess: () => { + moveTo('roomDetail'); + // TODO: 토스트 메시지로 방장을 위임했음을 알려야 해요. + }, + onError: (err) => console.error(err) + } + ); + }; + + return ( + <> + + +

+ "{nickname}" 님에게 + + + 방장을 위임 + + 하시겠어요? + +

+

+ 위임 후, 관리 페이지에서 자동으로 나가집니다. +

+ {error &&

{error.response?.data.message}

} + +
+ + ); +}; + +export default DelegationButton; diff --git a/src/RoomSetting/tabs/MemberTab/KickButton.tsx b/src/RoomSetting/tabs/MemberTab/KickButton.tsx new file mode 100644 index 00000000..b4cfe646 --- /dev/null +++ b/src/RoomSetting/tabs/MemberTab/KickButton.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import roomAPI from '@/core/api/functions/roomAPI'; +import { ModalHeadingStyle, descriptionStyle } from './styles'; +import { Input } from '@/shared/Input'; +import { BottomSheet, useBottomSheet } from '@/shared/BottomSheet'; +import { LoadingSpinner } from '@/shared/LoadingSpinner'; + +interface KickButtonProps { + roomId: string; + memberId: string; + nickname: string; +} + +const KickButton = ({ roomId, memberId, nickname }: KickButtonProps) => { + const [confirmInput, setConfirmInput] = useState(''); + + const { bottomSheetProps, open, close } = useBottomSheet(); + + const { mutate, isPending, error } = useMutation({ + mutationFn: roomAPI.deleteKickUser + }); + + const handleKick = () => { + mutate( + { roomId, memberId }, + { + onSuccess: () => { + close(); + // TODO: 토스트 메시지로 사용자를 추방했음을 알려야 해요. + }, + onError: (err) => console.error(err) + } + ); + }; + + return ( + <> + + +

+ "{nickname}" 님을 + + 추방하시겠어요? + +

+

바쁜 일이 있었던 걸지도 몰라요.

+
+ + setConfirmInput(e.target.value)} + /> +
+ {error && ( +

+ {error.response?.data.message} +

+ )} + +
+ + ); +}; + +export default KickButton; diff --git a/src/RoomSetting/tabs/MemberTab/index.tsx b/src/RoomSetting/tabs/MemberTab/index.tsx new file mode 100644 index 00000000..dff17a79 --- /dev/null +++ b/src/RoomSetting/tabs/MemberTab/index.tsx @@ -0,0 +1,46 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { roomOptions } from '@/core/api/options'; +import KickButton from './KickButton'; +import DelegationButton from './DelegationButton'; +import { Avatar } from '@/shared/Avatar'; + +interface MemberTabProps { + roomId: string; +} + +const MemberTab = ({ roomId }: MemberTabProps) => { + const { data: room } = useSuspenseQuery({ + ...roomOptions.detail(roomId), + staleTime: Infinity + }); + + return ( +
+ {room.todayCertificateRank.map((member) => ( +
+ +
+ + +
+
+ ))} +
+ ); +}; + +export default MemberTab; diff --git a/src/RoomSetting/tabs/MemberTab/styles.ts b/src/RoomSetting/tabs/MemberTab/styles.ts new file mode 100644 index 00000000..88583617 --- /dev/null +++ b/src/RoomSetting/tabs/MemberTab/styles.ts @@ -0,0 +1,8 @@ +// 모달의 헤딩에 적용되는 스타일 +export const ModalHeadingStyle = 'flex flex-col text-xl mt-4 leading-7'; + +// 회색으로 간결한 설명을 적을 때 적용할 스타일 +export const descriptionStyle = 'mt-2 text-base text-dark-gray'; + +// 빨간색으로 에러를 표시할 때 적용할 스타일 +export const errorStyle = 'mt-2 text-base text-danger'; diff --git a/src/RoomSetting/tabs/RemoveTab.tsx b/src/RoomSetting/tabs/RemoveTab.tsx index 5c71ff78..0a6dc4f3 100644 --- a/src/RoomSetting/tabs/RemoveTab.tsx +++ b/src/RoomSetting/tabs/RemoveTab.tsx @@ -1,5 +1,87 @@ -const RemoveTab = () => { - return <>Remove 탭입니다; +import { useState } from 'react'; +import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; +import { roomOptions } from '@/core/api/options'; +import roomAPI from '@/core/api/functions/roomAPI'; +import { useMoveRoute } from '@/core/hooks'; +import { Input } from '@/shared/Input'; +import { LoadingSpinner } from '@/shared/LoadingSpinner'; + +interface RemoveTabProps { + roomId: string; +} + +const RemoveTab = ({ roomId }: RemoveTabProps) => { + const [confirmInput, setConfirmInput] = useState(''); + const moveTo = useMoveRoute(); + + const { data: room } = useSuspenseQuery({ + ...roomOptions.detail(roomId), + staleTime: Infinity + }); + + const { mutate, isPending, error } = useMutation({ + mutationFn: roomAPI.deleteRoom + }); + + const handleRemove = () => { + mutate(roomId, { + onSuccess: () => { + moveTo('routines'); + // TODO: 토스트 메시지로 방을 삭제했음을 알려야 해요. + }, + onError: (err) => console.error(err) + }); + }; + + if (room.currentUserCount > 1) { + return ( + <> +

+

방에 혼자 남았을 때만

+

삭제할 수 있어요.

+

+ + ); + } else { + return ( + <> +

방을 삭제할까요?

+
+ + setConfirmInput(e.target.value)} + /> +
+ {error && ( +

+ {error.response?.data.message} +

+ )} + + + ); + } }; export default RemoveTab; + +// 헤딩에 적용할 스타일 +const headingStyle = 'text-xl font-bold'; + +// 회색으로 간결한 설명을 적을 때 적용할 스타일 +const descriptionStyle = 'text-base text-dark-gray'; diff --git a/src/core/api/functions/roomAPI.ts b/src/core/api/functions/roomAPI.ts index cad2b518..c1b3e9cb 100644 --- a/src/core/api/functions/roomAPI.ts +++ b/src/core/api/functions/roomAPI.ts @@ -12,6 +12,11 @@ const roomAPI = { }) => { return await baseInstance.post<{ message: string }>('/rooms', body); }, + + getRoomDetail: async (roomId: string) => { + return await baseInstance.get(`/rooms/${roomId}`); + }, + putRoom: async (params: { roomId: string; title: string; @@ -24,8 +29,21 @@ const roomAPI = { const { roomId, ...body } = params; return await baseInstance.put(`/rooms/${roomId}`, body); }, - getRoomDetail: async (roomId: string) => { - return await baseInstance.get(`/rooms/${roomId}`); + + deleteRoom: async (roomId: string) => { + return await baseInstance.delete(`/rooms/${roomId}`); + }, + + deleteKickUser: async (params: { roomId: string; memberId: string }) => { + const { roomId, memberId } = params; + return await baseInstance.delete(`/rooms/${roomId}/members/${memberId}`); + }, + + putDelegateMaster: async (params: { roomId: string; memberId: string }) => { + const { roomId, memberId } = params; + return await baseInstance.put( + `/rooms/${roomId}/members/${memberId}/delegation` + ); } }; diff --git a/src/core/mocks/handlers/rooms.ts b/src/core/mocks/handlers/rooms.ts index a541e723..9425ac69 100644 --- a/src/core/mocks/handlers/rooms.ts +++ b/src/core/mocks/handlers/rooms.ts @@ -34,6 +34,27 @@ const roomsHandlers = [ return HttpResponse.json(response, { status }); }), + http.get(baseURL('/rooms/:roomId'), async () => { + await delay(1000); + + const status: number = 200; + let response = {}; + + switch (status) { + case 200: + response = RoomInfo; + break; + case 401: + response = { message: '존재하지 않는 유저입니다.' }; + break; + case 404: + response = { message: '존재하지 않는 방입니다.' }; + break; + } + + return HttpResponse.json(response, { status }); + }), + http.put(baseURL('/rooms/:roomId'), async () => { await delay(1000); @@ -68,7 +89,7 @@ const roomsHandlers = [ return HttpResponse.json(response, { status }); }), - http.get(baseURL('/rooms/:roomId'), async () => { + http.delete(baseURL('/rooms/:roomId'), async () => { await delay(1000); const status: number = 200; @@ -76,13 +97,57 @@ const roomsHandlers = [ switch (status) { case 200: - response = RoomInfo; + response = {}; + break; + case 400: + response = { + message: '방장은 나가려면 위임 하거나 혼자 남아있어야 합니다.' + }; break; case 401: response = { message: '존재하지 않는 유저입니다.' }; break; - case 404: - response = { message: '존재하지 않는 방입니다.' }; + } + + return HttpResponse.json(response, { status }); + }), + + http.delete(baseURL('/rooms/:roomId/members/:memberId'), async () => { + await delay(1000); + + const status: number = 200; + let response = {}; + + switch (status) { + case 200: + response = {}; + break; + case 400: + response = { message: '방장만 추방할 수 있습니다.' }; + break; + case 401: + response = { message: '존재하지 않는 유저입니다.' }; + break; + } + + return HttpResponse.json(response, { status }); + }), + + http.put(baseURL('/rooms/:roomId/members/:memberId/delegation'), async () => { + await delay(1000); + + const status: number = 200; + let response = {}; + + switch (status) { + case 200: + response = {}; + break; + case 400: + response = { message: '방장만 위임할 수 있습니다.' }; + break; + case 401: + response = { message: '존재하지 않는 유저입니다.' }; break; } diff --git a/src/pages/RoomSettingPage.tsx b/src/pages/RoomSettingPage.tsx index ddaa4234..49002ea5 100644 --- a/src/pages/RoomSettingPage.tsx +++ b/src/pages/RoomSettingPage.tsx @@ -24,10 +24,14 @@ const RoomSettingPage = () => { - + }> + + - + }> + +