From f3eb26fd48741da1919fdc547c1658126eec4aeb Mon Sep 17 00:00:00 2001 From: Sung Ji Hyun <69228045+jhsung23@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:17:42 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=ED=88=AC=ED=91=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EA=B3=B5=EA=B0=90=20=EA=B8=B0=EB=8A=A5=20(#189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 - src/app/vote/[slug]/_component/Replies.tsx | 7 +- src/app/vote/[slug]/_component/VoteDetail.tsx | 4 +- .../features/vote/reply/Reply.stories.tsx | 3 + src/components/features/vote/reply/Reply.tsx | 5 +- .../shared/likeButton/LikeButton.tsx | 6 +- src/hooks/vote/index.ts | 1 + src/hooks/vote/useLikeVoteMutation.ts | 34 +++++----- src/hooks/vote/useLikeVoteReplyMutation.ts | 64 +++++++++++++++++++ yarn.lock | 18 ------ 10 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 src/hooks/vote/useLikeVoteReplyMutation.ts diff --git a/package.json b/package.json index 907d1f29..a10c1cb2 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "framer-motion": "^10.18.0", "github-label-sync": "^2.3.1", "immer": "^10.0.3", - "lodash.clonedeep": "^4.5.0", "lodash.compact": "^3.0.1", "lodash.debounce": "^4.0.8", "modern-screenshot": "^4.4.38", @@ -54,7 +53,6 @@ "@storybook/react": "^7.6.10", "@storybook/test": "^7.6.10", "@types/github-label-sync": "^2", - "@types/lodash.clonedeep": "^4", "@types/lodash.compact": "^3", "@types/lodash.debounce": "^4.0.9", "@types/node": "^20", diff --git a/src/app/vote/[slug]/_component/Replies.tsx b/src/app/vote/[slug]/_component/Replies.tsx index 22072f53..3f296f39 100644 --- a/src/app/vote/[slug]/_component/Replies.tsx +++ b/src/app/vote/[slug]/_component/Replies.tsx @@ -12,6 +12,7 @@ import { useCreateVoteReplyMutation, useDeleteVoteReplyMutation, useGetVoteReplies, + useLikeVoteReplyMutation, } from '@/hooks/vote'; import { VoteReplyType } from '@/types/vote'; @@ -23,6 +24,7 @@ const Replies = ({ voteId }: Props) => { const { status, data: replies } = useGetVoteReplies({ voteId }); const { mutateAsync: createVoteReplyAsync } = useCreateVoteReplyMutation(); const { mutate: deleteVoteReply } = useDeleteVoteReplyMutation(); + const { mutate: toggleLikeVoteReply } = useLikeVoteReplyMutation(); const [sortOption, setSortOption] = useState('등록순'); @@ -53,7 +55,7 @@ const Replies = ({ voteId }: Props) => { {/* TODO: Suspense or SSR */} {status === 'pending' ? ( -
+
) : status === 'error' ? ( @@ -64,6 +66,9 @@ const Replies = ({ voteId }: Props) => { + toggleLikeVoteReply({ voteId: reply.voteId, commentId: reply.commentId }) + } onDelete={() => deleteVoteReply({ commentId: reply.commentId, diff --git a/src/app/vote/[slug]/_component/VoteDetail.tsx b/src/app/vote/[slug]/_component/VoteDetail.tsx index 655df19e..30660f9e 100644 --- a/src/app/vote/[slug]/_component/VoteDetail.tsx +++ b/src/app/vote/[slug]/_component/VoteDetail.tsx @@ -57,9 +57,7 @@ const VoteDetail = ({ voteId }: Props) => { { - toggleLike({ voteId, isLiked: !data.isLiked }); - }} + onClick={() => toggleLike({ voteId })} />
diff --git a/src/components/features/vote/reply/Reply.stories.tsx b/src/components/features/vote/reply/Reply.stories.tsx index 3c4e7c4e..c843e107 100644 --- a/src/components/features/vote/reply/Reply.stories.tsx +++ b/src/components/features/vote/reply/Reply.stories.tsx @@ -42,6 +42,7 @@ export const Basic: Story = { modifiedAt: '1709391770112', }} onDelete={() => {}} + onLikeToggle={() => {}} /> {}} + onLikeToggle={() => {}} /> {}} + onLikeToggle={() => {}} /> ), diff --git a/src/components/features/vote/reply/Reply.tsx b/src/components/features/vote/reply/Reply.tsx index f0bea621..26a18c3a 100644 --- a/src/components/features/vote/reply/Reply.tsx +++ b/src/components/features/vote/reply/Reply.tsx @@ -8,12 +8,13 @@ import { fromNowOf } from '@/utils/dates'; type Props = { reply: VoteReplyType; // NOTE: 다른 피쳐에서 댓글 사용 시 변경 필요 + onLikeToggle: () => void; onDelete: () => void; }; type BottomSheetType = 'askDelete' | 'replyOption'; -const Reply = ({ reply, onDelete }: Props) => { +const Reply = ({ reply, onLikeToggle, onDelete }: Props) => { const [openedSheet, setOpenedSheet] = useState(null); const { nickname, createdAt, content, likes, status } = reply; @@ -38,7 +39,7 @@ const Reply = ({ reply, onDelete }: Props) => { {content}
- {}} /> +
diff --git a/src/components/shared/likeButton/LikeButton.tsx b/src/components/shared/likeButton/LikeButton.tsx index 443e3095..73c3f028 100644 --- a/src/components/shared/likeButton/LikeButton.tsx +++ b/src/components/shared/likeButton/LikeButton.tsx @@ -3,17 +3,17 @@ import { Button } from '@/components/common/button'; type Props = { isLiked: boolean; likeCount: number; - clickHandler: () => void; + onClick: () => void; }; -const LikeButton = ({ isLiked, likeCount, clickHandler }: Props) => { +const LikeButton = ({ isLiked, likeCount, onClick }: Props) => { return ( diff --git a/src/hooks/vote/index.ts b/src/hooks/vote/index.ts index 6fd0b4cc..f5f47c2f 100644 --- a/src/hooks/vote/index.ts +++ b/src/hooks/vote/index.ts @@ -5,6 +5,7 @@ export { useGetVoteById } from './useGetVoteById'; export { useGetVoteBySearch } from './useGetVoteBySearch'; export { default as useGetVoteReplies } from './useGetVoteReplies'; export { default as useLikeVoteMutation } from './useLikeVoteMutation'; +export { default as useLikeVoteReplyMutation } from './useLikeVoteReplyMutation'; export { default as useUpdateVoteReplyMutation } from './useUpdateVoteReplyMutation'; export { default as useVotingMutation } from './useVotingMutation'; diff --git a/src/hooks/vote/useLikeVoteMutation.ts b/src/hooks/vote/useLikeVoteMutation.ts index 63ac5aff..0d3e558a 100644 --- a/src/hooks/vote/useLikeVoteMutation.ts +++ b/src/hooks/vote/useLikeVoteMutation.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; -import cloneDeep from 'lodash.clonedeep'; +import { produce } from 'immer'; import { post } from '@/lib/axios'; import { SuccessResponse } from '@/types/response'; @@ -7,16 +7,13 @@ import { VoteType } from '@/types/vote'; type PostLikeVoteRequest = { voteId: number; - isLiked: boolean; }; type LikeVoteResponse = undefined; // TODO api 분리 -const postLikeVote = async ({ voteId, isLiked }: PostLikeVoteRequest) => { - const response = await post>(`/vote/${voteId}/likes`, { - isLiked, - }); +const postLikeVote = async ({ voteId }: PostLikeVoteRequest) => { + const response = await post>(`/vote/${voteId}/likes`); return response.data.data; }; @@ -25,11 +22,11 @@ const useLikeVoteMutation = () => { return useMutation({ mutationFn: postLikeVote, - onMutate: async ({ voteId, isLiked }) => { + onMutate: async ({ voteId }) => { await queryClient.cancelQueries({ queryKey: ['vote', voteId] }); const previousVoteDetail = queryClient.getQueryData(['vote', voteId]); queryClient.setQueryData(['vote', voteId], (oldVoteDetail: VoteType) => - getOptimisticUpdatedLikesVoteDetailData(oldVoteDetail, isLiked), + getOptimisticUpdatedLikesVoteDetailData(oldVoteDetail), ); return { previousVoteDetail, voteId }; }, @@ -42,17 +39,16 @@ const useLikeVoteMutation = () => { }); }; -const getOptimisticUpdatedLikesVoteDetailData = (oldData: VoteType, isLiked: boolean) => { - const clonedOldVoteDetail = cloneDeep(oldData); - - if (isLiked === true) { - clonedOldVoteDetail.likes += 1; - } else { - clonedOldVoteDetail.likes -= 1; - } - clonedOldVoteDetail.isLiked = isLiked; - - return clonedOldVoteDetail; +const getOptimisticUpdatedLikesVoteDetailData = (oldData: VoteType) => { + return produce(oldData, (draft) => { + if (draft.isLiked === true) { + draft.isLiked = false; + draft.likes -= 1; + } else { + draft.isLiked = true; + draft.likes += 1; + } + }); }; export default useLikeVoteMutation; diff --git a/src/hooks/vote/useLikeVoteReplyMutation.ts b/src/hooks/vote/useLikeVoteReplyMutation.ts new file mode 100644 index 00000000..23d0bc21 --- /dev/null +++ b/src/hooks/vote/useLikeVoteReplyMutation.ts @@ -0,0 +1,64 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { produce } from 'immer'; + +import { useToast } from '@/hooks'; +import { post } from '@/lib/axios'; +import { SuccessResponse } from '@/types/response'; +import { VoteReplyType } from '@/types/vote'; + +type PostLikeVoteReplyRequest = { + voteId: number; + commentId: number; +}; + +type LikeVoteReplyResponse = undefined; + +const postLikeVoteReply = async ({ commentId }: PostLikeVoteReplyRequest) => { + const response = await post>( + `/comment/${commentId}/likes`, + ); + return response.data; +}; + +const useLikeVoteReplyMutation = () => { + const queryClient = useQueryClient(); + const toast = useToast(); + + return useMutation({ + mutationFn: postLikeVoteReply, + onMutate: async ({ voteId, commentId }) => { + await queryClient.cancelQueries({ queryKey: ['vote-reply', voteId] }); + const previousVoteReplies = queryClient.getQueryData(['vote-reply', voteId]); + queryClient.setQueryData(['vote-reply', voteId], (oldVoteReplies: VoteReplyType[]) => + getOptimisticUpdatedVoteRepliesData(oldVoteReplies, { commentId }), + ); + return { previousVoteReplies }; + }, + onError: (err, { voteId }, context) => { + queryClient.setQueryData(['vote-reply', voteId], context?.previousVoteReplies); + toast({ message: 'ERROR' }); + }, + onSettled: (data, err, { voteId }) => { + queryClient.invalidateQueries({ queryKey: ['vote-reply', voteId] }); + }, + }); +}; + +const getOptimisticUpdatedVoteRepliesData = ( + oldData: VoteReplyType[], + { commentId }: Pick, +) => { + return produce(oldData, (draft) => { + const targetReply = draft.find((reply) => reply.commentId === commentId); + if (!targetReply) return; + if (targetReply.status === true) { + targetReply.status = false; + targetReply.likes -= 1; + } else { + targetReply.status = true; + targetReply.likes += 1; + } + }); +}; + +export default useLikeVoteReplyMutation; diff --git a/yarn.lock b/yarn.lock index 2a3a47dd..88bb1e6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5205,15 +5205,6 @@ __metadata: languageName: node linkType: hard -"@types/lodash.clonedeep@npm:^4": - version: 4.5.9 - resolution: "@types/lodash.clonedeep@npm:4.5.9" - dependencies: - "@types/lodash": "npm:*" - checksum: 2f224ce9578046bccd1cd9594fb73540600ebd3d59a45695166a6123e2c376b84ab106b005a00453f357907f25bc8bfd2271b822be76e8f5527eadb4690b5e96 - languageName: node - linkType: hard - "@types/lodash.compact@npm:^3": version: 3.0.9 resolution: "@types/lodash.compact@npm:3.0.9" @@ -8483,7 +8474,6 @@ __metadata: "@tanstack/react-query-devtools": "npm:^5.17.18" "@toss/hangul": "npm:^1.6.1" "@types/github-label-sync": "npm:^2" - "@types/lodash.clonedeep": "npm:^4" "@types/lodash.compact": "npm:^3" "@types/lodash.debounce": "npm:^4.0.9" "@types/node": "npm:^20" @@ -8514,7 +8504,6 @@ __metadata: husky: "npm:^8.0.0" immer: "npm:^10.0.3" jest: "npm:^29.7.0" - lodash.clonedeep: "npm:^4.5.0" lodash.compact: "npm:^3.0.1" lodash.debounce: "npm:^4.0.8" modern-screenshot: "npm:^4.4.38" @@ -12595,13 +12584,6 @@ __metadata: languageName: node linkType: hard -"lodash.clonedeep@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.clonedeep@npm:4.5.0" - checksum: 2caf0e4808f319d761d2939ee0642fa6867a4bbf2cfce43276698828380756b99d4c4fa226d881655e6ac298dd453fe12a5ec8ba49861777759494c534936985 - languageName: node - linkType: hard - "lodash.compact@npm:^3.0.1": version: 3.0.1 resolution: "lodash.compact@npm:3.0.1"