Skip to content

Commit

Permalink
New : 댓글 작성, 조회 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
jobkaeHenry committed Dec 3, 2023
1 parent 8cec864 commit 0459c2a
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 29 deletions.
53 changes: 30 additions & 23 deletions client/src/components/Navigation/NavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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(
() => [
{
Expand Down Expand Up @@ -45,35 +55,32 @@ const NavigationBar = () => {
],
[userInfo]
);
return (
<Paper sx={WrapperStyle} elevation={0}>
<BottomNavigation value={path} showLabels sx={BtnStyle}>
{NavbarData.map(({ label, href, iconComponent, ...others }) => {
return (
<BottomNavigationAction
icon={iconComponent as any}
key={String(label)}
component={href ? Link : "button"}
href={href}
value={href}
label={label}
{...others}
/>
);
})}
</BottomNavigation>
</Paper>
return isVisible ? (
<BottomNavigation value={path} showLabels sx={BtnStyle}>
{NavbarData.map(({ label, href, iconComponent, ...others }) => {
return (
<BottomNavigationAction
icon={iconComponent as any}
key={String(label)}
component={href ? Link : "button"}
href={href}
value={href}
label={label}
{...others}
/>
);
})}
</BottomNavigation>
) : (
<></>
);
};

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",
Expand Down
38 changes: 38 additions & 0 deletions client/src/components/post/detail/PostCommentDropdown.tsx
Original file line number Diff line number Diff line change
@@ -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 | HTMLElement>(null);
const open = Boolean(anchorEl);

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};

return (
<>
<ButtonBase aria-label="settings" sx={{ p: 0 }} onClick={handleClick}>
<MoreVertOutlined />
</ButtonBase>
<Menu open={open} anchorEl={anchorEl} onClose={handleClose}>
<MenuItem
onClick={() => {
if (confirm("정말 삭제하시겠습니까?")) {
console.log("눌림");
}
}}
>
삭제
</MenuItem>
<MenuItem>수정</MenuItem>
</Menu>
</>
);
};

export default PostCommentDropdown;
83 changes: 83 additions & 0 deletions client/src/components/post/detail/PostCommentInput.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Paper
sx={{
backgroundColor: "gray.primary",
border: "1px solid",
borderColor: "gray.secondary",
position: "fixed",
bottom: isEditing ? 0 : "44px",
borderRadius: 1.5,
left: 0,
right: 0,
p: 2,
pb: isEditing ? 2 : 3.5,
}}
>
<TextField
fullWidth
onFocus={() => {
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: (
<InputAdornment
position="end"
onClick={(e) => {
e.stopPropagation();
submitHandler(inputValue)
}}
sx={{
color: inputValue.length > 0 ? "primary.main" : "text.disabled",
}}
>
<SubmitCommentIcon />
</InputAdornment>
),
}}
sx={{ backgroundColor: "background.paper" }}
/>
</Paper>
);
};

export default PostCommentInput;
16 changes: 10 additions & 6 deletions client/src/const/serverPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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의 게시글의 좋아요 취소를 요청
Expand Down
29 changes: 29 additions & 0 deletions client/src/queries/post/comment/useGetCommentQuery.ts
Original file line number Diff line number Diff line change
@@ -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<PostCommentListInterface>(POST_COMMENT(id));
return data;
};

export const commentQueryKey = {
all: ["comment"] as const,
byId: (id?: string) => ["comment", { id }] as const,
};

export default useGetCommentQuery;
71 changes: 71 additions & 0 deletions client/src/queries/post/comment/useNewPostCommentMutation.ts
Original file line number Diff line number Diff line change
@@ -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<PostCommentListInterface>(
commentQueryKey.byId(id)
);

queryClient.setQueryData<PostCommentListInterface>(
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;

0 comments on commit 0459c2a

Please sign in to comment.