"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 { Button } from "@/components/ui/button"; 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 { useRouter } from "next/navigation"; import { getExistingTemplateNames } from "../service"; import { getAvailableProjectsForGtc } from "@/lib/gtc-contract/service"; // 고정 템플릿 옵션들 (GTC 제외) const FIXED_TEMPLATE_OPTIONS = [ "준법서약 (한글)", "준법서약 (영문)", "기술자료 요구서", "비밀유지 계약서", "표준하도급기본 계약서", "General GTC", // 기본 GTC (하나만) "안전보건관리 약정서", "동반성장", "윤리규범 준수 서약서", "기술자료 동의서", "내국신용장 미개설 합의서", "직납자재 하도급대급등 연동제 의향서" ] as const; // 프로젝트 타입 정의 type ProjectForFilter = { id: number; code: string; name: string; }; const templateFormSchema = z.object({ templateType: z.enum(['FIXED', 'PROJECT_GTC'], { required_error: "템플릿 타입을 선택해주세요.", }), templateName: z.string().min(1, "템플릿 이름을 선택하거나 입력해주세요."), selectedProjectId: z.number().optional(), legalReviewRequired: z.boolean().default(false), file: z.instanceof(File, { message: "파일을 업로드해주세요.", }), }) .refine((data) => { // PROJECT_GTC 타입인 경우 프로젝트 선택 필수 if (data.templateType === 'PROJECT_GTC' && !data.selectedProjectId) { return false; } return true; }, { message: "프로젝트를 선택해주세요.", path: ["selectedProjectId"], }) .refine((data) => { if (data.file && data.file.size > 100 * 1024 * 1024) return false; return true; }, { message: "파일 크기는 100MB 이하여야 합니다.", path: ["file"], }) .refine((data) => { if (data.file) { const isValidType = data.file.type === 'application/msword' || data.file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; return isValidType; } return true; }, { message: "워드 파일(.doc, .docx)만 업로드 가능합니다.", path: ["file"], }); 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 [availableFixedTemplates, setAvailableFixedTemplates] = React.useState([]); const [availableProjects, setAvailableProjects] = React.useState([]); const router = useRouter(); // 기본값 const defaultValues: Partial = { templateType: 'FIXED', templateName: '', selectedProjectId: undefined, legalReviewRequired: false, }; const form = useForm({ resolver: zodResolver(templateFormSchema), defaultValues, mode: "onChange", }); // 🔸 마운트 시 사용 가능한 고정 템플릿들과 프로젝트들 가져오기 React.useEffect(() => { let cancelled = false; const loadData = async () => { try { // 고정 템플릿 중 이미 사용된 것들 제외 const usedTemplateNames = await getExistingTemplateNames(); if (cancelled) return; const filteredFixedTemplates = FIXED_TEMPLATE_OPTIONS.filter( name => !usedTemplateNames.includes(name) ); setAvailableFixedTemplates(filteredFixedTemplates); // GTC 생성 가능한 프로젝트들 가져오기 const projects = await getAvailableProjectsForGtc(); if (cancelled) return; setAvailableProjects(projects); } catch (err) { console.log("Failed to load template data", err); console.error("Failed to load template data", err); toast.error("템플릿 정보를 불러오는데 실패했습니다."); } }; if (open) { loadData(); } return () => { cancelled = true; }; }, [open]); const handleFileChange = (files: File[]) => { if (files.length > 0) { const file = files[0]; setSelectedFile(file); form.setValue("file", file); } }; // 프로젝트 선택 시 템플릿 이름 자동 설정 const handleProjectChange = (projectId: string) => { const project = availableProjects.find(p => p.id === parseInt(projectId)); if (project) { form.setValue("selectedProjectId", project.id); form.setValue("templateName", `${project.code} GTC`); } }; // 청크 업로드 설정 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); 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; } } }; async function onSubmit(formData: TemplateFormValues) { setIsLoading(true); try { // 파일 업로드 진행 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, revision: 1, legalReviewRequired: formData.legalReviewRequired, status: "ACTIVE", fileName: uploadResult.fileName, filePath: uploadResult.filePath, }), next: { tags: ["basic-contract-templates"] }, }); const saveResult = await saveResponse.json(); if (!saveResult.success) { console.log(saveResult.error); throw new Error(saveResult.error || "템플릿 정보 저장에 실패했습니다."); } toast.success('템플릿이 성공적으로 추가되었습니다.'); form.reset(); setSelectedFile(null); setOpen(false); setShowProgress(false); router.refresh(); } catch (error) { console.error("Submit error:", error); toast.error(error instanceof Error ? error.message : "템플릿 추가 중 오류가 발생했습니다."); } 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 templateType = form.watch("templateType"); const selectedProjectId = form.watch("selectedProjectId"); const templateName = form.watch("templateName"); const isSubmitDisabled = isLoading || !templateType || !templateName || !form.watch("file") || (templateType === 'PROJECT_GTC' && !selectedProjectId); return ( 새 기본계약서 템플릿 추가 템플릿 정보를 입력하고 계약서 파일을 업로드하세요. (리비전은 자동으로 1로 설정됩니다) * 표시된 항목은 필수 입력사항입니다.
{/* 템플릿 타입 선택 */} 템플릿 종류 추가할 템플릿의 종류를 선택하세요 ( 템플릿 종류 * {templateType === 'FIXED' && "미리 정의된 표준 템플릿 중에서 선택합니다."} {templateType === 'PROJECT_GTC' && "특정 프로젝트용 GTC 템플릿을 생성합니다."} )} /> {/* 기본 정보 */} 기본 정보 {/* 표준 템플릿 선택 */} {templateType === 'FIXED' && ( ( 템플릿 이름 * 이미 등록되지 않은 템플릿만 표시됩니다. (리비전 1로 생성) )} /> )} {/* 프로젝트 GTC */} {templateType === 'PROJECT_GTC' && ( <> ( 프로젝트 선택 * 아직 GTC가 생성되지 않은 프로젝트만 표시됩니다. )} /> {/* 생성될 템플릿 이름 미리보기 */} {templateName && (
생성될 템플릿 이름
{templateName}
)} )} (
법무검토 필요 법무팀 검토가 필요한 템플릿인지 설정
)} />
{/* 파일 업로드 */} 파일 업로드 템플릿 파일을 업로드하세요 ( 템플릿 파일 * {selectedFile ? selectedFile.name : "워드 파일을 여기에 드래그하세요"} {selectedFile ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"} )} /> {showProgress && (
업로드 진행률 {uploadProgress}%
)}
); }