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 --- .../add-basic-contract-template-dialog.tsx | 359 +++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 lib/basic-contract/template/add-basic-contract-template-dialog.tsx (limited to 'lib/basic-contract/template/add-basic-contract-template-dialog.tsx') diff --git a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx new file mode 100644 index 00000000..cf0986f0 --- /dev/null +++ b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx @@ -0,0 +1,359 @@ +"use client"; + +import * as React from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { toast } from "sonner"; +import { v4 as uuidv4 } from 'uuid'; +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dropzone, + DropzoneZone, + DropzoneUploadIcon, + DropzoneTitle, + DropzoneDescription, + DropzoneInput +} from "@/components/ui/dropzone"; +import { Progress } from "@/components/ui/progress"; +import { useRouter } from "next/navigation" + +// 유효기간 필드가 추가된 계약서 템플릿 스키마 정의 +const templateFormSchema = z.object({ + templateName: z.string().min(1, "템플릿 이름은 필수입니다."), + validityPeriod: z.coerce + .number({ invalid_type_error: "유효기간은 숫자여야 합니다." }) + .int("유효기간은 정수여야 합니다.") + .min(1, "유효기간은 최소 1개월 이상이어야 합니다.") + .max(120, "유효기간은 최대 120개월(10년)을 초과할 수 없습니다.") + .default(12), + file: z + .instanceof(File, { message: "파일을 업로드해주세요." }) + .refine((file) => file.size <= 100 * 1024 * 1024, { + message: "파일 크기는 100MB 이하여야 합니다.", + }) + .refine( + (file) => file.type === 'application/pdf', + { message: "PDF 파일만 업로드 가능합니다." } + ), + status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"), +}); + +type TemplateFormValues = z.infer; + +export function AddTemplateDialog() { + const [open, setOpen] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); + const [selectedFile, setSelectedFile] = React.useState(null); + const [uploadProgress, setUploadProgress] = React.useState(0); + const [showProgress, setShowProgress] = React.useState(false); + const router = useRouter() + + // 기본값 설정 + const defaultValues: Partial = { + templateName: "", + validityPeriod: 12, // 기본값 1년 + status: "ACTIVE", + }; + + // 폼 초기화 + const form = useForm({ + resolver: zodResolver(templateFormSchema), + defaultValues, + mode: "onChange", + }); + + // 폼 값 감시 + const templateName = form.watch("templateName"); + const validityPeriod = form.watch("validityPeriod"); + const file = form.watch("file"); + + // 파일 선택 핸들러 + const handleFileChange = (files: File[]) => { + if (files.length > 0) { + const file = files[0]; + setSelectedFile(file); + form.setValue("file", file); + } + }; + + // 청크 크기 설정 (1MB) + const CHUNK_SIZE = 1 * 1024 * 1024; + + // 파일을 청크로 분할하여 업로드하는 함수 + const uploadFileInChunks = async (file: File, fileId: string) => { + const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + setShowProgress(true); + setUploadProgress(0); + + for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { + const start = chunkIndex * CHUNK_SIZE; + const end = Math.min(start + CHUNK_SIZE, file.size); + const chunk = file.slice(start, end); + + const formData = new FormData(); + formData.append('chunk', chunk); + formData.append('filename', file.name); + formData.append('chunkIndex', chunkIndex.toString()); + formData.append('totalChunks', totalChunks.toString()); + formData.append('fileId', fileId); + + try { + const response = await fetch('/api/upload/basicContract/chunk', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`청크 업로드 실패: ${response.statusText}`); + } + + // 진행률 업데이트 + const progress = Math.round(((chunkIndex + 1) / totalChunks) * 100); + setUploadProgress(progress); + + const result = await response.json(); + + // 마지막 청크인 경우 파일 경로 반환 + if (chunkIndex === totalChunks - 1) { + return result; + } + } catch (error) { + console.error(`청크 ${chunkIndex} 업로드 오류:`, error); + throw error; + } + } + }; + + // 폼 제출 핸들러 + async function onSubmit(formData: TemplateFormValues) { + setIsLoading(true); + try { + if (!formData.file) { + throw new Error("파일이 선택되지 않았습니다."); + } + + // 고유 파일 ID 생성 + const fileId = uuidv4(); + + // 파일 청크 업로드 + const uploadResult = await uploadFileInChunks(formData.file, fileId); + + if (!uploadResult.success) { + throw new Error("파일 업로드에 실패했습니다."); + } + + // 메타데이터 저장 + const saveResponse = await fetch('/api/upload/basicContract/complete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + templateName: formData.templateName, + validityPeriod: formData.validityPeriod, // 유효기간 추가 + status: formData.status, + fileName: uploadResult.fileName, + filePath: uploadResult.filePath, + }), + next: { tags: ["basic-contract-templates"] }, + }); + + const saveResult = await saveResponse.json(); + + if (!saveResult.success) { + throw new Error("템플릿 정보 저장에 실패했습니다."); + } + + toast.success('템플릿이 성공적으로 추가되었습니다.'); + form.reset(); + setSelectedFile(null); + setOpen(false); + setShowProgress(false); + + router.refresh(); + } catch (error) { + console.error("Submit error:", error); + toast.error("템플릿 추가 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + } + + // 모달이 닫힐 때 폼 초기화 + React.useEffect(() => { + if (!open) { + form.reset(); + setSelectedFile(null); + setShowProgress(false); + setUploadProgress(0); + } + }, [open, form]); + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset(); + } + setOpen(nextOpen); + } + + // 유효기간 선택 옵션 + const validityOptions = [ + { value: "3", label: "3개월" }, + { value: "6", label: "6개월" }, + { value: "12", label: "1년" }, + { value: "24", label: "2년" }, + { value: "36", label: "3년" }, + { value: "60", label: "5년" }, + ]; + + return ( + + + + + + + 새 기본계약서 템플릿 추가 + + 템플릿 이름을 입력하고 계약서 파일을 업로드하세요. + * 표시된 항목은 필수 입력사항입니다. + + +
+ + ( + + + 템플릿 이름 * + + + + + + + )} + /> + + ( + + + 계약 유효기간 * + + + + 계약서의 유효 기간을 설정합니다. 이 기간이 지나면 재계약이 필요합니다. + + + + )} + /> + + ( + + + 계약서 파일 * + + + + + + + {selectedFile ? selectedFile.name : "PDF 파일을 여기에 드래그하세요"} + + + {selectedFile + ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` + : "또는 클릭하여 PDF 파일을 선택하세요 (최대 100MB)"} + + + + + + + + )} + /> + + {showProgress && ( +
+
+ 업로드 진행률 + {uploadProgress}% +
+ +
+ )} + + + + + + + +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3