From 99ca52106b9a7beeb8d31aeb333b4084648007c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 1 Sep 2025 10:06:20 +0000 Subject: (최겸) 서명 미리보기 추가, 기술영업 아이템코드 수정 기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/additional-info/join-form.tsx | 594 ++++++++++++++++++------------- 1 file changed, 348 insertions(+), 246 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 220547df..8dca4b61 100644 --- a/components/additional-info/join-form.tsx +++ b/components/additional-info/join-form.tsx @@ -126,6 +126,7 @@ const cashFlowRatingScaleMap: Record = { } const MAX_FILE_SIZE = 3e9 +const IMAGE_FILE_SIZE = 5e6 // 5MB // 첨부파일 타입 정의 const ATTACHMENT_TYPES = [ @@ -183,6 +184,7 @@ export function InfoForm() { // 서명/직인 업로드 관련 상태 const [signatureFiles, setSignatureFiles] = React.useState([]) const [hasSignature, setHasSignature] = React.useState(false) + const [signaturePreviewUrl, setSignaturePreviewUrl] = React.useState(null) // React Hook Form const form = useForm({ @@ -280,6 +282,21 @@ export function InfoForm() { setExistingCreditFiles(creditFiles) setExistingCashFlowFiles(cashFlowFiles) setExistingSignatureFiles(signatureFiles) // 서명/직인 파일들 + + // 기존 서명/직인 파일이 이미지인 경우 미리보기 URL 생성 + if (signatureFiles.length > 0) { + const signatureFile = signatureFiles[0] + const fileName = signatureFile.fileName.toLowerCase() + const isImage = fileName.includes('.jpg') || fileName.includes('.jpeg') || + fileName.includes('.png') || fileName.includes('.gif') || + fileName.includes('.webp') + + if (isImage) { + // 실제 파일 경로 사용 (DB에 저장된 filePath) + // filePath는 이미 /vendors/{vendorId}/{hashedFileName} 형태 + setSignaturePreviewUrl(signatureFile.filePath) + } + } } // 폼 기본값 설정 (연락처 포함) @@ -352,6 +369,15 @@ export function InfoForm() { fetchVendorData() }, [companyId, form, replaceContacts]) + // 컴포넌트 언마운트 시 미리보기 URL 정리 (blob URL만) + React.useEffect(() => { + return () => { + if (signaturePreviewUrl && signaturePreviewUrl.startsWith('blob:')) { + URL.revokeObjectURL(signaturePreviewUrl) + } + } + }, [signaturePreviewUrl]) + // 보안 다운로드 유틸리티를 사용한 개별 파일 다운로드 const handleDownloadFile = async (file: AttachmentFile) => { try { @@ -529,6 +555,13 @@ export function InfoForm() { // 삭제할 ID 목록에 추가 setFilesToDelete([...filesToDelete, fileId]) + // 서명/직인 파일을 삭제하는 경우 미리보기도 정리 + const isSignatureFile = existingSignatureFiles.some(file => file.id === fileId) + if (isSignatureFile && signaturePreviewUrl) { + // 서버 URL인 경우 revokeObjectURL을 호출하지 않음 (blob URL이 아니므로) + setSignaturePreviewUrl(null) + } + // UI에서 제거 setExistingFiles(existingFiles.filter(file => file.id !== fileId)) setExistingCreditFiles(existingCreditFiles.filter(file => file.id !== fileId)) @@ -543,6 +576,12 @@ export function InfoForm() { + // 이미지 파일 검증 함수 (서명용) + const isImageFile = (file: File): boolean => { + const imageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] + return imageTypes.includes(file.type) + } + // 서명/직인 업로드 핸들러들 (한 개만 허용) const handleSignatureDropAccepted = (acceptedFiles: File[]) => { // 첫 번째 파일만 사용 (한 개만 허용) @@ -560,9 +599,22 @@ export function InfoForm() { }) } + // 기존 미리보기 URL 정리 (blob URL만) + if (signaturePreviewUrl && signaturePreviewUrl.startsWith('blob:')) { + URL.revokeObjectURL(signaturePreviewUrl) + } + setSignatureFiles([newFile]) // 새 파일 설정 setHasSignature(true) + // 이미지 파일인 경우 미리보기 생성 + if (isImageFile(newFile)) { + const previewUrl = URL.createObjectURL(newFile) + setSignaturePreviewUrl(previewUrl) + } else { + setSignaturePreviewUrl(null) + } + if (acceptedFiles.length > 1) { toast({ title: "파일 제한", @@ -585,6 +637,10 @@ export function InfoForm() { const removeSignatureFile = () => { setSignatureFiles([]) setHasSignature(false) + if (signaturePreviewUrl && signaturePreviewUrl.startsWith('blob:')) { + URL.revokeObjectURL(signaturePreviewUrl) + } + setSignaturePreviewUrl(null) } // 파일 타입 라벨 가져오기 @@ -755,112 +811,306 @@ export function InfoForm() { - {/* 서명/직인 등록 섹션 - 독립적으로 사용 가능 */} - - - - - 회사 서명/직인 등록 (선택사항) - - - 회사의 공식 서명이나 직인을 등록하여 계약서 및 공식 문서에 사용할 수 있습니다. - - - - {/* 현재 등록된 서명/직인 파일 표시 (한 개만) */} - {(existingSignatureFiles.length > 0 || signatureFiles.length > 0) && ( -
-
- - 서명/직인 등록됨 -
- - {/* 기존 등록된 서명/직인 (첫 번째만 표시) */} - {existingSignatureFiles.length > 0 && signatureFiles.length === 0 && ( -
- {(() => { - const file = existingSignatureFiles[0]; - const fileInfo = getFileInfo(file.fileName); - return ( -
- -
-
{fileInfo.icon} {file.fileName}
-
- {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} + {/* 서명/직인 등록과 첨부파일 요약을 하나의 행으로 배치 */} +
+ {/* 서명/직인 등록 섹션 */} + + + + + 회사 서명/직인 등록 (선택사항) + + + 회사의 공식 서명이나 직인을 등록하여 계약서 및 공식 문서에 사용할 수 있습니다. + + + + {/* 현재 등록된 서명/직인 파일 표시 (한 개만) */} + {(existingSignatureFiles.length > 0 || signatureFiles.length > 0) && ( +
+ {/* 기존 등록된 서명/직인 (첫 번째만 표시) */} + {existingSignatureFiles.length > 0 && signatureFiles.length === 0 && ( +
+
+ {(() => { + const file = existingSignatureFiles[0]; + const fileInfo = getFileInfo(file.fileName); + return ( +
+ +
+
{fileInfo.icon} {file.fileName}
+
+ {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} +
+
+
+ handleDownloadFile(file)} + disabled={isDownloading} + > + {isDownloading ? : } + + handleDeleteExistingFile(file.id)}> + + +
-
-
- handleDownloadFile(file)} - disabled={isDownloading} - > - {isDownloading ? : } - - handleDeleteExistingFile(file.id)}> - - + ); + })()} +
+ + {/* 기존 서명 미리보기 (이미지인 경우만) */} + {signaturePreviewUrl && !signatureFiles.length && ( +
+
등록된 서명/직인
+
+ 등록된 서명/직인 { + console.error("기존 서명 이미지 로드 실패") + setSignaturePreviewUrl(null) + }} + // 추가 보안: referrer policy 설정 + referrerPolicy="no-referrer" + />
- ); - })()} -
- )} - - {/* 새로 업로드된 서명/직인 */} - {signatureFiles.length > 0 && ( -
- {(() => { - const file = signatureFiles[0]; - return ( -
- -
-
{file.name}
-
- 서명/직인 (새 파일) | {prettyBytes(file.size)} + )} +
+ )} + + {/* 새로 업로드된 서명/직인 */} + {signatureFiles.length > 0 && ( +
+
+ {(() => { + const file = signatureFiles[0]; + return ( +
+ +
+
{file.name}
+
+ 서명/직인 (새 파일) | {prettyBytes(file.size)} +
+
+ + +
+ ); + })()} +
+ + {/* 서명 미리보기 (이미지인 경우만) */} + {signaturePreviewUrl && ( +
+
미리보기
+
+ 서명/직인 미리보기 { + toast({ + variant: "destructive", + title: "미리보기 오류", + description: "이미지를 불러올 수 없습니다.", + }) + setSignaturePreviewUrl(null) + }} + // 추가 보안: referrer policy 설정 + referrerPolicy="no-referrer" + />
- - -
- ); - })()} -
- )} -
- )} - - {/* 서명/직인 업로드 드롭존 */} - - {({ maxSize }) => ( - - -
- -
- - {existingSignatureFiles.length > 0 || signatureFiles.length > 0 - ? "서명/직인 교체" - : "서명/직인 업로드" - } - - - 한 개 파일만 업로드 가능 {maxSize ? ` | 최대: ${prettyBytes(maxSize)}` : ""} - + )}
-
-
+ )} +
)} - - - + + {/* 서명/직인 업로드 드롭존 */} + + {({ maxSize }) => ( + + +
+ +
+ + {existingSignatureFiles.length > 0 || signatureFiles.length > 0 + ? "서명/직인 교체" + : "서명/직인 업로드" + } + + + 한 개 파일만 업로드 가능 {maxSize ? ` | 최대: ${prettyBytes(maxSize)}` : ""} + +
+
+
+ )} +
+ + + + {/* 첨부파일 요약 카드 - 기존 파일 있는 경우만 표시 */} + {(existingFiles.length > 0 || existingCreditFiles.length > 0 || existingCashFlowFiles.length > 0) && ( + + + 첨부파일 요약 + + 현재 등록된 파일 목록입니다. 전체 다운로드하거나 개별 파일을 다운로드할 수 있습니다. + + + +
+ {existingFiles.length > 0 && ( +
+

첨부파일

+ + + {existingFiles.map((file) => { + const fileInfo = getFileInfo(file.fileName); + return ( + + + + + + {fileInfo.icon} {file.fileName} + + + {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} + + +
+ handleDownloadFile(file)} + disabled={isDownloading} + > + {isDownloading ? : } + + handleDeleteExistingFile(file.id)}> + + +
+
+
+ ); + })} +
+
+
+ )} + + {existingCreditFiles.length > 0 && ( +
+

신용평가 첨부파일

+ + + {existingCreditFiles.map((file) => { + const fileInfo = getFileInfo(file.fileName); + return ( + + + + + + {fileInfo.icon} {file.fileName} + + + {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} + + +
+ handleDownloadFile(file)} + disabled={isDownloading} + > + {isDownloading ? : } + + handleDeleteExistingFile(file.id)}> + + +
+
+
+ ); + })} +
+
+
+ )} + + {existingCashFlowFiles.length > 0 && ( +
+

현금흐름 첨부파일

+ + + {existingCashFlowFiles.map((file) => { + const fileInfo = getFileInfo(file.fileName); + return ( + + + + + + {fileInfo.icon} {file.fileName} + + + {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} + + +
+ handleDownloadFile(file)} + disabled={isDownloading} + > + {isDownloading ? : } + + handleDeleteExistingFile(file.id)}> + + +
+
+
+ ); + })} +
+
+
+ )} +
+
+ + {(existingFiles.length + existingCreditFiles.length + existingCashFlowFiles.length) > 1 && ( + + )} + +
+ )} +
{/* 정규업체 등록 현황 섹션 */} {registrationData ? ( @@ -930,154 +1180,6 @@ export function InfoForm() { )} - {/* 첨부파일 요약 카드 - 기존 파일 있는 경우만 표시 */} - {(existingFiles.length > 0 || existingCreditFiles.length > 0 || existingCashFlowFiles.length > 0) && ( - - - 첨부파일 요약 - - 현재 등록된 파일 목록입니다. 전체 다운로드하거나 개별 파일을 다운로드할 수 있습니다. - - - -
- {existingFiles.length > 0 && ( -
-

첨부파일

- - - {existingFiles.map((file) => { - const fileInfo = getFileInfo(file.fileName); - return ( - - - - - - {fileInfo.icon} {file.fileName} - - - {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} - - -
- handleDownloadFile(file)} - disabled={isDownloading} - > - {isDownloading ? : } - - handleDeleteExistingFile(file.id)}> - - -
-
-
- ); - })} -
-
-
- )} - - {existingCreditFiles.length > 0 && ( -
-

신용평가 첨부파일

- - - {existingCreditFiles.map((file) => { - const fileInfo = getFileInfo(file.fileName); - return ( - - - - - - {fileInfo.icon} {file.fileName} - - - {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} - - -
- handleDownloadFile(file)} - disabled={isDownloading} - > - {isDownloading ? : } - - handleDeleteExistingFile(file.id)}> - - -
-
-
- ); - })} -
-
-
- )} - - {existingCashFlowFiles.length > 0 && ( -
-

현금흐름 첨부파일

- - - {existingCashFlowFiles.map((file) => { - const fileInfo = getFileInfo(file.fileName); - return ( - - - - - - {fileInfo.icon} {file.fileName} - - - {getAttachmentTypeLabel(file.attachmentType)} | {file.fileSize ? formatFileSize(file.fileSize) : '크기 정보 없음'} - - -
- handleDownloadFile(file)} - disabled={isDownloading} - > - {isDownloading ? : } - - handleDeleteExistingFile(file.id)}> - - -
-
-
- ); - })} -
-
-
- )} -
-
- - {(existingFiles.length + existingCreditFiles.length + existingCashFlowFiles.length) > 1 && ( - - )} - -
- )} -
-- cgit v1.2.3