Skip to content

Commit d213fcb

Browse files
authored
Merge pull request #100 from wafflestudio/99-feature-post-commentModalApi
2 parents 0703a97 + 1c64cc8 commit d213fcb

21 files changed

+1553
-130
lines changed

src/components/post/CommentItem.tsx

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,45 @@ import { useMemo, useState } from 'react'
22
import { Heart, MoreHorizontal } from 'lucide-react'
33
import { formatRelativeTime } from '../../utils/date.ts'
44
import CommentMenuModal from './CommentMenuModal'
5-
import { useAuthStore } from '@/shared/auth/authStore'
65
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar.tsx'
6+
import { useDeleteCommentMutation } from '@/entities/post/model/hooks/useDeleteCommentMutation'
77

88
interface Comment {
99
id: number
10+
postId: number
1011
userId: number
1112
nickname: string
1213
content: string
1314
profileImageUrl: string
1415
createdAt: string
16+
updatedAt: string
17+
parentId: number | null
1518
likeCount: number
1619
liked: boolean
20+
likedUserIds: number[]
1721
}
1822

1923
interface CommentItemProps {
2024
comment: Comment
21-
isReply?: boolean
2225
isLiked: boolean
2326
onDoubleClick: (id: number) => void
2427
onHeartClick: (id: number, e: React.MouseEvent) => void
28+
onDeleteSuccess?: (commentId: number) => void
29+
onEditClick: (comment: Comment) => void
2530
}
2631

