From 8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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/file-upload.tsx')

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>
+  )
+}
-- 
cgit v1.2.3

