diff --git a/src/app/(pages)/albaTalk/[albatalkId]/page.tsx b/src/app/(pages)/albaTalk/[albatalkId]/page.tsx index e46b3b08..a1687ca5 100644 --- a/src/app/(pages)/albaTalk/[albatalkId]/page.tsx +++ b/src/app/(pages)/albaTalk/[albatalkId]/page.tsx @@ -1,311 +1,20 @@ "use client"; -import { useState, useEffect, useRef, useCallback } from "react"; import { useParams } from "next/navigation"; -import CommentDetail from "@/app/components/card/board/CommentDetail"; -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"; +import { PostDetailSection } from "./sections/PostDetailSection"; +import { CommentsSection } from "./sections/CommentsSection"; 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 { albatalkId } = useParams(); - 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 ? ( -
- - {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] lg:h-[160px]" - /> -
-
- -
-
-
- - {/* Comments List or Empty State */} -
- {comments.length > 0 ? ( -
- {comments.map((comment, index) => ( -
- handleEditComment({ commentId: id, newContent })} - onDelete={handleDeleteComment} - /> -
- ))} - {isLoading &&
로딩 중...
} -
- ) : ( -
- No comments -
- )} -
- {showEditModal && ( - setShowEditModal(false)} - onUpdate={(updatedPost) => { - setInitialPost(updatedPost); - setShowEditModal(false); - }} - /> - )} + +
-
+ ); } diff --git a/src/app/(pages)/albaTalk/[albatalkId]/sections/CommentsSection.tsx b/src/app/(pages)/albaTalk/[albatalkId]/sections/CommentsSection.tsx new file mode 100644 index 00000000..7e7259c2 --- /dev/null +++ b/src/app/(pages)/albaTalk/[albatalkId]/sections/CommentsSection.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useState, useCallback } from "react"; +import Image from "next/image"; +import Comment from "@/app/components/card/board/Comment"; +import BaseTextArea from "@/app/components/input/textarea/BaseTextArea"; +import Button from "@/app/components/button/default/Button"; +import { useAddComment } from "@/hooks/queries/post/comment/useAddComment"; +import { useUser } from "@/hooks/queries/user/me/useUser"; +import { useComments } from "@/hooks/queries/post/comment/useComments"; +import LoadingSpinner from "@/app/components/loading-spinner/LoadingSpinner"; +import Pagination from "@/app/components/pagination/Pagination"; + +interface CommentsSectionProps { + postId: string; +} + +export function CommentsSection({ postId }: CommentsSectionProps) { + const [currentPage, setCurrentPage] = useState(1); + const [newComment, setNewComment] = useState(""); + + const { user } = useUser(); + const { + data: commentsData, + isPending: isCommentsLoading, + error: commentsError, + } = useComments(postId, { + page: currentPage, + pageSize: 10, + }); + + const addComment = useAddComment(postId); + + const handleAddComment = useCallback(() => { + if (newComment.trim()) { + addComment.mutate( + { content: newComment }, + { + onSuccess: () => setNewComment(""), + } + ); + } + }, [addComment, newComment]); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + window.scrollTo({ top: 0, behavior: "smooth" }); + }; + + if (isCommentsLoading) { + return ( +
+ +
+ ); + } + + if (commentsError) { + return
댓글을 불러오는데 실패했습니다.
; + } + + return ( +
+

+ 댓글({commentsData?.totalItemCount || 0}) +

+ +
+ + {/* 댓글 입력 영역 */} +
+ setNewComment(e.target.value)} + className="mb-4 h-[132px] w-full lg:h-[160px]" + /> +
+ +
+
+ + {/* 댓글 목록 */} + {commentsData?.data && commentsData.data.length > 0 ? ( +
+ {commentsData.data.map((comment) => ( +
+ +
+ ))} +
+ ) : ( +
+ No comments +
+ )} + + {/* 페이지네이션 */} + {commentsData?.totalPages && commentsData.totalPages > 0 && ( +
+ +
+ )} +
+ ); +} diff --git a/src/app/(pages)/albaTalk/[albatalkId]/sections/PostDetailSection.tsx b/src/app/(pages)/albaTalk/[albatalkId]/sections/PostDetailSection.tsx new file mode 100644 index 00000000..9e19bb84 --- /dev/null +++ b/src/app/(pages)/albaTalk/[albatalkId]/sections/PostDetailSection.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { useUser } from "@/hooks/queries/user/me/useUser"; +import { useLikePost } from "@/hooks/queries/post/useLikePost"; +import { useDeletePost } from "@/hooks/queries/post/useDeletePost"; +import Image from "next/image"; +import { formatLocalDate } from "@/utils/workDayFormatter"; +import KebabDropdown from "@/app/components/button/dropdown/KebabDropdown"; +import useModalStore from "@/store/modalStore"; +import useWidth from "@/hooks/useWidth"; +import { usePostDetail } from "@/hooks/queries/post/usePostDetail"; + +export function PostDetailSection({ postId }: { postId: string }) { + const { isDesktop } = useWidth(); + const { user } = useUser(); + const { data: post } = usePostDetail(postId); + const { likePost, unlikePost } = useLikePost(postId); + const deletePost = useDeletePost(postId); + const { openModal } = useModalStore(); + + const handleLikeClick = () => { + if (post?.isLiked) { + unlikePost.mutate(); + } else { + likePost.mutate(); + } + }; + + const handleDelete = () => { + openModal("customForm", { + isOpen: true, + title: "게시글을 삭제할까요?", + content: "삭제된 게시글은 복구할 수 없습니다.", + confirmText: "삭제하기", + cancelText: "취소", + onConfirm: () => { + deletePost.mutate(undefined, { + onSuccess: () => { + openModal("customForm", { + isOpen: false, + title: "", + content: "", + onConfirm: () => {}, + onCancel: () => {}, + }); + }, + }); + }, + onCancel: () => { + openModal("customForm", { isOpen: false, title: "", content: "", onConfirm: () => {}, onCancel: () => {} }); + }, + }); + }; + + const dropdownOptions = [{ label: "삭제하기", onClick: handleDelete, disabled: deletePost.isPending }]; + + return ( +
+ {/* Header */} +
+

{post?.title}

+ {post?.writer.id === user?.id && } +
+ + {/* Author Info */} +
+ User Icon +
+ + {post?.writer.nickname} + + | + + {formatLocalDate(post?.createdAt || new Date())} + +
+
+ + {/* Content */} +
+ {post?.content} +
+ + {/* Footer */} +
+
+ Comment Icon + + {post?.commentCount} + +
+
+ Like Icon + + {post?.likeCount} + +
+
+
+ ); +} diff --git a/src/app/(pages)/albaTalk/addTalk/page.tsx b/src/app/(pages)/albaTalk/add/page.tsx similarity index 84% rename from src/app/(pages)/albaTalk/addTalk/page.tsx rename to src/app/(pages)/albaTalk/add/page.tsx index 37997adf..cbbe0024 100644 --- a/src/app/(pages)/albaTalk/addTalk/page.tsx +++ b/src/app/(pages)/albaTalk/add/page.tsx @@ -6,15 +6,11 @@ import { useRouter } from "next/navigation"; import Button from "@/app/components/button/default/Button"; import BaseInput from "@/app/components/input/text/BaseInput"; import ImageInputPlaceHolder from "@/app/components/input/file/ImageInput/ImageInputPlaceHolder"; -import { usePost } from "@/hooks/usePost"; +import { useAddPost } from "@/hooks/queries/post/useAddPost"; import axios from "axios"; import DotLoadingSpinner from "@/app/components/loading-spinner/DotLoadingSpinner"; - -interface FormInputs { - title: string; - content: string; - imageUrl?: string; -} +import { PostSchema } from "@/schemas/postSchema"; +import toast from "react-hot-toast"; interface ImageInputType { file: File | null; @@ -28,11 +24,11 @@ export default function AddTalk() { handleSubmit, setValue, formState: { errors }, - } = useForm({ defaultValues: { title: "", content: "" } }); + } = useForm({ defaultValues: { title: "", content: "" } }); const [imageList, setImageList] = useState([]); const router = useRouter(); - const { mutate: createPost, isPending } = usePost(); + const { mutate: createPost, isPending } = useAddPost(); const uploadImage = useCallback(async (file: File): Promise => { const formData = new FormData(); @@ -49,8 +45,9 @@ export default function AddTalk() { return response.data.url; } - throw new Error("이미지 업로드 실패"); + throw new Error("이미지 업로드에 실패했습니다."); } catch (error) { + toast.error("이미지 업로드에 실패했습니다."); console.error("이미지 업로드 실패:", error); throw error; } @@ -65,27 +62,27 @@ export default function AddTalk() { [setValue] ); - const onSubmit: SubmitHandler = async (data) => { - if (!data.title || !data.content) { - alert("제목과 내용을 입력하세요."); - return; - } + const onSubmit: SubmitHandler = async (data) => { + try { + if (!data.title || !data.content) { + toast.error("제목과 내용을 입력하세요."); + return; + } - const postData = { - ...data, - imageUrl: imageList.map((img) => img.url).join(","), - }; - - createPost(postData, { - onSuccess: (response) => { - alert("게시글이 등록되었습니다."); - const boardId = response?.id; - router.push(`/boards/${boardId}`); - }, - onError: (err) => { - alert(err.message || "게시글 등록에 실패했습니다."); - }, - }); + const postData: PostSchema = { + title: data.title, + content: data.content, + imageUrl: imageList.map((img) => img.url).join(","), + }; + + createPost(postData, { + onSuccess: (response) => { + router.push(`/albatalk/${response.id}`); + }, + }); + } catch (error) { + console.error("게시글 등록 실패:", error); + } }; return ( @@ -93,7 +90,7 @@ export default function AddTalk() {
-

글쓰기

+
게시글 쓰기
+ +
+
+
+
+ + ( + + )} + /> +
+ +
+ + ( +