summaryrefslogtreecommitdiff
path: root/components/additional-info/join-form.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/additional-info/join-form.tsx')
-rw-r--r--components/additional-info/join-form.tsx311
1 files changed, 191 insertions, 120 deletions
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>
)}