From 2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 2 Jul 2025 00:45:49 +0000 Subject: (대표님/최겸) 20250702 변경사항 업데이트 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/qna/table/create-qna-dialog.tsx | 203 +++++++++++++ lib/qna/table/delete-qna-dialog.tsx | 250 +++++++++++++++ lib/qna/table/improved-comment-section.tsx | 319 +++++++++++++++++++ lib/qna/table/qna-detail.tsx | 455 ++++++++++++++++++++++++++++ lib/qna/table/qna-export-actions.tsx | 261 ++++++++++++++++ lib/qna/table/qna-table-columns.tsx | 325 ++++++++++++++++++++ lib/qna/table/qna-table-toolbar-actions.tsx | 176 +++++++++++ lib/qna/table/qna-table.tsx | 236 +++++++++++++++ lib/qna/table/update-qna-sheet.tsx | 206 +++++++++++++ lib/qna/table/utils.tsx | 329 ++++++++++++++++++++ 10 files changed, 2760 insertions(+) create mode 100644 lib/qna/table/create-qna-dialog.tsx create mode 100644 lib/qna/table/delete-qna-dialog.tsx create mode 100644 lib/qna/table/improved-comment-section.tsx create mode 100644 lib/qna/table/qna-detail.tsx create mode 100644 lib/qna/table/qna-export-actions.tsx create mode 100644 lib/qna/table/qna-table-columns.tsx create mode 100644 lib/qna/table/qna-table-toolbar-actions.tsx create mode 100644 lib/qna/table/qna-table.tsx create mode 100644 lib/qna/table/update-qna-sheet.tsx create mode 100644 lib/qna/table/utils.tsx (limited to 'lib/qna/table') diff --git a/lib/qna/table/create-qna-dialog.tsx b/lib/qna/table/create-qna-dialog.tsx new file mode 100644 index 00000000..d5af932b --- /dev/null +++ b/lib/qna/table/create-qna-dialog.tsx @@ -0,0 +1,203 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + +import { createQnaSchema, type CreateQnaSchema } from "@/lib/qna/validation" +import { createQna } from "../service" +import { QNA_CATEGORY_LABELS } from "@/db/schema" +import TiptapEditor from "@/components/qna/tiptap-editor" + +interface CreateQnaDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function CreateQnaDialog({ open, onOpenChange }: CreateQnaDialogProps) { + const [isCreatePending, startCreateTransition] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(createQnaSchema), + defaultValues: { + title: "", + content: "", + category: undefined, + }, + }) + + function onSubmit(input: CreateQnaSchema) { + startCreateTransition(async () => { + try { + const result = await createQna(input) + + if (result.success) { + toast.success(result.message || "질문이 성공적으로 등록되었습니다.") + form.reset() + onOpenChange(false) + } else { + toast.error(result.error || "질문 등록에 실패했습니다.") + } + } catch (error) { + toast.error("예기치 못한 오류가 발생했습니다.") + console.error("질문 생성 오류:", error) + } + }) + } + + // 다이얼로그가 닫힐 때 폼 리셋 + React.useEffect(() => { + if (!open) { + form.reset() + } + }, [open, form]) + + + console.log(form.getValues(),"생성") + + return ( + + + + 새 질문 작성 + + 질문의 제목과 내용을 입력해주세요. 다른 사용자들이 이해하기 쉽도록 구체적으로 작성해주세요. + + + + {/* 폼 영역: flex-1으로 남은 공간 모두 사용 */} +
+
+ + {/* 카테고리와 제목은 스크롤 없이 고정 */} +
+ {/* 카테고리 선택 */} + ( + + 카테고리 * + + + + )} + /> + + {/* 제목 입력 */} + ( + + 제목 * + + + + + + )} + /> +
+ + {/* 내용 입력 영역: 고정 높이로 스크롤 생성 */} + ( + + 내용 * + + {/* 고정 높이 400px로 설정하여 스크롤 보장 */} +
+ +
+
+ +
+ • 문제 상황을 구체적으로 설명해주세요
+ • 이미지 복사&붙여넣기, 드래그&드롭 지원
+ • 예상하는 결과와 실제 결과를 명시해주세요 +
+
+ )} + /> + + +
+ + + + + +
+
+ ) +} \ No newline at end of file diff --git a/lib/qna/table/delete-qna-dialog.tsx b/lib/qna/table/delete-qna-dialog.tsx new file mode 100644 index 00000000..55dcd366 --- /dev/null +++ b/lib/qna/table/delete-qna-dialog.tsx @@ -0,0 +1,250 @@ +"use client" + +import * as React from "react" +import { toast } from "sonner" +import { Trash2 } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" + +import { QnaViewSelect } from "@/db/schema" +import { useMediaQuery } from "@/hooks/use-media-query" +import { deleteQna } from "../service" + +interface DeleteQnaDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + qnas: QnaViewSelect[] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteQnaDialog({ + open, + onOpenChange, + qnas, + showTrigger = true, + onSuccess +}: DeleteQnaDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + const qnaCount = qnas.length + const isMultiple = qnaCount > 1 + + async function handleDelete() { + startDeleteTransition(async () => { + try { + const promises = qnas.map(qna => deleteQna(qna.id)) + const results = await Promise.all(promises) + + const successCount = results.filter(result => result.success).length + const failCount = results.length - successCount + + if (successCount > 0) { + toast.success( + isMultiple + ? `${successCount}개의 질문이 삭제되었습니다.` + : "질문이 삭제되었습니다." + ) + onSuccess?.() + } + + if (failCount > 0) { + toast.error( + isMultiple + ? `${failCount}개의 질문 삭제에 실패했습니다.` + : "질문 삭제에 실패했습니다." + ) + } + + if (successCount > 0) { + onOpenChange(false) + } + } catch (error) { + toast.error("삭제 중 오류가 발생했습니다.") + console.error("질문 삭제 오류:", error) + } + }) + } + + const title = isMultiple ? `${qnaCount}개 질문 삭제` : "질문 삭제" + const description = isMultiple + ? "선택한 질문들을 정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + : "이 질문을 정말로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + + if (isDesktop) { + return ( + + {showTrigger && ( + + + + )} + + + {title} + {description} + + + {/* 삭제할 질문 목록 */} +
+
삭제 대상:
+ +
+ {qnas.map((qna, index) => ( +
+
+ {index + 1}. +
+
+
+ {qna.title} +
+
+ {qna.authorName} + + {qna.companyName || "미지정"} + + 답변 {qna.totalAnswers}개 +
+
+ {qna.hasAnswers && ( + + 답변있음 + + )} + {qna.isPopular && ( + + 인기질문 + + )} +
+
+
+ ))} +
+
+
+ + {/* 경고 메시지 */} +
+
+ 주의: 질문을 삭제하면 해당 질문의 모든 답변과 댓글도 함께 삭제됩니다. +
+
+ + + + + +
+
+ ) + } + + return ( + + {showTrigger && ( + + + + )} + + + {title} + {description} + + +
+ {/* 삭제할 질문 목록 */} +
+
삭제 대상:
+
+ {qnas.map((qna, index) => ( +
+
+ {index + 1}. +
+
+
+ {qna.title} +
+
+ {qna.authorName} • 답변 {qna.totalAnswers}개 +
+
+
+ ))} +
+
+ + {/* 경고 메시지 */} +
+
+ 주의: 삭제된 질문과 관련 데이터는 복구할 수 없습니다. +
+
+
+ + + + + +
+
+ ) +} \ No newline at end of file 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; + onDeleteComment: (commentId: string | number) => Promise; + onUpdateComment?: (commentId: string | number, content: string) => Promise; +} + +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(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 ( +
+ {/* 헤더 */} +
+
+ + + 댓글 + + {comments.length > 0 && ( + + {comments.length} + + )} +
+ + {session?.user && !showCommentForm && ( + + )} +
+ + {/* 댓글 목록 */} + {comments.length > 0 && ( +
+ {comments.map((comment) => ( +
+
+ {/* 아바타 */} + + + + + + + + {/* 댓글 내용 */} +
+
+ {/* 작성자 정보 */} +
+ + {comment.authorName || comment.author} + + + {format(new Date(comment.createdAt), "MM월 dd일 HH:mm")} + +
+ + {/* 댓글 텍스트 */} + {editingId === comment.id ? ( +
+