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

SKRR-28 feat: 클럽 게시글 삭제 및 수정 기능 구현 #251

Merged
merged 8 commits into from
Jan 25, 2024
10 changes: 10 additions & 0 deletions src/apis/club/deleteClubPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { END_POINTS } from '@/constants/api';
import { DeleteClubPostRequest } from '@/types/api/deleteClubPost';

import { axiosClientWithAuth } from '../axiosClient';

const deleteClubPost = async ({ postId }: DeleteClubPostRequest) => {
await axiosClientWithAuth.delete(END_POINTS.DELETE_CLUB_POST(postId));
};

export default deleteClubPost;
29 changes: 29 additions & 0 deletions src/apis/club/putClubPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { END_POINTS } from '@/constants/api';
import { PutClubPostRequest } from '@/types/api/putClubPost';

import { axiosClientWithAuth } from '../axiosClient';

const putClubPost = async ({ postRequest, image, postId }: PutClubPostRequest) => {
if (!postId) {
return null;
}

const formData = new FormData();

if (postRequest) {
const blobRequest = new Blob([JSON.stringify(postRequest)], { type: 'application/json' });
formData.append('postRequest', blobRequest);
}

if (image) {
formData.append('image', image);
}

await axiosClientWithAuth.put(END_POINTS.PUT_CLUB_POST(postId), formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
};

export default putClubPost;
7 changes: 6 additions & 1 deletion src/components/ClubPost/ClubPost.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PATH } from '@/constants/path';
import { getDateStamp } from '@/utils/getTimeStamp';

import { useNavigate } from 'react-router-dom';

