From ee57cc221ff2edafd3c0f12a181214c602ed257e Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Tue, 22 Jul 2025 02:57:00 +0000
Subject: (대표님, 최겸) 이메일 템플릿, 벤더데이터 변경사항 대응, 기술영업
 변경요구사항 구현
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/additional-info/join-form.tsx | 311 +++++++++++++++++++------------
 1 file changed, 191 insertions(+), 120 deletions(-)

(limited to 'components/additional-info/join-form.tsx')

diff --git a/components/additional-info/join-form.tsx b/components/additional-info/join-form.tsx
index b6cb0d9c..da2ddac7 100644
--- a/components/additional-info/join-form.tsx
+++ b/components/additional-info/join-form.tsx
@@ -80,6 +80,17 @@ import {
   CardTitle,
 } from "@/components/ui/card"
 import { InformationButton } from "@/components/information/information-button"
+
+// 보안 파일 다운로드 유틸리티 import
+import { 
+  downloadFile, 
+  quickDownload, 
+  smartFileAction, 
+  getFileInfo, 
+  formatFileSize,
+  getSecurityInfo 
+} from "@/lib/file-download"
+
 i18nIsoCountries.registerLocale(enLocale)
 i18nIsoCountries.registerLocale(koLocale)
 
@@ -111,7 +122,6 @@ const cashFlowRatingScaleMap: Record<string, string[]> = {
   SCI: ["Level 1", "Level 2", "Level 3", "Level 4"],
 }
 
-
 const MAX_FILE_SIZE = 3e9
 
 // 파일 타입 정의
@@ -277,6 +287,7 @@ export function InfoForm() {
     fetchVendorData()
   }, [companyId, form, replaceContacts])
 
