summaryrefslogtreecommitdiff
path: root/lib/tech-vendors/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tech-vendors/service.ts')
-rw-r--r--lib/tech-vendors/service.ts757
1 files changed, 755 insertions, 2 deletions
diff --git a/lib/tech-vendors/service.ts b/lib/tech-vendors/service.ts
index c5926e8e..72f8632d 100644
--- a/lib/tech-vendors/service.ts
+++ b/lib/tech-vendors/service.ts
@@ -39,6 +39,7 @@ import path from "path";
import { sql } from "drizzle-orm";
import { decryptWithServerAction } from "@/components/drm/drmUtils";
import { deleteFile, saveDRMFile } from "../file-stroage";
+import { techSalesContactPossibleItems } from "@/db/schema";
/* -----------------------------------------------------
1) 조회 관련
@@ -2729,7 +2730,7 @@ export async function importTechVendorContacts(
// 3. 연락처 생성
await db.insert(techVendorContacts).values({
vendorId: vendor.id,
- contactName: row.contactName,
+ contactName: row.contactName || null,
contactPosition: row.contactPosition || null,
contactEmail: row.contactEmail,
contactPhone: row.contactPhone || null,
@@ -2765,7 +2766,7 @@ export async function generateContactImportTemplate(): Promise<Blob> {
// 헤더 설정
worksheet.columns = [
{ header: "벤더대표이메일*", key: "vendorEmail", width: 25 },
- { header: "담당자명*", key: "contactName", width: 20 },
+ { header: "담당자명", key: "contactName", width: 20 },
{ header: "직책", key: "contactPosition", width: 15 },
{ header: "담당자이메일*", key: "contactEmail", width: 25 },
{ header: "담당자연락처", key: "contactPhone", width: 15 },
@@ -3363,4 +3364,756 @@ export async function generatePossibleItemsErrorExcel(errors: PossibleItemErrorD
return new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
})
+}
+
+/* -----------------------------------------------------
+ Distinct Contact Import 관련 함수들
+----------------------------------------------------- */
+
+export interface DistinctContactImportData {
+ vendorName: string
+ email: string
+}
+
+export interface DistinctContactImportResult {
+ success: boolean
+ totalRows: number
+ successCount: number
+ failedRows: Array<{
+ row: number
+ error: string
+ vendorName: string
+ email: string
+ }>
+}
+
+/**
+ * Distinct Contact Import용 템플릿 생성 (벤더명, 이메일만)
+ */
+export async function generateDistinctContactImportTemplate(): Promise<Blob> {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("벤더담당자_템플릿")
+
+ // 헤더 설정
+ worksheet.columns = [
+ { header: "벤더이름*", key: "vendorName", width: 25 },
+ { header: "이메일*", key: "email", width: 30 },
+ ]
+
+ // 헤더 스타일 설정
+ const headerRow = worksheet.getRow(1)
+ headerRow.font = { bold: true }
+ headerRow.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFE0E0E0" },
+ }
+
+ // 예시 데이터 추가
+ worksheet.addRow({
+ vendorName: "ABB",
+ email: "dong-rak.cho@kr.abb.com",
+ })
+
+ worksheet.addRow({
+ vendorName: "ABB",
+ email: "woo-jin.joo@kr.abb.com",
+ })
+
+ worksheet.addRow({
+ vendorName: "삼성중공업",
+ email: "contact@samsung.com",
+ })
+
+ // 설명 시트 추가
+ const infoSheet = workbook.addWorksheet("설명")
+ infoSheet.getColumn(1).width = 50
+ infoSheet.getColumn(2).width = 100
+
+ infoSheet.addRow(["Distinct Contact Import 사용 방법"])
+ infoSheet.addRow(["1. 벤더이름", "벤더의 이름 (필수)"])
+ infoSheet.addRow(["2. 이메일", "담당자 이메일 주소 (필수)"])
+ infoSheet.addRow([])
+ infoSheet.addRow(["기능 설명"])
+ infoSheet.addRow(["• 벤더이름과 이메일을 입력하면 자동으로 벤더를 찾습니다"])
+ infoSheet.addRow(["• 벤더 대표 이메일과 다른 이메일인 경우에만 담당자로 추가됩니다"])
+ infoSheet.addRow(["• 벤더 대표 이메일과 같은 이메일인 경우는 건너뜁니다"])
+ infoSheet.addRow(["• 이미 등록된 담당자 이메일은 중복 등록되지 않습니다"])
+ infoSheet.addRow([])
+ infoSheet.addRow(["예시"])
+ infoSheet.addRow(["ABB 벤더의 대표 이메일이 dong-rak.cho@kr.abb.com 인 경우:"])
+ infoSheet.addRow([" - dong-rak.cho@kr.abb.com 입력 시: 건너뜀 (대표 이메일과 동일)"])
+ infoSheet.addRow([" - woo-jin.joo@kr.abb.com 입력 시: 담당자로 추가됨"])
+
+ const buffer = await workbook.xlsx.writeBuffer()
+ return new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ })
+}
+
+/**
+ * Distinct Contact Import용 Excel 파일 파싱
+ */
+export async function parseDistinctContactImportFile(file: File): Promise<DistinctContactImportData[]> {
+ const arrayBuffer = await file.arrayBuffer()
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(arrayBuffer)
+
+ const worksheet = workbook.worksheets[0]
+ if (!worksheet) {
+ throw new Error("Excel 파일에 워크시트가 없습니다.")
+ }
+
+ const data: DistinctContactImportData[] = []
+
+ worksheet.eachRow((row, index) => {
+ // 헤더 행 건너뛰기 (1행)
+ if (index === 1) return
+
+ const values = row.values as (string | null)[]
+ if (!values || values.length < 2) return
+
+ const vendorName = values[1]?.toString().trim()
+ const email = values[2]?.toString().trim()
+
+ // 필수 필드 검증
+ if (!vendorName || !email) {
+ return
+ }
+
+ data.push({
+ vendorName,
+ email,
+ })
+ })
+
+ return data
+}
+
+/**
+ * 벤더명으로 벤더 찾기 함수 (대소문자 구분 없이)
+ */
+async function findVendorByName(vendorName: string) {
+ const vendor = await db
+ .select({
+ id: techVendors.id,
+ vendorName: techVendors.vendorName,
+ email: techVendors.email,
+ })
+ .from(techVendors)
+ .where(ilike(techVendors.vendorName, `%${vendorName}%`))
+ .limit(1)
+
+ return vendor[0] || null
+}
+
+/**
+ * 벤더의 기본 담당자(대표 이메일) 조회
+ */
+async function getVendorPrimaryContact(vendorId: number) {
+ const contact = await db
+ .select({
+ contactName: techVendorContacts.contactName,
+ contactEmail: techVendorContacts.contactEmail,
+ })
+ .from(techVendorContacts)
+ .where(
+ and(
+ eq(techVendorContacts.vendorId, vendorId),
+ eq(techVendorContacts.contactEmail, techVendors.email) // 기본 담당자 체크
+ )
+ )
+ .innerJoin(techVendors, eq(techVendorContacts.vendorId, techVendors.id))
+ .limit(1)
+
+ return contact[0] || null
+}
+
+/**
+ * Distinct Contact Import 메인 함수
+ */
+export async function importTechVendorDistinctContacts(
+ data: DistinctContactImportData[]
+): Promise<DistinctContactImportResult> {
+ const result: DistinctContactImportResult = {
+ success: true,
+ totalRows: data.length,
+ successCount: 0,
+ failedRows: [],
+ }
+
+ for (let i = 0; i < data.length; i++) {
+ const row = data[i]
+ const rowNumber = i + 1
+
+ try {
+ // 1. 벤더명으로 벤더 찾기
+ if (!row.vendorName || !row.vendorName.trim()) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "벤더이름은 필수입니다.",
+ vendorName: row.vendorName,
+ email: row.email,
+ })
+ continue
+ }
+
+ const vendor = await findVendorByName(row.vendorName.trim())
+ if (!vendor) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: `벤더이름 '${row.vendorName}'을(를) 찾을 수 없습니다.`,
+ vendorName: row.vendorName,
+ email: row.email,
+ })
+ continue
+ }
+
+ // 2. 이메일 검증
+ if (!row.email || !row.email.trim()) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "이메일은 필수입니다.",
+ vendorName: row.vendorName,
+ email: row.email,
+ })
+ continue
+ }
+
+ // 이메일 형식 검증 (기본적인 검증)
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ if (!emailRegex.test(row.email.trim())) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "올바른 이메일 형식이 아닙니다.",
+ vendorName: row.vendorName,
+ email: row.email,
+ })
+ continue
+ }
+
+ const email = row.email.trim()
+
+ // 3. 벤더 대표 이메일과 비교
+ if (email === vendor.email) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "벤더 대표 이메일과 동일하여 건너뜁니다.",
+ vendorName: row.vendorName,
+ email: row.email,
+ })
+ continue
+ }
+
+ // 4. 기존 담당자 중복 체크
+ const existingContact = await db
+ .select()
+ .from(techVendorContacts)
+ .where(
+ and(
+ eq(techVendorContacts.vendorId, vendor.id),
+ eq(techVendorContacts.contactEmail, email)
+ )
+ )
+ .limit(1)
+
+ if (existingContact.length > 0) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "이미 등록된 담당자 이메일입니다.",
+ vendorName: row.vendorName,
+ email: row.email,
+ })
+ continue
+ }
+
+ // 5. 기본 담당자 확인 (대표 이메일이 기본 담당자로 등록되어 있는지)
+ const primaryContact = await getVendorPrimaryContact(vendor.id)
+
+ // 6. 담당자 추가
+ await db.insert(techVendorContacts).values({
+ vendorId: vendor.id,
+ contactName: `${row.vendorName} 담당자`, // 벤더명 + 담당자
+ contactPosition: null,
+ contactEmail: email,
+ contactPhone: null,
+ contactCountry: null,
+ isPrimary: false,
+ })
+
+ result.successCount++
+ } catch (error) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: error instanceof Error ? error.message : "알 수 없는 오류",
+ vendorName: row.vendorName,
+ email: row.email,
+ })
+ }
+ }
+
+ // 캐시 무효화
+ revalidateTag("tech-vendor-contacts")
+
+ return result
+}
+
+/* -----------------------------------------------------
+ Contact Possible Items Import 관련 함수들
+----------------------------------------------------- */
+
+export interface ContactPossibleItemImportData {
+ contactEmail: string
+ itemCodes: string[] // 쉼표로 분리된 아이템코드 배열
+}
+
+export interface ContactPossibleItemImportResult {
+ success: boolean
+ totalRows: number
+ successCount: number
+ failedRows: Array<{
+ row: number
+ error: string
+ contactEmail: string
+ itemCode: string
+ }>
+}
+
+export interface ContactPossibleItemErrorData {
+ contactEmail: string
+ itemCode: string
+ error: string
+}
+
+/**
+ * Contact Possible Items Import용 템플릿 생성 (이메일, 아이템코드)
+ */
+export async function generateContactPossibleItemsImportTemplate(): Promise<Blob> {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("담당자별_아이템매핑_템플릿")
+
+ // 헤더 설정
+ worksheet.columns = [
+ { header: "담당자이메일*", key: "contactEmail", width: 30 },
+ { header: "아이템코드*", key: "itemCode", width: 20 },
+ ]
+
+ // 헤더 스타일 설정
+ const headerRow = worksheet.getRow(1)
+ headerRow.font = { bold: true }
+ headerRow.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFE0E0E0" },
+ }
+
+ // 예시 데이터 추가
+ worksheet.addRow({
+ contactEmail: "dong-rak.cho@kr.abb.com",
+ itemCode: "ITEM001,ITEM002",
+ })
+
+ worksheet.addRow({
+ contactEmail: "woo-jin.joo@kr.abb.com",
+ itemCode: "TOP001,TOP002,TOP003",
+ })
+
+ worksheet.addRow({
+ contactEmail: "contact@samsung.com",
+ itemCode: "HULL001",
+ })
+
+ worksheet.addRow({
+ contactEmail: "test@example.com",
+ itemCode: "BF8101,BF6101,BF8401,BF8201",
+ })
+
+ // 설명 시트 추가
+ const infoSheet = workbook.addWorksheet("설명")
+ infoSheet.getColumn(1).width = 50
+ infoSheet.getColumn(2).width = 100
+
+ infoSheet.addRow(["Contact Possible Items Import 사용 방법"])
+ infoSheet.addRow(["1. 담당자이메일", "등록된 담당자의 이메일 주소 (필수)"])
+ infoSheet.addRow(["2. 아이템코드", "기술영업 아이템 코드 (필수)"])
+ infoSheet.addRow([])
+ infoSheet.addRow(["기능 설명"])
+ infoSheet.addRow(["• 담당자 이메일로 등록된 담당자를 찾습니다"])
+ infoSheet.addRow(["• 담당자가 속한 벤더의 타입을 확인합니다"])
+ infoSheet.addRow(["• 벤더 타입별로 해당 아이템 테이블에서 아이템코드를 검색합니다"])
+ infoSheet.addRow(["• 찾은 아이템들을 벤더의 possible items에 추가합니다"])
+ infoSheet.addRow(["• 추가된 possible item과 담당자를 연결합니다"])
+ infoSheet.addRow([])
+ infoSheet.addRow(["예시"])
+ infoSheet.addRow(["ABB 벤더의 담당자 dong-rak.cho@kr.abb.com이 ITEM001,ITEM002를 담당하는 경우:"])
+ infoSheet.addRow([" - 해당 담당자의 벤더 타입을 확인 (조선, 해양TOP 등)"])
+ infoSheet.addRow([" - 해당 타입의 아이템 테이블에서 ITEM001과 ITEM002를 각각 검색"])
+ infoSheet.addRow([" - 찾은 아이템들을 벤더의 possible items에 추가 (중복 제외)"])
+ infoSheet.addRow([" - 추가된 아이템들과 담당자를 연결"])
+ infoSheet.addRow([])
+ infoSheet.addRow(["다중 아이템코드 지원"])
+ infoSheet.addRow(["• 아이템코드 필드에 쉼표(,)로 구분하여 여러 아이템코드를 입력할 수 있습니다"])
+ infoSheet.addRow(["• 예: ITEM001,ITEM002,BF8101,BF6101"])
+ infoSheet.addRow(["• 각 아이템코드는 개별적으로 처리되며, 실패한 코드만 에러로 기록됩니다"])
+
+ const buffer = await workbook.xlsx.writeBuffer()
+ return new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ })
+}
+
+/**
+ * Contact Possible Items Import용 Excel 파일 파싱
+ */
+export async function parseContactPossibleItemsImportFile(file: File): Promise<ContactPossibleItemImportData[]> {
+ const arrayBuffer = await file.arrayBuffer()
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(arrayBuffer)
+
+ const worksheet = workbook.worksheets[0]
+ if (!worksheet) {
+ throw new Error("Excel 파일에 워크시트가 없습니다.")
+ }
+
+ const data: ContactPossibleItemImportData[] = []
+
+ worksheet.eachRow((row, index) => {
+ // 헤더 행 건너뛰기 (1행)
+ if (index === 1) return
+
+ const values = row.values as (string | null)[]
+ if (!values || values.length < 2) return
+
+ const contactEmail = values[1]?.toString().trim()
+ const itemCodeStr = values[2]?.toString().trim()
+
+ // 필수 필드 검증
+ if (!contactEmail || !itemCodeStr) {
+ return
+ }
+
+ // 아이템코드를 쉼표로 분리하고 공백 제거
+ const itemCodes = itemCodeStr.split(',').map(code => code.trim()).filter(code => code.length > 0)
+
+ if (itemCodes.length === 0) {
+ return
+ }
+
+ data.push({
+ contactEmail,
+ itemCodes,
+ })
+ })
+
+ return data
+}
+
+/**
+ * 이메일로 담당자 찾기 함수
+ */
+async function findContactByEmail(email: string) {
+ const contact = await db
+ .select({
+ id: techVendorContacts.id,
+ contactName: techVendorContacts.contactName,
+ contactEmail: techVendorContacts.contactEmail,
+ vendorId: techVendorContacts.vendorId,
+ })
+ .from(techVendorContacts)
+ .where(eq(techVendorContacts.contactEmail, email))
+ .limit(1)
+
+ return contact[0] || null
+}
+
+/**
+ * 벤더의 타입별 아이템 찾기 함수
+ */
+async function findItemsByVendorType(vendorId: number, itemCode: string) {
+ // 벤더 정보 조회로 타입 확인
+ const vendor = await db.query.techVendors.findFirst({
+ where: eq(techVendors.id, vendorId),
+ columns: {
+ techVendorType: true
+ }
+ })
+
+ if (!vendor) {
+ throw new Error("벤더를 찾을 수 없습니다.")
+ }
+
+ const foundItems = []
+
+ // 벤더 타입 파싱 - 콤마로 구분된 문자열을 배열로 변환
+ let vendorTypes: string[] = []
+ if (typeof vendor.techVendorType === 'string') {
+ vendorTypes = vendor.techVendorType.split(',').map(type => type.trim()).filter(type => type.length > 0)
+ } else {
+ vendorTypes = [vendor.techVendorType]
+ }
+
+ // 각 벤더 타입별로 아이템 검색
+ for (const vendorType of vendorTypes) {
+ switch (vendorType) {
+ case "조선":
+ const shipItems = await db
+ .select({
+ id: itemShipbuilding.id,
+ itemCode: itemShipbuilding.itemCode,
+ })
+ .from(itemShipbuilding)
+ .where(eq(itemShipbuilding.itemCode, itemCode))
+
+ foundItems.push(...shipItems.map(item => ({ ...item, itemType: "SHIP" })))
+ break
+
+ case "해양TOP":
+ const topItems = await db
+ .select({
+ id: itemOffshoreTop.id,
+ itemCode: itemOffshoreTop.itemCode,
+ })
+ .from(itemOffshoreTop)
+ .where(eq(itemOffshoreTop.itemCode, itemCode))
+
+ foundItems.push(...topItems.map(item => ({ ...item, itemType: "TOP" })))
+ break
+
+ case "해양HULL":
+ const hullItems = await db
+ .select({
+ id: itemOffshoreHull.id,
+ itemCode: itemOffshoreHull.itemCode,
+ })
+ .from(itemOffshoreHull)
+ .where(eq(itemOffshoreHull.itemCode, itemCode))
+
+ foundItems.push(...hullItems.map(item => ({ ...item, itemType: "HULL" })))
+ break
+ }
+ }
+
+ return foundItems
+}
+
+/**
+ * 벤더의 possible items에 아이템 추가 함수 (중복 체크 포함)
+ */
+async function addVendorPossibleItems(vendorId: number, items: Array<{id: number, itemType: string}>) {
+ const addedItems = []
+
+ for (const item of items) {
+ // 중복 체크
+ let existingItem = null
+
+ if (item.itemType === "SHIP") {
+ existingItem = await db.query.techVendorPossibleItems.findFirst({
+ where: and(
+ eq(techVendorPossibleItems.vendorId, vendorId),
+ eq(techVendorPossibleItems.shipbuildingItemId, item.id)
+ )
+ })
+ } else if (item.itemType === "TOP") {
+ existingItem = await db.query.techVendorPossibleItems.findFirst({
+ where: and(
+ eq(techVendorPossibleItems.vendorId, vendorId),
+ eq(techVendorPossibleItems.offshoreTopItemId, item.id)
+ )
+ })
+ } else if (item.itemType === "HULL") {
+ existingItem = await db.query.techVendorPossibleItems.findFirst({
+ where: and(
+ eq(techVendorPossibleItems.vendorId, vendorId),
+ eq(techVendorPossibleItems.offshoreHullItemId, item.id)
+ )
+ })
+ }
+
+ if (!existingItem) {
+ // 새 아이템 추가
+ const insertData: {
+ vendorId: number
+ shipbuildingItemId?: number
+ offshoreTopItemId?: number
+ offshoreHullItemId?: number
+ } = {
+ vendorId,
+ }
+
+ if (item.itemType === "SHIP") {
+ insertData.shipbuildingItemId = item.id
+ } else if (item.itemType === "TOP") {
+ insertData.offshoreTopItemId = item.id
+ } else if (item.itemType === "HULL") {
+ insertData.offshoreHullItemId = item.id
+ }
+
+ const [newItem] = await db
+ .insert(techVendorPossibleItems)
+ .values(insertData)
+ .returning()
+
+ addedItems.push(newItem)
+ }
+ }
+
+ return addedItems
+}
+
+/**
+ * Contact Possible Items Import 메인 함수
+ */
+export async function importContactPossibleItemsFromExcel(
+ data: ContactPossibleItemImportData[]
+): Promise<ContactPossibleItemImportResult> {
+ const result: ContactPossibleItemImportResult = {
+ success: true,
+ totalRows: data.length,
+ successCount: 0,
+ failedRows: [],
+ }
+
+ for (let i = 0; i < data.length; i++) {
+ const row = data[i]
+ const rowNumber = i + 1
+
+ try {
+ // 1. 담당자 이메일로 담당자 찾기
+ if (!row.contactEmail || !row.contactEmail.trim()) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "담당자 이메일은 필수입니다.",
+ contactEmail: row.contactEmail,
+ itemCode: row.itemCode,
+ })
+ continue
+ }
+
+ const contact = await findContactByEmail(row.contactEmail.trim())
+ if (!contact) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: `담당자 이메일 '${row.contactEmail}'을(를) 찾을 수 없습니다.`,
+ contactEmail: row.contactEmail,
+ itemCode: row.itemCode,
+ })
+ continue
+ }
+
+ // 2. 아이템 코드 배열 검증
+ if (!row.itemCodes || row.itemCodes.length === 0) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: "아이템 코드는 필수입니다.",
+ contactEmail: row.contactEmail,
+ itemCode: row.itemCodes.join(', '),
+ })
+ continue
+ }
+
+ // 3. 각 아이템코드에 대해 처리
+ for (const itemCode of row.itemCodes) {
+ try {
+ // 벤더 타입별로 아이템 검색
+ const foundItems = await findItemsByVendorType(contact.vendorId, itemCode)
+
+ if (foundItems.length === 0) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: `아이템 코드 '${itemCode}'을(를) 벤더 타입에서 찾을 수 없습니다.`,
+ contactEmail: row.contactEmail,
+ itemCode: itemCode,
+ })
+ continue
+ }
+
+ // 4. 벤더의 possible items에 아이템 추가 (중복 체크 포함)
+ const addedItems = await addVendorPossibleItems(contact.vendorId, foundItems)
+
+ if (addedItems.length === 0) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: `아이템 코드 '${itemCode}'은(는) 이미 등록되어 있습니다.`,
+ contactEmail: row.contactEmail,
+ itemCode: itemCode,
+ })
+ continue
+ }
+
+ // 5. 추가된 아이템들을 담당자와 연결
+ for (const addedItem of addedItems) {
+ await db.insert(techSalesContactPossibleItems).values({
+ contactId: contact.id,
+ vendorPossibleItemId: addedItem.id,
+ })
+ }
+
+ result.successCount += addedItems.length
+ } catch (error) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: `아이템 코드 '${itemCode}' 처리 중 오류: ${error instanceof Error ? error.message : '알 수 없는 오류'}`,
+ contactEmail: row.contactEmail,
+ itemCode: itemCode,
+ })
+ }
+ }
+ console.log(result)
+ } catch (error) {
+ result.failedRows.push({
+ row: rowNumber,
+ error: error instanceof Error ? error.message : "알 수 없는 오류",
+ contactEmail: row.contactEmail,
+ itemCode: row.itemCodes.join(', '),
+ })
+ }
+ }
+
+ // 캐시 무효화
+ revalidateTag("tech-vendor-possible-items")
+ revalidateTag("tech-sales-contact-possible-items")
+
+ return result
+}
+
+/**
+ * Contact Possible Items Import용 에러 엑셀 파일 생성
+ */
+export async function generateContactPossibleItemsErrorExcel(errors: Array<{
+ contactEmail: string
+ itemCode: string
+ error: string
+}>): Promise<Blob> {
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("Import_에러_내역")
+
+ // 헤더 설정
+ worksheet.columns = [
+ { header: "담당자이메일", key: "contactEmail", width: 30 },
+ { header: "아이템코드", key: "itemCode", width: 20 },
+ { header: "에러내용", key: "error", width: 80, style: { alignment: { wrapText: true } } },
+ ]
+
+ // 헤더 스타일 설정
+ const headerRow = worksheet.getRow(1)
+ headerRow.font = { bold: true }
+ headerRow.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFFFCCCC" },
+ }
+
+ // 에러 데이터 추가
+ errors.forEach(error => {
+ worksheet.addRow({
+ contactEmail: error.contactEmail,
+ itemCode: error.itemCode,
+ error: error.error,
+ })
+ })
+
+ const buffer = await workbook.xlsx.writeBuffer()
+ return new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ })
} \ No newline at end of file