Skip to content

Commit 11ac1f8

Browse files
authored
[#118] 커뮤니티 댓글 및 커뮤니티 수정 페이지 tanstack query 도입 및 리팩토링 (#139)
* feat: 댓글 리스트 불러오기, 수정, 삭제 기능 tanstack query로 변경 * feat: 에러바운더리 추가 * chore: comment 섹션 함수 변수명 변경 * feat: post edit 기능 tanstack query로 변경 * feat: postedit 기능 로딩 suspense 변경 및 에러바운더리 추가 * feat: throw new Error 상수화 처리 * refactor: 데이터 조회, 업데이트, 삭제 기능 hook 분리 * fix: 인증방식 requiresAuth: true 플래그로 변경 * chore: 불필요 이미지 삭제 * fix: userId undefined 방지 * fix: 불필요 메모이제이션 삭제
1 parent 6b63731 commit 11ac1f8

File tree

8 files changed

+364
-274
lines changed

8 files changed

+364
-274
lines changed
Lines changed: 20 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,26 @@
1-
'use client';
2-
3-
import React, { useMemo } from 'react';
4-
import dynamic from 'next/dynamic';
5-
import { useSession } from 'next-auth/react';
6-
import { usePost } from '@/hooks/queries/usePostEdit';
1+
import React, { Suspense } from 'react';
2+
import { getServerSession } from 'next-auth';
73
import '@/styles/pages/community/community.scss';
8-
import 'react-quill/dist/quill.snow.css';
9-
import type Quill from 'quill';
10-
import { axiosInstance } from '@/services/common/axiosInstance';
11-
import { API_URLS } from '@/constants/urls';
12-
import Cookies from 'js-cookie';
13-
import { ALERT_MESSAGES } from '@/constants/alertMessage';
14-
15-
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
16-
17-
export default function EditPostPage({ params }: { params: { id: string } }) {
18-
const { id } = params;
19-
const { data: session } = useSession();
20-
21-
const userId = session?.user?.id;
22-
const accessToken = session?.user?.accessToken || '';
23-
24-
const { title, setTitle, content, setContent, loading, handleUpdatePost, handleDeletePost } = usePost(id, userId, accessToken);
25-
26-
function handleImageUpload(this: { quill: Quill }) {
27-
const editor = this.quill;
28-
29-
const input = document.createElement("input");
30-
input.type = "file";
31-
input.accept = "image/*";
32-
input.click();
33-
34-
input.onchange = async () => {
35-
if (input.files && input.files.length > 0) {
36-
const file = input.files[0];
37-
try {
38-
const formData = new FormData();
39-
formData.append("image", file);
40-
41-
const response = await axiosInstance.post(API_URLS.UPLOADS, formData, {
42-
headers: {
43-
"Content-Type": "multipart/form-data",
44-
Authorization: `Bearer ${Cookies.get("accessToken")}`,
45-
},
46-
});
47-
48-
const imageUrl = response.data.url;
49-
const range = editor.getSelection(true);
50-
editor.insertEmbed(range.index, "image", imageUrl);
51-
editor.setSelection(range.index + 1);
52-
} catch (error) {
53-
console.error("Image upload failed:", error);
54-
alert(ALERT_MESSAGES.ERROR.POST.IMAGE_UPLOAD_ERROR);
55-
}
56-
}
57-
};
58-
}
59-
60-
const modules = useMemo(() => {
61-
return {
62-
toolbar: {
63-
container: [
64-
[{ header: '1' }, { header: '2' }, { font: [] }],
65-
[{ size: [] }],
66-
['bold', 'italic', 'underline', 'strike', 'blockquote'],
67-
[
68-
{ list: 'ordered' },
69-
{ list: 'bullet' },
70-
{ indent: '-1' },
71-
{ indent: '+1' },
72-
],
73-
['link', 'image'],
74-
['clean'],
75-
],
76-
handlers: {
77-
image: handleImageUpload,
78-
},
79-
},
80-
};
81-
}, []);
82-
83-
const formats = useMemo(
84-
() => [
85-
'header',
86-
'font',
87-
'size',
88-
'bold',
89-
'italic',
90-
'underline',
91-
'strike',
92-
'blockquote',
93-
'list',
94-
'bullet',
95-
'indent',
96-
'link',
97-
'image',
98-
],
99-
[]
100-
);
101-
102-
if (loading) {
103-
return <p>게시글을 불러오는 중...</p>;
4+
import ErrorBoundary from '@/components/common/ErrorBoundary';
5+
import LoadingSpinner from '@/components/common/LoadingSpinner';
6+
import EditPostContent from '@/components/community/EditPostContent';
7+
import { authOptions } from '@/app/auth/authOptions';
8+
import { ERROR_MESSAGES } from '@/constants/errors';
9+
10+
export default async function EditPostPage({ params }: { params: { id: string } }) {
11+
const session = await getServerSession(authOptions);
12+
13+
if (!session?.user?.id) {
14+
throw new Error(ERROR_MESSAGES.LOGIN_REQUIRED);
10415
}
16+
17+
const userId = session.user.id;
10518

10619
return (
107-
<div className="community edit_post">
108-
<h1>커뮤니티</h1>
109-
<p className="sub_title">자유롭게 건강에 관련 지식을 공유해봅시다!</p>
110-
<div className="form_group">
111-
<input
112-
id="title"
113-
type="text"
114-
value={title}
115-
onChange={(e) => setTitle(e.target.value)}
116-
/>
117-
</div>
118-
<div className="form_group">
119-
<ReactQuill
120-
theme="snow"
121-
modules={modules}
122-
formats={formats}
123-
value={content}
124-
onChange={(val) => setContent(val)}
125-
/>
126-
</div>
127-
<div className="actions">
128-
<button onClick={handleUpdatePost} className='create_button'>수정 완료</button>
129-
<button onClick={handleDeletePost} className="delete_button">
130-
삭제
131-
</button>
132-
</div>
133-
</div>
20+
<ErrorBoundary>
21+
<Suspense fallback={<LoadingSpinner />}>
22+
<EditPostContent params={params} userId={userId} />
23+
</Suspense>
24+
</ErrorBoundary>
13425
);
13526
}

apps/next-client/src/components/community/CommentForm.tsx

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,37 @@
11
'use client';
22

33
import { useState } from 'react';
4-
import { API_URLS } from '@/constants/urls';
4+
import { useAddComment } from '@/hooks/queries/useComments';
55
import { ALERT_MESSAGES } from '@/constants/alertMessage';
6-
import { axiosInstance } from '@/services/common/axiosInstance';
76

87
interface CommentFormProps {
98
urlPostId: string;
10-
fetchComments: () => void;
119
}
1210

13-
const CommentForm = ({ urlPostId, fetchComments }: CommentFormProps) => {
11+
const CommentForm = ({ urlPostId }: CommentFormProps) => {
1412
const [newComment, setNewComment] = useState('');
13+
const addCommentMutation = useAddComment(urlPostId);
1514

16-
const handleAddComment = async () => {
15+
const handleAddComment = () => {
1716
if (!newComment.trim()) {
1817
alert(ALERT_MESSAGES.ERROR.COMMENT.COMMENT_EMPTY_FIELDS);
1918
return;
2019
}
21-
22-
try {
23-
await axiosInstance.post(`${API_URLS.POSTS}/${urlPostId}/comments`, { content: newComment },
24-
{ headers: { requiresAuth: true } }
25-
);
26-
setNewComment('');
27-
fetchComments();
28-
alert(ALERT_MESSAGES.SUCCESS.COMMENT.COMMENT_ADD);
29-
} catch (error) {
30-
console.error('Error adding comment:', error);
31-
alert(ALERT_MESSAGES.ERROR.COMMENT.COMMENT_ADD_ERROR);
32-
}
20+
addCommentMutation.mutate(newComment, {
21+
onSuccess: () => {
22+
setNewComment('');
23+
alert(ALERT_MESSAGES.SUCCESS.COMMENT.COMMENT_ADD);
24+
},
25+
});
3326
};
3427

3528
return (
36-
<div className='comment_section'>
29+
<div className="comment_section">
3730
<div className="add_comment">
38-
<textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} />
31+
<textarea
32+
value={newComment}
33+
onChange={(e) => setNewComment(e.target.value)}
34+
/>
3935
<button onClick={handleAddComment}>댓글 추가</button>
4036
</div>
4137
</div>

apps/next-client/src/components/community/CommentList.tsx

Lines changed: 36 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,85 @@
11
import { useState } from 'react';
22
import { Comment } from '@/types/post';
3-
import { API_URLS } from '@/constants/urls';
3+
import { useDeleteComment, useEditComment } from '@/hooks/queries/useComments';
44
import { ALERT_MESSAGES } from '@/constants/alertMessage';
5-
import { axiosInstance } from '@/services/common/axiosInstance';
65

76
interface CommentListProps {
87
comments: Comment[];
98
userId: string | undefined;
10-
fetchComments: () => Promise<void>;
9+
urlPostId: string;
1110
}
1211

13-
const CommentList = ({ comments, userId, fetchComments }: CommentListProps) => {
12+
const CommentList = ({ comments, userId, urlPostId }: CommentListProps) => {
1413
const [editingCommentId, setEditingCommentId] = useState<number | null>(null);
1514
const [editedComment, setEditedComment] = useState<string>('');
1615

17-
const handleDeleteComment = async (commentId: number) => {
18-
if (!window.confirm(ALERT_MESSAGES.CONFIRM.CHECK_DELETE)) return;
19-
20-
try {
21-
await axiosInstance.delete(`${API_URLS.POSTS}/comments/${commentId}`, {
22-
headers: { requiresAuth: true },
23-
});
24-
alert(ALERT_MESSAGES.SUCCESS.COMMENT.COMMENT_DELETE);
25-
fetchComments();
26-
} catch (error) {
27-
console.error('Error deleting comment:', error);
28-
alert(ALERT_MESSAGES.ERROR.COMMENT.COMMENT_DELETE_ERROR);
29-
}
30-
};
16+
const deleteCommentMutation = useDeleteComment(urlPostId);
17+
const editCommentMutation = useEditComment(urlPostId);
3118

32-
const startEditingComment = (commentId: number, currentContent: string) => {
33-
setEditingCommentId(commentId);
34-
setEditedComment(currentContent);
19+
const handleDeleteComment = (commentId: number) => {
20+
if (!window.confirm(ALERT_MESSAGES.CONFIRM.CHECK_DELETE)) return;
21+
deleteCommentMutation.mutate(commentId);
3522
};
3623

37-
const handleEditComment = async (commentId: number) => {
24+
const handleEditComment = (commentId: number) => {
3825
if (!editedComment.trim()) {
3926
alert(ALERT_MESSAGES.ERROR.COMMENT.COMMENT_EMPTY_FIELDS);
4027
return;
4128
}
42-
43-
try {
44-
await axiosInstance.put(
45-
`${API_URLS.POSTS}/comments/${commentId}`,
46-
{ content: editedComment },
47-
{ headers: { requiresAuth: true } }
48-
);
49-
50-
alert(ALERT_MESSAGES.SUCCESS.COMMENT.COMMENT_EDIT);
51-
setEditingCommentId(null);
52-
setEditedComment('');
53-
fetchComments();
54-
55-
} catch (error) {
56-
console.error('Error editing comment:', error);
57-
alert(ALERT_MESSAGES.ERROR.COMMENT.COMMENT_EDIT_ERROR);
58-
}
29+
editCommentMutation.mutate({ commentId, content: editedComment });
30+
setEditingCommentId(null);
31+
setEditedComment('');
5932
};
6033

6134
return (
6235
<ul className="comments_list">
6336
{comments.map((comment) => (
64-
<li className='comment_item' key={comment.id}>
37+
<li className="comment_item" key={comment.id}>
6538
{editingCommentId === comment.id ? (
66-
<div className='edit_comment'>
39+
<div className="edit_comment">
6740
<textarea
6841
value={editedComment}
6942
onChange={(e) => setEditedComment(e.target.value)}
7043
/>
7144
<div className="button_box">
72-
<button className="common_button save_button" onClick={() => handleEditComment(comment.id)}>
45+
<button
46+
className="common_button save_button"
47+
onClick={() => handleEditComment(comment.id)}
48+
>
7349
저장
7450
</button>
75-
<button className="common_button cancel_button" onClick={() => setEditingCommentId(null)}>
51+
<button
52+
className="common_button cancel_button"
53+
onClick={() => setEditingCommentId(null)}
54+
>
7655
취소
7756
</button>
7857
</div>
7958
</div>
8059
) : (
81-
<div className='edit_comment'>
60+
<div className="edit_comment">
8261
<div className="top_cont">
8362
<p>{comment.author}</p>
84-
<p className='date'>
63+
<p className="date">
8564
{new Date(comment.createdAt).toLocaleString('ko-KR')}
8665
</p>
8766
</div>
88-
8967
<p>{comment.content}</p>
90-
9168
{comment.userId === userId && (
92-
<div className='button_box'>
93-
<button className="common_button edit_button" onClick={() => startEditingComment(comment.id, comment.content)}>
69+
<div className="button_box">
70+
<button
71+
className="common_button edit_button"
72+
onClick={() => {
73+
setEditingCommentId(comment.id);
74+
setEditedComment(comment.content);
75+
}}
76+
>
9477
수정
9578
</button>
96-
<button className="common_button delete_button" onClick={() => handleDeleteComment(comment.id)}>
79+
<button
80+
className="common_button delete_button"
81+
onClick={() => handleDeleteComment(comment.id)}
82+
>
9783
삭제
9884
</button>
9985
</div>

0 commit comments

Comments
 (0)