// /lib/vendor-export.ts import ExcelJS from "exceljs" import { VendorWithType } from "@/db/schema/vendors" import { exportVendorDetails } from "../service"; // 연락처 인터페이스 정의 interface VendorContact { contactName: string; contactPosition?: string | null; contactEmail: string; contactPhone?: string | null; isPrimary: boolean; } // 아이템 인터페이스 정의 interface VendorItem { itemCode: string; itemName: string; description?: string | null; createdAt?: Date | string; } // RFQ 인터페이스 정의 interface VendorRFQ { rfqNumber: string; title: string; status: string; requestDate?: Date | string | null; dueDate?: Date | string | null; description?: string | null; } // 계약 인터페이스 정의 interface VendorContract { projectCode: string; projectName: string; contractNo: string; contractName: string; status: string; paymentTerms: string; deliveryTerms: string; deliveryDate: Date | string; deliveryLocation: string; startDate?: Date | string | null; endDate?: Date | string | null; currency: string; totalAmount?: number | null; } // 서비스에서 반환하는 실제 데이터 구조 interface VendorData { id: number; vendorName: string; vendorCode: string | null; taxId: string; address: string | null; country: string | null; phone: string | null; email: string | null; website: string | null; status: string; representativeName: string | null; representativeBirth: string | null; representativeEmail: string | null; representativePhone: string | null; corporateRegistrationNumber: string | null; creditAgency: string | null; creditRating: string | null; cashFlowRating: string | null; // items: string | null; createdAt: Date; updatedAt: Date; vendorContacts: VendorContact[]; vendorItems: VendorItem[]; vendorRfqs: VendorRFQ[]; vendorContracts: VendorContract[]; } /** * 선택된 벤더의 모든 관련 정보를 통합 시트 형식으로 엑셀로 내보내는 함수 * - 기본정보 시트 * - 연락처 시트 * - 아이템 시트 * - RFQ 시트 * - 계약 시트 * 각 시트에는 식별을 위한 벤더 코드, 벤더명, 세금ID가 포함됨 */ export async function exportVendorsWithRelatedData( vendors: VendorWithType[], filename = "vendors-detailed" ): Promise { if (!vendors.length) return; // 선택된 벤더 ID 목록 const vendorIds = vendors.map(vendor => vendor.id); try { // 서버로부터 모든 관련 데이터 가져오기 const vendorsWithDetails = await exportVendorDetails(vendorIds); if (!vendorsWithDetails.length) { throw new Error("내보내기 데이터를 가져오는 중 오류가 발생했습니다."); } // 워크북 생성 const workbook = new ExcelJS.Workbook(); // 데이터 타입 확인 (서비스에서 반환하는 실제 데이터 형태) const vendorData = vendorsWithDetails as unknown as any[]; // ===== 1. 기본 정보 시트 ===== createBasicInfoSheet(workbook, vendorData); // ===== 2. 연락처 시트 ===== createContactsSheet(workbook, vendorData); // ===== 3. 아이템 시트 ===== createItemsSheet(workbook, vendorData); // ===== 4. RFQ 시트 ===== createRFQsSheet(workbook, vendorData); // ===== 5. 계약 시트 ===== createContractsSheet(workbook, vendorData); // 파일 다운로드 const buffer = await workbook.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 = `${filename}-${new Date().toISOString().split("T")[0]}.xlsx`; link.click(); URL.revokeObjectURL(url); return; } catch (error) { console.error("Export error:", error); throw error; } } // 기본 정보 시트 생성 함수 function createBasicInfoSheet( workbook: ExcelJS.Workbook, vendors: VendorData[] ): void { const basicInfoSheet = workbook.addWorksheet("기본정보"); // 기본 정보 시트 헤더 설정 basicInfoSheet.columns = [ { header: "업체코드", key: "vendorCode", width: 15 }, { header: "업체명", key: "vendorName", width: 20 }, { header: "세금ID", key: "taxId", width: 15 }, { header: "국가", key: "country", width: 10 }, { header: "상태", key: "status", width: 15 }, { header: "이메일", key: "email", width: 20 }, { header: "전화번호", key: "phone", width: 15 }, { header: "웹사이트", key: "website", width: 20 }, { header: "주소", key: "address", width: 30 }, { header: "대표자명", key: "representativeName", width: 15 }, { header: "신용등급", key: "creditRating", width: 10 }, { header: "현금흐름등급", key: "cashFlowRating", width: 10 }, { header: "생성일", key: "createdAt", width: 15 }, ]; // 헤더 스타일 설정 applyHeaderStyle(basicInfoSheet); // 벤더 데이터 추가 vendors.forEach((vendor: VendorData) => { basicInfoSheet.addRow({ vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, country: vendor.country, status: getStatusText(vendor.status), // 상태 코드를 읽기 쉬운 텍스트로 변환 email: vendor.email, phone: vendor.phone, website: vendor.website, address: vendor.address, representativeName: vendor.representativeName, creditRating: vendor.creditRating, cashFlowRating: vendor.cashFlowRating, createdAt: vendor.createdAt ? formatDate(vendor.createdAt, "KR") : "", }); }); } // 연락처 시트 생성 함수 function createContactsSheet( workbook: ExcelJS.Workbook, vendors: VendorData[] ): void { const contactsSheet = workbook.addWorksheet("연락처"); contactsSheet.columns = [ // 벤더 식별 정보 { header: "업체코드", key: "vendorCode", width: 15 }, { header: "업체명", key: "vendorName", width: 20 }, { header: "세금ID", key: "taxId", width: 15 }, // 연락처 정보 { header: "이름", key: "contactName", width: 15 }, { header: "직책", key: "contactPosition", width: 15 }, { header: "이메일", key: "contactEmail", width: 25 }, { header: "전화번호", key: "contactPhone", width: 15 }, { header: "주요 연락처", key: "isPrimary", width: 10 }, ]; // 헤더 스타일 설정 applyHeaderStyle(contactsSheet); // 벤더별 연락처 데이터 추가 vendors.forEach((vendor: VendorData) => { if (vendor.vendorContacts && vendor.vendorContacts.length > 0) { vendor.vendorContacts.forEach((contact: VendorContact) => { contactsSheet.addRow({ // 벤더 식별 정보 vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, // 연락처 정보 contactName: contact.contactName, contactPosition: contact.contactPosition || "", contactEmail: contact.contactEmail, contactPhone: contact.contactPhone || "", isPrimary: contact.isPrimary ? "예" : "아니오", }); }); } else { // 연락처가 없는 경우에도 벤더 정보만 추가 contactsSheet.addRow({ vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, contactName: "", contactPosition: "", contactEmail: "", contactPhone: "", isPrimary: "", }); } }); } // 아이템 시트 생성 함수 function createItemsSheet( workbook: ExcelJS.Workbook, vendors: VendorData[] ): void { const itemsSheet = workbook.addWorksheet("아이템"); itemsSheet.columns = [ // 벤더 식별 정보 { header: "업체코드", key: "vendorCode", width: 15 }, { header: "업체명", key: "vendorName", width: 20 }, { header: "세금ID", key: "taxId", width: 15 }, // 아이템 정보 { header: "아이템 코드", key: "itemCode", width: 15 }, { header: "아이템명", key: "itemName", width: 25 }, { header: "설명", key: "description", width: 30 }, { header: "등록일", key: "createdAt", width: 15 }, ]; // 헤더 스타일 설정 applyHeaderStyle(itemsSheet); // 벤더별 아이템 데이터 추가 vendors.forEach((vendor: VendorData) => { if (vendor.vendorItems && vendor.vendorItems.length > 0) { vendor.vendorItems.forEach((item: VendorItem) => { itemsSheet.addRow({ // 벤더 식별 정보 vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, // 아이템 정보 itemCode: item.itemCode, itemName: item.itemName, description: item.description || "", createdAt: item.createdAt ? formatDate(item.createdAt, "KR") : "", }); }); } else { // 아이템이 없는 경우에도 벤더 정보만 추가 itemsSheet.addRow({ vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, itemCode: "", itemName: "", description: "", createdAt: "", }); } }); } // RFQ 시트 생성 함수 function createRFQsSheet( workbook: ExcelJS.Workbook, vendors: VendorData[] ): void { const rfqsSheet = workbook.addWorksheet("RFQ"); rfqsSheet.columns = [ // 벤더 식별 정보 { header: "업체코드", key: "vendorCode", width: 15 }, { header: "업체명", key: "vendorName", width: 20 }, { header: "세금ID", key: "taxId", width: 15 }, // RFQ 정보 { header: "RFQ 번호", key: "rfqNumber", width: 15 }, { header: "제목", key: "title", width: 25 }, { header: "상태", key: "status", width: 15 }, { header: "요청일", key: "requestDate", width: 15 }, { header: "마감일", key: "dueDate", width: 15 }, { header: "설명", key: "description", width: 30 }, ]; // 헤더 스타일 설정 applyHeaderStyle(rfqsSheet); // 벤더별 RFQ 데이터 추가 vendors.forEach((vendor: VendorData) => { if (vendor.vendorRfqs && vendor.vendorRfqs.length > 0) { vendor.vendorRfqs.forEach((rfq: VendorRFQ) => { rfqsSheet.addRow({ // 벤더 식별 정보 vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, // RFQ 정보 rfqNumber: rfq.rfqNumber, title: rfq.title, status: rfq.status, requestDate: rfq.requestDate ? formatDate(rfq.requestDate, "KR") : "", dueDate: rfq.dueDate ? formatDate(rfq.dueDate, "KR") : "", description: rfq.description || "", }); }); } else { // RFQ가 없는 경우에도 벤더 정보만 추가 rfqsSheet.addRow({ vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, rfqNumber: "", title: "", status: "", requestDate: "", dueDate: "", description: "", }); } }); } // 계약 시트 생성 함수 function createContractsSheet( workbook: ExcelJS.Workbook, vendors: VendorData[] ): void { const contractsSheet = workbook.addWorksheet("계약"); contractsSheet.columns = [ // 벤더 식별 정보 { header: "업체코드", key: "vendorCode", width: 15 }, { header: "업체명", key: "vendorName", width: 20 }, { header: "세금ID", key: "taxId", width: 15 }, // 계약 정보 { header: "프로젝트 코드", key: "projectCode", width: 15 }, { header: "프로젝트명", key: "projectName", width: 20 }, { header: "계약 번호", key: "contractNo", width: 15 }, { header: "계약명", key: "contractName", width: 25 }, { header: "상태", key: "status", width: 15 }, { header: "지급 조건", key: "paymentTerms", width: 15 }, { header: "납품 조건", key: "deliveryTerms", width: 15 }, { header: "납품 일자", key: "deliveryDate", width: 15 }, { header: "납품 위치", key: "deliveryLocation", width: 20 }, { header: "계약 시작일", key: "startDate", width: 15 }, { header: "계약 종료일", key: "endDate", width: 15 }, { header: "통화", key: "currency", width: 10 }, { header: "총액", key: "totalAmount", width: 15 }, ]; // 헤더 스타일 설정 applyHeaderStyle(contractsSheet); // 벤더별 계약 데이터 추가 vendors.forEach((vendor: VendorData) => { if (vendor.vendorContracts && vendor.vendorContracts.length > 0) { vendor.vendorContracts.forEach((contract: VendorContract) => { contractsSheet.addRow({ // 벤더 식별 정보 vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, // 계약 정보 projectCode: contract.projectCode, projectName: contract.projectName, contractNo: contract.contractNo, contractName: contract.contractName, status: contract.status, paymentTerms: contract.paymentTerms, deliveryTerms: contract.deliveryTerms, deliveryDate: contract.deliveryDate ? formatDate(contract.deliveryDate, "KR") : "", deliveryLocation: contract.deliveryLocation, startDate: contract.startDate ? formatDate(contract.startDate, "KR") : "", endDate: contract.endDate ? formatDate(contract.endDate, "KR") : "", currency: contract.currency, totalAmount: contract.totalAmount ? formatAmount(contract.totalAmount) : "", }); }); } else { // 계약이 없는 경우에도 벤더 정보만 추가 contractsSheet.addRow({ vendorCode: vendor.vendorCode || "", vendorName: vendor.vendorName, taxId: vendor.taxId, projectCode: "", projectName: "", contractNo: "", contractName: "", status: "", paymentTerms: "", deliveryTerms: "", deliveryDate: "", deliveryLocation: "", startDate: "", endDate: "", currency: "", totalAmount: "", }); } }); } // 헤더 스타일 적용 함수 function applyHeaderStyle(sheet: ExcelJS.Worksheet): void { const headerRow = sheet.getRow(1); headerRow.font = { bold: true }; headerRow.alignment = { horizontal: "center" }; headerRow.eachCell((cell: ExcelJS.Cell) => { cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFCCCCCC" }, }; }); } // 날짜 포맷 함수 function formatDate(date: Date | string): string { if (!date) return ""; if (typeof date === 'string') { date = new Date(date); } return date.toISOString().split('T')[0]; } // 금액 포맷 함수 function formatAmount(amount: number): string { return amount.toLocaleString(); } // 상태 코드를 읽기 쉬운 텍스트로 변환하는 함수 function getStatusText(status: string): string { const statusMap: Record = { "PENDING_REVIEW": "검토 대기중", "IN_REVIEW": "검토 중", "REJECTED": "거부됨", "IN_PQ": "PQ 진행 중", "PQ_SUBMITTED": "PQ 제출됨", "PQ_FAILED": "PQ 실패", "PQ_APPROVED": "PQ 승인됨", "APPROVED": "승인됨", "READY_TO_SEND": "발송 준비됨", "ACTIVE": "활성", "INACTIVE": "비활성", "BLACKLISTED": "거래 금지" }; return statusMap[status] || status; }