summaryrefslogtreecommitdiff
path: root/lib/basic-contract/template/update-basicContract-sheet.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/template/update-basicContract-sheet.tsx')
-rw-r--r--lib/basic-contract/template/update-basicContract-sheet.tsx487
1 files changed, 243 insertions, 244 deletions
diff --git a/lib/basic-contract/template/update-basicContract-sheet.tsx b/lib/basic-contract/template/update-basicContract-sheet.tsx
index 810e1b77..88783461 100644
--- a/lib/basic-contract/template/update-basicContract-sheet.tsx
+++ b/lib/basic-contract/template/update-basicContract-sheet.tsx
@@ -47,15 +47,30 @@ import {
} from "@/components/ui/dropzone"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
-import { Badge } from "@/components/ui/badge"
import { updateTemplate } from "../service"
import { BasicContractTemplate } from "@/db/schema"
import { BUSINESS_UNITS, scopeHelpers } from "@/config/basicContractColumnsConfig"
-// 업데이트 템플릿 스키마 정의
+// 템플릿 이름 옵션 정의
+const TEMPLATE_NAME_OPTIONS = [
+ "준법서약",
+ "기술자료 요구서",
+ "비밀유지 계약서",
+ "표준하도급기본 계약서",
+ "General GTC",
+ "안전보건관리 약정서",
+ "동반성장",
+ "윤리규범 준수 서약서",
+ "기술자료 동의서",
+ "내국신용장 미개설 합의서",
+ "직납자재 하도급대급등 연동제 의향서"
+] as const;
+
+// 업데이트 템플릿 스키마 정의 (templateCode, status 제거, 워드파일만 허용)
export const updateTemplateSchema = z.object({
- templateCode: z.string().min(1, "템플릿 코드는 필수입니다."), // readonly로 처리
- templateName: z.string().min(1, "템플릿 이름은 필수입니다."),
+ templateName: z.enum(TEMPLATE_NAME_OPTIONS, {
+ required_error: "템플릿 이름을 선택해주세요.",
+ }),
revision: z.coerce.number().int().min(1, "리비전은 1 이상이어야 합니다."),
legalReviewRequired: z.boolean(),
@@ -69,10 +84,18 @@ export const updateTemplateSchema = z.object({
sysApplicable: z.boolean(),
infraApplicable: z.boolean(),
- status: z.enum(["ACTIVE", "DISPOSED"], {
- required_error: "상태는 필수 선택사항입니다.",
- }),
- file: z.instanceof(File, { message: "파일을 업로드해주세요." }).optional(),
+ 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)만 업로드 가능합니다." }
+ )
+ .optional(),
}).refine((data) => {
// 적어도 하나의 적용 범위는 선택되어야 함
const hasAnyScope = BUSINESS_UNITS.some(unit =>
@@ -99,8 +122,7 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
const form = useForm<UpdateTemplateSchema>({
resolver: zodResolver(updateTemplateSchema),
defaultValues: {
- templateCode: template?.templateCode ?? "",
- templateName: template?.templateName ?? "",
+ templateName: template?.templateName as typeof TEMPLATE_NAME_OPTIONS[number] ?? "준법서약",
revision: template?.revision ?? 1,
legalReviewRequired: template?.legalReviewRequired ?? false,
shipBuildingApplicable: template?.shipBuildingApplicable ?? false,
@@ -111,7 +133,6 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
gyApplicable: template?.gyApplicable ?? false,
sysApplicable: template?.sysApplicable ?? false,
infraApplicable: template?.infraApplicable ?? false,
- status: (template?.status as "ACTIVE" | "DISPOSED") || "ACTIVE"
},
mode: "onChange"
})
@@ -136,8 +157,7 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
React.useEffect(() => {
if (template) {
form.reset({
- templateCode: template.templateCode,
- templateName: template.templateName,
+ templateName: template.templateName as typeof TEMPLATE_NAME_OPTIONS[number],
revision: template.revision ?? 1,
legalReviewRequired: template.legalReviewRequired ?? false,
shipBuildingApplicable: template.shipBuildingApplicable ?? false,
@@ -148,7 +168,6 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
gyApplicable: template.gyApplicable ?? false,
sysApplicable: template.sysApplicable ?? false,
infraApplicable: template.infraApplicable ?? false,
- status: template.status as "ACTIVE" | "DISPOSED",
});
}
}, [template, form]);
@@ -162,9 +181,8 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
startUpdateTransition(async () => {
if (!template) return
- // FormData 객체 생성하여 파일과 데이터를 함께 전송
+ // FormData 객체 생성하여 파일과 데이터를 함께 전송 (templateCode, status 제거)
const formData = new FormData();
- formData.append("templateCode", input.templateCode);
formData.append("templateName", input.templateName);
formData.append("revision", input.revision.toString());
formData.append("legalReviewRequired", input.legalReviewRequired.toString());
@@ -175,8 +193,6 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
formData.append(unit.key, value.toString());
});
- formData.append("status", input.status);
-
if (input.file) {
formData.append("file", input.file);
}
@@ -209,259 +225,242 @@ export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTem
return (
<Sheet {...props}>
- <SheetContent className="flex flex-col gap-6 sm:max-w-[600px] overflow-y-auto">
- <SheetHeader className="text-left">
+ <SheetContent className="sm:max-w-[600px] h-[100vh] flex flex-col p-0">
+ {/* 고정된 헤더 */}
+ <SheetHeader className="p-6 pb-4 border-b">
<SheetTitle>템플릿 업데이트</SheetTitle>
<SheetDescription>
템플릿 정보를 수정하고 변경사항을 저장하세요
+ <span className="text-red-500 mt-1 block text-sm">* 표시된 항목은 필수 입력사항입니다.</span>
</SheetDescription>
</SheetHeader>
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col gap-6"
- >
- {/* 기본 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">기본 정보</CardTitle>
- <CardDescription>
- 현재 적용 범위: {scopeHelpers.getScopeDisplayText(template)}
- </CardDescription>
- </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>템플릿 코드</FormLabel>
- <FormControl>
- <Input
- {...field}
- readOnly
- className="bg-muted"
- />
- </FormControl>
- <FormDescription>
- 템플릿 코드는 수정할 수 없습니다.
- </FormDescription>
- </FormItem>
- )}
- />
+ {/* 스크롤 가능한 컨텐츠 영역 */}
+ <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>
+ <CardDescription>
+ 현재 적용 범위: {scopeHelpers.getScopeDisplayText(template)}
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="templateName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>
+ 템플릿 이름 <span className="text-red-500">*</span>
+ </FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="템플릿 이름을 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectGroup>
+ {TEMPLATE_NAME_OPTIONS.map((option) => (
+ <SelectItem key={option} value={option}>
+ {option}
+ </SelectItem>
+ ))}
+ </SelectGroup>
+ </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>
+ 템플릿 버전을 업데이트하세요.
+ <br />
+ <span className="text-xs text-muted-foreground">
+ 동일한 템플릿 이름의 리비전은 중복될 수 없습니다.
+ </span>
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
<FormField
control={form.control}
- name="revision"
+ name="legalReviewRequired"
render={({ field }) => (
- <FormItem>
- <FormLabel>리비전</FormLabel>
+ <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>
- <Input
- type="number"
- min="1"
- {...field}
- onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
+ <Switch
+ checked={field.value}
+ onCheckedChange={field.onChange}
/>
</FormControl>
- <FormDescription>
- 템플릿 버전을 업데이트하세요.
- </FormDescription>
- <FormMessage />
</FormItem>
)}
/>
- </div>
+ </CardContent>
+ </Card>
- <FormField
- control={form.control}
- name="templateName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>템플릿 이름</FormLabel>
- <FormControl>
- <Input placeholder="템플릿 이름을 입력하세요" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
+ {/* 적용 범위 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">
+ 적용 범위 <span className="text-red-500">*</span>
+ </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>
+
+ <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 UpdateTemplateSchema}
+ 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>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ {/* 파일 업데이트 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">파일 업데이트</CardTitle>
+ <CardDescription>
+ 현재 파일: {template.fileName}
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
<FormField
control={form.control}
- name="status"
- render={({ field }) => (
+ name="file"
+ render={() => (
<FormItem>
- <FormLabel>상태</FormLabel>
- <Select
- defaultValue={field.value}
- onValueChange={field.onChange}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="템플릿 상태 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="ACTIVE">활성</SelectItem>
- <SelectItem value="DISPOSED">폐기</SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
+ <FormLabel>템플릿 파일 (선택사항)</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>
)}
/>
- </div>
-
- <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>
-
- <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 UpdateTemplateSchema}
- 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>
+ </CardContent>
+ </Card>
+ </form>
+ </Form>
+ </div>
- {/* 파일 업데이트 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">파일 업데이트</CardTitle>
- <CardDescription>
- 현재 파일: {template.fileName}
- </CardDescription>
- </CardHeader>
- <CardContent>
- <FormField
- control={form.control}
- name="file"
- render={() => (
- <FormItem>
- <FormLabel>템플릿 파일 (선택사항)</FormLabel>
- <FormControl>
- <Dropzone
- onDrop={handleFileChange}
- accept={{
- 'application/pdf': ['.pdf']
- }}
- >
- <DropzoneZone>
- <DropzoneUploadIcon className="h-10 w-10 text-muted-foreground" />
- <DropzoneTitle>
- {selectedFile
- ? selectedFile.name
- : "새 파일을 드래그하세요 (선택사항)"}
- </DropzoneTitle>
- <DropzoneDescription>
- {selectedFile
- ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB`
- : "또는 클릭하여 파일을 선택하세요"}
- </DropzoneDescription>
- <DropzoneInput />
- </DropzoneZone>
- </Dropzone>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </CardContent>
- </Card>
-
- <SheetFooter className="gap-2 pt-2 sm:space-x-0">
- <SheetClose asChild>
- <Button type="button" variant="outline">
- 취소
- </Button>
- </SheetClose>
- <Button
- type="submit"
- disabled={isUpdatePending || !form.formState.isValid}
- >
- {isUpdatePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- 저장
- </Button>
- </SheetFooter>
- </form>
- </Form>
+ {/* 고정된 푸터 */}
+ <SheetFooter className="p-6 pt-4 border-t">
+ <SheetClose asChild>
+ <Button type="button" variant="outline">
+ 취소
+ </Button>
+ </SheetClose>
+ <Button
+ type="button"
+ onClick={form.handleSubmit(onSubmit)}
+ disabled={isUpdatePending || !form.formState.isValid}
+ >
+ {isUpdatePending && (
+ <Loader
+ className="mr-2 size-4 animate-spin"
+ aria-hidden="true"
+ />
+ )}
+ 저장
+ </Button>
+ </SheetFooter>
</SheetContent>
</Sheet>
)