diff options
Diffstat (limited to 'lib/rfqs/table/attachment-rfq-sheet.tsx')
| -rw-r--r-- | lib/rfqs/table/attachment-rfq-sheet.tsx | 429 |
1 files changed, 0 insertions, 429 deletions
diff --git a/lib/rfqs/table/attachment-rfq-sheet.tsx b/lib/rfqs/table/attachment-rfq-sheet.tsx deleted file mode 100644 index fdfb5e9a..00000000 --- a/lib/rfqs/table/attachment-rfq-sheet.tsx +++ /dev/null @@ -1,429 +0,0 @@ -"use client" - -import * as React from "react" -import { z } from "zod" -import { useForm, useFieldArray } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" - -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetDescription, - SheetFooter, - SheetClose, -} from "@/components/ui/sheet" -import { Button } from "@/components/ui/button" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - FormDescription -} from "@/components/ui/form" -import { Loader, Download, X, Eye, AlertCircle } from "lucide-react" -import { useToast } from "@/hooks/use-toast" -import { Badge } from "@/components/ui/badge" - -import { - Dropzone, - DropzoneDescription, - DropzoneInput, - DropzoneTitle, - DropzoneUploadIcon, - DropzoneZone, -} from "@/components/ui/dropzone" -import { - FileList, - FileListAction, - FileListDescription, - FileListHeader, - FileListIcon, - FileListInfo, - FileListItem, - FileListName, -} from "@/components/ui/file-list" - -import prettyBytes from "pretty-bytes" -import { processRfqAttachments } from "../service" -import { formatDate } from "@/lib/utils" -import { RfqType } from "../validations" -import { RfqWithItemCount } from "@/db/schema/rfq" -import { quickDownload } from "@/lib/file-download" -import { type FileRejection } from "react-dropzone" - -const MAX_FILE_SIZE = 6e8 // 600MB - -/** 기존 첨부 파일 정보 */ -interface ExistingAttachment { - id: number - fileName: string - filePath: string - createdAt?: Date // or Date - vendorId?: number | null - size?: number -} - -/** 새로 업로드할 파일 */ -const newUploadSchema = z.object({ - fileObj: z.any().optional(), // 실제 File -}) - -/** 기존 첨부 (react-hook-form에서 관리) */ -const existingAttachSchema = z.object({ - id: z.number(), - fileName: z.string(), - filePath: z.string(), - vendorId: z.number().nullable().optional(), - createdAt: z.custom<Date>().optional(), // or use z.any().optional() - size: z.number().optional(), -}) - -/** RHF 폼 전체 스키마 */ -const attachmentsFormSchema = z.object({ - rfqId: z.number().int(), - existing: z.array(existingAttachSchema), - newUploads: z.array(newUploadSchema), -}) - -type AttachmentsFormValues = z.infer<typeof attachmentsFormSchema> - -interface RfqAttachmentsSheetProps - extends React.ComponentPropsWithRef<typeof Sheet> { - defaultAttachments?: ExistingAttachment[] - rfqType?: RfqType - rfq: RfqWithItemCount | null - /** 업로드/삭제 후 상위 테이블에 itemCount 등을 업데이트하기 위한 콜백 */ - onAttachmentsUpdated?: (rfqId: number, newItemCount: number) => void -} - -/** - * RfqAttachmentsSheet: - * - 기존 첨부 목록 (다운로드 + 삭제) - * - 새 파일 Dropzone - * - Save 시 processRfqAttachments(server action) - */ -export function RfqAttachmentsSheet({ - defaultAttachments = [], - onAttachmentsUpdated, - rfq, - rfqType, - ...props -}: RfqAttachmentsSheetProps) { - const { toast } = useToast() - const [isPending, startUpdate] = React.useTransition() - const rfqId = rfq?.rfqId ?? 0; - - // 편집 가능 여부 확인 - DRAFT 상태일 때만 편집 가능 - const isEditable = rfq?.status === "DRAFT"; - - // React Hook Form - const form = useForm<AttachmentsFormValues>({ - resolver: zodResolver(attachmentsFormSchema), - defaultValues: { - rfqId, - existing: [], - newUploads: [], - }, - }) - - const { reset, control, handleSubmit } = form - - // defaultAttachments가 바뀔 때마다, RHF 상태를 reset - React.useEffect(() => { - reset({ - rfqId, - existing: defaultAttachments.map((att) => ({ - ...att, - vendorId: att.vendorId ?? null, - size: att.size ?? undefined, - })), - newUploads: [], - }) - }, [rfqId, defaultAttachments, reset]) - - // Field Arrays - const { - fields: existingFields, - remove: removeExisting, - } = useFieldArray({ control, name: "existing" }) - - const { - fields: newUploadFields, - append: appendNewUpload, - remove: removeNewUpload, - } = useFieldArray({ control, name: "newUploads" }) - - // 기존 첨부 항목 중 삭제된 것 찾기 - function findRemovedExistingIds(data: AttachmentsFormValues): number[] { - const finalIds = data.existing.map((att) => att.id) - const originalIds = defaultAttachments.map((att) => att.id) - return originalIds.filter((id) => !finalIds.includes(id)) - } - - async function onSubmit(data: AttachmentsFormValues) { - // 편집 불가능한 상태에서는 제출 방지 - if (!isEditable) return; - - startUpdate(async () => { - try { - const removedExistingIds = findRemovedExistingIds(data) - const newFiles = data.newUploads - .map((it) => it.fileObj) - .filter((f): f is File => !!f) - - // 서버 액션 - const res = await processRfqAttachments({ - rfqId, - removedExistingIds, - newFiles, - vendorId: null, // vendor ID if needed - rfqType - }) - - if (!res.ok) throw new Error(res.error ?? "Unknown error") - - const newCount = res.updatedItemCount ?? 0 - - toast({ - variant: "default", - title: "Success", - description: "File(s) updated", - }) - - // 상위 테이블 등에 itemCount 업데이트 - onAttachmentsUpdated?.(rfqId, newCount) - - // 모달 닫기 - props.onOpenChange?.(false) - } catch (err) { - toast({ - variant: "destructive", - title: "Error", - description: String(err), - }) - } - }) - } - - /** 기존 첨부 - X 버튼 */ - function handleRemoveExisting(idx: number) { - // 편집 불가능한 상태에서는 삭제 방지 - if (!isEditable) return; - removeExisting(idx) - } - - /** 드롭존에서 파일 받기 */ - function handleDropAccepted(acceptedFiles: File[]) { - // 편집 불가능한 상태에서는 파일 추가 방지 - if (!isEditable) return; - const mapped = acceptedFiles.map((file) => ({ fileObj: file })) - appendNewUpload(mapped) - } - - /** 드롭존에서 파일 거부(에러) */ - function handleDropRejected(fileRejections: FileRejection[]) { - // 편집 불가능한 상태에서는 무시 - if (!isEditable) return; - - fileRejections.forEach((rej) => { - toast({ - variant: "destructive", - title: "File Error", - description: rej.file.name + " not accepted", - }) - }) - } - - return ( - <Sheet {...props}> - <SheetContent className="flex flex-col gap-6 sm:max-w-sm"> - <SheetHeader> - <SheetTitle className="flex items-center gap-2"> - {isEditable ? "Manage Attachments" : "View Attachments"} - {rfq?.status && ( - <Badge - variant={rfq.status === "DRAFT" ? "outline" : "secondary"} - className="ml-1" - > - {rfq.status} - </Badge> - )} - </SheetTitle> - <SheetDescription> - {`RFQ ${rfq?.rfqCode} - `} - {isEditable ? '파일 첨부/삭제' : '첨부 파일 보기'} - {!isEditable && ( - <div className="mt-1 text-xs flex items-center gap-1 text-amber-600"> - <AlertCircle className="h-3 w-3" /> - <span>드래프트 상태가 아닌 RFQ는 첨부파일을 수정할 수 없습니다.</span> - </div> - )} - </SheetDescription> - </SheetHeader> - - <Form {...form}> - <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4"> - {/* 1) 기존 첨부 목록 */} - <div className="space-y-2"> - <p className="font-semibold text-sm">Existing Attachments</p> - {existingFields.length === 0 && ( - <p className="text-sm text-muted-foreground">No existing attachments</p> - )} - {existingFields.map((field, index) => { - const vendorLabel = field.vendorId ? "(Vendor)" : "(Internal)" - return ( - <div - key={field.id} - className="flex items-center justify-between rounded border p-2" - > - <div className="flex flex-col text-sm"> - <span className="font-medium"> - {field.fileName} {vendorLabel} - </span> - {field.size && ( - <span className="text-xs text-muted-foreground"> - {Math.round(field.size / 1024)} KB - </span> - )} - {field.createdAt && ( - <span className="text-xs text-muted-foreground"> - Created at {formatDate(field.createdAt, "KR")} - </span> - )} - </div> - <div className="flex items-center gap-2"> - {/* 1) Download button (if filePath) */} - {field.filePath && ( - <Button - variant="ghost" - size="icon" - type="button" - onClick={() => quickDownload(field.filePath, field.fileName)} - > - <Download className="h-4 w-4" /> - </Button> - )} - {/* 2) Remove button - 편집 가능할 때만 표시 */} - {isEditable && ( - <Button - type="button" - variant="ghost" - size="icon" - onClick={() => handleRemoveExisting(index)} - > - <X className="h-4 w-4" /> - </Button> - )} - </div> - </div> - ) - })} - </div> - - {/* 2) Dropzone for new uploads - 편집 가능할 때만 표시 */} - {isEditable ? ( - <> - <Dropzone - maxSize={MAX_FILE_SIZE} - onDropAccepted={handleDropAccepted} - onDropRejected={handleDropRejected} - > - {({ maxSize }) => ( - <FormField - control={control} - name="newUploads" // not actually used for storing each file detail - render={() => ( - <FormItem> - <FormLabel>Drop Files Here</FormLabel> - <DropzoneZone className="flex justify-center"> - <FormControl> - <DropzoneInput /> - </FormControl> - <div className="flex items-center gap-6"> - <DropzoneUploadIcon /> - <div className="grid gap-0.5"> - <DropzoneTitle>Drop to upload</DropzoneTitle> - <DropzoneDescription> - Max size: {maxSize ? prettyBytes(maxSize) : "??? MB"} - </DropzoneDescription> - </div> - </div> - </DropzoneZone> - <FormDescription>Alternatively, click browse.</FormDescription> - <FormMessage /> - </FormItem> - )} - /> - )} - </Dropzone> - - {/* newUpload fields -> FileList */} - {newUploadFields.length > 0 && ( - <div className="grid gap-4"> - <h6 className="font-semibold leading-none tracking-tight"> - {`Files (${newUploadFields.length})`} - </h6> - <FileList> - {newUploadFields.map((field, idx) => { - const fileObj = form.getValues(`newUploads.${idx}.fileObj`) - if (!fileObj) return null - - const fileName = fileObj.name - const fileSize = fileObj.size - return ( - <FileListItem key={field.id}> - <FileListHeader> - <FileListIcon /> - <FileListInfo> - <FileListName>{fileName}</FileListName> - <FileListDescription> - {`${prettyBytes(fileSize)}`} - </FileListDescription> - </FileListInfo> - <FileListAction onClick={() => removeNewUpload(idx)}> - <X /> - <span className="sr-only">Remove</span> - </FileListAction> - </FileListHeader> - </FileListItem> - ) - })} - </FileList> - </div> - )} - </> - ) : ( - <div className="p-3 bg-muted rounded-md flex items-center justify-center"> - <div className="text-center text-sm text-muted-foreground"> - <Eye className="h-4 w-4 mx-auto mb-2" /> - <p>보기 모드에서는 파일 첨부를 할 수 없습니다.</p> - </div> - </div> - )} - - <SheetFooter className="gap-2 pt-2 sm:space-x-0"> - <SheetClose asChild> - <Button type="button" variant="outline"> - {isEditable ? "Cancel" : "Close"} - </Button> - </SheetClose> - {isEditable && ( - <Button - type="submit" - disabled={isPending || (form.getValues().newUploads.length === 0 && defaultAttachments.length === form.getValues().existing.length)} - > - {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />} - Save - </Button> - )} - </SheetFooter> - </form> - </Form> - </SheetContent> - </Sheet> - ) -}
\ No newline at end of file |
