diff --git a/public/images/emptyComment-md.svg b/public/images/emptyComment-md.svg new file mode 100644 index 00000000..ab78b5d1 --- /dev/null +++ b/public/images/emptyComment-md.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/emptyComment-sm.svg b/public/images/emptyComment-sm.svg new file mode 100644 index 00000000..27d6fd15 --- /dev/null +++ b/public/images/emptyComment-sm.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/(pages)/albaTalk/[id]/page.tsx b/src/app/(pages)/albaTalk/[id]/page.tsx deleted file mode 100644 index 156cc821..00000000 --- a/src/app/(pages)/albaTalk/[id]/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; -import { useRouter } from "next/router"; - -const DetailPage = () => { - const router = useRouter(); - const { id } = router.query; - - return
글 상세 페이지 - ID: {id}
; -}; - -export default DetailPage; diff --git a/src/app/(pages)/albaTalk/albaTalks/[talkId]/page.tsx b/src/app/(pages)/albaTalk/albaTalks/[talkId]/page.tsx new file mode 100644 index 00000000..d414ec57 --- /dev/null +++ b/src/app/(pages)/albaTalk/albaTalks/[talkId]/page.tsx @@ -0,0 +1,318 @@ +"use client"; + +import { useState, useEffect, useRef, useCallback } from "react"; +import { useParams } from "next/navigation"; +import Comment from "@/app/components/card/board/detailComment"; +import BaseTextArea from "@/app/components/input/textarea/BaseTextArea"; +import Button from "@/app/components/button/default/Button"; +import { usePostActions } from "@/hooks/usePostActions"; +import { Post } from "@/types/post"; +import Image from "next/image"; +import { format } from "date-fns"; +import { useUser } from "@/hooks/queries/user/me/useUser"; +import EditPostModal from "@/app/components/modal/modals/edit/EditPost"; + +export default function PostDetailPage() { + const { talkId } = useParams(); + const [isLoading, setIsLoading] = useState(true); + const [newComment, setNewComment] = useState(""); + const [initialPost, setInitialPost] = useState(null); + const [initialComments, setInitialComments] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [showOptions, setShowOptions] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [authorImageError, setAuthorImageError] = useState(false); // Added state for image error handling + const optionsRef = useRef(null); + const observer = useRef(); + + const { user } = useUser(); + + const lastCommentElementRef = useCallback( + (node: HTMLDivElement) => { + if (isLoading) return; + if (observer.current) observer.current.disconnect(); + observer.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasMore) { + setPage((prevPage) => prevPage + 1); + } + }); + if (node) observer.current.observe(node); + }, + [isLoading, hasMore] + ); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (optionsRef.current && !optionsRef.current.contains(event.target as Node)) { + setShowOptions(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const { + post, + comments = [], + handleLike, + handleDeletePost, + handleAddComment, + handleEditComment, + handleDeleteComment, + isPendingLike, + } = usePostActions(initialPost, initialComments); + + useEffect(() => { + const fetchPostAndComments = async () => { + try { + const postResponse = await fetch(`/api/posts/${talkId}`); + const postData = await postResponse.json(); + setInitialPost(postData); + + const commentsResponse = await fetch(`/api/posts/${talkId}/comments?page=${page}&pageSize=10`); + const commentsData = await commentsResponse.json(); + setInitialComments((prev) => { + const newComments = page === 1 ? commentsData.data : [...prev, ...commentsData.data]; + return newComments.map((comment: any) => ({ + ...comment, + userName: comment.writer.nickname, + userImageUrl: comment.writer.imageUrl, + isAuthor: comment.writer.id === user?.id, + })); + }); + setHasMore(commentsData.data.length > 0); + setIsLoading(false); + } catch (error) { + console.error("Error fetching data:", error); + setIsLoading(false); + } + }; + + if (user) { + fetchPostAndComments(); + } + }, [talkId, page, user]); + + if (isLoading || !post) { + return
로딩 중...
; + } + + console.log("Author image URL:", post.writer.imageUrl); // Added console log for image URL + + const formatDate = (dateString: string) => { + return format(new Date(dateString), "yyyy. MM. dd"); + }; + + const handleLikeClick = () => { + handleLike(); + }; + + return ( +
+
+ {/* Post Content Box */} +
+
+ {/* Title and Profile Section */} +
+
+

{post.title}

