Skip to content

Commit a9fe7c2

Browse files
committed
chore(recomment): 답글목록 컴포넌트 구현
1 parent 40cfc57 commit a9fe7c2

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { IRecomment } from "@/interfaces";
2+
import { useRecommentListQuery } from "@/page/forum-post/services/query.service";
3+
import { color, flex } from "@/styles";
4+
import React from "react";
5+
import InfiniteScroll from "react-infinite-scroll-component";
6+
import { PuffLoader } from "react-spinners";
7+
import styled from "styled-components";
8+
import RecommentListItem from "./RecommentListItem";
9+
10+
interface IRecommentListBoxProps {
11+
commentId: number;
12+
}
13+
14+
const RecommentList = ({ commentId }: IRecommentListBoxProps) => {
15+
const {
16+
data: recommentList,
17+
fetchNextPage,
18+
hasNextPage,
19+
} = useRecommentListQuery({ commentId });
20+
21+
return (
22+
<Container>
23+
<InfiniteScroll
24+
dataLength={recommentList?.flatMap(({ data }) => data).length || 0}
25+
next={fetchNextPage}
26+
hasMore={hasNextPage || false}
27+
loader={
28+
<LoadingBox>
29+
<PuffLoader size={40} />
30+
</LoadingBox>
31+
}
32+
>
33+
{recommentList?.map((recomments) => (
34+
<RecommentListBox>
35+
{recomments.entity.map((recomment: IRecomment) => (
36+
<RecommentListItem key={recomment.id} recomment={recomment} />
37+
))}
38+
</RecommentListBox>
39+
))}
40+
</InfiniteScroll>
41+
</Container>
42+
);
43+
};
44+
45+
const Container = styled.div`
46+
width: 100%;
47+
height: 100%;
48+
background-color: ${color.white};
49+
`;
50+
51+
const RecommentListBox = styled.div`
52+
width: 100%;
53+
height: 100%;
54+
${flex.COLUMN};
55+
`;
56+
57+
const LoadingBox = styled.div`
58+
margin-top: 20px;
59+
width: 100%;
60+
${flex.CENTER};
61+
`;
62+
63+
export default RecommentList;
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import { LikeIcon } from "@/assets/icons";
2+
import { defaultProfile } from "@/assets/images";
3+
import { Column, Row } from "@/components/Flex";
4+
import { ImageWithFallback } from "@/components/atoms";
5+
import useDate from "@/hooks/useDate";
6+
import useUser from "@/hooks/useUser";
7+
import { checkTextOverflow, getTextDepth } from "@/helpers";
8+
import { IRecomment } from "@/interfaces";
9+
import { color, flex, font } from "@/styles";
10+
import React from "react";
11+
import { toast } from "react-toastify";
12+
import styled from "styled-components";
13+
import Swal from "sweetalert2";
14+
import {
15+
useDeleteRecommentMutation,
16+
useUpdateRecommentLikeMutation,
17+
useUpdateRecommentMutation,
18+
} from "../../../services/mutation.service";
19+
20+
interface ICommentListItemProps {
21+
recomment: IRecomment;
22+
}
23+
24+
const RecommentListItem = ({ recomment }: ICommentListItemProps) => {
25+
const [isEditMode, setIsEditMode] = React.useState(false);
26+
const [isDetailMode, setIsDetailMode] = React.useState(false);
27+
28+
const { user } = useUser();
29+
const [editDetail, setEditDetail] = React.useState(recomment.detail);
30+
const [isLiked, setIsLiked] = React.useState(recomment.doesLike);
31+
const [currentLikeCount, setCurrentLikeCount] = React.useState(
32+
recomment.likeCount,
33+
);
34+
const { formatDate } = useDate();
35+
const { mutate: updateRecommentMutate } = useUpdateRecommentMutation();
36+
const { mutate: deleteRecommentMutate } = useDeleteRecommentMutation();
37+
const { mutate: updateRecommentLikeMutate } =
38+
useUpdateRecommentLikeMutation();
39+
40+
const handleChangeEditModeClick = () => {
41+
setIsEditMode(!isEditMode);
42+
};
43+
44+
const handleUpdateCommentDetailClick = () => {
45+
if (!editDetail) return toast.error("내용을 입력해주세요.");
46+
updateRecommentMutate({
47+
id: recomment.id,
48+
detail: editDetail,
49+
});
50+
setIsEditMode(false);
51+
};
52+
53+
const handleLikeButtonClick = async () => {
54+
updateRecommentLikeMutate(recomment.id);
55+
setIsLiked(!isLiked);
56+
setCurrentLikeCount(isLiked ? currentLikeCount - 1 : currentLikeCount + 1);
57+
};
58+
59+
const handleDeleteCommentDetailClick = async () => {
60+
const { isConfirmed } = await Swal.fire({
61+
title: "게시글 삭제",
62+
text: "해당 게시글을 삭제할까요?",
63+
icon: "question",
64+
showCancelButton: true,
65+
confirmButtonText: "확인",
66+
cancelButtonText: "취소",
67+
});
68+
if (isConfirmed) deleteRecommentMutate(recomment.id);
69+
};
70+
71+
return (
72+
<Container>
73+
<ProfileImage>
74+
<ImageWithFallback
75+
src={recomment.user.profileImage}
76+
fallbackSrc={defaultProfile}
77+
alt="프로필"
78+
rounded
79+
width={34}
80+
height={34}
81+
/>
82+
</ProfileImage>
83+
<Column gap="4px" width="100%">
84+
<Column justifyContent="center" width="100%">
85+
<Row gap="4px" width="100%">
86+
<CommentWriter>{recomment.user.nickName}</CommentWriter>
87+
<CommentSeparator />
88+
<CommentCreatedAt>
89+
{formatDate(recomment.createdAt)}
90+
</CommentCreatedAt>
91+
{recomment.user.id === user.id && (
92+
<CommentButtonBox>
93+
{isEditMode ? (
94+
<>
95+
<CommentButton
96+
color={color.primary_red}
97+
onClick={handleChangeEditModeClick}
98+
>
99+
취소
100+
</CommentButton>
101+
<CommentButton
102+
color={color.primary_blue}
103+
onClick={handleUpdateCommentDetailClick}
104+
>
105+
수정
106+
</CommentButton>
107+
</>
108+
) : (
109+
<>
110+
<CommentButton
111+
color={color.primary_blue}
112+
onClick={handleChangeEditModeClick}
113+
>
114+
수정
115+
</CommentButton>
116+
<CommentButton
117+
color={color.primary_red}
118+
onClick={handleDeleteCommentDetailClick}
119+
>
120+
삭제
121+
</CommentButton>
122+
</>
123+
)}
124+
</CommentButtonBox>
125+
)}
126+
</Row>
127+
{isEditMode ? (
128+
<CommentTextArea
129+
onChange={(e) => setEditDetail(e.target.value)}
130+
value={editDetail}
131+
/>
132+
) : (
133+
<Column gap="4px">
134+
<CommentDetail>
135+
{isDetailMode
136+
? recomment.detail
137+
: checkTextOverflow(recomment.detail)}
138+
</CommentDetail>
139+
{getTextDepth(editDetail) > 4 && (
140+
<DetailViewButton
141+
onClick={() => setIsDetailMode(!isDetailMode)}
142+
>
143+
{isDetailMode ? "간략히" : "자세히 보기"}
144+
</DetailViewButton>
145+
)}
146+
</Column>
147+
)}
148+
</Column>
149+
<Row gap="6px">
150+
<StyledBox onClick={handleLikeButtonClick}>
151+
<LikeIcon isLiked={isLiked} />
152+
<StyledText>{currentLikeCount}</StyledText>
153+
</StyledBox>
154+
</Row>
155+
</Column>
156+
</Container>
157+
);
158+
};
159+
160+
const Container = styled.div`
161+
padding: 8px 0;
162+
display: flex;
163+
gap: 10px;
164+
`;
165+
166+
const ProfileImage = styled.div`
167+
padding: 2px;
168+
border-radius: 50%;
169+
width: 44px;
170+
height: 100%;
171+
${flex.CENTER};
172+
`;
173+
174+
const CommentWriter = styled.span`
175+
${font.caption};
176+
font-weight: 600;
177+
color: ${color.gray};
178+
`;
179+
180+
const CommentCreatedAt = styled.span`
181+
${font.caption};
182+
color: ${color.gray};
183+
`;
184+
185+
const CommentDetail = styled.p`
186+
${font.p3};
187+
white-space: pre-wrap;
188+
`;
189+
190+
const CommentSeparator = styled.span`
191+
&:after {
192+
content: "·";
193+
}
194+
`;
195+
196+
const StyledBox = styled.div`
197+
${flex.HORIZONTAL};
198+
gap: 4px;
199+
cursor: pointer;
200+
padding: 2px 6px;
201+
202+
&:hover {
203+
background-color: ${color.on_tertiary};
204+
border-radius: 999px;
205+
}
206+
`;
207+
208+
const StyledText = styled.span`
209+
${font.p3};
210+
color: ${color.gray};
211+
`;
212+
213+
const CommentButtonBox = styled.div`
214+
display: flex;
215+
margin-left: auto;
216+
gap: 6px;
217+
`;
218+
219+
const CommentButton = styled.button<{ color: string }>`
220+
background-color: ${(props) => props.color};
221+
${font.caption};
222+
color: ${color.white};
223+
padding: 2px 10px;
224+
border-radius: 4px;
225+
`;
226+
227+
const CommentTextArea = styled.textarea`
228+
border: 2px solid ${color.on_tertiary};
229+
border-radius: 4px;
230+
padding: 6px 12px;
231+
margin: 6px 0;
232+
${font.p3};
233+
`;
234+
235+
const DetailViewButton = styled.button`
236+
border: none;
237+
width: fit-content;
238+
color: ${color.gray};
239+
${font.caption};
240+
border-radius: 999px;
241+
242+
&:hover {
243+
text-decoration: underline;
244+
}
245+
`;
246+
247+
export default RecommentListItem;

0 commit comments

Comments
 (0)