summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/dolce/actions.ts14
-rw-r--r--lib/dolce/components/file-upload-progress-list.tsx2
-rw-r--r--lib/dolce/dialogs/add-detail-drawing-dialog.tsx2
-rw-r--r--lib/dolce/dialogs/b4-bulk-upload-dialog.tsx172
-rw-r--r--lib/dolce/dialogs/upload-files-to-detail-dialog.tsx2
-rw-r--r--lib/dolce/hooks/use-file-upload.ts107
-rw-r--r--lib/dolce/utils/upload-with-progress.ts74
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);
});