diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-27 22:05:20 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-27 22:05:20 +0900 |
| commit | 33f8dc52beefb8d23233643c775879787267e4c5 (patch) | |
| tree | b9741c98ac7b971bc45a5aa32c3b4008cf9900a8 | |
| parent | 0362ee43518bc6e85ec5973697ff7b0210a53be8 (diff) | |
(김준회) dolce: 상세도면 Standby 상태는 MOD(수정) 지원 처리
| -rw-r--r-- | app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx | 33 | ||||
| -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 |
3 files changed, 194 insertions, 87 deletions
diff --git a/app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx b/app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx index e607588f..b3d24504 100644 --- a/app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx +++ b/app/[lng]/partners/(partners)/document-list-ship/dolce-upload-page-v2.tsx @@ -86,6 +86,8 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro // 다이얼로그 const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false); + const [dialogMode, setDialogMode] = useState<"add" | "edit">("add"); + const [editingDetail, setEditingDetail] = useState<DetailDwgReceiptItem | null>(null); const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false); // 초기 데이터 로드 @@ -299,6 +301,13 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro } }; + // 상세도면 수정 핸들러 + const handleEditDetail = (detail: DetailDwgReceiptItem) => { + setDialogMode("edit"); + setEditingDetail(detail); + setAddDialogOpen(true); + }; + // 필터된 도면 목록 (클라이언트 사이드 필터링) const filteredDrawings = useMemo(() => { let result = drawings.filter((drawing) => { @@ -543,17 +552,15 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro <DrawingListTableV2 columns={ vendorInfo.drawingKind === "B4" - ? (createGttDrawingListColumns({ documentType, lng, t }) as unknown as typeof drawingListColumns) - : (drawingListColumns(lng, t) as unknown as typeof drawingListColumns) + ? (createGttDrawingListColumns({ documentType, lng, t }) as any) + : (drawingListColumns(lng, t) as any) } data={filteredDrawings} onRowClick={handleDrawingClick} selectedRow={selectedDrawing || undefined} getRowId={getDrawingId} - selectedRow={selectedDrawing || undefined} - getRowId={getDrawingId} minHeight="0" - defaultPageSize={10} + defaultPageSize={"all"} /> )} </CardContent> @@ -585,7 +592,11 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro <Button variant="default" size="sm" - onClick={() => setAddDialogOpen(true)} + onClick={() => { + setDialogMode("add"); + setEditingDetail(null); + setAddDialogOpen(true); + }} disabled={!selectedDrawing} > <Plus className="h-4 w-4 mr-2" /> @@ -599,7 +610,7 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro <div className="h-full flex items-center justify-center text-muted-foreground p-4" style={{ minHeight: "400px" }}> <div className="text-center"> <InfoIcon className="h-12 w-12 mx-auto mb-2 opacity-50" /> - <p>도면을 선택해주세요</p> + <p>Select a drawing</p> </div> </div> ) : isLoadingDetails ? ( @@ -608,13 +619,13 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro </div> ) : ( <DrawingListTableV2<DetailDwgReceiptItem, unknown> - columns={createDetailDrawingColumns(lng, t)} + columns={createDetailDrawingColumns(lng, t, handleEditDetail)} data={detailDrawings} onRowClick={setSelectedDetail} selectedRow={selectedDetail || undefined} getRowId={getDetailDrawingId} minHeight="0" - defaultPageSize={10} + defaultPageSize={"all"} /> )} </CardContent> @@ -659,7 +670,7 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro columns={fileColumns} data={files} minHeight="0" - defaultPageSize={10} + defaultPageSize={"all"} /> )} </CardContent> @@ -696,6 +707,8 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro onComplete={handleAddComplete} drawingKind={vendorInfo.drawingKind} lng={lng} + mode={dialogMode} + detailDrawing={editingDetail} /> )} 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>({ |
