"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, prepareB4DetailDrawingsV2, type B4BulkUploadResult, type GttDwgReceiptItem, } from "../actions"; 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 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("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(); }; // 검증 시작 (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(); 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: prepareB4DetailDrawingsV2 + uploadFilesWithProgress 사용) const handleConfirmUpload = async (validFiles: FileValidationResult[]) => { setIsUploading(true); setCurrentStep("uploading"); setShowValidationDialog(false); try { console.log(`[V2 Dialog] 업로드 시작: ${validFiles.length}개 파일`); // 0단계: 모든 파일에 대한 진행도 상태 초기화 const initialProgresses: FileUploadProgress[] = validFiles.map((fileResult) => ({ file: fileResult.file, progress: 0, status: "pending" as const, })); setFileProgresses(initialProgresses); // 파일 인덱스 맵 생성 (파일명 기반) const fileIndexMap = new Map(); validFiles.forEach((fileResult, index) => { fileIndexMap.set(fileResult.file.name, index); }); // 1단계: DrawingNo + RevNo별로 그룹화 // - 동일한 Drawing/Revision에 속하는 파일들을 하나의 그룹으로 묶음 // - 이렇게 하면 같은 리비전의 상세도면을 1번만 생성/조회함 const uploadGroups = new Map< string, { drawingNo: string; revNo: string; files: File[]; fileIndices: number[]; // 전체 배열에서의 인덱스 } >(); validFiles.forEach((fileResult, index) => { const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`; if (!uploadGroups.has(groupKey)) { uploadGroups.set(groupKey, { drawingNo: fileResult.parsed!.drawingNo, revNo: fileResult.parsed!.revNo, files: [], fileIndices: [], }); } uploadGroups.get(groupKey)!.files.push(fileResult.file); uploadGroups.get(groupKey)!.fileIndices.push(index); }); console.log( `[V2 Dialog] ${uploadGroups.size}개 리비전 그룹 생성 (${validFiles.length}개 파일)` ); // 2단계: 상세도면 준비 (서버 액션) // - 각 리비전별로 상세도면 존재 여부 확인 // - 기존 상세도면이 있으면 uploadId 재사용 // - 없으면 새로 생성 (1번만!) const drawingRevisions = Array.from(uploadGroups.values()).map((group) => ({ drawingNo: group.drawingNo, revNo: group.revNo, })); console.log(`[V2 Dialog] 상세도면 준비 요청: ${drawingRevisions.length}개 리비전`); const prepareResult = await prepareB4DetailDrawingsV2({ projectNo, userId, userNm: userName, email: userEmail, vendorCode, registerKind, drawingRevisions, }); if (!prepareResult.success || !prepareResult.detailDrawings) { throw new Error(prepareResult.error || "상세도면 준비 실패"); } const newDrawings = prepareResult.detailDrawings.filter((d) => d.isNew); const existingDrawings = prepareResult.detailDrawings.filter((d) => !d.isNew); console.log( `[V2 Dialog] 상세도면 준비 완료: 총 ${prepareResult.detailDrawings.length}개 ` + `(기존 ${existingDrawings.length}개 재사용, 신규 ${newDrawings.length}개 생성)` ); // 3단계: 각 그룹별로 파일 업로드 // - 준비된 uploadId를 사용하여 파일 업로드 const detailDrawingMap = new Map( prepareResult.detailDrawings.map((d) => [`${d.drawingNo}_${d.revNo}`, d]) ); let successCount = 0; let failCount = 0; let completedGroups = 0; const results: B4BulkUploadResult["results"] = []; for (const [groupKey, group] of uploadGroups.entries()) { try { const detailDrawing = detailDrawingMap.get(groupKey); if (!detailDrawing) { throw new Error(`상세도면 정보를 찾을 수 없습니다: ${groupKey}`); } console.log( `[V2 Dialog] 그룹 ${groupKey} 업로드 시작\n` + ` - 파일 수: ${group.files.length}개\n` + ` - UploadId: ${detailDrawing.uploadId}\n` + ` - 상태: ${detailDrawing.isNew ? "신규 생성" : "기존 재사용"}` ); // 그룹 내 모든 파일 상태를 uploading으로 변경 setFileProgresses((prev) => prev.map((fp, index) => group.fileIndices.includes(index) ? { ...fp, status: "uploading" as const } : fp ) ); // uploadFilesWithProgress 사용 (클라이언트 fetch) const uploadResult = await uploadFilesWithProgress({ uploadId: detailDrawing.uploadId, userId: userId, files: group.files, callbacks: { onProgress: (fileIndexInGroup, progress) => { // 그룹 내 파일 인덱스를 전체 인덱스로 변환 const globalFileIndex = group.fileIndices[fileIndexInGroup]; // 개별 파일 진행도 업데이트 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 = group.fileIndices[fileIndexInGroup]; setFileProgresses((prev) => prev.map((fp, index) => index === globalFileIndex ? { ...fp, progress: 100, status: "completed" as const } : fp ) ); }, onFileError: (fileIndexInGroup, error) => { const globalFileIndex = group.fileIndices[fileIndexInGroup]; console.error(`[V2 Dialog] 파일 ${globalFileIndex} 업로드 실패:`, error); setFileProgresses((prev) => prev.map((fp, index) => index === globalFileIndex ? { ...fp, status: "error" as const, error } : fp ) ); }, }, }); if (uploadResult.success) { console.log( `[V2 Dialog] ✓ 그룹 ${groupKey} 업로드 완료 (${group.files.length}개 파일)` ); successCount += group.files.length; // 성공 결과 추가 group.files.forEach((file) => { results?.push({ drawingNo: group.drawingNo, revNo: group.revNo, fileName: file.name, success: true, }); }); } else { throw new Error(uploadResult.error || "파일 업로드 실패"); } } catch (error) { console.error( `[V2 Dialog] ✗ 그룹 ${groupKey} 업로드 실패 (${group.files.length}개 파일):`, error ); const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; failCount += group.files.length; // 실패 결과 추가 group.files.forEach((file) => { results?.push({ drawingNo: group.drawingNo, revNo: group.revNo, fileName: file.name, success: false, error: errorMessage, }); }); } completedGroups++; setUploadProgress(Math.round((completedGroups / uploadGroups.size) * 100)); } console.log( `[V2 Dialog] ========================================\n` + `[V2 Dialog] 일괄 업로드 최종 결과\n` + `[V2 Dialog] - 총 파일 수: ${validFiles.length}개\n` + `[V2 Dialog] - 성공: ${successCount}개\n` + `[V2 Dialog] - 실패: ${failCount}개\n` + `[V2 Dialog] - 처리된 리비전: ${uploadGroups.size}개\n` + `[V2 Dialog] ========================================` ); const result: B4BulkUploadResult = { success: successCount > 0, successCount, failCount, results, }; 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 ( <> {t("bulkUpload.title")} (V2) {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-v2" />
{/* 선택된 파일 목록 */} {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 })}

)}
)}
{/* 푸터 버튼 */} {currentStep !== "uploading" && currentStep !== "complete" && currentStep !== "validation" && ( {currentStep === "settings" && ( <> )} {currentStep === "files" && ( <> )} )}
{/* 검증 다이얼로그 */} { setShowValidationDialog(open); if (!open) { onOpenChange(false); } }} validationResults={validationResults} onConfirmUpload={handleConfirmUpload} isUploading={isUploading} /> ); }