From 8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 12 Nov 2025 10:42:36 +0000 Subject: (최겸) 구매 일반계약, 입찰 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ui/file-upload.tsx | 169 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 components/ui/file-upload.tsx (limited to 'components/ui') diff --git a/components/ui/file-upload.tsx b/components/ui/file-upload.tsx new file mode 100644 index 00000000..01f09d48 --- /dev/null +++ b/components/ui/file-upload.tsx @@ -0,0 +1,169 @@ +'use client' + +import * as React from 'react' +import { Upload, X, FileText } from 'lucide-react' +import { Button } from './button' +import { cn } from '@/lib/utils' + +interface FileUploadProps { + value: File[] + onChange: (files: File[]) => void + accept?: Record + maxSize?: number + maxFiles?: number + placeholder?: string + disabled?: boolean + className?: string +} + +export function FileUpload({ + value = [], + onChange, + accept, + maxSize = 10 * 1024 * 1024, // 10MB + maxFiles = 5, + placeholder = '파일을 선택하거나 드래그하세요', + disabled = false, + className +}: FileUploadProps) { + const [isDragOver, setIsDragOver] = React.useState(false) + const fileInputRef = React.useRef(null) + + const handleFileSelect = (files: FileList | null) => { + if (!files || disabled) return + + const fileArray = Array.from(files) + const validFiles = fileArray.filter(file => { + // 파일 크기 검증 + if (file.size > maxSize) { + console.warn(`파일 ${file.name}이(가) 최대 크기(${maxSize / 1024 / 1024}MB)를 초과합니다.`) + return false + } + + // 파일 타입 검증 + if (accept) { + const fileType = file.type + const fileName = file.name.toLowerCase() + const isAccepted = Object.entries(accept).some(([mimeType, extensions]) => { + if (fileType && mimeType !== '*/*') { + return fileType.startsWith(mimeType.split('/')[0]) + } + return extensions.some(ext => fileName.endsWith(ext.toLowerCase())) + }) + + if (!isAccepted) { + console.warn(`파일 ${file.name}이(가) 지원되지 않는 형식입니다.`) + return false + } + } + + return true + }) + + const newFiles = [...value, ...validFiles].slice(0, maxFiles) + onChange(newFiles) + } + + const handleInputChange = (event: React.ChangeEvent) => { + handleFileSelect(event.target.files) + // Reset input value to allow re-uploading the same file + event.target.value = '' + } + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault() + setIsDragOver(false) + handleFileSelect(event.dataTransfer.files) + } + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault() + if (!disabled) { + setIsDragOver(true) + } + } + + const handleDragLeave = () => { + setIsDragOver(false) + } + + const removeFile = (index: number) => { + const newFiles = value.filter((_, i) => i !== index) + onChange(newFiles) + } + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + return ( +
+ {/* Drop zone */} +
!disabled && fileInputRef.current?.click()} + > + +

{placeholder}

+

+ 최대 {maxFiles}개 파일, 각 파일 {formatFileSize(maxSize)}까지 +

+
+ + {/* Hidden file input */} + + + {/* File list */} + {value.length > 0 && ( +
+ {value.map((file, index) => ( +
+
+ +
+

{file.name}

+

+ {formatFileSize(file.size)} +

+
+
+ +
+ ))} +
+ )} +
+ ) +} -- cgit v1.2.3