From ee57cc221ff2edafd3c0f12a181214c602ed257e Mon Sep 17 00:00:00 2001 From: dujinkim 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 = { 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 (
@@ -563,6 +597,11 @@ const handleDownloadAllFiles = async () => {
)} + + {/* 보안 정보 표시 (선택적) */} +
+

📁 허용 파일 크기: {securityInfo.maxFileSizeFormatted} | 남은 다운로드: {securityInfo.remainingDownloads}/분

+
@@ -583,27 +622,35 @@ const handleDownloadAllFiles = async () => {

일반 첨부파일

- {existingFiles.map((file) => ( - - - - - {file.fileName} - - {file.fileSize ? prettyBytes(file.fileSize) : '크기 정보 없음'} - - -
- handleDownloadFile(file)}> - - - handleDeleteExistingFile(file.id)}> - - -
-
-
- ))} + {existingFiles.map((file) => { + const fileInfo = getFileInfo(file.fileName); + return ( + + + + + + {fileInfo.icon} {file.fileName} + + + {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} + + +
+ handleDownloadFile(file)} + disabled={isDownloading} + > + {isDownloading ? : } + + handleDeleteExistingFile(file.id)}> + + +
+
+
+ ); + })}
@@ -614,27 +661,35 @@ const handleDownloadAllFiles = async () => {

신용평가 첨부파일

- {existingCreditFiles.map((file) => ( - - - - - {file.fileName} - - {file.fileSize ? prettyBytes(file.fileSize) : '크기 정보 없음'} - - -
- handleDownloadFile(file)}> - - - handleDeleteExistingFile(file.id)}> - - -
-
-
- ))} + {existingCreditFiles.map((file) => { + const fileInfo = getFileInfo(file.fileName); + return ( + + + + + + {fileInfo.icon} {file.fileName} + + + {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} + + +
+ handleDownloadFile(file)} + disabled={isDownloading} + > + {isDownloading ? : } + + handleDeleteExistingFile(file.id)}> + + +
+
+
+ ); + })}
@@ -645,27 +700,35 @@ const handleDownloadAllFiles = async () => {

현금흐름 첨부파일

- {existingCashFlowFiles.map((file) => ( - - - - - {file.fileName} - - {file.fileSize ? prettyBytes(file.fileSize) : '크기 정보 없음'} - - -
- handleDownloadFile(file)}> - - - handleDeleteExistingFile(file.id)}> - - -
-
-
- ))} + {existingCashFlowFiles.map((file) => { + const fileInfo = getFileInfo(file.fileName); + return ( + + + + + + {fileInfo.icon} {file.fileName} + + + {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} + + +
+ handleDownloadFile(file)} + disabled={isDownloading} + > + {isDownloading ? : } + + handleDeleteExistingFile(file.id)}> + + +
+
+
+ ); + })}
@@ -674,8 +737,16 @@ const handleDownloadAllFiles = async () => { {(existingFiles.length + existingCreditFiles.length + existingCashFlowFiles.length) > 1 && ( - )} -- cgit v1.2.3