From a50bc9baea332f996e6bc3a5d70c69f6d2d0f194 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 23 Jul 2025 09:08:03 +0000
Subject: (대표님, 최겸) 기본계약 템플릿 및 에디터, 기술영업 벤더정보, 파일
 보안다운로드, 벤더 document sync 상태 서비스, 메뉴 Config, 기술영업 미사용
 제거
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../template/update-basicContract-sheet.tsx        | 487 ++++++++++-----------
 1 file changed, 243 insertions(+), 244 deletions(-)

(limited to 'lib/basic-contract/template/update-basicContract-sheet.tsx')

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>
   )
-- 
cgit v1.2.3

