From fd4909bba7be8abc1eeab9ae1b4621c66a61604a Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Sun, 23 Nov 2025 16:40:37 +0900 Subject: (김준회) 돌체 재개발 - 1차 (다운로드 오류 수정 필요) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/dolce/dialogs/b4-bulk-upload-dialog.tsx | 608 ++++++++++++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 lib/dolce/dialogs/b4-bulk-upload-dialog.tsx (limited to 'lib/dolce/dialogs/b4-bulk-upload-dialog.tsx') diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx new file mode 100644 index 00000000..f4816328 --- /dev/null +++ b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx @@ -0,0 +1,608 @@ +"use client"; + +import * as React from "react"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { FolderOpen, Loader2, ChevronRight, ChevronLeft, CheckCircle2 } from "lucide-react"; +import { toast } from "sonner"; +import { Progress } from "@/components/ui/progress"; +import { + validateB4FileName, + B4UploadValidationDialog, + type FileValidationResult, +} from "./b4-upload-validation-dialog"; +import { + checkB4MappingStatus, + bulkUploadB4Files, + type MappingCheckItem, + type B4BulkUploadResult, +} from "../actions"; + +interface B4BulkUploadDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + projectNo: string; + userId: string; + userName: string; + userEmail: string; + vendorCode: string; + onUploadComplete?: () => void; +} + +// B4 GTT 옵션 +const B4_DRAWING_USAGE_OPTIONS = [ + { value: "REC", label: "RECEIVE (입수용)" }, +]; + +const B4_REGISTER_KIND_OPTIONS: Record> = { + REC: [ + { value: "RECP", label: "Pre. 도면입수" }, + { value: "RECW", label: "Working 도면입수" }, + ], +}; + +type UploadStep = "settings" | "files" | "validation" | "uploading" | "complete"; + +export function B4BulkUploadDialog({ + open, + onOpenChange, + projectNo, + userId, + userName, + userEmail, + vendorCode, + onUploadComplete, +}: B4BulkUploadDialogProps) { + const [currentStep, setCurrentStep] = useState("settings"); + const [drawingUsage, setDrawingUsage] = useState("REC"); + const [registerKind, setRegisterKind] = useState(""); + const [selectedFiles, setSelectedFiles] = useState([]); + const [isUploading, setIsUploading] = useState(false); + const [validationResults, setValidationResults] = useState([]); + const [showValidationDialog, setShowValidationDialog] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadResult, setUploadResult] = useState(null); + + // 다이얼로그 닫을 때 초기화 + React.useEffect(() => { + if (!open) { + setCurrentStep("settings"); + setDrawingUsage("REC"); + setRegisterKind(""); + setSelectedFiles([]); + setValidationResults([]); + setShowValidationDialog(false); + setIsDragging(false); + setUploadProgress(0); + setUploadResult(null); + } + }, [open]); + + // 파일 선택 핸들러 + const handleFilesChange = (files: File[]) => { + if (files.length === 0) return; + + // 중복 제거 + const existingNames = new Set(selectedFiles.map((f) => f.name)); + const newFiles = files.filter((f) => !existingNames.has(f.name)); + + if (newFiles.length === 0) { + toast.error("이미 선택된 파일입니다"); + return; + } + + setSelectedFiles((prev) => [...prev, ...newFiles]); + toast.success(`${newFiles.length}개 파일이 선택되었습니다`); + }; + + // Drag & Drop 핸들러 + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.currentTarget === e.target) { + setIsDragging(false); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + e.dataTransfer.dropEffect = "copy"; + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const droppedFiles = Array.from(e.dataTransfer.files); + if (droppedFiles.length > 0) { + handleFilesChange(droppedFiles); + } + }; + + // 파일 제거 + const handleRemoveFile = (index: number) => { + setSelectedFiles((prev) => prev.filter((_, i) => i !== index)); + }; + + // 1단계 완료 (설정) + const handleSettingsNext = () => { + if (!registerKind) { + toast.error("등록종류를 선택하세요"); + return; + } + setCurrentStep("files"); + }; + + // 2단계 완료 (파일 선택) + const handleFilesNext = () => { + if (selectedFiles.length === 0) { + toast.error("파일을 선택해주세요"); + return; + } + setCurrentStep("validation"); + handleValidate(); + }; + + // 검증 시작 + const handleValidate = async () => { + try { + // 1단계: 파일명 파싱 + const parseResults: FileValidationResult[] = selectedFiles.map((file) => { + const validation = validateB4FileName(file.name); + return { + file, + valid: validation.valid, + parsed: validation.parsed, + error: validation.error, + }; + }); + + // 파싱에 실패한 파일이 있으면 바로 검증 다이얼로그 표시 + const parsedFiles = parseResults.filter((r) => r.valid && r.parsed); + if (parsedFiles.length === 0) { + setValidationResults(parseResults); + setShowValidationDialog(true); + return; + } + + // 2단계: 매핑 현황 조회 + const mappingCheckItems: MappingCheckItem[] = parsedFiles.map((r) => ({ + DrawingNo: r.parsed!.drawingNo, + RevNo: r.parsed!.revNo, + FileNm: r.file.name, + })); + + const mappingResults = await checkB4MappingStatus( + projectNo, + mappingCheckItems + ); + + // 3단계: 검증 결과 병합 + const finalResults: FileValidationResult[] = parseResults.map((parseResult) => { + if (!parseResult.valid || !parseResult.parsed) { + return parseResult; + } + + // 매핑 결과 찾기 + const mappingResult = mappingResults.find( + (m) => + m.DrawingNo === parseResult.parsed!.drawingNo && + m.RevNo === parseResult.parsed!.revNo + ); + + if (!mappingResult) { + return { + ...parseResult, + mappingStatus: "not_found" as const, + error: "DOLCE 시스템에서 도면을 찾을 수 없습니다", + }; + } + + // RegisterGroupId가 0이거나 MappingYN이 N이면 도면이 존재하지 않음 + if (mappingResult.RegisterGroupId === 0 || mappingResult.MappingYN === "N") { + return { + ...parseResult, + mappingStatus: "not_found" as const, + error: "해당 도면번호가 프로젝트에 등록되어 있지 않습니다", + }; + } + + // DrawingMoveGbn이 "도면입수"가 아니면 업로드 불가 + if (mappingResult.DrawingMoveGbn !== "도면입수") { + return { + ...parseResult, + mappingStatus: "not_found" as const, + error: "도면입수(GTT Deliverables)인 도면만 업로드 가능합니다", + }; + } + + // MappingYN이 Y이고 도면입수인 경우 업로드 가능 + return { + ...parseResult, + mappingStatus: "available" as const, + drawingName: mappingResult.DrawingName || undefined, + registerGroupId: mappingResult.RegisterGroupId, + }; + }); + + setValidationResults(finalResults); + setShowValidationDialog(true); + } catch (error) { + console.error("검증 실패:", error); + toast.error( + error instanceof Error ? error.message : "검증 중 오류가 발생했습니다" + ); + } + }; + + // 업로드 확인 + const handleConfirmUpload = async (validFiles: FileValidationResult[]) => { + setIsUploading(true); + setCurrentStep("uploading"); + setShowValidationDialog(false); + + // 진행률 시뮬레이션 + const progressInterval = setInterval(() => { + setUploadProgress((prev) => { + if (prev >= 90) { + clearInterval(progressInterval); + return prev; + } + return prev + 10; + }); + }, 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) + ); + }); + + formData.append("fileCount", String(validFiles.length)); + + // 서버 액션 호출 + const result: B4BulkUploadResult = await bulkUploadB4Files(formData); + + clearInterval(progressInterval); + setUploadProgress(100); + setUploadResult(result); + + if (result.success) { + setCurrentStep("complete"); + toast.success( + `${result.successCount}/${validFiles.length}개 파일 업로드 완료` + ); + } else { + setCurrentStep("files"); + toast.error(result.error || "업로드 실패"); + } + } catch (error) { + console.error("업로드 실패:", error); + toast.error( + error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다" + ); + } finally { + setIsUploading(false); + } + }; + + const registerKindOptions = drawingUsage + ? B4_REGISTER_KIND_OPTIONS[drawingUsage] || [] + : []; + + const handleDrawingUsageChange = (value: string) => { + setDrawingUsage(value); + setRegisterKind(""); + }; + + return ( + <> + + + + B4 일괄 업로드 + + {currentStep === "settings" && "업로드 설정을 선택하세요"} + {currentStep === "files" && "파일명 형식: [버림] [DrawingNo] [RevNo].[확장자] (예: testfile GTT-DE-007 R01.pdf)"} + {currentStep === "validation" && "파일 검증 중..."} + + + +
+ {/* 1단계: 설정 입력 */} + {currentStep === "settings" && ( + <> + {/* 도면용도 선택 */} +
+ + +
+ + {/* 등록종류 선택 */} +
+ + +

+ 선택한 등록종류가 모든 파일에 적용됩니다 +

+
+ + )} + + {/* 2단계: 파일 선택 */} + {currentStep === "files" && ( + <> + {/* 파일 선택 영역 */} +
+ handleFilesChange(Array.from(e.target.files || []))} + className="hidden" + id="b4-file-upload" + /> + +
+ + {/* 선택된 파일 목록 */} + {selectedFiles.length > 0 && ( +
+
+

+ 선택된 파일 ({selectedFiles.length}개) +

+ +
+
+ {selectedFiles.map((file, index) => ( +
+
+

{file.name}

+

+ {(file.size / 1024 / 1024).toFixed(2)} MB +

+
+ +
+ ))} +
+
+ )} + + )} + + {/* 3단계: 검증 중 표시 */} + {currentStep === "validation" && ( +
+ +

+ 파일 검증 중입니다... +

+
+ )} + + {/* 4단계: 업로드 진행 중 */} + {currentStep === "uploading" && ( +
+
+ +

파일 업로드 중...

+

+ 잠시만 기다려주세요 +

+
+
+
+ 진행률 + {uploadProgress}% +
+ +
+
+ )} + + {/* 5단계: 업로드 완료 */} + {currentStep === "complete" && uploadResult && ( +
+
+ +

업로드 완료!

+

+ {uploadResult.successCount}개 파일이 성공적으로 업로드되었습니다 +

+
+ + {uploadResult.failCount && uploadResult.failCount > 0 && ( +
+

+ {uploadResult.failCount}개 파일 업로드 실패 +

+
+ )} + +
+ +
+
+ )} +
+ + {/* 푸터 버튼 (uploading, complete 단계에서는 숨김) */} + {currentStep !== "uploading" && currentStep !== "complete" && currentStep !== "validation" && ( + + {currentStep === "settings" && ( + <> + + + + )} + + {currentStep === "files" && ( + <> + + + + )} + + )} +
+
+ + {/* 검증 다이얼로그 */} + + + ); +} + -- cgit v1.2.3