"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 { FileText, Loader, Copy } from "lucide-react"; import { Dialog, 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 { Switch } from "@/components/ui/switch"; 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 { Badge } from "@/components/ui/badge"; import { useRouter } from "next/navigation"; import { GeneralContractTemplate } from "@/db/schema"; import { createGeneralContractTemplateRevisionAction } from "../actions"; // 리비전 생성 스키마 정의 const createRevisionSchema = z.object({ contractTemplateName: z.string().min(1, "계약 문서명을 입력해주세요."), contractTemplateType: z.string().min(1, "계약 종류를 입력해주세요."), revision: z.number().min(1, "리비전 번호는 1 이상이어야 합니다."), legalReviewRequired: z.boolean(), file: z.instanceof(File).optional(), }); type CreateRevisionFormValues = z.infer; interface CreateRevisionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; baseTemplate: GeneralContractTemplate | null; onSuccess?: () => void; } export function CreateRevisionDialog({ open, onOpenChange, baseTemplate, onSuccess }: CreateRevisionDialogProps) { const router = useRouter(); const [isLoading, setIsLoading] = React.useState(false); const [uploadProgress, setUploadProgress] = React.useState(0); const [suggestedRevision, setSuggestedRevision] = React.useState(1); // 기본 템플릿의 다음 리비전 번호 계산 React.useEffect(() => { if (baseTemplate) { setSuggestedRevision(baseTemplate.revision + 1); } }, [baseTemplate]); // 기본값 설정 (기존 템플릿의 설정을 상속) const defaultValues: Partial = React.useMemo(() => { if (!baseTemplate) return {}; return { contractTemplateName: baseTemplate.contractTemplateName, contractTemplateType: baseTemplate.contractTemplateType, revision: suggestedRevision, legalReviewRequired: baseTemplate.legalReviewRequired || false, }; }, [baseTemplate, suggestedRevision]); // 폼 초기화 const form = useForm({ resolver: zodResolver(createRevisionSchema), defaultValues, mode: "onChange", }); // baseTemplate이 변경될 때 폼 값 재설정 React.useEffect(() => { if (baseTemplate && defaultValues) { form.reset(defaultValues); } }, [baseTemplate, defaultValues, form]); // 파일 업로드 핸들러 (basic-contract와 동일한 방식) const uploadFileInChunks = async (file: File, fileId: string) => { const chunkSize = 1024 * 1024; // 1MB chunks const totalChunks = Math.ceil(file.size / chunkSize); for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { const start = chunkIndex * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('chunk', chunk); formData.append('chunkIndex', chunkIndex.toString()); formData.append('totalChunks', totalChunks.toString()); formData.append('fileId', fileId); formData.append('filename', file.name); try { const response = await fetch('/api/upload/generalContract/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: CreateRevisionFormValues) { if (!baseTemplate) { toast.error("기본 템플릿 정보가 없습니다."); return; } setIsLoading(true); setUploadProgress(0); try { let fileName = baseTemplate.fileName || ""; let filePath = baseTemplate.filePath || ""; // 새 파일이 업로드된 경우 if (formData.file) { const fileId = uuidv4(); const uploadResult = await uploadFileInChunks(formData.file, fileId); if (!uploadResult.success) { throw new Error("파일 업로드에 실패했습니다."); } fileName = uploadResult.fileName; filePath = uploadResult.filePath; } // Server Action으로 리비전 생성 const result = await createGeneralContractTemplateRevisionAction({ baseTemplateId: baseTemplate.id, contractTemplateName: formData.contractTemplateName, contractTemplateType: formData.contractTemplateType, revision: formData.revision, legalReviewRequired: formData.legalReviewRequired, fileName, filePath, }); toast.success(result.message); onSuccess?.(); onOpenChange(false); form.reset(); // 페이지 새로고침 window.location.reload(); } catch (error) { console.error("리비전 생성 오류:", error); toast.error("리비전 생성 중 오류가 발생했습니다."); } finally { setIsLoading(false); setUploadProgress(0); } } if (!baseTemplate) return null; return ( {/* 고정된 헤더 */} 새 리비전 생성
{baseTemplate.contractTemplateName} 현재 v{baseTemplate.revision} 새 v{suggestedRevision}

기존 템플릿을 기반으로 새로운 리비전을 생성합니다. * 표시된 항목은 필수 입력사항입니다.

{/* 스크롤 가능한 컨텐츠 영역 */}
{/* 리비전 정보 */} 리비전 정보 새로 생성할 리비전의 번호를 설정하세요 ( 리비전 번호 * field.onChange(parseInt(e.target.value) || suggestedRevision)} /> 권장 리비전: {suggestedRevision} (현재 리비전보다 큰 숫자여야 합니다) )} /> (
법무검토 필요 법무팀 검토가 필요한 템플릿인지 설정
)} />
{/* 기본 정보 */} 기본 정보 템플릿의 기본 정보를 입력하세요 ( 계약 문서명 * )} /> ( 계약 종류 * )} /> {/* 파일 업로드 */} 파일 업로드 새로운 파일을 업로드하거나 기존 파일을 사용할 수 있습니다. ( 새 파일 (선택사항) { if (acceptedFiles.length > 0) { onChange(acceptedFiles[0]); } }} accept={{ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], 'application/msword': ['.doc'], }} maxFiles={1} > 파일을 드래그하거나 클릭하여 업로드 DOCX 또는 DOC 파일만 업로드 가능합니다. {value && (
{value.name}
)} {uploadProgress > 0 && uploadProgress < 100 && (

업로드 중... {Math.round(uploadProgress)}%

)}
)} /> {baseTemplate?.fileName && (

기존 파일:

{baseTemplate.fileName} 기존 파일 사용
)}
{/* 고정된 푸터 */}
); }