From 1dc24d48e52f2e490f5603ceb02842586ecae533 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 24 Jul 2025 11:06:32 +0000
Subject: (대표님) 정기평가 피드백 반영, 설계 피드백 반영, (최겸) 기술영업
 피드백 반영
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../add-basic-contract-template-dialog.tsx         | 268 +++++++++++----------
 1 file changed, 135 insertions(+), 133 deletions(-)

(limited to 'lib/basic-contract/template/add-basic-contract-template-dialog.tsx')

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 9b036445..fd1bd333 100644
--- a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx
+++ b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx
@@ -40,10 +40,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
 import { Separator } from "@/components/ui/separator";
 import { useRouter } from "next/navigation";
 import { BUSINESS_UNITS } from "@/config/basicContractColumnsConfig";
+import { getExistingTemplateNames } from "../service";
 
-// 템플릿 이름 옵션 정의
+// ✅ 서버 액션 import
+
+// 전체 템플릿 후보
 const TEMPLATE_NAME_OPTIONS = [
-  "준법서약",
+  "준법서약 (한글)",
+  "준법서약 (영문)",
   "기술자료 요구서", 
   "비밀유지 계약서",
   "표준하도급기본 계약서",
@@ -56,14 +60,11 @@ const TEMPLATE_NAME_OPTIONS = [
   "직납자재 하도급대급등 연동제 의향서"
 ] as const;
 
-// 업데이트된 계약서 템플릿 스키마 정의 (워드파일만 허용)
 const templateFormSchema = z.object({
   templateName: z.enum(TEMPLATE_NAME_OPTIONS, {
     required_error: "템플릿 이름을 선택해주세요.",
   }),
-  revision: z.coerce.number().int().min(1).default(1),
   legalReviewRequired: z.boolean().default(false),
-  
   // 적용 범위
   shipBuildingApplicable: z.boolean().default(false),
   windApplicable: z.boolean().default(false),
@@ -73,31 +74,42 @@ const templateFormSchema = z.object({
   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, {
-      message: "파일 크기는 100MB 이하여야 합니다.",
-    })
-    .refine(
-      (file) => 
-        file.type === 'application/msword' || 
-        file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-      { message: "워드 파일(.doc, .docx)만 업로드 가능합니다." }
-    ),
-  status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"),
-}).refine((data) => {
-  // 적어도 하나의 적용 범위는 선택되어야 함
+  file: z.instanceof(File).optional(),
+})
+.refine((data) => {
+  if (data.templateName !== "General GTC" && !data.file) return false;
+  return true;
+}, {
+  message: "파일을 업로드해주세요.",
+  path: ["file"],
+})
+.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"],
+})
+.refine((data) => {
   const scopeFields = [
     'shipBuildingApplicable', 'windApplicable', 'pcApplicable', 'nbApplicable',
     'rcApplicable', 'gyApplicable', 'sysApplicable', 'infraApplicable'
   ];
-  
-  const hasAnyScope = scopeFields.some(field => data[field as keyof typeof data] === true);
-  return hasAnyScope;
+  return scopeFields.some(field => data[field as keyof typeof data] === true);
 }, {
   message: "적어도 하나의 적용 범위를 선택해야 합니다.",
-  path: ["shipBuildingApplicable"], // 에러를 첫 번째 체크박스에 표시
+  path: ["shipBuildingApplicable"],
 });
 
 type TemplateFormValues = z.infer<typeof templateFormSchema>;
@@ -108,12 +120,12 @@ export function AddTemplateDialog() {
   const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
   const [uploadProgress, setUploadProgress] = React.useState(0);
   const [showProgress, setShowProgress] = React.useState(false);
+  const [availableTemplateNames, setAvailableTemplateNames] = React.useState<typeof TEMPLATE_NAME_OPTIONS[number][]>(TEMPLATE_NAME_OPTIONS);
   const router = useRouter();
 
-  // 기본값 설정 (templateCode 제거)
+  // 기본값
   const defaultValues: Partial<TemplateFormValues> = {
     templateName: undefined,
-    revision: 1,
     legalReviewRequired: false,
     shipBuildingApplicable: false,
     windApplicable: false,
@@ -123,17 +135,33 @@ export function AddTemplateDialog() {
     gyApplicable: false,
     sysApplicable: false,
     infraApplicable: false,
-    status: "ACTIVE",
   };
 
-  // 폼 초기화
   const form = useForm<TemplateFormValues>({
     resolver: zodResolver(templateFormSchema),
     defaultValues,
     mode: "onChange",
   });
 
-  // 파일 선택 핸들러
+  // 🔸 마운트 시 이미 등록된 templateName 목록 가져와서 필터링
+  React.useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        const usedNames = await getExistingTemplateNames();
+        if (cancelled) return;
+
+        // 이미 있는 이름 제외
+        const filtered = TEMPLATE_NAME_OPTIONS.filter(name => !usedNames.includes(name));
+        setAvailableTemplateNames(filtered);
+      } catch (err) {
+        console.error("Failed to fetch existing template names", err);
+        // 실패 시 전체 옵션 보여주거나, 오류 알려주기
+      }
+    })();
+    return () => { cancelled = true; };
+  }, []);
+
   const handleFileChange = (files: File[]) => {
     if (files.length > 0) {
       const file = files[0];
@@ -142,88 +170,71 @@ 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;
 
-  // 파일을 청크로 분할하여 업로드하는 함수
   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);
-      
-      try {
-        const response = await fetch('/api/upload/basicContract/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;
-        }
-      } catch (error) {
-        console.error(`청크 ${chunkIndex} 업로드 오류:`, error);
-        throw error;
+
+      const response = await fetch('/api/upload/basicContract/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;
       }
     }
   };
 
