summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor/components/simple-file-upload.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/vendor/components/simple-file-upload.tsx')
-rw-r--r--lib/bidding/vendor/components/simple-file-upload.tsx315
1 files changed, 315 insertions, 0 deletions
diff --git a/lib/bidding/vendor/components/simple-file-upload.tsx b/lib/bidding/vendor/components/simple-file-upload.tsx
new file mode 100644
index 00000000..b1eb8b8f
--- /dev/null
+++ b/lib/bidding/vendor/components/simple-file-upload.tsx
@@ -0,0 +1,315 @@
+'use client'
+
+import * as React from 'react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Badge } from '@/components/ui/badge'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import {
+ Upload,
+ FileText,
+ Download,
+ Trash2
+} from 'lucide-react'
+import { useToast } from '@/hooks/use-toast'
+import { useTransition } from 'react'
+import {
+ uploadPreQuoteDocument,
+ getPreQuoteDocuments,
+ getPreQuoteDocumentForDownload,
+ deletePreQuoteDocument
+} from '../../pre-quote/service'
+import { downloadFile } from '@/lib/file-download'
+
+interface UploadedDocument {
+ id: number
+ fileName: string
+ originalFileName: string
+ fileSize: number | null
+ filePath: string
+ title: string | null
+ description: string | null
+ uploadedAt: string
+ uploadedBy: string
+}
+
+interface SimpleFileUploadProps {
+ biddingId: number
+ companyId: number
+ userId: string
+ readOnly?: boolean
+}
+
+export function SimpleFileUpload({
+ biddingId,
+ companyId,
+ userId,
+ readOnly = false
+}: SimpleFileUploadProps) {
+ const { toast } = useToast()
+ const [isPending, startTransition] = useTransition()
+ const [documents, setDocuments] = React.useState<UploadedDocument[]>([])
+ const [isLoading, setIsLoading] = React.useState(true)
+
+ // 업로드된 문서 목록 로드
+ const loadDocuments = React.useCallback(async () => {
+ try {
+ setIsLoading(true)
+ const docs = await getPreQuoteDocuments(biddingId, companyId)
+ // Date를 string으로 변환
+ const mappedDocs = docs.map(doc => ({
+ ...doc,
+ uploadedAt: doc.uploadedAt.toString(),
+ uploadedBy: doc.uploadedBy || ''
+ }))
+ setDocuments(mappedDocs)
+ } catch (error) {
+ console.error('Failed to load documents:', error)
+ toast({
+ title: '오류',
+ description: '업로드된 문서 목록을 불러오는데 실패했습니다.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsLoading(false)
+ }
+ }, [biddingId, companyId, toast])
+
+ React.useEffect(() => {
+ loadDocuments()
+ }, [loadDocuments])
+
+ // 파일 업로드 처리
+ const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const files = event.target.files
+ if (!files || files.length === 0) return
+
+ const file = files[0]
+
+ // 파일 크기 체크 (50MB 제한)
+ if (file.size > 50 * 1024 * 1024) {
+ toast({
+ title: '파일 크기 초과',
+ description: '파일 크기가 50MB를 초과합니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ // 파일 타입 체크
+ const allowedTypes = [
+ 'application/pdf',
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.ms-excel',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'image/jpeg',
+ 'image/png',
+ 'application/zip'
+ ]
+
+ if (!allowedTypes.includes(file.type)) {
+ toast({
+ title: '지원하지 않는 파일 형식',
+ description: 'PDF, Word, Excel, 이미지, ZIP 파일만 업로드 가능합니다.',
+ variant: 'destructive',
+ })
+ return
+ }
+
+ startTransition(async () => {
+ const result = await uploadPreQuoteDocument(biddingId, companyId, file, userId)
+
+ if (result.success) {
+ toast({
+ title: '업로드 완료',
+ description: result.message,
+ })
+ await loadDocuments() // 문서 목록 새로고침
+ } else {
+ toast({
+ title: '업로드 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+
+ // input 초기화
+ event.target.value = ''
+ }
+
+ // 파일 다운로드
+ const handleDownload = (document: UploadedDocument) => {
+ startTransition(async () => {
+ const result = await getPreQuoteDocumentForDownload(document.id, biddingId, companyId)
+
+ if (result.success) {
+ try {
+ await downloadFile(result.document?.filePath, result.document?.originalFileName, {
+ showToast: true
+ })
+ } catch (error) {
+ toast({
+ title: '다운로드 실패',
+ description: '파일 다운로드에 실패했습니다.',
+ variant: 'destructive',
+ })
+ }
+ } else {
+ toast({
+ title: '다운로드 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ // 파일 삭제
+ const handleDelete = (document: UploadedDocument) => {
+ if (!confirm(`"${document.originalFileName}" 파일을 삭제하시겠습니까?`)) {
+ return
+ }
+
+ startTransition(async () => {
+ const result = await deletePreQuoteDocument(document.id, biddingId, companyId, userId)
+
+ if (result.success) {
+ toast({
+ title: '삭제 완료',
+ description: result.message,
+ })
+ await loadDocuments() // 문서 목록 새로고침
+ } else {
+ toast({
+ title: '삭제 실패',
+ description: result.error,
+ variant: 'destructive',
+ })
+ }
+ })
+ }
+
+ // 파일 크기 포맷팅
+ const formatFileSize = (bytes: number | null) => {
+ if (!bytes) return '-'
+ 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 (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 견적 문서 업로드
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {!readOnly && (
+ <div className="space-y-2">
+ <Label htmlFor="file-upload">견적서 파일</Label>
+ <Input
+ id="file-upload"
+ type="file"
+ accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.zip"
+ onChange={handleFileUpload}
+ disabled={isPending}
+ />
+ <p className="text-xs text-muted-foreground">
+ 지원 형식: PDF, Word, Excel, 이미지, ZIP (최대 50MB)
+ </p>
+ </div>
+ )}
+
+ {/* 업로드된 문서 목록 */}
+ {isLoading ? (
+ <div className="text-center py-4">
+ <p className="text-muted-foreground">문서 목록을 불러오는 중...</p>
+ </div>
+ ) : documents.length > 0 ? (
+ <div className="space-y-2">
+ <Label className="text-sm font-medium">업로드된 문서</Label>
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>파일명</TableHead>
+ <TableHead>크기</TableHead>
+ <TableHead>업로드일</TableHead>
+ <TableHead>작성자</TableHead>
+ <TableHead className="w-24">작업</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {documents.map((doc) => (
+ <TableRow key={doc.id}>
+ <TableCell>
+ <div className="flex items-center gap-2">
+ <FileText className="w-4 h-4 text-gray-500" />
+ <span className="truncate max-w-48" title={doc.originalFileName}>
+ {doc.originalFileName}
+ </span>
+ </div>
+ </TableCell>
+ <TableCell className="text-sm text-gray-500">
+ {formatFileSize(doc.fileSize)}
+ </TableCell>
+ <TableCell className="text-sm text-gray-500">
+ {new Date(doc.uploadedAt).toLocaleDateString('ko-KR')}
+ </TableCell>
+ <TableCell className="text-sm text-gray-500">
+ {doc.uploadedBy}
+ </TableCell>
+ <TableCell>
+ <div className="flex items-center gap-1">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleDownload(doc)}
+ disabled={isPending}
+ title="다운로드"
+ >
+ <Download className="w-3 h-3" />
+ </Button>
+ {!readOnly && doc.uploadedBy === userId && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleDelete(doc)}
+ disabled={isPending}
+ title="삭제"
+ className="text-red-600 hover:text-red-700"
+ >
+ <Trash2 className="w-3 h-3" />
+ </Button>
+ )}
+ </div>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </div>
+ ) : (
+ <div className="text-center py-4 text-gray-500">
+ <FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
+ <p className="text-sm">업로드된 문서가 없습니다</p>
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ )
+}