From d0eab4d9ea68c19bb0fffd0cc0a574a18e2a59b6 Mon Sep 17 00:00:00 2001 From: afds4567 <33995840+afds4567@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:06:20 +0900 Subject: [PATCH] =?UTF-8?q?[FE]=20Feat/#597=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 댓글 기능 컴포넌트 구현 * feat: 핀 상세보기 페이지 댓글기능 반영 * chore: 자동완성 컴포넌트 lint * refacotr: api 응답 명세 반영 * feat: 대댓글 구현 * feat: 수정 삭제 기능 구현 * refactor: 수정하기 기능 수정 * refactor: 수정 삭제 권한 반영 * refactor: 댓글 재요청 로직 수정 * refactor :수정 삭제시 commentId에서 pinId로 변경 * refactor: fetch로직 수정 * refactor: 에러 수정 * refactor: default prod url 수정 * refactor: 리뷰 반영 --- .../components/common/Input/ReplyComment.tsx | 35 +++ .../components/common/Input/SingleComment.tsx | 275 ++++++++++++++++++ frontend/src/pages/PinDetail.tsx | 129 ++++++-- frontend/src/pages/UpdatedPinDetail.tsx | 23 +- frontend/src/types/Comment.ts | 9 + 5 files changed, 429 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/common/Input/ReplyComment.tsx create mode 100644 frontend/src/components/common/Input/SingleComment.tsx create mode 100644 frontend/src/types/Comment.ts diff --git a/frontend/src/components/common/Input/ReplyComment.tsx b/frontend/src/components/common/Input/ReplyComment.tsx new file mode 100644 index 00000000..f40065d2 --- /dev/null +++ b/frontend/src/components/common/Input/ReplyComment.tsx @@ -0,0 +1,35 @@ +import SingleComment from './SingleComment'; + +interface ReplyCommentProps { + commentList: Comment[]; + pageTotalCommentList: Comment[]; + depth: number; + refetch: (pinId: number) => Promise; +} + +function ReplyComment({ + commentList, + pageTotalCommentList, + depth, + refetch, +}: ReplyCommentProps) { + if (depth === 2) return null; + return ( + <> + {commentList.length > 0 && + commentList.map((comment) => ( + <> + + + ))} + + ); +} + +export default ReplyComment; diff --git a/frontend/src/components/common/Input/SingleComment.tsx b/frontend/src/components/common/Input/SingleComment.tsx new file mode 100644 index 00000000..c8fb143f --- /dev/null +++ b/frontend/src/components/common/Input/SingleComment.tsx @@ -0,0 +1,275 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import { useState } from 'react'; +import styled from 'styled-components'; + +import { deleteApi } from '../../../apis/deleteApi'; +import { postApi } from '../../../apis/postApi'; +import { putApi } from '../../../apis/putApi'; +import useToast from '../../../hooks/useToast'; +import { ConfirmCommentButton, CustomInput } from '../../../pages/PinDetail'; +import Flex from '../Flex'; +import Text from '../Text'; +import ReplyComment from './ReplyComment'; + +const localStorageUser = localStorage?.getItem('user'); +const user = JSON.parse(localStorageUser || '{}'); +// { 댓글, 댓글목록, 전체목록, depth = 0 } +function SingleComment({ + comment, + commentList, + totalList, + depth = 0, + refetch, +}: any) { + const [replyOpen, setReplyOpen] = useState(false); + const [seeMore, setSeeMore] = useState(false); + const [newComment, setNewComment] = useState(''); + const { showToast } = useToast(); + const [isEditing, setIsEditing] = useState(false); + const [content, setContent] = useState(comment.content); + const params = new URLSearchParams(window.location.search); + const pinDetail = params.get('pinDetail'); + + const toggleReplyOpen = () => { + setReplyOpen((prev) => !prev); + }; + const toggleSeeMore = () => { + setSeeMore((prev) => !prev); + }; + + const replyList = commentList?.filter( + (curComment: any) => curComment.parentPinCommentId === comment.id, + ); + const replyCount = replyList.length; + + const onClickCommentBtn = async (e: React.MouseEvent) => { + e.stopPropagation(); + + try { + // 댓글 추가 + // comment 값이랑 추가 정보 body에 담아서 보내기 + + await postApi( + `/pins/comments`, + { + pinId: Number(pinDetail), + content: newComment, + parentPinCommentId: comment.id, + }, + 'application/json', + ); + await refetch(Number(pinDetail)); + setReplyOpen(false); + setNewComment(''); + showToast('info', '댓글이 추가되었습니다.'); + } catch (e) { + showToast('error', '댓글을 다시 작성해주세요'); + } + }; + + const onClickDeleteBtn = async (e: React.MouseEvent) => { + e.stopPropagation(); + try { + // 댓글 삭제 + await deleteApi(`/pins/comments/${comment.id}`); + await refetch(Number(pinDetail)); + showToast('info', '댓글이 삭제되었습니다.'); + } catch (e) { + console.error(e); + showToast('error', '댓글을 다시 작성해주세요'); + } + }; + + const onContentChange = (e: React.ChangeEvent) => { + setContent(e.target.value); + }; + + const onClickModifyConfirmBtn = async ( + e: React.MouseEvent, + ) => { + e.stopPropagation(); + try { + // 댓글 수정 + await putApi(`/pins/comments/${comment.id}`, { + content, + }); + await refetch(Number(pinDetail)); + setIsEditing(false); + showToast('info', '댓글이 수정되었습니다.'); + } catch (e) { + console.error(e); + showToast('error', '댓글을 다시 작성해주세요'); + } + }; + + const onClickModifyBtn = () => { + setIsEditing((prev) => !prev); + }; + + return ( + + + + + +
+ + + @{comment.creator} + + +
+ {comment.canChange && ( + + + 수정 + + + 삭제 + + + )} +
+ + {isEditing ? ( +
+ + + 등록 + +
+ ) : ( + + {comment.content} + + )} +
+
+ {depth === 1 ? null : ( +
+ + 답글 작성 + +
+ )} + {replyOpen && ( +
+ +
+ setNewComment(e.target.value)} + placeholder="댓글 추가" + // onClick={toggleReplyOpen} + /> + + 등록 + +
+
+ )} +
+ {replyCount > 0 && ( + + + + {seeMore ? '\u25B2' : '\u25BC'} 답글 {replyCount}개 + + + )} +
+
+ + {seeMore && ( + + )} +
+ ); +} + +export default SingleComment; + +const CommentWrapper = styled.li<{ depth: number }>` + margin-left: ${(props) => props.depth * 20}px; + margin-top: 12px; + list-style: none; +`; + +export const ProfileImage = styled.img` + display: block; + + border-radius: 50%; +`; + +const CommentInfo = styled.div` + flex: 1; +`; + +const Writer = styled.div` + white-space: nowrap; +`; + +const Content = styled.div` + overflow-wrap: anywhere; +`; + +const MoreReplyButton = styled.button` + padding: 2px; + border: none; + background: none; + border-radius: 8px; + color: black; + font-weight: 600; + &:hover { + background: gray; + cursor: pointer; + } +`; diff --git a/frontend/src/pages/PinDetail.tsx b/frontend/src/pages/PinDetail.tsx index f4f43ba1..1ae547da 100644 --- a/frontend/src/pages/PinDetail.tsx +++ b/frontend/src/pages/PinDetail.tsx @@ -8,6 +8,9 @@ import UpdateBtnSVG from '../assets/updateBtn.svg'; import Box from '../components/common/Box'; import Button from '../components/common/Button'; import Flex from '../components/common/Flex'; +import SingleComment, { + ProfileImage, +} from '../components/common/Input/SingleComment'; import Space from '../components/common/Space'; import Text from '../components/common/Text'; import Modal from '../components/Modal'; @@ -17,6 +20,7 @@ import { ModalContext } from '../context/ModalContext'; import useCompressImage from '../hooks/useCompressImage'; import useFormValues from '../hooks/useFormValues'; import useToast from '../hooks/useToast'; +import theme from '../themes'; import { ModifyPinFormProps } from '../types/FormValues'; import { PinProps } from '../types/Pin'; import UpdatedPinDetail from './UpdatedPinDetail'; @@ -28,7 +32,9 @@ interface PinDetailProps { setIsEditPinDetail: React.Dispatch>; } -const userToken = localStorage.getItem('userToken'); +const userToken = localStorage?.getItem('userToken'); +const localStorageUser = localStorage?.getItem('user'); +const user = JSON.parse(localStorageUser || '{}'); function PinDetail({ width, @@ -38,6 +44,8 @@ function PinDetail({ }: PinDetailProps) { const [searchParams, setSearchParams] = useSearchParams(); const [pin, setPin] = useState(null); + const [commentList, setCommentList] = useState([]); // 댓글 리스트 + const [newComment, setNewComment] = useState(''); const { showToast } = useToast(); const { formValues, @@ -72,6 +80,7 @@ function PinDetail({ useEffect(() => { getPinData(); + setCurrentPageCommentList(pinId); }, [pinId, searchParams]); const onClickEditPin = () => { @@ -119,6 +128,40 @@ function PinDetail({ getPinData(); }; + // 댓글 구현 부분 + const setCurrentPageCommentList = async (pinId: number) => { + const data = await getApi(`/pins/${pinId}/comments`); + setCommentList(data); + return data; + }; + + useEffect(() => { + setCurrentPageCommentList(pinId); + }, []); + + const onClickCommentBtn = async (e: React.MouseEvent) => { + e.stopPropagation(); + + try { + // 댓글 추가 + // comment 값이랑 추가 정보 body에 담아서 보내기 + await postApi( + `/pins/comments`, + { + pinId, + content: newComment, + parentPinCommentId: null, + }, + 'application/json', + ); + + setCurrentPageCommentList(pinId); + setNewComment(''); + showToast('info', '댓글이 추가되었습니다.'); + } catch { + showToast('error', '댓글을 다시 작성해주세요'); + } + }; if (!pin) return <>; if (isEditPinDetail) @@ -144,9 +187,7 @@ function PinDetail({ {pin.name} - - {pin.creator} @@ -164,9 +205,7 @@ function PinDetail({ - - 파일업로드 - - - 어디에 있나요? @@ -198,21 +234,50 @@ function PinDetail({ {pin.description} - + {/* Comment Section */} - - - 내 지도에 저장하기 - - - - 공유하기 - - + + 어떻게 생각하나요?{' '} + + + {userToken && ( +
+ +
+ ) => + setNewComment(e.target.value) + } + placeholder="댓글 추가" + /> + + 등록 + +
+
+ )} + {commentList?.length > 0 && + commentList.map( + (comment) => + !comment.parentPinCommentId ? ( + + ) : null, // <-- comment.parentPinCommentId가 존재하는 경우 null 반환 + )} + {/* comment section END */} - theme.fontSize.extraSmall}; + font-weight: ${({ theme }) => theme.fontWeight.bold}; + + box-shadow: 8px 8px 8px 0px rgba(69, 69, 69, 0.15); + + margin-top: 12px; + float: right; + font-size: 12px; +`; export default PinDetail; diff --git a/frontend/src/pages/UpdatedPinDetail.tsx b/frontend/src/pages/UpdatedPinDetail.tsx index 71edf779..46069be7 100644 --- a/frontend/src/pages/UpdatedPinDetail.tsx +++ b/frontend/src/pages/UpdatedPinDetail.tsx @@ -73,27 +73,6 @@ function UpdatedPinDetail({ return ( - - - - + 사진을 추가해주시면 더 알찬 정보를 제공해줄 수 있을 것 같아요. - - -