Expand Down Expand Up @@ -36,6 +37,8 @@ const ClubPost = ({
}: ClubPostProps) => {
const navigate = useNavigate();
const hasImage = Boolean(postImageUrl);
const postedDate = getDateStamp(createdDate.split('T')[0]);
const postedTime = createdDate.split('T')[1];

return (
<BoardContainer>
Expand All @@ -45,7 +48,9 @@ const ClubPost = ({
<ContentStyled>{content}</ContentStyled>
</BoardContentWrapper>
<BoardInfoWrapper>
<PostDateWrapper>{createdDate}</PostDateWrapper>
<PostDateWrapper>
{postedDate} {postedTime}
</PostDateWrapper>
<Avatar avatarSize="small" profileImageSrc={authorImageUrl} />
<AuthorWrapper>{author}</AuthorWrapper>
</BoardInfoWrapper>
Expand Down
67 changes: 49 additions & 18 deletions src/components/ClubPostDetail/ClubPostDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { MODAL_TEXT } from '@/constants/modalMessage';
import { PATH } from '@/constants/path';
import useDeleteClubPostMutation from '@/hooks/query/club/useDeleteClubPostMutation';
import useGetClubPostDetail from '@/hooks/query/club/useGetClubPostDetail';
import useModal from '@/hooks/useModal';
import { getStorage } from '@/utils/localStorage';

import { useNavigate } from 'react-router-dom';

import ConfirmModal from '../Modals/ConfirmModal';
import Avatar from '../common/Avatar/Avatar';
import Button from '../common/Button/Button';
import {
Expand All @@ -20,6 +27,9 @@ interface ClubPostDetailProps {

const ClubPostDetail = ({ clubId, postId }: ClubPostDetailProps) => {
const { clubPostDetail } = useGetClubPostDetail({ clubId, postId });
const { deletePost } = useDeleteClubPostMutation();
const { modalClose, modalOpen, showModal } = useModal();
const navigate = useNavigate();

if (!clubPostDetail) {
return null;
Expand All @@ -42,25 +52,46 @@ const ClubPostDetail = ({ clubId, postId }: ClubPostDetailProps) => {
const postedTime = createdDate.split('T')[1];

return (
<ClubPostDetailContainer>
{isAuthor && (
<ButtonWrapper>
<Button buttonText="수정" outline />
<Button buttonText="삭제" />
</ButtonWrapper>
<>
{showModal && (
<ConfirmModal
message={MODAL_TEXT.DELETE_CLUB_POST}
confirmLabel="확인"
onConfirm={() => deletePost({ postId })}
onClose={modalClose}
/>
)}
<PostAuthorWrapper>
<Avatar avatarSize="small" profileImageSrc={authorImageUrl} />
{author}
</PostAuthorWrapper>
<PostTitleStyled>{title}</PostTitleStyled>
{postImageUrl && <img src={postImageUrl} />}
<PostContentStyled>{content}</PostContentStyled>
<PostedDateStyled>
{postedDate} {postedTime} {isEdited && <span>(편집됨)</span>}
</PostedDateStyled>
<PostSeparatorStyled />
</ClubPostDetailContainer>
<ClubPostDetailContainer>
{isAuthor && (
<ButtonWrapper>
<Button
buttonText="수정"
outline
onClick={() =>
navigate(PATH.CLUB.WRITE_POST(clubId), {
state: {
clubPostDetail,
postId,
},
})
}
/>
<Button buttonText="삭제" onClick={modalOpen} />
</ButtonWrapper>
)}
<PostAuthorWrapper>
<Avatar avatarSize="small" profileImageSrc={authorImageUrl} />
{author}
</PostAuthorWrapper>
<PostTitleStyled>{title}</PostTitleStyled>
{postImageUrl && <img src={postImageUrl} />}
<PostContentStyled>{content}</PostContentStyled>
<PostedDateStyled>
{postedDate} {postedTime} {isEdited && <span>(편집됨)</span>}
</PostedDateStyled>
<PostSeparatorStyled />
</ClubPostDetailContainer>
</>
);
};

Expand Down
2 changes: 2 additions & 0 deletions src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const END_POINTS = {
CLUB_POSTS: (clubId: string, pageNumber: number) =>
`/boards/posts/${clubId}?page=${pageNumber}&size=20&sort=id,desc`,
CLUB_POST: (clubId: string, postId: string) => `/boards/posts/${clubId}/${postId}`,
PUT_CLUB_POST: (postId: string) => `/boards/posts/${postId}`,
DELETE_CLUB_POST: (postId: string) => `/boards/posts/${postId}`,
CLUB_COMMENTS: (postId: string, pageNumber: number) =>
`/boards/posts/${postId}/comments?page=${pageNumber}&size=20&sort=id,asc`,
CLUB_COMMENT: (postId: string) => `/boards/posts/${postId}/comments`,
Expand Down
2 changes: 2 additions & 0 deletions src/constants/modalMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const MODAL_TEXT = {
'정말 스페이스 클럽을 탈퇴하시겠어요? 3일 안에 다시 로그인 하시면 데이터를 복구할 수 있지만 이후에는 관련 데이터가 모두 삭제됩니다',
FORM_OPTION_EMPTY_TITLE: '제목이 입력되지 않은 항목이 있습니다.',
CANCELED_SUBMITTED_FORM: '제출된 폼을 취소 시키겠습니까? 취소된 폼은 되돌릴 수 없습니다.',

DELETE_CLUB_POST: '게시물을 삭제하겠습니까?',
};

const MODAL_BUTTON_TEXT = {
Expand Down
29 changes: 29 additions & 0 deletions src/hooks/query/club/useDeleteClubPostMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import deleteClubPost from '@/apis/club/deleteClubPost';
import useToast from '@/hooks/useToast';

import { useNavigate } from 'react-router-dom';

import { useMutation, useQueryClient } from '@tanstack/react-query';

import { QUERY_KEY } from './useGetClubPostsQuery';

const useDeleteClubPostMutation = () => {
const queryClient = useQueryClient();
const { createToast } = useToast();
const navigate = useNavigate();

const { mutate: deletePost } = useMutation(deleteClubPost, {
onSuccess: () => {
queryClient.invalidateQueries([QUERY_KEY.CLUB_POSTS]);
createToast({ message: '게시물이 삭제되었습니다.', toastType: 'success' });
navigate(-1);
},
onError: () => {
createToast({ message: '게시물 삭제에 실패했습니다.', toastType: 'error' });
},
});

return { deletePost };
};

export default useDeleteClubPostMutation;
2 changes: 1 addition & 1 deletion src/hooks/query/club/usePostCreateClubPost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const usePostCreateClubPost = () => {
navigate(PATH.CLUB.POST(clubId, postId));
},
onError: () => {
createToast({ message: '글 작성 실패', toastType: 'error' });
createToast({ message: '글 작성에 실패하였습니다.', toastType: 'error' });
},
});

Expand Down
37 changes: 37 additions & 0 deletions src/hooks/query/club/usePutClubPostMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import putClubPost from '@/apis/club/putClubPost';
import { PATH } from '@/constants/path';
import useToast from '@/hooks/useToast';

import { useNavigate } from 'react-router-dom';

import { useMutation, useQueryClient } from '@tanstack/react-query';

import { QUERY_KEY } from './useGetClubPostDetail';

interface usePutClubPostMutationProps {
clubId: string;
postId: string | null;
}

const usePutClubPostMutation = ({ clubId, postId }: usePutClubPostMutationProps) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { createToast } = useToast();

const { mutate: putPost } = useMutation(putClubPost, {
onSuccess: () => {
queryClient.invalidateQueries([QUERY_KEY.GET_CLUB_POST_DETAIL]);
createToast({ message: '수정이 완료되었습니다.', toastType: 'success' });
if (postId) {
navigate(PATH.CLUB.POST(clubId, postId));
}
},
onError: () => {
createToast({ message: '수정에 실패했습니다.', toastType: 'error' });
},
});

return { putPost };
};

export default usePutClubPostMutation;
1 change: 1 addition & 0 deletions src/pages/club/ClubBoardPage/ClubBoardPage.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const ClubBoardContentWrapper = styled.div`
flex-direction: column;
justify-content: center;
align-items: center;
gap: 3rem;
width: 100%;
height: 100%;
padding: 1rem;
Expand Down
4 changes: 3 additions & 1 deletion src/pages/club/ClubPostWritePage/ClubPostWritePage.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ const FileInputWrapper = styled.div`
`;

const ErrorMessageStyled = styled.div`
padding-left: 10rem;
display: flex;
justify-content: flex-start;
width: 100%;
font-size: ${Theme.fontSize.smallContent};
color: ${Theme.color.tRed};
`;
Expand Down
54 changes: 42 additions & 12 deletions src/pages/club/ClubPostWritePage/ClubPostWritePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
MIN_CLUB_POST_TITLE_LENGTH,
} from '@/constants/club';
import usePostCreateClubPost from '@/hooks/query/club/usePostCreateClubPost';
import usePutClubPostMutation from '@/hooks/query/club/usePutClubPostMutation';
import { GetClubPostResponse } from '@/types/api/getClubPost';

import { SubmitHandler, useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom';
import { useLocation, useParams } from 'react-router-dom';

import {
ButtonWrapper,
Expand All @@ -29,30 +31,56 @@ interface ClubPostWriteValue {
content: string;
}

interface ClubPostState {
clubPostDetail: GetClubPostResponse;
postId: string;
}

const ClubPostWritePage = () => {
const { clubId } = useParams();
const { state } = useLocation() as { state: ClubPostState | null };
const isEdit = Boolean(state);
const { clubPostDetail, postId } = state || {
clubPostDetail: { title: null, content: null },
postId: null,
};

const {
register,
handleSubmit,
formState: { errors },
} = useForm<ClubPostWriteValue>({
defaultValues: {
title: '',
content: '',
title: clubPostDetail.title ?? '',
content: clubPostDetail.content ?? '',
image: undefined,
},
});
if (!clubId) throw new Error('클럽 ID를 찾을 수 없습니다');
if (!clubId) throw new Error('ID를 찾을 수 없습니다');
const { createPost } = usePostCreateClubPost();
const { putPost } = usePutClubPostMutation({ clubId, postId });

const onSubmit: SubmitHandler<ClubPostWriteValue> = (data) => {
createPost({
clubId,
...data,
image: data.image,
title: data.title?.trim(),
content: data.content?.trim(),
});
if (isEdit) {
putPost({
postId,
...data,
postRequest: {
title: data.title?.trim(),
content: data.content?.trim(),
doesPostImageExist: Boolean(data.image),
},
image: data.image,
});
} else {
createPost({
clubId,
...data,
image: data.image,
title: data.title?.trim(),
content: data.content?.trim(),
});
}
};

const handleInputValueValidate = (value: string) => {
Expand All @@ -71,7 +99,7 @@ const ClubPostWritePage = () => {
<form onSubmit={handleSubmit(onSubmit)}>
<ContentWrapper>
<ButtonWrapper>
<Button buttonText="작성 완료" />
<Button buttonText={isEdit ? '수정 완료' : '작성 완료'} />
</ButtonWrapper>
<TitleStyled
{...register('title', {
Expand All @@ -87,6 +115,7 @@ const ClubPostWritePage = () => {
validate: (value) => handleInputValueValidate(value ?? ''),
})}
placeholder="제목을 입력해주세요."
maxLength={MAX_CLUB_POST_TITLE_LENGTH}
/>
<ErrorMessageStyled>{errors?.title ? errors.title.message : ''}</ErrorMessageStyled>
<ContentStyled
Expand All @@ -103,6 +132,7 @@ const ClubPostWritePage = () => {
validate: (value) => handleInputValueValidate(value ?? ''),
})}
placeholder="내용을 입력해주세요."
maxLength={MAX_CLUB_POST_CONTENT_LENGTH}
/>
<ErrorMessageStyled>{errors?.content ? errors.content.message : ''}</ErrorMessageStyled>
<FileInputWrapper>
Expand Down
5 changes: 5 additions & 0 deletions src/types/api/deleteClubPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface DeleteClubPostRequest {
postId: string;
}

export { DeleteClubPostRequest };
11 changes: 11 additions & 0 deletions src/types/api/putClubPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
interface PutClubPostRequest {
postRequest: {
title: string;
content: string;
doesPostImageExist: boolean;
};
image: File | null;
postId: string | null;
}

export { PutClubPostRequest };
Loading