diff options
Diffstat (limited to 'lib/dolce/dialogs')
| -rw-r--r-- | lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx | 625 | ||||
| -rw-r--r-- | lib/dolce/dialogs/b4-bulk-upload-dialog.tsx | 87 |
2 files changed, 680 insertions, 32 deletions
diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx new file mode 100644 index 00000000..3207c00b --- /dev/null +++ b/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx @@ -0,0 +1,625 @@ +"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 { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +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 "./b4-upload-validation-dialog"; +import { + fetchDwgReceiptList, + bulkUploadB4FilesV2, + type B4BulkUploadResult, + type GttDwgReceiptItem, +} from "../actions"; + +interface B4BulkUploadDialogV2Props { + open: boolean; + onOpenChange: (open: boolean) => void; + projectNo: string; + userId: string; + userName: string; + userEmail: string; + vendorCode: string; + onUploadComplete?: () => void; + lng: string; +} + +type UploadStep = "settings" | "files" | "validation" | "uploading" | "complete"; + +export function B4BulkUploadDialogV2({ + open, + onOpenChange, + projectNo, + userId, + userName, + userEmail, + vendorCode, + onUploadComplete, + lng, +}: B4BulkUploadDialogV2Props) { + const { t } = useTranslation(lng, "dolce"); + const [currentStep, setCurrentStep] = useState<UploadStep>("settings"); + const [drawingUsage, setDrawingUsage] = useState<string>("REC"); + const [registerKind, setRegisterKind] = useState<string>(""); + const [selectedFiles, setSelectedFiles] = useState<File[]>([]); + const [isUploading, setIsUploading] = useState(false); + const [validationResults, setValidationResults] = useState<FileValidationResult[]>([]); + const [showValidationDialog, setShowValidationDialog] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadResult, setUploadResult] = useState<B4BulkUploadResult | null>(null); + + // B4 GTT 옵션 + const drawingUsageOptions = [ + { value: "REC", label: t("bulkUpload.drawingUsageReceive") }, + ]; + + const registerKindOptionsMap: Record<string, Array<{ value: string; label: string }>> = { + REC: [ + { value: "RECP", label: t("bulkUpload.registerKindRecP") }, + { value: "RECW", label: t("bulkUpload.registerKindRecW") }, + ], + }; + + // 다이얼로그 닫을 때 초기화 + React.useEffect(() => { + if (!open) { + setCurrentStep("settings"); + setDrawingUsage("REC"); + setRegisterKind(""); + setSelectedFiles([]); + setValidationResults([]); + setShowValidationDialog(false); + setIsDragging(false); + setUploadProgress(0); + setUploadResult(null); + } + }, [open]); + + // 파일 선택 핸들러 + 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 핸들러 + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.currentTarget === e.target) { + 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)); + }; + + // 1단계 완료 (설정) + const handleSettingsNext = () => { + if (!registerKind) { + toast.error(t("bulkUpload.selectRegisterKindError")); + return; + } + setCurrentStep("files"); + }; + + // 2단계 완료 (파일 선택) + const handleFilesNext = () => { + if (selectedFiles.length === 0) { + toast.error(t("bulkUpload.selectFilesError")); + return; + } + setCurrentStep("validation"); + handleValidate(); + }; + + // 검증 시작 (V2: fetchDwgReceiptList 사용) + const handleValidate = async () => { + try { + console.log("[V2 Dialog] 검증 시작"); + + // 1단계: 파일명 파싱 + 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단계: DrawingNo별로 도면 정보 조회 + const drawingNoSet = new Set(parsedFiles.map((r) => r.parsed!.drawingNo)); + const drawingInfoMap = new Map<string, GttDwgReceiptItem>(); + + console.log(`[V2 Dialog] ${drawingNoSet.size}개 도면번호 조회`); + + for (const drawingNo of drawingNoSet) { + try { + const dwgList = await fetchDwgReceiptList({ + project: projectNo, + drawingKind: "B4", + drawingMoveGbn: "도면입수", + drawingNo: drawingNo, + }); + + // 해당 DrawingNo 찾기 + const dwgInfo = dwgList.find( + (d) => (d as GttDwgReceiptItem).DrawingNo === drawingNo + ) as GttDwgReceiptItem | undefined; + + if (dwgInfo) { + drawingInfoMap.set(drawingNo, dwgInfo); + console.log(`[V2 Dialog] 도면 정보 조회 완료: ${drawingNo}`); + } else { + console.log(`[V2 Dialog] 도면 정보 없음: ${drawingNo}`); + } + } catch (error) { + console.error(`[V2 Dialog] 도면 정보 조회 실패: ${drawingNo}`, error); + } + } + + // 3단계: 검증 결과 병합 + const finalResults: FileValidationResult[] = parseResults.map((parseResult) => { + if (!parseResult.valid || !parseResult.parsed) { + return parseResult; + } + + const drawingInfo = drawingInfoMap.get(parseResult.parsed.drawingNo); + + if (!drawingInfo) { + return { + ...parseResult, + mappingStatus: "not_found" as const, + error: t("validation.notRegistered"), + }; + } + + // DrawingMoveGbn이 "도면입수"가 아니면 업로드 불가 + if (drawingInfo.DrawingMoveGbn !== "도면입수") { + return { + ...parseResult, + mappingStatus: "not_found" as const, + error: t("validation.notGttDeliverables"), + }; + } + + // 업로드 가능 + return { + ...parseResult, + mappingStatus: "available" as const, + drawingName: drawingInfo.DrawingName || undefined, + registerGroupId: drawingInfo.RegisterGroupId, + }; + }); + + console.log("[V2 Dialog] 검증 완료"); + setValidationResults(finalResults); + setShowValidationDialog(true); + } catch (error) { + console.error("[V2 Dialog] 검증 실패:", error); + toast.error( + error instanceof Error ? error.message : t("bulkUpload.validationError") + ); + } + }; + + // 업로드 확인 (V2: bulkUploadB4FilesV2 사용) + const handleConfirmUpload = async (validFiles: FileValidationResult[]) => { + setIsUploading(true); + setCurrentStep("uploading"); + setShowValidationDialog(false); + + try { + console.log(`[V2 Dialog] 업로드 시작: ${validFiles.length}개 파일`); + + // 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", registerKind); + formData.append("fileCount", String(validFiles.length)); + + validFiles.forEach((fileResult, index) => { + formData.append(`file_${index}`, fileResult.file); + }); + + // 업로드 프로그레스 시뮬레이션 + const progressInterval = setInterval(() => { + setUploadProgress((prev) => { + if (prev >= 90) return 90; + return prev + 10; + }); + }, 500); + + // V2 함수 호출 + const result = await bulkUploadB4FilesV2(formData); + + clearInterval(progressInterval); + setUploadProgress(100); + + console.log("[V2 Dialog] 업로드 완료:", result); + + setUploadResult(result); + setCurrentStep("complete"); + + if (result.success) { + toast.success( + t("bulkUpload.uploadSuccessToast", { + successCount: result.successCount, + total: validFiles.length, + }) + ); + } else { + toast.error(result.error || t("bulkUpload.uploadError")); + } + } catch (error) { + console.error("[V2 Dialog] 업로드 실패:", error); + toast.error( + error instanceof Error ? error.message : t("bulkUpload.uploadError") + ); + setCurrentStep("files"); + } finally { + setIsUploading(false); + } + }; + + const registerKindOptions = drawingUsage + ? registerKindOptionsMap[drawingUsage] || [] + : []; + + const handleDrawingUsageChange = (value: string) => { + setDrawingUsage(value); + setRegisterKind(""); + }; + + return ( + <> + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-2xl"> + <DialogHeader> + <DialogTitle>{t("bulkUpload.title")} (V2)</DialogTitle> + <DialogDescription> + {currentStep === "settings" && t("bulkUpload.stepSettings")} + {currentStep === "files" && t("bulkUpload.stepFiles")} + {currentStep === "validation" && t("bulkUpload.stepValidation")} + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {/* 1단계: 설정 입력 */} + {currentStep === "settings" && ( + <> + {/* 도면용도 선택 */} + <div className="space-y-2"> + <Label>{t("bulkUpload.drawingUsage")} *</Label> + <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}> + <SelectTrigger> + <SelectValue placeholder={t("bulkUpload.drawingUsagePlaceholder")} /> + </SelectTrigger> + <SelectContent> + {drawingUsageOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + + {/* 등록종류 선택 */} + <div className="space-y-2"> + <Label>{t("bulkUpload.registerKind")} *</Label> + <Select + value={registerKind} + onValueChange={setRegisterKind} + disabled={!drawingUsage} + > + <SelectTrigger> + <SelectValue placeholder={t("bulkUpload.registerKindPlaceholder")} /> + </SelectTrigger> + <SelectContent> + {registerKindOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <p className="text-sm text-muted-foreground"> + {t("bulkUpload.registerKindNote")} + </p> + </div> + </> + )} + + {/* 2단계: 파일 선택 */} + {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-v2" + /> + <label + htmlFor="b4-file-upload-v2" + 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 transition-colors ${ + isDragging + ? "text-primary font-medium" + : "text-muted-foreground" + }`} + > + {isDragging + ? t("bulkUpload.fileDropHere") + : t("bulkUpload.fileSelectArea")} + </p> + <p className="text-xs text-muted-foreground mt-1"> + {t("bulkUpload.fileTypes")} + </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" + > + <div className="flex-1 min-w-0"> + <p className="text-sm truncate">{file.name}</p> + <p className="text-xs text-muted-foreground"> + {(file.size / 1024 / 1024).toFixed(2)} MB + </p> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveFile(index)} + > + {t("bulkUpload.removeFile")} + </Button> + </div> + ))} + </div> + </div> + )} + </> + )} + + {/* 3단계: 검증 중 표시 */} + {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> + )} + + {/* 4단계: 업로드 진행 중 */} + {currentStep === "uploading" && ( + <div className="space-y-6 py-8"> + <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">{t("bulkUpload.uploading")}</h3> + <p className="text-sm text-muted-foreground"> + {t("bulkUpload.uploadingWait")} + </p> + </div> + <div className="space-y-2"> + <div className="flex justify-between text-sm"> + <span>{t("bulkUpload.uploadProgress")}</span> + <span>{uploadProgress}%</span> + </div> + <Progress value={uploadProgress} className="h-2" /> + {uploadProgress >= 90 && uploadProgress < 100 && ( + <p className="text-xs text-muted-foreground text-center pt-2"> + {t("bulkUpload.uploadingToServer")} + </p> + )} + </div> + </div> + )} + + {/* 5단계: 업로드 완료 */} + {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">{t("bulkUpload.uploadComplete")}</h3> + <p className="text-sm text-muted-foreground"> + {t("bulkUpload.uploadSuccessMessage", { count: uploadResult.successCount })} + </p> + </div> + + {uploadResult.failCount && uploadResult.failCount > 0 && ( + <div className="bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4"> + <p className="text-sm text-yellow-800 dark:text-yellow-200"> + {t("bulkUpload.uploadFailMessage", { count: uploadResult.failCount })} + </p> + </div> + )} + + <div className="flex justify-center"> + <Button + onClick={() => { + onOpenChange(false); + onUploadComplete?.(); + }} + > + {t("bulkUpload.confirmButton")} + </Button> + </div> + </div> + )} + </div> + + {/* 푸터 버튼 */} + {currentStep !== "uploading" && currentStep !== "complete" && currentStep !== "validation" && ( + <DialogFooter> + {currentStep === "settings" && ( + <> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + > + {t("bulkUpload.cancelButton")} + </Button> + <Button + onClick={handleSettingsNext} + disabled={!registerKind} + > + {t("bulkUpload.nextButton")} + <ChevronRight className="ml-2 h-4 w-4" /> + </Button> + </> + )} + + {currentStep === "files" && ( + <> + <Button + variant="outline" + onClick={() => setCurrentStep("settings")} + > + <ChevronLeft className="mr-2 h-4 w-4" /> + {t("bulkUpload.previousButton")} + </Button> + <Button + onClick={handleFilesNext} + disabled={selectedFiles.length === 0} + > + {t("bulkUpload.validateButton")} + <ChevronRight className="ml-2 h-4 w-4" /> + </Button> + </> + )} + </DialogFooter> + )} + </DialogContent> + </Dialog> + + {/* 검증 다이얼로그 */} + <B4UploadValidationDialog + open={showValidationDialog} + onOpenChange={(open) => { + setShowValidationDialog(open); + if (!open) { + onOpenChange(false); + } + }} + validationResults={validationResults} + onConfirmUpload={handleConfirmUpload} + isUploading={isUploading} + /> + </> + ); +} + diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx index 1be7f226..21647e63 100644 --- a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx +++ b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx @@ -30,9 +30,10 @@ import { } from "./b4-upload-validation-dialog"; import { checkB4MappingStatus, - editDetailDwgReceipt, + saveB4MappingBatch, type MappingCheckItem, type B4BulkUploadResult, + type B4MappingSaveItem, } from "../actions"; import { v4 as uuidv4 } from "uuid"; @@ -343,37 +344,54 @@ export function B4BulkUploadDialog({ console.log(`[B4 업로드] 그룹 ${groupKey} 파일 업로드 완료`); - // 3. 상세도면 등록 - await editDetailDwgReceipt({ - dwgList: [ - { - Mode: "ADD", - Status: "Draft", - RegisterId: 0, - ProjectNo: projectNo, - Discipline: "", - DrawingKind: "B4", - DrawingNo: drawingNo, - DrawingName: "", - RegisterGroupId: registerGroupId, - RegisterSerialNo: 0, - RegisterKind: registerKind, - DrawingRevNo: revNo, - Category: "TS", - Receiver: null, - Manager: "", - RegisterDesc: "", - UploadId: uploadId, - RegCompanyCode: vendorCode, - }, - ], - userId, - userNm: userName, - vendorCode, - email: userEmail, - }); + // 3. 매핑 현황 재조회 (MatchBatchFileDwg) + const mappingCheckResults = await checkB4MappingStatus(projectNo, [ + { + DrawingNo: drawingNo, + RevNo: revNo, + FileNm: files[0].fileName, + }, + ]); + + const mappingData = mappingCheckResults[0]; + if (!mappingData || mappingData.RegisterGroupId === 0) { + throw new Error(`매핑 정보를 찾을 수 없습니다: ${groupKey}`); + } + + console.log(`[B4 업로드] 그룹 ${groupKey} 매핑 정보 조회 완료`); + + // 4. 매핑 정보 저장 (MatchBatchFileDwgEdit) + const mappingSaveItem: B4MappingSaveItem = { + CGbn: mappingData.CGbn, + Category: mappingData.Category, + CheckBox: "0", + DGbn: mappingData.DGbn, + DegreeGbn: mappingData.DegreeGbn, + DeptGbn: mappingData.DeptGbn, + Discipline: mappingData.Discipline, + DrawingKind: "B4", + DrawingMoveGbn: "도면입수", + DrawingName: mappingData.DrawingName, + DrawingNo: drawingNo, + DrawingUsage: "입수용", + FileNm: files[0].fileName, + JGbn: mappingData.JGbn, + Manager: mappingData.Manager || "970043", + MappingYN: "Y", + NewOrNot: "N", + ProjectNo: projectNo, + RegisterGroup: 0, + RegisterGroupId: registerGroupId, + RegisterKindCode: registerKind, + RegisterSerialNo: mappingData.RegisterSerialNo, + RevNo: revNo, + SGbn: mappingData.SGbn, + UploadId: uploadId, + }; + + await saveB4MappingBatch([mappingSaveItem], userId); - console.log(`[B4 업로드] 그룹 ${groupKey} 상세도면 등록 완료`); + console.log(`[B4 업로드] 그룹 ${groupKey} 매핑 정보 저장 완료`); successCount += files.length; } catch (error) { @@ -685,7 +703,12 @@ export function B4BulkUploadDialog({ {/* 검증 다이얼로그 */} <B4UploadValidationDialog open={showValidationDialog} - onOpenChange={setShowValidationDialog} + onOpenChange={(open) => { + setShowValidationDialog(open); + if (!open) { + onOpenChange(false); // 검증 다이얼로그가 닫히면 메인 다이얼로그도 닫기 + } + }} validationResults={validationResults} onConfirmUpload={handleConfirmUpload} isUploading={isUploading} |
