Skip to content

Commit

Permalink
[FE] Feat/#597 댓글 기능 구현 (#598)
Browse files Browse the repository at this point in the history
* feat: 댓글 기능 컴포넌트 구현

* feat: 핀 상세보기 페이지 댓글기능 반영

* chore: 자동완성 컴포넌트 lint

* refacotr: api 응답 명세 반영

* feat: 대댓글 구현

* feat: 수정 삭제 기능 구현

* refactor: 수정하기 기능 수정

* refactor: 수정 삭제 권한 반영

* refactor: 댓글 재요청 로직 수정

* refactor :수정 삭제시 commentId에서 pinId로 변경

* refactor: fetch로직 수정

* refactor: 에러 수정

* refactor: default prod url 수정

* refactor: 리뷰 반영
  • Loading branch information
jiwonh423 authored Oct 19, 2023
1 parent d7fc2e0 commit d0eab4d
Show file tree
Hide file tree
Showing 5 changed files with 429 additions and 42 deletions.
35 changes: 35 additions & 0 deletions frontend/src/components/common/Input/ReplyComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import SingleComment from './SingleComment';

interface ReplyCommentProps {
commentList: Comment[];
pageTotalCommentList: Comment[];
depth: number;
refetch: (pinId: number) => Promise<Comment[]>;
}

function ReplyComment({
commentList,
pageTotalCommentList,
depth,
refetch,
}: ReplyCommentProps) {
if (depth === 2) return null;
return (
<>
{commentList.length > 0 &&
commentList.map((comment) => (
<>
<SingleComment
comment={comment}
commentList={commentList}
totalList={pageTotalCommentList}
depth={depth}
refetch={refetch}
/>
</>
))}
</>
);
}

export default ReplyComment;
275 changes: 275 additions & 0 deletions frontend/src/components/common/Input/SingleComment.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('');
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<HTMLButtonElement>) => {
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<HTMLDivElement>) => {
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<HTMLInputElement>) => {
setContent(e.target.value);
};

const onClickModifyConfirmBtn = async (
e: React.MouseEvent<HTMLButtonElement>,
) => {
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 (
<CommentWrapper depth={depth} key={comment.id}>
<Flex $gap="12px">
<ProfileImage
src={comment.creatorImageUrl}
width="40px"
height="40px"
/>
<CommentInfo>
<Flex $justifyContent="space-between">
<div>
<Writer>
<Text $fontSize="default" $fontWeight="bold" color="black">
@{comment.creator}
</Text>
</Writer>
</div>
{comment.canChange && (
<Flex $gap="8px" cursor="pointer">
<Text
$fontSize="small"
color="gray"
$fontWeight="bold"
onClick={onClickModifyBtn}
>
수정
</Text>
<Text
$fontSize="small"
color="primary"
$fontWeight="bold"
onClick={onClickDeleteBtn}
>
삭제
</Text>
</Flex>
)}
</Flex>
<Content>
{isEditing ? (
<div
style={{ width: '100%', display: 'flex', marginTop: '12px' }}
>
<CustomInput
value={content}
onChange={onContentChange}
placeholder="댓글 수정"
/>
<ConfirmCommentButton
variant="secondary"
style={{ width: '60px', padding: '4px 12px' }}
// type="button"
onClick={onClickModifyConfirmBtn}
>
등록
</ConfirmCommentButton>
</div>
) : (
<Text $fontSize="default" $fontWeight="normal" color="darkGray">
{comment.content}
</Text>
)}
</Content>
<div>
{depth === 1 ? null : (
<div
onClick={toggleReplyOpen}
style={{ cursor: 'pointer', marginBottom: '8px' }}
>
<Text color="black" $fontSize="small" $fontWeight="bold">
답글 작성
</Text>
</div>
)}
{replyOpen && (
<div style={{ display: 'flex', gap: '12px' }}>
<ProfileImage
src={user?.imageUrl || ''}
width="40px"
height="40px"
/>
<div style={{ width: '100%' }}>
<CustomInput
value={newComment}
onChange={(e: any) => setNewComment(e.target.value)}
placeholder="댓글 추가"
// onClick={toggleReplyOpen}
/>
<ConfirmCommentButton
variant="secondary"
style={{}}
// type="button"
onClick={onClickCommentBtn}
>
등록
</ConfirmCommentButton>
</div>
</div>
)}
</div>
{replyCount > 0 && (
<Flex>
<ProfileImage
src={replyList[0].creatorImageUrl || ''}
width="28px"
height="28px"
/>
<MoreReplyButton onClick={toggleSeeMore}>
{seeMore ? '\u25B2' : '\u25BC'} 답글 {replyCount}
</MoreReplyButton>
</Flex>
)}
</CommentInfo>
</Flex>

{seeMore && (
<ReplyComment
commentList={replyList ?? []}
parentId={comment.id}
pageTotalCommentList={totalList}
depth={depth + 1}
refetch={refetch}
/>
)}
</CommentWrapper>
);
}

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;
}
`;
Loading

0 comments on commit d0eab4d

Please sign in to comment.