From 95866a13ba4e1c235373834460aa284b763fe0d9 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<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>
-- 
cgit v1.2.3

