summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/vendor-response/detail/quotation-response-tab.tsx
diff options
context:
space:
mode:
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.tsx228
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>