From ef4c533ebacc2cdc97e518f30e9a9350004fcdfb Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Apr 2025 02:13:30 +0000 Subject: ~20250428 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cbe/table/comments-sheet.tsx | 345 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 lib/cbe/table/comments-sheet.tsx (limited to 'lib/cbe/table/comments-sheet.tsx') diff --git a/lib/cbe/table/comments-sheet.tsx b/lib/cbe/table/comments-sheet.tsx new file mode 100644 index 00000000..2ac9049f --- /dev/null +++ b/lib/cbe/table/comments-sheet.tsx @@ -0,0 +1,345 @@ +"use client" + +import * as React from "react" +import { useForm, useFieldArray } from "react-hook-form" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { Loader, Download, X ,Loader2} from "lucide-react" +import prettyBytes from "pretty-bytes" +import { toast } from "sonner" + +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Textarea, +} from "@/components/ui/textarea" + +import { + Dropzone, + DropzoneZone, + DropzoneUploadIcon, + DropzoneTitle, + DropzoneDescription, + DropzoneInput +} from "@/components/ui/dropzone" + +import { + Table, + TableHeader, + TableRow, + TableHead, + TableBody, + TableCell +} from "@/components/ui/table" + +// DB 스키마에서 필요한 타입들을 가져온다고 가정 +// (실제 프로젝트에 맞춰 import를 수정하세요.) +import { formatDate } from "@/lib/utils" +import { createRfqCommentWithAttachments } from "@/lib/rfqs/service" + +// 코멘트 + 첨부파일 구조 (단순 예시) +// 실제 DB 스키마에 맞춰 조정 +export interface CbeComment { + id: number + commentText: string + commentedBy?: number + commentedByEmail?: string + createdAt?: Date + attachments?: { + id: number + fileName: string + filePath: string + }[] +} + +interface CommentSheetProps extends React.ComponentPropsWithRef { + initialComments?: CbeComment[] + currentUserId: number + rfqId: number + // tbeId?: number + cbeId?: number + vendorId: number + onCommentsUpdated?: (comments: CbeComment[]) => void + isLoading?: boolean // New prop +} + +// 새 코멘트 작성 폼 스키마 +const commentFormSchema = z.object({ + commentText: z.string().min(1, "댓글을 입력하세요."), + newFiles: z.array(z.any()).optional() // File[] +}) +type CommentFormValues = z.infer + +const MAX_FILE_SIZE = 30e6 // 30MB + +export function CommentSheet({ + rfqId, + vendorId, + initialComments = [], + currentUserId, + // tbeId, + cbeId, + onCommentsUpdated, + isLoading = false, // Default to false + ...props +}: CommentSheetProps) { + const [comments, setComments] = React.useState(initialComments) + const [isPending, startTransition] = React.useTransition() + + React.useEffect(() => { + setComments(initialComments) + }, [initialComments]) + + + // RHF 세팅 + const form = useForm({ + resolver: zodResolver(commentFormSchema), + defaultValues: { + commentText: "", + newFiles: [] + } + }) + + // formFieldArray 예시 (파일 목록) + const { fields: newFileFields, append, remove } = useFieldArray({ + control: form.control, + name: "newFiles" + }) + + // (A) 기존 코멘트 렌더링 + function renderExistingComments() { + + if (isLoading) { + return ( +
+ + Loading comments... +
+ ) + } + + if (comments.length === 0) { + return

No comments yet

+ } + + return ( + + + + Comment + Attachments + Created At + Created By + + + + {comments.map((c) => ( + + {c.commentText} + + {/* 첨부파일 표시 */} + {!c.attachments?.length && ( + No files + )} + {c.attachments?.length && ( +
+ {c.attachments.map((att) => ( + + ))} +
+ )} +
+ { c.createdAt ? formatDate(c.createdAt): "-"} + + {c.commentedByEmail ?? "-"} + +
+ ))} +
+
+ ) + } + + // 2) 새 파일 Drop + function handleDropAccepted(files: File[]) { + append(files) + } + + + // 3) 저장(Submit) + async function onSubmit(data: CommentFormValues) { + + if (!rfqId) return + startTransition(async () => { + try { + // console.log("rfqId", rfqId) + // console.log("vendorId", vendorId) + // console.log("cbeId", cbeId) + // console.log("currentUserId", currentUserId) + const res = await createRfqCommentWithAttachments({ + rfqId: rfqId, + vendorId: vendorId, // 필요시 세팅 + commentText: data.commentText, + commentedBy: currentUserId, + evaluationId: null, // 필요시 세팅 + cbeId: cbeId, + files: data.newFiles + }) + + if (!res.ok) { + throw new Error("Failed to create comment") + } + + toast.success("Comment created") + + // 새 코멘트를 다시 불러오거나, + // 여기서는 임시로 "새로운 코멘트가 추가됐다" 라고 가정하여 클라이언트에서 상태 업데이트 + const newComment: CbeComment = { + id: res.commentId, // 서버에서 반환된 commentId + commentText: data.commentText, + commentedBy: currentUserId, + createdAt: res.createdAt, + attachments: (data.newFiles?.map((f, idx) => ({ + id: Math.random() * 100000, + fileName: f.name, + filePath: "/uploads/" + f.name, + })) || []) + } + setComments((prev) => [...prev, newComment]) + onCommentsUpdated?.([...comments, newComment]) + + // 폼 리셋 + form.reset() + } catch (err: any) { + console.error(err) + toast.error("Error: " + err.message) + } + }) + } + + return ( + + + + Comments + + 필요시 첨부파일과 함께 문의/코멘트를 남길 수 있습니다. + + + + {/* 기존 코멘트 목록 */} +
+ {renderExistingComments()} +
+ + {/* 새 코멘트 작성 Form */} +
+ + ( + + New Comment + +