diff options
Diffstat (limited to 'lib/basic-contract/template/update-basicContract-sheet.tsx')
| -rw-r--r-- | lib/basic-contract/template/update-basicContract-sheet.tsx | 487 |
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> ) |
