diff options
Diffstat (limited to 'lib/general-contract-template/template/add-general-contract-template-dialog.tsx')
| -rw-r--r-- | lib/general-contract-template/template/add-general-contract-template-dialog.tsx | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/lib/general-contract-template/template/add-general-contract-template-dialog.tsx b/lib/general-contract-template/template/add-general-contract-template-dialog.tsx new file mode 100644 index 00000000..8862fb4b --- /dev/null +++ b/lib/general-contract-template/template/add-general-contract-template-dialog.tsx @@ -0,0 +1,383 @@ +"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"; +// 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 { Input } from "@/components/ui/input"; +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 { createTemplateFromUpload } from "../actions"; + +// (불필요한 템플릿/프로젝트 로딩 로직 제거) + +const templateFormSchema = z.object({ + contractTemplateType: z.string().min(2, "계약 종류는 2자리 영문입니다.").max(2, "계약 종류는 2자리 영문입니다.").regex(/^[A-Za-z]{2}$/, "영문 2자리로 입력하세요."), + contractTemplateName: z.string().min(1, "계약 문서명을 입력하세요."), + legalReviewRequired: z.boolean().default(false), + file: z.instanceof(File, { + message: "파일을 업로드해주세요.", + }), +}) +.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<typeof templateFormSchema>; + +export function AddGeneralContractTemplateDialog() { + 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> = { + contractTemplateType: "", + contractTemplateName: "", + legalReviewRequired: false, + }; + + const form = useForm<TemplateFormValues>({ + resolver: zodResolver(templateFormSchema), + defaultValues, + mode: "onChange", + }); + + // (불필요한 데이터 로딩 제거) + + const handleFileChange = (files: File[]) => { + if (files.length > 0) { + const file = files[0]; + setSelectedFile(file); + form.setValue("file", file); + } + }; + + // (프로젝트/템플릿 관련 핸들러 제거) + + // 청크 업로드 설정 (basic과 동일 패턴) + 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/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; + } + } + }; + + async function onSubmit(formData: TemplateFormValues) { + setIsLoading(true); + try { + // 파일 업로드 (청크 업로드 → 마지막 청크에서 filePath 반환) + const { v4: uuidv4 } = await import('uuid'); + const fileId = uuidv4(); + const uploadResult = await uploadFileInChunks(formData.file, fileId); + + if (!uploadResult?.success) { + throw new Error("파일 업로드에 실패했습니다."); + } + + // 업로드 완료 후 DB 저장 API 호출 (basic과 동일 플로우) + const saveResponse = await fetch('/api/upload/generalContract/complete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contractTemplateType: formData.contractTemplateType, + contractTemplateName: formData.contractTemplateName, + legalReviewRequired: formData.legalReviewRequired, + revision: 1, + status: 'ACTIVE', + fileName: uploadResult.fileName, + filePath: uploadResult.filePath, + }), + }); + + const saveResult = await saveResponse.json(); + if (!saveResult?.success) { + 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); + } + + // (이전 필드 watch 제거) + + const isSubmitDisabled = isLoading || + !form.watch("contractTemplateType") || + !form.watch("contractTemplateName") || + !form.watch("file"); + + return ( + <Dialog open={open} onOpenChange={handleDialogOpenChange}> + <DialogTrigger asChild> + <Button variant="default" size="sm"> + 신규등록 + </Button> + </DialogTrigger> + <DialogContent className="sm:max-w-[600px] h-[90vh] flex flex-col p-0"> + <DialogHeader className="p-6 pb-4 border-b"> + <DialogTitle>신규등록 - 일반계약 표준양식</DialogTitle> + <DialogDescription> + 계약 종류, 계약 문서명, 법무 검토, 첨부파일을 입력하세요. + <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span> + </DialogDescription> + </DialogHeader> + + <div className="flex-1 overflow-y-auto px-6"> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 py-4"> + <Card> + <CardHeader> + <CardTitle className="text-lg">계약 종류</CardTitle> + </CardHeader> + <CardContent> + <FormField + control={form.control} + name="contractTemplateType" + render={({ field }) => ( + <FormItem> + <FormLabel> + 계약 종류 <span className="text-red-500">*</span> + </FormLabel> + <Input + placeholder="2자리 영문 (예: LO)" + value={field.value} + onChange={(e) => field.onChange(e.target.value.toUpperCase().slice(0, 2))} + maxLength={2} + /> + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + </Card> + + <Card> + <CardHeader> + <CardTitle className="text-lg">계약 문서명</CardTitle> + </CardHeader> + <CardContent> + <FormField + control={form.control} + name="contractTemplateName" + render={({ field }) => ( + <FormItem> + <FormLabel> + 계약 문서명 <span className="text-red-500">*</span> + </FormLabel> + <Input + placeholder="계약문서명을 입력하세요" + value={field.value} + onChange={field.onChange} + /> + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + </Card> + + <Card> + <CardHeader> + <CardTitle className="text-lg">법무 검토</CardTitle> + </CardHeader> + <CardContent> + <FormField + control={form.control} + name="legalReviewRequired" + render={({ field }) => ( + <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm"> + <div className="space-y-0.5"> + <FormLabel>법무검토 필요</FormLabel> + <FormDescription> + 법무팀 검토가 필요한 템플릿인지 설정 + </FormDescription> + </div> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + </FormItem> + )} + /> + </CardContent> + </Card> + + <Card> + <CardHeader> + <CardTitle className="text-lg">파일 업로드</CardTitle> + <CardDescription> + 템플릿 파일을 업로드하세요 + </CardDescription> + </CardHeader> + <CardContent> + <FormField + control={form.control} + name="file" + render={() => ( + <FormItem> + <FormLabel> + 템플릿 파일 <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <Dropzone + onDrop={handleFileChange} + accept={{ + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'] + }} + > + <DropzoneZone> + <DropzoneUploadIcon className="h-10 w-10 text-muted-foreground" /> + <DropzoneTitle> + {selectedFile ? selectedFile.name : "워드 파일을 여기에 드래그하세요"} + </DropzoneTitle> + <DropzoneDescription> + {selectedFile + ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` + : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"} + </DropzoneDescription> + <DropzoneInput /> + </DropzoneZone> + </Dropzone> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {showProgress && ( + <div className="space-y-2 mt-4"> + <div className="flex justify-between text-sm"> + <span>업로드 진행률</span> + <span>{uploadProgress}%</span> + </div> + <Progress value={uploadProgress} /> + </div> + )} + </CardContent> + </Card> + </form> + </Form> + </div> + + <DialogFooter className="p-6 pt-4 border-t"> + <Button + type="button" + variant="outline" + onClick={() => setOpen(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button + type="button" + onClick={form.handleSubmit(onSubmit)} + disabled={isSubmitDisabled} + > + {isLoading ? "처리 중..." : "추가"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