+  // 보안 다운로드 유틸리티를 사용한 개별 파일 다운로드
   const handleDownloadFile = async (file: AttachmentFile) => {
     try {
       setIsDownloading(true);
@@ -285,74 +296,94 @@ export function InfoForm() {
       const fileId = typeof file === 'object' ? file.id : file;
       const fileName = typeof file === 'object' ? file.fileName : `file-${fileId}`;
       
-      // 다운로드 링크 생성 (URL 인코딩 적용)
+      // API 엔드포인트 URL 구성
       const downloadUrl = `/api/vendors/attachments/download?id=${fileId}&vendorId=${Number(companyId)}`;
       
-      // a 태그를 사용한 다운로드
-      const downloadLink = document.createElement('a');
-      downloadLink.href = downloadUrl;
-      downloadLink.download = fileName;
-      downloadLink.target = '_blank'; // 추가: 새 탭에서 열도록 설정 (일부 브라우저에서 더 안정적)
-      document.body.appendChild(downloadLink);
-      downloadLink.click();
-      
-      // 정리 (메모리 누수 방지)
-      setTimeout(() => {
-        document.body.removeChild(downloadLink);
-      }, 100);
-      
-      toast({
-        title: "다운로드 시작",
-        description: "파일 다운로드가 시작되었습니다.",
+      // 보안 다운로드 유틸리티 사용
+      const result = await downloadFile(downloadUrl, fileName, {
+        action: 'download',
+        showToast: false, // 우리가 직접 토스트 관리
+        onSuccess: (fileName, fileSize) => {
+          const sizeText = fileSize ? ` (${formatFileSize(fileSize)})` : '';
+          toast({
+            title: "다운로드 완료",
+            description: `파일 다운로드가 완료되었습니다: ${fileName}${sizeText}`,
+          });
+        },
+        onError: (error) => {
+          console.error("Download error:", error);
+          toast({
+            variant: "destructive",
+            title: "다운로드 오류",
+            description: error || "파일 다운로드 중 오류가 발생했습니다.",
+          });
+        }
       });
+
+      if (!result.success && result.error) {
+        // 오류 처리는 onError 콜백에서 이미 처리됨
+        console.error("Download failed:", result.error);
+      }
+      
     } catch (error) {
       console.error("Error downloading file:", error);
       toast({
         variant: "destructive",
         title: "다운로드 오류",
-        description: "파일 다운로드 중 오류가 발생했습니다.",
+        description: "파일 다운로드 중 예상치 못한 오류가 발생했습니다.",
       });
     } finally {
       setIsDownloading(false);
     }
   };
   
-  // 전체 파일 다운로드 함수
-const handleDownloadAllFiles = async () => {
-  try {
-    setIsDownloading(true);
-
-    // 다운로드 URL 생성
-    const downloadUrl = `/api/vendors/attachments/download-all?vendorId=${Number(companyId)}`;
-    
-    // a 태그를 사용한 다운로드
-    const downloadLink = document.createElement('a');
-    downloadLink.href = downloadUrl;
-    downloadLink.download = `vendor-${companyId}-files.zip`;
-    downloadLink.target = '_blank';
-    document.body.appendChild(downloadLink);
-    downloadLink.click();
-    
-    // 정리
-    setTimeout(() => {
-      document.body.removeChild(downloadLink);
-    }, 100);
-    
-    toast({
-      title: "다운로드 시작",
-      description: "전체 파일 다운로드가 시작되었습니다.",
-    });
-  } catch (error) {
-    console.error("Error downloading files:", error);
-    toast({
-      variant: "destructive",
-      title: "다운로드 오류",
-      description: "파일 다운로드 중 오류가 발생했습니다.",
-    });
-  } finally {
-    setIsDownloading(false);
-  }
-};
+  // 보안 다운로드 유틸리티를 사용한 전체 파일 다운로드
+  const handleDownloadAllFiles = async () => {
+    try {
+      setIsDownloading(true);
+
+      // 전체 파일 다운로드 API 엔드포인트
+      const downloadUrl = `/api/vendors/attachments/download-all?vendorId=${Number(companyId)}`;
+      const fileName = `vendor-${companyId}-files.zip`;
+      
+      // 보안 다운로드 유틸리티 사용
+      const result = await downloadFile(downloadUrl, fileName, {
+        action: 'download',
+        showToast: false, // 우리가 직접 토스트 관리
+        onSuccess: (fileName, fileSize) => {
+          const sizeText = fileSize ? ` (${formatFileSize(fileSize)})` : '';
+          toast({
+            title: "전체 다운로드 완료",
+            description: `전체 파일 다운로드가 완료되었습니다: ${fileName}${sizeText}`,
+          });
+        },
+        onError: (error) => {
+          console.error("Download all error:", error);
+          toast({
+            variant: "destructive",
+            title: "다운로드 오류",
+            description: error || "전체 파일 다운로드 중 오류가 발생했습니다.",
+          });
+        }
+      });
+
+      if (!result.success && result.error) {
+        // 오류 처리는 onError 콜백에서 이미 처리됨
+        console.error("Download all failed:", result.error);
+      }
+      
+    } catch (error) {
+      console.error("Error downloading files:", error);
+      toast({
+        variant: "destructive",
+        title: "다운로드 오류",
+        description: "전체 파일 다운로드 중 예상치 못한 오류가 발생했습니다.",
+      });
+    } finally {
+      setIsDownloading(false);
+    }
+  };
+
   // Dropzone handlers
   const handleDropAccepted = (acceptedFiles: File[]) => {
     const newFiles = [...selectedFiles, ...acceptedFiles]
@@ -529,6 +560,9 @@ const handleDownloadAllFiles = async () => {
     )
   }
 
+  // 보안 정보 가져오기 (선택적으로 사용자에게 표시)
+  const securityInfo = getSecurityInfo();
+
   // Render
   return (
     <div className="container py-6">
@@ -563,6 +597,11 @@ const handleDownloadAllFiles = async () => {
                 </Badge>
               </div>
             )}
+
+            {/* 보안 정보 표시 (선택적) */}
+            <div className="text-xs text-muted-foreground">
+              <p>📁 허용 파일 크기: {securityInfo.maxFileSizeFormatted} | 남은 다운로드: {securityInfo.remainingDownloads}/분</p>
+            </div>
           </div>
 
           <Separator />
@@ -583,27 +622,35 @@ const handleDownloadAllFiles = async () => {
                       <h4 className="font-medium mb-2">일반 첨부파일</h4>
                       <ScrollArea className="h-32">
                         <FileList className="gap-2">
-                          {existingFiles.map((file) => (
-                            <FileListItem key={file.id}>
-                              <FileListHeader>
-                                <FileListIcon />
-                                <FileListInfo>
-                                  <FileListName>{file.fileName}</FileListName>
-                                  <FileListDescription>
-                                    {file.fileSize ? prettyBytes(file.fileSize) : '크기 정보 없음'}
-                                  </FileListDescription>
-                                </FileListInfo>
-                                <div className="flex items-center space-x-2">
-                                  <FileListAction onClick={() => handleDownloadFile(file)}>
-                                    <Download className="h-4 w-4" />
-                                  </FileListAction>
-                                  <FileListAction onClick={() => handleDeleteExistingFile(file.id)}>
-                                    <X className="h-4 w-4" />
-                                  </FileListAction>
-                                </div>
-                              </FileListHeader>
-                            </FileListItem>
-                          ))}
+                          {existingFiles.map((file) => {
+                            const fileInfo = getFileInfo(file.fileName);
+                            return (
+                              <FileListItem key={file.id}>
+                                <FileListHeader>
+                                  <FileListIcon />
+                                  <FileListInfo>
+                                    <FileListName>
+                                      {fileInfo.icon} {file.fileName}
+                                    </FileListName>
+                                    <FileListDescription>
+                                      {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
+                                    </FileListDescription>
+                                  </FileListInfo>
+                                  <div className="flex items-center space-x-2">
+                                    <FileListAction 
+                                      onClick={() => handleDownloadFile(file)}
+                                      disabled={isDownloading}
+                                    >
+                                      {isDownloading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4" />}
+                                    </FileListAction>
+                                    <FileListAction onClick={() => handleDeleteExistingFile(file.id)}>
+                                      <X className="h-4 w-4" />
+                                    </FileListAction>
+                                  </div>
+                                </FileListHeader>
+                              </FileListItem>
+                            );
+                          })}
                         </FileList>
                       </ScrollArea>
                     </div>
@@ -614,27 +661,35 @@ const handleDownloadAllFiles = async () => {
                       <h4 className="font-medium mb-2">신용평가 첨부파일</h4>
                       <ScrollArea className="h-32">
                         <FileList className="gap-2">
-                          {existingCreditFiles.map((file) => (
-                            <FileListItem key={file.id}>
-                              <FileListHeader>
-                                <FileListIcon />
-                                <FileListInfo>
-                                  <FileListName>{file.fileName}</FileListName>
-                                  <FileListDescription>
-                                    {file.fileSize ? prettyBytes(file.fileSize) : '크기 정보 없음'}
-                                  </FileListDescription>
-                                </FileListInfo>
-                                <div className="flex items-center space-x-2">
-                                  <FileListAction onClick={() => handleDownloadFile(file)}>
-                                    <Download className="h-4 w-4" />
-                                  </FileListAction>
-                                  <FileListAction onClick={() => handleDeleteExistingFile(file.id)}>
-                                    <X className="h-4 w-4" />
-                                  </FileListAction>
-                                </div>
-                              </FileListHeader>
-                            </FileListItem>
-                          ))}
+                          {existingCreditFiles.map((file) => {
+                            const fileInfo = getFileInfo(file.fileName);
+                            return (
+                              <FileListItem key={file.id}>
+                                <FileListHeader>
+                                  <FileListIcon />
+                                  <FileListInfo>
+                                    <FileListName>
+                                      {fileInfo.icon} {file.fileName}
+                                    </FileListName>
+                                    <FileListDescription>
+                                      {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
+                                    </FileListDescription>
+                                  </FileListInfo>
+                                  <div className="flex items-center space-x-2">
+                                    <FileListAction 
+                                      onClick={() => handleDownloadFile(file)}
+                                      disabled={isDownloading}
+                                    >
+                                      {isDownloading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4" />}
+                                    </FileListAction>
+                                    <FileListAction onClick={() => handleDeleteExistingFile(file.id)}>
+                                      <X className="h-4 w-4" />
+                                    </FileListAction>
+                                  </div>
+                                </FileListHeader>
+                              </FileListItem>
+                            );
+                          })}
                         </FileList>
                       </ScrollArea>
                     </div>
@@ -645,27 +700,35 @@ const handleDownloadAllFiles = async () => {
                       <h4 className="font-medium mb-2">현금흐름 첨부파일</h4>
                       <ScrollArea className="h-32">
                         <FileList className="gap-2">
-                          {existingCashFlowFiles.map((file) => (
-                            <FileListItem key={file.id}>
-                              <FileListHeader>
-                                <FileListIcon />
-                                <FileListInfo>
-                                  <FileListName>{file.fileName}</FileListName>
-                                  <FileListDescription>
-                                    {file.fileSize ? prettyBytes(file.fileSize) : '크기 정보 없음'}
-                                  </FileListDescription>
-                                </FileListInfo>
-                                <div className="flex items-center space-x-2">
-                                  <FileListAction onClick={() => handleDownloadFile(file)}>
-                                    <Download className="h-4 w-4" />
-                                  </FileListAction>
-                                  <FileListAction onClick={() => handleDeleteExistingFile(file.id)}>
-                                    <X className="h-4 w-4" />
-                                  </FileListAction>
-                                </div>
-                              </FileListHeader>
-                            </FileListItem>
-                          ))}
+                          {existingCashFlowFiles.map((file) => {
+                            const fileInfo = getFileInfo(file.fileName);
+                            return (
+                              <FileListItem key={file.id}>
+                                <FileListHeader>
+                                  <FileListIcon />
+                                  <FileListInfo>
+                                    <FileListName>
+                                      {fileInfo.icon} {file.fileName}
+                                    </FileListName>
+                                    <FileListDescription>
+                                      {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'}
+                                    </FileListDescription>
+                                  </FileListInfo>
+                                  <div className="flex items-center space-x-2">
+                                    <FileListAction 
+                                      onClick={() => handleDownloadFile(file)}
+                                      disabled={isDownloading}
+                                    >
+                                      {isDownloading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4" />}
+                                    </FileListAction>
+                                    <FileListAction onClick={() => handleDeleteExistingFile(file.id)}>
+                                      <X className="h-4 w-4" />
+                                    </FileListAction>
+                                  </div>
+                                </FileListHeader>
+                              </FileListItem>
+                            );
+                          })}
                         </FileList>
                       </ScrollArea>
                     </div>
@@ -674,8 +737,16 @@ const handleDownloadAllFiles = async () => {
               </CardContent>
               <CardFooter>
                 {(existingFiles.length + existingCreditFiles.length + existingCashFlowFiles.length) > 1 && (
-                  <Button variant="outline" onClick={handleDownloadAllFiles}>
-                    <Download className="mr-2 h-4 w-4" />
+                  <Button 
+                    variant="outline" 
+                    onClick={handleDownloadAllFiles}
+                    disabled={isDownloading}
+                  >
+                    {isDownloading ? (
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                    ) : (
+                      <Download className="mr-2 h-4 w-4" />
+                    )}
                     전체 다운로드
                   </Button>
                 )}
-- 
cgit v1.2.3

