diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/dolce/actions.ts | 14 | ||||
| -rw-r--r-- | lib/dolce/components/file-upload-progress-list.tsx | 2 | ||||
| -rw-r--r-- | lib/dolce/dialogs/add-detail-drawing-dialog.tsx | 2 | ||||
| -rw-r--r-- | lib/dolce/dialogs/b4-bulk-upload-dialog.tsx | 172 | ||||
| -rw-r--r-- | lib/dolce/dialogs/upload-files-to-detail-dialog.tsx | 2 | ||||
| -rw-r--r-- | lib/dolce/hooks/use-file-upload.ts | 107 | ||||
| -rw-r--r-- | lib/dolce/utils/upload-with-progress.ts | 74 |
7 files changed, 201 insertions, 172 deletions
diff --git a/lib/dolce/actions.ts b/lib/dolce/actions.ts index a9cda76a..77de430f 100644 --- a/lib/dolce/actions.ts +++ b/lib/dolce/actions.ts @@ -275,6 +275,10 @@ export async function fetchFileInfoList(uploadId: string): Promise<FileInfoItem[ /** * 4. 상세도면 추가/수정 + * + * 참고: DetailDwgReceiptMmgtEditResult는 실제 성공 건수를 정확히 반영하지 않음 + * (1개 추가되어도 0을 반환하는 경우 있음) + * API 호출이 성공하면 요청한 건수가 처리된 것으로 간주 */ export async function editDetailDwgReceipt(params: { dwgList: DetailDwgEditRequest[]; @@ -294,7 +298,15 @@ export async function editDetailDwgReceipt(params: { EMAIL: params.email, }); - return response.DetailDwgReceiptMmgtEditResult; + // 응답값이 신뢰할 수 없으므로 로그만 남김 + if (response.DetailDwgReceiptMmgtEditResult !== params.dwgList.length) { + console.warn( + `[DOLCE API] DetailDwgReceiptMmgtEditResult 불일치: 요청=${params.dwgList.length}, 응답=${response.DetailDwgReceiptMmgtEditResult}` + ); + } + + // API 호출 성공 시 요청한 건수 반환 (응답값 무시) + return params.dwgList.length; } catch (error) { console.error("상세도면 수정 실패:", error); throw error; diff --git a/lib/dolce/components/file-upload-progress-list.tsx b/lib/dolce/components/file-upload-progress-list.tsx index e016402d..d54e9eaa 100644 --- a/lib/dolce/components/file-upload-progress-list.tsx +++ b/lib/dolce/components/file-upload-progress-list.tsx @@ -18,7 +18,7 @@ export function FileUploadProgressList({ fileProgresses }: FileUploadProgressLis <h4 className="text-sm font-medium"> 파일 업로드 진행 상황 ({fileProgresses.length}개) </h4> - <div className="max-h-64 overflow-auto space-y-2"> + <div className="max-h-64 overflow-y-auto space-y-2"> {fileProgresses.map((fileProgress, index) => ( <FileUploadProgressItem key={index} fileProgress={fileProgress} /> ))} diff --git a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx b/lib/dolce/dialogs/add-detail-drawing-dialog.tsx index 34d06368..2f7f15a7 100644 --- a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx +++ b/lib/dolce/dialogs/add-detail-drawing-dialog.tsx @@ -366,7 +366,7 @@ export function AddDetailDrawingDialog({ 전체 제거 </Button> </div> - <div className="max-h-48 overflow-auto space-y-2"> + <div className="max-h-60 overflow-y-auto space-y-2"> {files.map((file, index) => ( <div key={index} diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx index f4816328..cd336e92 100644 --- a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx +++ b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx @@ -29,10 +29,11 @@ import { } from "./b4-upload-validation-dialog"; import { checkB4MappingStatus, - bulkUploadB4Files, + editDetailDwgReceipt, type MappingCheckItem, type B4BulkUploadResult, } from "../actions"; +import { v4 as uuidv4 } from "uuid"; interface B4BulkUploadDialogProps { open: boolean; @@ -266,62 +267,139 @@ export function B4BulkUploadDialog({ setCurrentStep("uploading"); setShowValidationDialog(false); - // 진행률 시뮬레이션 - const progressInterval = setInterval(() => { - setUploadProgress((prev) => { - if (prev >= 90) { - clearInterval(progressInterval); - return prev; + try { + console.log(`[B4 일괄 업로드] 시작: ${validFiles.length}개 파일`); + + // 파일을 DrawingNo + RevNo로 그룹화 + const uploadGroups = new Map< + string, + Array<{ + file: File; + drawingNo: string; + revNo: string; + fileName: string; + registerGroupId: number; + }> + >(); + + validFiles.forEach((fileResult) => { + const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`; + if (!uploadGroups.has(groupKey)) { + uploadGroups.set(groupKey, []); } - return prev + 10; + uploadGroups.get(groupKey)!.push({ + file: fileResult.file, + drawingNo: fileResult.parsed!.drawingNo, + revNo: fileResult.parsed!.revNo, + fileName: fileResult.file.name, + registerGroupId: fileResult.registerGroupId || 0, + }); }); - }, 500); - try { - // FormData 생성 - const formData = new FormData(); - formData.append("projectNo", projectNo); - formData.append("userId", userId); - formData.append("userName", userName); - formData.append("userEmail", userEmail); - formData.append("vendorCode", vendorCode); - formData.append("registerKind", registerKind); // RegisterKind 추가 - - // 파일 및 메타데이터 추가 - validFiles.forEach((fileResult, index) => { - formData.append(`file_${index}`, fileResult.file); - formData.append(`drawingNo_${index}`, fileResult.parsed!.drawingNo); - formData.append(`revNo_${index}`, fileResult.parsed!.revNo); - formData.append(`fileName_${index}`, fileResult.file.name); - formData.append( - `registerGroupId_${index}`, - String(fileResult.registerGroupId || 0) - ); - }); + console.log(`[B4 일괄 업로드] ${uploadGroups.size}개 그룹으로 묶임`); + + let successCount = 0; + let failCount = 0; + let completedGroups = 0; + + // 각 그룹별로 순차 처리 + for (const [groupKey, files] of uploadGroups.entries()) { + const { drawingNo, revNo, registerGroupId } = files[0]; + + try { + console.log(`[B4 업로드] 그룹 ${groupKey}: ${files.length}개 파일`); + + // 1. UploadId 생성 + const uploadId = uuidv4(); + + // 2. 파일 업로드 (공통 API 사용) + const formData = new FormData(); + formData.append("uploadId", uploadId); + formData.append("userId", userId); + formData.append("fileCount", String(files.length)); + + files.forEach((fileInfo, index) => { + formData.append(`file_${index}`, fileInfo.file); + }); + + const uploadResponse = await fetch("/api/dolce/upload-files", { + method: "POST", + body: formData, + }); + + if (!uploadResponse.ok) { + throw new Error(`파일 업로드 실패: ${uploadResponse.status}`); + } + + const uploadResult = await uploadResponse.json(); + + if (!uploadResult.success) { + throw new Error(uploadResult.error || "파일 업로드 실패"); + } + + console.log(`[B4 업로드] 그룹 ${groupKey} 파일 업로드 완료`); + + // 3. 상세도면 등록 + await editDetailDwgReceipt({ + dwgList: [ + { + Mode: "ADD", + Status: "Draft", + RegisterId: 0, + ProjectNo: projectNo, + Discipline: "", + DrawingKind: "B4", + DrawingNo: drawingNo, + DrawingName: "", + RegisterGroupId: registerGroupId, + RegisterSerialNo: 0, + RegisterKind: registerKind, + DrawingRevNo: revNo, + Category: "TS", + Receiver: null, + Manager: "", + RegisterDesc: "", + UploadId: uploadId, + RegCompanyCode: vendorCode, + }, + ], + userId, + userNm: userName, + vendorCode, + email: userEmail, + }); + + console.log(`[B4 업로드] 그룹 ${groupKey} 상세도면 등록 완료`); + + successCount += files.length; + } catch (error) { + console.error(`[B4 업로드] 그룹 ${groupKey} 실패:`, error); + failCount += files.length; + } - formData.append("fileCount", String(validFiles.length)); + // 진행도 업데이트 + completedGroups++; + const progress = Math.round((completedGroups / uploadGroups.size) * 100); + setUploadProgress(progress); + } - // 서버 액션 호출 - const result: B4BulkUploadResult = await bulkUploadB4Files(formData); + console.log(`[B4 일괄 업로드] ✅ 완료: 성공 ${successCount}, 실패 ${failCount}`); - clearInterval(progressInterval); - setUploadProgress(100); - setUploadResult(result); + const result: B4BulkUploadResult = { + success: true, + successCount, + failCount, + }; - if (result.success) { - setCurrentStep("complete"); - toast.success( - `${result.successCount}/${validFiles.length}개 파일 업로드 완료` - ); - } else { - setCurrentStep("files"); - toast.error(result.error || "업로드 실패"); - } + setUploadResult(result); + setCurrentStep("complete"); + toast.success(`${successCount}/${validFiles.length}개 파일 업로드 완료`); } catch (error) { - console.error("업로드 실패:", error); + console.error("[B4 일괄 업로드] 실패:", error); toast.error( error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다" ); + setCurrentStep("files"); } finally { setIsUploading(false); } @@ -460,7 +538,7 @@ export function B4BulkUploadDialog({ 전체 제거 </Button> </div> - <div className="max-h-48 overflow-auto space-y-2"> + <div className="max-h-60 overflow-y-auto space-y-2"> {selectedFiles.map((file, index) => ( <div key={index} diff --git a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx index af73aea6..431a2c77 100644 --- a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx +++ b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx @@ -181,7 +181,7 @@ export function UploadFilesToDetailDialog({ 전체 제거 </Button> </div> - <div className="max-h-48 overflow-auto space-y-2"> + <div className="max-h-60 overflow-y-auto space-y-2"> {selectedFiles.map((file, index) => ( <div key={index} diff --git a/lib/dolce/hooks/use-file-upload.ts b/lib/dolce/hooks/use-file-upload.ts deleted file mode 100644 index 38556cb9..00000000 --- a/lib/dolce/hooks/use-file-upload.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { useState, useCallback } from "react"; -import { useDropzone, FileRejection } from "react-dropzone"; -import { toast } from "sonner"; - -interface UseFileUploadOptions { - onFilesAdded?: (files: File[]) => void; -} - -export function useFileUpload(options: UseFileUploadOptions = {}) { - const [files, setFiles] = useState<File[]>([]); - - // 파일 검증 - const validateFiles = useCallback((filesToValidate: File[]): { valid: File[]; invalid: string[] } => { - const MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GB - const FORBIDDEN_EXTENSIONS = ['exe', 'com', 'dll', 'vbs', 'js', 'asp', 'aspx', 'bat', 'cmd']; - - const validFiles: File[] = []; - const invalidFiles: string[] = []; - - filesToValidate.forEach((file) => { - // 크기 검증 - if (file.size > MAX_FILE_SIZE) { - invalidFiles.push(`${file.name}: 파일 크기가 1GB를 초과합니다`); - return; - } - - // 확장자 검증 (블랙리스트) - const extension = file.name.split('.').pop()?.toLowerCase(); - if (extension && FORBIDDEN_EXTENSIONS.includes(extension)) { - invalidFiles.push(`${file.name}: 금지된 파일 형식입니다 (.${extension})`); - return; - } - - validFiles.push(file); - }); - - return { valid: validFiles, invalid: invalidFiles }; - }, []); - - // 파일 드롭 핸들러 - const onDrop = useCallback((acceptedFiles: File[], rejectedFiles: FileRejection[]) => { - const { valid: validFiles, invalid: invalidMessages } = validateFiles(acceptedFiles); - - // 거부된 파일 처리 - if (rejectedFiles.length > 0) { - rejectedFiles.forEach((rejected) => { - const errorMsg = rejected.errors?.[0]?.message || "파일이 거부되었습니다"; - toast.error(`${rejected.file.name}: ${errorMsg}`); - }); - } - - // 유효하지 않은 파일 메시지 표시 - if (invalidMessages.length > 0) { - invalidMessages.forEach((msg) => toast.error(msg)); - } - - if (validFiles.length > 0) { - // 중복 제거 - const existingNames = new Set(files.map((f) => f.name)); - const newFiles = validFiles.filter((f) => !existingNames.has(f.name)); - - if (newFiles.length === 0) { - toast.error("이미 선택된 파일입니다"); - return; - } - - setFiles((prev) => { - const updated = [...prev, ...newFiles]; - options.onFilesAdded?.(updated); - return updated; - }); - toast.success(`${newFiles.length}개 파일이 선택되었습니다`); - } - }, [files, validateFiles, options]); - - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, - multiple: true, - maxSize: 1024 * 1024 * 1024, // 1GB - }); - - // 파일 제거 - const removeFile = useCallback((index: number) => { - setFiles((prev) => prev.filter((_, i) => i !== index)); - }, []); - - // 전체 파일 제거 - const clearFiles = useCallback(() => { - setFiles([]); - }, []); - - // 파일 배열 직접 설정 - const setFileList = useCallback((newFiles: File[]) => { - setFiles(newFiles); - }, []); - - return { - files, - setFiles: setFileList, - removeFile, - clearFiles, - getRootProps, - getInputProps, - isDragActive, - }; -} - diff --git a/lib/dolce/utils/upload-with-progress.ts b/lib/dolce/utils/upload-with-progress.ts index 8e36afe4..1204bf36 100644 --- a/lib/dolce/utils/upload-with-progress.ts +++ b/lib/dolce/utils/upload-with-progress.ts @@ -40,24 +40,30 @@ export async function uploadFilesWithProgress({ }); const xhr = new XMLHttpRequest(); + + // 타임아웃 설정 (1시간) + xhr.timeout = 3600000; // 1시간 (밀리초) - // 전체 업로드 진행도 (단순화: 전체 진행도를 각 파일에 분배) + // 전체 업로드 진행도 + // 주의: xhr.upload.progress는 클라이언트→서버 전송만 추적 + // 서버에서 DOLCE API로 재업로드하는 과정은 별도 (추적 불가) xhr.upload.addEventListener("progress", (event) => { if (event.lengthComputable) { - const totalProgress = (event.loaded / event.total) * 100; + // 전송 완료 = 서버에 도착 (실제 처리 시작) + // 서버 처리를 위해 최대 95%까지만 표시 + const totalProgress = Math.min((event.loaded / event.total) * 95, 95); // 현재 업로드 중인 파일 인덱스 추정 - const filesCompleted = Math.floor((totalProgress / 100) * files.length); + const filesCompleted = Math.floor((totalProgress / 95) * files.length); const currentFileIndex = Math.min(filesCompleted, files.length - 1); // 각 파일별 진행도 계산 files.forEach((_, index) => { if (index < filesCompleted) { - callbacks.onProgress(index, 100); - callbacks.onFileComplete(index); + callbacks.onProgress(index, 95); } else if (index === currentFileIndex) { - const fileProgress = ((totalProgress / 100) * files.length - filesCompleted) * 100; - callbacks.onProgress(index, Math.min(fileProgress, 99)); + const fileProgress = ((totalProgress / 95) * files.length - filesCompleted) * 95; + callbacks.onProgress(index, Math.min(fileProgress, 94)); } else { callbacks.onProgress(index, 0); } @@ -70,15 +76,35 @@ export async function uploadFilesWithProgress({ try { const response = JSON.parse(xhr.responseText); - // 모든 파일 완료 처리 - files.forEach((_, index) => { - callbacks.onProgress(index, 100); - callbacks.onFileComplete(index); - }); + // 서버 응답 검증 + if (response.success) { + console.log(`[업로드 클라이언트] 서버 처리 완료: ${response.uploadedCount}개 파일`); + + // 서버에서 실제 처리 완료 시에만 100% + files.forEach((_, index) => { + callbacks.onProgress(index, 100); + callbacks.onFileComplete(index); + }); - resolve(response); + resolve(response); + } else { + // 서버에서 에러 응답 + const errorMsg = response.error || "서버에서 업로드 실패"; + console.error(`[업로드 클라이언트] 서버 에러:`, errorMsg); + + files.forEach((_, index) => { + callbacks.onFileError(index, errorMsg); + }); + + resolve({ + success: false, + error: errorMsg, + }); + } } catch (error) { - const errorMsg = "응답 파싱 실패"; + const errorMsg = `응답 파싱 실패: ${xhr.responseText?.substring(0, 100)}`; + console.error(`[업로드 클라이언트] 파싱 에러:`, error, xhr.responseText); + files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); @@ -89,6 +115,8 @@ export async function uploadFilesWithProgress({ } } else { const errorMsg = `업로드 실패: ${xhr.status} ${xhr.statusText}`; + console.error(`[업로드 클라이언트] HTTP 에러:`, errorMsg, xhr.responseText); + files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); @@ -101,6 +129,8 @@ export async function uploadFilesWithProgress({ xhr.addEventListener("error", () => { const errorMsg = "네트워크 오류"; + console.error(`[업로드 클라이언트] 네트워크 에러`); + files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); @@ -112,6 +142,21 @@ export async function uploadFilesWithProgress({ xhr.addEventListener("abort", () => { const errorMsg = "업로드가 취소되었습니다"; + console.warn(`[업로드 클라이언트] 업로드 취소됨`); + + files.forEach((_, index) => { + callbacks.onFileError(index, errorMsg); + }); + resolve({ + success: false, + error: errorMsg, + }); + }); + + xhr.addEventListener("timeout", () => { + const errorMsg = "업로드 타임아웃 (1시간 초과)"; + console.error(`[업로드 클라이언트] 타임아웃 발생 (1시간 초과)`); + files.forEach((_, index) => { callbacks.onFileError(index, errorMsg); }); @@ -121,6 +166,7 @@ export async function uploadFilesWithProgress({ }); }); + console.log(`[업로드 클라이언트] 시작: ${files.length}개 파일`); xhr.open("POST", "/api/dolce/upload-files"); xhr.send(formData); }); |
