summaryrefslogtreecommitdiff
path: root/lib/qna/table/improved-comment-section.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-02 00:45:49 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-02 00:45:49 +0000
commit2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 (patch)
treef406b5c86f563347c7fd088a85fd1a82284dc5ff /lib/qna/table/improved-comment-section.tsx
parent6a9ca20deddcdcbe8495cf5a73ec7ea5f53f9b55 (diff)
(대표님/최겸) 20250702 변경사항 업데이트
Diffstat (limited to 'lib/qna/table/improved-comment-section.tsx')
-rw-r--r--lib/qna/table/improved-comment-section.tsx319
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