"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 { Checkbox } from "@/components/ui/checkbox"; import { Switch } from "@/components/ui/switch"; 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { useRouter } from "next/navigation"; import { BUSINESS_UNITS } from "@/config/basicContractColumnsConfig"; // 템플릿 이름 옵션 정의 const TEMPLATE_NAME_OPTIONS = [ "준법서약", "기술자료 요구서", "비밀유지 계약서", "표준하도급기본 계약서", "General GTC", "안전보건관리 약정서", "동반성장", "윤리규범 준수 서약서", "기술자료 동의서", "내국신용장 미개설 합의서", "직납자재 하도급대급등 연동제 의향서" ] as const; // 업데이트된 계약서 템플릿 스키마 정의 (워드파일만 허용) const templateFormSchema = z.object({ templateName: z.enum(TEMPLATE_NAME_OPTIONS, { required_error: "템플릿 이름을 선택해주세요.", }), revision: z.coerce.number().int().min(1).default(1), legalReviewRequired: z.boolean().default(false), // 적용 범위 shipBuildingApplicable: z.boolean().default(false), windApplicable: z.boolean().default(false), pcApplicable: z.boolean().default(false), nbApplicable: z.boolean().default(false), rcApplicable: z.boolean().default(false), gyApplicable: z.boolean().default(false), sysApplicable: z.boolean().default(false), infraApplicable: z.boolean().default(false), file: z .instanceof(File, { message: "파일을 업로드해주세요." }) .refine((file) => file.size <= 100 * 1024 * 1024, { message: "파일 크기는 100MB 이하여야 합니다.", }) .refine( (file) => file.type === 'application/msword' || file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', { message: "워드 파일(.doc, .docx)만 업로드 가능합니다." } ), status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"), }).refine((data) => { // 적어도 하나의 적용 범위는 선택되어야 함 const scopeFields = [ 'shipBuildingApplicable', 'windApplicable', 'pcApplicable', 'nbApplicable', 'rcApplicable', 'gyApplicable', 'sysApplicable', 'infraApplicable' ]; const hasAnyScope = scopeFields.some(field => data[field as keyof typeof data] === true); return hasAnyScope; }, { message: "적어도 하나의 적용 범위를 선택해야 합니다.", path: ["shipBuildingApplicable"], // 에러를 첫 번째 체크박스에 표시 }); 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(); // 기본값 설정 (templateCode 제거) const defaultValues: Partial = { templateName: undefined, revision: 1, legalReviewRequired: false, shipBuildingApplicable: false, windApplicable: false, pcApplicable: false, nbApplicable: false, rcApplicable: false, gyApplicable: false, sysApplicable: false, infraApplicable: false, status: "ACTIVE", }; // 폼 초기화 const form = useForm({ resolver: zodResolver(templateFormSchema), defaultValues, mode: "onChange", }); // 파일 선택 핸들러 const handleFileChange = (files: File[]) => { if (files.length > 0) { const file = files[0]; setSelectedFile(file); form.setValue("file", file); } }; // 모든 적용 범위 선택/해제 const handleSelectAllScopes = (checked: boolean) => { BUSINESS_UNITS.forEach(unit => { form.setValue(unit.key as keyof TemplateFormValues, checked); }); }; // 청크 크기 설정 (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; } } }; // 폼 제출 핸들러 (templateCode 제거) 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("파일 업로드에 실패했습니다."); } // 메타데이터 저장 (templateCode 제거됨) const saveResponse = await fetch('/api/upload/basicContract/complete', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ templateName: formData.templateName, revision: formData.revision, legalReviewRequired: formData.legalReviewRequired, shipBuildingApplicable: formData.shipBuildingApplicable, windApplicable: formData.windApplicable, pcApplicable: formData.pcApplicable, nbApplicable: formData.nbApplicable, rcApplicable: formData.rcApplicable, gyApplicable: formData.gyApplicable, sysApplicable: formData.sysApplicable, infraApplicable: formData.infraApplicable, 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 selectedScopesCount = BUSINESS_UNITS.filter(unit => form.watch(unit.key as keyof TemplateFormValues) ).length; return ( {/* 고정된 헤더 */} 새 기본계약서 템플릿 추가 템플릿 정보를 입력하고 계약서 파일을 업로드하세요. * 표시된 항목은 필수 입력사항입니다. {/* 스크롤 가능한 컨텐츠 영역 */}
{/* 기본 정보 */} 기본 정보
( 템플릿 이름 * 미리 정의된 템플릿 중에서 선택 )} /> ( 리비전 field.onChange(parseInt(e.target.value) || 1)} /> 템플릿 버전 (기본값: 1)
동일한 템플릿 이름의 리비전은 중복될 수 없습니다.
)} />
(
법무검토 필요 법무팀 검토가 필요한 템플릿인지 설정
)} />
{/* 적용 범위 */} 적용 범위 * 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨)
{BUSINESS_UNITS.map((unit) => ( (
{unit.label}
)} /> ))}
{form.formState.errors.shipBuildingApplicable && (

{form.formState.errors.shipBuildingApplicable.message}

)}
{/* 파일 업로드 */} 파일 업로드 ( 계약서 파일 * {selectedFile ? selectedFile.name : "워드 파일을 여기에 드래그하세요"} {selectedFile ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"} )} /> {showProgress && (
업로드 진행률 {uploadProgress}%
)}
{/* 고정된 푸터 */}
); }