diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-02 00:45:49 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-02 00:45:49 +0000 |
| commit | 2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 (patch) | |
| tree | f406b5c86f563347c7fd088a85fd1a82284dc5ff /lib/qna/table/improved-comment-section.tsx | |
| parent | 6a9ca20deddcdcbe8495cf5a73ec7ea5f53f9b55 (diff) | |
(대표님/최겸) 20250702 변경사항 업데이트
Diffstat (limited to 'lib/qna/table/improved-comment-section.tsx')
| -rw-r--r-- | lib/qna/table/improved-comment-section.tsx | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/lib/qna/table/improved-comment-section.tsx b/lib/qna/table/improved-comment-section.tsx new file mode 100644 index 00000000..ce32b706 --- /dev/null +++ b/lib/qna/table/improved-comment-section.tsx @@ -0,0 +1,319 @@ +import * as React from "react"; +import { useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { format } from "date-fns"; +import { Comment } from "@/lib/qna/types"; +import { + MessageCircle, + Trash2, + Edit, + Check, + X, + User, + Plus +} from "lucide-react"; + +interface ImprovedCommentSectionProps { + answerId: string | number; + comments: Comment[]; + onAddComment: (content: string) => Promise<void>; + onDeleteComment: (commentId: string | number) => Promise<void>; + onUpdateComment?: (commentId: string | number, content: string) => Promise<void>; +} + +export function ImprovedCommentSection({ + answerId, + comments, + onAddComment, + onDeleteComment, + onUpdateComment +}: ImprovedCommentSectionProps) { + const { data: session } = useSession(); + + // 상태 관리 + const [content, setContent] = React.useState(""); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [editingId, setEditingId] = React.useState<string | number | null>(null); + const [editContent, setEditContent] = React.useState(""); + const [showCommentForm, setShowCommentForm] = React.useState(false); + + // 댓글 작성 + const handleSubmit = async () => { + if (!content.trim() || !session?.user) return; + + setIsSubmitting(true); + try { + await onAddComment(content); + setContent(""); + setShowCommentForm(false); + } catch (error) { + console.error("댓글 작성 실패:", error); + } finally { + setIsSubmitting(false); + } + }; + + // 댓글 수정 시작 + const handleEditStart = (comment: Comment) => { + setEditingId(comment.id); + setEditContent(comment.content); + }; + + // 댓글 수정 취소 + const handleEditCancel = () => { + setEditingId(null); + setEditContent(""); + }; + + // 댓글 수정 저장 + const handleEditSave = async (commentId: string | number) => { + if (!editContent.trim() || !onUpdateComment) return; + + try { + await onUpdateComment(commentId, editContent); + setEditingId(null); + setEditContent(""); + } catch (error) { + console.error("댓글 수정 실패:", error); + } + }; + + return ( + <div className="space-y-4"> + {/* 헤더 */} + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <MessageCircle className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm font-medium text-muted-foreground"> + 댓글 + </span> + {comments.length > 0 && ( + <Badge variant="secondary" className="text-xs h-5"> + {comments.length} + </Badge> + )} + </div> + + {session?.user && !showCommentForm && ( + <Button + variant="ghost" + size="sm" + onClick={() => setShowCommentForm(true)} + className="gap-2 text-xs" + > + <Plus className="h-3 w-3" /> + 댓글 작성 + </Button> + )} + </div> + + {/* 댓글 목록 */} + {comments.length > 0 && ( + <div className="space-y-3"> + {comments.map((comment) => ( + <div key={comment.id} className="group"> + <div className="flex gap-3"> + {/* 아바타 */} + <Avatar className="h-7 w-7 shrink-0"> + <AvatarImage src={comment.authorImageUrl} /> + <AvatarFallback className="text-xs"> + <User className="h-3 w-3" /> + </AvatarFallback> + </Avatar> + + {/* 댓글 내용 */} + <div className="flex-1 min-w-0"> + <div className="bg-muted/50 rounded-lg px-3 py-2"> + {/* 작성자 정보 */} + <div className="flex items-center gap-2 mb-1"> + <span className="text-sm font-medium"> + {comment.authorName || comment.author} + </span> + <span className="text-xs text-muted-foreground"> + {format(new Date(comment.createdAt), "MM월 dd일 HH:mm")} + </span> + </div> + + {/* 댓글 텍스트 */} + {editingId === comment.id ? ( + <div className="space-y-2"> + <Textarea + value={editContent} + onChange={(e) => setEditContent(e.target.value)} + className="min-h-[60px] text-sm resize-none" + maxLength={250} + placeholder="댓글을 입력하세요..." + /> + <div className="flex items-center justify-between"> + <span className="text-xs text-muted-foreground"> + {editContent.length}/250 + </span> + <div className="flex gap-1"> + <Button + variant="ghost" + size="sm" + onClick={() => handleEditSave(comment.id)} + disabled={!editContent.trim()} + className="h-7 px-2 text-green-600 hover:text-green-700 hover:bg-green-50" + > + <Check className="h-3 w-3" /> + </Button> + <Button + variant="ghost" + size="sm" + onClick={handleEditCancel} + className="h-7 px-2 text-muted-foreground hover:bg-muted" + > + <X className="h-3 w-3" /> + </Button> + </div> + </div> + </div> + ) : ( + <p className="text-sm leading-relaxed whitespace-pre-wrap"> + {comment.content} + </p> + )} + </div> + + {/* 액션 버튼들 */} + {session?.user?.id === comment.author && editingId !== comment.id && ( + <div className="flex gap-1 mt-1 opacity-0 group-hover:opacity-100 transition-opacity"> + {onUpdateComment && ( + <Button + variant="ghost" + size="sm" + onClick={() => handleEditStart(comment)} + className="h-6 px-2 text-xs text-muted-foreground hover:text-blue-600" + > + <Edit className="h-3 w-3 mr-1" /> + 수정 + </Button> + )} + + <AlertDialog> + <AlertDialogTrigger asChild> + <Button + variant="ghost" + size="sm" + className="h-6 px-2 text-xs text-muted-foreground hover:text-destructive" + > + <Trash2 className="h-3 w-3 mr-1" /> + 삭제 + </Button> + </AlertDialogTrigger> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>댓글 삭제</AlertDialogTitle> + <AlertDialogDescription> + 정말로 이 댓글을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>취소</AlertDialogCancel> + <AlertDialogAction + onClick={() => onDeleteComment(comment.id)} + className="bg-destructive hover:bg-destructive/90" + > + 삭제 + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + </div> + )} + </div> + </div> + </div> + ))} + </div> + )} + + {/* 댓글 작성 폼 */} + {session?.user && showCommentForm && ( + <div className="space-y-3 pt-2 border-t"> + <div className="flex gap-3"> + <Avatar className="h-7 w-7 shrink-0"> + <AvatarImage src={session.user.image} /> + <AvatarFallback className="text-xs"> + <User className="h-3 w-3" /> + </AvatarFallback> + </Avatar> + <div className="flex-1 space-y-2"> + <Textarea + value={content} + onChange={(e) => { + if (e.target.value.length <= 250) { + setContent(e.target.value); + } + }} + placeholder="댓글을 입력하세요..." + className="min-h-[80px] resize-none" + maxLength={250} + disabled={isSubmitting} + /> + <div className="flex items-center justify-between"> + <span className="text-xs text-muted-foreground"> + {content.length}/250 + </span> + <div className="flex gap-2"> + <Button + variant="ghost" + size="sm" + onClick={() => { + setShowCommentForm(false); + setContent(""); + }} + disabled={isSubmitting} + > + 취소 + </Button> + <Button + size="sm" + onClick={handleSubmit} + disabled={isSubmitting || !content.trim()} + > + {isSubmitting ? "저장 중..." : "등록"} + </Button> + </div> + </div> + </div> + </div> + </div> + )} + + {/* 빈 상태 */} + {comments.length === 0 && !showCommentForm && ( + <div className="text-center py-6 text-muted-foreground"> + <MessageCircle className="h-8 w-8 mx-auto mb-2 opacity-50" /> + <p className="text-sm">아직 댓글이 없습니다.</p> + {session?.user && ( + <Button + variant="ghost" + size="sm" + onClick={() => setShowCommentForm(true)} + className="mt-2 gap-2" + > + <Plus className="h-3 w-3" /> + 첫 번째 댓글 작성하기 + </Button> + )} + </div> + )} + </div> + ); +}
\ No newline at end of file |
