From 4254681e0813a6f53721dae689800dd398056a15 Mon Sep 17 00:00:00 2001 From: hellosonic-r Date: Mon, 24 Jun 2024 15:27:35 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat=20:=20post=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=ED=82=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/queryKey.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants/queryKey.ts b/src/constants/queryKey.ts index 17afcb57..3874fb54 100644 --- a/src/constants/queryKey.ts +++ b/src/constants/queryKey.ts @@ -4,3 +4,4 @@ export const ME = "me"; export const NOTIFICATION = "notification"; export const CHATS = "chats"; export const CHAT = "chat"; +export const POST = "post"; From e4a0cb8831f964ca579f4170f973f78b1116b395 Mon Sep 17 00:00:00 2001 From: hellosonic-r Date: Mon, 24 Jun 2024 15:28:30 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat=20:=20=EB=94=94=EB=B0=94=EC=9A=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDebounce.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/hooks/useDebounce.ts diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 00000000..eba3f424 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,20 @@ +import { useCallback, useRef } from "react"; + +export const useDebounce = ( + callback: (...params: any) => void, + delay: number +) => { + const timer = useRef(null); + return useCallback( + (...params: any) => { + const later = () => { + clearTimeout(timer.current); + callback(...params); + }; + + clearTimeout(timer.current); + timer.current = setTimeout(later, delay); + }, + [callback, delay] + ); +}; From acff0aba3a96f05f4aa475498e377add62f3b88f Mon Sep 17 00:00:00 2001 From: hellosonic-r Date: Mon, 24 Jun 2024 15:29:59 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor=20:=20=EC=A2=8B=EC=95=84=EC=9A=94,?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20=EA=B8=B0=EB=8A=A5=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=9C=20=EB=82=99=EA=B4=80=EC=A0=81=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8,=20=EB=94=94=EB=B0=94=EC=9A=B4=EC=8B=B1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostDetailModal.controller.tsx | 79 +++++++++--- .../PostDetailModal/PostDetailModal.model.ts | 119 ++++++++++-------- 2 files changed, 131 insertions(+), 67 deletions(-) diff --git a/src/components/modalViews/PostDetailModal/PostDetailModal.controller.tsx b/src/components/modalViews/PostDetailModal/PostDetailModal.controller.tsx index 454c3603..f10e49d4 100644 --- a/src/components/modalViews/PostDetailModal/PostDetailModal.controller.tsx +++ b/src/components/modalViews/PostDetailModal/PostDetailModal.controller.tsx @@ -14,8 +14,11 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { useUI } from "@/components/common/uiContext"; import { notify } from "@/utils/toast"; import { ICreateComment } from "@/types"; -import { ME } from "@/constants/queryKey"; +import { ME, POST } from "@/constants/queryKey"; import { Spinner } from "@/components/common/Spinner"; +import { _GET } from "@/api"; +import { useQuery } from "@tanstack/react-query"; +import { useDebounce } from "@/hooks/useDebounce"; interface ModalProps { postId: string; @@ -65,13 +68,16 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => { }); const { data: myData } = useInitData(ME, "/auth-user"); - const { data: postData, isLoading } = useInitData( - `postId-${postId}`, - `/posts/${postId}` - ); - const userId = postData?.data.author._id; - const userName = postData?.data.author.fullName; - const imageUrl = postData?.data.image; + + const { data: postData, isLoading } = useQuery({ + queryKey: [POST, postId], + queryFn: async () => await _GET(`/posts/${postId}`), + gcTime: 0, + }); + + const userId = postData?.data?.author._id; + const userName = postData?.data?.author.fullName; + const imageUrl = postData?.data?.image; useEffect(() => { if (postData) { @@ -115,6 +121,7 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => { const { createLikeMutation, deleteLikeMutation } = useLikeMutation({ myId, userId, + postId, notify, setLikeInfo, likeDataBinding, @@ -145,13 +152,21 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => { if (confirm("포스트를 삭제할까요? 삭제 후에는 되돌릴 수 없습니다.")) { deletePostMutation.mutate({ id: postId }); closeModal(); - // TODO : 포스트 삭제 api통신 후 홈 화면이 리렌더링될 수 있도록 해야함 } }; const handleClose = () => { closeModal(); }; + + const changeServerFollowState = useDebounce(userId => { + if (!followInfo.isIFollowed) { + myId !== null && followMutation.mutate({ userId }); + } else { + unfollowMutation.mutate({ id: followInfo.followId }); + } + }, 400); + const handleFollow = () => { if (!myId) { if (confirm(`로그인이 필요합니다. 로그인 페이지로 이동할까요?`)) { @@ -161,15 +176,26 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => { return; } if (!followInfo.isIFollowed) { - myId !== null && followMutation.mutate({ userId }); + myId !== null && + setFollowInfo(prevState => ({ ...prevState, isIFollowed: true })); } else { - unfollowMutation.mutate({ id: followInfo.followId }); + setFollowInfo(prevState => ({ ...prevState, isIFollowed: false })); } + + changeServerFollowState(userId); }; const handleContentDetail = () => { setIsContentDetail(true); }; + const changeServerLikeState = useDebounce(postId => { + if (postData?.data.likes.some((like: any) => like.user === myId)) { + likeInfo.isILiked && deleteLikeMutation.mutate({ id: likeInfo.myLikeId }); + } else { + !likeInfo.isILiked && createLikeMutation.mutate({ postId: postId }); + } + }, 400); + const handleLike = () => { if (!myId) { if (confirm(`로그인이 필요합니다. 로그인 페이지로 이동할까요?`)) { @@ -178,16 +204,36 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => { } return; } - if (likeInfo.isILiked === true) { - deleteLikeMutation.mutate({ id: likeInfo.myLikeId }); - } - if (likeInfo.isILiked === false) { + + if (!likeInfo.isILiked) { + myId !== null && + setLikeInfo(prevState => ({ + ...prevState, + isILiked: true, + count: prevState.count + 1, + })); setHeartAnimation(previousState => ({ isShow: true, key: previousState.key + 1, })); - createLikeMutation.mutate({ postId: postId }); + notify({ + type: "success", + text: "좋아요를 눌렀어요!", + }); + } else { + setLikeInfo(prevState => ({ + ...prevState, + isILiked: false, + count: prevState.count - 1, + })); + notify({ + type: "default", + text: "좋아요를 취소했어요.", + }); } + + changeServerLikeState(postId); + console.log(likeInfo.isILiked); }; const handleChat = () => { @@ -251,6 +297,7 @@ const PostDetailModalController = ({ props }: IPostDetailModalProps) => { handleLike={handleLike} isILiked={likeInfo.isILiked} likeCount={likeInfo.count} + // likeCount={postData?.data.likes.length} toggleShowComments={toggleShowComments} handleChat={handleChat} register={register} diff --git a/src/components/modalViews/PostDetailModal/PostDetailModal.model.ts b/src/components/modalViews/PostDetailModal/PostDetailModal.model.ts index bfe0f649..228566e9 100644 --- a/src/components/modalViews/PostDetailModal/PostDetailModal.model.ts +++ b/src/components/modalViews/PostDetailModal/PostDetailModal.model.ts @@ -14,7 +14,8 @@ import { INotification, IUnfollow, } from "@/types"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { POST } from "@/constants/queryKey"; interface ILikeInfo { count: number; @@ -30,6 +31,7 @@ interface IFollowInfo { interface ILikeMuationProps { myId: string | null; userId: string; + postId: string; notify: ({ type, text }: IToastProps) => void; setLikeInfo: React.Dispatch>; likeDataBinding: (data: any) => void; @@ -74,19 +76,42 @@ export const useNotifyMutation = () => { export const useLikeMutation = ({ myId, userId, + postId, notify, - setLikeInfo, - likeDataBinding, }: ILikeMuationProps) => { + const queryClient = useQueryClient(); const notificationMutation = useNotifyMutation(); const createLikeMutation = useMutation({ mutationFn: async (formData: ICreateLike) => await _CREATE_LIKE(formData), - onMutate() { - setLikeInfo(prevState => ({ - ...prevState, - isILiked: true, - count: prevState.count + 1, - })); + onMutate: async ({ postId }) => { + queryClient.cancelQueries({ queryKey: [POST, postId] }); + const previousPostData: any = queryClient.getQueryData([POST, postId]); + + const previousLikes = previousPostData?.data?.likes.filter( + (like: any) => { + if (like.user !== myId) { + return like; + } + } + ); + + const newPostData = { + ...previousPostData?.data, + likes: [ + ...previousLikes, + { + createdAt: "", + updatedAt: "", + post: postId, + user: myId, + __v: 0, + _id: "", + }, + ], + }; + queryClient.setQueryData([POST, postId], { data: newPostData }); + + return { previousPostData }; }, onSuccess(data) { if (myId) { @@ -98,49 +123,51 @@ export const useLikeMutation = ({ }; notificationMutation.mutate(newNotification); + } + }, + onError: (_, __, context) => { + if (context?.previousPostData) { + queryClient.setQueryData([POST, postId], context.previousPostData); notify({ - type: "success", - text: "좋아요를 눌렀어요!", + type: "error", + text: "좋아요 생성에 실패했어요.", }); - likeDataBinding(data); - setLikeInfo(prevState => ({ ...prevState, myLikeId: data._id })); } }, - onError() { - notify({ - type: "error", - text: "좋아요 생성에 실패했어요.", - }); - setLikeInfo(prevState => ({ - ...prevState, - isILiked: false, - count: prevState.count - 1, - })); + onSettled() { + queryClient.invalidateQueries({ queryKey: [POST, postId] }); }, }); const deleteLikeMutation = useMutation({ mutationFn: async (formData: IDeleteLike) => await _DELETE_LIKE(formData), - onMutate() { - setLikeInfo(prevState => ({ - ...prevState, - isILiked: false, - count: prevState.count - 1, - })); - }, - onSuccess(data) { - notify({ - type: "default", - text: "좋아요를 취소했어요.", + onMutate: async ({ id }) => { + queryClient.cancelQueries({ queryKey: [POST, postId] }); + const previousPostData: any = queryClient.getQueryData([POST, postId]); + + const newLikes = previousPostData?.data?.likes.filter((like: any) => { + if (like._id !== id && like.user !== myId) { + return like; + } }); - likeDataBinding(data); + + const newPostData = { + ...previousPostData?.data, + likes: newLikes, + }; + + queryClient.setQueryData([POST, postId], { data: newPostData }); + + return { previousPostData }; }, - onError() { - setLikeInfo(prevState => ({ - ...prevState, - isILiked: true, - count: prevState.count + 1, - })); + + onError: (_, __, context) => { + if (context?.previousPostData) { + queryClient.setQueryData([POST, postId], context.previousPostData); + } + }, + onSettled() { + queryClient.invalidateQueries({ queryKey: [POST, postId] }); }, }); @@ -174,7 +201,6 @@ export const useCommentMutation = ({ notificationMutation.mutate(newNotification); setComments(newComments); - console.log(newComments); } }, onError(error) { @@ -189,7 +215,6 @@ export const useCommentMutation = ({ type: "default", text: "댓글을 삭제했어요.", }); - console.log("API : 댓글 삭제 성공", data); const newComments = comments.filter(({ _id }: any) => _id !== data._id); setComments(newComments); }, @@ -224,10 +249,6 @@ export const useFollowMutation = ({ notificationMutation.mutate(newNotification); } - notify({ - type: "success", - text: "팔로우를 성공했어요.", - }); setFollowInfo(prevState => ({ ...prevState, followId: data._id })); }, onError(error) { @@ -246,10 +267,6 @@ export const useFollowMutation = ({ setFollowInfo(prevState => ({ ...prevState, isIFollowed: false })); }, onSuccess() { - notify({ - type: "default", - text: "팔로우를 해제했어요.", - }); setFollowInfo(prevState => ({ ...prevState, followId: "" })); }, onError(error) {