-  // 폼 제출 핸들러 (templateCode 제거)
   async function onSubmit(formData: TemplateFormValues) {
     setIsLoading(true);
     try {
-      if (!formData.file) {
-        throw new Error("파일이 선택되지 않았습니다.");
-      }
-      
-      // 고유 파일 ID 생성
-      const fileId = uuidv4();
-      
-      // 파일 청크 업로드
-      const uploadResult = await uploadFileInChunks(formData.file, fileId);
-      
-      if (!uploadResult.success) {
-        throw new Error("파일 업로드에 실패했습니다.");
+      let uploadResult = null;
+
+      if (formData.file) {
+        const fileId = uuidv4();
+        uploadResult = await uploadFileInChunks(formData.file, fileId);
+
+        if (!uploadResult?.success) {
+          throw new Error("파일 업로드에 실패했습니다.");
+        }
       }
-      
-      // 메타데이터 저장 (templateCode 제거됨)
+
       const saveResponse = await fetch('/api/upload/basicContract/complete', {
         method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
+        headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({
           templateName: formData.templateName,
-          revision: formData.revision,
+          revision: 1,
           legalReviewRequired: formData.legalReviewRequired,
           shipBuildingApplicable: formData.shipBuildingApplicable,
           windApplicable: formData.windApplicable,
@@ -233,35 +244,32 @@ export function AddTemplateDialog() {
           gyApplicable: formData.gyApplicable,
           sysApplicable: formData.sysApplicable,
           infraApplicable: formData.infraApplicable,
-          status: formData.status,
-          fileName: uploadResult.fileName,
-          filePath: uploadResult.filePath,
+          status: "ACTIVE",
+          fileName: uploadResult?.fileName || `${formData.templateName}_v1.docx`,
+          filePath: uploadResult?.filePath || "",
         }),
         next: { tags: ["basic-contract-templates"] },
       });
-      
+
       const saveResult = await saveResponse.json();
-      
       if (!saveResult.success) {
-        throw new Error("템플릿 정보 저장에 실패했습니다.");
+        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("템플릿 추가 중 오류가 발생했습니다.");
+      toast.error(error instanceof Error ? error.message : "템플릿 추가 중 오류가 발생했습니다.");
     } finally {
       setIsLoading(false);
     }
   }
 
-  // 모달이 닫힐 때 폼 초기화
   React.useEffect(() => {
     if (!open) {
       form.reset();
@@ -278,11 +286,17 @@ export function AddTemplateDialog() {
     setOpen(nextOpen);
   }
 
-  // 현재 선택된 적용 범위 수
-  const selectedScopesCount = BUSINESS_UNITS.filter(unit => 
+  const selectedScopesCount = BUSINESS_UNITS.filter(unit =>
     form.watch(unit.key as keyof TemplateFormValues)
   ).length;
 
+  const templateNameIsRequired = form.watch("templateName") !== "General GTC";
+
+  const isSubmitDisabled = isLoading ||
+    !form.watch("templateName") ||
+    (templateNameIsRequired && !form.watch("file")) ||
+    !BUSINESS_UNITS.some(unit => form.watch(unit.key as keyof TemplateFormValues));
+
   return (
     <Dialog open={open} onOpenChange={handleDialogOpenChange}>
       <DialogTrigger asChild>
@@ -291,16 +305,14 @@ export function AddTemplateDialog() {
         </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>
-            템플릿 정보를 입력하고 계약서 파일을 업로드하세요.
+            템플릿 정보를 입력하고 계약서 파일을 업로드하세요. (리비전은 자동으로 1로 설정됩니다)
             <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">
@@ -310,7 +322,7 @@ export function AddTemplateDialog() {
                   <CardTitle className="text-lg">기본 정보</CardTitle>
                 </CardHeader>
                 <CardContent className="space-y-4">
-                  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                  <div className="grid grid-cols-1 gap-4">
                     <FormField
                       control={form.control}
                       name="templateName"
@@ -319,14 +331,18 @@ export function AddTemplateDialog() {
                           <FormLabel>
                             템플릿 이름 <span className="text-red-500">*</span>
                           </FormLabel>
-                          <Select onValueChange={field.onChange} defaultValue={field.value}>
+                          <Select onValueChange={field.onChange} value={field.value}>
                             <FormControl>
-                              <SelectTrigger>
-                                <SelectValue placeholder="템플릿 이름을 선택하세요" />
+                              <SelectTrigger disabled={availableTemplateNames.length === 0}>
+                                <SelectValue placeholder={
+                                  availableTemplateNames.length === 0
+                                    ? "사용 가능한 템플릿이 없습니다"
+                                    : "템플릿 이름을 선택하세요"
+                                } />
                               </SelectTrigger>
                             </FormControl>
                             <SelectContent>
-                              {TEMPLATE_NAME_OPTIONS.map((option) => (
+                              {availableTemplateNames.map((option) => (
                                 <SelectItem key={option} value={option}>
                                   {option}
                                 </SelectItem>
@@ -334,33 +350,7 @@ export function AddTemplateDialog() {
                             </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)
-                            <br />
-                            <span className="text-xs text-muted-foreground">
-                              동일한 템플릿 이름의 리비전은 중복될 수 없습니다.
-                            </span>
+                            이미 등록되지 않은 템플릿만 표시됩니다. (리비전 1로 생성)
                           </FormDescription>
                           <FormMessage />
                         </FormItem>
@@ -412,9 +402,9 @@ export function AddTemplateDialog() {
                       전체 선택
                     </label>
                   </div>
-                  
+
                   <Separator />
-                  
+
                   <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
                     {BUSINESS_UNITS.map((unit) => (
                       <FormField
@@ -439,7 +429,7 @@ export function AddTemplateDialog() {
                       />
                     ))}
                   </div>
-                  
+
                   {form.formState.errors.shipBuildingApplicable && (
                     <p className="text-sm text-destructive">
                       {form.formState.errors.shipBuildingApplicable.message}
@@ -452,6 +442,11 @@ export function AddTemplateDialog() {
               <Card>
                 <CardHeader>
                   <CardTitle className="text-lg">파일 업로드</CardTitle>
+                  <CardDescription>
+                    {form.watch("templateName") === "General GTC" 
+                      ? "General GTC는 파일 업로드가 선택사항입니다"
+                      : "템플릿 파일을 업로드하세요"}
+                  </CardDescription>
                 </CardHeader>
                 <CardContent>
                   <FormField
@@ -460,7 +455,13 @@ export function AddTemplateDialog() {
                     render={() => (
                       <FormItem>
                         <FormLabel>
-                          계약서 파일 <span className="text-red-500">*</span>
+                          템플릿 파일 
+                          {form.watch("templateName") !== "General GTC" && (
+                            <span className="text-red-500"> *</span>
+                          )}
+                          {form.watch("templateName") === "General GTC" && (
+                            <span className="text-muted-foreground"> (선택사항)</span>
+                          )}
                         </FormLabel>
                         <FormControl>
                           <Dropzone
@@ -478,7 +479,9 @@ export function AddTemplateDialog() {
                               <DropzoneDescription>
                                 {selectedFile
                                   ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB`
-                                  : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"}
+                                  : form.watch("templateName") === "General GTC"
+                                    ? "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (선택사항, 최대 100MB)"
+                                    : "또는 클릭하여 워드 파일(.doc, .docx)을 선택하세요 (최대 100MB)"}
                               </DropzoneDescription>
                               <DropzoneInput />
                             </DropzoneZone>
@@ -488,7 +491,7 @@ export function AddTemplateDialog() {
                       </FormItem>
                     )}
                   />
-                  
+
                   {showProgress && (
                     <div className="space-y-2 mt-4">
                       <div className="flex justify-between text-sm">
@@ -504,7 +507,6 @@ export function AddTemplateDialog() {
           </Form>
         </div>
 
-        {/* 고정된 푸터 */}
         <DialogFooter className="p-6 pt-4 border-t">
           <Button
             type="button"
@@ -517,7 +519,7 @@ export function AddTemplateDialog() {
           <Button
             type="button"
             onClick={form.handleSubmit(onSubmit)}
-            disabled={isLoading || !form.formState.isValid}
+            disabled={isSubmitDisabled}
           >
             {isLoading ? "처리 중..." : "추가"}
           </Button>
@@ -525,4 +527,4 @@ export function AddTemplateDialog() {
       </DialogContent>
     </Dialog>
   );
-}
\ No newline at end of file
+}
-- 
cgit v1.2.3

