From b6c9ac31358206ad291583d4b045cc5d83ca3987 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 4 Sep 2025 10:51:24 +0000
Subject: (최겸) 구매 입찰내 입찰 조건, 사전견적 기능, 품목별 견적 작성 개발
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vendor/components/pr-items-pricing-table.tsx   | 195 +++++++++++++++------
 1 file changed, 138 insertions(+), 57 deletions(-)

(limited to 'lib/bidding/vendor/components/pr-items-pricing-table.tsx')

diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
index 320ed6eb..01885f7a 100644
--- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx
+++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
@@ -22,8 +22,14 @@ import {
   Calculator
 } from 'lucide-react'
 import { formatDate } from '@/lib/utils'
-import { downloadFile } from '@/lib/file-download'
+import { downloadFile, formatFileSize, getFileInfo } from '@/lib/file-download'
 import { getSpecDocumentsForPrItem } from '../../pre-quote/service'
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from '@/components/ui/tooltip'
 
 interface PrItem {
   id: number
@@ -57,6 +63,93 @@ interface SpecDocument {
   uploadedAt: string
 }
 
+// 파일 다운로드 훅
+const useFileDownload = () => {
+  const [downloadingFiles, setDownloadingFiles] = React.useState<Set<string>>(new Set())
+
+  const handleDownload = async (filePath: string, fileName: string, options?: { 
+    action?: 'download' | 'preview' 
+  }) => {
+    const fileKey = `${filePath}_${fileName}`
+    if (downloadingFiles.has(fileKey)) return
+    
+    setDownloadingFiles(prev => new Set(prev).add(fileKey))
+    
+    try {
+      await downloadFile(filePath, fileName, {
+        action: options?.action || 'download',
+        showToast: true,
+        showSuccessToast: true,
+        onError: (error) => {
+          console.error("파일 다운로드 실패:", error)
+        },
+        onSuccess: (fileName, fileSize) => {
+          console.log("파일 다운로드 성공:", fileName, fileSize ? formatFileSize(fileSize) : '')
+        }
+      })
+    } catch (error) {
+      console.error("다운로드 처리 중 오류:", error)
+    } finally {
+      setDownloadingFiles(prev => {
+        const newSet = new Set(prev)
+        newSet.delete(fileKey)
+        return newSet
+      })
+    }
+  }
+
+  return { handleDownload, downloadingFiles }
+}
+
+// 파일 다운로드 링크 컴포넌트
+interface FileDownloadLinkProps {
+  filePath: string
+  fileName: string
+  fileSize?: number | null
+  title?: string | null
+  className?: string
+}
+
+const FileDownloadLink: React.FC<FileDownloadLinkProps> = ({
+  filePath,
+  fileName,
+  fileSize,
+  title,
+  className = ""
+}) => {
+  const { handleDownload, downloadingFiles } = useFileDownload()
+  const fileInfo = getFileInfo(fileName)
+  const fileKey = `${filePath}_${fileName}`
+  const isDownloading = downloadingFiles.has(fileKey)
+
+  return (
+    <TooltipProvider>
+      <Tooltip>
+        <TooltipTrigger asChild>
+          <button
+            onClick={() => handleDownload(filePath, fileName)}
+            disabled={isDownloading}
+            className={`inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 hover:underline disabled:opacity-50 disabled:cursor-not-allowed ${className}`}
+          >
+            <span className="text-xs">{fileInfo.icon}</span>
+            <span className="truncate max-w-[150px]">
+              {isDownloading ? "다운로드 중..." : (title || fileName)}
+            </span>
+            <Download className="h-3 w-3 opacity-60" />
+          </button>
+        </TooltipTrigger>
+        <TooltipContent>
+          <div className="text-xs">
+            <div className="font-medium">{fileName}</div>
+            {fileSize && <div className="text-muted-foreground">{formatFileSize(fileSize)}</div>}
+            <div className="text-muted-foreground">클릭하여 다운로드</div>
+          </div>
+        </TooltipContent>
+      </Tooltip>
+    </TooltipProvider>
+  )
+}
+
 interface PrItemsPricingTableProps {
   prItems: PrItem[]
   initialQuotations?: PrItemQuotation[]
@@ -76,9 +169,8 @@ export function PrItemsPricingTable({
 }: PrItemsPricingTableProps) {
   const [quotations, setQuotations] = React.useState<PrItemQuotation[]>([])
   const [specDocuments, setSpecDocuments] = React.useState<Record<number, SpecDocument[]>>({})
-  const [loadingSpecs, setLoadingSpecs] = React.useState<Record<number, boolean>>({})
 
-  // 초기 견적 데이터 설정
+  // 초기 견적 데이터 설정 및 SPEC 문서 로드
   React.useEffect(() => {
     const initQuotations = prItems.map(item => {
       const existing = initialQuotations.find(q => q.prItemId === item.id)
@@ -94,27 +186,34 @@ export function PrItemsPricingTable({
       }
     })
     setQuotations(initQuotations)
-  }, [prItems, initialQuotations])
 
-  // SPEC 문서 로드
-  const loadSpecDocuments = async (prItemId: number) => {
-    if (loadingSpecs[prItemId]) return
-    
-    setLoadingSpecs(prev => ({ ...prev, [prItemId]: true }))
-    try {
-      const docs = await getSpecDocumentsForPrItem(prItemId)
-      // Date를 string으로 변환
-      const mappedDocs = docs.map(doc => ({
-        ...doc,
-        uploadedAt: doc.uploadedAt.toString()
-      }))
-      setSpecDocuments(prev => ({ ...prev, [prItemId]: mappedDocs }))
-    } catch (error) {
-      console.error('Failed to load spec documents:', error)
-    } finally {
-      setLoadingSpecs(prev => ({ ...prev, [prItemId]: false }))
+    // SPEC 문서가 있는 모든 PR 아이템의 문서를 미리 로드
+    const loadAllSpecDocuments = async () => {
+      const itemsWithSpecs = prItems.filter(item => item.hasSpecDocument)
+      console.log('Loading spec documents for items:', itemsWithSpecs.map(item => ({ id: item.id, itemNumber: item.itemNumber })))
+      
+      for (const item of itemsWithSpecs) {
+        try {
+          console.log('Loading spec documents for prItemId:', item.id)
+          const docs = await getSpecDocumentsForPrItem(item.id)
+          console.log('Loaded spec documents for item', item.id, ':', docs)
+          
+          // Date를 string으로 변환
+          const mappedDocs = docs.map(doc => ({
+            ...doc,
+            uploadedAt: doc.uploadedAt.toString()
+          }))
+          
+          setSpecDocuments(prev => ({ ...prev, [item.id]: mappedDocs }))
+        } catch (error) {
+          console.error('Failed to load spec documents for item', item.id, ':', error)
+        }
+      }
     }
-  }
+
+    loadAllSpecDocuments()
+  }, [prItems, initialQuotations])
+
 
   // 견적 데이터 업데이트
   const updateQuotation = (prItemId: number, field: keyof PrItemQuotation, value: any) => {
@@ -142,16 +241,6 @@ export function PrItemsPricingTable({
     onTotalAmountChange(totalAmount)
   }
 
-  // 파일 다운로드
-  const handleDownloadSpec = async (document: SpecDocument) => {
-    try {
-      await downloadFile(document.filePath, document.originalFileName, {
-        showToast: true
-      })
-    } catch (error) {
-      console.error('Failed to download spec document:', error)
-    }
-  }
 
   // 통화 포맷팅
   const formatCurrency = (amount: number) => {
@@ -187,7 +276,7 @@ export function PrItemsPricingTable({
                   <TableHead>견적단가</TableHead>
                   <TableHead>견적금액</TableHead>
                   <TableHead>납품예정일</TableHead>
-                  <TableHead>기술사양</TableHead>
+                  {/* <TableHead>기술사양</TableHead> */}
                   <TableHead>SPEC</TableHead>
                 </TableRow>
               </TableHeader>
@@ -262,7 +351,7 @@ export function PrItemsPricingTable({
                           />
                         )}
                       </TableCell>
-                      <TableCell>
+                      {/* <TableCell>
                         {readOnly ? (
                           <div className="max-w-32 truncate" title={quotation.technicalSpecification || ''}>
                             {quotation.technicalSpecification || '-'}
@@ -280,37 +369,29 @@ export function PrItemsPricingTable({
                             rows={2}
                           />
                         )}
-                      </TableCell>
+                      </TableCell> */}
                       <TableCell>
                         {item.hasSpecDocument ? (
                           <div className="space-y-1">
-                            {!specDocuments[item.id] ? (
-                              <Button
-                                variant="outline"
-                                size="sm"
-                                onClick={() => loadSpecDocuments(item.id)}
-                                disabled={loadingSpecs[item.id]}
-                              >
-                                <FileText className="w-3 h-3 mr-1" />
-                                {loadingSpecs[item.id] ? '로딩...' : 'SPEC 보기'}
-                              </Button>
-                            ) : specDocuments[item.id].length > 0 ? (
+                            {specDocuments[item.id] && specDocuments[item.id].length > 0 ? (
                               <div className="space-y-1">
                                 {specDocuments[item.id].map((doc) => (
-                                  <Button
-                                    key={doc.id}
-                                    variant="outline"
-                                    size="sm"
-                                    onClick={() => handleDownloadSpec(doc)}
-                                    className="block text-xs"
-                                  >
-                                    <Download className="w-3 h-3 mr-1" />
-                                    {doc.originalFileName}
-                                  </Button>
+                                  <div key={doc.id} className="text-xs">
+                                    <FileDownloadLink
+                                      filePath={doc.filePath}
+                                      fileName={doc.originalFileName}
+                                      fileSize={doc.fileSize}
+                                      title={doc.title}
+                                      className="text-xs"
+                                    />
+                                  </div>
                                 ))}
                               </div>
                             ) : (
-                              <Badge variant="secondary">문서 없음</Badge>
+                              <div className="flex items-center gap-2">
+                                <Badge variant="secondary">문서 없음</Badge>
+                                <span className="text-xs text-muted-foreground">로딩 중...</span>
+                              </div>
                             )}
                           </div>
                         ) : (
-- 
cgit v1.2.3

