From 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 25 Mar 2025 15:55:45 +0900 Subject: initial commit --- lib/tbe/service.ts | 0 lib/tbe/table/comments-sheet.tsx | 334 ++++++++++++++++++++++++++++ lib/tbe/table/feature-flags-provider.tsx | 108 +++++++++ lib/tbe/table/file-dialog.tsx | 141 ++++++++++++ lib/tbe/table/invite-vendors-dialog.tsx | 203 +++++++++++++++++ lib/tbe/table/tbe-table-columns.tsx | 249 +++++++++++++++++++++ lib/tbe/table/tbe-table-toolbar-actions.tsx | 60 +++++ lib/tbe/table/tbe-table.tsx | 204 +++++++++++++++++ 8 files changed, 1299 insertions(+) create mode 100644 lib/tbe/service.ts create mode 100644 lib/tbe/table/comments-sheet.tsx create mode 100644 lib/tbe/table/feature-flags-provider.tsx create mode 100644 lib/tbe/table/file-dialog.tsx create mode 100644 lib/tbe/table/invite-vendors-dialog.tsx create mode 100644 lib/tbe/table/tbe-table-columns.tsx create mode 100644 lib/tbe/table/tbe-table-toolbar-actions.tsx create mode 100644 lib/tbe/table/tbe-table.tsx (limited to 'lib/tbe') diff --git a/lib/tbe/service.ts b/lib/tbe/service.ts new file mode 100644 index 00000000..e69de29b diff --git a/lib/tbe/table/comments-sheet.tsx b/lib/tbe/table/comments-sheet.tsx new file mode 100644 index 00000000..7fcde35d --- /dev/null +++ b/lib/tbe/table/comments-sheet.tsx @@ -0,0 +1,334 @@ +"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 } 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 { RfqWithAll } from "@/db/schema/rfq" +import { formatDate } from "@/lib/utils" +import { createRfqCommentWithAttachments } from "@/lib/rfqs/service" + +// 코멘트 + 첨부파일 구조 (단순 예시) +// 실제 DB 스키마에 맞춰 조정 +export interface TbeComment { + id: number + commentText: string + commentedBy?: number + createdAt?: string | Date + attachments?: { + id: number + fileName: string + filePath: string + }[] +} + +interface CommentSheetProps extends React.ComponentPropsWithRef { + /** 코멘트를 작성할 RFQ 정보 */ + /** 이미 존재하는 모든 코멘트 목록 (서버에서 불러와 주입) */ + initialComments?: TbeComment[] + + /** 사용자(작성자) ID (로그인 세션 등에서 가져옴) */ + currentUserId: number + rfqId:number + vendorId:number + /** 댓글 저장 후 갱신용 콜백 (옵션) */ + onCommentsUpdated?: (comments: TbeComment[]) => void +} + +// 새 코멘트 작성 폼 스키마 +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, + onCommentsUpdated, + ...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" + }) + + // 1) 기존 코멘트 + 첨부 보여주기 + // 간단히 테이블 하나로 표현 + // 실제로는 Bubble 형태의 UI, Accordion, Timeline 등 다양하게 구성할 수 있음 + function renderExistingComments() { + if (comments.length === 0) { + return

No comments yet

+ } + + return ( + + + + Comment + Attachments + Created At + Created By + + + + {comments.map((c) => ( + + {c.commentText} + + {/* 첨부파일 표시 */} + {(!c.attachments || c.attachments.length === 0) && ( + No files + )} + {c.attachments && c.attachments.length > 0 && ( +
+ {c.attachments.map((att) => ( + + ))} +
+ )} +
+ { c.createdAt ? formatDate(c.createdAt): "-"} + + {c.commentedBy ?? "-"} + +
+ ))} +
+
+ ) + } + + // 2) 새 파일 Drop + function handleDropAccepted(files: File[]) { + // 드롭된 File[]을 RHF field array에 추가 + const toAppend = files.map((f) => f) + append(toAppend) + } + + + // 3) 저장(Submit) + async function onSubmit(data: CommentFormValues) { + + if (!rfqId) return + startTransition(async () => { + try { + // 서버 액션 호출 + const res = await createRfqCommentWithAttachments({ + rfqId: rfqId, + vendorId: vendorId, // 필요시 세팅 + commentText: data.commentText, + commentedBy: currentUserId, + evaluationId: null, // 필요시 세팅 + files: data.newFiles + }) + + if (!res.ok) { + throw new Error("Failed to create comment") + } + + toast.success("Comment created") + + // 새 코멘트를 다시 불러오거나, + // 여기서는 임시로 "새로운 코멘트가 추가됐다" 라고 가정하여 클라이언트에서 상태 업데이트 + const newComment: TbeComment = { + id: res.commentId, // 서버에서 반환된 commentId + commentText: data.commentText, + commentedBy: currentUserId, + createdAt: new Date().toISOString(), + 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 + +