From 4863ba5d336297dddcc8d6d4b414beceb5559742 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 8 Dec 2025 03:03:26 +0000
Subject: (최겸) 기술영업 rfq eml첨부기능 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../table/detail-table/rfq-detail-table.tsx        | 47 +++++++++++++++++++++-
 1 file changed, 45 insertions(+), 2 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 72f03dc3..aee15594 100644
--- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
+++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
@@ -19,6 +19,7 @@ import { VendorCommunicationDrawer } from "./vendor-communication-drawer"
 import { DeleteVendorDialog } 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 { TechSalesVendorEmlAttachmentsSheet, type VendorEmlAttachment } from "../tech-sales-vendor-eml-attachments-sheet"
 import type { QuotationInfo } from "./rfq-detail-column"
 import { VendorContactSelectionDialog } from "./vendor-contact-selection-dialog"
 import { QuotationContactsViewDialog } from "./quotation-contacts-view-dialog"
@@ -89,6 +90,12 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
   const [quotationAttachments, setQuotationAttachments] = useState<QuotationAttachment[]>([])
   const [isLoadingAttachments, setIsLoadingAttachments] = useState(false)
 
+  // eml 첨부파일 sheet 상태 관리
+  const [emlAttachmentsSheetOpen, setEmlAttachmentsSheetOpen] = useState(false)
+  const [selectedQuotationForEml, setSelectedQuotationForEml] = useState<QuotationInfo | null>(null)
+  const [emlAttachments, setEmlAttachments] = useState<VendorEmlAttachment[]>([])
+  const [isLoadingEmlAttachments, setIsLoadingEmlAttachments] = useState(false)
+
   // 벤더 contact 선택 다이얼로그 상태 관리
   const [contactSelectionDialogOpen, setContactSelectionDialogOpen] = useState(false)
 
@@ -463,6 +470,31 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
     }
   }, [])
 
