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

댓글 등록, 조회, 수정, 삭제 API fetch 함수 구현 및 커스텀 쿼리 구현 #170

Merged
merged 9 commits into from
Aug 1, 2023
Merged
23 changes: 23 additions & 0 deletions frontend/src/api/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CommentRequest, CommentResponse } from '@type/comment';

import { getFetch, postFetch, putFetch, deleteFetch } from '@utils/fetch';

export const getCommentList = async (postId: number): Promise<CommentResponse[]> => {
return await getFetch<CommentResponse[]>(`/posts/${postId}/comments`);
};

export const createComment = async (postId: number, newComment: CommentRequest) => {
return await postFetch(`/posts/${postId}/comments`, newComment);
};

export const editComment = async (
postId: number,
commentId: number,
updatedComment: CommentRequest
) => {
return await putFetch(`/posts/${postId}/comments/${commentId}`, updatedComment);
};

export const deleteComment = async (postId: number, commentId: number) => {
return await deleteFetch(`/posts/${postId}/comments/${commentId}`);
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ export default function TimePickerOption({
return () => {
timeBox.removeEventListener('scroll', handleScroll);
};
}, [handlePickTime, timeUnit]);
}, [currentTime, handlePickTime, option, timeUnit]);

return (
<S.TimeBox ref={timeBoxRef}>
{Array.from({ length: timeUnit }).map((_, index) => (
<S.Time
key={index}
ref={index === currentTime ? timeBoxChildRef : null}
isPicked={currentTime === index}
$isPicked={currentTime === index}
>
{index}
</S.Time>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/queryKey.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const QUERY_KEY = {
POSTS: 'posts',
COMMENTS: 'comments',
CATEGORIES: 'categories',
USER_INFO: 'user_info',
};
22 changes: 22 additions & 0 deletions frontend/src/hooks/query/comment/useCommentList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';

import { getCommentList } from '@api/comment';

import { QUERY_KEY } from '@constants/queryKey';

export const useCommentList = (postId: number) => {
const { data, error, isLoading } = useQuery(
[QUERY_KEY.POSTS, postId, QUERY_KEY.COMMENTS],
() => getCommentList(postId),
{
onSuccess: data => {
return data;
},
onError: error => {
window.console.log('get comment list error', error);
},
}
);

return { data, error, isLoading };
};
24 changes: 24 additions & 0 deletions frontend/src/hooks/query/comment/useCreateComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { CommentRequest } from '@type/comment';

import { createComment } from '@api/comment';

import { QUERY_KEY } from '@constants/queryKey';

export const useCreateComment = (postId: number) => {
const queryClient = useQueryClient();
const { mutate, isLoading, isError, error } = useMutation(
(newComment: CommentRequest) => createComment(postId, newComment),
{
onSuccess: () => {
queryClient.invalidateQueries([QUERY_KEY.POSTS, postId, QUERY_KEY.COMMENTS]);
},
onError: error => {
window.console.log('createComment error', error);
},
}
);

return { mutate, isLoading, isError, error };
};
22 changes: 22 additions & 0 deletions frontend/src/hooks/query/comment/useDeleteComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { deleteComment } from '@api/comment';

import { QUERY_KEY } from '@constants/queryKey';

export const useDeleteComment = (postId: number, commentId: number) => {
const queryClient = useQueryClient();
const { mutate, isLoading, isError, error } = useMutation(
() => deleteComment(postId, commentId),
{
onSuccess: () => {
queryClient.invalidateQueries([QUERY_KEY.POSTS, postId, QUERY_KEY.COMMENTS]);
},
onError: error => {
window.console.log('Delete Comment error', error);
},
}
);

return { mutate, isLoading, isError, error };
};
48 changes: 48 additions & 0 deletions frontend/src/hooks/query/comment/useEditComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { CommentRequest } from '@type/comment';

import { editComment } from '@api/comment';

import { QUERY_KEY } from '@constants/queryKey';

export const useEditComment = (
postId: number,
commentId: number,
updatedComment: CommentRequest
) => {
const queryClient = useQueryClient();
const queryKey = [QUERY_KEY.POSTS, postId, QUERY_KEY.COMMENTS];

const { mutate, isLoading, isError, error } = useMutation(
() => editComment(postId, commentId, updatedComment),
{
onMutate: async () => {
// 댓글 데이터에 대한 모든 퀴리요청을 취소하여 이전 서버 데이터가 낙관적 업데이트를 덮어쓰지 않도록 함 -> refetch 취소시킴
await queryClient.cancelQueries(queryKey);

const oldComment = queryClient.getQueryData(queryKey); // 기존 댓글 데이터의 snapshot

window.console.log('기존 댓글 데이터: ', oldComment);
Copy link
Collaborator

Choose a reason for hiding this comment

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

요건 그냥 제로가 확인하기 위함인가요?

Copy link
Member Author

@inyeong-kang inyeong-kang Aug 1, 2023

Choose a reason for hiding this comment

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

넵 맞습니당 API랑 연동해보고 불필요한 콘솔들 나중에 지우도록 하겠습니다!


queryClient.setQueryData(queryKey, updatedComment); // 낙관적 업데이트 실시
Copy link
Collaborator

Choose a reason for hiding this comment

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

낙관적 업데이트 👍👍👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분 낙관적 업데이트 할 때 updatedComment랑 update할 댓글 아이디로 전체 댓글에 대해 낙관적 업데이트를 할 수 있나요?

const updatedCommentList = oldCommentList.map((comment) =>{
if(comment.id === commentId){
comment.댓글 내용 = 업데이트  댓글 내용
}

return comment;
})

queryClient.setQueryData(queryKey, updatedCommentList); // 낙관적 업데이트 실시

Copy link
Member Author

Choose a reason for hiding this comment

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

      onMutate: async () => {
        // 댓글 데이터에 대한 모든 퀴리요청을 취소하여 이전 서버 데이터가 낙관적 업데이트를 덮어쓰지 않도록 함 -> refetch 취소시킴
        await queryClient.cancelQueries(queryKey);

        const oldCommentList: CommentResponse[] | undefined = queryClient.getQueryData(queryKey); // 기존 댓글 데이터의 snapshot
        window.console.log('기존 댓글 데이터: ', oldCommentList);

        if (oldCommentList) {
          const updatedCommentList = oldCommentList.map(comment => {
            if (comment.id === commentId) return updatedComment;
          });

          queryClient.setQueryData(queryKey, updatedCommentList); // 낙관적 업데이트 실시

          return { oldCommentList, updatedCommentList }; // context를 return, context 예시에는 이전 스냅샷, 새로운 값(또는 롤백하는 함수)이 있음
        }
      },

댓글 아이디에 대한 쿼리 키는 없으므로 댓글 리스트에 대해 낙관적 업데이트하는 것이 맞겠네요!! 감사합니다~~! 위 코드처럼 수정했습니다😃


return { oldComment, updatedComment }; // context를 return, context 예시에는 이전 스냅샷, 새로운 값(또는 롤백하는 함수)이 있음
},
onError: (error, _, context) => {
// 캐시를 저장된 값으로 롤백
queryClient.setQueryData(queryKey, context?.oldComment);
Copy link
Collaborator

Choose a reason for hiding this comment

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

예전 데이터가 어디에 사용되나 했는데 이렇게 사용되는군요!👍

window.console.log('댓글 수정에 실패했습니다. 다시 시도해주세요.', error);
},
onSuccess: () => {
window.console.log('댓글이 성공적으로 수정되었습니다!');
},
onSettled: () => {
// 쿼리 함수의 성공하든 실패하든 모든 실행 -> 기존 댓글 데이터 무효화
queryClient.invalidateQueries(queryKey);
Copy link
Collaborator

Choose a reason for hiding this comment

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

"기존 댓글 데이터 무효화"가 어떤 의미인가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

기존의 댓글 리스트 데이터 (oldCommentList) 가 업데이트가 필요하다고 판단하는 것입니다! (onSettled은 약간 try-catch-finally 에서 finally 같은 느낌인거 같아요! 혹시나 catch에서 오류를 잡아내지 못했을 경우를 대비하려고, onSettle에서 무조건 기존 데이터가 유효하지 않다고 판단해야 한다고 하네요..~)

},
}
);

return { mutate, isLoading, isError, error };
};
33 changes: 33 additions & 0 deletions frontend/src/mocks/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { rest } from 'msw';

import { MOCK_COMMENT_LIST } from './mockData/comment';

export const mockComment = [
rest.get('/posts/:postId/comments', (req, res, ctx) => {
return res(ctx.delay(1000), ctx.status(200), ctx.json(MOCK_COMMENT_LIST));
}),

rest.post('/posts/:postId/comments', (req, res, ctx) => {
window.console.log('등록한 댓글 내용', req.body);

return res(
ctx.delay(1000),
ctx.status(201),
ctx.json({ message: '댓글이 성공적으로 등록되었습니다!!' })
);
}),

rest.put('/posts/:postId/comments/:commentId', (req, res, ctx) => {
window.console.log('수정한 댓글 내용', req.body);

return res(
ctx.delay(1000),
ctx.status(200),
ctx.json({ message: '댓글이 성공적으로 수정되었습니다!!' })
);
}),

rest.delete('/posts/:postId/comments/:commentId', (req, res, ctx) => {
return res(ctx.delay(1000), ctx.status(204));
}),
];
2 changes: 2 additions & 0 deletions frontend/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mockComment } from './comment';
import { example } from './example/get';
import { mockVoteResult } from './getVoteDetail';
import { mockPost } from './post';
Expand All @@ -14,4 +15,5 @@ export const handlers = [
...mockVote,
...mockCategoryHandlers,
...mockUserInfo,
...mockComment,
];
43 changes: 43 additions & 0 deletions frontend/src/mocks/mockData/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CommentResponse } from '@type/comment';

export const MOCK_COMMENT_LIST: CommentResponse[] = [];

const commentList = [
'Woah, your project looks awesome! How long have you been coding for? ',
'일하기 싫어서 화장실에 앉아서 보는 중은 아닌데 아 원숭이 김종민보려고 눈뜬거 진짜웃겨ㅠㅠㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ',
'진짜 다보고 나니 눈물이 ㅜㅜ 너무 참아서 눈물이 줄줄 ㅜㅜ 미쳤네요',
'ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 생일 축하드립니다 🎉🎉🎉 뭔가 예전에 무한도전에서 했던 돌+아이 콘테스트도 조금 생각나요',
'1:08 4:01 4:20 6:04\n제가 계속 보고 싶어서 정리한 타임코드입니다\n역시나 생일파티 콘텐츠는 아무리봐도 안 질리네요\n유병재님 덕분에 오늘도 마음이 풍선해집니다💚❤️',
'진짜ㅋㅋㅋㅋ레전드중 레전드인 컨텐츠인 것 같아요ㅋㅋㅋ큐ㅠㅠㅠ 몇번을 봐도 웃음이 멈추질 않는ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ!!>w<!!😂💞 ㅋㅋㅋㅋ살려주세요ㅠㅠㅠ 배가ㅋㅋㅋㅋㅋㅋㅋㅋㅋ',
'심판들의 엄격한 평가가 있어야 한다고 봅니다!!!',
'나도 모르게 숨을 참게되네..',
'정말 멋진 프로젝트네요! 코딩을 얼마나 오래하셨나요? 저는 아직 새내기인데, 곧 리액트를 배울 생각인데 어떻게 배울 수 있을까요? 조언 좀 부탁드려도 될까요? 감사합니다!',
'방금 보다 너무 웃긴거 같아요ㅋㅋㅋ 글쎄요 원숭이 김종민이랑 수호랑 같이 보려고 일부러 눈뜬거 같았는데ㅋㅋㅋ 힌우해요ㅠㅠㅋㅋㅋㅋ',
'이 영상을 보면서 눈물과 미소가 번갈아 오네요 ㅜㅜ 너무나 감동적이고 멋지네요',
];

const nicknameList = [
'방방뛰는 코끼리',
'환상의 드래곤',
'컴퓨터 마법사',
'무한한 상상력',
'꿈을 향한 여행자',
'플레이메이커',
'뛰어난 전략가',
'뚜렷한 개성',
];

const getMockComment = (): CommentResponse => ({
id: Math.floor(Math.random() * 100000),
content: commentList[Math.floor(Math.random() * 12)],
createdAt: '2023.7.27. 07:43',
member: {
id: Math.floor(Math.random() * 100000),
nickname: nicknameList[Math.floor(Math.random() * 8)],
},
updatedAt: '2023.7.28. 07:43',
});

for (let index = 0; index < 50; index++) {
MOCK_COMMENT_LIST.push(getMockComment());
}
12 changes: 12 additions & 0 deletions frontend/src/types/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface CommentResponse {
id: number;
member: {
id: number;
nickname: string;
};
content: string;
createdAt: string;
updatedAt: string;
}

export type CommentRequest = Pick<CommentResponse, 'content'>;