2732
export default function CommentItem({
2833
comment,
29-
isReply = false,
3034
isLiked,
3135
onDoubleClick,
3236
onHeartClick,
37+
onDeleteSuccess,
38+
onEditClick,
3339
}: CommentItemProps) {
3440
const [isMenuOpen, setIsMenuOpen] = useState(false)
35-
const { user } = useAuthStore()
41+
const deleteMutation = useDeleteCommentMutation(comment.postId)
42+
43+
const isEdited = comment.createdAt !== comment.updatedAt
3644

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

4755
const handleDelete = () => {
56+
deleteMutation.mutate(comment.id, {
57+
onSuccess: () => {
58+
if (onDeleteSuccess) {
59+
onDeleteSuccess(comment.id)
60+
}
61+
},
62+
})
63+
setIsMenuOpen(false)
64+
}
65+
66+
const handleHide = () => {
67+
if (onDeleteSuccess) {
68+
onDeleteSuccess(comment.id)
69+
}
70+
setIsMenuOpen(false)
71+
}
72+
73+
const handleEdit = () => {
74+
onEditClick(comment)
4875
setIsMenuOpen(false)
4976
}
5077

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

76101
<div className="mt-[7px] flex h-4 items-center gap-3 text-xs font-semibold text-gray-500">
77-
<span className="font-normal">{timeDisplay}</span>
102+
<span className="font-normal">
103+
{timeDisplay}
104+
{isEdited && ' (수정됨)'}
105+
</span>
78106
{displayLikeCount > 0 && (
79107
<span className="font-semibold text-gray-500">
80108
좋아요 {displayLikeCount}
81109
</span>
82110
)}
83-
<button
84-
className="hover:text-gray-900"
85-
onClick={(e) => e.stopPropagation()}
86-
>
87-
답글 달기
88-
</button>
89111
<button
90112
className="p-1 opacity-0 transition-opacity group-hover:opacity-100"
91113
onClick={handleMenuClick}
@@ -117,7 +139,10 @@ export default function CommentItem({
117139
<CommentMenuModal
118140
onClose={() => setIsMenuOpen(false)}
119141
onDelete={handleDelete}
120-
isMine={user ? comment.userId === user.id : false}
142+
onEdit={handleEdit}
143+
onHide={handleHide}
144+
authorId={comment.userId}
145+
nickname={comment.nickname}
121146
/>
122147
)}
123148
</>

src/components/post/CommentMenuModal.tsx

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,84 @@
11
import { useState, useEffect } from 'react'
2+
import { useAuthStore } from '@/shared/auth/authStore'
3+
import ReportModal from './ReportModal'
24

35
interface CommentMenuModalProps {
46
onClose: () => void
57
onDelete?: () => void
6-
isMine: boolean
8+
onEdit?: () => void
9+
onHide: () => void
10+
authorId: number
11+
nickname: string
712
}
813

914
export default function CommentMenuModal({
1015
onClose,
1116
onDelete,
12-
isMine,
17+
onEdit,
18+
onHide,
19+
authorId,
20+
nickname,
1321
}: CommentMenuModalProps) {
1422
const [isVisible, setIsVisible] = useState(false)
23+
const [isReportOpen, setIsReportOpen] = useState(false)
24+
const { user } = useAuthStore()
25+
const isMine = user?.id === authorId
1526

1627
useEffect(() => {
1728
const frame = requestAnimationFrame(() => setIsVisible(true))
1829
return () => cancelAnimationFrame(frame)
1930
}, [])
2031

32+
if (isReportOpen) {
33+
return (
34+
<ReportModal
35+
onClose={onClose}
36+
onHideComment={onHide}
37+
nickname={nickname}
38+
/>
39+
)
40+
}
41+
2142
return (
2243
<div
2344
className={`fixed inset-0 z-[100] flex items-center justify-center bg-black/50 transition-opacity duration-200 ${
2445
isVisible ? 'opacity-100' : 'opacity-0'
2546
}`}
47+
onClick={onClose}
2648
>
2749
<div
28-
className={`w-full max-w-[560px] overflow-hidden rounded-[24px] bg-white transition-all duration-200 ease-out ${
50+
className={`w-full max-w-[400px] overflow-hidden rounded-[12px] bg-white transition-all duration-200 ease-out ${
2951
isVisible ? 'scale-100 opacity-100' : 'scale-110 opacity-0'
3052
}`}
3153
onClick={(e) => e.stopPropagation()}
3254
>
3355
<div className="flex flex-col text-center">
3456
{isMine ? (
57+
<>
58+
<button
59+
onClick={onDelete}
60+
className="w-full border-b border-gray-200 py-3.5 text-[14px] font-bold text-[#ED4956] active:bg-gray-100"
61+
>
62+
삭제
63+
</button>
64+
<button
65+
onClick={onEdit}
66+
className="w-full border-b border-gray-200 py-3.5 text-[14px] font-normal text-black active:bg-gray-100"
67+
>
68+
수정
69+
</button>
70+
</>
71+
) : (
3572
<button
36-
onClick={onDelete}
37-
className="w-full border-b border-gray-200 py-4 text-[14px] font-bold text-[#ED4956] active:bg-gray-100"
73+
onClick={() => setIsReportOpen(true)}
74+
className="w-full border-b border-gray-200 py-3.5 text-[14px] font-bold text-[#ED4956] active:bg-gray-100"
3875
>
39-
삭제
40-
</button>
41-
) : (
42-
<button className="w-full border-b border-gray-200 py-4 text-[14px] font-bold text-[#ED4956] active:bg-gray-100">
4376
신고
4477
</button>
4578
)}
4679
<button
4780
onClick={onClose}
48-
className="w-full py-4 text-[14px] font-normal text-black active:bg-gray-100"
81+
className="w-full py-3.5 text-[14px] font-normal text-black active:bg-gray-100"
4982
>
5083
취소
5184
</button>

src/components/post/PostActionSection.tsx

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { useState } from 'react'
2-
import { Heart, MessageCircle, Send, Bookmark, Smile } from 'lucide-react'
3-
import { useAuthStore } from '@/shared/auth/authStore'
4-
import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/avatar.tsx'
2+
import { Heart, MessageCircle, Bookmark, Smile, X } from 'lucide-react'
53

64
interface PostActionSectionProps {
75
likeCount: number
@@ -11,6 +9,10 @@ interface PostActionSectionProps {
119
onLikeClick: () => void
1210
onBookmarkClick: () => void
1311
onCommentSubmit: (content: string) => void
12+
onCommentIconClick: () => void
13+
inputRef: React.RefObject<HTMLInputElement | null>
14+
editValue?: string
15+
onCancelEdit?: () => void
1416
}
1517

1618
export default function PostActionSection({
@@ -21,9 +23,12 @@ export default function PostActionSection({
2123
onLikeClick,
2224
onBookmarkClick,
2325
onCommentSubmit,
26+
onCommentIconClick,
27+
inputRef,
28+
editValue,
29+
onCancelEdit,
2430
}: PostActionSectionProps) {
25-
const [comment, setComment] = useState('')
26-
const { user } = useAuthStore()
31+
const [comment, setComment] = useState(editValue ?? '')
2732

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

40+
const handleCancel = () => {
41+
setComment('')
42+
onCancelEdit?.()
43+
}
44+
45+
const isEditMode = editValue !== undefined
46+
const isUnchanged = isEditMode && comment === editValue
47+
const isSubmitDisabled = !comment.trim() || isUnchanged
48+
3549
return (
3650
<div className="flex flex-col border-t border-gray-200 bg-white">
3751
<div className="flex items-center justify-between px-4 pt-3 pb-2">
@@ -44,12 +58,12 @@ export default function PostActionSection({
4458
className={`h-6 w-6 ${isLiked ? 'fill-[#ED4956] text-[#ED4956]' : 'text-black'}`}
4559
/>
4660
</button>
47-
<button className="transition-opacity hover:opacity-60">
61+
<button
62+
onClick={onCommentIconClick}
63+
className="transition-opacity hover:opacity-60"
64+
>
4865
<MessageCircle className="h-6 w-6 text-black" />
4966
</button>
50-
<button className="transition-opacity hover:opacity-60">
51-
<Send className="h-6 w-6 text-black" />
52-
</button>
5367
</div>
5468
<button
5569
onClick={onBookmarkClick}
@@ -68,38 +82,47 @@ export default function PostActionSection({
6882
<div className="text-[10px] text-gray-500 uppercase">{createdAt}</div>
6983
</div>
7084

85+
{isEditMode && (
86+
<div className="flex items-center justify-between border-t border-gray-100 bg-gray-50 px-4 py-2">
87+
<span className="text-xs font-medium text-gray-500">
88+
댓글 수정 중...
89+
</span>
90+
<button
91+
onClick={handleCancel}
92+
className="p-1 transition-opacity hover:opacity-60"
93+
>
94+
<X className="h-4 w-4 text-gray-400" />
95+
</button>
96+
</div>
97+
)}
98+
7199
<form
72100
onSubmit={handleSubmit}
73-
className="mt-2 flex items-center border-t border-gray-100 px-4 py-3"
101+
className="flex items-center border-t border-gray-100 px-4 py-3"
74102
>
75-
<div className="mr-3 flex items-center gap-3">
103+
<div className="mr-3 flex items-center">
76104
<button type="button" className="transition-opacity hover:opacity-60">
77105
<Smile className="h-6 w-6 text-black" />
78106
</button>
79-
{user && (
80-
<Avatar className="h-6 w-6">
81-
<AvatarImage src={user.profileImageUrl} alt={user.nickname} />
82-
<AvatarFallback>{user.nickname[0]}</AvatarFallback>
83-
</Avatar>
84-
)}
85107
</div>
86108
<input
87109
type="text"
110+
ref={inputRef}
88111
placeholder="댓글 달기..."
89112
className="flex-1 text-sm outline-none placeholder:text-gray-500"
90113
value={comment}
91114
onChange={(e) => setComment(e.target.value)}
92115
/>
93116
<button
94117
type="submit"
95-
disabled={!comment.trim()}
118+
disabled={isSubmitDisabled}
96119
className={`ml-2 text-sm font-bold transition-opacity ${
97-
comment.trim()
98-
? 'cursor-pointer text-[#0095F6]'
99-
: 'cursor-default text-[#0095F6]/50'
120+
isSubmitDisabled
121+
? 'cursor-default text-[#0095F6]/50'
122+
: 'cursor-pointer text-[#0095F6]'
100123
}`}
101124
>
102-
게시
125+
{isEditMode ? '수정' : '게시'}
103126
</button>
104127
</form>
105128
</div>

0 commit comments

Comments
 (0)