diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-12 10:42:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-12 10:42:36 +0000 |
| commit | 8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 (patch) | |
| tree | 36bd57d147ba929f1d72918d1fb91ad2c4778624 /components/ui | |
| parent | 57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff) | |
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'components/ui')
| -rw-r--r-- | components/ui/file-upload.tsx | 169 |
1 files changed, 169 insertions, 0 deletions
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<string, string[]> + 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<HTMLInputElement>(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<HTMLInputElement>) => { + 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 ( + <div className={cn('space-y-2', className)}> + {/* Drop zone */} + <div + className={cn( + 'border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors', + isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/25', + disabled ? 'cursor-not-allowed opacity-50' : 'hover:border-primary/50' + )} + onDrop={handleDrop} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onClick={() => !disabled && fileInputRef.current?.click()} + > + <Upload className="mx-auto h-8 w-8 text-muted-foreground mb-2" /> + <p className="text-sm text-muted-foreground">{placeholder}</p> + <p className="text-xs text-muted-foreground mt-1"> + 최대 {maxFiles}개 파일, 각 파일 {formatFileSize(maxSize)}까지 + </p> + </div> + + {/* Hidden file input */} + <input + ref={fileInputRef} + type="file" + multiple + accept={accept ? Object.values(accept).flat().join(',') : undefined} + onChange={handleInputChange} + className="hidden" + disabled={disabled} + /> + + {/* File list */} + {value.length > 0 && ( + <div className="space-y-2"> + {value.map((file, index) => ( + <div + key={index} + className="flex items-center justify-between p-2 bg-muted rounded-md" + > + <div className="flex items-center space-x-2"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <div className="flex-1 min-w-0"> + <p className="text-sm font-medium truncate">{file.name}</p> + <p className="text-xs text-muted-foreground"> + {formatFileSize(file.size)} + </p> + </div> + </div> + <Button + variant="ghost" + size="sm" + onClick={(e) => { + e.stopPropagation() + removeFile(index) + }} + disabled={disabled} + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + )} + </div> + ) +} |
