From ef4c533ebacc2cdc97e518f30e9a9350004fcdfb Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<typeof templateFormSchema>;
+
+export function AddTemplateDialog() {
+  const [open, setOpen] = React.useState(false);
+  const [isLoading, setIsLoading] = React.useState(false);
+  const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
+  const [uploadProgress, setUploadProgress] = React.useState(0);
+  const [showProgress, setShowProgress] = React.useState(false);
+  const router = useRouter()
+
+  // 기본값 설정
+  const defaultValues: Partial<TemplateFormValues> = {
+    templateName: "",
+    validityPeriod: 12, // 기본값 1년
+    status: "ACTIVE",
+  };
+
+  // 폼 초기화
+  const form = useForm<TemplateFormValues>({
+    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 (
+    <Dialog open={open} onOpenChange={handleDialogOpenChange}>
+      <DialogTrigger asChild>
+        <Button variant="default" size="sm">
+          템플릿 추가
+        </Button>
+      </DialogTrigger>
+      <DialogContent className="sm:max-w-[500px]">
+        <DialogHeader>
+          <DialogTitle>새 기본계약서 템플릿 추가</DialogTitle>
+          <DialogDescription>
+            템플릿 이름을 입력하고 계약서 파일을 업로드하세요.
+            <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
+          </DialogDescription>
+        </DialogHeader>
+        <Form {...form}>
+          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+            <FormField
+              control={form.control}
+              name="templateName"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>
+                    템플릿 이름 <span className="text-red-500">*</span>
+                  </FormLabel>
+                  <FormControl>
+                    <Input placeholder="템플릿 이름을 입력하세요" {...field} />
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="validityPeriod"
+              render={({ field }) => (
+                <FormItem>
+                  <FormLabel>
+                    계약 유효기간 <span className="text-red-500">*</span>
+                  </FormLabel>
+                  <Select
+                    value={field.value?.toString()}
+                    onValueChange={(value) => field.onChange(parseInt(value))}
+                  >
+                    <FormControl>
+                      <SelectTrigger>
+                        <SelectValue placeholder="유효기간을 선택하세요" />
+                      </SelectTrigger>
+                    </FormControl>
+                    <SelectContent>
+                      {validityOptions.map(option => (
+                        <SelectItem key={option.value} value={option.value}>
+                          {option.label}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                  <FormDescription>
+                    계약서의 유효 기간을 설정합니다. 이 기간이 지나면 재계약이 필요합니다.
+                  </FormDescription>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+
+            <FormField
+              control={form.control}
+              name="file"
+              render={() => (
+                <FormItem>
+                  <FormLabel>
+                    계약서 파일 <span className="text-red-500">*</span>
+                  </FormLabel>
+                  <FormControl>
+                    <Dropzone
+                      onDrop={handleFileChange}
+                      accept={{
+                        'application/pdf': ['.pdf']
+                      }}
+                    >
+                      <DropzoneZone>
+                        <DropzoneUploadIcon className="h-10 w-10 text-muted-foreground" />
+                        <DropzoneTitle>
+                          {selectedFile ? selectedFile.name : "PDF 파일을 여기에 드래그하세요"}
+                        </DropzoneTitle>
+                        <DropzoneDescription>
+                          {selectedFile
+                            ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB`
+                            : "또는 클릭하여 PDF 파일을 선택하세요 (최대 100MB)"}
+                        </DropzoneDescription>
+                        <DropzoneInput />
+                      </DropzoneZone>
+                    </Dropzone>
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+            
+            {showProgress && (
+              <div className="space-y-2">
+                <div className="flex justify-between text-sm">
+                  <span>업로드 진행률</span>
+                  <span>{uploadProgress}%</span>
+                </div>
+                <Progress value={uploadProgress} />
+              </div>
+            )}
+
+            <DialogFooter>
+              <Button
+                type="button"
+                variant="outline"
+                onClick={() => setOpen(false)}
+                disabled={isLoading}
+              >
+                취소
+              </Button>
+              <Button
+                type="submit"
+                disabled={isLoading || !templateName || !validityPeriod || !file}
+              >
+                {isLoading ? "처리 중..." : "추가"}
+              </Button>
+            </DialogFooter>
+          </form>
+        </Form>
+      </DialogContent>
+    </Dialog>
+  );
+}
\ No newline at end of file
-- 
cgit v1.2.3

