Skip to content

Commit 1c443c5

Browse files
authored
Merge pull request #8 from diglog-project/feature/comment
Feature/comment
2 parents eb7ef9b + e8bb37e commit 1c443c5

File tree

13 files changed

+403
-79
lines changed

13 files changed

+403
-79
lines changed

src/common/apis/comment.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import axiosApi from "./AxiosApi.tsx";
2+
import {CommentListRequest, CommentRequest, CommentUpdateRequest} from "../types/comment.tsx";
3+
import {commentListRequestToParameter} from "../util/url.tsx";
4+
5+
export const getComments = async (commentListRequest: CommentListRequest) =>
6+
await axiosApi.get(`/comment${commentListRequestToParameter(commentListRequest)}`);
7+
8+
export const saveComment = async (commentRequest: CommentRequest) =>
9+
await axiosApi.post("/comment", commentRequest);
10+
11+
export const updateComment = async (commentUpdateRequest: CommentUpdateRequest) => {
12+
await axiosApi.patch("/comment", commentUpdateRequest);
13+
}
14+
15+
export const deleteComment = async (id: string) =>
16+
await axiosApi.patch(`/comment/delete/${id}`);

src/common/apis/member.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,11 @@ export const getProfile = async () =>
3838
export const updateUsername = async (username: string) =>
3939
await axiosApi.post("/member/username", {
4040
username: username,
41-
});
41+
});
42+
43+
export const updateProfileImage = async (image: File) => {
44+
const formData = new FormData();
45+
formData.append("file", image);
46+
47+
return await axiosApi.post("/member/image", formData);
48+
}

src/common/slices/loginSlice.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const initialState = {
55
accessToken: "",
66
email: "",
77
username: "",
8+
profileUrl: "",
89
roles: [],
910
}
1011

@@ -18,6 +19,7 @@ const loginSlice = createSlice({
1819
accessToken: action.payload.accessToken,
1920
email: action.payload.email,
2021
username: action.payload.username,
22+
profileUrl: action.payload.profileUrl,
2123
roles: action.payload.roles,
2224
};
2325
},

src/common/types/comment.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {PageResponse} from "./common.tsx";
2+
3+
export interface CommentType {
4+
id: string,
5+
member: CommentMember,
6+
content: string,
7+
taggedUsername: string | null,
8+
replyCount: number,
9+
createdAt: Date,
10+
deleted: boolean,
11+
subComments: CommentType[],
12+
}
13+
14+
export interface CommentListRequest {
15+
postId: string,
16+
parentCommentId: string | null,
17+
page: number,
18+
size: number,
19+
}
20+
21+
export interface CommentListResponse {
22+
content: CommentResponse[],
23+
page: PageResponse,
24+
}
25+
26+
export interface CommentResponse {
27+
id: string,
28+
member: CommentMember,
29+
content: string,
30+
taggedUsername: string | null,
31+
replyCount: number,
32+
createdAt: Date,
33+
deleted: boolean,
34+
}
35+
36+
export interface CommentMember {
37+
username: string,
38+
profileUrl: string | null,
39+
}
40+
41+
export interface CommentRequest {
42+
content: string,
43+
postId: string,
44+
parentCommentId: string | null,
45+
taggedUsername: string | null,
46+
}
47+
48+
export interface CommentUpdateRequest {
49+
id: string,
50+
content: string,
51+
taggedUsername: string | null,
52+
}

src/common/util/url.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import {PostListRequest} from "../types/post.tsx";
2+
import {CommentListRequest} from "../types/comment.tsx";
23

