|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { useState, useEffect, useRef, useCallback } from "react"; |
4 | 3 | import { useParams } from "next/navigation"; |
5 | | -import CommentDetail from "@/app/components/card/board/CommentDetail"; |
6 | | -import BaseTextArea from "@/app/components/input/textarea/BaseTextArea"; |
7 | | -import Button from "@/app/components/button/default/Button"; |
8 | | -import { usePostActions } from "@/hooks/usePostActions"; |
9 | | -import { Post } from "@/types/post"; |
10 | | -import Image from "next/image"; |
11 | | -import { format } from "date-fns"; |
12 | | -import { useUser } from "@/hooks/queries/user/me/useUser"; |
13 | | -import EditPostModal from "@/app/components/modal/modals/edit/EditPost"; |
| 4 | +import { PostDetailSection } from "./sections/PostDetailSection"; |
| 5 | +import { CommentsSection } from "./sections/CommentsSection"; |
14 | 6 |
|
15 | 7 | export default function PostDetailPage() { |
16 | | - const { talkId } = useParams(); |
17 | | - const [isLoading, setIsLoading] = useState(true); |
18 | | - const [newComment, setNewComment] = useState(""); |
19 | | - const [initialPost, setInitialPost] = useState<Post | null>(null); |
20 | | - const [initialComments, setInitialComments] = useState<any[]>([]); |
21 | | - const [page, setPage] = useState(1); |
22 | | - const [hasMore, setHasMore] = useState(true); |
23 | | - const [showOptions, setShowOptions] = useState(false); |
24 | | - const [showEditModal, setShowEditModal] = useState(false); |
25 | | - const [authorImageError, setAuthorImageError] = useState(false); // Added state for image error handling |
26 | | - const optionsRef = useRef<HTMLDivElement>(null); |
27 | | - const observer = useRef<IntersectionObserver>(); |
| 8 | + const { albatalkId } = useParams(); |
28 | 9 |
|
29 | | - const { user } = useUser(); |
30 | | - |
31 | | - const lastCommentElementRef = useCallback( |
32 | | - (node: HTMLDivElement) => { |
33 | | - if (isLoading) return; |
34 | | - if (observer.current) observer.current.disconnect(); |
35 | | - observer.current = new IntersectionObserver((entries) => { |
36 | | - if (entries[0].isIntersecting && hasMore) { |
37 | | - setPage((prevPage) => prevPage + 1); |
38 | | - } |
39 | | - }); |
40 | | - if (node) observer.current.observe(node); |
41 | | - }, |
42 | | - [isLoading, hasMore] |
43 | | - ); |
44 | | - |
45 | | - useEffect(() => { |
46 | | - const handleClickOutside = (event: MouseEvent) => { |
47 | | - if (optionsRef.current && !optionsRef.current.contains(event.target as Node)) { |
48 | | - setShowOptions(false); |
49 | | - } |
50 | | - }; |
51 | | - |
52 | | - document.addEventListener("mousedown", handleClickOutside); |
53 | | - return () => document.removeEventListener("mousedown", handleClickOutside); |
54 | | - }, []); |
55 | | - |
56 | | - const { |
57 | | - post, |
58 | | - comments = [], |
59 | | - handleLike, |
60 | | - handleDeletePost, |
61 | | - handleAddComment, |
62 | | - handleEditComment, |
63 | | - handleDeleteComment, |
64 | | - isPendingLike, |
65 | | - } = usePostActions(initialPost, initialComments); |
66 | | - |
67 | | - useEffect(() => { |
68 | | - const fetchPostAndComments = async () => { |
69 | | - try { |
70 | | - const postResponse = await fetch(`/api/posts/${talkId}`); |
71 | | - const postData = await postResponse.json(); |
72 | | - setInitialPost(postData); |
73 | | - |
74 | | - const commentsResponse = await fetch(`/api/posts/${talkId}/comments?page=${page}&pageSize=10`); |
75 | | - const commentsData = await commentsResponse.json(); |
76 | | - setInitialComments((prev) => { |
77 | | - const newComments = page === 1 ? commentsData.data : [...prev, ...commentsData.data]; |
78 | | - return newComments.map((comment: any) => ({ |
79 | | - ...comment, |
80 | | - userName: comment.writer.nickname, |
81 | | - userImageUrl: comment.writer.imageUrl, |
82 | | - isAuthor: comment.writer.id === user?.id, |
83 | | - })); |
84 | | - }); |
85 | | - setHasMore(commentsData.data.length > 0); |
86 | | - setIsLoading(false); |
87 | | - } catch (error) { |
88 | | - console.error("Error fetching data:", error); |
89 | | - setIsLoading(false); |
90 | | - } |
91 | | - }; |
92 | | - |
93 | | - if (user) { |
94 | | - fetchPostAndComments(); |
95 | | - } |
96 | | - }, [talkId, page, user]); |
97 | | - |
98 | | - if (isLoading || !post) { |
99 | | - return <div>๋ก๋ฉ ์ค...</div>; |
100 | | - } |
101 | | - |
102 | | - console.log("Author image URL:", post.writer.imageUrl); // Added console log for image URL |
103 | | - |
104 | | - const formatDate = (dateString: string) => { |
105 | | - return format(new Date(dateString), "yyyy. MM. dd"); |
106 | | - }; |
107 | | - |
108 | | - const handleLikeClick = () => { |
109 | | - handleLike(); |
110 | | - }; |
| 10 | + // ์๋ฌ ์ฒ๋ฆฌ |
111 | 11 |
|
112 | 12 | return ( |
113 | | - <div className="min-h-screen bg-white py-12"> |
| 13 | + <main className="min-h-screen bg-white py-12"> |
114 | 14 | <div className="mx-auto flex w-full max-w-[1480px] flex-col items-center px-4 lg:px-8"> |
115 | | - {/* Post Content Box */} |
116 | | - <div className="mb-12 flex h-[372px] w-full max-w-[327px] flex-col lg:h-[356px]"> |
117 | | - <div className="flex h-full flex-col"> |
118 | | - {/* Title and Profile Section */} |
119 | | - <div className="flex h-[98px] flex-col justify-between lg:h-[128px]"> |
120 | | - <div className="mb-4 flex items-center justify-between"> |
121 | | - <h1 className="text-[16px] font-semibold lg:text-[24px]">{post.title}</h1> |
122 | | - {post.writer.id === user?.id && ( |
123 | | - <div className="relative" ref={optionsRef}> |
124 | | - <button onClick={() => setShowOptions(!showOptions)} className="text-grayscale-500"> |
125 | | - <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
126 | | - <path |
127 | | - d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" |
128 | | - stroke="currentColor" |
129 | | - strokeWidth="2" |
130 | | - strokeLinecap="round" |
131 | | - strokeLinejoin="round" |
132 | | - /> |
133 | | - <path |
134 | | - d="M19 13C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11C18.4477 11 18 11.4477 18 12C18 12.5523 18.4477 13 19 13Z" |
135 | | - stroke="currentColor" |
136 | | - strokeWidth="2" |
137 | | - strokeLinecap="round" |
138 | | - strokeLinejoin="round" |
139 | | - /> |
140 | | - <path |
141 | | - d="M5 13C5.55228 13 6 12.5523 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13Z" |
142 | | - stroke="currentColor" |
143 | | - strokeWidth="2" |
144 | | - strokeLinecap="round" |
145 | | - strokeLinejoin="round" |
146 | | - /> |
147 | | - </svg> |
148 | | - </button> |
149 | | - {showOptions && ( |
150 | | - <div className="absolute right-0 mt-2 w-[80px] rounded-lg bg-white shadow-lg lg:w-[132px]"> |
151 | | - <div className="flex h-[68px] flex-col justify-center gap-2 p-2 lg:h-[104px]"> |
152 | | - <button |
153 | | - onClick={() => { |
154 | | - setShowEditModal(true); |
155 | | - setShowOptions(false); |
156 | | - }} |
157 | | - className="rounded-md bg-grayscale-50 p-2 text-xs text-grayscale-400 hover:bg-orange-50 hover:text-black-400 lg:text-sm" |
158 | | - > |
159 | | - ์์ ํ๊ธฐ |
160 | | - </button> |
161 | | - <button |
162 | | - onClick={() => { |
163 | | - if (confirm("์ ๋ง๋ก ์ญ์ ํ์๊ฒ ์ต๋๊น?")) { |
164 | | - handleDeletePost(); |
165 | | - } |
166 | | - setShowOptions(false); |
167 | | - }} |
168 | | - className="rounded-md bg-grayscale-50 p-2 text-xs text-grayscale-400 hover:bg-orange-50 hover:text-black-400 lg:text-sm" |
169 | | - > |
170 | | - ์ญ์ ํ๊ธฐ |
171 | | - </button> |
172 | | - </div> |
173 | | - </div> |
174 | | - )} |
175 | | - </div> |
176 | | - )} |
177 | | - </div> |
178 | | - <hr className="mb-4 border-t border-line-200" /> |
179 | | - <div className="flex items-center justify-between"> |
180 | | - <div className="flex items-center gap-2"> |
181 | | - {authorImageError ? ( |
182 | | - <div className="flex h-6 w-6 items-center justify-center rounded-full bg-gray-300"> |
183 | | - <span className="text-sm font-semibold text-gray-600"> |
184 | | - {post.writer.nickname.charAt(0).toUpperCase()} |
185 | | - </span> |
186 | | - </div> |
187 | | - ) : ( |
188 | | - <Image |
189 | | - src={post.writer.imageUrl || "/icons/user/user-profile-sm.svg"} |
190 | | - alt="User Icon" |
191 | | - width={24} |
192 | | - height={24} |
193 | | - className="rounded-full" |
194 | | - onError={() => setAuthorImageError(true)} |
195 | | - /> |
196 | | - )} |
197 | | - <span className="text-xs text-grayscale-500 lg:text-base">{post.writer.nickname}</span> |
198 | | - <span className="text-xs text-grayscale-500 lg:text-base">|</span> |
199 | | - <span className="text-xs text-grayscale-500 lg:text-base">{formatDate(post.createdAt)}</span> |
200 | | - </div> |
201 | | - <div className="flex items-center gap-4"> |
202 | | - <div className="flex items-center gap-1 sm:gap-2"> |
203 | | - <Image |
204 | | - src="/icons/comment/comment-sm.svg" |
205 | | - alt="Comments" |
206 | | - width={24} |
207 | | - height={24} |
208 | | - className="h-[22px] w-[22px] lg:h-6 lg:w-6" |
209 | | - /> |
210 | | - <span className="text-xs text-grayscale-500 lg:text-base">{post.commentCount}</span> |
211 | | - </div> |
212 | | - <div className="flex items-center gap-1 lg:gap-2"> |
213 | | - <button onClick={handleLikeClick} disabled={isPendingLike}> |
214 | | - <Image |
215 | | - src={post.isLiked ? "/icons/like/like-sm-active.svg" : "/icons/like/like-sm.svg"} |
216 | | - alt="Like" |
217 | | - width={24} |
218 | | - height={24} |
219 | | - className="h-[22px] w-[22px] lg:h-6 lg:w-6" |
220 | | - /> |
221 | | - </button> |
222 | | - <span className="text-xs text-grayscale-500 lg:text-base">{post.likeCount}</span> |
223 | | - </div> |
224 | | - </div> |
225 | | - </div> |
226 | | - </div> |
227 | | - {/* Content Section */} |
228 | | - <div className="mt-auto h-[210px] overflow-y-auto whitespace-pre-wrap text-xs text-black-400 lg:h-[140px] lg:text-base"> |
229 | | - {post.content} |
230 | | - </div> |
231 | | - </div> |
232 | | - </div> |
233 | | - |
234 | | - {/* Comment Section */} |
235 | | - <div className="mb-12 flex w-full max-w-[327px] flex-col lg:max-w-[1480px]"> |
236 | | - <h2 className="mb-4 text-[16px] font-semibold sm:text-[20px] lg:text-[24px]">๋๊ธ({post.commentCount})</h2> |
237 | | - <hr className="mb-4 border-t border-line-200" /> |
238 | | - <div className="mb-[7px] flex-grow lg:mb-[10px]"></div> |
239 | | - {/* Comment Input Box */} |
240 | | - <div className="mt-auto"> |
241 | | - <div className="relative mb-4"> |
242 | | - <BaseTextArea |
243 | | - name="newComment" |
244 | | - variant="white" |
245 | | - placeholder="๋๊ธ์ ์
๋ ฅํด์ฃผ์ธ์." |
246 | | - value={newComment} |
247 | | - onChange={(e) => setNewComment(e.target.value)} |
248 | | - size="w-full h-[132px] lg:h-[160px]" |
249 | | - /> |
250 | | - </div> |
251 | | - <div className="flex justify-end"> |
252 | | - <Button |
253 | | - onClick={() => { |
254 | | - if (newComment.trim()) { |
255 | | - handleAddComment(newComment); |
256 | | - setNewComment(""); |
257 | | - } |
258 | | - }} |
259 | | - className="h-[52px] w-[108px] text-base lg:h-[64px] lg:w-[214px] lg:text-xl" |
260 | | - > |
261 | | - ๋ฑ๋กํ๊ธฐ |
262 | | - </Button> |
263 | | - </div> |
264 | | - </div> |
265 | | - </div> |
266 | | - |
267 | | - {/* Comments List or Empty State */} |
268 | | - <div className="w-full max-w-[327px]"> |
269 | | - {comments.length > 0 ? ( |
270 | | - <div className="space-y-4"> |
271 | | - {comments.map((comment, index) => ( |
272 | | - <div |
273 | | - key={comment.id} |
274 | | - ref={index === comments.length - 1 ? lastCommentElementRef : undefined} |
275 | | - className="w-full" |
276 | | - > |
277 | | - <CommentDetail |
278 | | - key={comment.id} |
279 | | - id={comment.id} |
280 | | - userName={comment.userName} |
281 | | - userImageUrl={comment.userImageUrl} |
282 | | - date={formatDate(comment.createdAt)} |
283 | | - comment={comment.content} |
284 | | - isAuthor={comment.isAuthor} |
285 | | - onEdit={(id, newContent) => handleEditComment({ commentId: id, newContent })} |
286 | | - onDelete={handleDeleteComment} |
287 | | - /> |
288 | | - </div> |
289 | | - ))} |
290 | | - {isLoading && <div className="text-center">๋ก๋ฉ ์ค...</div>} |
291 | | - </div> |
292 | | - ) : ( |
293 | | - <div className="mt-8 flex justify-center"> |
294 | | - <Image src={`/images/emptyComment-md.svg`} alt="No comments" width={206} height={204} /> |
295 | | - </div> |
296 | | - )} |
297 | | - </div> |
298 | | - {showEditModal && ( |
299 | | - <EditPostModal |
300 | | - post={post} |
301 | | - onClose={() => setShowEditModal(false)} |
302 | | - onUpdate={(updatedPost) => { |
303 | | - setInitialPost(updatedPost); |
304 | | - setShowEditModal(false); |
305 | | - }} |
306 | | - /> |
307 | | - )} |
| 15 | + <PostDetailSection postId={albatalkId.toString()} /> |
| 16 | + <CommentsSection postId={albatalkId.toString()} /> |
308 | 17 | </div> |
309 | | - </div> |
| 18 | + </main> |
310 | 19 | ); |
311 | 20 | } |
0 commit comments