summaryrefslogtreecommitdiff
path: root/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dolce/dialogs/upload-files-to-detail-dialog.tsx')
-rw-r--r--lib/dolce/dialogs/upload-files-to-detail-dialog.tsx314
1 files changed, 314 insertions, 0 deletions
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>
+ );
+}
+