"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, saveB4MappingBatch, type MappingCheckItem, type B4BulkUploadResult, type B4MappingSaveItem, } from "../actions"; import { v4 as uuidv4 } from "uuid"; import { uploadFilesWithProgress } from "../utils/upload-with-progress"; import { FileUploadProgressList } from "../components/file-upload-progress-list"; import type { FileUploadProgress } from "../hooks/use-file-upload-with-progress"; 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); const [fileProgresses, setFileProgresses] = useState([]); // 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); setFileProgresses([]); } }, [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}개 파일`); // 0단계: 모든 파일에 대한 진행도 상태 초기화 const initialProgresses: FileUploadProgress[] = validFiles.map((fileResult) => ({ file: fileResult.file, progress: 0, status: "pending" as const, })); setFileProgresses(initialProgresses); // 파일을 DrawingNo + RevNo로 그룹화 const uploadGroups = new Map< string, Array<{ file: File; drawingNo: string; revNo: string; fileName: string; registerGroupId: number; fileIndex: number; // 전체 배열에서의 인덱스 }> >(); validFiles.forEach((fileResult, index) => { 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, fileIndex: index, }); }); 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(); // 그룹 내 모든 파일 상태를 uploading으로 변경 setFileProgresses((prev) => prev.map((fp, index) => files.some((f) => f.fileIndex === index) ? { ...fp, status: "uploading" as const } : fp ) ); // 2. 파일 업로드 (uploadFilesWithProgress 사용) const uploadResult = await uploadFilesWithProgress({ uploadId, userId, files: files.map((f) => f.file), callbacks: { onProgress: (fileIndexInGroup, progress) => { // 그룹 내 파일 인덱스를 전체 인덱스로 변환 const globalFileIndex = files[fileIndexInGroup].fileIndex; // 개별 파일 진행도 업데이트 setFileProgresses((prev) => prev.map((fp, index) => index === globalFileIndex ? { ...fp, progress, status: "uploading" as const } : fp ) ); // 전체 진행도 계산 const groupProgress = (completedGroups / uploadGroups.size) * 100; const currentGroupProgress = (progress / 100) * (100 / uploadGroups.size); setUploadProgress(Math.round(groupProgress + currentGroupProgress)); }, onFileComplete: (fileIndexInGroup) => { const globalFileIndex = files[fileIndexInGroup].fileIndex; setFileProgresses((prev) => prev.map((fp, index) => index === globalFileIndex ? { ...fp, progress: 100, status: "completed" as const } : fp ) ); }, onFileError: (fileIndexInGroup, error) => { const globalFileIndex = files[fileIndexInGroup].fileIndex; console.error(`[B4 업로드] 파일 ${globalFileIndex} 업로드 실패:`, error); setFileProgresses((prev) => prev.map((fp, index) => index === globalFileIndex ? { ...fp, status: "error" as const, error } : fp ) ); }, }, }); if (!uploadResult.success) { throw new Error(uploadResult.error || "파일 업로드 실패"); } console.log(`[B4 업로드] 그룹 ${groupKey} 파일 업로드 완료`); // 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} 매핑 정보 저장 완료`); 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}%
{/* 개별 파일 진행도 리스트 */} {fileProgresses.length > 0 && (
)}
)} {/* 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" && ( <> )} )}
{/* 검증 다이얼로그 */} { setShowValidationDialog(open); if (!open) { onOpenChange(false); // 검증 다이얼로그가 닫히면 메인 다이얼로그도 닫기 } }} validationResults={validationResults} onConfirmUpload={handleConfirmUpload} isUploading={isUploading} lng={lng} /> ); }