diff options
Diffstat (limited to 'lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3.tsx')
| -rw-r--r-- | lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3.tsx | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3.tsx b/lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3.tsx new file mode 100644 index 00000000..5cce514c --- /dev/null +++ b/lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3.tsx @@ -0,0 +1,372 @@ +"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 { FolderOpen, Loader2, ChevronRight, ChevronLeft, CheckCircle2 } from "lucide-react"; +import { toast } from "sonner"; +import { Progress } from "@/components/ui/progress"; +import { useTranslation } from "@/i18n/client"; +import { + validateB4FileName, + B4UploadValidationDialog, + type FileValidationResult, +} from "@/lib/dolce/dialogs/b4-upload-validation-dialog"; +import { + checkB4MappingStatus, + type MappingCheckResult, + type B4BulkUploadResult, +} from "@/lib/dolce/actions"; +import { bulkUploadB4FilesV3 } from "@/lib/dolce-v2/actions"; + +interface B4BulkUploadDialogV3SyncProps { + open: boolean; + onOpenChange: (open: boolean) => void; + projectNo: string; + userId: string; + userName: string; + userEmail: string; + vendorCode: string; + onUploadComplete?: () => void; + lng: string; +} + +type UploadStep = "files" | "validation" | "uploading" | "complete"; + +export function B4BulkUploadDialogV3Sync({ + open, + onOpenChange, + projectNo, + userId, + userName, + userEmail, + vendorCode, + onUploadComplete, + lng, +}: B4BulkUploadDialogV3SyncProps) { + const { t } = useTranslation(lng, "dolce"); + const [currentStep, setCurrentStep] = useState<UploadStep>("files"); + const [selectedFiles, setSelectedFiles] = useState<File[]>([]); + const [isUploading, setIsUploading] = useState(false); + const [validationResults, setValidationResults] = useState<FileValidationResult[]>([]); + const [mappingResultsMap, setMappingResultsMap] = useState<Map<string, MappingCheckResult>>(new Map()); + const [showValidationDialog, setShowValidationDialog] = useState(false); + const [isDragging, setIsDragging] = useState(false); + // const [uploadProgress, setUploadProgress] = useState(0); // 로컬 저장은 순식간이라 프로그레스 불필요 + const [uploadResult, setUploadResult] = useState<{ success: boolean, syncIds: string[], error?: string } | null>(null); + + // Reset on close + React.useEffect(() => { + if (!open) { + setCurrentStep("files"); + setSelectedFiles([]); + setValidationResults([]); + setMappingResultsMap(new Map()); + setShowValidationDialog(false); + setIsDragging(false); + setUploadResult(null); + } + }, [open]); + + // File Selection Handler (동일) + 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(t("bulkUpload.duplicateFileError")); + return; + } + setSelectedFiles((prev) => [...prev, ...newFiles]); + toast.success(t("bulkUpload.filesSelectedSuccess", { count: newFiles.length })); + }; + + // Drag & Drop Handlers (생략 - 코드 길이 줄임) + const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; + const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); 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)); }; + + // Step 1 Next: Validation (동일) + const handleFilesNext = () => { + if (selectedFiles.length === 0) { + toast.error(t("bulkUpload.selectFilesError")); + return; + } + setCurrentStep("validation"); + handleValidate(); + }; + + // Validation Process (V3 - 기존과 동일) + const handleValidate = async () => { + try { + // 1. Parse Filenames + 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. Call MatchBatchFileDwg to check mapping status + const mappingCheckItems = parsedFiles.map((r) => ({ + DrawingNo: r.parsed!.drawingNo, + RevNo: r.parsed!.revNo, + FileNm: r.file.name, + })); + + const mappingResults = await checkB4MappingStatus(projectNo, mappingCheckItems); + + const newMappingResultsMap = new Map<string, MappingCheckResult>(); + mappingResults.forEach((result) => { + newMappingResultsMap.set(result.FileNm, result); + }); + setMappingResultsMap(newMappingResultsMap); + + // 3. Merge results + const finalResults: FileValidationResult[] = parseResults.map((parseResult) => { + if (!parseResult.valid || !parseResult.parsed) return parseResult; + const mappingResult = newMappingResultsMap.get(parseResult.file.name); + + if (!mappingResult) return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notFound") }; + if (mappingResult.MappingYN !== "Y") return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notRegistered") }; + if (mappingResult.DrawingMoveGbn !== "도면입수") return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notGttDeliverables") }; + + return { + ...parseResult, + mappingStatus: "available" as const, + drawingName: mappingResult.DrawingName || undefined, + registerGroupId: mappingResult.RegisterGroupId, + }; + }); + + setValidationResults(finalResults); + setShowValidationDialog(true); + } catch (error) { + console.error("Validation failed:", error); + toast.error(error instanceof Error ? error.message : t("bulkUpload.validationError")); + setCurrentStep("files"); + } + }; + + // Confirm Upload & Save (V3 Sync - 수정됨) + const handleConfirmUpload = async (validFiles: FileValidationResult[]) => { + setIsUploading(true); + setCurrentStep("uploading"); + setShowValidationDialog(false); + + try { + // FormData 구성 + const formData = new FormData(); + formData.append("projectNo", projectNo); + formData.append("userId", userId); + formData.append("userNm", userName); + formData.append("email", userEmail); + formData.append("vendorCode", vendorCode); + formData.append("registerKind", ""); // B4는 mappingData에 있음, 혹은 필요하다면 추가 + formData.append("fileCount", String(validFiles.length)); + + validFiles.forEach((fileResult, index) => { + formData.append(`file_${index}`, fileResult.file); + + const mappingData = mappingResultsMap.get(fileResult.file.name); + if (mappingData) { + // UploadId가 없으면 생성 + if (!mappingData.UploadId) { + mappingData.UploadId = uuidv4(); // 임시 ID 생성 (서버에서 그룹핑용) + } + formData.append(`mappingData_${index}`, JSON.stringify(mappingData)); + } + }); + + // Action 호출 + const result = await bulkUploadB4FilesV3(formData); + + setUploadResult(result); + setCurrentStep("complete"); + + if (result.success) { + toast.success(t("bulkUpload.uploadSuccessToast", { successCount: validFiles.length, total: validFiles.length })); + } else { + toast.error(result.error || t("bulkUpload.uploadError")); + } + + } catch (error) { + console.error("Upload process failed:", error); + toast.error(error instanceof Error ? error.message : t("bulkUpload.uploadError")); + setCurrentStep("files"); + } finally { + setIsUploading(false); + } + }; + + return ( + <> + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-2xl"> + <DialogHeader> + <DialogTitle>{t("bulkUpload.title")} (Offline)</DialogTitle> + <DialogDescription> + {currentStep === "files" && t("bulkUpload.stepFiles")} + {currentStep === "validation" && t("bulkUpload.stepValidation")} + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* Step 1: Files */} + {currentStep === "files" && ( + <> + <div + className={`border-2 border-dashed rounded-lg p-8 transition-all duration-200 ${ + isDragging + ? "border-primary bg-primary/5 scale-[1.02]" + : "border-muted-foreground/30 hover:border-muted-foreground/50" + }`} + onDragEnter={handleDragEnter} + onDragLeave={handleDragLeave} + onDragOver={handleDragOver} + onDrop={handleDrop} + > + <input + type="file" + multiple + accept=".pdf,.doc,.docx,.xls,.xlsx,.dwg,.dxf,.zip" + onChange={(e) => handleFilesChange(Array.from(e.target.files || []))} + className="hidden" + id="b4-file-upload-v3-sync" + /> + <label + htmlFor="b4-file-upload-v3-sync" + className="flex flex-col items-center justify-center cursor-pointer" + > + <FolderOpen + className={`h-12 w-12 mb-3 transition-colors ${ + isDragging ? "text-primary" : "text-muted-foreground" + }`} + /> + <p className="text-sm text-muted-foreground"> + {isDragging ? t("bulkUpload.fileDropHere") : t("bulkUpload.fileSelectArea")} + </p> + </label> + </div> + + {selectedFiles.length > 0 && ( + <div className="border rounded-lg p-4"> + <div className="flex items-center justify-between mb-3"> + <h4 className="text-sm font-medium"> + {t("bulkUpload.selectedFiles", { count: selectedFiles.length })} + </h4> + <Button variant="ghost" size="sm" onClick={() => setSelectedFiles([])}> + {t("bulkUpload.removeAll")} + </Button> + </div> + <div className="max-h-60 overflow-y-auto space-y-2"> + {selectedFiles.map((file, index) => ( + <div key={index} className="flex items-center justify-between p-2 rounded bg-muted/50"> + <p className="text-sm truncate">{file.name}</p> + <Button variant="ghost" size="sm" onClick={() => handleRemoveFile(index)}> + {t("bulkUpload.removeFile")} + </Button> + </div> + ))} + </div> + </div> + )} + </> + )} + + {/* Loading Indicator */} + {currentStep === "validation" && ( + <div className="flex flex-col items-center justify-center py-12"> + <Loader2 className="h-12 w-12 animate-spin text-primary mb-4" /> + <p className="text-sm text-muted-foreground">{t("bulkUpload.validating")}</p> + </div> + )} + + {/* Uploading (Saving locally) */} + {currentStep === "uploading" && ( + <div className="space-y-6 py-4"> + <div className="flex flex-col items-center"> + <Loader2 className="h-12 w-12 animate-spin text-primary mb-4" /> + <h3 className="text-lg font-semibold mb-2">Saving to Local...</h3> + <p className="text-sm text-muted-foreground">Please wait while we buffer your files.</p> + </div> + </div> + )} + + {/* Completion Screen */} + {currentStep === "complete" && uploadResult && ( + <div className="space-y-6 py-8"> + <div className="flex flex-col items-center"> + <CheckCircle2 className="h-16 w-16 text-green-500 mb-4" /> + <h3 className="text-lg font-semibold mb-2">Saved Locally</h3> + <p className="text-sm text-muted-foreground"> + {uploadResult.syncIds.length} items are ready to sync. + </p> + </div> + + <div className="flex justify-center"> + <Button onClick={() => { onOpenChange(false); onUploadComplete?.(); }}> + {t("bulkUpload.confirmButton")} + </Button> + </div> + </div> + )} + </div> + + {/* Footer */} + {currentStep !== "uploading" && currentStep !== "complete" && currentStep !== "validation" && ( + <DialogFooter> + {currentStep === "files" && ( + <> + <Button variant="outline" onClick={() => onOpenChange(false)}> + {t("bulkUpload.cancelButton")} + </Button> + <Button onClick={handleFilesNext} disabled={selectedFiles.length === 0}> + {t("bulkUpload.validateButton")} + <ChevronRight className="ml-2 h-4 w-4" /> + </Button> + </> + )} + </DialogFooter> + )} + </DialogContent> + </Dialog> + + {/* Validation Dialog */} + <B4UploadValidationDialog + open={showValidationDialog} + onOpenChange={(open) => { + setShowValidationDialog(open); + if (!open && currentStep !== "uploading" && currentStep !== "complete") { + setCurrentStep("files"); + } + }} + validationResults={validationResults} + onConfirmUpload={handleConfirmUpload} + isUploading={isUploading} + /> + </> + ); +} + |
