diff options
Diffstat (limited to 'components/additional-info')
| -rw-r--r-- | components/additional-info/join-form.tsx | 311 | ||||
| -rw-r--r-- | components/additional-info/tech-vendor-info-form.tsx | 188 |
2 files changed, 318 insertions, 181 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> )} diff --git a/components/additional-info/tech-vendor-info-form.tsx b/components/additional-info/tech-vendor-info-form.tsx index 55d01d21..02358a45 100644 --- a/components/additional-info/tech-vendor-info-form.tsx +++ b/components/additional-info/tech-vendor-info-form.tsx @@ -31,6 +31,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"
+
// 타입 정의
interface TechVendorContact {
id: number
@@ -46,6 +57,7 @@ interface TechVendorAttachment { fileName: string
filePath: string
attachmentType: string
+ fileSize?: number
createdAt: Date
updatedAt: Date
}
@@ -133,69 +145,96 @@ export function TechVendorInfoForm() { fetchTechVendorData()
}, [techCompanyId, form])
+ // 보안 다운로드 유틸리티를 사용한 개별 파일 다운로드
const handleDownloadFile = async (file: TechVendorAttachment) => {
try {
setIsDownloading(true)
+ // API 엔드포인트 URL 구성
const downloadUrl = `/api/tech-vendors/attachments/download?id=${file.id}&vendorId=${Number(techCompanyId)}`
- const downloadLink = document.createElement('a')
- downloadLink.href = downloadUrl
- downloadLink.download = file.fileName
- downloadLink.target = '_blank'
- document.body.appendChild(downloadLink)
- downloadLink.click()
-
- setTimeout(() => {
- document.body.removeChild(downloadLink)
- }, 100)
+ // 보안 다운로드 유틸리티 사용
+ const result = await downloadFile(downloadUrl, file.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);
+ }
- toast({
- title: "다운로드 시작",
- description: "파일 다운로드가 시작되었습니다.",
- })
} catch (error) {
- console.error("Error downloading file:", error)
+ console.error("Error downloading file:", error);
toast({
variant: "destructive",
title: "다운로드 오류",
- description: "파일 다운로드 중 오류가 발생했습니다.",
- })
+ description: "파일 다운로드 중 예상치 못한 오류가 발생했습니다.",
+ });
} finally {
- setIsDownloading(false)
+ setIsDownloading(false);
}
}
+ // 보안 다운로드 유틸리티를 사용한 전체 파일 다운로드
const handleDownloadAllFiles = async () => {
try {
setIsDownloading(true)
+ // 전체 파일 다운로드 API 엔드포인트
const downloadUrl = `/api/tech-vendors/attachments/download-all?vendorId=${Number(techCompanyId)}`
+ const fileName = `tech-vendor-${techCompanyId}-files.zip`
- const downloadLink = document.createElement('a')
- downloadLink.href = downloadUrl
- downloadLink.download = `tech-vendor-${techCompanyId}-files.zip`
- downloadLink.target = '_blank'
- document.body.appendChild(downloadLink)
- downloadLink.click()
-
- setTimeout(() => {
- document.body.removeChild(downloadLink)
- }, 100)
+ // 보안 다운로드 유틸리티 사용
+ 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);
+ }
- toast({
- title: "다운로드 시작",
- description: "전체 파일 다운로드가 시작되었습니다.",
- })
} catch (error) {
- console.error("Error downloading files:", error)
+ console.error("Error downloading files:", error);
toast({
variant: "destructive",
title: "다운로드 오류",
- description: "파일 다운로드 중 오류가 발생했습니다.",
- })
+ description: "전체 파일 다운로드 중 예상치 못한 오류가 발생했습니다.",
+ });
} finally {
- setIsDownloading(false)
+ setIsDownloading(false);
}
}
@@ -247,6 +286,9 @@ export function TechVendorInfoForm() { )
}
+ // 보안 정보 가져오기
+ const securityInfo = getSecurityInfo();
+
return (
<div className="space-y-6 p-6">
<div className="flex items-center justify-between">
@@ -256,6 +298,11 @@ export function TechVendorInfoForm() { <InformationButton pagePath="partners/info" />
</div>
<p className="text-gray-600">기술영업 벤더 정보를 확인하고 업데이트할 수 있습니다.</p>
+
+ {/* 보안 정보 표시 */}
+ <div className="text-xs text-muted-foreground mt-1">
+ <p>📁 허용 파일 크기: {securityInfo.maxFileSizeFormatted} | 남은 다운로드: {securityInfo.remainingDownloads}/분</p>
+ </div>
</div>
{attachments.length > 0 && (
<Button
@@ -263,7 +310,11 @@ export function TechVendorInfoForm() { onClick={handleDownloadAllFiles}
disabled={isDownloading}
>
- <Download className="h-4 w-4 mr-2" />
+ {isDownloading ? (
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
+ ) : (
+ <Download className="h-4 w-4 mr-2" />
+ )}
{isDownloading ? "다운로드 중..." : "전체 파일 다운로드"}
</Button>
)}
@@ -395,7 +446,6 @@ export function TechVendorInfoForm() { </CardContent>
</Card>
-
{/* 연락처 정보 */}
{contacts.length > 0 && (
<Card>
@@ -455,7 +505,7 @@ export function TechVendorInfoForm() { </Card>
)}
- {/* 첨부파일 정보 */}
+ {/* 첨부파일 정보 - 보안 강화된 버전 */}
{attachments.length > 0 && (
<Card>
<CardHeader>
@@ -466,29 +516,45 @@ export function TechVendorInfoForm() { </CardHeader>
<CardContent>
<div className="space-y-2">
- {attachments.map((file) => (
- <div
- key={file.id}
- className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
- >
- <div className="flex-1">
- <h4 className="font-medium">{file.fileName}</h4>
- <div className="flex items-center space-x-2 text-sm text-gray-600">
- <span>{file.attachmentType}</span>
- <span>•</span>
- <span>{new Date(file.createdAt).toLocaleDateString()}</span>
+ {attachments.map((file) => {
+ const fileInfo = getFileInfo(file.fileName);
+ return (
+ <div
+ key={file.id}
+ className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
+ >
+ <div className="flex-1">
+ <div className="flex items-center space-x-2">
+ <span className="text-lg">{fileInfo.icon}</span>
+ <h4 className="font-medium">{file.fileName}</h4>
+ </div>
+ <div className="flex items-center space-x-2 text-sm text-gray-600 mt-1">
+ <span>{file.attachmentType}</span>
+ <span>•</span>
+ <span>{new Date(file.createdAt).toLocaleDateString()}</span>
+ {file.fileSize && (
+ <>
+ <span>•</span>
+ <span>{formatFileSize(file.fileSize)}</span>
+ </>
+ )}
+ </div>
</div>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDownloadFile(file)}
+ disabled={isDownloading}
+ >
+ {isDownloading ? (
+ <Loader2 className="h-4 w-4 animate-spin" />
+ ) : (
+ <Download className="h-4 w-4" />
+ )}
+ </Button>
</div>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleDownloadFile(file)}
- disabled={isDownloading}
- >
- <Download className="h-4 w-4" />
- </Button>
- </div>
- ))}
+ );
+ })}
</div>
</CardContent>
</Card>
@@ -513,4 +579,4 @@ export function TechVendorInfoForm() { </Form>
</div>
)
-}
\ No newline at end of file +}
\ No newline at end of file |