+  // eml 첨부파일 sheet 열기 핸들러
+  const handleOpenEmlAttachmentsSheet = useCallback(async (quotationId: number, quotationInfo: QuotationInfo) => {
+    try {
+      setIsLoadingEmlAttachments(true)
+      setSelectedQuotationForEml(quotationInfo)
+      setEmlAttachmentsSheetOpen(true)
+
+      const { getTechSalesVendorQuotationEmlAttachments } = await import("@/lib/techsales-rfq/service")
+      const result = await getTechSalesVendorQuotationEmlAttachments(quotationId)
+
+      if (result.error) {
+        toast.error(result.error)
+        setEmlAttachments([])
+      } else {
+        setEmlAttachments(result.data || [])
+      }
+    } catch (error) {
+      console.error("eml 첨부파일 조회 오류:", error)
+      toast.error("eml 첨부파일을 불러오는 중 오류가 발생했습니다.")
+      setEmlAttachments([])
+    } finally {
+      setIsLoadingEmlAttachments(false)
+    }
+  }, [])
+
   // 담당자 조회 다이얼로그 열기 함수
   const handleOpenContactsDialog = useCallback((quotationId: number, vendorName?: string) => {
     setSelectedQuotationForContacts({ id: quotationId, vendorName })
@@ -554,8 +586,9 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
       unreadMessages,
       onQuotationClick: handleOpenHistoryDialog,
       openQuotationAttachmentsSheet: handleOpenQuotationAttachmentsSheet,
-      openContactsDialog: handleOpenContactsDialog
-    }), [unreadMessages, handleOpenHistoryDialog, handleOpenQuotationAttachmentsSheet, handleOpenContactsDialog])
+      openContactsDialog: handleOpenContactsDialog,
+      openEmlAttachmentsSheet: handleOpenEmlAttachmentsSheet
+    }), [unreadMessages, handleOpenHistoryDialog, handleOpenQuotationAttachmentsSheet, handleOpenContactsDialog, handleOpenEmlAttachmentsSheet])
 
   // 필터 필드 정의 (메모이제이션)
   const advancedFilterFields = useMemo(
@@ -928,6 +961,16 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
         isLoading={isLoadingAttachments}
       />
 
+      {/* eml 첨부파일 Sheet */}
+      <TechSalesVendorEmlAttachmentsSheet
+        open={emlAttachmentsSheetOpen}
+        onOpenChange={setEmlAttachmentsSheetOpen}
+        quotation={selectedQuotationForEml}
+        attachments={emlAttachments}
+        isLoading={isLoadingEmlAttachments}
+        onAttachmentsChange={setEmlAttachments}
+      />
+
       {/* 벤더 contact 선택 다이얼로그 */}
       <VendorContactSelectionDialog
         open={contactSelectionDialogOpen}
-- 
cgit v1.2.3


From 198cbcf93b20d3849705dcdbba439326d82b0cae Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 8 Dec 2025 03:24:15 +0000
Subject: (최겸) 기술영업 rfq 프로젝트 숨기기 기능 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/soap/ecc/mapper/bidding-and-pr-mapper.ts       |  1 +
 lib/techsales-rfq/repository.ts                    |  1 +
 lib/techsales-rfq/service.ts                       | 28 ++++++++++++++++++++--
 .../table/detail-table/rfq-detail-table.tsx        |  3 ++-
 .../vendor-contact-selection-dialog.tsx            | 22 +++++++++++++----
 .../vendor-response/detail/project-info-tab.tsx    | 13 +++++++---
 .../table/vendor-quotations-table-columns.tsx      |  7 ++++--
 7 files changed, 63 insertions(+), 12 deletions(-)

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

diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
index 9bf61452..4cdaf90d 100644
--- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
+++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
@@ -296,6 +296,7 @@ export async function mapECCBiddingHeaderToBidding(
     // PR 정보
     prNumber, // 첫번째 PR의 ZREQ_FN 값
     hasPrDocument: false, // PR문서는 POS를 말하는 것으로 보임.
+    plant: eccHeader.WERKS || null, // 플랜트 코드(WERKS)
     
     // 상태 및 설정
     status: 'bidding_generated', // 입찰생성 상태
diff --git a/lib/techsales-rfq/repository.ts b/lib/techsales-rfq/repository.ts
index e6138651..61072d3f 100644
--- a/lib/techsales-rfq/repository.ts
+++ b/lib/techsales-rfq/repository.ts
@@ -94,6 +94,7 @@ export async function selectTechSalesRfqsWithJoin(
     
     // 담당자 및 비고
     picCode: techSalesRfqs.picCode,
+    hideProjectInfoForVendors: techSalesRfqs.hideProjectInfoForVendors,
     remark: techSalesRfqs.remark,
     cancelReason: techSalesRfqs.cancelReason,
     description: techSalesRfqs.description,
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts
index 13d0bbce..8ce41cba 100644
--- a/lib/techsales-rfq/service.ts
+++ b/lib/techsales-rfq/service.ts
@@ -557,6 +557,7 @@ export async function sendTechSalesRfqToVendors(input: {
     email?: string | null;
     epId?: string | null;
   };
+  hideProjectInfoForVendors?: boolean;
 }) {
   unstable_noStore();
   try {
@@ -573,6 +574,7 @@ export async function sendTechSalesRfqToVendors(input: {
         materialCode: true,
         description: true,
         rfqType: true,
+        hideProjectInfoForVendors: true,
       },
       with: {
         biddingProject: true,
@@ -604,6 +606,23 @@ export async function sendTechSalesRfqToVendors(input: {
     }
 
     const isResend = rfq.status === TECH_SALES_RFQ_STATUSES.RFQ_SENT;
+    const effectiveHideProjectInfo =
+      typeof input.hideProjectInfoForVendors === "boolean"
+        ? input.hideProjectInfoForVendors
+        : rfq.hideProjectInfoForVendors ?? false;
+
+    if (
+      typeof input.hideProjectInfoForVendors === "boolean" &&
+      input.hideProjectInfoForVendors !== rfq.hideProjectInfoForVendors
+    ) {
+      await db
+        .update(techSalesRfqs)
+        .set({
+          hideProjectInfoForVendors: input.hideProjectInfoForVendors,
+          updatedAt: new Date(),
+        })
+        .where(eq(techSalesRfqs.id, input.rfqId));
+    }
 
     // 현재 사용자 정보 조회
     const sender = await db.query.users.findFirst({
@@ -728,6 +747,9 @@ export async function sendTechSalesRfqToVendors(input: {
           const rfqItemsResult = await getTechSalesRfqItems(rfq.id);
           const rfqItems = rfqItemsResult.data || [];
 
+          const projectNameForVendor = effectiveHideProjectInfo ? "" : rfq.biddingProject?.projNm || "";
+          const projectCodeForVendor = effectiveHideProjectInfo ? "" : rfq.biddingProject?.pspid || "";
+
           // 이메일 컨텍스트 구성
           const emailContext = {
             language: language,
@@ -735,8 +757,8 @@ export async function sendTechSalesRfqToVendors(input: {
               id: rfq.id,
               code: rfq.rfqCode,
               title: rfqItems.length > 0 ? rfqItems.map(item => item.itemList).join(', ') : '',
-              projectCode: rfq.biddingProject?.pspid || '',
-              projectName: rfq.biddingProject?.projNm || '',
+              projectCode: projectCodeForVendor,
+              projectName: projectNameForVendor,
               description: rfq.remark || '',
               dueDate: rfq.dueDate ? formatDate(rfq.dueDate, "KR") : 'N/A',
               materialCode: rfq.materialCode || '',
@@ -990,6 +1012,7 @@ export async function getTechSalesVendorQuotation(quotationId: number) {
           projMsrm: quotation.projMsrm,
           ptypeNm: quotation.ptypeNm,
         } : null,
+        hideProjectInfoForVendors: quotation.hideProjectInfoForVendors ?? false,
       },
       
       // 벤더 정보
@@ -1414,6 +1437,7 @@ export async function getVendorQuotations(input: {
             dueDate: techSalesRfqs.dueDate,
             rfqStatus: techSalesRfqs.status,
             description: techSalesRfqs.description,
+        hideProjectInfoForVendors: techSalesRfqs.hideProjectInfoForVendors,
             // 프로젝트 정보 (직접 조인)
             projNm: biddingProjects.projNm,
             // 아이템 개수
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 aee15594..d8ced6f8 100644
--- a/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
+++ b/lib/techsales-rfq/table/detail-table/rfq-detail-table.tsx
@@ -257,7 +257,7 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
     contactId: number;
     contactEmail: string;
     contactName: string;
-  }>) => {
+  }>, options?: { hideProjectInfoForVendors?: boolean }) => {
     if (!selectedRfqId) {
       toast.error("선택된 RFQ가 없습니다.");
       return;
@@ -301,6 +301,7 @@ export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps
           name: session.data.user.name || undefined,
           email: session.data.user.email || undefined,
         },
+        hideProjectInfoForVendors: options?.hideProjectInfoForVendors,
       });
       
       if (result.success) {
diff --git a/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx b/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx
index d83394bb..8daa9be7 100644
--- a/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx
+++ b/lib/techsales-rfq/table/detail-table/vendor-contact-selection-dialog.tsx
@@ -49,7 +49,10 @@ interface VendorContactSelectionDialogProps {
   onOpenChange: (open: boolean) => void
   vendorIds: number[]
   rfqId?: number // RFQ ID 추가
-  onSendRfq: (selectedContacts: SelectedContact[]) => Promise<void>
+  onSendRfq: (
+    selectedContacts: SelectedContact[],
+    options: { hideProjectInfoForVendors: boolean }
+  ) => Promise<void>
 }
 
 export function VendorContactSelectionDialog({
@@ -63,6 +66,7 @@ export function VendorContactSelectionDialog({
   const [selectedContacts, setSelectedContacts] = useState<SelectedContact[]>([])
   const [isLoading, setIsLoading] = useState(false)
   const [isSending, setIsSending] = useState(false)
+  const [hideProjectInfoForVendors, setHideProjectInfoForVendors] = useState(false)
 
   // 벤더 contact 정보 조회
   useEffect(() => {
@@ -77,6 +81,7 @@ export function VendorContactSelectionDialog({
       setVendorsWithContacts({})
       setSelectedContacts([])
       setIsLoading(false)
+      setHideProjectInfoForVendors(false)
     }
   }, [open])
 
@@ -177,7 +182,7 @@ export function VendorContactSelectionDialog({
 
     try {
       setIsSending(true)
-      await onSendRfq(selectedContacts)
+      await onSendRfq(selectedContacts, { hideProjectInfoForVendors })
       onOpenChange(false)
     } catch (error) {
       console.error("RFQ 발송 오류:", error)
@@ -328,8 +333,17 @@ export function VendorContactSelectionDialog({
 
         <DialogFooter>
           <div className="flex items-center justify-between w-full">
-            <div className="text-sm text-muted-foreground">
-              총 {selectedContacts.length}명의 연락처가 선택됨
+            <div className="flex flex-col gap-2">
+              <div className="text-sm text-muted-foreground">
+                총 {selectedContacts.length}명의 연락처가 선택됨
+              </div>
+              <label className="flex items-center gap-2 text-sm">
+                <Checkbox
+                  checked={hideProjectInfoForVendors}
+                  onCheckedChange={(checked) => setHideProjectInfoForVendors(!!checked)}
+                />
+                벤더 화면에서 프로젝트명/선주명을 숨기기
+              </label>
             </div>
             <div className="flex gap-2">
               <Button variant="outline" onClick={() => onOpenChange(false)}>
diff --git a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx
index 8a45f529..31e87330 100644
--- a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx
+++ b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx
@@ -16,6 +16,7 @@ interface ProjectInfoTabProps {
       dueDate: Date | null
       status: string | null
       remark: string | null
+      hideProjectInfoForVendors?: boolean
       biddingProject?: {
         id: number
         pspid: string | null
@@ -110,7 +111,9 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) {
             <CardHeader>
               <CardTitle className="flex items-center gap-2">
                 프로젝트 기본 정보
-                <Badge variant="outline">{rfq.biddingProject.pspid || "N/A"}</Badge>
+                <Badge variant="outline">
+                  {rfq.hideProjectInfoForVendors ? "비공개" : (rfq.biddingProject.pspid || "N/A")}
+                </Badge>
               </CardTitle>
               <CardDescription>
                 연결된 프로젝트의 기본 정보
@@ -120,11 +123,15 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) {
               <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                 <div className="space-y-2">
                   <div className="text-sm font-medium text-muted-foreground">프로젝트 ID</div>
-                  <div className="text-sm">{rfq.biddingProject.pspid || "N/A"}</div>
+                  <div className="text-sm">
+                    {rfq.hideProjectInfoForVendors ? "비공개" : (rfq.biddingProject.pspid || "N/A")}
+                  </div>
                 </div>
                 <div className="space-y-2">
                   <div className="text-sm font-medium text-muted-foreground">프로젝트명</div>
-                  <div className="text-sm">{rfq.biddingProject.projNm || "N/A"}</div>
+                  <div className="text-sm">
+                    {rfq.hideProjectInfoForVendors ? "비공개" : (rfq.biddingProject.projNm || "N/A")}
+                  </div>
                 </div>
                 <div className="space-y-2">
                   <div className="text-sm font-medium text-muted-foreground">프로젝트 섹터</div>
diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
index aabe7a64..97f21be2 100644
--- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
+++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx
@@ -27,6 +27,7 @@ interface QuotationWithRfqCode extends TechSalesVendorQuotations {
   materialCode?: string;
   dueDate?: Date;
   rfqStatus?: string;
+  hideProjectInfoForVendors?: boolean;
 
   // 아이템 정보
   itemName?: string;
@@ -258,17 +259,19 @@ export function getColumns({ router, openAttachmentsSheet, openItemsDialog, open
       ),
       cell: ({ row }) => {
         const projNm = row.getValue("projNm") as string;
+        const hideProjectInfo = row.original.hideProjectInfoForVendors === true;
+        const displayValue = hideProjectInfo ? "비공개" : projNm || "N/A";
         return (
           <div className="min-w-48 max-w-64">
             <TooltipProvider>
               <Tooltip>
                 <TooltipTrigger asChild>
                   <span className="truncate block text-sm">
-                    {projNm || "N/A"}
+                    {displayValue}
                   </span>
                 </TooltipTrigger>
                 <TooltipContent>
-                  <p className="max-w-xs">{projNm || "N/A"}</p>
+                  <p className="max-w-xs">{displayValue}</p>
                 </TooltipContent>
               </Tooltip>
             </TooltipProvider>
-- 
cgit v1.2.3

