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

feat: 방 관리 기능 완성 #137

Merged
merged 10 commits into from
Nov 15, 2023
5 changes: 0 additions & 5 deletions src/RoomSetting/tabs/MemberTab.tsx

This file was deleted.

77 changes: 77 additions & 0 deletions src/RoomSetting/tabs/MemberTab/DelegationButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<button
className="btn btn-light-point dark:btn-dark-point rounded-2xl px-3 py-1"
onClick={open}
>
방장 위임
</button>
<BottomSheet
{...bottomSheetProps}
className="p-6"
>
<h1 className={ModalHeadingStyle}>
<b>"{nickname}" 님에게</b>
<b>
<span className="font-bold text-light-point dark:text-dark-point">
방장을 위임
</span>
하시겠어요?
</b>
</h1>
<p className={descriptionStyle}>
위임 후, 관리 페이지에서 자동으로 나가집니다.
</p>
{error && <p className={errorStyle}>{error.response?.data.message}</p>}
<button
className="btn btn-light-point dark:btn-dark-point mt-6 flex w-full items-center justify-center"
onClick={handleDelegation}
disabled={isPending}
>
{isPending ? <LoadingSpinner size="2xl" /> : '방장 위임'}
</button>
</BottomSheet>
</>
);
};

export default DelegationButton;
88 changes: 88 additions & 0 deletions src/RoomSetting/tabs/MemberTab/KickButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<button
className="btn btn-danger rounded-2xl px-3 py-1"
onClick={open}
>
추방
</button>
<BottomSheet
{...bottomSheetProps}
className="p-6"
>
<h1 className={ModalHeadingStyle}>
<b>"{nickname}" 님을</b>
<b>
<span className="font-bold text-danger">추방</span>하시겠어요?
</b>
</h1>
<p className={descriptionStyle}>바쁜 일이 있었던 걸지도 몰라요.</p>
<section className="mt-10 flex flex-col gap-2">
<label
htmlFor="nickname"
className="text-base"
>
추방하려는 멤버의 닉네임을 적어주세요.
</label>
<Input
id="nickname"
value={confirmInput}
autoComplete="off"
placeholder={nickname}
onChange={(e) => setConfirmInput(e.target.value)}
/>
Comment on lines +68 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1; onChange에 들어가는 함수는 따로 함수를 선언해서 쓰면 어떨까요?
https://www.notion.so/prgrms/FE-568228d7d8f64051b5652548944214e9?pvs=4#e81e072bdf404604862aee5f488e1de4

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇.. 요기선 세터 함수를 호출하는 한 줄의 로직을 수행하는 것이라 구현이 드러나는 방법을 선택했어요!!
(Input 컴포넌트의 동작을 바로 확인할 수 있게 colocation 하는 느낌?)
물론 onChange 동작이 몇 줄 단위로 복잡해진다면, 다른 핸들러 함수로 빼는 것이 좋겠지만.. 한 줄 로직도 빼는것이 좋을까요!?

Copy link
Contributor

@nayeon-hub nayeon-hub Nov 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그것도 괜찮네요! 한 줄 일 때는 그대로 코드를 작성하는 걸로 할까요? 💡

</section>
{error && (
<p className="ml-2 mt-2 text-base text-danger">
{error.response?.data.message}
</p>
)}
<button
className="btn btn-danger mt-6 flex w-full items-start justify-center"
onClick={handleKick}
disabled={confirmInput !== nickname || isPending}
>
{isPending ? <LoadingSpinner size="2xl" /> : '추방'}
</button>
</BottomSheet>
</>
);
};

export default KickButton;
46 changes: 46 additions & 0 deletions src/RoomSetting/tabs/MemberTab/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col gap-4">
{room.todayCertificateRank.map((member) => (
<div
className="flex items-center justify-between"
key={member.memberId}
>
<Avatar
imgUrl={member.profileImage}
nickname={member.nickname}
userId={member.memberId}
contribution={member.contributionPoint}
/>
<div className="flex gap-2">
<KickButton
{...member}
roomId={roomId}
/>
<DelegationButton
{...member}
roomId={roomId}
/>
</div>
</div>
))}
</div>
);
};

export default MemberTab;
8 changes: 8 additions & 0 deletions src/RoomSetting/tabs/MemberTab/styles.ts
Original file line number Diff line number Diff line change
@@ -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';
86 changes: 84 additions & 2 deletions src/RoomSetting/tabs/RemoveTab.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h1 className={headingStyle}>
<p className="font-bold">방에 혼자 남았을 때만</p>
<p className="font-bold">삭제할 수 있어요.</p>
</h1>
</>
);
} else {
return (
<>
<h1 className={headingStyle}>방을 삭제할까요?</h1>
<section className="mt-4 flex flex-col gap-2">
<label
htmlFor="confirm"
className={descriptionStyle}
>
방의 이름 "{room.title}" 을 적어주세요.
</label>
<Input
id="confirm"
value={confirmInput}
autoComplete="off"
placeholder={room.title}
onChange={(e) => setConfirmInput(e.target.value)}
/>
</section>
{error && (
<p className="ml-2 mt-2 text-base text-danger">
{error.response?.data.message}
</p>
)}
<button
className="btn btn-danger mt-8 flex w-full items-start justify-center"
onClick={handleRemove}
disabled={confirmInput !== room.title || isPending}
>
{isPending ? <LoadingSpinner size="2xl" /> : '방 삭제'}
</button>
</>
);
}
};

export default RemoveTab;

// 헤딩에 적용할 스타일
const headingStyle = 'text-xl font-bold';

// 회색으로 간결한 설명을 적을 때 적용할 스타일
const descriptionStyle = 'text-base text-dark-gray';
22 changes: 20 additions & 2 deletions src/core/api/functions/roomAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const roomAPI = {
}) => {
return await baseInstance.post<{ message: string }>('/rooms', body);
},

getRoomDetail: async (roomId: string) => {
return await baseInstance.get<RoomInfo>(`/rooms/${roomId}`);
},

putRoom: async (params: {
roomId: string;
title: string;
Expand All @@ -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<RoomInfo>(`/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`
);
}
};

Expand Down
Loading