3-
export const postListRequestToParameter = (postListRequest: PostListRequest)=> {
4+
export const postListRequestToParameter = (postListRequest: PostListRequest) => {
45
const sorts = postListRequest.sorts.map(sort => `sorts=${sort}`).join("&");
56

67
return `?${sorts}&page=${postListRequest.page}&size=${postListRequest.size}&isDescending=${postListRequest.isDescending}`;
7-
}
8+
}
9+
10+
export const commentListRequestToParameter = (commentListRequest: CommentListRequest) => {
11+
let query = `?postId=${commentListRequest.postId}&page=${commentListRequest.page}&size=${commentListRequest.size}`;
12+
13+
if (commentListRequest.parentCommentId) {
14+
query += `&parentCommentId=${commentListRequest.parentCommentId}`;
15+
}
16+
17+
return query;
18+
}

src/components/blog/BlogSideBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function BlogSideBar({folders, username, addTag, setSelectedFolder, bgColor, sid
2020
return (
2121
<div className={`${bgColor} ${side && "h-screen overflow-y-scroll"}`}>
2222
<div className="flex flex-col justify-start items-center py-4 gap-4 z-200">
23-
<img className="border border-gray-300 h-32 w-32 rounded-full"
23+
<img className="border border-gray-300 size-32 rounded-full"
2424
src={faker.image.avatar()} alt="username"/>
2525
<div className="flex justify-center items-center text-2xl font-black">
2626
{username}

src/components/common/Header.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import {Link, useNavigate} from "react-router-dom";
22
import {useDispatch, useSelector} from "react-redux";
33
import {RootState} from "../../store.tsx";
4-
import {faker} from "@faker-js/faker/locale/ko";
54
import {useEffect, useRef, useState} from "react";
65
import {TextLink} from "./TextButton.tsx";
7-
import {MdOutlineSearch} from "react-icons/md";
6+
import {MdOutlinePerson, MdOutlineSearch} from "react-icons/md";
87
import IconButton from "./IconButton.tsx";
98
import {getProfile, logoutApi} from "../../common/apis/member.tsx";
109
import {login, logout} from "../../common/slices/loginSlice.tsx";
1110

1211
function Header() {
1312

14-
1513
const dispatch = useDispatch();
1614
const loginState = useSelector((state: RootState) => state.loginSlice);
1715
const navigate = useNavigate();
@@ -26,6 +24,7 @@ function Header() {
2624
...loginState,
2725
email: res.data.email,
2826
username: res.data.username,
27+
profileUrl: res.data.profileUrl,
2928
}));
3029
})
3130
}, []);
@@ -80,9 +79,12 @@ function Header() {
8079
? <div ref={dashboardRef}>
8180
<div
8281
className="relative flex justify-around items-center w-full">
83-
<img className="size-10 rounded-full hover:cursor-pointer"
84-
onClick={handleDropDown}
85-
src={faker.image.avatar()} alt="user_image"/>
82+
{loginState.profileUrl
83+
? <img className="size-10 rounded-full border border-gray-200 hover:cursor-pointer"
84+
onClick={handleDropDown}
85+
src={loginState.profileUrl} alt="user_image"/>
86+
: <MdOutlinePerson className="size-6 m-1 rounded-full hover:cursor-pointer"
87+
onClick={handleDropDown}/>}
8688
<div
8789
className={`${isOpen ? "" : "hidden"} absolute z-50 top-12 right-0 bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44`}>
8890
<div className="flex flex-col gap-1 px-4 py-3 text-sm text-gray-900">

src/components/post/CommentCard.tsx

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,160 @@
1-
import {faker} from "@faker-js/faker/locale/ko";
21
import {dateToKorean} from "../../common/util/date.tsx";
3-
import {MdOutlineAddComment, MdOutlineComment} from "react-icons/md";
2+
import {MdOutlineAddComment, MdOutlineComment, MdOutlinePerson} from "react-icons/md";
43
import {ChangeEvent, useState} from "react";
54
import CommentTextField from "./CommentTextField.tsx";
5+
import {CommentType} from "../../common/types/comment.tsx";
6+
import {LoadMoreButton} from "../common/FillButton.tsx";
7+
import {useSelector} from "react-redux";
8+
import {RootState} from "../../store.tsx";
9+
import {TextButton} from "../common/TextButton.tsx";
10+
import {deleteComment} from "../../common/apis/comment.tsx";
11+
import {useNavigate} from "react-router-dom";
612

7-
function CommentCard({depth = 0}: { depth?: number }) {
13+
function CommentCard({comment, handleLoadMoreSubComment, handleCommentSubmit, pageSize, depth = 0}: {
14+
comment: CommentType,
15+
handleLoadMoreSubComment: (page: number, parentId: string) => void,
16+
handleCommentSubmit: (commentId: string | null, content: string, taggedUsername: string | null, originalComment: CommentType | null) => void,
17+
pageSize: number,
18+
depth?: number,
19+
}) {
20+
21+
const navigate = useNavigate();
22+
const loginState = useSelector((state: RootState) => state.loginSlice);
823

9-
const [openComment, setOpenComment] = useState(depth !== 0);
1024
const [commentInput, setCommentInput] = useState("");
25+
const [editCommentInput, setEditCommentInput] = useState("");
1126
const [showTextField, setShowTextField] = useState(false);
27+
const [showEditTextField, setShowEditTextField] = useState(false);
28+
const [page, setPage] = useState(0);
1229

1330
const handleCommentInput = (e: ChangeEvent<HTMLTextAreaElement>) => {
1431
setCommentInput(e.currentTarget.value);
1532
}
1633
const handleShowTextField = () => {
1734
setShowTextField(prev => !prev);
1835
}
19-
const handleSubmit = (commentId: string | undefined) => {
20-
confirm("댓글을 등록하시겠습니까?");
21-
alert("등록되었습니다.");
22-
console.log(commentId);
36+
const handleOpenTextField = () => {
37+
setCommentInput("");
38+
setShowTextField(true);
39+
}
40+
41+
const handleShowEdit = () => {
42+
setEditCommentInput(comment.content);
43+
setShowEditTextField(prev => !prev);
44+
}
45+
const handleEditCommentInput = (e: ChangeEvent<HTMLTextAreaElement>) => {
46+
setEditCommentInput(e.currentTarget.value);
47+
}
48+
49+
const handleDeleteComment = () => {
50+
if (!confirm("댓글을 삭제하시겠습니까?")) {
51+
return;
52+
}
53+
54+
deleteComment(comment.id)
55+
.then(() => {
56+
alert("삭제되었습니다.");
57+
navigate(0);
58+
})
59+
.catch((error) => alert(error.response.data.message));
2360
}
2461

62+
const taggedUsername = depth > 0 ? comment.member.username : null;
63+
64+
2565
return (
2666
<div className={`flex ${depth === 0 && "pb-4"}`}>
2767
{depth !== 0 && <div className="w-8"/>}
2868
<div className="flex-1 flex flex-col justify-center mt-4 text-sm">
2969
<div className="flex flex-col gap-y-2 border rounded-2xl border-gray-300 px-4 pt-4 pb-2">
3070
<div className="flex items-center gap-x-2">
31-
<img className="size-6 rounded-full hover:cursor-pointer"
32-
src={faker.image.avatar()} alt="user_image"/>
71+
{comment.member.profileUrl
72+
? <img className="size-6 rounded-full hover:cursor-pointer"
73+
src={comment.member.profileUrl} alt="user_image"/>
74+
: <MdOutlinePerson className="size-5 text-gray-600"/>}
3375
<p className="font-bold">
34-
{faker.animal.cow()}
76+
{comment.member.username}
3577
</p>
3678
<p className="text-gray-400 text-sm">
37-
{dateToKorean(faker.date.anytime())}
79+
{dateToKorean(comment.createdAt)}
3880
</p>
3981
</div>
40-
<p className="mt-4 text-gray-900">
41-
{faker.lorem.paragraph()}
82+
<p className="mt-4 text-gray-900 flex items-center gap-x-2">
83+
{!showEditTextField &&
84+
comment.taggedUsername &&
85+
<span className="text-lime-700">
86+
@{comment.taggedUsername}
87+
</span>}
88+
{showEditTextField
89+
? <CommentTextField
90+
value={editCommentInput}
91+
onChange={handleEditCommentInput}
92+
handleSubmit={handleCommentSubmit}
93+
taggedUsername={comment.taggedUsername}
94+
originalComment={comment}
95+
handleShowTextField={handleShowEdit}/>
96+
: <div>{comment.content}</div>}
4297
</p>
4398
<div className="flex justify-between items-center">
4499
{depth === 0 &&
45-
<button
46-
className="w-fit py-2 flex justify-center items-center gap-x-2 text-gray-600 hover:cursor-pointer rounded-md hover:brightness-120"
47-
onClick={() => setOpenComment(prev => !prev)}>
48-
<MdOutlineComment className="text-gray-600 size-4"/>
49-
{!openComment ? "답글 (10)" : "답글 닫기"}
50-
</button>}
100+
<button
101+
className="w-fit py-2 flex justify-center items-center gap-x-2 text-gray-600 hover:cursor-pointer rounded-md hover:brightness-120"
102+
onClick={() => {
103+
handleLoadMoreSubComment(page, comment.id);
104+
setPage(page + 1);
105+
}}>
106+
<MdOutlineComment className="text-gray-600 size-4"/>
107+
답글 ({comment.replyCount})
108+
</button>}
51109
<div/>
52-
<button
53-
className="w-fit py-2 flex justify-center items-center gap-x-2 text-gray-600 hover:cursor-pointer rounded-md hover:brightness-120"
54-
onClick={() => setShowTextField(true)}>
55-
<MdOutlineAddComment className="text-gray-600 size-4"/>
56-
댓글 작성하기
57-
</button>
110+
{!showEditTextField &&
111+
<div className="flex items-center gap-x-4 text-gray-600">
112+
{comment.member.username === loginState.username &&
113+
<div className="flex items-center">
114+
<TextButton text={"수정"} onClick={handleShowEdit}/>
115+
<TextButton text={"삭제"} onClick={handleDeleteComment}/>
116+
</div>
117+
}
118+
<button
119+
className="w-fit py-2 flex justify-center items-center gap-x-2 hover:cursor-pointer rounded-md hover:brightness-120"
120+
onClick={handleOpenTextField}>
121+
<MdOutlineAddComment className="size-4"/>
122+
댓글 작성하기
123+
</button>
124+
</div>
125+
}
58126
</div>
59127
</div>
60-
<div className="ml-8">
128+
<div>
61129
{showTextField &&
62130
<CommentTextField
63131
value={commentInput}
64132
onChange={handleCommentInput}
65-
handleSubmit={handleSubmit}
66-
commentId={faker.number.int().toString()}
133+
handleSubmit={handleCommentSubmit}
134+
commentId={comment.id}
135+
taggedUsername={taggedUsername}
67136
handleShowTextField={handleShowTextField}/>}
68137
</div>
69-
{openComment &&
138+
{comment.subComments &&
70139
<div>
71-
{Array.from({length: 2}).map(() =>
140+
{comment.subComments.map((comment) =>
72141
(depth <= 2)
73-
? <CommentCard key={faker.animal.cow()} depth={depth + 1}/>
142+
? <CommentCard
143+
key={comment.id}
144+
comment={comment}
145+
handleLoadMoreSubComment={handleLoadMoreSubComment}
146+
handleCommentSubmit={handleCommentSubmit}
147+
pageSize={pageSize}
148+
depth={depth + 1}/>
74149
: null)}
75-
</div>
76-
}
150+
{page !== 0 && (page) * 10 < comment.replyCount &&
151+
<LoadMoreButton
152+
addStyle="mt-2 ml-8 w-[calc(100%-36px)] !bg-gray-400"
153+
onClick={() => {
154+
handleLoadMoreSubComment(page, comment.id);
155+
setPage(page + 1);
156+
}}/>}
157+
</div>}
77158
</div>
78159
</div>
79160
);

0 commit comments

Comments
 (0)