diff options
Diffstat (limited to 'lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx')
| -rw-r--r-- | lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx | 228 |
1 files changed, 183 insertions, 45 deletions
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<Array<{ + id?: number + fileName: string + fileSize: number + filePath: string + isNew?: boolean + file?: File + }>>([]) + 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<HTMLInputElement>) => { + 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) { /> </div> + {/* 첨부파일 */} + <div className="space-y-4"> + <Label>첨부파일</Label> + + {/* 파일 업로드 버튼 */} + {canEdit && ( + <div className="flex items-center gap-2"> + <Button + type="button" + variant="outline" + size="sm" + disabled={isUploadingFiles} + onClick={() => document.getElementById('file-input')?.click()} + > + <Upload className="h-4 w-4 mr-2" /> + 파일 선택 + </Button> + <input + id="file-input" + type="file" + multiple + onChange={handleFileSelect} + className="hidden" + accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.zip" + /> + <span className="text-sm text-muted-foreground"> + PDF, 문서파일, 이미지파일, 압축파일 등 + </span> + </div> + )} + + {/* 첨부파일 목록 */} + {attachments.length > 0 && ( + <div className="space-y-2"> + {attachments.map((attachment, index) => ( + <div + key={index} + className="flex items-center justify-between p-3 border rounded-lg bg-muted/50" + > + <div className="flex items-center gap-2"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <div> + <div className="text-sm font-medium">{attachment.fileName}</div> + <div className="text-xs text-muted-foreground"> + {(attachment.fileSize / 1024 / 1024).toFixed(2)} MB + {attachment.isNew && ( + <Badge variant="secondary" className="ml-2"> + 새 파일 + </Badge> + )} + </div> + </div> + </div> + <div className="flex items-center gap-2"> + {!attachment.isNew && ( + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => window.open(attachment.filePath, '_blank')} + > + <Download className="h-4 w-4" /> + </Button> + )} + {canEdit && ( + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => removeAttachment(index)} + > + <X className="h-4 w-4" /> + </Button> + )} + </div> + </div> + ))} + </div> + )} + </div> + {/* 액션 버튼 */} - {canEdit && ( - <div className="flex gap-2 pt-4"> + {canEdit && canSubmit && ( + <div className="flex justify-center pt-4"> <Button - variant="outline" - onClick={handleSaveDraft} - disabled={isLoading} - className="flex-1" + onClick={handleSubmit} + disabled={isLoading || !totalPrice || !currency || !validUntil} + className="w-full " > - <Save className="mr-2 h-4 w-4" /> - 임시 저장 + <Send className="mr-2 h-4 w-4" /> + 견적서 제출 </Button> - {canSubmit && ( - <Button - onClick={handleSubmit} - disabled={isLoading || !totalPrice || !currency || !validUntil} - className="flex-1" - > - <Send className="mr-2 h-4 w-4" /> - 견적서 제출 - </Button> - )} </div> )} </CardContent> |
