diff --git a/src/apis/httpClient/httpClient.ts b/src/apis/httpClient/httpClient.ts index 65e0bc4c..d4a39bac 100644 --- a/src/apis/httpClient/httpClient.ts +++ b/src/apis/httpClient/httpClient.ts @@ -162,6 +162,7 @@ export default { user: new HttpClient("api/user", axiosConfig), timetable: new HttpClient("api/timetable", axiosConfig), post: new HttpClient("api/post/", axiosConfig), + recomment: new HttpClient("api/recomment", axiosConfig), comment: new HttpClient("api/comment", axiosConfig), refresh: new HttpClient("api/auth/refresh/access", axiosConfig), auth: new HttpClient("api/auth/", axiosConfig), diff --git a/src/constants/key.constant.ts b/src/constants/key.constant.ts index f893d0b5..7803cfc0 100644 --- a/src/constants/key.constant.ts +++ b/src/constants/key.constant.ts @@ -3,6 +3,7 @@ const KEY = { TIMETABLE: "useTimetable", POST: "usePost", COMMENT: "useComment", + RECOMMENT: "useRecomment", BAMBOO: "useBamboo", BAMBOO_ADMIN: "useBambooAdmin", } as const; diff --git a/src/helpers/checkTextOverflow.helper.ts b/src/helpers/checkTextOverflow.helper.ts new file mode 100644 index 00000000..31d84ee0 --- /dev/null +++ b/src/helpers/checkTextOverflow.helper.ts @@ -0,0 +1,12 @@ +const checkTextOverflow = (content: string) => { + const sentences = content.split("\n"); + const depth = sentences.length; + + if (depth > 4) { + const summaryContent = sentences.slice(0, 4).join("\n"); + return `${summaryContent}...`; + } + return content; +}; + +export default checkTextOverflow; diff --git a/src/helpers/getTextDepth.helper.ts b/src/helpers/getTextDepth.helper.ts new file mode 100644 index 00000000..dba97ae0 --- /dev/null +++ b/src/helpers/getTextDepth.helper.ts @@ -0,0 +1,6 @@ +const getTextDepth = (content: string) => { + const depth = content.split("\n").length; + return depth; +}; + +export default getTextDepth; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index c8eced3c..66367f2b 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -6,3 +6,5 @@ export { default as isAdmin } from "./isAdmin.helper"; export { default as filterInputPost } from "./filterInputPost.helper"; export { default as getWriteContentLabel } from "./getWriteContentLabel.helper"; export { default as checkPostValid } from "./checkPostValid.helper"; +export { default as checkTextOverflow } from "./checkTextOverflow.helper"; +export { default as getTextDepth } from "./getTextDepth.helper"; diff --git a/src/hooks/useTextarea.ts b/src/hooks/useTextarea.ts new file mode 100644 index 00000000..5addecb1 --- /dev/null +++ b/src/hooks/useTextarea.ts @@ -0,0 +1,48 @@ +import React from "react"; + +const useTextarea = (defaultContent?: string) => { + const [content, setContent] = React.useState(defaultContent || ""); + const [textareaHeight, setTextareaHeight] = React.useState({ + row: 2, + lineBreak: [] as Array, + }); + + const onInput = (e: React.ChangeEvent) => { + const { scrollHeight, clientHeight, value } = e.target; + + if (scrollHeight > clientHeight) { + setTextareaHeight((prev) => ({ + row: prev.row + 1, + lineBreak: { ...prev.lineBreak, [value.length - 1]: true }, + })); + } + if (textareaHeight.lineBreak[value.length]) { + setTextareaHeight((prev) => ({ + row: prev.row - 1, + lineBreak: { ...prev.lineBreak, [value.length]: false }, + })); + } + }; + + const onKeyEnter = ( + e: KeyboardEvent & React.ChangeEvent, + ) => { + if (e.code === "Enter") { + setTextareaHeight((prev) => ({ + row: prev.row + 1, + lineBreak: { ...prev.lineBreak, [e.target.value.length]: true }, + })); + } + }; + + return { + content, + setContent, + row: textareaHeight.row, + handleResizeTextAreaOnInput: onInput, + handleResizeTextareaKeyEnter: + onKeyEnter as unknown as React.KeyboardEventHandler, + }; +}; + +export default useTextarea; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 606c6bb1..98e27126 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -15,3 +15,4 @@ export type { default as IBambooPost } from "./bambooPost.interface"; export type { default as IPostInfiniteList } from "./postInfiniteList.interface"; export type { default as IInfiniteResult } from "./infiniteResult.interface"; export type { default as IInputPost } from "./inputPost.interface"; +export type { default as IRecomment } from "./recomment.interface"; diff --git a/src/interfaces/recomment.interface.ts b/src/interfaces/recomment.interface.ts new file mode 100644 index 00000000..cca86456 --- /dev/null +++ b/src/interfaces/recomment.interface.ts @@ -0,0 +1,5 @@ +import IComment from "./comment.interface"; + +export default interface IRecomment extends Omit { + commentId: number; +} diff --git a/src/page/forum-post/layouts/PostBody/CommentList.tsx b/src/page/forum-post/layouts/PostBody/Comment/CommentList.tsx similarity index 95% rename from src/page/forum-post/layouts/PostBody/CommentList.tsx rename to src/page/forum-post/layouts/PostBody/Comment/CommentList.tsx index 8dcf924d..ca94212c 100644 --- a/src/page/forum-post/layouts/PostBody/CommentList.tsx +++ b/src/page/forum-post/layouts/PostBody/Comment/CommentList.tsx @@ -5,7 +5,7 @@ import InfiniteScroll from "react-infinite-scroll-component"; import { PuffLoader } from "react-spinners"; import { IComment } from "@/interfaces"; import CommentListItem from "./CommentListItem"; -import { useCommentListQuery } from "../../services/query.service"; +import { useCommentListQuery } from "../../../services/query.service"; interface ICommentListBoxProps { postId: string; diff --git a/src/page/forum-post/layouts/PostBody/Comment/CommentListItem.tsx b/src/page/forum-post/layouts/PostBody/Comment/CommentListItem.tsx new file mode 100644 index 00000000..2589aad9 --- /dev/null +++ b/src/page/forum-post/layouts/PostBody/Comment/CommentListItem.tsx @@ -0,0 +1,298 @@ +import { AddCommentIcon, Arrow, LikeIcon } from "@/assets/icons"; +import { defaultProfile } from "@/assets/images"; +import { Column, Row } from "@/components/Flex"; +import { ImageWithFallback } from "@/components/atoms"; +import useDate from "@/hooks/useDate"; +import useUser from "@/hooks/useUser"; +import { checkTextOverflow, getTextDepth } from "@/helpers"; +import { IComment } from "@/interfaces"; +import { color, flex, font } from "@/styles"; +import React from "react"; +import { toast } from "react-toastify"; +import styled from "styled-components"; +import Swal from "sweetalert2"; +import { + useDeletePostCommentMutation, + useUpdateCommentLikeMutation, + useUpdatePostCommentMutation, +} from "../../../services/mutation.service"; +import CreateRecommentBox from "../Recomment/CreateRecommentBox"; +import RecommentList from "../Recomment/RecommentList"; + +interface ICommentListItemProps { + comment: IComment; +} + +const CommentListItem = ({ comment }: ICommentListItemProps) => { + const [isEditMode, setIsEditMode] = React.useState(false); + const [isDetailMode, setIsDetailMode] = React.useState(false); + const [isRecommentMode, setIsRecommentMode] = React.useState(false); + const [isViewRecommentMode, setIsViewRecommentMode] = React.useState(false); + + const { user } = useUser(); + const [editDetail, setEditDetail] = React.useState(comment.detail); + const [isLiked, setIsLiked] = React.useState(comment.doesLike); + const [currentLikeCount, setCurrentLikeCount] = React.useState( + comment.likeCount, + ); + const { formatDate } = useDate(); + const { mutate: updateCommentMutate } = useUpdatePostCommentMutation(); + const { mutate: deleteCommentMutate } = useDeletePostCommentMutation(); + const { mutate: updateCommentLikeMutate } = useUpdateCommentLikeMutation(); + + const handleUpdateCommentDetailClick = () => { + if (!editDetail) return toast.error("내용을 입력해주세요."); + updateCommentMutate({ + id: comment.id, + detail: editDetail, + }); + setIsEditMode(false); + }; + + const handleLikeButtonClick = async () => { + updateCommentLikeMutate(comment.id); + setIsLiked(!isLiked); + setCurrentLikeCount(isLiked ? currentLikeCount - 1 : currentLikeCount + 1); + }; + + const handleDeleteCommentDetailClick = async () => { + const { isConfirmed } = await Swal.fire({ + title: "게시글 삭제", + text: "해당 게시글을 삭제할까요?", + icon: "question", + showCancelButton: true, + confirmButtonText: "확인", + cancelButtonText: "취소", + }); + if (isConfirmed) deleteCommentMutate(comment.id); + }; + + return ( + + + + + + + + {comment.user.nickName} + + {formatDate(comment.createdAt)} + {comment.user.id === user.id && ( + + {isEditMode ? ( + <> + setIsEditMode(!isEditMode)} + > + 취소 + + + 수정 + + + ) : ( + <> + setIsEditMode(!isEditMode)} + > + 수정 + + + 삭제 + + + )} + + )} + + {isEditMode ? ( + setEditDetail(e.target.value)} + value={editDetail} + /> + ) : ( + + + {isDetailMode + ? comment.detail + : checkTextOverflow(comment.detail)} + + {getTextDepth(editDetail) > 4 && ( + setIsDetailMode(!isDetailMode)} + > + {isDetailMode ? "간략히" : "자세히 보기"} + + )} + + )} + + + + + {currentLikeCount} + + setIsRecommentMode(!isRecommentMode)}> + + 답글 + + + {isRecommentMode && ( + setIsRecommentMode(!isRecommentMode)} + /> + )} + {!!comment.reCommentCount && ( + setIsViewRecommentMode(!isViewRecommentMode)} + > + + + {comment.reCommentCount} + + + )} + {isViewRecommentMode && } + + + ); +}; + +const Container = styled.div` + padding: 20px 0; + display: flex; + gap: 10px; +`; + +const ProfileImage = styled.div` + padding: 2px; + border-radius: 50%; + width: 44px; + height: 100%; + ${flex.CENTER}; +`; + +const CommentWriter = styled.span` + ${font.caption}; + font-weight: 600; + color: ${color.gray}; +`; + +const CommentCreatedAt = styled.span` + ${font.caption}; + color: ${color.gray}; +`; + +const CommentDetail = styled.p` + ${font.p3}; + white-space: pre-wrap; +`; + +const CommentSeparator = styled.span` + &:after { + content: "·"; + } +`; + +const StyledBox = styled.div` + ${flex.HORIZONTAL}; + gap: 4px; + cursor: pointer; + padding: 2px 6px; + + &:hover { + background-color: ${color.on_tertiary}; + border-radius: 999px; + } +`; + +const StyledText = styled.span` + ${font.p3}; + color: ${color.gray}; +`; + +const CommentButtonBox = styled.div` + display: flex; + margin-left: auto; + gap: 6px; +`; + +const CommentButton = styled.button<{ color: string }>` + background-color: ${(props) => props.color}; + ${font.caption}; + color: ${color.white}; + padding: 2px 10px; + border-radius: 4px; +`; + +const CommentTextArea = styled.textarea` + border: 2px solid ${color.on_tertiary}; + border-radius: 4px; + padding: 6px 12px; + margin: 6px 0; + ${font.p3}; +`; + +const DetailViewButton = styled.button` + border: none; + width: fit-content; + color: ${color.gray}; + ${font.caption}; + border-radius: 999px; + + &:hover { + text-decoration: underline; + } +`; + +const RecommentViewButton = styled.button` + width: fit-content; + ${flex.CENTER}; + gap: 6px; + margin-top: 6px; + padding: 8px 10px 4px 10px; + border-radius: 999px; + + &:hover { + background-color: ${color.on_tertiary}; + } +`; + +const RecommentViewCountText = styled.span` + color: ${color.primary_blue}; + ${font.caption}; + font-weight: 600; + margin-top: -4px; + + &:before { + content: "답글 "; + } + + &:after { + content: "개"; + } +`; + +export default CommentListItem; diff --git a/src/page/forum-post/layouts/PostBody/CreateCommentBox.tsx b/src/page/forum-post/layouts/PostBody/Comment/CreateCommentBox.tsx similarity index 94% rename from src/page/forum-post/layouts/PostBody/CreateCommentBox.tsx rename to src/page/forum-post/layouts/PostBody/Comment/CreateCommentBox.tsx index 71a9f5c7..5df3a8a7 100644 --- a/src/page/forum-post/layouts/PostBody/CreateCommentBox.tsx +++ b/src/page/forum-post/layouts/PostBody/Comment/CreateCommentBox.tsx @@ -4,10 +4,10 @@ import { color, font } from "@/styles"; import { Emoji } from "@/assets/icons"; import useEmoji from "@/hooks/useEmoji"; import { EmojiModal } from "@/components/common"; -import { useCreatePostCommentMutation } from "../../services/mutation.service"; +import { useCreatePostCommentMutation } from "../../../services/mutation.service"; interface ICreateCommentBoxProps { - postId: string; + postId: number; } const CreateCommentBox = ({ postId: id }: ICreateCommentBoxProps) => { diff --git a/src/page/forum-post/layouts/PostBody/Recomment/CreateRecommentBox.tsx b/src/page/forum-post/layouts/PostBody/Recomment/CreateRecommentBox.tsx new file mode 100644 index 00000000..cf89adc9 --- /dev/null +++ b/src/page/forum-post/layouts/PostBody/Recomment/CreateRecommentBox.tsx @@ -0,0 +1,124 @@ +import { defaultProfile } from "@/assets/images"; +import { Column, Row } from "@/components/Flex"; +import { ImageWithFallback } from "@/components/atoms"; +import useTextarea from "@/hooks/useTextarea"; +import useUser from "@/hooks/useUser"; +import { useCreateRecommentMutation } from "@/page/forum-post/services/mutation.service"; +import { color, font } from "@/styles"; +import React from "react"; +import styled from "styled-components"; + +interface ICreateRecommentBoxProps { + handleModeCancelClick: () => void; + id: number; +} + +const CreateRecommentBox = ({ + handleModeCancelClick, + id, +}: ICreateRecommentBoxProps) => { + const { + content: detail, + setContent, + row, + handleResizeTextAreaOnInput, + handleResizeTextareaKeyEnter, + } = useTextarea(""); + const { user } = useUser(); + const { mutate } = useCreateRecommentMutation(); + const textareaRef = React.useRef(null); + + const handleCreateRecommentClick = () => { + mutate({ id, detail }); + handleModeCancelClick(); + }; + + React.useEffect(() => { + if (textareaRef.current) textareaRef.current.focus(); + }, [textareaRef]); + + return ( + + + + + + setContent(e.target.value)} + value={detail} + onInput={handleResizeTextAreaOnInput} + onKeyDown={handleResizeTextareaKeyEnter} + row={row} + /> + + + + + + + ); +}; + +const Container = styled.div` + display: flex; + padding: 10px 0; + align-items: center; + gap: 6px; +`; + +const ProfileImage = styled.div` + height: 100%; +`; + +const RecommentTextArea = styled.textarea<{ row: number }>` + border-bottom: 2px solid ${color.on_tertiary}; + width: 100%; + height: ${({ row }) => `${(row || 1) * 14}px`}; + padding-left: 8px; + ${font.caption}; +`; + +const CancelButton = styled.button` + ${font.caption}; + border: none; + border-radius: 999px; + width: fit-content; + padding: 4px 14px; + margin-left: auto; + + &:after { + content: "취소"; + } + + &:hover { + background-color: ${color.on_tertiary}; + } +`; + +const CreateButton = styled.button` + ${font.caption}; + border: none; + border-radius: 999px; + width: fit-content; + padding: 2px 14px; + background-color: ${color.primary_blue}; + color: ${color.white}; + + &:after { + content: "작성"; + } + + &:hover { + opacity: 0.8; + } +`; + +export default CreateRecommentBox; diff --git a/src/page/forum-post/layouts/PostBody/Recomment/RecommentList.tsx b/src/page/forum-post/layouts/PostBody/Recomment/RecommentList.tsx new file mode 100644 index 00000000..2b31e758 --- /dev/null +++ b/src/page/forum-post/layouts/PostBody/Recomment/RecommentList.tsx @@ -0,0 +1,63 @@ +import { IRecomment } from "@/interfaces"; +import { useRecommentListQuery } from "@/page/forum-post/services/query.service"; +import { color, flex } from "@/styles"; +import React from "react"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { PuffLoader } from "react-spinners"; +import styled from "styled-components"; +import RecommentListItem from "./RecommentListItem"; + +interface IRecommentListBoxProps { + commentId: number; +} + +const RecommentList = ({ commentId }: IRecommentListBoxProps) => { + const { + data: recommentList, + fetchNextPage, + hasNextPage, + } = useRecommentListQuery({ commentId }); + + return ( + + data).length || 0} + next={fetchNextPage} + hasMore={hasNextPage || false} + loader={ + + + + } + > + {recommentList?.map((recomments) => ( + + {recomments.entity.map((recomment: IRecomment) => ( + + ))} + + ))} + + + ); +}; + +const Container = styled.div` + width: 100%; + height: 100%; + background-color: ${color.white}; +`; + +const RecommentListBox = styled.div` + width: 100%; + height: 100%; + ${flex.COLUMN}; +`; + +const LoadingBox = styled.div` + margin-top: 20px; + width: 100%; + ${flex.CENTER}; +`; + +export default RecommentList; diff --git a/src/page/forum-post/layouts/PostBody/CommentListItem.tsx b/src/page/forum-post/layouts/PostBody/Recomment/RecommentListItem.tsx similarity index 69% rename from src/page/forum-post/layouts/PostBody/CommentListItem.tsx rename to src/page/forum-post/layouts/PostBody/Recomment/RecommentListItem.tsx index 69d016de..e94988b6 100644 --- a/src/page/forum-post/layouts/PostBody/CommentListItem.tsx +++ b/src/page/forum-post/layouts/PostBody/Recomment/RecommentListItem.tsx @@ -1,37 +1,41 @@ -import { AddCommentIcon, LikeIcon } from "@/assets/icons"; +import { LikeIcon } from "@/assets/icons"; import { defaultProfile } from "@/assets/images"; import { Column, Row } from "@/components/Flex"; import { ImageWithFallback } from "@/components/atoms"; import useDate from "@/hooks/useDate"; import useUser from "@/hooks/useUser"; -import { IComment } from "@/interfaces"; +import { checkTextOverflow, getTextDepth } from "@/helpers"; +import { IRecomment } from "@/interfaces"; import { color, flex, font } from "@/styles"; import React from "react"; import { toast } from "react-toastify"; import styled from "styled-components"; import Swal from "sweetalert2"; import { - useDeletePostCommentMutation, - useUpdateCommentLikeMutation, - useUpdatePostCommentMutation, -} from "../../services/mutation.service"; + useDeleteRecommentMutation, + useUpdateRecommentLikeMutation, + useUpdateRecommentMutation, +} from "../../../services/mutation.service"; interface ICommentListItemProps { - comment: IComment; + recomment: IRecomment; } -const CommentListItem = ({ comment }: ICommentListItemProps) => { +const RecommentListItem = ({ recomment }: ICommentListItemProps) => { const [isEditMode, setIsEditMode] = React.useState(false); + const [isDetailMode, setIsDetailMode] = React.useState(false); + const { user } = useUser(); - const [editDetail, setEditDetail] = React.useState(comment.detail); - const [isLiked, setIsLiked] = React.useState(comment.doesLike); + const [editDetail, setEditDetail] = React.useState(recomment.detail); + const [isLiked, setIsLiked] = React.useState(recomment.doesLike); const [currentLikeCount, setCurrentLikeCount] = React.useState( - comment.likeCount, + recomment.likeCount, ); const { formatDate } = useDate(); - const { mutate: updateCommentMutate } = useUpdatePostCommentMutation(); - const { mutate: deleteCommentMutate } = useDeletePostCommentMutation(); - const { mutate: updateCommentLikeMutate } = useUpdateCommentLikeMutation(); + const { mutate: updateRecommentMutate } = useUpdateRecommentMutation(); + const { mutate: deleteRecommentMutate } = useDeleteRecommentMutation(); + const { mutate: updateRecommentLikeMutate } = + useUpdateRecommentLikeMutation(); const handleChangeEditModeClick = () => { setIsEditMode(!isEditMode); @@ -39,15 +43,15 @@ const CommentListItem = ({ comment }: ICommentListItemProps) => { const handleUpdateCommentDetailClick = () => { if (!editDetail) return toast.error("내용을 입력해주세요."); - updateCommentMutate({ - id: comment.id, + updateRecommentMutate({ + id: recomment.id, detail: editDetail, }); setIsEditMode(false); }; const handleLikeButtonClick = async () => { - updateCommentLikeMutate(comment.id); + updateRecommentLikeMutate(recomment.id); setIsLiked(!isLiked); setCurrentLikeCount(isLiked ? currentLikeCount - 1 : currentLikeCount + 1); }; @@ -61,28 +65,30 @@ const CommentListItem = ({ comment }: ICommentListItemProps) => { confirmButtonText: "확인", cancelButtonText: "취소", }); - if (isConfirmed) deleteCommentMutate(comment.id); + if (isConfirmed) deleteRecommentMutate(recomment.id); }; return ( - + - {comment.user.nickName} + {recomment.user.nickName} - {formatDate(comment.createdAt)} - {comment.user.id === user.id && ( + + {formatDate(recomment.createdAt)} + + {recomment.user.id === user.id && ( {isEditMode ? ( <> @@ -124,7 +130,20 @@ const CommentListItem = ({ comment }: ICommentListItemProps) => { value={editDetail} /> ) : ( - {comment.detail} + + + {isDetailMode + ? recomment.detail + : checkTextOverflow(recomment.detail)} + + {getTextDepth(editDetail) > 4 && ( + setIsDetailMode(!isDetailMode)} + > + {isDetailMode ? "간략히" : "자세히 보기"} + + )} + )} @@ -132,10 +151,6 @@ const CommentListItem = ({ comment }: ICommentListItemProps) => { {currentLikeCount} - - - 답글 - @@ -143,7 +158,7 @@ const CommentListItem = ({ comment }: ICommentListItemProps) => { }; const Container = styled.div` - padding: 20px 0; + padding: 8px 0; display: flex; gap: 10px; `; @@ -169,6 +184,7 @@ const CommentCreatedAt = styled.span` const CommentDetail = styled.p` ${font.p3}; + white-space: pre-wrap; `; const CommentSeparator = styled.span` @@ -216,4 +232,16 @@ const CommentTextArea = styled.textarea` ${font.p3}; `; -export default CommentListItem; +const DetailViewButton = styled.button` + border: none; + width: fit-content; + color: ${color.gray}; + ${font.caption}; + border-radius: 999px; + + &:hover { + text-decoration: underline; + } +`; + +export default RecommentListItem; diff --git a/src/page/forum-post/layouts/PostBody/index.tsx b/src/page/forum-post/layouts/PostBody/index.tsx index a638ce7c..725f9e52 100644 --- a/src/page/forum-post/layouts/PostBody/index.tsx +++ b/src/page/forum-post/layouts/PostBody/index.tsx @@ -6,8 +6,8 @@ import { CustomViewer } from "@/components/atoms"; import { color, flex, font } from "@/styles"; import CountBox from "./CountBox"; import SectionBox from "./SectionBox"; -import CreateCommentBox from "./CreateCommentBox"; -import CommentList from "./CommentList"; +import CreateCommentBox from "./Comment/CreateCommentBox"; +import CommentList from "./Comment/CommentList"; interface IPostBodyProps { post: IPost; @@ -63,7 +63,7 @@ const PostBody = ({ post }: IPostBodyProps) => { {post.commentCount} - + {post.id !== "-1" && } diff --git a/src/page/forum-post/services/api.service.ts b/src/page/forum-post/services/api.service.ts index 712bc8a8..73c9ba26 100644 --- a/src/page/forum-post/services/api.service.ts +++ b/src/page/forum-post/services/api.service.ts @@ -6,35 +6,18 @@ interface IGetPostCommentListProps { pageParam: number; } -export const getPostCommentList = async ({ - id, - pageParam: page, -}: IGetPostCommentListProps) => { - const { data } = await httpClient.comment.getById({ params: { id, page } }); - return data; -}; - -interface ICreatePostCommentProps { - id: string; - detail: string; +interface IGetRecommentListProps { + id: number; + pageParam: number; } -export const createPostComment = async ({ - id, - detail, -}: ICreatePostCommentProps) => { - const { data } = await httpClient.comment.postById( - { detail }, - { params: { id } }, - ); - return data; -}; - -interface IUpdatePostCommentProps { +interface IPostCommentProps { id: number; detail: string; } +// like + export const updatePostLike = async (id: string) => { const { data } = await httpClient.like.put({ type: LIKE.POST, @@ -59,7 +42,25 @@ export const updateRecommentLike = async (id: number) => { return data; }; -export const updatePostComment = async (comment: IUpdatePostCommentProps) => { +// comment + +export const getPostCommentList = async ({ + id, + pageParam: page, +}: IGetPostCommentListProps) => { + const { data } = await httpClient.comment.getById({ params: { id, page } }); + return data; +}; + +export const createPostComment = async ({ id, detail }: IPostCommentProps) => { + const { data } = await httpClient.comment.postById( + { detail }, + { params: { id } }, + ); + return data; +}; + +export const updatePostComment = async (comment: IPostCommentProps) => { const { data } = await httpClient.comment.put(comment); return data; }; @@ -70,3 +71,35 @@ export const deletePostComment = async (id: number) => { }); return data; }; + +// recomment + +export const getRecommentList = async ({ + id, + pageParam: page, +}: IGetRecommentListProps) => { + const { data } = await httpClient.recomment.getById({ params: { id, page } }); + return data; +}; + +export const createRecomment = async ({ id, detail }: IPostCommentProps) => { + const { data } = await httpClient.recomment.postById( + { detail }, + { + params: { id }, + }, + ); + return data; +}; + +export const updateRecomment = async (comment: IPostCommentProps) => { + const { data } = await httpClient.recomment.put(comment); + return data; +}; + +export const deleteRecomment = async (id: number) => { + const { data } = await httpClient.recomment.deleteById({ + params: { id }, + }); + return data; +}; diff --git a/src/page/forum-post/services/mutation.service.ts b/src/page/forum-post/services/mutation.service.ts index d6593a91..7656d59b 100644 --- a/src/page/forum-post/services/mutation.service.ts +++ b/src/page/forum-post/services/mutation.service.ts @@ -9,46 +9,63 @@ import { useRouter } from "next/navigation"; import { toast } from "react-toastify"; import { createPostComment, + createRecomment, deletePostComment, + deleteRecomment, updateCommentLike, updatePostComment, updatePostLike, + updateRecomment, + updateRecommentLike, } from "./api.service"; -export const useDeletePostMutation = () => { +interface IUsePostCommentMutationProps { + id: number; + detail: string; +} + +// like + +export const useUpdatePostLikeMutation = () => { const apolloClient = useApolloClient(); - const router = useRouter(); - const mutations = useApolloMutation(DELETE_POST, { - onCompleted: () => { + return useMutation((id: string) => updatePostLike(id), { + onSuccess: () => { apolloClient.cache.reset(); - toast.success("글이 삭제되었습니다!"); - router.push(ROUTER.POST.LIST); }, }); - return mutations; }; -interface IUseCreateCommentMutationProps { - id: string; - detail: string; -} +export const useUpdateCommentLikeMutation = () => { + return useMutation((id: number) => updateCommentLike(id)); +}; -export const useUpdatePostLikeMutation = () => { +export const useUpdateRecommentLikeMutation = () => { + return useMutation((id: number) => updateRecommentLike(id)); +}; + +// post +export const useDeletePostMutation = () => { const apolloClient = useApolloClient(); + const router = useRouter(); - return useMutation((id: string) => updatePostLike(id), { - onSuccess: () => { + const mutations = useApolloMutation(DELETE_POST, { + onCompleted: () => { apolloClient.cache.reset(); + toast.success("글이 삭제되었습니다!"); + router.push(ROUTER.POST.LIST); }, }); + return mutations; }; +// comment + export const useCreatePostCommentMutation = () => { const queryClient = useQueryClient(); return useMutation( - (props: IUseCreateCommentMutationProps) => createPostComment(props), + (comment: IUsePostCommentMutationProps) => createPostComment(comment), { onSuccess: () => { toast.success("댓글을 작성했어요!"); @@ -58,16 +75,11 @@ export const useCreatePostCommentMutation = () => { ); }; -interface IUseUpdatePostCommentMutationProps { - id: number; - detail: string; -} - export const useUpdatePostCommentMutation = () => { const queryClient = useQueryClient(); return useMutation( - (comment: IUseUpdatePostCommentMutationProps) => updatePostComment(comment), + (comment: IUsePostCommentMutationProps) => updatePostComment(comment), { onSuccess: () => { toast.success("댓글이 수정되었어요!"); @@ -88,6 +100,44 @@ export const useDeletePostCommentMutation = () => { }); }; -export const useUpdateCommentLikeMutation = () => { - return useMutation((id: number) => updateCommentLike(id)); +// recomment + +export const useCreateRecommentMutation = () => { + const queryClient = useQueryClient(); + + return useMutation( + (comment: IUsePostCommentMutationProps) => createRecomment(comment), + { + onSuccess: () => { + toast.success("답글을 작성했어요!"); + queryClient.invalidateQueries([KEY.RECOMMENT]); + queryClient.invalidateQueries([KEY.COMMENT]); + }, + }, + ); +}; + +export const useUpdateRecommentMutation = () => { + const queryClient = useQueryClient(); + + return useMutation( + (comment: IUsePostCommentMutationProps) => updateRecomment(comment), + { + onSuccess: () => { + toast.success("답글이 수정되었어요!"); + queryClient.invalidateQueries([KEY.RECOMMENT]); + }, + }, + ); +}; + +export const useDeleteRecommentMutation = () => { + const queryClient = useQueryClient(); + + return useMutation((id: number) => deleteRecomment(id), { + onSuccess: () => { + toast.success("답글이 삭제되었어요."); + queryClient.invalidateQueries([KEY.RECOMMENT]); + }, + }); }; diff --git a/src/page/forum-post/services/query.service.ts b/src/page/forum-post/services/query.service.ts index 8fae2407..c324291a 100644 --- a/src/page/forum-post/services/query.service.ts +++ b/src/page/forum-post/services/query.service.ts @@ -3,7 +3,7 @@ import { GET_POST } from "@/gql/post/queries"; import { useQuery as useApolloQuery } from "@apollo/client"; import { useInfiniteQuery } from "@tanstack/react-query"; import { KEY } from "@/constants"; -import { getPostCommentList } from "./api.service"; +import { getPostCommentList, getRecommentList } from "./api.service"; export const usePostQuery = ({ id }: IPostQuery) => { const { data, ...queryRest } = useApolloQuery(GET_POST({ id }), { @@ -17,6 +17,10 @@ interface IUseCommentListQueryProps { postId: string; } +interface IUseRecommentQueryProps { + commentId: number; +} + export const useCommentListQuery = ({ postId: id, }: IUseCommentListQueryProps) => { @@ -31,3 +35,18 @@ export const useCommentListQuery = ({ }); return { data: data?.pages, ...queryRest }; }; + +export const useRecommentListQuery = ({ + commentId: id, +}: IUseRecommentQueryProps) => { + const { data, ...queryRest } = useInfiniteQuery({ + queryKey: [KEY.RECOMMENT], + queryFn: ({ pageParam = 0 }) => getRecommentList({ id, pageParam }), + getNextPageParam: (lastPage) => { + return lastPage.currentPage !== lastPage.totalPage - 1 + ? lastPage.currentPage + 1 + : undefined; + }, + }); + return { data: data?.pages, ...queryRest }; +};