From b67e36df49f067cbd5ba899f9fbcc755f38d4b4f Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 4 Sep 2025 08:31:31 +0000
Subject: (대표님, 최겸, 임수민) 작업사항 커밋
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vendor/components/pr-items-pricing-table.tsx   | 347 +++++++++++++++++++++
 1 file changed, 347 insertions(+)
 create mode 100644 lib/bidding/vendor/components/pr-items-pricing-table.tsx

(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
new file mode 100644
index 00000000..320ed6eb
--- /dev/null
+++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
@@ -0,0 +1,347 @@
+'use client'
+
+import * as React from 'react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Button } from '@/components/ui/button'
+import { Textarea } from '@/components/ui/textarea'
+import { Badge } from '@/components/ui/badge'
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/components/ui/table'
+import {
+  Package,
+  FileText,
+  Download,
+  Calculator
+} from 'lucide-react'
+import { formatDate } from '@/lib/utils'
+import { downloadFile } from '@/lib/file-download'
+import { getSpecDocumentsForPrItem } from '../../pre-quote/service'
+
+interface PrItem {
+  id: number
+  itemNumber: string | null
+  prNumber: string | null
+  itemInfo: string | null
+  materialDescription: string | null
+  quantity: string | null
+  quantityUnit: string | null
+  currency: string | null
+  requestedDeliveryDate: string | null
+  hasSpecDocument: boolean | null
+}
+
+interface PrItemQuotation {
+  prItemId: number
+  bidUnitPrice: number
+  bidAmount: number
+  proposedDeliveryDate?: string
+  technicalSpecification?: string
+}
+
+interface SpecDocument {
+  id: number
+  fileName: string
+  originalFileName: string
+  fileSize: number | null
+  filePath: string
+  title: string | null
+  description: string | null
+  uploadedAt: string
+}
+
+interface PrItemsPricingTableProps {
+  prItems: PrItem[]
+  initialQuotations?: PrItemQuotation[]
+  currency?: string
+  onQuotationsChange: (quotations: PrItemQuotation[]) => void
+  onTotalAmountChange: (total: number) => void
+  readOnly?: boolean
+}
+
+export function PrItemsPricingTable({
+  prItems,
+  initialQuotations = [],
+  currency = 'KRW',
+  onQuotationsChange,
+  onTotalAmountChange,
+  readOnly = false
+}: PrItemsPricingTableProps) {
+  const [quotations, setQuotations] = React.useState<PrItemQuotation[]>([])
+  const [specDocuments, setSpecDocuments] = React.useState<Record<number, SpecDocument[]>>({})
+  const [loadingSpecs, setLoadingSpecs] = React.useState<Record<number, boolean>>({})
+
+  // 초기 견적 데이터 설정
+  React.useEffect(() => {
+    const initQuotations = prItems.map(item => {
+      const existing = initialQuotations.find(q => q.prItemId === item.id)
+      if (existing) {
+        return existing
+      }
+      return {
+        prItemId: item.id,
+        bidUnitPrice: 0,
+        bidAmount: 0,
+        proposedDeliveryDate: '',
+        technicalSpecification: ''
+      }
+    })
+    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 }))
+    }
+  }
+
+  // 견적 데이터 업데이트
+  const updateQuotation = (prItemId: number, field: keyof PrItemQuotation, value: any) => {
+    const updatedQuotations = quotations.map(q => {
+      if (q.prItemId === prItemId) {
+        const updated = { ...q, [field]: value }
+        
+        // 단가나 수량이 변경되면 금액 자동 계산
+        if (field === 'bidUnitPrice') {
+          const prItem = prItems.find(item => item.id === prItemId)
+          const quantity = parseFloat(prItem?.quantity || '1')
+          updated.bidAmount = updated.bidUnitPrice * quantity
+        }
+        
+        return updated
+      }
+      return q
+    })
+    
+    setQuotations(updatedQuotations)
+    onQuotationsChange(updatedQuotations)
+    
+    // 총 금액 계산
+    const totalAmount = updatedQuotations.reduce((sum, q) => sum + q.bidAmount, 0)
+    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) => {
+    return new Intl.NumberFormat('ko-KR', {
+      style: 'currency',
+      currency: currency,
+    }).format(amount)
+  }
+
+  // 총 금액 계산
+  const totalAmount = quotations.reduce((sum, q) => sum + q.bidAmount, 0)
+
+  return (
+    <Card>
+      <CardHeader>
+        <CardTitle className="flex items-center gap-2">
+          <Package className="w-5 h-5" />
+          품목별 견적 작성
+        </CardTitle>
+      </CardHeader>
+      <CardContent>
+        <div className="space-y-4">
+          <div className="overflow-x-auto">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>아이템번호</TableHead>
+                  <TableHead>PR번호</TableHead>
+                  <TableHead>품목정보</TableHead>
+                  <TableHead>자재내역</TableHead>
+                  <TableHead>수량</TableHead>
+                  <TableHead>단위</TableHead>
+                  <TableHead>견적단가</TableHead>
+                  <TableHead>견적금액</TableHead>
+                  <TableHead>납품예정일</TableHead>
+                  <TableHead>기술사양</TableHead>
+                  <TableHead>SPEC</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {prItems.map((item) => {
+                  const quotation = quotations.find(q => q.prItemId === item.id) || {
+                    prItemId: item.id,
+                    bidUnitPrice: 0,
+                    bidAmount: 0,
+                    proposedDeliveryDate: '',
+                    technicalSpecification: ''
+                  }
+                  
+                  return (
+                    <TableRow key={item.id}>
+                      <TableCell className="font-medium">
+                        {item.itemNumber || '-'}
+                      </TableCell>
+                      <TableCell>{item.prNumber || '-'}</TableCell>
+                      <TableCell>
+                        <div className="max-w-32 truncate" title={item.itemInfo || ''}>
+                          {item.itemInfo || '-'}
+                        </div>
+                      </TableCell>
+                      <TableCell>
+                        <div className="max-w-32 truncate" title={item.materialDescription || ''}>
+                          {item.materialDescription || '-'}
+                        </div>
+                      </TableCell>
+                      <TableCell className="text-right">
+                        {item.quantity ? parseFloat(item.quantity).toLocaleString() : '-'}
+                      </TableCell>
+                      <TableCell>{item.quantityUnit || '-'}</TableCell>
+                      <TableCell>
+                        {readOnly ? (
+                          <span className="font-medium">
+                            {quotation.bidUnitPrice.toLocaleString()}
+                          </span>
+                        ) : (
+                          <Input
+                            type="number"
+                            value={quotation.bidUnitPrice}
+                            onChange={(e) => updateQuotation(
+                              item.id,
+                              'bidUnitPrice',
+                              parseFloat(e.target.value) || 0
+                            )}
+                            className="w-32 text-right"
+                            placeholder="단가"
+                          />
+                        )}
+                      </TableCell>
+                      <TableCell>
+                        <div className="font-semibold text-primary">
+                          {formatCurrency(quotation.bidAmount)}
+                        </div>
+                      </TableCell>
+                      <TableCell>
+                        {readOnly ? (
+                          quotation.proposedDeliveryDate ? 
+                            formatDate(quotation.proposedDeliveryDate, 'KR') : '-'
+                        ) : (
+                          <Input
+                            type="date"
+                            value={quotation.proposedDeliveryDate}
+                            onChange={(e) => updateQuotation(
+                              item.id,
+                              'proposedDeliveryDate',
+                              e.target.value
+                            )}
+                            className="w-40"
+                          />
+                        )}
+                      </TableCell>
+                      <TableCell>
+                        {readOnly ? (
+                          <div className="max-w-32 truncate" title={quotation.technicalSpecification || ''}>
+                            {quotation.technicalSpecification || '-'}
+                          </div>
+                        ) : (
+                          <Textarea
+                            value={quotation.technicalSpecification}
+                            onChange={(e) => updateQuotation(
+                              item.id,
+                              'technicalSpecification',
+                              e.target.value
+                            )}
+                            placeholder="기술사양 입력"
+                            className="w-48 min-h-[60px]"
+                            rows={2}
+                          />
+                        )}
+                      </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 ? (
+                              <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>
+                            ) : (
+                              <Badge variant="secondary">문서 없음</Badge>
+                            )}
+                          </div>
+                        ) : (
+                          <Badge variant="outline">SPEC 없음</Badge>
+                        )}
+                      </TableCell>
+                    </TableRow>
+                  )
+                })}
+              </TableBody>
+            </Table>
+          </div>
+
+          {/* 총 금액 표시 */}
+          <div className="flex justify-end">
+            <Card className="w-80">
+              <CardContent className="pt-4">
+                <div className="flex items-center justify-between">
+                  <div className="flex items-center gap-2">
+                    <Calculator className="w-4 h-4" />
+                    <Label className="font-semibold">총 사전견적 금액</Label>
+                  </div>
+                  <div className="text-2xl font-bold text-primary">
+                    {formatCurrency(totalAmount)}
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </div>
+        </div>
+      </CardContent>
+    </Card>
+  )
+}
-- 
cgit v1.2.3

