diff --git a/client/src/components/Navigation/NavigationBar.tsx b/client/src/components/Navigation/NavigationBar.tsx index fef2927..122b669 100644 --- a/client/src/components/Navigation/NavigationBar.tsx +++ b/client/src/components/Navigation/NavigationBar.tsx @@ -5,8 +5,15 @@ import HomeIcon from "~/assets/icons/HomeIcon.svg"; import SearchIcon from "~/assets/icons/SearchIcon.svg"; import PostIcon from "~/assets/icons/PostIcon.svg"; import BeverageIcon from "~/assets/icons/BeverageIcon.svg"; +import { useGlobalNavbarVisibility } from "@/store/useGlobalNavbarVisibility"; -import HOME, { MY_PROFILE, NEW_POST, SEARCH, SIGNIN, WIKI } from "@/const/clientPath"; +import HOME, { + MY_PROFILE, + NEW_POST, + SEARCH, + SIGNIN, + WIKI, +} from "@/const/clientPath"; import Link from "next/link"; import { usePathname } from "next/navigation"; import NavbarUserImage from "@/components/Navigation/NavbarUserImage"; @@ -16,6 +23,9 @@ import { useMyInfoQuery } from "@/queries/auth/useMyInfoQuery"; const NavigationBar = () => { const path = usePathname(); const { data: userInfo } = useMyInfoQuery(); + + const isVisible = useGlobalNavbarVisibility(({ isVisible }) => isVisible); + const NavbarData = useMemo( () => [ { @@ -45,35 +55,32 @@ const NavigationBar = () => { ], [userInfo] ); - return ( - - - {NavbarData.map(({ label, href, iconComponent, ...others }) => { - return ( - - ); - })} - - + return isVisible ? ( + + {NavbarData.map(({ label, href, iconComponent, ...others }) => { + return ( + + ); + })} + + ) : ( + <> ); }; -const WrapperStyle = { +const BtnStyle = { position: "fixed", bottom: 0, left: 0, right: 0, - borderRadius: 0, -}; -const BtnStyle = { borderRadius: "12px 12px 0 0", border: "1px solid", borderBottom: "none", diff --git a/client/src/components/post/detail/PostCommentDropdown.tsx b/client/src/components/post/detail/PostCommentDropdown.tsx new file mode 100644 index 0000000..734c0c4 --- /dev/null +++ b/client/src/components/post/detail/PostCommentDropdown.tsx @@ -0,0 +1,38 @@ +import { ButtonBase, Menu, MenuItem } from "@mui/material"; +import { MoreVertOutlined } from "@mui/icons-material"; +import { useState } from "react"; + +const PostCommentDropdown = () => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + + + { + if (confirm("정말 삭제하시겠습니까?")) { + console.log("눌림"); + } + }} + > + 삭제 + + 수정 + + + ); +}; + +export default PostCommentDropdown; diff --git a/client/src/components/post/detail/PostCommentInput.tsx b/client/src/components/post/detail/PostCommentInput.tsx new file mode 100644 index 0000000..328eafe --- /dev/null +++ b/client/src/components/post/detail/PostCommentInput.tsx @@ -0,0 +1,83 @@ +"use client"; +import { InputAdornment, Paper, TextField } from "@mui/material"; +import { useCallback, useContext, useState } from "react"; +import SubmitCommentIcon from "@/assets/icons/comment/SubmitCommentIcon.svg"; +import { useGlobalNavbarVisibility } from "@/store/useGlobalNavbarVisibility"; +import PostDetailPageContext from "@/store/post/PostDetailPageContext"; +import useNewPostCommentMutation from "@/queries/post/comment/useNewPostCommentMutation"; + +const PostCommentInput = () => { + const setIsShowingNavbar = useGlobalNavbarVisibility( + ({ setIsVisible }) => setIsVisible + ); + const { data: currentData } = useContext(PostDetailPageContext); + const [isEditing, setIsEditing] = useState(false); + const [inputValue, setInputValue] = useState(""); + + const { mutateAsync: submitForm } = useNewPostCommentMutation( + currentData?.postNo ? String(currentData?.postNo) : undefined + ); + const submitHandler = useCallback( + (content: string) => { + submitForm(content).then(() => { + setInputValue(""); + }); + }, + [submitForm, setInputValue] + ); + + return ( + + { + setIsShowingNavbar(false); + setIsEditing(true); + }} + onBlur={() => { + setIsShowingNavbar(true); + setIsEditing(false); + }} + size="small" + autoComplete="off" + placeholder="회원님의 생각을 올려보세요" + multiline + value={inputValue} + onChange={(e) => setInputValue(e.target.value)} + rows={isEditing ? 5 : 1} + InputProps={{ + endAdornment: ( + { + e.stopPropagation(); + submitHandler(inputValue) + }} + sx={{ + color: inputValue.length > 0 ? "primary.main" : "text.disabled", + }} + > + + + ), + }} + sx={{ backgroundColor: "background.paper" }} + /> + + ); +}; + +export default PostCommentInput; diff --git a/client/src/const/serverPath.ts b/client/src/const/serverPath.ts index fe1135f..15aa942 100644 --- a/client/src/const/serverPath.ts +++ b/client/src/const/serverPath.ts @@ -30,6 +30,10 @@ export const LOGOUT_BFF = "/api/auth/logout-internal" as const; * 게시물리스트를 받아오거나, 작성하는 Path */ export const POST_LIST = "/posts" as const; +/** + * 게시물 pk 를 입력받아 댓글을 조회,생성 하는 URL + */ +export const POST_COMMENT = (pk:string) => `${POST_LIST}/${pk}/comments` /** * 게시물리스트를 받아오거나, 작성하는 Path 버전2 (Breaking Change) @@ -39,7 +43,11 @@ export const POST_LIST_V2 = "/posts/v2" as const; * ID(pk) 를 입력받아 해당 포스트를 지우는 URL */ export const REMOVE_POST = (pk: number) => `${POST_LIST}/${pk}` as const; - +/** + * 포스트의 PK를 입력받아 해당 PK의 게시글의 좋아요를 요청 + * @param id 게시글의 PK + */ +export const POST_LIKE_URL = (id: string) => `/posts/like/${id}` as const; /** * * @param type : 리소스의 타입 POST|PROFILE|ALCOHOL @@ -63,11 +71,7 @@ export const REMOVE_FILE = (attachNo: string) => `/attach/${attachNo}` as const; */ export const GET_ALCOHOL_LIST = "/alcohols" as const; -/** - * 포스트의 PK를 입력받아 해당 PK의 게시글의 좋아요를 요청 - * @param id 게시글의 PK - */ -export const POST_LIKE_URL = (id: string) => `/posts/like/${id}` as const; + /** * 포스트의 PK를 입력받아 해당 PK의 게시글의 좋아요 취소를 요청 diff --git a/client/src/queries/post/comment/useGetCommentQuery.ts b/client/src/queries/post/comment/useGetCommentQuery.ts new file mode 100644 index 0000000..a1d8704 --- /dev/null +++ b/client/src/queries/post/comment/useGetCommentQuery.ts @@ -0,0 +1,29 @@ +import { POST_COMMENT } from "@/const/serverPath"; +import axios from "@/libs/axios"; +import PostCommentListInterface from "@/types/post/PostCommentInterface"; +import { useSuspenseQuery } from "@tanstack/react-query"; + +interface CommentQueryInterface { + postNo: string; +} + +const useGetCommentQuery = ({ postNo }: CommentQueryInterface) => { + return useSuspenseQuery({ + queryKey: commentQueryKey.byId(postNo), + queryFn: async () => await getCommentListQueryFn(postNo), + }); +}; + +export const getCommentListQueryFn = async ( + id: CommentQueryInterface["postNo"] +) => { + const { data } = await axios.get(POST_COMMENT(id)); + return data; +}; + +export const commentQueryKey = { + all: ["comment"] as const, + byId: (id?: string) => ["comment", { id }] as const, +}; + +export default useGetCommentQuery; diff --git a/client/src/queries/post/comment/useNewPostCommentMutation.ts b/client/src/queries/post/comment/useNewPostCommentMutation.ts new file mode 100644 index 0000000..789bfe9 --- /dev/null +++ b/client/src/queries/post/comment/useNewPostCommentMutation.ts @@ -0,0 +1,71 @@ +import { POST_COMMENT } from "@/const/serverPath"; +import useAxiosPrivate from "@/hooks/useAxiosPrivate"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { commentQueryKey } from "./useGetCommentQuery"; +import PostCommentListInterface from "@/types/post/PostCommentInterface"; +import { useErrorHandler } from "@/utils/errorHandler"; +import { useMyInfoQuery } from "@/queries/auth/useMyInfoQuery"; + +const useNewPostCommentMutation = (id?: string) => { + const queryClient = useQueryClient(); + const errorHandler = useErrorHandler(); + const { data: myInfo } = useMyInfoQuery(); + + return useMutation({ + mutationFn: async (content: string) => { + if (!id) { + return Promise.reject("id가 제공되지않음"); + } else return postComment(id, content); + }, + onMutate: (content) => { + queryClient.cancelQueries({ queryKey: commentQueryKey.byId(id) }); + + const querySnapShot = queryClient.getQueryData( + commentQueryKey.byId(id) + ); + + queryClient.setQueryData( + commentQueryKey.byId(id), + (prev) => { + return { + list: [ + { + commentNo: Number.MAX_SAFE_INTEGER, + commentContent: content, + createdDate: String(new Date()), + lastModifiedDate: String(new Date()), + createdBy: myInfo?.nickname, + userId: myInfo?.id, + nickname: myInfo?.nickname, + profileImgUrls: myInfo?.profileImages, + }, + ...(prev?.list ?? []), + ] as PostCommentListInterface["list"], + totalCount: (prev?.totalCount ?? 0) + 1, + }; + } + ); + return { querySnapShot }; + }, + onError: (err, queryFnParams, context) => { + errorHandler(err); + queryClient.setQueryData( + commentQueryKey.byId(queryFnParams), + context?.querySnapShot + ); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: commentQueryKey.byId(id) }); + }, + }); +}; +export const postComment = async (postNo: string, content: string) => { + const axiosPrivate = useAxiosPrivate(); + const { data } = await axiosPrivate.post<{ commentNo: string }>( + POST_COMMENT(postNo), + { commentContent: content } + ); + return data; +}; + +export default useNewPostCommentMutation;