summaryrefslogtreecommitdiff
path: root/components/ui
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-12 10:42:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-12 10:42:36 +0000
commit8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 (patch)
tree36bd57d147ba929f1d72918d1fb91ad2c4778624 /components/ui
parent57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff)
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'components/ui')
-rw-r--r--components/ui/file-upload.tsx169
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>
+ )
+}