diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx | 246 | ||||
| -rw-r--r-- | lib/dolce/table/drawing-list-table-v2.tsx | 2 |
2 files changed, 171 insertions, 77 deletions
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" diff --git a/lib/dolce/table/drawing-list-table-v2.tsx b/lib/dolce/table/drawing-list-table-v2.tsx index 1274a148..929a1252 100644 --- a/lib/dolce/table/drawing-list-table-v2.tsx +++ b/lib/dolce/table/drawing-list-table-v2.tsx @@ -44,7 +44,7 @@ interface DrawingListTableV2Props<TData extends DrawingData, TValue> { getRowId?: (row: TData) => string; maxHeight?: string; // e.g., "45vh" minHeight?: string; // e.g., "400px" - defaultPageSize?: number; + defaultPageSize?: number | "all" } export function DrawingListTableV2<TData extends DrawingData, TValue>({ |
