summaryrefslogtreecommitdiff
path: root/components/documents/add-document-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/documents/add-document-dialog.tsx')
-rw-r--r--components/documents/add-document-dialog.tsx515
1 files changed, 515 insertions, 0 deletions
diff --git a/components/documents/add-document-dialog.tsx b/components/documents/add-document-dialog.tsx
new file mode 100644
index 00000000..15c1e021
--- /dev/null
+++ b/components/documents/add-document-dialog.tsx
@@ -0,0 +1,515 @@
+"use client"
+
+import * as React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { z } from "zod"
+
+import {
+ Dialog, DialogTrigger, DialogContent, DialogHeader,
+ DialogTitle, DialogDescription, DialogFooter
+} from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import {
+ Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription
+} from "@/components/ui/form"
+import {
+ Select, SelectContent, SelectGroup,
+ SelectItem, SelectTrigger, SelectValue
+} from "@/components/ui/select"
+import { Textarea } from "@/components/ui/textarea"
+import { createRevisionAction } from "@/lib/vendor-document/service"
+import { useToast } from "@/hooks/use-toast"
+import { FilePlus, X, Loader2, Building2, User, Building, Plus } from "lucide-react"
+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,
+ FileListSize,
+} from "@/components/ui/file-list"
+import prettyBytes from "pretty-bytes"
+import { ScrollArea } from "../ui/scroll-area"
+
+// 최대 파일 크기 설정 (3000MB)
+const MAX_FILE_SIZE = 3e9
+
+// zod 스키마
+export const createDocumentVersionSchema = z.object({
+ attachments: z.array(z.instanceof(File)).min(1, "At least one file is required"),
+ stage: z.string().min(1, "Stage is required"),
+ revision: z.string().min(1, "Revision is required"),
+ uploaderType: z.enum(["vendor", "client", "shi"]).default("vendor"),
+ uploaderName: z.string().optional(),
+ comment: z.string().optional(),
+})
+export type CreateDocumentVersionSchema = z.infer<typeof createDocumentVersionSchema>
+
+// First, let's add a function to generate filenames based on your pattern
+const generateFileName = (
+ documentNo: string,
+ stage: string,
+ revision: string,
+ originalFileName: string,
+ index: number,
+ totalFiles: number
+) => {
+ // Get the file extension
+ const extension = originalFileName.split('.').pop() || '';
+
+ // Base name without extension
+ const baseName = `${documentNo}_${stage}_${revision}`;
+
+ // For multiple files, add a suffix
+ if (totalFiles > 1) {
+ return `${baseName}_${index + 1}.${extension}`;
+ }
+
+ // For a single file, no suffix needed
+ return `${baseName}.${extension}`;
+};
+
+// AddDocumentDialog Props
+interface AddDocumentDialogProps {
+ onSuccess?: () => void
+ stageOptions?: string[]
+ documentId: number
+ documentNo: string
+ // 업로더 타입
+ uploaderType?: "vendor" | "client" | "shi"
+ // 버튼 라벨 추가
+ buttonLabel?: string
+}
+
+export function AddDocumentDialog({
+ onSuccess,
+ stageOptions = [],
+ documentId,
+ documentNo,
+ uploaderType = "vendor",
+ buttonLabel = "Add Document",
+}: AddDocumentDialogProps) {
+ const [open, setOpen] = React.useState(false)
+ const [selectedFiles, setSelectedFiles] = React.useState<File[]>([])
+ const [isUploading, setIsUploading] = React.useState(false)
+ const [uploadProgress, setUploadProgress] = React.useState(0)
+ const { toast } = useToast()
+
+ const form = useForm<CreateDocumentVersionSchema>({
+ resolver: zodResolver(createDocumentVersionSchema),
+ defaultValues: {
+ stage: "",
+ revision: "",
+ attachments: [],
+ uploaderType,
+ uploaderName: "",
+ comment: "",
+ },
+ })
+
+ // 업로더 타입이 바뀌면 폼 값도 업데이트
+ React.useEffect(() => {
+ form.setValue('uploaderType', uploaderType);
+ }, [uploaderType, form]);
+
+ // 드롭존 - 파일 드랍 처리
+ const handleDropAccepted = (acceptedFiles: File[]) => {
+ const newFiles = [...selectedFiles, ...acceptedFiles];
+ setSelectedFiles(newFiles);
+ form.setValue('attachments', newFiles, { shouldValidate: true });
+ };
+
+ // 드롭존 - 파일 거부(에러) 처리
+ const handleDropRejected = (fileRejections: any[]) => {
+ fileRejections.forEach((rejection) => {
+ toast({
+ variant: "destructive",
+ title: "File Error",
+ description: `${rejection.file.name}: ${rejection.errors[0]?.message || "Upload failed"}`,
+ });
+ });
+ };
+
+ // 파일 제거 핸들러
+ const removeFile = (index: number) => {
+ const updatedFiles = [...selectedFiles]
+ updatedFiles.splice(index, 1)
+ setSelectedFiles(updatedFiles)
+ form.setValue('attachments', updatedFiles, { shouldValidate: true })
+ }
+
+ // Submit
+ async function onSubmit(data: CreateDocumentVersionSchema) {
+ setIsUploading(true)
+ setUploadProgress(0)
+
+ try {
+ // 각 파일별로 별도 요청
+ const totalFiles = data.attachments.length;
+ let successCount = 0;
+
+ for (let i = 0; i < totalFiles; i++) {
+ const file = data.attachments[i];
+
+ const newFileName = generateFileName(
+ documentNo,
+ data.stage,
+ data.revision,
+ file.name,
+ i,
+ totalFiles
+ );
+
+ const fData = new FormData();
+ fData.append("documentId", String(documentId));
+ fData.append("stage", data.stage);
+ fData.append("revision", data.revision);
+ fData.append("uploaderType", data.uploaderType);
+
+ if (data.uploaderName) {
+ fData.append("uploaderName", data.uploaderName);
+ }
+
+ if (data.comment) {
+ fData.append("comment", data.comment);
+ }
+
+ fData.append("attachment", file);
+ fData.append("customFileName", newFileName);
+
+ // 각 파일 업로드를 요청
+ await createRevisionAction(fData);
+
+ // 진행 상황 업데이트
+ successCount++;
+ setUploadProgress(Math.round((successCount / totalFiles) * 100));
+ }
+
+ // 성공 메시지
+ toast({
+ title: "Success",
+ description: `${successCount} 파일이 업로드되었습니다.`,
+ });
+
+ // 폼 초기화
+ form.reset();
+ setSelectedFiles([]);
+ setOpen(false);
+
+ // 콜백 실행
+ if (onSuccess) {
+ onSuccess();
+ }
+ } catch (err) {
+ console.error(err);
+ toast({
+ title: "Error",
+ description: "파일 업로드 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsUploading(false);
+ setUploadProgress(0);
+ }
+ }
+
+ function handleDialogOpenChange(nextOpen: boolean) {
+ if (!nextOpen) {
+ form.reset();
+ form.setValue('uploaderType', uploaderType);
+ setSelectedFiles([]);
+ setIsUploading(false);
+ setUploadProgress(0);
+ }
+ setOpen(nextOpen);
+ }
+
+ // 업로더 타입에 따른 UI 설정
+ const uploaderConfig = {
+ vendor: {
+ icon: <Plus className="h-4 w-4" />,
+ buttonStyle: "bg-blue-600 hover:bg-blue-700",
+ badgeStyle: "bg-blue-100 text-blue-800",
+ title: "업체 문서 등록",
+ nameLabel: "업체 이름",
+ namePlaceholder: "예: 홍길동(ABC 업체)"
+ },
+ client: {
+ icon: <User className="mr-2 h-4 w-4" />,
+ buttonStyle: "bg-amber-600 hover:bg-amber-700",
+ badgeStyle: "bg-amber-100 text-amber-800",
+ title: "고객사 문서 등록",
+ nameLabel: "담당자 이름",
+ namePlaceholder: "예: 김철수(고객사)"
+ },
+ shi: {
+ icon: <Building className="mr-2 h-4 w-4" />,
+ buttonStyle: "bg-purple-600 hover:bg-purple-700",
+ badgeStyle: "bg-purple-100 text-purple-800",
+ title: "삼성중공업 문서 등록",
+ nameLabel: "담당자 이름",
+ namePlaceholder: "예: 이영희(삼성중공업)"
+ }
+ }
+
+ const config = uploaderConfig[uploaderType as keyof typeof uploaderConfig];
+
+ return (
+ <Dialog open={open} onOpenChange={handleDialogOpenChange}>
+ <DialogTrigger asChild>
+ <Button
+ size="sm"
+ className="border-blue-200"
+ variant="outline"
+ >
+ {config.icon}
+ {buttonLabel}
+ </Button>
+ </DialogTrigger>
+
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle>
+ {config.title}
+ </DialogTitle>
+ <DialogDescription>
+ 스테이지/리비전을 입력하고 파일을 업로드하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} encType="multipart/form-data">
+ <div className="space-y-4 py-4">
+ {/* stage */}
+ <FormField
+ control={form.control}
+ name="stage"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Stage</FormLabel>
+ <FormControl>
+ {stageOptions.length > 0 ? (
+ <Select
+ onValueChange={field.onChange}
+ defaultValue={field.value}
+ >
+ <SelectTrigger>
+ <SelectValue placeholder="Select a stage" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectGroup>
+ {stageOptions.map((st) => (
+ <SelectItem key={st} value={st}>
+ {st}
+ </SelectItem>
+ ))}
+ </SelectGroup>
+ </SelectContent>
+ </Select>
+ ) : (
+ <Input {...field} placeholder="예: Issued for Review" />
+ )}
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* revision */}
+ <FormField
+ control={form.control}
+ name="revision"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Revision</FormLabel>
+ <FormControl>
+ <Input {...field} placeholder="예: A, B, 1, 2..." />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* attachments - 드롭존 */}
+ <FormField
+ control={form.control}
+ name="attachments"
+ render={() => (
+ <FormItem>
+ <FormLabel>파일 첨부</FormLabel>
+ <Dropzone
+ maxSize={MAX_FILE_SIZE}
+ multiple={true}
+ onDropAccepted={handleDropAccepted}
+ onDropRejected={handleDropRejected}
+ disabled={isUploading}
+ >
+ {({ maxSize }) => (
+ <>
+ <DropzoneZone className="flex justify-center">
+ <FormControl>
+ <DropzoneInput />
+ </FormControl>
+ <div className="flex items-center gap-6">
+ <DropzoneUploadIcon />
+ <div className="grid gap-0.5">
+ <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
+ <DropzoneDescription>
+ 또는 클릭하여 파일을 선택하세요.
+ 최대 크기: {maxSize ? prettyBytes(maxSize) : "무제한"}
+ </DropzoneDescription>
+ </div>
+ </div>
+ </DropzoneZone>
+ <FormDescription className="text-xs text-muted-foreground">
+ 여러 파일을 선택할 수 있습니다.
+ </FormDescription>
+ </>
+ )}
+ </Dropzone>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 선택된 파일 목록 */}
+
+ {selectedFiles.length > 0 && (
+ <div className="grid gap-2">
+ <div className="flex items-center justify-between">
+ <h6 className="text-sm font-semibold">
+ 선택된 파일 ({selectedFiles.length})
+ </h6>
+ <Badge variant="secondary">
+ {selectedFiles.length}개 파일
+ </Badge>
+ </div>
+ <ScrollArea>
+ <FileList className="max-h-[200px] gap-3">
+ {selectedFiles.map((file, index) => (
+ <FileListItem key={index} className="p-3">
+ <FileListHeader>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>{file.name}</FileListName>
+ <FileListDescription>
+ {prettyBytes(file.size)}
+ </FileListDescription>
+ </FileListInfo>
+ <FileListAction onClick={() => removeFile(index)} disabled={isUploading}>
+ <X className="h-4 w-4" />
+ <span className="sr-only">Remove</span>
+ </FileListAction>
+ </FileListHeader>
+ </FileListItem>
+ ))}
+ </FileList>
+ </ScrollArea>
+
+ </div>
+ )}
+
+ {/* 업로드 진행 상태 */}
+ {isUploading && (
+ <div className="flex flex-col gap-1 mt-2">
+ <div className="flex items-center gap-2">
+ <Loader2 className="h-4 w-4 animate-spin" />
+ <span className="text-sm">
+ {uploadProgress}% 업로드 중...
+ </span>
+ </div>
+ <div className="h-2 w-full bg-muted rounded-full overflow-hidden">
+ <div
+ className="h-full bg-primary rounded-full transition-all"
+ style={{ width: `${uploadProgress}%` }}
+ />
+ </div>
+ </div>
+ )}
+
+ {/* 선택적 필드들 */}
+ {/* <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="uploaderName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{config.nameLabel} (선택)</FormLabel>
+ <FormControl>
+ <Input
+ {...field}
+ placeholder={config.namePlaceholder}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div> */}
+
+ {/* comment (optional) */}
+ {/* <FormField
+ control={form.control}
+ name="comment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>코멘트 (선택)</FormLabel>
+ <FormControl>
+ <Textarea
+ {...field}
+ placeholder="파일에 대한 설명이나 코멘트를 입력하세요."
+ className="resize-none"
+ rows={2}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ /> */}
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => {
+ // 파일 리스트 리셋
+ setSelectedFiles([]);
+ form.reset({
+ ...form.getValues(),
+ attachments: []
+ });
+ setOpen(false);
+ }}
+ disabled={isUploading}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isUploading || selectedFiles.length === 0 || !form.formState.isValid}
+ className={config.buttonStyle}
+ >
+ {isUploading ? "업로드 중..." : "등록"}
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file