Skip to content
Merged
55 changes: 40 additions & 15 deletions src/components/post/CommentItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,45 @@ import { useMemo, useState } from 'react'
import { Heart, MoreHorizontal } from 'lucide-react'
import { formatRelativeTime } from '../../utils/date.ts'
import CommentMenuModal from './CommentMenuModal'
import { useAuthStore } from '@/shared/auth/authStore'
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar.tsx'
import { useDeleteCommentMutation } from '@/entities/post/model/hooks/useDeleteCommentMutation'

interface Comment {
id: number
postId: number
userId: number
nickname: string
content: string
profileImageUrl: string
createdAt: string
updatedAt: string
parentId: number | null
likeCount: number
liked: boolean
likedUserIds: number[]
}

interface CommentItemProps {
comment: Comment
isReply?: boolean
isLiked: boolean
onDoubleClick: (id: number) => void
onHeartClick: (id: number, e: React.MouseEvent) => void
onDeleteSuccess?: (commentId: number) => void
onEditClick: (comment: Comment) => void
}

export default function CommentItem({
comment,
isReply = false,
isLiked,
onDoubleClick,
onHeartClick,
onDeleteSuccess,
onEditClick,
}: CommentItemProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const { user } = useAuthStore()
const deleteMutation = useDeleteCommentMutation(comment.postId)

const isEdited = comment.createdAt !== comment.updatedAt

const timeDisplay = useMemo(
() => formatRelativeTime(comment.createdAt),
Expand All @@ -45,6 +53,25 @@ export default function CommentItem({
}

const handleDelete = () => {
deleteMutation.mutate(comment.id, {
onSuccess: () => {
if (onDeleteSuccess) {
onDeleteSuccess(comment.id)
}
},
})
setIsMenuOpen(false)
}

const handleHide = () => {
if (onDeleteSuccess) {
onDeleteSuccess(comment.id)
}
setIsMenuOpen(false)
}

const handleEdit = () => {
onEditClick(comment)
setIsMenuOpen(false)
}

Expand All @@ -57,9 +84,7 @@ export default function CommentItem({
onDoubleClick={() => onDoubleClick(comment.id)}
>
<div className="flex flex-1 items-start gap-3">
<Avatar
className={`${isReply ? 'h-6 w-6' : 'h-8 w-8'} mt-0.5 shrink-0`}
>
<Avatar className="mt-0.5 h-8 w-8 shrink-0">
<AvatarImage
src={comment.profileImageUrl}
alt={comment.nickname}
Expand All @@ -74,18 +99,15 @@ export default function CommentItem({
<span className="break-all text-black">{comment.content}</span>

<div className="mt-[7px] flex h-4 items-center gap-3 text-xs font-semibold text-gray-500">
<span className="font-normal">{timeDisplay}</span>
<span className="font-normal">
{timeDisplay}
{isEdited && ' (수정됨)'}
</span>
{displayLikeCount > 0 && (
<span className="font-semibold text-gray-500">
좋아요 {displayLikeCount}
</span>
)}
<button
className="hover:text-gray-900"
onClick={(e) => e.stopPropagation()}
>
답글 달기
</button>
<button
className="p-1 opacity-0 transition-opacity group-hover:opacity-100"
onClick={handleMenuClick}
Expand Down Expand Up @@ -117,7 +139,10 @@ export default function CommentItem({
<CommentMenuModal
onClose={() => setIsMenuOpen(false)}
onDelete={handleDelete}
isMine={user ? comment.userId === user.id : false}
onEdit={handleEdit}
onHide={handleHide}
authorId={comment.userId}
nickname={comment.nickname}
/>
)}
</>
Expand Down
53 changes: 43 additions & 10 deletions src/components/post/CommentMenuModal.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,84 @@
import { useState, useEffect } from 'react'
import { useAuthStore } from '@/shared/auth/authStore'
import ReportModal from './ReportModal'

interface CommentMenuModalProps {
onClose: () => void
onDelete?: () => void
isMine: boolean
onEdit?: () => void
onHide: () => void
authorId: number
nickname: string
}

export default function CommentMenuModal({
onClose,
onDelete,
isMine,
onEdit,
onHide,
authorId,
nickname,
}: CommentMenuModalProps) {
const [isVisible, setIsVisible] = useState(false)
const [isReportOpen, setIsReportOpen] = useState(false)
const { user } = useAuthStore()
const isMine = user?.id === authorId

useEffect(() => {
const frame = requestAnimationFrame(() => setIsVisible(true))
return () => cancelAnimationFrame(frame)
}, [])

if (isReportOpen) {
return (
<ReportModal
onClose={onClose}
onHideComment={onHide}
nickname={nickname}
/>
)
}

return (
<div
className={`fixed inset-0 z-[100] flex items-center justify-center bg-black/50 transition-opacity duration-200 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
onClick={onClose}
>
<div
className={`w-full max-w-[560px] overflow-hidden rounded-[24px] bg-white transition-all duration-200 ease-out ${
className={`w-full max-w-[400px] overflow-hidden rounded-[12px] bg-white transition-all duration-200 ease-out ${
isVisible ? 'scale-100 opacity-100' : 'scale-110 opacity-0'
}`}
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col text-center">
{isMine ? (
<>
<button
onClick={onDelete}
className="w-full border-b border-gray-200 py-3.5 text-[14px] font-bold text-[#ED4956] active:bg-gray-100"
>
삭제
</button>
<button
onClick={onEdit}
className="w-full border-b border-gray-200 py-3.5 text-[14px] font-normal text-black active:bg-gray-100"
>
수정
</button>
</>
) : (
<button
onClick={onDelete}
className="w-full border-b border-gray-200 py-4 text-[14px] font-bold text-[#ED4956] active:bg-gray-100"
onClick={() => setIsReportOpen(true)}
className="w-full border-b border-gray-200 py-3.5 text-[14px] font-bold text-[#ED4956] active:bg-gray-100"
>
삭제
</button>
) : (
<button className="w-full border-b border-gray-200 py-4 text-[14px] font-bold text-[#ED4956] active:bg-gray-100">
신고
</button>
)}
<button
onClick={onClose}
className="w-full py-4 text-[14px] font-normal text-black active:bg-gray-100"
className="w-full py-3.5 text-[14px] font-normal text-black active:bg-gray-100"
>
취소
</button>
Expand Down
67 changes: 45 additions & 22 deletions src/components/post/PostActionSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useState } from 'react'
import { Heart, MessageCircle, Send, Bookmark, Smile } from 'lucide-react'
import { useAuthStore } from '@/shared/auth/authStore'
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar.tsx'
import { Heart, MessageCircle, Bookmark, Smile, X } from 'lucide-react'

interface PostActionSectionProps {
likeCount: number
Expand All @@ -11,6 +9,10 @@ interface PostActionSectionProps {
onLikeClick: () => void
onBookmarkClick: () => void
onCommentSubmit: (content: string) => void
onCommentIconClick: () => void
inputRef: React.RefObject<HTMLInputElement | null>
editValue?: string
onCancelEdit?: () => void
}

export default function PostActionSection({
Expand All @@ -21,9 +23,12 @@ export default function PostActionSection({
onLikeClick,
onBookmarkClick,
onCommentSubmit,
onCommentIconClick,
inputRef,
editValue,
onCancelEdit,
}: PostActionSectionProps) {
const [comment, setComment] = useState('')
const { user } = useAuthStore()
const [comment, setComment] = useState(editValue ?? '')

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
Expand All @@ -32,6 +37,15 @@ export default function PostActionSection({
setComment('')
}

const handleCancel = () => {
setComment('')
onCancelEdit?.()
}

const isEditMode = editValue !== undefined
const isUnchanged = isEditMode && comment === editValue
const isSubmitDisabled = !comment.trim() || isUnchanged

return (
<div className="flex flex-col border-t border-gray-200 bg-white">
<div className="flex items-center justify-between px-4 pt-3 pb-2">
Expand All @@ -44,12 +58,12 @@ export default function PostActionSection({
className={`h-6 w-6 ${isLiked ? 'fill-[#ED4956] text-[#ED4956]' : 'text-black'}`}
/>
</button>
<button className="transition-opacity hover:opacity-60">
<button
onClick={onCommentIconClick}
className="transition-opacity hover:opacity-60"
>
<MessageCircle className="h-6 w-6 text-black" />
</button>
<button className="transition-opacity hover:opacity-60">
<Send className="h-6 w-6 text-black" />
</button>
</div>
<button
onClick={onBookmarkClick}
Expand All @@ -68,38 +82,47 @@ export default function PostActionSection({
<div className="text-[10px] text-gray-500 uppercase">{createdAt}</div>
</div>

{isEditMode && (
<div className="flex items-center justify-between border-t border-gray-100 bg-gray-50 px-4 py-2">
<span className="text-xs font-medium text-gray-500">
댓글 수정 중...
</span>
<button
onClick={handleCancel}
className="p-1 transition-opacity hover:opacity-60"
>
<X className="h-4 w-4 text-gray-400" />
</button>
</div>
)}

<form
onSubmit={handleSubmit}
className="mt-2 flex items-center border-t border-gray-100 px-4 py-3"
className="flex items-center border-t border-gray-100 px-4 py-3"
>
<div className="mr-3 flex items-center gap-3">
<div className="mr-3 flex items-center">
<button type="button" className="transition-opacity hover:opacity-60">
<Smile className="h-6 w-6 text-black" />
</button>
{user && (
<Avatar className="h-6 w-6">
<AvatarImage src={user.profileImageUrl} alt={user.nickname} />
<AvatarFallback>{user.nickname[0]}</AvatarFallback>
</Avatar>
)}
</div>
<input
type="text"
ref={inputRef}
placeholder="댓글 달기..."
className="flex-1 text-sm outline-none placeholder:text-gray-500"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
<button
type="submit"
disabled={!comment.trim()}
disabled={isSubmitDisabled}
className={`ml-2 text-sm font-bold transition-opacity ${
comment.trim()
? 'cursor-pointer text-[#0095F6]'
: 'cursor-default text-[#0095F6]/50'
isSubmitDisabled
? 'cursor-default text-[#0095F6]/50'
: 'cursor-pointer text-[#0095F6]'
}`}
>
게시
{isEditMode ? '수정' : '게시'}
</button>
</form>
</div>
Expand Down
Loading