Skip to content

✨ feat: 게시글 상세 댓글ui추가 #364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions client/src/components/common/Card/detail/CommentAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState } from "react";

export default function CommentAction({ id, handleModify }: { id: number; handleModify: (id: number) => void }) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const handleOpen = () => {
setIsOpen(!isOpen);
};
return (
<div className="flex gap-2 text-sm">
<button onClick={() => handleModify(id)} className="text-gray-400">
수정
</button>
<button onClick={handleOpen} className="text-gray-400">
삭제
</button>
{isOpen && <DeleteButton id={id} handleOpen={handleOpen} />}
</div>
);
}

function DeleteButton({ id, handleOpen }: { id: number; handleOpen: () => void }) {
return (
<div className="w-[100%] h-[100%] absolute top-0 left-0 z-[1000] bg-white/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0">
<div className="flex flex-col fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-full max-w-xs md:max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg">
<header className="flex flex-col space-y-2 text-center sm:text-left">
<span className="text-lg font-semibold">댓글 삭제</span>
</header>
<section>
<p className="text-sm text-muted-foreground text-center md:text-start">댓글을 정말로 삭제하시겠습니까?</p>
</section>
<footer className="flex flex-row justify-end space-x-2">
<button onClick={handleOpen} className="py-2 px-4 rounded-sm hover:bg-gray-100">
취소
</button>
<button className="bg-primary py-2 px-4 text-white rounded-sm hover:bg-primary/90">확인</button>
</footer>
</div>
</div>
);
}
108 changes: 108 additions & 0 deletions client/src/components/common/Card/detail/PostComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useState } from "react";

import { Heart } from "lucide-react";

import CommentAction from "@/components/common/Card/detail/CommentAction";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";

import { timeAgo } from "@/utils/timeago";

import { PostCommentType } from "@/types/post";

type PostCommentProps = {
comments: PostCommentType[];
};
export default function PostComment({ comments }: PostCommentProps) {
const [modifyId, setModifyId] = useState<number | null>(null);
const handleModify = (id: number | null) => {
setModifyId(id);
};
return (
<div className="w-full space-y-6">
{/* 댓글 입력 영역 */}
<div className="bg-gray-50 rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="p-4">
<div className="flex items-start gap-3">
<Avatar className="w-10 h-10">
<AvatarImage src="https://github.com/shadcn.png" alt="사용자 프로필" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<textarea
placeholder="댓글을 입력하세요..."
className="flex-1 bg-transparent p-2 rounded-md h-20 focus:outline-none focus:ring-2 focus:ring-gray-300 focus:border-transparent resize-none"
></textarea>
</div>
</div>
<div className="flex justify-end px-4 pb-4">
<button className="bg-primary hover:bg-primary/90 text-white font-medium py-2 px-4 rounded-full transition-colors">
등록
</button>
</div>
</div>

{/* 댓글 목록 헤더 */}
<div className="flex items-center border-b border-gray-200 pb-2">
<h3 className="font-bold text-lg">
댓글 <span className="text-yellow-500">{comments.length}</span>
</h3>
</div>

{/* 댓글 목록 */}
<ul className="space-y-4">
{comments
.sort((a, b) => Number(new Date(b.createdAt)) - Number(new Date(a.createdAt)))
.map((comment) => (
<li key={comment.id} className="border-b border-gray-100 pb-4">
<div className="flex items-start gap-3">
<Avatar className="w-8 h-8">
<AvatarImage src={comment.authorImage} alt={comment.author} />
<AvatarFallback>{comment.author.substring(0, 2)}</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2">
<div className="flex justify-between w-full">
<div className="flex gap-2 items-center">
<p className="font-semibold text-sm">{comment.author}</p>
<p className="text-sm text-gray-400">{timeAgo(comment.createdAt)}</p>
<span className="flex text-[10px] items-center gap-1 border rounded-sm p-1 hover:bg-red-300">
<Heart size={15} color="red" fill={comment.isLiked ? `red` : "#fff"} />
{comment.likes}
</span>
</div>
{modifyId !== comment.id && <CommentAction id={comment.id} handleModify={handleModify} />}
</div>
</div>
{modifyId !== comment.id ? (
<p className="mt-1 text-gray-800">{comment.content}</p>
) : (
<div className="">
<textarea className="w-[100%] mt-2 flex-1 bg-transparent p-2 rounded-md h-20 outline-none ring-2 ring-gray-300 border-transparent resize-none">
{comment.content}
</textarea>
<div className="flex justify-end gap-3 text-sm">
<button onClick={() => handleModify(null)} className="hover:bg-gray-200 py-2 px-4 rounded-lg">
취소
</button>
<button className="bg-primary hover:bg-primary/80 py-2 px-4 text-white rounded-lg">
댓글 수정
</button>
</div>
</div>
)}
</div>
</div>
</li>
))}
</ul>

{/* 더보기 버튼 */}
{comments.length > 1 && (
<div className="flex justify-center">
<button className="px-4 py-2 border border-gray-200 rounded-full text-sm text-black hover:bg-gray-200 transition-colors">
댓글 더보기
</button>
</div>
)}
</div>
);
}
6 changes: 5 additions & 1 deletion client/src/components/common/Card/detail/PostContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import React from "react";
import Markdown from "react-markdown";

