From 7bf90e71ee98abe2d65e18eaf20a449cd1bd097c Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Tue, 4 Nov 2025 16:28:17 +0900
Subject: (김준회) 파트너 RFQ 응답: RFQ PR 아이템 다이얼로그와 유사하게 변경,
 기존 액션들은 유지
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../editor/quotation-items-table.tsx               | 515 +++++++++++++++------
 1 file changed, 363 insertions(+), 152 deletions(-)

(limited to 'lib/rfq-last/vendor-response/editor/quotation-items-table.tsx')

diff --git a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx
index 54866822..23ddc924 100644
--- a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx
+++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx
@@ -11,10 +11,15 @@ import { Calendar } from "@/components/ui/calendar"
 import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
 import { Badge } from "@/components/ui/badge"
 import { Label } from "@/components/ui/label"
-import { CalendarIcon, Eye, Calculator, AlertCircle } from "lucide-react"
+import { Separator } from "@/components/ui/separator"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { CalendarIcon, Eye, FileText, Download, ExternalLink } from "lucide-react"
 import { format } from "date-fns"
 import { cn, formatCurrency } from "@/lib/utils"
 import { useState, useEffect } from "react"
+import { toast } from "sonner"
+import { checkPosFileExists, getDownloadUrlByMaterialCode } from "@/lib/pos"
+import { PosFileSelectionDialog } from "@/lib/pos/components/pos-file-selection-dialog"
 import {
   Dialog,
   DialogContent,
@@ -23,17 +28,6 @@ import {
   DialogHeader,
   DialogTitle,
 } from "@/components/ui/dialog"
-import {
-  AlertDialog,
-  AlertDialogAction,
-  AlertDialogCancel,
-  AlertDialogContent,
-  AlertDialogDescription,
-  AlertDialogFooter,
-  AlertDialogHeader,
-  AlertDialogTitle,
-  AlertDialogTrigger,
-} from "@/components/ui/alert-dialog"
 
 interface QuotationItemsTableProps {
   prItems: any[]
@@ -50,12 +44,35 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp
   const [showDetail, setShowDetail] = useState(false)
   const [showBulkDateDialog, setShowBulkDateDialog] = useState(false)
   const [bulkDeliveryDate, setBulkDeliveryDate] = useState<Date | undefined>(undefined)
+  
+  // POS 파일 관련 상태
+  const [posDialogOpen, setPosDialogOpen] = useState(false)
+  const [selectedMaterialCode, setSelectedMaterialCode] = useState<string>("")
+  const [posFiles, setPosFiles] = useState<Array<{
+    fileName: string
+    dcmtmId: string
+    projNo: string
+    posNo: string
+    posRevNo: string
+    fileSer: string
+  }>>([])
+  const [loadingPosFiles, setLoadingPosFiles] = useState(false)
+  const [downloadingFileIndex, setDownloadingFileIndex] = useState<number | null>(null)
 
   const currency = watch("vendorCurrency") || "USD"
   const quotationItems = watch("quotationItems")
 
   console.log(prItems,"prItems")
   
+  // 통계 정보 계산
+  const statistics = {
+    total: prItems.length,
+    regular: prItems.filter(item => !item.majorYn).length,
+    major: prItems.filter(item => item.majorYn).length,
+    totalQuantity: prItems.reduce((sum, item) => sum + (item.quantity || 0), 0),
+    totalWeight: prItems.reduce((sum, item) => sum + (item.grossWeight || 0), 0),
+  }
+  
   // PR 아이템 정보를 quotationItems에 초기화
   useEffect(() => {
     if (prItems && prItems.length > 0) {
@@ -90,19 +107,6 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp
       setValue(`quotationItems.${index}.uom`, prItem.uom)
     }
   }
-  
-  // 할인 적용
-  const applyDiscount = (index: number) => {
-    const item = quotationItems[index]
-    const prItem = prItems[index]
-    if (item && prItem && item.discountRate) {
-      const originalTotal = (item.unitPrice || 0) * (prItem.quantity || 0)
-      const discountAmount = originalTotal * (item.discountRate / 100)
-      const finalTotal = originalTotal - discountAmount
-      setValue(`quotationItems.${index}.totalPrice`, finalTotal)
-      setValue(`quotationItems.${index}.discountAmount`, discountAmount)
-    }
-  }
 
   // 일괄 납기일 적용
   const applyBulkDeliveryDate = () => {
@@ -122,6 +126,79 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp
     })
   }
   
+  // 사양서 링크 열기
+  const handleOpenSpec = (specUrl: string) => {
+    window.open(specUrl, '_blank', 'noopener,noreferrer')
+  }
+
+  // POS 파일 목록 조회 및 다이얼로그 열기
+  const handleOpenPosDialog = async (materialCode: string) => {
+    if (!materialCode) {
+      toast.error("자재코드가 없습니다")
+      return
+    }
+
+    setLoadingPosFiles(true)
+    setSelectedMaterialCode(materialCode)
+
+    try {
+      toast.loading(`POS 파일 목록 조회 중... (${materialCode})`, { id: `pos-check-${materialCode}` })
+      
+      const result = await checkPosFileExists(materialCode)
+      
+      if (result.exists && result.files && result.files.length > 0) {
+        const detailResult = await getDownloadUrlByMaterialCode(materialCode)
+        
+        if (detailResult.success && detailResult.availableFiles) {
+          setPosFiles(detailResult.availableFiles)
+          setPosDialogOpen(true)
+          toast.success(`${result.fileCount}개의 POS 파일을 찾았습니다`, { id: `pos-check-${materialCode}` })
+        } else {
+          toast.error('POS 파일 정보를 가져올 수 없습니다', { id: `pos-check-${materialCode}` })
+        }
+      } else {
+        toast.error(result.error || 'POS 파일을 찾을 수 없습니다', { id: `pos-check-${materialCode}` })
+      }
+    } catch (error) {
+      console.error("POS 파일 조회 오류:", error)
+      toast.error("POS 파일 조회에 실패했습니다", { id: `pos-check-${materialCode}` })
+    } finally {
+      setLoadingPosFiles(false)
+    }
+  }
+
+  // POS 파일 다운로드 실행
+  const handleDownloadPosFile = async (fileIndex: number, fileName: string) => {
+    if (!selectedMaterialCode) return
+
+    setDownloadingFileIndex(fileIndex)
+
+    try {
+      toast.loading(`POS 파일 다운로드 준비 중...`, { id: `download-${fileIndex}` })
+      
+      const downloadUrl = `/api/pos/download-on-demand?materialCode=${encodeURIComponent(selectedMaterialCode)}&fileIndex=${fileIndex}`
+      
+      toast.success(`POS 파일 다운로드 시작: ${fileName}`, { id: `download-${fileIndex}` })
+      window.open(downloadUrl, '_blank', 'noopener,noreferrer')
+      
+      setTimeout(() => {
+        setDownloadingFileIndex(null)
+      }, 1000)
+    } catch (error) {
+      console.error("POS 파일 다운로드 오류:", error)
+      toast.error("POS 파일 다운로드에 실패했습니다", { id: `download-${fileIndex}` })
+      setDownloadingFileIndex(null)
+    }
+  }
+
+  // POS 다이얼로그 닫기
+  const handleClosePosDialog = () => {
+    setPosDialogOpen(false)
+    setSelectedMaterialCode("")
+    setPosFiles([])
+    setDownloadingFileIndex(null)
+  }
+  
   const totalAmount = quotationItems?.reduce(
     (sum: number, item: any) => sum + (item.totalPrice || 0), 0
   ) || 0
@@ -397,139 +474,263 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp
             </div>
           </div>
         </div>
+        
+        {/* 통계 정보 */}
+        <div className="mt-4">
+          <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
+            <div className="text-center p-3 border rounded-lg bg-muted/50">
+              <div className="text-2xl font-bold text-primary">{statistics.total}</div>
+              <div className="text-xs text-muted-foreground">전체 품목</div>
+            </div>
+            <div className="text-center p-3 border rounded-lg bg-muted/50">
+              <div className="text-2xl font-bold text-gray-600">{statistics.regular}</div>
+              <div className="text-xs text-muted-foreground">일반 품목</div>
+            </div>
+            <div className="text-center p-3 border rounded-lg bg-muted/50">
+              <div className="text-2xl font-bold text-green-600">{statistics.totalQuantity.toLocaleString()}</div>
+              <div className="text-xs text-muted-foreground">총 수량</div>
+            </div>
+            <div className="text-center p-3 border rounded-lg bg-muted/50">
+              <div className="text-2xl font-bold text-orange-600">{statistics.totalWeight.toLocaleString()}</div>
+              <div className="text-xs text-muted-foreground">총 중량 (KG)</div>
+            </div>
+          </div>
+          <Separator className="mt-4" />
+        </div>
       </CardHeader>
       <CardContent>
-        <div className="overflow-x-auto">
-          <Table>
-            <TableHeader>
-              <TableRow>
-                <TableHead className="w-[50px]">No</TableHead>
-                <TableHead className="w-[100px]">PR No</TableHead>
-                <TableHead className="min-w-[150px]">자재코드</TableHead>
-                <TableHead className="min-w-[200px]">자재명</TableHead>
-                <TableHead className="text-right w-[100px]">수량</TableHead>
-                <TableHead className="w-[150px]">단가</TableHead>
-                <TableHead className="text-right w-[150px]">총액</TableHead>
-                <TableHead className="w-[150px]">납기일</TableHead>
-                <TableHead className="w-[80px]">작업</TableHead>
-              </TableRow>
-            </TableHeader>
-            <TableBody>
-              {fields.map((field, index) => {
-                const prItem = prItems[index]
-                const quotationItem = quotationItems[index]
-                const isMajor = prItem?.majorYn
-                
-                return (
-                  <TableRow key={field.id} className={isMajor ? "bg-yellow-50" : ""}>
-                    <TableCell>
-                      <div className="flex items-center gap-2">
-                        {prItem?.rfqItem || index + 1}
-                        {isMajor && (
-                          <Badge variant="secondary" className="text-xs">
-                            주요
-                          </Badge>
-                        )}
-                      </div>
-                    </TableCell>
-                    <TableCell className="font-mono text-xs">
-                      {prItem?.prNo}
-                    </TableCell>
-                    <TableCell className="font-mono text-xs">
-                      {prItem?.materialCode}
-                    </TableCell>
-                    <TableCell>
-                      <div className="max-w-[200px]">
-                        <p className="truncate text-sm" title={prItem?.materialDescription}>
-                          {prItem?.materialDescription}
-                        </p>
-                        {prItem?.size && (
-                          <p className="text-xs text-muted-foreground">
-                            사이즈: {prItem.size}
+        <ScrollArea className="h-[600px]">
+          <div className="overflow-x-auto">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead className="w-[60px]">No</TableHead>
+                  <TableHead className="w-[100px]">PR No</TableHead>
+                  <TableHead className="w-[80px]">PR 아이템</TableHead>
+                  <TableHead className="min-w-[150px]">자재코드</TableHead>
+                  <TableHead className="min-w-[200px]">자재명</TableHead>
+                  <TableHead className="text-right w-[80px]">수량</TableHead>
+                  <TableHead className="w-[60px]">단위</TableHead>
+                  <TableHead className="text-right w-[80px]">중량</TableHead>
+                  <TableHead className="w-[60px]">중량단위</TableHead>
+                  <TableHead className="w-[150px]">단가</TableHead>
+                  <TableHead className="text-right w-[150px]">총액</TableHead>
+                  <TableHead className="w-[150px]">납기일</TableHead>
+                  <TableHead className="w-[180px]">사양/POS</TableHead>
+                  <TableHead className="w-[120px]">프로젝트</TableHead>
+                  <TableHead className="w-[80px]">상세</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {fields.map((field, index) => {
+                  const prItem = prItems[index]
+                  const quotationItem = quotationItems[index]
+                  const isMajor = prItem?.majorYn
+                  
+                  return (
+                    <TableRow key={field.id} className={isMajor ? "bg-blue-50 border-l-4 border-l-blue-500" : ""}>
+                      <TableCell>
+                        <div className="flex flex-col items-center gap-1">
+                          <span className="text-xs font-mono">#{index + 1}</span>
+                          {isMajor && (
+                            <Badge variant="default" className="text-xs px-1 py-0">
+                              주요
+                            </Badge>
+                          )}
+                        </div>
+                      </TableCell>
+                      <TableCell className="font-mono text-xs">
+                        {prItem?.prNo || "-"}
+                      </TableCell>
+                      <TableCell className="font-mono text-xs">
+                        {prItem?.prItem || prItem?.rfqItem || "-"}
+                      </TableCell>
+                      <TableCell>
+                        <div className="flex flex-col">
+                          <span className="font-mono text-sm font-medium">{prItem?.materialCode || "-"}</span>
+                          {prItem?.acc && (
+                            <span className="text-xs text-muted-foreground font-mono">
+                              ACC: {prItem.acc}
+                            </span>
+                          )}
+                        </div>
+                      </TableCell>
+                      <TableCell>
+                        <div className="flex flex-col max-w-[200px]">
+                          <p className="truncate text-sm font-medium" title={prItem?.materialDescription}>
+                            {prItem?.materialDescription || "-"}
                           </p>
-                        )}
-                      </div>
-                    </TableCell>
-                    <TableCell className="text-right">
-                      {prItem?.quantity} {prItem?.uom}
-                    </TableCell>
-                    <TableCell>
-                      <div className="flex items-center gap-1">
-                        <Input
-                          type="number"
-                          min="0"
-                          step="1"
-                          {...register(`quotationItems.${index}.unitPrice`, { valueAsNumber: true })}
-                          onChange={(e) => {
-                            const value = Math.max(0, Math.floor(parseFloat(e.target.value) || 0))
-                            setValue(`quotationItems.${index}.unitPrice`, value)
-                            calculateTotal(index)
-                          }}
-                          className="w-[120px]"
-                          placeholder="0"
-                        />
-                        <span className="text-xs text-muted-foreground">
-                          {currency}
+                          {prItem?.materialCategory && (
+                            <span className="text-xs text-muted-foreground">
+                              {prItem.materialCategory}
+                            </span>
+                          )}
+                          {prItem?.size && (
+                            <span className="text-xs text-muted-foreground">
+                              크기: {prItem.size}
+                            </span>
+                          )}
+                        </div>
+                      </TableCell>
+                      <TableCell className="text-right">
+                        <span className="text-sm font-medium">
+                          {prItem?.quantity ? prItem.quantity.toLocaleString() : "-"}
+                        </span>
+                      </TableCell>
+                      <TableCell>
+                        <span className="text-sm text-muted-foreground">
+                          {prItem?.uom || "-"}
                         </span>
-                      </div>
-                    </TableCell>
-                    <TableCell className="text-right font-medium">
-                      {formatCurrency(quotationItem?.totalPrice || 0, currency)}
-                    </TableCell>
-                    <TableCell>
-                      <Popover>
-                        <PopoverTrigger asChild>
-                          <Button
-                            variant="outline"
-                            size="sm"
-                            className={cn(
-                              "w-[130px] justify-start text-left font-normal",
-                              !quotationItem?.vendorDeliveryDate && "text-muted-foreground"
-                            )}
-                          >
-                            <CalendarIcon className="mr-2 h-3 w-3" />
-                            {quotationItem?.vendorDeliveryDate 
-                              ? format(quotationItem.vendorDeliveryDate, "yyyy-MM-dd") 
-                              : "선택"}
-                          </Button>
-                        </PopoverTrigger>
-                        <PopoverContent className="w-auto p-0" align="start">
-                          <Calendar
-                            mode="single"
-                            selected={quotationItem?.vendorDeliveryDate}
-                            onSelect={(date) => setValue(`quotationItems.${index}.vendorDeliveryDate`, date)}
-                            initialFocus
+                      </TableCell>
+                      <TableCell className="text-right">
+                        <span className="text-sm font-medium">
+                          {prItem?.grossWeight ? prItem.grossWeight.toLocaleString() : "-"}
+                        </span>
+                      </TableCell>
+                      <TableCell>
+                        <span className="text-sm text-muted-foreground">
+                          {prItem?.gwUom || "-"}
+                        </span>
+                      </TableCell>
+                      <TableCell>
+                        <div className="flex items-center gap-1">
+                          <Input
+                            type="number"
+                            min="0"
+                            step="1"
+                            {...register(`quotationItems.${index}.unitPrice`, { valueAsNumber: true })}
+                            onChange={(e) => {
+                              const value = Math.max(0, Math.floor(parseFloat(e.target.value) || 0))
+                              setValue(`quotationItems.${index}.unitPrice`, value)
+                              calculateTotal(index)
+                            }}
+                            className="w-[120px]"
+                            placeholder="0"
                           />
-                        </PopoverContent>
-                      </Popover>
-                      {prItem?.deliveryDate && quotationItem?.vendorDeliveryDate && 
-                       new Date(quotationItem.vendorDeliveryDate) > new Date(prItem.deliveryDate) && (
-                        <div className="mt-1">
-                          <Badge variant="destructive" className="text-xs">
-                            지연
-                          </Badge>
+                          <span className="text-xs text-muted-foreground">
+                            {currency}
+                          </span>
                         </div>
-                      )}
-                    </TableCell>
-                    <TableCell>
-                      <Button
-                        type="button"
-                        variant="ghost"
-                        size="sm"
-                        onClick={() => {
-                          setSelectedItem({ item: quotationItem, prItem, index })
-                          setShowDetail(true)
-                        }}
-                      >
-                        <Eye className="h-4 w-4" />
-                      </Button>
-                    </TableCell>
-                  </TableRow>
-                )
-              })}
-            </TableBody>
-          </Table>
-        </div>
+                      </TableCell>
+                      <TableCell className="text-right font-medium">
+                        {formatCurrency(quotationItem?.totalPrice || 0, currency)}
+                      </TableCell>
+                      <TableCell>
+                        <Popover>
+                          <PopoverTrigger asChild>
+                            <Button
+                              variant="outline"
+                              size="sm"
+                              className={cn(
+                                "w-[130px] justify-start text-left font-normal",
+                                !quotationItem?.vendorDeliveryDate && "text-muted-foreground"
+                              )}
+                            >
+                              <CalendarIcon className="mr-2 h-3 w-3" />
+                              {quotationItem?.vendorDeliveryDate 
+                                ? format(quotationItem.vendorDeliveryDate, "yyyy-MM-dd") 
+                                : "선택"}
+                            </Button>
+                          </PopoverTrigger>
+                          <PopoverContent className="w-auto p-0" align="start">
+                            <Calendar
+                              mode="single"
+                              selected={quotationItem?.vendorDeliveryDate}
+                              onSelect={(date) => setValue(`quotationItems.${index}.vendorDeliveryDate`, date)}
+                              initialFocus
+                            />
+                          </PopoverContent>
+                        </Popover>
+                        {prItem?.deliveryDate && quotationItem?.vendorDeliveryDate && 
+                         new Date(quotationItem.vendorDeliveryDate) > new Date(prItem.deliveryDate) && (
+                          <div className="mt-1">
+                            <Badge variant="destructive" className="text-xs">
+                              지연
+                            </Badge>
+                          </div>
+                        )}
+                      </TableCell>
+                      <TableCell>
+                        <div className="flex flex-col gap-1">
+                          {/* 사양서 정보 */}
+                          {(prItem?.specNo || prItem?.specUrl) && (
+                            <div className="flex items-center gap-1">
+                              {prItem.specNo && (
+                                <span className="text-xs font-mono">{prItem.specNo}</span>
+                              )}
+                              {prItem.specUrl && (
+                                <Button
+                                  type="button"
+                                  variant="ghost"
+                                  size="sm"
+                                  className="h-5 w-5 p-0"
+                                  onClick={() => handleOpenSpec(prItem.specUrl!)}
+                                  title="사양서 열기"
+                                >
+                                  <ExternalLink className="h-3 w-3" />
+                                </Button>
+                              )}
+                            </div>
+                          )}
+                          
+                          {/* POS 파일 다운로드 */}
+                          {prItem?.materialCode && (
+                            <div className="flex items-center gap-1">
+                              <FileText className="h-3 w-3 text-green-500" />
+                              <Button
+                                type="button"
+                                variant="ghost"
+                                size="sm"
+                                className="h-5 p-1 text-xs text-green-600 hover:text-green-800"
+                                onClick={() => handleOpenPosDialog(prItem.materialCode!)}
+                                disabled={loadingPosFiles && selectedMaterialCode === prItem.materialCode}
+                                title={`POS 파일 다운로드 (자재코드: ${prItem.materialCode})`}
+                              >
+                                <Download className="h-3 w-3 mr-1" />
+                                {loadingPosFiles && selectedMaterialCode === prItem.materialCode ? '조회중...' : 'POS'}
+                              </Button>
+                            </div>
+                          )}
+                          
+                          {/* 트래킹 번호 */}
+                          {prItem?.trackingNo && (
+                            <div className="text-xs text-muted-foreground">
+                              TRK: {prItem.trackingNo}
+                            </div>
+                          )}
+                        </div>
+                      </TableCell>
+                      <TableCell>
+                        <div className="text-xs">
+                          {[
+                            prItem?.projectDef && `${prItem.projectDef}`,
+                            prItem?.projectSc && `SC: ${prItem.projectSc}`,
+                            prItem?.projectKl && `KL: ${prItem.projectKl}`,
+                            prItem?.projectLc && `LC: ${prItem.projectLc}`,
+                            prItem?.projectDl && `DL: ${prItem.projectDl}`
+                          ].filter(Boolean).join(" | ") || "-"}
+                        </div>
+                      </TableCell>
+                      <TableCell>
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => {
+                            setSelectedItem({ item: quotationItem, prItem, index })
+                            setShowDetail(true)
+                          }}
+                        >
+                          <Eye className="h-4 w-4" />
+                        </Button>
+                      </TableCell>
+                    </TableRow>
+                  )
+                })}
+              </TableBody>
+            </Table>
+          </div>
+        </ScrollArea>
         
         {/* 총액 요약 */}
         <div className="mt-4 flex justify-end">
@@ -633,6 +834,16 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp
           </DialogFooter>
         </DialogContent>
       </Dialog>
+      
+      {/* POS 파일 선택 다이얼로그 */}
+      <PosFileSelectionDialog
+        isOpen={posDialogOpen}
+        onClose={handleClosePosDialog}
+        materialCode={selectedMaterialCode}
+        files={posFiles}
+        onDownload={handleDownloadPosFile}
+        downloadingIndex={downloadingFileIndex}
+      />
     </Card>
   )
 }
\ No newline at end of file
-- 
cgit v1.2.3

