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

---
 .../table/detail-table/rfq-detail-table.tsx        | 178 ++++++++++++++++-----
 1 file changed, 134 insertions(+), 44 deletions(-)

(limited to 'lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx')

diff --git a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
index f2eda8d9..1d701bd5 100644
--- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
+++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
@@ -12,12 +12,14 @@ import { toast } from "sonner"
 import { Skeleton } from "@/components/ui/skeleton"
 import { Badge } from "@/components/ui/badge"
 import { Button } from "@/components/ui/button"
-import { Loader2, UserPlus, BarChart2, Send, Trash2 } from "lucide-react"
+import { Loader2, UserPlus, Send, Trash2, CheckCircle } from "lucide-react"
 import { ClientDataTable } from "@/components/client-data-table/data-table"
 import { AddVendorDialog } from "./add-vendor-dialog"
 import { VendorCommunicationDrawer } from "./vendor-communication-drawer"
-import { VendorQuotationComparisonDialog } from "./vendor-quotation-comparison-dialog"
 import { DeleteVendorsDialog } from "../delete-vendors-dialog"
+import { QuotationHistoryDialog } from "@/lib/techsales-rfq/table/detail-table/quotation-history-dialog"
+import { TechSalesQuotationAttachmentsSheet, type QuotationAttachment } from "../tech-sales-quotation-attachments-sheet"
+import type { QuotationInfo } from "./rfq-detail-column"
 
 // 기본적인 RFQ 타입 정의
 interface TechSalesRfq {
@@ -30,6 +32,8 @@ interface TechSalesRfq {
   rfqSendDate?: Date | null
   dueDate?: Date | null
   createdByName?: string | null
+  rfqType: "SHIP" | "TOP" | "HULL" | null
+  ptypeNm?: string | null
 }
 
 // 프로퍼티 정의
@@ -58,9 +62,6 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
   // 읽지 않은 메시지 개수
   const [unreadMessages, setUnreadMessages] = useState<Record<number, number>>({})
 
-  // 견적 비교 다이얼로그 상태 관리
-  const [comparisonDialogOpen, setComparisonDialogOpen] = useState(false)
-
   // 테이블 선택 상태 관리
   const [selectedRows, setSelectedRows] = useState<RfqDetailView[]>([])
   const [isSendingRfq, setIsSendingRfq] = useState(false)
@@ -69,6 +70,16 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
   // 벤더 삭제 확인 다이얼로그 상태 추가
   const [deleteConfirmDialogOpen, setDeleteConfirmDialogOpen] = useState(false)
 
+  // 견적 히스토리 다이얼로그 상태 관리
+  const [historyDialogOpen, setHistoryDialogOpen] = useState(false)
+  const [selectedQuotationId, setSelectedQuotationId] = useState<number | null>(null)
+
+  // 견적서 첨부파일 sheet 상태 관리
+  const [quotationAttachmentsSheetOpen, setQuotationAttachmentsSheetOpen] = useState(false)
+  const [selectedQuotationInfo, setSelectedQuotationInfo] = useState<QuotationInfo | null>(null)
+  const [quotationAttachments, setQuotationAttachments] = useState<QuotationAttachment[]>([])
+  const [isLoadingAttachments, setIsLoadingAttachments] = useState(false)
+
   // selectedRfq ID 메모이제이션 (객체 참조 변경 방지)
   const selectedRfqId = useMemo(() => selectedRfq?.id, [selectedRfq?.id])
 
@@ -108,6 +119,8 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
         detailId: item.id,
         rfqId: selectedRfqId,
         rfqCode: selectedRfq?.rfqCode || null,
+        rfqType: selectedRfq?.rfqType || null,
+        ptypeNm: selectedRfq?.ptypeNm || null,
         vendorId: item.vendorId ? Number(item.vendorId) : undefined,
       })) || []
       
@@ -121,7 +134,7 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
       console.error("데이터 새로고침 오류:", error)
       toast.error("데이터를 새로고침하는 중 오류가 발생했습니다")
     }
