summaryrefslogtreecommitdiff
path: root/lib/dolce-v2/dialogs/b4-bulk-upload-dialog-v3.tsx
diff options
context:
space:
mode:
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.tsx372
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}
+ />
+ </>
+ );
+}
+