From 14f61e24947fb92dd71ec0a7196a6e815f8e66da Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 21 Jul 2025 07:54:26 +0000
Subject: (최겸)기술영업 RFQ 담당자 초대, 요구사항 반영
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../contact-table-toolbar-actions.tsx              | 264 +++++++++++++--------
 1 file changed, 162 insertions(+), 102 deletions(-)

(limited to 'lib/tech-vendors/contacts-table/contact-table-toolbar-actions.tsx')

diff --git a/lib/tech-vendors/contacts-table/contact-table-toolbar-actions.tsx b/lib/tech-vendors/contacts-table/contact-table-toolbar-actions.tsx
index 7622c6d6..84228a54 100644
--- a/lib/tech-vendors/contacts-table/contact-table-toolbar-actions.tsx
+++ b/lib/tech-vendors/contacts-table/contact-table-toolbar-actions.tsx
@@ -1,103 +1,163 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { Download, Upload } from "lucide-react"
-import { toast } from "sonner"
-
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-import { TechVendorContact } from "@/db/schema/techVendors"
-import { AddContactDialog } from "./add-contact-dialog"
-import { importTasksExcel } from "@/lib/tasks/service"
-
-interface TechVendorContactsTableToolbarActionsProps {
-  table: Table<TechVendorContact>
-  vendorId: number
-}
-
-export function TechVendorContactsTableToolbarActions({ table, vendorId }: TechVendorContactsTableToolbarActionsProps) {
-  // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식
-  const fileInputRef = React.useRef<HTMLInputElement>(null)
-
-  // 파일이 선택되었을 때 처리
-  async function onFileChange(event: React.ChangeEvent<HTMLInputElement>) {
-    const file = event.target.files?.[0]
-    if (!file) return
-
-    // 파일 초기화 (동일 파일 재업로드 시에도 onChange가 트리거되도록)
-    event.target.value = ""
-
-    // 서버 액션 or API 호출
-    try {
-      // 예: 서버 액션 호출
-      const { errorFile, errorMessage } = await importTasksExcel(file)
-
-      if (errorMessage) {
-        toast.error(errorMessage)
-      }
-      if (errorFile) {
-        // 에러 엑셀을 다운로드
-        const url = URL.createObjectURL(errorFile)
-        const link = document.createElement("a")
-        link.href = url
-        link.download = "errors.xlsx"
-        link.click()
-        URL.revokeObjectURL(url)
-      } else {
-        // 성공
-        toast.success("Import success")
-        // 필요 시 revalidateTag("tasks") 등
-      }
-
-    } catch (error) {
-      toast.error("파일 업로드 중 오류가 발생했습니다.")
-      
-    }
-  }
-
-  function handleImportClick() {
-    // 숨겨진 <input type="file" /> 요소를 클릭
-    fileInputRef.current?.click()
-  }
-
-  return (
-    <div className="flex items-center gap-2">
-
-      <AddContactDialog vendorId={vendorId}/>
-
-      {/** 3) Import 버튼 (파일 업로드) */}
-      <Button variant="outline" size="sm" className="gap-2" onClick={handleImportClick}>
-        <Upload className="size-4" aria-hidden="true" />
-        <span className="hidden sm:inline">Import</span>
-      </Button>
-      {/*
-        실제로는 숨겨진 input과 연결:
-        - accept=".xlsx,.xls" 등으로 Excel 파일만 업로드 허용
-      */}
-      <input
-        ref={fileInputRef}
-        type="file"
-        accept=".xlsx,.xls"
-        className="hidden"
-        onChange={onFileChange}
-      />
-
-      {/** 4) Export 버튼 */}
-      <Button
-        variant="outline"
-        size="sm"
-        onClick={() =>
-          exportTableToExcel(table, {
-            filename: "tech-vendor-contacts",
-            excludeColumns: ["select", "actions"],
-          })
-        }
-        className="gap-2"
-      >
-        <Download className="size-4" aria-hidden="true" />
-        <span className="hidden sm:inline">Export</span>
-      </Button>
-    </div>
-  )
+"use client"
+
+import * as React from "react"
+import { type Table } from "@tanstack/react-table"
+import { Download, Upload } from "lucide-react"
+import { toast } from "sonner"
+import ExcelJS from "exceljs"
+
+import { exportTableToExcel } from "@/lib/export"
+import { Button } from "@/components/ui/button"
+import { TechVendorContact } from "@/db/schema/techVendors"
+import { AddContactDialog } from "./add-contact-dialog"
+import { 
+  importTechVendorContacts, 
+  generateContactImportTemplate, 
+  parseContactImportFile 
+} from "@/lib/tech-vendors/service"
+
+interface TechVendorContactsTableToolbarActionsProps {
+  table: Table<TechVendorContact>
+  vendorId: number
+}
+
+export function TechVendorContactsTableToolbarActions({ table, vendorId }: TechVendorContactsTableToolbarActionsProps) {
+  // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식
+  const fileInputRef = React.useRef<HTMLInputElement>(null)
+
+  // 파일이 선택되었을 때 처리
+  async function onFileChange(event: React.ChangeEvent<HTMLInputElement>) {
+    const file = event.target.files?.[0]
+    if (!file) return
+
+    // 파일 초기화 (동일 파일 재업로드 시에도 onChange가 트리거되도록)
+    event.target.value = ""
+
+    try {
+      // Excel 파일 파싱
+      const contactData = await parseContactImportFile(file)
+      
+      if (contactData.length === 0) {
+        toast.error("유효한 데이터가 없습니다. 템플릿 형식을 확인해주세요.")
+        return
+      }
+
+      // 서버로 데이터 전송
+      const result = await importTechVendorContacts(contactData)
+
+      if (result.successCount > 0) {
+        toast.success(`${result.successCount}개 연락처가 성공적으로 추가되었습니다.`)
+      }
+      
+      if (result.failedRows.length > 0) {
+        toast.error(`${result.failedRows.length}개 행에서 오류가 발생했습니다.`)
+        
+        // 에러 데이터를 Excel로 다운로드
+        const errorWorkbook = new ExcelJS.Workbook()
+        const errorWorksheet = errorWorkbook.addWorksheet("오류내역")
+        
+        // 헤더 추가
+        errorWorksheet.columns = [
+          { header: "행번호", key: "row", width: 10 },
+          { header: "벤더이메일", key: "vendorEmail", width: 25 },
+          { header: "담당자명", key: "contactName", width: 20 },
+          { header: "담당자이메일", key: "contactEmail", width: 25 },
+          { header: "오류내용", key: "error", width: 80, style: { alignment: { wrapText: true } , font: { color: { argb: "FFFF0000" } } } },
+        ]
+        
+        // 오류 데이터 추가
+        result.failedRows.forEach(failedRow => {
+          errorWorksheet.addRow({
+            row: failedRow.row,
+            error: failedRow.error,
+            vendorEmail: failedRow.vendorEmail,
+            contactName: failedRow.contactName,
+            contactEmail: failedRow.contactEmail,
+          })
+        })
+        
+        const buffer = await errorWorkbook.xlsx.writeBuffer()
+        const blob = new Blob([buffer], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        })
+        const url = URL.createObjectURL(blob)
+        const link = document.createElement("a")
+        link.href = url
+        link.download = "contact-import-errors.xlsx"
+        link.click()
+        URL.revokeObjectURL(url)
+      }
+
+    } catch (error) {
+      toast.error("파일 업로드 중 오류가 발생했습니다.")
+      console.error("Import error:", error)
+    }
+  }
+
+  function handleImportClick() {
+    // 숨겨진 <input type="file" /> 요소를 클릭
+    fileInputRef.current?.click()
+  }
+
+  async function handleTemplateDownload() {
+    try {
+      const templateBlob = await generateContactImportTemplate()
+      const url = URL.createObjectURL(templateBlob)
+      const link = document.createElement("a")
+      link.href = url
+      link.download = "tech-vendor-contacts-template.xlsx"
+      link.click()
+      URL.revokeObjectURL(url)
+      toast.success("템플릿이 다운로드되었습니다.")
+    } catch (error) {
+      toast.error("템플릿 다운로드 중 오류가 발생했습니다.")
+      console.error("Template download error:", error)
+    }
+  }
+
+  return (
+    <div className="flex items-center gap-2">
+
+      <AddContactDialog vendorId={vendorId}/>
+
+      {/** 템플릿 다운로드 버튼 */}
+      <Button variant="outline" size="sm" className="gap-2" onClick={handleTemplateDownload}>
+        <Download className="size-4" aria-hidden="true" />
+        <span className="hidden sm:inline">템플릿</span>
+      </Button>
+
+      {/** Import 버튼 (파일 업로드) */}
+      <Button variant="outline" size="sm" className="gap-2" onClick={handleImportClick}>
+        <Upload className="size-4" aria-hidden="true" />
+        <span className="hidden sm:inline">Import</span>
+      </Button>
+      {/*
+        실제로는 숨겨진 input과 연결:
+        - accept=".xlsx,.xls" 등으로 Excel 파일만 업로드 허용
+      */}
+      <input
+        ref={fileInputRef}
+        type="file"
+        accept=".xlsx,.xls"
+        className="hidden"
+        onChange={onFileChange}
+      />
+
+      {/** Export 버튼 */}
+      <Button
+        variant="outline"
+        size="sm"
+        onClick={() =>
+          exportTableToExcel(table, {
+            filename: "tech-vendor-contacts",
+            excludeColumns: ["select", "actions"],
+          })
+        }
+        className="gap-2"
+      >
+        <Download className="size-4" aria-hidden="true" />
+        <span className="hidden sm:inline">Export</span>
+      </Button>
+    </div>
+  )
 }
\ No newline at end of file
-- 
cgit v1.2.3