-  }, [selectedRfqId, selectedRfq?.rfqCode, loadUnreadMessages])
+  }, [selectedRfqId, selectedRfq?.rfqCode, selectedRfq?.rfqType, selectedRfq?.ptypeNm, loadUnreadMessages])
 
   // 벤더 추가 핸들러 메모이제이션
   const handleAddVendor = useCallback(async () => {
@@ -180,6 +193,54 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
     }
   }, [selectedRows, selectedRfqId, handleRefreshData]);
 
+  // 벤더 선택 핸들러 추가
+  const [isAcceptingVendors, setIsAcceptingVendors] = useState(false);
+
+  const handleAcceptVendors = useCallback(async () => {
+    if (selectedRows.length === 0) {
+      toast.warning("선택할 벤더를 선택해주세요.");
+      return;
+    }
+
+    if (selectedRows.length > 1) {
+      toast.warning("하나의 벤더만 선택할 수 있습니다.");
+      return;
+    }
+
+    const selectedQuotation = selectedRows[0];
+    if (selectedQuotation.status !== "Submitted") {
+      toast.warning("제출된 견적서만 선택할 수 있습니다.");
+      return;
+    }
+
+    try {
+      setIsAcceptingVendors(true);
+      
+      // 벤더 견적 승인 서비스 함수 호출
+      const { acceptTechSalesVendorQuotationAction } = await import("@/lib/techsales-rfq/actions");
+      
+      const result = await acceptTechSalesVendorQuotationAction(selectedQuotation.id);
+      
+      if (result.success) {
+        toast.success(result.message || "벤더가 성공적으로 선택되었습니다.");
+      } else {
+        toast.error(result.error || "벤더 선택 중 오류가 발생했습니다.");
+      }
+      
+      // 선택 해제
+      setSelectedRows([]);
+      
+      // 데이터 새로고침
+      await handleRefreshData();
+      
+    } catch (error) {
+      console.error("벤더 선택 오류:", error);
+      toast.error("벤더 선택 중 오류가 발생했습니다.");
+    } finally {
+      setIsAcceptingVendors(false);
+    }
+  }, [selectedRows, handleRefreshData]);
+
   // 벤더 삭제 핸들러 메모이제이션
   const handleDeleteVendors = useCallback(async () => {
     if (selectedRows.length === 0) {
@@ -246,27 +307,47 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
     await handleDeleteVendors();
   }, [handleDeleteVendors]);
 
-  // 견적 비교 다이얼로그 열기 핸들러 메모이제이션
-  const handleOpenComparisonDialog = useCallback(() => {
-    // 제출된 견적이 있는 벤더가 최소 1개 이상 있는지 확인
-    const hasSubmittedQuotations = details.some(detail =>
-      detail.status === "Submitted" // RfqDetailView의 실제 필드 사용
-    );
 
-    if (!hasSubmittedQuotations) {
-      toast.warning("제출된 견적이 없습니다.");
-      return;
-    }
+  // 견적 히스토리 다이얼로그 열기 핸들러 메모이제이션
+  const handleOpenHistoryDialog = useCallback((quotationId: number) => {
+    setSelectedQuotationId(quotationId);
+    setHistoryDialogOpen(true);
+  }, [])
 
-    setComparisonDialogOpen(true);
-  }, [details])
+  // 견적서 첨부파일 sheet 열기 핸들러 메모이제이션
+  const handleOpenQuotationAttachmentsSheet = useCallback(async (quotationId: number, quotationInfo: QuotationInfo) => {
+    try {
+      setIsLoadingAttachments(true);
+      setSelectedQuotationInfo(quotationInfo);
+      setQuotationAttachmentsSheetOpen(true);
+
+      // 견적서 첨부파일 조회
+      const { getTechSalesVendorQuotationAttachments } = await import("@/lib/techsales-rfq/service");
+      const result = await getTechSalesVendorQuotationAttachments(quotationId);
+
+      if (result.error) {
+        toast.error(result.error);
+        setQuotationAttachments([]);
+      } else {
+        setQuotationAttachments(result.data || []);
+      }
+    } catch (error) {
+      console.error("견적서 첨부파일 조회 오류:", error);
+      toast.error("견적서 첨부파일을 불러오는 중 오류가 발생했습니다.");
+      setQuotationAttachments([]);
+    } finally {
+      setIsLoadingAttachments(false);
+    }
+  }, [])
 
   // 칼럼 정의 - unreadMessages 상태 전달 (메모이제이션)
   const columns = useMemo(() =>
     getRfqDetailColumns({
       setRowAction,
-      unreadMessages
-    }), [unreadMessages])
+      unreadMessages,
+      onQuotationClick: handleOpenHistoryDialog,
+      openQuotationAttachmentsSheet: handleOpenQuotationAttachmentsSheet
+    }), [unreadMessages, handleOpenHistoryDialog, handleOpenQuotationAttachmentsSheet])
 
   // 필터 필드 정의 (메모이제이션)
   const advancedFilterFields = useMemo(
@@ -493,6 +574,22 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
               )}
             </div>
             <div className="flex gap-2">
