From fd4909bba7be8abc1eeab9ae1b4621c66a61604a Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Sun, 23 Nov 2025 16:40:37 +0900
Subject: (김준회) 돌체 재개발 - 1차 (다운로드 오류 수정 필요)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/dolce/dialogs/b4-bulk-upload-dialog.tsx | 608 ++++++++++++++++++++++++++++
 1 file changed, 608 insertions(+)
 create mode 100644 lib/dolce/dialogs/b4-bulk-upload-dialog.tsx

(limited to 'lib/dolce/dialogs/b4-bulk-upload-dialog.tsx')

diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx
new file mode 100644
index 00000000..f4816328
--- /dev/null
+++ b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx
@@ -0,0 +1,608 @@
+"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 {
+  validateB4FileName,
+  B4UploadValidationDialog,
+  type FileValidationResult,
+} from "./b4-upload-validation-dialog";
+import {
+  checkB4MappingStatus,
+  bulkUploadB4Files,
+  type MappingCheckItem,
+  type B4BulkUploadResult,
+} from "../actions";
+
+interface B4BulkUploadDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  projectNo: string;
+  userId: string;
+  userName: string;
+  userEmail: string;
+  vendorCode: string;
+  onUploadComplete?: () => void;
+}
+
+// B4 GTT 옵션
+const B4_DRAWING_USAGE_OPTIONS = [
+  { value: "REC", label: "RECEIVE (입수용)" },
+];
+
+const B4_REGISTER_KIND_OPTIONS: Record<string, Array<{ value: string; label: string }>> = {
+  REC: [
+    { value: "RECP", label: "Pre. 도면입수" },
+    { value: "RECW", label: "Working 도면입수" },
+  ],
+};
+
+type UploadStep = "settings" | "files" | "validation" | "uploading" | "complete";
+
+export function B4BulkUploadDialog({
+  open,
+  onOpenChange,
+  projectNo,
+  userId,
+  userName,
+  userEmail,
+  vendorCode,
+  onUploadComplete,
+}: B4BulkUploadDialogProps) {
+  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);
+
+  // 다이얼로그 닫을 때 초기화
+  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("이미 선택된 파일입니다");
+      return;
+    }
+
+    setSelectedFiles((prev) => [...prev, ...newFiles]);
+    toast.success(`${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("등록종류를 선택하세요");
+      return;
+    }
+    setCurrentStep("files");
+  };
+
+  // 2단계 완료 (파일 선택)
+  const handleFilesNext = () => {
+    if (selectedFiles.length === 0) {
+      toast.error("파일을 선택해주세요");
+      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: "DOLCE 시스템에서 도면을 찾을 수 없습니다",
+          };
+        }
+
+        // RegisterGroupId가 0이거나 MappingYN이 N이면 도면이 존재하지 않음
+        if (mappingResult.RegisterGroupId === 0 || mappingResult.MappingYN === "N") {
+          return {
+            ...parseResult,
+            mappingStatus: "not_found" as const,
+            error: "해당 도면번호가 프로젝트에 등록되어 있지 않습니다",
+          };
+        }
+
+        // DrawingMoveGbn이 "도면입수"가 아니면 업로드 불가
+        if (mappingResult.DrawingMoveGbn !== "도면입수") {
+          return {
+            ...parseResult,
+            mappingStatus: "not_found" as const,
+            error: "도면입수(GTT Deliverables)인 도면만 업로드 가능합니다",
+          };
+        }
+
+        // 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 : "검증 중 오류가 발생했습니다"
+      );
+    }
+  };
+
+  // 업로드 확인
+  const handleConfirmUpload = async (validFiles: FileValidationResult[]) => {
+    setIsUploading(true);
+    setCurrentStep("uploading");
+    setShowValidationDialog(false);
+
+    // 진행률 시뮬레이션
+    const progressInterval = setInterval(() => {
+      setUploadProgress((prev) => {
+        if (prev >= 90) {
+          clearInterval(progressInterval);
+          return prev;
+        }
+        return prev + 10;
+      });
+    }, 500);
+
+    try {
+      // FormData 생성
+      const formData = new FormData();
+      formData.append("projectNo", projectNo);
+      formData.append("userId", userId);
+      formData.append("userName", userName);
+      formData.append("userEmail", userEmail);
+      formData.append("vendorCode", vendorCode);
+      formData.append("registerKind", registerKind); // RegisterKind 추가
+
+      // 파일 및 메타데이터 추가
+      validFiles.forEach((fileResult, index) => {
+        formData.append(`file_${index}`, fileResult.file);
+        formData.append(`drawingNo_${index}`, fileResult.parsed!.drawingNo);
+        formData.append(`revNo_${index}`, fileResult.parsed!.revNo);
+        formData.append(`fileName_${index}`, fileResult.file.name);
+        formData.append(
+          `registerGroupId_${index}`,
+          String(fileResult.registerGroupId || 0)
+        );
+      });
+
+      formData.append("fileCount", String(validFiles.length));
+
+      // 서버 액션 호출
+      const result: B4BulkUploadResult = await bulkUploadB4Files(formData);
+
+      clearInterval(progressInterval);
+      setUploadProgress(100);
+      setUploadResult(result);
+
+      if (result.success) {
+        setCurrentStep("complete");
+        toast.success(
+          `${result.successCount}/${validFiles.length}개 파일 업로드 완료`
+        );
+      } else {
+        setCurrentStep("files");
+        toast.error(result.error || "업로드 실패");
+      }
+    } catch (error) {
+      console.error("업로드 실패:", error);
+      toast.error(
+        error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다"
+      );
+    } finally {
+      setIsUploading(false);
+    }
+  };
+
+  const registerKindOptions = drawingUsage
+    ? B4_REGISTER_KIND_OPTIONS[drawingUsage] || []
+    : [];
+
+  const handleDrawingUsageChange = (value: string) => {
+    setDrawingUsage(value);
+    setRegisterKind("");
+  };
+
+  return (
+    <>
+      <Dialog open={open} onOpenChange={onOpenChange}>
+        <DialogContent className="max-w-2xl">
+          <DialogHeader>
+            <DialogTitle>B4 일괄 업로드</DialogTitle>
+            <DialogDescription>
+              {currentStep === "settings" && "업로드 설정을 선택하세요"}
+              {currentStep === "files" && "파일명 형식: [버림] [DrawingNo] [RevNo].[확장자] (예: testfile GTT-DE-007 R01.pdf)"}
+              {currentStep === "validation" && "파일 검증 중..."}
+            </DialogDescription>
+          </DialogHeader>
+
+          <div className="space-y-4">
+            {/* 1단계: 설정 입력 */}
+            {currentStep === "settings" && (
+              <>
+                {/* 도면용도 선택 */}
+                <div className="space-y-2">
+                  <Label>도면용도 (Drawing Usage) *</Label>
+                  <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}>
+                    <SelectTrigger>
+                      <SelectValue placeholder="도면용도를 선택하세요" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {B4_DRAWING_USAGE_OPTIONS.map((option) => (
+                        <SelectItem key={option.value} value={option.value}>
+                          {option.label}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                </div>
+
+                {/* 등록종류 선택 */}
+                <div className="space-y-2">
+                  <Label>등록종류 (Register Kind) *</Label>
+                  <Select
+                    value={registerKind}
+                    onValueChange={setRegisterKind}
+                    disabled={!drawingUsage}
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="등록종류를 선택하세요" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      {registerKindOptions.map((option) => (
+                        <SelectItem key={option.value} value={option.value}>
+                          {option.label}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                  <p className="text-sm text-muted-foreground">
+                    선택한 등록종류가 모든 파일에 적용됩니다
+                  </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"
+                  />
+                  <label
+                    htmlFor="b4-file-upload"
+                    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
+                        ? "파일을 여기에 놓으세요"
+                        : "클릭하거나 파일을 드래그하여 선택"}
+                    </p>
+                    <p className="text-xs text-muted-foreground mt-1">
+                      PDF, DOC, DOCX, XLS, XLSX, DWG, DXF, ZIP
+                    </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">
+                        선택된 파일 ({selectedFiles.length}개)
+                      </h4>
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => setSelectedFiles([])}
+                      >
+                        전체 제거
+                      </Button>
+                    </div>
+                    <div className="max-h-48 overflow-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)}
+                          >
+                            제거
+                          </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">
+                  파일 검증 중입니다...
+                </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">파일 업로드 중...</h3>
+                  <p className="text-sm text-muted-foreground">
+                    잠시만 기다려주세요
+                  </p>
+                </div>
+                <div className="space-y-2">
+                  <div className="flex justify-between text-sm">
+                    <span>진행률</span>
+                    <span>{uploadProgress}%</span>
+                  </div>
+                  <Progress value={uploadProgress} className="h-2" />
+                </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">업로드 완료!</h3>
+                  <p className="text-sm text-muted-foreground">
+                    {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">
+                      {uploadResult.failCount}개 파일 업로드 실패
+                    </p>
+                  </div>
+                )}
+
+                <div className="flex justify-center">
+                  <Button
+                    onClick={() => {
+                      onOpenChange(false);
+                      onUploadComplete?.();
+                    }}
+                  >
+                    확인
+                  </Button>
+                </div>
+              </div>
+            )}
+          </div>
+
+          {/* 푸터 버튼 (uploading, complete 단계에서는 숨김) */}
+          {currentStep !== "uploading" && currentStep !== "complete" && currentStep !== "validation" && (
+            <DialogFooter>
+              {currentStep === "settings" && (
+              <>
+                <Button
+                  variant="outline"
+                  onClick={() => onOpenChange(false)}
+                >
+                  취소
+                </Button>
+                <Button
+                  onClick={handleSettingsNext}
+                  disabled={!registerKind}
+                >
+                  다음
+                  <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" />
+                  이전
+                </Button>
+                <Button
+                  onClick={handleFilesNext}
+                  disabled={selectedFiles.length === 0}
+                >
+                  검증 시작
+                  <ChevronRight className="ml-2 h-4 w-4" />
+                </Button>
+              </>
+            )}
+            </DialogFooter>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 검증 다이얼로그 */}
+      <B4UploadValidationDialog
+        open={showValidationDialog}
+        onOpenChange={setShowValidationDialog}
+        validationResults={validationResults}
+        onConfirmUpload={handleConfirmUpload}
+        isUploading={isUploading}
+      />
+    </>
+  );
+}
+
-- 
cgit v1.2.3

