"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 { checkB4MappingStatus, editDetailDwgReceipt, type MappingCheckItem, type B4BulkUploadResult, } from "../actions"; import { v4 as uuidv4 } from "uuid"; interface B4BulkUploadDialogProps { 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 B4BulkUploadDialog({ open, onOpenChange, projectNo, userId, userName, userEmail, vendorCode, onUploadComplete, lng, }: B4BulkUploadDialogProps) { const { t } = useTranslation(lng, "dolce"); const [currentStep, setCurrentStep] = useState("settings"); const [drawingUsage, setDrawingUsage] = useState("REC"); const [registerKind, setRegisterKind] = useState(""); const [selectedFiles, setSelectedFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [validationResults, setValidationResults] = useState([]); const [showValidationDialog, setShowValidationDialog] = useState(false); const [isDragging, setIsDragging] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadResult, setUploadResult] = useState(null); // B4 GTT 옵션 (코드 번역 유틸리티 사용) const drawingUsageOptions = [ { value: "REC", label: t("bulkUpload.drawingUsageReceive") }, ]; const registerKindOptionsMap: Record> = { 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(); }; // 검증 시작 const handleValidate = async () => { try { // 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단계: 매핑 현황 조회 const mappingCheckItems: MappingCheckItem[] = parsedFiles.map((r) => ({ DrawingNo: r.parsed!.drawingNo, RevNo: r.parsed!.revNo, FileNm: r.file.name, })); const mappingResults = await checkB4MappingStatus( projectNo, mappingCheckItems ); // 3단계: 검증 결과 병합 const finalResults: FileValidationResult[] = parseResults.map((parseResult) => { if (!parseResult.valid || !parseResult.parsed) { return parseResult; } // 매핑 결과 찾기 const mappingResult = mappingResults.find( (m) => m.DrawingNo === parseResult.parsed!.drawingNo && m.RevNo === parseResult.parsed!.revNo ); if (!mappingResult) { return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notFound"), }; } // RegisterGroupId가 0이거나 MappingYN이 N이면 도면이 존재하지 않음 if (mappingResult.RegisterGroupId === 0 || mappingResult.MappingYN === "N") { return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notRegistered"), }; } // DrawingMoveGbn이 "도면입수"가 아니면 업로드 불가 if (mappingResult.DrawingMoveGbn !== "도면입수") { return { ...parseResult, mappingStatus: "not_found" as const, error: t("validation.notGttDeliverables"), }; } // MappingYN이 Y이고 도면입수인 경우 업로드 가능 return { ...parseResult, mappingStatus: "available" as const, drawingName: mappingResult.DrawingName || undefined, registerGroupId: mappingResult.RegisterGroupId, }; }); setValidationResults(finalResults); setShowValidationDialog(true); } catch (error) { console.error("검증 실패:", error); toast.error( error instanceof Error ? error.message : t("bulkUpload.validationError") ); } }; // 업로드 확인 const handleConfirmUpload = async (validFiles: FileValidationResult[]) => { setIsUploading(true); setCurrentStep("uploading"); setShowValidationDialog(false); try { console.log(`[B4 일괄 업로드] 시작: ${validFiles.length}개 파일`); // 파일을 DrawingNo + RevNo로 그룹화 const uploadGroups = new Map< string, Array<{ file: File; drawingNo: string; revNo: string; fileName: string; registerGroupId: number; }> >(); validFiles.forEach((fileResult) => { const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`; if (!uploadGroups.has(groupKey)) { uploadGroups.set(groupKey, []); } uploadGroups.get(groupKey)!.push({ file: fileResult.file, drawingNo: fileResult.parsed!.drawingNo, revNo: fileResult.parsed!.revNo, fileName: fileResult.file.name, registerGroupId: fileResult.registerGroupId || 0, }); }); console.log(`[B4 일괄 업로드] ${uploadGroups.size}개 그룹으로 묶임`); let successCount = 0; let failCount = 0; let completedGroups = 0; // 각 그룹별로 순차 처리 for (const [groupKey, files] of uploadGroups.entries()) { const { drawingNo, revNo, registerGroupId } = files[0]; try { console.log(`[B4 업로드] 그룹 ${groupKey}: ${files.length}개 파일`); // 1. UploadId 생성 const uploadId = uuidv4(); // 2. 파일 업로드 (공통 API 사용) const formData = new FormData(); formData.append("uploadId", uploadId); formData.append("userId", userId); formData.append("fileCount", String(files.length)); files.forEach((fileInfo, index) => { formData.append(`file_${index}`, fileInfo.file); }); const uploadResponse = await fetch("/api/dolce/upload-files", { method: "POST", body: formData, }); if (!uploadResponse.ok) { throw new Error(`파일 업로드 실패: ${uploadResponse.status}`); } const uploadResult = await uploadResponse.json(); if (!uploadResult.success) { throw new Error(uploadResult.error || "파일 업로드 실패"); } 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, }); console.log(`[B4 업로드] 그룹 ${groupKey} 상세도면 등록 완료`); successCount += files.length; } catch (error) { console.error(`[B4 업로드] 그룹 ${groupKey} 실패:`, error); failCount += files.length; } // 진행도 업데이트 completedGroups++; const progress = Math.round((completedGroups / uploadGroups.size) * 100); setUploadProgress(progress); } console.log(`[B4 일괄 업로드] ✅ 완료: 성공 ${successCount}, 실패 ${failCount}`); const result: B4BulkUploadResult = { success: true, successCount, failCount, }; setUploadResult(result); setCurrentStep("complete"); toast.success(t("bulkUpload.uploadSuccessToast", { successCount, total: validFiles.length })); } catch (error) { console.error("[B4 일괄 업로드] 실패:", 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 ( <> {t("bulkUpload.title")} {currentStep === "settings" && t("bulkUpload.stepSettings")} {currentStep === "files" && t("bulkUpload.stepFiles")} {currentStep === "validation" && t("bulkUpload.stepValidation")}
{/* 1단계: 설정 입력 */} {currentStep === "settings" && ( <> {/* 도면용도 선택 */}
{/* 등록종류 선택 */}

{t("bulkUpload.registerKindNote")}

)} {/* 2단계: 파일 선택 */} {currentStep === "files" && ( <> {/* 파일 선택 영역 */}
handleFilesChange(Array.from(e.target.files || []))} className="hidden" id="b4-file-upload" />
{/* 선택된 파일 목록 */} {selectedFiles.length > 0 && (

{t("bulkUpload.selectedFiles", { count: selectedFiles.length })}

{selectedFiles.map((file, index) => (

{file.name}

{(file.size / 1024 / 1024).toFixed(2)} MB

))}
)} )} {/* 3단계: 검증 중 표시 */} {currentStep === "validation" && (

{t("bulkUpload.validating")}

)} {/* 4단계: 업로드 진행 중 */} {currentStep === "uploading" && (

{t("bulkUpload.uploading")}

{t("bulkUpload.uploadingWait")}

{t("bulkUpload.uploadProgress")} {uploadProgress}%
{/* 90% 이상일 때 추가 안내 메시지 */} {uploadProgress >= 90 && uploadProgress < 100 && (

{t("bulkUpload.uploadingToServer")}

)}
)} {/* 5단계: 업로드 완료 */} {currentStep === "complete" && uploadResult && (

{t("bulkUpload.uploadComplete")}

{t("bulkUpload.uploadSuccessMessage", { count: uploadResult.successCount })}

{uploadResult.failCount && uploadResult.failCount > 0 && (

{t("bulkUpload.uploadFailMessage", { count: uploadResult.failCount })}

)}
)}
{/* 푸터 버튼 (uploading, complete 단계에서는 숨김) */} {currentStep !== "uploading" && currentStep !== "complete" && currentStep !== "validation" && ( {currentStep === "settings" && ( <> )} {currentStep === "files" && ( <> )} )}
{/* 검증 다이얼로그 */} ); }