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

---
 .../dialogs/upload-files-to-detail-dialog.tsx      | 314 +++++++++++++++++++++
 1 file changed, 314 insertions(+)
 create mode 100644 lib/dolce/dialogs/upload-files-to-detail-dialog.tsx

(limited to 'lib/dolce/dialogs/upload-files-to-detail-dialog.tsx')

diff --git a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
new file mode 100644
index 00000000..1d8ac582
--- /dev/null
+++ b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
@@ -0,0 +1,314 @@
+"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 { Alert, AlertDescription } from "@/components/ui/alert";
+import { Upload, FolderOpen, Loader2, X, FileText, AlertCircle } from "lucide-react";
+import { toast } from "sonner";
+import { uploadFilesToDetailDrawing, type UploadFilesResult } from "../actions";
+
+interface UploadFilesToDetailDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  uploadId: string;
+  drawingNo: string;
+  revNo: string;
+  userId: string;
+  onUploadComplete?: () => void;
+}
+
+export function UploadFilesToDetailDialog({
+  open,
+  onOpenChange,
+  uploadId,
+  drawingNo,
+  revNo,
+  userId,
+  onUploadComplete,
+}: UploadFilesToDetailDialogProps) {
+  const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
+  const [isUploading, setIsUploading] = useState(false);
+  const [isDragging, setIsDragging] = useState(false);
+
+  // 다이얼로그 닫을 때 초기화
+  React.useEffect(() => {
+    if (!open) {
+      setSelectedFiles([]);
+      setIsDragging(false);
+    }
+  }, [open]);
+
+  // 파일 선택 핸들러
+  const handleFilesChange = (files: File[]) => {
+    if (files.length === 0) return;
+
+    // 파일 크기 및 확장자 검증
+    const MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GB
+    const FORBIDDEN_EXTENSIONS = ['exe', 'com', 'dll', 'vbs', 'js', 'asp', 'aspx', 'bat', 'cmd'];
+
+    const validFiles: File[] = [];
+    const invalidFiles: string[] = [];
+
+    files.forEach((file) => {
+      // 크기 검증
+      if (file.size > MAX_FILE_SIZE) {
+        invalidFiles.push(`${file.name}: 파일 크기가 1GB를 초과합니다`);
+        return;
+      }
+
+      // 확장자 검증
+      const extension = file.name.split('.').pop()?.toLowerCase();
+      if (extension && FORBIDDEN_EXTENSIONS.includes(extension)) {
+        invalidFiles.push(`${file.name}: 금지된 파일 형식입니다 (.${extension})`);
+        return;
+      }
+
+      validFiles.push(file);
+    });
+
+    if (invalidFiles.length > 0) {
+      invalidFiles.forEach((msg) => toast.error(msg));
+    }
+
+    if (validFiles.length > 0) {
+      // 중복 제거
+      const existingNames = new Set(selectedFiles.map((f) => f.name));
+      const newFiles = validFiles.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));
+  };
+
+  // 업로드 처리
+  const handleUpload = async () => {
+    if (selectedFiles.length === 0) {
+      toast.error("파일을 선택해주세요");
+      return;
+    }
+
+    setIsUploading(true);
+
+    try {
+      // FormData 생성
+      const formData = new FormData();
+      formData.append("uploadId", uploadId);
+      formData.append("userId", userId);
+      formData.append("fileCount", String(selectedFiles.length));
+
+      selectedFiles.forEach((file, index) => {
+        formData.append(`file_${index}`, file);
+      });
+
+      // 서버 액션 호출
+      const result: UploadFilesResult = await uploadFilesToDetailDrawing(formData);
+
+      if (result.success) {
+        toast.success(`${result.uploadedCount}개 파일 업로드 완료`);
+        onOpenChange(false);
+        onUploadComplete?.();
+      } else {
+        toast.error(result.error || "업로드 실패");
+      }
+    } catch (error) {
+      console.error("업로드 실패:", error);
+      toast.error(
+        error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다"
+      );
+    } finally {
+      setIsUploading(false);
+    }
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="max-w-2xl">
+        <DialogHeader>
+          <DialogTitle>파일 업로드</DialogTitle>
+          <DialogDescription>
+            {drawingNo} - Rev. {revNo}에 파일을 업로드합니다
+          </DialogDescription>
+        </DialogHeader>
+
+        <div className="space-y-4">
+          {/* 안내 메시지 */}
+          <Alert>
+            <AlertCircle className="h-4 w-4" />
+            <AlertDescription>
+              선택한 상세도면의 UploadId에 파일을 추가합니다. 파일 업로드 후 자동으로 메타데이터가 저장됩니다.
+            </AlertDescription>
+          </Alert>
+
+          {/* 파일 선택 영역 */}
+          <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="detail-file-upload"
+            />
+            <label
+              htmlFor="detail-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 (max 1GB per file)
+              </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([])}
+                  disabled={isUploading}
+                >
+                  전체 제거
+                </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 items-center gap-2 flex-1 min-w-0">
+                      <FileText className="h-4 w-4 text-muted-foreground shrink-0" />
+                      <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>
+                    </div>
+                    <Button
+                      variant="ghost"
+                      size="sm"
+                      onClick={() => handleRemoveFile(index)}
+                      disabled={isUploading}
+                    >
+                      <X className="h-4 w-4" />
+                    </Button>
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+        </div>
+
+        <DialogFooter>
+          <Button
+            variant="outline"
+            onClick={() => onOpenChange(false)}
+            disabled={isUploading}
+          >
+            취소
+          </Button>
+          <Button
+            onClick={handleUpload}
+            disabled={selectedFiles.length === 0 || isUploading}
+          >
+            {isUploading ? (
+              <>
+                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                업로드 중...
+              </>
+            ) : (
+              <>
+                <Upload className="mr-2 h-4 w-4" />
+                업로드 ({selectedFiles.length}개)
+              </>
+            )}
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}
+
-- 
cgit v1.2.3

