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

[#505] [모임 상세] 모임 삭제 기능 구현 #537

Merged
merged 3 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/app/group/[groupId]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Button from '@/v1/base/Button';
import Image from 'next/image';
import Link from 'next/link';

export default function NotFound() {
return (
<div className="absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center gap-[2rem]">
<Image src="/images/loading.gif" width={230} height={160} alt="loading" />
<p className="text-2xl">
<span className="font-bold text-main-900">다독이</span>가 길을 잃었어요.
</p>
<Link href="/group">
<Button size="large" colorScheme="main" fill={false}>
모임 둘러보기
</Button>
</Link>
</div>
);
}
14 changes: 9 additions & 5 deletions src/app/group/[groupId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
'use client';

import { notFound } from 'next/navigation';
import { ErrorBoundary } from 'react-error-boundary';

import { checkAuthentication } from '@/utils/helpers';

import SSRSafeSuspense from '@/components/SSRSafeSuspense';
import BookGroupInfo from '@/v1/bookGroup/detail/BookGroupInfo';
import BookGroupCommentList from '@/v1/comment/BookGroupCommentList';
import LoginBottomActionButton from '@/v1/base/LoginBottomActionButton';
import BookGroupNavigation from '@/v1/bookGroup/BookGroupNavigation';
import BookGroupInfo from '@/v1/bookGroup/detail/BookGroupInfo';
import JoinBookGroupButton from '@/v1/bookGroup/detail/JoinBookGroupButton';
import LoginBottomActionButton from '@/v1/base/LoginBottomActionButton';
import BookGroupCommentList from '@/v1/comment/BookGroupCommentList';

const DetailBookGroupPage = ({
params: { groupId },
}: {
params: { groupId: number };
}) => {
const isAuthenticated = checkAuthentication();

return (
<>
<ErrorBoundary fallbackRender={notFound}>
Copy link
Member Author

@gxxrxn gxxrxn Apr 24, 2024

Choose a reason for hiding this comment

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

comment;

존재하지 않는 모임 페이지인 경우, 서버에서 404 에러를 응답으로 보내기 때문에 런타임 오류가 발생해요. 모임 상세 페이지 최상단에서 error를 캐치해서 notFound 페이지를 렌더링하도록 구현했어요.

/app/group/[groupId]/error.tsx 파일에 작성하려고 했는데, next에서는 NotFoundBoundary가 ErrorBoundary보다 하위에 위치하고 있어 상위의 NotFound 페이지를 렌더링하는 문제가 있었어요. 그래서 직접 ErrorBoundary를 컴포넌트에 감싸도록 구현했어요.

<BookGroupNavigation groupId={groupId}>
<BookGroupNavigation.BackButton />
<BookGroupNavigation.Title />
Expand All @@ -39,7 +43,7 @@ const DetailBookGroupPage = ({
<LoginBottomActionButton />
)}
</SSRSafeSuspense>
</>
</ErrorBoundary>
);
};

Expand Down
3 changes: 2 additions & 1 deletion src/components/ReactQueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const ReactQueryProvider: NextPage<PropTypes> = ({ children }) => {
new QueryClient({
defaultOptions: {
queries: {
retry: 0,
refetchOnWindowFocus: false,
retry: false,
Comment on lines +16 to +17
Copy link
Member Author

Choose a reason for hiding this comment

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

원하지 않는 시점에 refetch가 되는 불편함이 있어 false로 변경했어요.

},
},
})
Expand Down
14 changes: 14 additions & 0 deletions src/queries/group/useDeleteBookGroupMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMutation } from '@tanstack/react-query';

import type { APIGroup } from '@/types/group';

import groupAPI from '@/apis/group';

const useDeleteBookGroupMutation = () => {
return useMutation({
mutationFn: (bookGroupId: APIGroup['bookGroupId']) =>
groupAPI.deleteGroup({ bookGroupId }).then(({ data }) => data),
});
};

export default useDeleteBookGroupMutation;
2 changes: 1 addition & 1 deletion src/v1/base/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const getSchemeClasses = (theme: ColorScheme, isFill: boolean) => {
};

const BASE_BUTTON_CLASSES =
'cursor-pointer border-[0.1rem] leading-none inline-block font-bold';
'cursor-pointer border-[0.1rem] leading-none inline-block font-bold focus:outline-none focus:ring-2';
Copy link
Member Author

Choose a reason for hiding this comment

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

모달을 띄우면 button에 focus가 발생해요. focus 시 outline style을 수정했어요.


const Button = ({
size = 'medium',
Expand Down
2 changes: 1 addition & 1 deletion src/v1/base/Toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const ToastProvider = ({ children }: { children?: ReactNode }) => {

const controller = useMemo<ToastController>(
() => ({
show: ({ type, message, duration = 1000 }) => {
show: ({ type, message, duration = 2000 }) => {
setToast({ type, message, duration });

setAnimation('slide-init');
Expand Down
109 changes: 97 additions & 12 deletions src/v1/bookGroup/BookGroupNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { useRouter } from 'next/navigation';
import {
ReactNode,
Children,
createContext,
isValidElement,
ReactNode,
useContext,
useRef,
} from 'react';
import { useRouter } from 'next/navigation';

import SSRSafeSuspense from '@/components/SSRSafeSuspense';
import TopNavigation from '@/v1/base/TopNavigation';
import CommentDrawer from '@/v1/comment/CommentDrawer';
import { IconArrowLeft, IconHamburger, IconPost } from '@public/icons';
import { SERVICE_ERROR_MESSAGE } from '@/constants';
import { IconArrowLeft, IconPost } from '@public/icons';
import { isAxiosErrorWithCustomCode } from '@/utils/helpers';

import useDisclosure from '@/hooks/useDisclosure';
import {
useBookGroupJoinInfo,
useBookGroupOwner,
useBookGroupTitle,
useBookGroupJoinInfo,
} from '@/queries/group/useBookGroupQuery';
import useToast from '@/v1/base/Toast/useToast';
import useDisclosure from '@/hooks/useDisclosure';
import useCreateBookGroupCommentMutation from '@/queries/group/useCreateBookGroupCommentMutation';
import { isAxiosErrorWithCustomCode } from '@/utils/helpers';
import { SERVICE_ERROR_MESSAGE } from '@/constants';
import useDeleteBookGroupMutation from '@/queries/group/useDeleteBookGroupMutation';

import SSRSafeSuspense from '@/components/SSRSafeSuspense';
import Button from '@/v1/base/Button';
import Menu from '@/v1/base/Menu';
import Modal from '@/v1/base/Modal';
import useToast from '@/v1/base/Toast/useToast';
import TopNavigation from '@/v1/base/TopNavigation';
import CommentDrawer from '@/v1/comment/CommentDrawer';

const NavigationContext = createContext({} as { groupId: number });

Expand Down Expand Up @@ -102,7 +108,52 @@ const MenuButton = () => {
const { groupId } = useContext(NavigationContext);
const { data: owner } = useBookGroupOwner(groupId);

return <>{owner.isMe && <IconHamburger />}</>;
const router = useRouter();

const deleteBookGroup = useDeleteBookGroupMutation();

const { show: showToast } = useToast();
const { isOpen, onClose, onOpen } = useDisclosure();

const handleModalConfirm = async () => {
await deleteBookGroup.mutateAsync(groupId, {
onSuccess: () => {
showToast({ type: 'success', message: '모임을 삭제했어요' });
router.replace('/group');
},
onError: error => {
if (isAxiosErrorWithCustomCode(error)) {
const { code } = error.response.data;
const message = SERVICE_ERROR_MESSAGE[code];
showToast({ type: 'error', message });
return;
}

showToast({ type: 'error', message: '잠시 후 다시 시도해주세요' });
},
});
};

return (
<>
{owner.isMe && (
<>
<Menu>
<Menu.Toggle />
<Menu.DropdownList>
<Menu.Item>수졍하기</Menu.Item>
<Menu.Item onSelect={onOpen}>삭제하기</Menu.Item>
</Menu.DropdownList>
</Menu>
<DeleteBookGroupModal
isOpen={isOpen}
onClose={onClose}
onConfirm={handleModalConfirm}
/>
</>
)}
</>
);
};

const WriteButton = () => {
Expand Down Expand Up @@ -173,6 +224,40 @@ const TitleSkeleton = () => (
<div className="h-[1.5rem] w-[40%] animate-pulse bg-black-400"></div>
);

const DeleteBookGroupModal = ({
isOpen,
onClose,
onConfirm,
}: {
isOpen: boolean;
onClose: () => void;
onConfirm?: () => void;
}) => {
const handleConfirm = () => {
onConfirm && onConfirm();
onClose();
};

return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="text-lg font-bold leading-loose">
모임을 정말 삭제할까요?
<p className="text-sm font-normal leading-tight text-black-500">
참여 중인 모임원이 있는 경우, 모임을 삭제할 수 없어요.
</p>
</div>
<div className="flex justify-end gap-[1rem]">
<Button onClick={onClose} fill={false} colorScheme="grey" size="small">
취소
</Button>
<Button onClick={handleConfirm} size="small">
확인
</Button>
</div>
</Modal>
);
};

const BackButtonType = (<BackButton />).type;
const TitleType = (<Title />).type;
const MenuButtonType = (<MenuButton />).type;
Expand Down
Loading