summaryrefslogtreecommitdiff
path: root/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx')
-rw-r--r--lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx334
1 files changed, 334 insertions, 0 deletions
diff --git a/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx b/lib/vendor-rfq-response/vendor-tbe-table/comments-sheet.tsx
new file mode 100644
index 00000000..1eee54f5
--- /dev/null
+++ b/lib/vendor-rfq-response/vendor-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 { createRfqCommentWithAttachments } from "../../rfqs/service"
+import { formatDate } from "@/lib/utils"
+
+// 코멘트 + 첨부파일 구조 (단순 예시)
+// 실제 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<typeof Sheet> {
+ /** 코멘트를 작성할 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<typeof commentFormSchema>
+
+const MAX_FILE_SIZE = 30e6 // 30MB
+
+export function CommentSheet({
+ rfqId,
+ vendorId,
+ initialComments = [],
+ currentUserId,
+ onCommentsUpdated,
+ ...props
+}: CommentSheetProps) {
+ const [comments, setComments] = React.useState<TbeComment[]>(initialComments)
+ const [isPending, startTransition] = React.useTransition()
+
+ React.useEffect(() => {
+ setComments(initialComments)
+ }, [initialComments])
+
+
+ // RHF 세팅
+ const form = useForm<CommentFormValues>({
+ 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 <p className="text-sm text-muted-foreground">No comments yet</p>
+ }
+
+ return (
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-1/2">Comment</TableHead>
+ <TableHead>Attachments</TableHead>
+ <TableHead>Created At</TableHead>
+ <TableHead>Created By</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {comments.map((c) => (
+ <TableRow key={c.id}>
+ <TableCell>{c.commentText}</TableCell>
+ <TableCell>
+ {/* 첨부파일 표시 */}
+ {(!c.attachments || c.attachments.length === 0) && (
+ <span className="text-sm text-muted-foreground">No files</span>
+ )}
+ {c.attachments && c.attachments.length > 0 && (
+ <div className="flex flex-col gap-1">
+ {c.attachments.map((att) => (
+ <div key={att.id} className="flex items-center gap-2">
+ <a
+ href={att.filePath}
+ download
+ target="_blank"
+ rel="noreferrer"
+ className="inline-flex items-center gap-1 text-blue-600 underline"
+ >
+ <Download className="h-4 w-4" />
+ {att.fileName}
+ </a>
+ </div>
+ ))}
+ </div>
+ )}
+ </TableCell>
+ <TableCell> { c.createdAt ? formatDate(c.createdAt): "-"}</TableCell>
+ <TableCell>
+ {c.commentedBy ?? "-"}
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ )
+ }
+
+ // 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 (
+ <Sheet {...props}>
+ <SheetContent className="flex flex-col gap-6 sm:max-w-lg">
+ <SheetHeader className="text-left">
+ <SheetTitle>Comments</SheetTitle>
+ <SheetDescription>
+ 필요시 첨부파일과 함께 문의/코멘트를 남길 수 있습니다.
+ </SheetDescription>
+ </SheetHeader>
+
+ {/* 기존 코멘트 목록 */}
+ <div className="max-h-[300px] overflow-y-auto">
+ {renderExistingComments()}
+ </div>
+
+ {/* 새 코멘트 작성 Form */}
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
+ <FormField
+ control={form.control}
+ name="commentText"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>New Comment</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="Enter your comment..."
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* Dropzone (파일 첨부) */}
+ <Dropzone
+ maxSize={MAX_FILE_SIZE}
+ onDropAccepted={handleDropAccepted}
+ onDropRejected={(rej) => {
+ toast.error("File rejected: " + (rej[0]?.file?.name || ""))
+ }}
+ >
+ {({ maxSize }) => (
+ <DropzoneZone className="flex justify-center">
+ <DropzoneInput />
+ <div className="flex items-center gap-6">
+ <DropzoneUploadIcon />
+ <div className="grid gap-0.5">
+ <DropzoneTitle>Drop to attach files</DropzoneTitle>
+ <DropzoneDescription>
+ Max size: {prettyBytes(maxSize || 0)}
+ </DropzoneDescription>
+ </div>
+ </div>
+ </DropzoneZone>
+ )}
+ </Dropzone>
+
+ {/* 선택된 파일 목록 */}
+ {newFileFields.length > 0 && (
+ <div className="flex flex-col gap-2">
+ {newFileFields.map((field, idx) => {
+ const file = form.getValues(`newFiles.${idx}`)
+ if (!file) return null
+ return (
+ <div key={field.id} className="flex items-center justify-between border rounded p-2">
+ <span className="text-sm">{file.name} ({prettyBytes(file.size)})</span>
+ <Button
+ variant="ghost"
+ size="icon"
+ type="button"
+ onClick={() => remove(idx)}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ )
+ })}
+ </div>
+ )}
+
+ <SheetFooter className="gap-2 pt-4">
+ <SheetClose asChild>
+ <Button type="button" variant="outline">
+ Cancel
+ </Button>
+ </SheetClose>
+ <Button disabled={isPending}>
+ {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />}
+ Save
+ </Button>
+ </SheetFooter>
+ </form>
+ </Form>
+ </SheetContent>
+ </Sheet>
+ )
+} \ No newline at end of file