Skip to content

Commit

Permalink
[FEAT] 투표 댓글 공감 기능 (#189)
Browse files Browse the repository at this point in the history
  • Loading branch information
jhsung23 committed Mar 7, 2024
1 parent 769fc8d commit f3eb26f
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 48 deletions.
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion src/app/vote/[slug]/_component/Replies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useCreateVoteReplyMutation,
useDeleteVoteReplyMutation,
useGetVoteReplies,
useLikeVoteReplyMutation,
} from '@/hooks/vote';
import { VoteReplyType } from '@/types/vote';

Expand All @@ -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<ReplySortOptions>('등록순');

Expand Down Expand Up @@ -53,7 +55,7 @@ const Replies = ({ voteId }: Props) => {

{/* TODO: Suspense or SSR */}
{status === 'pending' ? (
<div className="flex items-center justify-center py-lg">
<div className="flex h-full items-center justify-center py-lg">
<Spinner />
</div>
) : status === 'error' ? (
Expand All @@ -64,6 +66,9 @@ const Replies = ({ voteId }: Props) => {
<Reply
key={reply.commentId}
reply={reply}
onLikeToggle={() =>
toggleLikeVoteReply({ voteId: reply.voteId, commentId: reply.commentId })
}
onDelete={() =>
deleteVoteReply({
commentId: reply.commentId,
Expand Down
4 changes: 1 addition & 3 deletions src/app/vote/[slug]/_component/VoteDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ const VoteDetail = ({ voteId }: Props) => {
<LikeButton
isLiked={data.isLiked}
likeCount={data.likes}
clickHandler={() => {
toggleLike({ voteId, isLiked: !data.isLiked });
}}
onClick={() => toggleLike({ voteId })}
/>
<Button iconOnly icon="share" variant="empty" className="!p-0" />
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/components/features/vote/reply/Reply.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const Basic: Story = {
modifiedAt: '1709391770112',
}}
onDelete={() => {}}
onLikeToggle={() => {}}
/>
<Reply
reply={{
Expand All @@ -57,6 +58,7 @@ export const Basic: Story = {
modifiedAt: '1709391770112',
}}
onDelete={() => {}}
onLikeToggle={() => {}}
/>
<Reply
reply={{
Expand All @@ -72,6 +74,7 @@ export const Basic: Story = {
modifiedAt: '1709391770112',
}}
onDelete={() => {}}
onLikeToggle={() => {}}
/>
</>
),
Expand Down
5 changes: 3 additions & 2 deletions src/components/features/vote/reply/Reply.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<BottomSheetType | null>(null);

const { nickname, createdAt, content, likes, status } = reply;
Expand All @@ -38,7 +39,7 @@ const Reply = ({ reply, onDelete }: Props) => {
{content}
</Typography>
<div className="ml-md mt-5xs">
<LikeButton isLiked={status} likeCount={likes} clickHandler={() => {}} />
<LikeButton isLiked={status} likeCount={likes} onClick={onLikeToggle} />
</div>
</li>

Expand Down
6 changes: 3 additions & 3 deletions src/components/shared/likeButton/LikeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Button
icon={isLiked ? 'filledHeart' : 'heart'}
iconColor="primary-700"
variant="empty"
className="gap-6xs !p-0 text-[14px] text-gray-600"
onClick={clickHandler}
onClick={onClick}
>
{likeCount}
</Button>
Expand Down
1 change: 1 addition & 0 deletions src/hooks/vote/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

34 changes: 15 additions & 19 deletions src/hooks/vote/useLikeVoteMutation.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
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';
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<SuccessResponse<LikeVoteResponse>>(`/vote/${voteId}/likes`, {
isLiked,
});
const postLikeVote = async ({ voteId }: PostLikeVoteRequest) => {
const response = await post<SuccessResponse<LikeVoteResponse>>(`/vote/${voteId}/likes`);
return response.data.data;
};

Expand All @@ -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<VoteType>(['vote', voteId]);
queryClient.setQueryData(['vote', voteId], (oldVoteDetail: VoteType) =>
getOptimisticUpdatedLikesVoteDetailData(oldVoteDetail, isLiked),
getOptimisticUpdatedLikesVoteDetailData(oldVoteDetail),
);
return { previousVoteDetail, voteId };
},
Expand All @@ -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;
64 changes: 64 additions & 0 deletions src/hooks/vote/useLikeVoteReplyMutation.ts
Original file line number Diff line number Diff line change
@@ -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<SuccessResponse<LikeVoteReplyResponse>>(
`/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<VoteReplyType[]>(['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<PostLikeVoteReplyRequest, 'commentId'>,
) => {
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;
18 changes: 0 additions & 18 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit f3eb26f

Please sign in to comment.