diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-15 00:50:39 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-15 00:50:39 +0000 |
| commit | 15c3ae6536c264db0508e4fc4aaa59c3e6d1af30 (patch) | |
| tree | 8e2ad5e6a06999bfaaf00ab4ee30083a87050bad /lib/basic-contract/template/add-basic-contract-template-dialog.tsx | |
| parent | d5d27847a7eded9db59462fa744b76772bc9ce1d (diff) | |
(대표님) 기본계약 및 정기평가 작업사항, OCR 변경사항
Diffstat (limited to 'lib/basic-contract/template/add-basic-contract-template-dialog.tsx')
| -rw-r--r-- | lib/basic-contract/template/add-basic-contract-template-dialog.tsx | 387 |
1 files changed, 268 insertions, 119 deletions
diff --git a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx index cf0986f0..3a83d50f 100644 --- a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx +++ b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx @@ -18,6 +18,8 @@ import { } 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,
@@ -34,17 +36,31 @@ import { DropzoneInput
} from "@/components/ui/dropzone";
import { Progress } from "@/components/ui/progress";
-import { useRouter } from "next/navigation"
+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 templateFormSchema = z.object({
+ templateCode: z.string()
+ .min(1, "템플릿 코드는 필수입니다.")
+ .max(50, "템플릿 코드는 50자 이하여야 합니다.")
+ .regex(/^[A-Z0-9_-]+$/, "템플릿 코드는 영문 대문자, 숫자, '_', '-'만 사용 가능합니다."),
templateName: z.string().min(1, "템플릿 이름은 필수입니다."),
- validityPeriod: z.coerce
- .number({ invalid_type_error: "유효기간은 숫자여야 합니다." })
- .int("유효기간은 정수여야 합니다.")
- .min(1, "유효기간은 최소 1개월 이상이어야 합니다.")
- .max(120, "유효기간은 최대 120개월(10년)을 초과할 수 없습니다.")
- .default(12),
+ 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, {
@@ -55,6 +71,15 @@ const templateFormSchema = z.object({ { message: "PDF 파일만 업로드 가능합니다." }
),
status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"),
+}).refine((data) => {
+ // 적어도 하나의 적용 범위는 선택되어야 함
+ const hasAnyScope = BUSINESS_UNITS.some(unit =>
+ data[unit.key as keyof typeof data] as boolean
+ );
+ return hasAnyScope;
+}, {
+ message: "적어도 하나의 적용 범위를 선택해야 합니다.",
+ path: ["shipBuildingApplicable"], // 에러를 첫 번째 체크박스에 표시
});
type TemplateFormValues = z.infer<typeof templateFormSchema>;
@@ -65,12 +90,22 @@ export function AddTemplateDialog() { const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
const [uploadProgress, setUploadProgress] = React.useState(0);
const [showProgress, setShowProgress] = React.useState(false);
- const router = useRouter()
+ const router = useRouter();
// 기본값 설정
const defaultValues: Partial<TemplateFormValues> = {
+ templateCode: "",
templateName: "",
- validityPeriod: 12, // 기본값 1년
+ revision: 1,
+ legalReviewRequired: false,
+ shipBuildingApplicable: false,
+ windApplicable: false,
+ pcApplicable: false,
+ nbApplicable: false,
+ rcApplicable: false,
+ gyApplicable: false,
+ sysApplicable: false,
+ infraApplicable: false,
status: "ACTIVE",
};
@@ -81,11 +116,6 @@ export function AddTemplateDialog() { 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) {
@@ -95,6 +125,13 @@ export function AddTemplateDialog() { }
};
+ // 모든 적용 범위 선택/해제
+ const handleSelectAllScopes = (checked: boolean) => {
+ BUSINESS_UNITS.forEach(unit => {
+ form.setValue(unit.key as keyof TemplateFormValues, checked);
+ });
+ };
+
// 청크 크기 설정 (1MB)
const CHUNK_SIZE = 1 * 1024 * 1024;
@@ -161,15 +198,25 @@ export function AddTemplateDialog() { throw new Error("파일 업로드에 실패했습니다.");
}
- // 메타데이터 저장
+ // 메타데이터 저장 (업데이트된 필드들 포함)
const saveResponse = await fetch('/api/upload/basicContract/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
+ templateCode: formData.templateCode,
templateName: formData.templateName,
- validityPeriod: formData.validityPeriod, // 유효기간 추가
+ 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,
@@ -215,15 +262,10 @@ export function AddTemplateDialog() { 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년" },
- ];
+ // 현재 선택된 적용 범위 수
+ const selectedScopesCount = BUSINESS_UNITS.filter(unit =>
+ form.watch(unit.key as keyof TemplateFormValues)
+ ).length;
return (
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
@@ -232,108 +274,215 @@ export function AddTemplateDialog() { 템플릿 추가
</Button>
</DialogTrigger>
- <DialogContent className="sm:max-w-[500px]">
+ <DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<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>
- )}
- />
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+ {/* 기본 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">기본 정보</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="templateCode"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>
+ 템플릿 코드 <span className="text-red-500">*</span>
+ </FormLabel>
+ <FormControl>
+ <Input
+ placeholder="TEMPLATE_001"
+ {...field}
+ style={{ textTransform: 'uppercase' }}
+ onChange={(e) => field.onChange(e.target.value.toUpperCase())}
+ />
+ </FormControl>
+ <FormDescription>
+ 영문 대문자, 숫자, '_', '-'만 사용 가능
+ </FormDescription>
+ <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="revision"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>리비전</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ min="1"
+ {...field}
+ onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
+ />
+ </FormControl>
+ <FormDescription>
+ 템플릿 버전 (기본값: 1)
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
- <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>
+ <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="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>
+ 이 템플릿이 적용될 사업부를 선택하세요. ({selectedScopesCount}개 선택됨)
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="select-all"
+ checked={selectedScopesCount === BUSINESS_UNITS.length}
+ onCheckedChange={handleSelectAllScopes}
+ />
+ <label htmlFor="select-all" className="text-sm font-medium">
+ 전체 선택
+ </label>
</div>
- <Progress value={uploadProgress} />
- </div>
- )}
+
+ <Separator />
+
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+ {BUSINESS_UNITS.map((unit) => (
+ <FormField
+ key={unit.key}
+ control={form.control}
+ name={unit.key as keyof TemplateFormValues}
+ render={({ field }) => (
+ <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+ <FormControl>
+ <Checkbox
+ checked={field.value as boolean}
+ onCheckedChange={field.onChange}
+ />
+ </FormControl>
+ <div className="space-y-1 leading-none">
+ <FormLabel className="text-sm font-normal">
+ {unit.label}
+ </FormLabel>
+ </div>
+ </FormItem>
+ )}
+ />
+ ))}
+ </div>
+
+ {form.formState.errors.shipBuildingApplicable && (
+ <p className="text-sm text-destructive">
+ {form.formState.errors.shipBuildingApplicable.message}
+ </p>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 파일 업로드 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">파일 업로드</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <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 mt-4">
+ <div className="flex justify-between text-sm">
+ <span>업로드 진행률</span>
+ <span>{uploadProgress}%</span>
+ </div>
+ <Progress value={uploadProgress} />
+ </div>
+ )}
+ </CardContent>
+ </Card>
<DialogFooter>
<Button
@@ -346,7 +495,7 @@ export function AddTemplateDialog() { </Button>
<Button
type="submit"
- disabled={isLoading || !templateName || !validityPeriod || !file}
+ disabled={isLoading || !form.formState.isValid}
>
{isLoading ? "처리 중..." : "추가"}
</Button>
|
