From b67e36df49f067cbd5ba899f9fbcc755f38d4b4f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 4 Sep 2025 08:31:31 +0000 Subject: (대표님, 최겸, 임수민) 작업사항 커밋 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor/components/pre-quote-file-upload.tsx | 367 +++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 lib/bidding/vendor/components/pre-quote-file-upload.tsx (limited to 'lib/bidding/vendor/components/pre-quote-file-upload.tsx') diff --git a/lib/bidding/vendor/components/pre-quote-file-upload.tsx b/lib/bidding/vendor/components/pre-quote-file-upload.tsx new file mode 100644 index 00000000..b6d8990b --- /dev/null +++ b/lib/bidding/vendor/components/pre-quote-file-upload.tsx @@ -0,0 +1,367 @@ +'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 { Progress } from '@/components/ui/progress' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + Upload, + FileText, + Download, + Trash2, + AlertCircle +} from 'lucide-react' +import { useToast } from '@/hooks/use-toast' +import { saveFile } from '@/lib/file-stroage' +import { downloadFile } from '@/lib/file-download' +import { + uploadPreQuoteDocument, + getPreQuoteDocuments +} from '../../pre-quote/service' + +interface UploadedDocument { + id: number + fileName: string + originalFileName: string + fileSize: number | null + filePath: string + title: string | null + description: string | null + uploadedAt: string +} + +interface PreQuoteFileUploadProps { + biddingId: number + companyId: number + onUploadComplete?: (documentId: number) => void + readOnly?: boolean +} + +export function PreQuoteFileUpload({ + biddingId, + companyId, + onUploadComplete, + readOnly = false +}: PreQuoteFileUploadProps) { + const { toast } = useToast() + const [documents, setDocuments] = React.useState([]) + const [isUploading, setIsUploading] = React.useState(false) + const [uploadProgress, setUploadProgress] = React.useState(0) + const [dragActive, setDragActive] = React.useState(false) + + // 업로드된 문서 목록 로드 + const loadDocuments = React.useCallback(async () => { + try { + const docs = await getPreQuoteDocuments(biddingId, companyId) + // Date를 string으로 변환 + const mappedDocs = docs.map(doc => ({ + ...doc, + uploadedAt: doc.uploadedAt.toString() + })) + setDocuments(mappedDocs) + } catch (error) { + console.error('Failed to load documents:', error) + toast({ + title: '오류', + description: '업로드된 문서 목록을 불러오는데 실패했습니다.', + variant: 'destructive', + }) + } + }, [biddingId, companyId, toast]) + + React.useEffect(() => { + loadDocuments() + }, [loadDocuments]) + + // 파일 업로드 처리 + const handleFileUpload = async (files: FileList | File[]) => { + if (readOnly) return + + const fileArray = Array.from(files) + if (fileArray.length === 0) return + + setIsUploading(true) + setUploadProgress(0) + + try { + for (let i = 0; i < fileArray.length; i++) { + const file = fileArray[i] + + // 파일 크기 체크 (50MB 제한) + if (file.size > 50 * 1024 * 1024) { + toast({ + title: '파일 크기 초과', + description: `${file.name}의 크기가 50MB를 초과합니다.`, + variant: 'destructive', + }) + continue + } + + // 파일 타입 체크 + 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: `${file.name}: PDF, Word, Excel, 이미지, ZIP 파일만 업로드 가능합니다.`, + variant: 'destructive', + }) + continue + } + + // 파일 저장 + const saveResult = await saveFile({ + file, + directory: `bidding/${biddingId}/quotations`, + originalName: file.name, + userId: 'current-user' // TODO: 실제 사용자 ID + }) + + if (!saveResult.success) { + toast({ + title: '업로드 실패', + description: `${file.name}: ${saveResult.error}`, + variant: 'destructive', + }) + continue + } + + // 데이터베이스에 문서 정보 저장 + const uploadResult = await uploadPreQuoteDocument( + biddingId, + companyId, + { + fileName: saveResult.fileName!, + originalFileName: file.name, + fileSize: file.size, + mimeType: file.type, + filePath: saveResult.path! + }, + 'current-user' // TODO: 실제 사용자 ID + ) + + if (uploadResult.success) { + toast({ + title: '업로드 완료', + description: `${file.name}이 성공적으로 업로드되었습니다.`, + }) + + if (onUploadComplete && uploadResult.documentId) { + onUploadComplete(uploadResult.documentId) + } + } else { + toast({ + title: '업로드 실패', + description: uploadResult.error, + variant: 'destructive', + }) + } + + // 진행률 업데이트 + setUploadProgress(((i + 1) / fileArray.length) * 100) + } + + // 문서 목록 새로고침 + await loadDocuments() + + } catch (error) { + console.error('Upload error:', error) + toast({ + title: '업로드 오류', + description: '파일 업로드 중 오류가 발생했습니다.', + variant: 'destructive', + }) + } finally { + setIsUploading(false) + setUploadProgress(0) + } + } + + // 드래그 앤 드롭 처리 + const handleDrag = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.type === 'dragenter' || e.type === 'dragover') { + setDragActive(true) + } else if (e.type === 'dragleave') { + setDragActive(false) + } + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragActive(false) + + if (readOnly) return + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleFileUpload(e.dataTransfer.files) + } + } + + // 파일 다운로드 + const handleDownload = async (document: UploadedDocument) => { + try { + await downloadFile(document.filePath, document.originalFileName, { + showToast: true + }) + } catch (error) { + console.error('Failed to download document:', error) + toast({ + title: '다운로드 실패', + description: '파일 다운로드에 실패했습니다.', + 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 ( + + + + + 견적 문서 업로드 + + + + {!readOnly && ( +
+ +
+

+ 파일을 드래그하여 업로드하거나 클릭하여 선택하세요 +

+ e.target.files && handleFileUpload(e.target.files)} + className="hidden" + id="file-upload" + /> + +
+

+ 지원 형식: PDF, Word, Excel, 이미지, ZIP (최대 50MB) +

+
+ )} + + {isUploading && ( +
+
+ + 업로드 중... +
+ +
+ )} + + {/* 업로드된 문서 목록 */} + {documents.length > 0 ? ( +
+ + + + + 파일명 + 크기 + 업로드일 + 작업 + + + + {documents.map((doc) => ( + + +
+ + + {doc.originalFileName} + +
+
+ + {formatFileSize(doc.fileSize)} + + + {new Date(doc.uploadedAt).toLocaleDateString('ko-KR')} + + + + +
+ ))} +
+
+
+ ) : ( +
+ +

업로드된 문서가 없습니다

+
+ )} + + {readOnly && documents.length === 0 && ( +
+ + + 견적 문서가 업로드되지 않았습니다. + +
+ )} +
+
+ ) +} -- cgit v1.2.3