From 33f8dc52beefb8d23233643c775879787267e4c5 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Thu, 27 Nov 2025 22:05:20 +0900
Subject: (김준회) dolce: 상세도면 Standby 상태는 MOD(수정) 지원 처리
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx | 246 +++++++++++++++++--------
 1 file changed, 170 insertions(+), 76 deletions(-)

(limited to 'lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx')

diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx
index e34b76c7..e7322439 100644
--- a/lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx
+++ b/lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx
@@ -58,6 +58,7 @@ export function B4BulkUploadDialogV3({
   lng,
 }: B4BulkUploadDialogV3Props) {
   const { t } = useTranslation(lng, "dolce");
+  const dragCounter = React.useRef(0);
   const [currentStep, setCurrentStep] = useState<UploadStep>("files");
   const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
   const [isUploading, setIsUploading] = useState(false);
@@ -81,6 +82,7 @@ export function B4BulkUploadDialogV3({
       setUploadProgress(0);
       setUploadResult(null);
       setFileProgresses([]);
+      dragCounter.current = 0;
     }
   }, [open]);
 
@@ -89,28 +91,48 @@ export function B4BulkUploadDialogV3({
     if (files.length === 0) return;
 
     const existingNames = new Set(selectedFiles.map((f) => f.name));
-    const newFiles = files.filter((f) => !existingNames.has(f.name));
+    const uniqueFiles: File[] = [];
+    const duplicates: string[] = [];
+    const newNames = new Set<string>(); // To check duplicates within the new batch
+
+    files.forEach((file) => {
+        if (existingNames.has(file.name) || newNames.has(file.name)) {
+            duplicates.push(file.name);
+        } else {
+            newNames.add(file.name);
+            uniqueFiles.push(file);
+        }
+    });
 
-    if (newFiles.length === 0) {
+    if (duplicates.length > 0) {
       toast.error(t("bulkUpload.duplicateFileError"));
-      return;
     }
 
-    setSelectedFiles((prev) => [...prev, ...newFiles]);
-    toast.success(t("bulkUpload.filesSelectedSuccess", { count: newFiles.length }));
+    if (uniqueFiles.length > 0) {
+        setSelectedFiles((prev) => [...prev, ...uniqueFiles]);
+        toast.success(t("bulkUpload.filesSelectedSuccess", { count: uniqueFiles.length }));
+    }
   };
 
   // Drag & Drop Handlers
   const handleDragEnter = (e: React.DragEvent) => {
     e.preventDefault();
     e.stopPropagation();
-    setIsDragging(true);
+    if (currentStep !== "files") return;
+
+    dragCounter.current++;
+    if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
+      setIsDragging(true);
+    }
   };
 
   const handleDragLeave = (e: React.DragEvent) => {
     e.preventDefault();
     e.stopPropagation();
-    if (e.currentTarget === e.target) {
+    if (currentStep !== "files") return;
+
+    dragCounter.current--;
+    if (dragCounter.current === 0) {
       setIsDragging(false);
     }
   };
@@ -118,13 +140,18 @@ export function B4BulkUploadDialogV3({
   const handleDragOver = (e: React.DragEvent) => {
     e.preventDefault();
     e.stopPropagation();
+    if (currentStep !== "files") return;
+
     e.dataTransfer.dropEffect = "copy";
   };
 
   const handleDrop = (e: React.DragEvent) => {
     e.preventDefault();
     e.stopPropagation();
+    if (currentStep !== "files") return;
+
     setIsDragging(false);
+    dragCounter.current = 0;
 
     const droppedFiles = Array.from(e.dataTransfer.files);
     if (droppedFiles.length > 0) {
@@ -162,6 +189,18 @@ export function B4BulkUploadDialogV3({
         };
       });
 
+      // Check for duplicates ignoring extension
+      const nameMap = new Map<string, number>(); // nameWithoutExt -> count
+      parseResults.forEach(r => {
+          const nameWithoutExt = r.file.name.substring(0, r.file.name.lastIndexOf('.'));
+          nameMap.set(nameWithoutExt, (nameMap.get(nameWithoutExt) || 0) + 1);
+      });
+
+      const duplicateNames = new Set<string>();
+      nameMap.forEach((count, name) => {
+          if (count > 1) duplicateNames.add(name);
+      });
+
       // 2. Call MatchBatchFileDwg to check mapping status for ALL files
       // Even if local parsing failed, we send the filename to the server
       const mappingCheckItems = parseResults.map((r) => ({
@@ -178,11 +217,21 @@ export function B4BulkUploadDialogV3({
       );
 
       // Store mapping results for later use (upload/save)
-      // Use the original file name from our request list as the key to ensure we can look it up later.
-      // The API response 'FileNm' might differ (e.g., missing extension), so we rely on the array index order (1:1).
+      // Use FileNm (without extension) to match response results
+      const responseMap = new Map<string, MappingCheckResult>();
+      if (mappingResults && Array.isArray(mappingResults)) {
+          mappingResults.forEach((res) => {
+              if (res && res.FileNm) {
+                  responseMap.set(res.FileNm, res);
+              }
+          });
+      }
+
       const newMappingResultsMap = new Map<string, MappingCheckResult>();
-      parseResults.forEach((parseResult, index) => {
-        const result = mappingResults[index];
+      parseResults.forEach((parseResult) => {
+        // Look up by file name without extension
+        const nameWithoutExt = parseResult.file.name.substring(0, parseResult.file.name.lastIndexOf('.'));
+        const result = responseMap.get(nameWithoutExt);
         if (result) {
            newMappingResultsMap.set(parseResult.file.name, result);
         }
@@ -191,14 +240,36 @@ export function B4BulkUploadDialogV3({
 
       // 3. Merge results
       const finalResults: FileValidationResult[] = parseResults.map((parseResult) => {
-        // Retrieve by file name (now reliably mapped)
+        const nameWithoutExt = parseResult.file.name.substring(0, parseResult.file.name.lastIndexOf('.'));
+        
+        // Priority 1: Check for duplicates first (must be invalid regardless of server response)
+        if (duplicateNames.has(nameWithoutExt)) {
+             return {
+                ...parseResult,
+                valid: false,
+                mappingStatus: "not_found" as const,
+                error: "Duplicate file name",
+             };
+        }
+
+        // Priority 2: If local parsing failed, keep it as invalid (server response doesn't matter)
+        if (!parseResult.valid || !parseResult.parsed) {
+            // Return the original parse error
+            return {
+                ...parseResult,
+                valid: false,
+                mappingStatus: "not_found" as const,
+            };
+        }
+
+        // Priority 3: Check server mapping result for valid local parses only
         const mappingResult = newMappingResultsMap.get(parseResult.file.name);
 
-        // If mapping exists and is valid, it overrides local validation errors
+        // If mapping exists and is valid on server
         if (mappingResult && mappingResult.MappingYN === "Y" && mappingResult.DrawingMoveGbn === "도면입수") {
              return {
                 file: parseResult.file,
-                valid: true, // Valid because server recognized it
+                valid: true,
                 parsed: {
                     drawingNo: mappingResult.DrawingNo,
                     revNo: mappingResult.RevNo || "",
@@ -210,38 +281,38 @@ export function B4BulkUploadDialogV3({
              };
         }
 
-        // If server didn't validate it, fall back to local validation error or server error
-        if (!parseResult.valid || !parseResult.parsed) {
-            // It was invalid locally, and server didn't save it
-            return parseResult;
-        }
-
+        // Server validation failed - provide specific error messages
         if (!mappingResult) {
           return {
             ...parseResult,
+            valid: false,
             mappingStatus: "not_found" as const,
-            error: t("validation.notFound"),
+            error: "No matching drawing number found",
           };
         }
 
         if (mappingResult.MappingYN !== "Y") {
            return {
             ...parseResult,
+            valid: false,
             mappingStatus: "not_found" as const,
-            error: t("validation.notRegistered"), 
+            error: "Drawing not registered in the project", 
           };
         }
         
         if (mappingResult.DrawingMoveGbn !== "도면입수") {
             return {
                 ...parseResult,
+                valid: false,
                 mappingStatus: "not_found" as const,
-                error: t("validation.notGttDeliverables"),
+                error: "Not GTT Deliverables",
             };
         }
 
+        // Fallback (should not reach here)
         return {
           ...parseResult,
+          valid: false,
           mappingStatus: "available" as const,
           drawingName: mappingResult.DrawingName || undefined,
           registerGroupId: mappingResult.RegisterGroupId,
@@ -263,6 +334,12 @@ export function B4BulkUploadDialogV3({
 
   // Confirm Upload & Save (V3)
   const handleConfirmUpload = async (validFiles: FileValidationResult[]) => {
+    // Prevent duplicate calls
+    if (isUploading) {
+      console.warn("[V3 Dialog] Upload already in progress, ignoring duplicate call");
+      return;
+    }
+
     setIsUploading(true);
     setCurrentStep("uploading");
     setShowValidationDialog(false);
@@ -309,13 +386,69 @@ export function B4BulkUploadDialogV3({
       let completedGroups = 0;
       const results: B4BulkUploadResult["results"] = [];
 
-      // 2. Process each group
+      // 2. Prepare Metadata for Batch Save (Call ONCE)
+      const allMappingSaveItems: B4MappingSaveItem[] = [];
+      const groupUploadIdMap = new Map<string, string>();
+
+      for (const [groupKey, groupItems] of uploadGroups.entries()) {
+        const firstItemMapping = groupItems[0].mappingData;
+        // Reuse existing UploadId if present in API response, otherwise generate new one
+        const uploadId = firstItemMapping.UploadId || uuidv4();
+        groupUploadIdMap.set(groupKey, uploadId);
+
+        const groupMappingItems = groupItems.map((item) => {
+          const m = item.mappingData;
+          return {
+            CGbn: m.CGbn,
+            Category: "TS", // Hardcoded fixed value is required!
+            CheckBox: m.CheckBox,
+            DGbn: m.DGbn,
+            DegreeGbn: m.DegreeGbn,
+            DeptGbn: m.DeptGbn,
+            Discipline: m.Discipline,
+            DrawingKind: m.DrawingKind,
+            DrawingMoveGbn: m.DrawingMoveGbn,
+            DrawingName: m.DrawingName,
+            DrawingNo: m.DrawingNo,
+            DrawingUsage: m.DrawingUsage,
+            FileNm: item.file.name,
+            JGbn: m.JGbn,
+            Manager: m.Manager,
+            MappingYN: m.MappingYN,
+            NewOrNot: m.NewOrNot,
+            ProjectNo: projectNo,
+            RegisterGroup: m.RegisterGroup,
+            RegisterGroupId: m.RegisterGroupId,
+            RegisterKindCode: m.RegisterKindCode,
+            RegisterSerialNo: m.RegisterSerialNo,
+            RevNo: m.RevNo,
+            SGbn: m.SGbn,
+            UploadId: uploadId,
+            status: "Standby", // Hardcoded fixed value is required!
+          };
+        });
+        allMappingSaveItems.push(...groupMappingItems);
+      }
+
+      // Call API Once
+      try {
+        console.log(`[V3 Dialog] Saving metadata batch for ${allMappingSaveItems.length} items`);
+        await saveB4MappingBatch(allMappingSaveItems, {
+          userId,
+          userName,
+          vendorCode,
+          email: userEmail,
+        });
+      } catch (error) {
+        console.error("[V3 Dialog] Metadata save failed:", error);
+        throw new Error(
+          error instanceof Error ? error.message : t("bulkUpload.uploadError")
+        );
+      }
+
+      // 3. Process each group for Physical Upload
       for (const [groupKey, groupItems] of uploadGroups.entries()) {
-          // Reuse UploadId from the first item's mapping data if available, else generate new
-          const firstItemMapping = groupItems[0].mappingData;
-          // Reuse existing UploadId if present in API response, otherwise generate new one
-          // UploadId는 있으면 재활용하고, 없으면 UUID로 만들어서 사용
-          const uploadId = firstItemMapping.UploadId || uuidv4();
+          const uploadId = groupUploadIdMap.get(groupKey)!;
           
           console.log(`[V3 Dialog] Processing group ${groupKey}, UploadId: ${uploadId}`);
 
@@ -329,7 +462,7 @@ export function B4BulkUploadDialogV3({
                 )
              );
 
-             // A. Upload Files (Physical Upload)
+             // B. Upload Files (Physical Upload)
              const uploadResult = await uploadFilesWithProgress({
                 uploadId: uploadId,
                 userId: userId,
@@ -378,47 +511,6 @@ export function B4BulkUploadDialogV3({
                  throw new Error(uploadResult.error || "File upload failed");
              }
 
-             // B. Save Metadata (MatchBatchFileDwgEdit)
-             // Construct payload from mappingData + generated UploadId + hardcoded values as per prompt
-             const mappingSaveLists: B4MappingSaveItem[] = groupItems.map(item => {
-                 const m = item.mappingData;
-                 return {
-                    CGbn: m.CGbn,
-                    Category: "TS", // Hardcoded fixed value is required!
-                    CheckBox: m.CheckBox,
-                    DGbn: m.DGbn,
-                    DegreeGbn: m.DegreeGbn,
-                    DeptGbn: m.DeptGbn,
-                    Discipline: m.Discipline,
-                    DrawingKind: m.DrawingKind,
-                    DrawingMoveGbn: m.DrawingMoveGbn,
-                    DrawingName: m.DrawingName,
-                    DrawingNo: m.DrawingNo,
-                    DrawingUsage: m.DrawingUsage,
-                    FileNm: item.file.name,
-                    JGbn: m.JGbn,
-                    Manager: m.Manager,
-                    MappingYN: m.MappingYN,
-                    NewOrNot: m.NewOrNot,
-                    ProjectNo: projectNo,
-                    RegisterGroup: m.RegisterGroup,
-                    RegisterGroupId: m.RegisterGroupId,
-                    RegisterKindCode: m.RegisterKindCode,
-                    RegisterSerialNo: m.RegisterSerialNo,
-                    RevNo: m.RevNo,
-                    SGbn: m.SGbn,
-                    UploadId: uploadId, // Used for all files in this group
-                    status: "Standby", // Hardcoded fixed value is required!
-                 };
-             });
-
-             await saveB4MappingBatch(mappingSaveLists, {
-                userId,
-                userName,
-                vendorCode,
-                email: userEmail,
-             });
-
              console.log(`[V3 Dialog] Group ${groupKey} complete`);
              successCount += groupItems.length;
              
@@ -487,7 +579,13 @@ export function B4BulkUploadDialogV3({
   return (
     <>
       <Dialog open={open} onOpenChange={onOpenChange}>
-        <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
+        <DialogContent
+          className="max-w-2xl max-h-[85vh] overflow-y-auto"
+          onDragEnter={handleDragEnter}
+          onDragLeave={handleDragLeave}
+          onDragOver={handleDragOver}
+          onDrop={handleDrop}
+        >
           <DialogHeader>
             <DialogTitle>{t("bulkUpload.title")} (V3)</DialogTitle>
             <DialogDescription>
@@ -506,10 +604,6 @@ export function B4BulkUploadDialogV3({
                       ? "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"
-- 
cgit v1.2.3