import LikeButton from "@/components/common/Card/detail/LikeButton";
import PostComment from "@/components/common/Card/detail/PostComment";
import ShareButton from "@/components/common/Card/detail/ShareButton";

import { usePostCardActions } from "@/hooks/common/usePostCardActions";

import { POST_COMMENT_DATA } from "@/constants/dummyData";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P5) 혹시 실제로 배포된 페이지에서 댓글이 사용될 때도 dummy data가 쓰이는걸까요?


import { useMediaStore } from "@/store/useMediaStore";
import { Post } from "@/types/post";

Expand Down Expand Up @@ -51,10 +54,11 @@ export const PostContent = React.memo(({ post }: PostContentProps) => {
<p className="text-gray-400">💡 인공지능이 요약한 내용입니다. 오류가 포함될 수 있으니 참고 바랍니다.</p>
)}
</div>
<div className="flex gap-3">
<div className="flex gap-3 border-b pb-5">
<LikeButton />
<ShareButton post={post} />
</div>
<PostComment comments={POST_COMMENT_DATA} />
</div>
);
});
Expand Down
21 changes: 21 additions & 0 deletions client/src/constants/dummyData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChatType } from "@/types/chat";
import { PostDetailType } from "@/types/post";
import { PostCommentType } from "@/types/post";

// export const TRENDING_POSTS: Post[] = [
// {
Expand Down Expand Up @@ -151,3 +152,23 @@ export const POST_MODAL_DATA: PostDetailType = {
tag: ["JavaScript", "React", "Frontend"],
},
};
export const POST_COMMENT_DATA: PostCommentType[] = [
{
id: 1,
author: "나무보다 숲을",
content: "정말 유익한 포스트에요!!",
authorImage: "https://github.com/shadcn.png",
createdAt: "2025-03-23T01:00:00.000Z",
likes: 5,
isLiked: true,
},
{
id: 2,
author: "월성참치",
content: "엌ㅋㅋㅋ 개추요",
authorImage: "https://github.com/shadcn.png",
createdAt: "2025-03-24T01:00:00.000Z",
likes: 10,
isLiked: false,
},
];
3 changes: 1 addition & 2 deletions client/src/hooks/common/usePostCardActions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { usePostViewIncrement } from "@/hooks/queries/usePostViewIncrement";

// import { pipe } from "@/utils/pipe";
import { Post } from "@/types/post";

interface PostWithState {
Expand All @@ -24,7 +23,7 @@ export const usePostCardActions = (post: Post) => {
if (isWindowOpened) {
mutate(undefined, {
onSuccess: () => {
console.log("조회수 증가 성공");
// console.log("조회수 증가 성공");
},
onError: (error) => {
console.error("조회수 증가 실패", error);
Expand Down
9 changes: 9 additions & 0 deletions client/src/types/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,12 @@ export interface PostDetailType {
message: string;
data: Post;
}
export interface PostCommentType {
id: number;
author: string;
content: string;
authorImage: string;
createdAt: string;
likes: number;
isLiked: boolean;
}
13 changes: 13 additions & 0 deletions client/src/utils/timeago.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function timeAgo(dateString: string) {
const now = new Date();
const past = new Date(dateString);
const diff = Number(now) - Number(past);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P5) 파일명과 함수명이 달라요 !


const diffMin = Math.floor(diff / (1000 * 60));
const diffHour = Math.floor(diff / (1000 * 60 * 60));
const diffDay = Math.floor(diff / (1000 * 60 * 60 * 24));

if (diffMin < 60) return `${diffMin}분 전`;
if (diffHour < 24) return `${diffHour}시간 전`;
return `${diffDay}일 전`;
}