+ {post.writer.id === user?.id && ( +
+ + {showOptions && ( +
+
+ + +
+
+ )} +
+ )} +
+
+
+
+ {authorImageError ? ( // Updated author image rendering +
+ + {post.writer.nickname.charAt(0).toUpperCase()} + +
+ ) : ( + User Icon setAuthorImageError(true)} + /> + )} + {post.writer.nickname} + | + + {formatDate(post.createdAt)} + +
+
+
+ Comments + {post.commentCount} +
+
+ + {post.likeCount} +
+
+
+
+ {/* Content Section */} +
+ {post.content} +
+
+
+ + {/* Comment Section */} +
+

댓글({post.commentCount})

+
+
+ {/* Comment Input Box */} +
+
+ setNewComment(e.target.value)} + size="w-full h-[132px] sm:h-[132px] lg:h-[160px]" + /> +
+
+ +
+
+
+ + {/* Comments List or Empty State */} +
+ {comments.length > 0 ? ( +
+ {comments.map((comment, index) => ( +
+ handleEditComment({ commentId: id, newContent })} + onDelete={handleDeleteComment} + /> +
+ ))} + {isLoading &&
로딩 중...
} +
+ ) : ( +
+ = 1024 ? "md" : "sm"}.svg`} + alt="No comments" + width={window.innerWidth >= 1024 ? 206 : 206} + height={window.innerWidth >= 1024 ? 204 : 152} + /> +
+ )} +
+ {showEditModal && ( + setShowEditModal(false)} + onUpdate={(updatedPost) => { + setInitialPost(updatedPost); + setShowEditModal(false); + }} + /> + )} +
+
+ ); +} diff --git a/src/app/components/card/board/detailComment.tsx b/src/app/components/card/board/detailComment.tsx new file mode 100644 index 00000000..3204fa98 --- /dev/null +++ b/src/app/components/card/board/detailComment.tsx @@ -0,0 +1,132 @@ +"use client"; + +import React, { useState, useRef, useEffect } from "react"; +import Image from "next/image"; +import BaseTextArea from "@/app/components/input/textarea/BaseTextArea"; +import Button from "@/app/components/button/default/Button"; + +export interface CommentProps { + id: number; + userName: string; + userImageUrl?: string; + date: string; + comment: string; + isAuthor: boolean; + onEdit: (id: number, newContent: string) => void; + onDelete: (id: number) => void; +} + +const Comment: React.FC = ({ id, userName, userImageUrl, date, comment, isAuthor, onEdit, onDelete }) => { + const [showOptions, setShowOptions] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editedComment, setEditedComment] = useState(comment); + const [imageError, setImageError] = useState(false); + const optionsRef = useRef(null); + + const handleEdit = () => { + onEdit(id, editedComment); + setIsEditing(false); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (optionsRef.current && !optionsRef.current.contains(event.target as Node)) { + setShowOptions(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+
+
+ {imageError || !userImageUrl ? ( +
+ + {userName?.[0]?.toUpperCase() || "?"} + +
+ ) : ( + {`${userName setImageError(true)} + /> + )} +
+ + {userName || "Anonymous"} + + | + {date} +
+
+ + {isAuthor && ( +
+ + {showOptions && ( +
+
+ + +
+
+ )} +
+ )} +
+ + {isEditing ? ( +
+ setEditedComment(e.target.value)} + size="w-full h-[132px] sm:h-[132px] lg:h-[160px]" + /> +
+ + +
+
+ ) : ( +
+

{comment}

+
+ )} +
+ ); +}; + +export default Comment; diff --git a/src/app/components/input/file/ImageInput/ImageInputwithPlaceHolder.tsx b/src/app/components/input/file/ImageInput/ImageInputwithPlaceHolder.tsx index 5733519b..393788b6 100644 --- a/src/app/components/input/file/ImageInput/ImageInputwithPlaceHolder.tsx +++ b/src/app/components/input/file/ImageInput/ImageInputwithPlaceHolder.tsx @@ -13,9 +13,14 @@ interface ImageInputType { interface ImageInputwithPlaceHolderProps { onImageUpload: (file: File) => Promise; onImagesChange: (images: ImageInputType[]) => void; + size?: "small" | "large"; } -const ImageInputwithPlaceHolder: React.FC = ({ onImageUpload, onImagesChange }) => { +const ImageInputwithPlaceHolder: React.FC = ({ + onImageUpload, + onImagesChange, + size = "large", +}) => { const [imageList, setImageList] = useState([]); const fileInputRef = useRef(null); @@ -51,15 +56,16 @@ const ImageInputwithPlaceHolder: React.FC = ({ o fileInputRef.current?.click(); }; - const size = "size-[160px] lg:size-[240px]"; + const sizeClass = size === "small" ? "size-[160px]" : "size-[160px] lg:size-[240px]"; + const gapClass = size === "small" ? "gap-5" : "gap-5 lg:gap-6"; return ( -
+