From 95866a13ba4e1c235373834460aa284b763fe0d9 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 23 Jun 2025 09:03:29 +0000 Subject: (최겸) 기술영업 RFQ 개발(0620 요구사항, 첨부파일, REV 등) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/quotation-response-tab.tsx | 228 +++++++++++++++++---- 1 file changed, 183 insertions(+), 45 deletions(-) (limited to 'lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx') diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx index 3449dcb6..20b2703c 100644 --- a/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx +++ b/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { useState } from "react" +import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" @@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" -import { CalendarIcon, Save, Send, AlertCircle } from "lucide-react" +import { CalendarIcon, Send, AlertCircle, Upload, X, FileText, Download } from "lucide-react" import { Calendar } from "@/components/ui/calendar" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" @@ -26,6 +26,13 @@ interface QuotationResponseTabProps { currency: string | null validUntil: Date | null remark: string | null + quotationAttachments?: Array<{ + id: number + fileName: string + fileSize: number + filePath: string + description?: string | null + }> rfq: { id: number rfqCode: string | null @@ -58,38 +65,93 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) { ) const [remark, setRemark] = useState(quotation.remark || "") const [isLoading, setIsLoading] = useState(false) + const [attachments, setAttachments] = useState>([]) + const [isUploadingFiles, setIsUploadingFiles] = useState(false) const router = useRouter() + // // 초기 첨부파일 데이터 로드 + // useEffect(() => { + // if (quotation.quotationAttachments) { + // setAttachments(quotation.quotationAttachments.map(att => ({ + // id: att.id, + // fileName: att.fileName, + // fileSize: att.fileSize, + // filePath: att.filePath, + // isNew: false + // }))) + // } + // }, [quotation.quotationAttachments]) + const rfq = quotation.rfq const isDueDatePassed = rfq?.dueDate ? new Date(rfq.dueDate) < new Date() : false - const canSubmit = quotation.status === "Draft" && !isDueDatePassed - const canEdit = ["Draft", "Revised"].includes(quotation.status) && !isDueDatePassed + const canSubmit = !["Accepted", "Rejected"].includes(quotation.status) && !isDueDatePassed + const canEdit = !["Accepted", "Rejected"].includes(quotation.status) && !isDueDatePassed + + // 파일 업로드 핸들러 + const handleFileSelect = (event: React.ChangeEvent) => { + const files = event.target.files + if (!files) return + + Array.from(files).forEach(file => { + setAttachments(prev => [ + ...prev, + { + fileName: file.name, + fileSize: file.size, + filePath: '', + isNew: true, + file + } + ]) + }) + } + + // 첨부파일 제거 + const removeAttachment = (index: number) => { + setAttachments(prev => prev.filter((_, i) => i !== index)) + } + + // 파일 업로드 함수 + const uploadFiles = async () => { + const newFiles = attachments.filter(att => att.isNew && att.file) + if (newFiles.length === 0) return [] + + setIsUploadingFiles(true) + const uploadedFiles = [] - const handleSaveDraft = async () => { - setIsLoading(true) try { - const { updateTechSalesVendorQuotation } = await import("@/lib/techsales-rfq/service") - - const result = await updateTechSalesVendorQuotation({ - id: quotation.id, - currency, - totalPrice, - validUntil: validUntil!, - remark, - updatedBy: 1 // TODO: 실제 사용자 ID로 변경 - }) + for (const attachment of newFiles) { + const formData = new FormData() + formData.append('file', attachment.file!) + + const response = await fetch('/api/upload', { + method: 'POST', + body: formData + }) - if (result.error) { - toast.error(result.error) - } else { - toast.success("임시 저장되었습니다.") - // 페이지 새로고침 대신 router.refresh() 사용 - router.refresh() + if (!response.ok) throw new Error('파일 업로드 실패') + + const result = await response.json() + uploadedFiles.push({ + fileName: result.fileName, + filePath: result.url, + fileSize: attachment.fileSize + }) } - } catch { - toast.error("저장 중 오류가 발생했습니다.") + return uploadedFiles + } catch (error) { + console.error('파일 업로드 오류:', error) + toast.error('파일 업로드 중 오류가 발생했습니다.') + return [] } finally { - setIsLoading(false) + setIsUploadingFiles(false) } } @@ -101,6 +163,9 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) { setIsLoading(true) try { + // 파일 업로드 먼저 처리 + const uploadedFiles = await uploadFiles() + const { submitTechSalesVendorQuotation } = await import("@/lib/techsales-rfq/service") const result = await submitTechSalesVendorQuotation({ @@ -109,6 +174,7 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) { totalPrice, validUntil: validUntil!, remark, + attachments: uploadedFiles, updatedBy: 1 // TODO: 실제 사용자 ID로 변경 }) @@ -116,8 +182,10 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) { toast.error(result.error) } else { toast.success("견적서가 제출되었습니다.") - // 페이지 새로고침 대신 router.refresh() 사용 - router.refresh() + // // 페이지 새로고침 대신 router.refresh() 사용 + // router.refresh() + // 페이지 새로고침 + window.location.reload() } } catch { toast.error("제출 중 오류가 발생했습니다.") @@ -312,28 +380,98 @@ export function QuotationResponseTab({ quotation }: QuotationResponseTabProps) { /> + {/* 첨부파일 */} +
+ + + {/* 파일 업로드 버튼 */} + {canEdit && ( +
+ + + + PDF, 문서파일, 이미지파일, 압축파일 등 + +
+ )} + + {/* 첨부파일 목록 */} + {attachments.length > 0 && ( +
+ {attachments.map((attachment, index) => ( +
+
+ +
+
{attachment.fileName}
+
+ {(attachment.fileSize / 1024 / 1024).toFixed(2)} MB + {attachment.isNew && ( + + 새 파일 + + )} +
+
+
+
+ {!attachment.isNew && ( + + )} + {canEdit && ( + + )} +
+
+ ))} +
+ )} +
+ {/* 액션 버튼 */} - {canEdit && ( -
+ {canEdit && canSubmit && ( +
- {canSubmit && ( - - )}
)} -- cgit v1.2.3