+              {/* 벤더 선택 버튼 */}
+              <Button
+                variant="default"
+                size="sm"
+                onClick={handleAcceptVendors}
+                disabled={selectedRows.length === 0 || isAcceptingVendors}
+                className="gap-2"
+              >
+                {isAcceptingVendors ? (
+                  <Loader2 className="size-4 animate-spin" aria-hidden="true" />
+                ) : (
+                  <CheckCircle className="size-4" aria-hidden="true" />
+                )}
+                <span>벤더 선택</span>
+              </Button>
+
               {/* RFQ 발송 버튼 */}
             <Button
                 variant="outline"
@@ -525,22 +622,6 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
                 <span>벤더 삭제</span>
               </Button>
             
-              {/* 견적 비교 버튼 */}
-            <Button
-                variant="outline"
-              size="sm"
-                onClick={handleOpenComparisonDialog}
-                className="gap-2"
-                disabled={
-                  !selectedRfq ||
-                  details.length === 0 ||
-                  vendorsWithQuotations === 0
-                }
-              >
-                <BarChart2 className="size-4" aria-hidden="true" />
-                <span>견적 비교/선택</span>
-            </Button>
-            
               {/* 벤더 추가 버튼 */}
               <Button
                 variant="outline"
@@ -586,7 +667,7 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
       <AddVendorDialog
         open={vendorDialogOpen}
         onOpenChange={setVendorDialogOpen}
-        selectedRfq={selectedRfq}
+        selectedRfq={selectedRfq as unknown as TechSalesRfq}
         existingVendorIds={existingVendorIds}
         onSuccess={handleRefreshData}
       />
@@ -600,13 +681,6 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
         onSuccess={handleRefreshData}
       />
 
-      {/* 견적 비교 다이얼로그 */}
-      <VendorQuotationComparisonDialog
-        open={comparisonDialogOpen}
-        onOpenChange={setComparisonDialogOpen}
-        selectedRfq={selectedRfq}
-      />
-
       {/* 다중 벤더 삭제 확인 다이얼로그 */}
       <DeleteVendorsDialog
         open={deleteConfirmDialogOpen}
@@ -615,6 +689,22 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
         onConfirm={executeDeleteVendors}
         isLoading={isDeletingVendors}
       />
+
+      {/* 견적 히스토리 다이얼로그 */}
+      <QuotationHistoryDialog
+        open={historyDialogOpen}
+        onOpenChange={setHistoryDialogOpen}
+        quotationId={selectedQuotationId}
+      />
+
+      {/* 견적서 첨부파일 Sheet */}
+      <TechSalesQuotationAttachmentsSheet
+        open={quotationAttachmentsSheetOpen}
+        onOpenChange={setQuotationAttachmentsSheetOpen}
+        quotation={selectedQuotationInfo}
+        attachments={quotationAttachments}
+        isLoading={isLoadingAttachments}
+      />
     </div>
   )
 }
\ No newline at end of file
-- 
cgit v1.2.3

