import { type Table } from "@tanstack/react-table" import ExcelJS from "exceljs" import { PartnersBiddingListItem } from '../detail/service' import { biddingStatusLabels, contractTypeLabels, } from "@/db/schema" import { formatDate } from "@/lib/utils" /** * Partners 입찰 목록을 Excel로 내보내기 * - 계약구분, 진행상태는 라벨(명칭)로 변환 * - 입찰기간은 submissionStartDate, submissionEndDate 기준 * - 날짜는 적절한 형식으로 변환 */ export async function exportPartnersBiddingsToExcel( table: Table, { filename = "협력업체입찰목록", onlySelected = false, }: { filename?: string onlySelected?: boolean } = {} ): Promise { // 테이블에서 실제 사용 중인 leaf columns 가져오기 const allColumns = table.getAllLeafColumns() // select, actions, attachments 컬럼 제외 const columns = allColumns.filter( (col) => !["select", "actions", "attachments"].includes(col.id) ) // 헤더 매핑 (컬럼 id -> Excel 헤더명) const headerMap: Record = { biddingNumber: "입찰 No.", status: "입찰상태", isUrgent: "긴급여부", title: "입찰명", isAttendingMeeting: "사양설명회", isBiddingParticipated: "입찰 참여의사", biddingSubmissionStatus: "입찰 제출여부", contractType: "계약구분", submissionStartDate: "입찰기간", contractStartDate: "계약기간", bidPicName: "입찰담당자", supplyPicName: "조달담당자", updatedAt: "최종수정일", } // 헤더 행 생성 const headerRow = columns.map((col) => { return headerMap[col.id] || col.id }) // 데이터 행 생성 const rowModel = onlySelected ? table.getFilteredSelectedRowModel() : table.getRowModel() const dataRows = rowModel.rows.map((row) => { const original = row.original return columns.map((col) => { const colId = col.id let value: any // 특별 처리 필요한 컬럼들 switch (colId) { case "contractType": // 계약구분: 라벨로 변환 value = contractTypeLabels[original.contractType as keyof typeof contractTypeLabels] || original.contractType break case "status": // 입찰상태: 라벨로 변환 value = biddingStatusLabels[original.status as keyof typeof biddingStatusLabels] || original.status break case "isUrgent": // 긴급여부: Yes/No value = original.isUrgent ? "긴급" : "일반" break case "isAttendingMeeting": // 사양설명회: 참석/불참/미결정 if (original.isAttendingMeeting === null) { value = "해당없음" } else { value = original.isAttendingMeeting ? "참석" : "불참" } break case "isBiddingParticipated": // 입찰 참여의사: 참여/불참/미결정 if (original.isBiddingParticipated === null) { value = "미결정" } else { value = original.isBiddingParticipated ? "참여" : "불참" } break case "biddingSubmissionStatus": // 입찰 제출여부: 최종제출/제출/미제출 const finalQuoteAmount = original.finalQuoteAmount const isFinalSubmission = original.isFinalSubmission if (!finalQuoteAmount) { value = "미제출" } else if (isFinalSubmission) { value = "최종제출" } else { value = "제출" } break case "submissionStartDate": // 입찰기간: submissionStartDate, submissionEndDate 기준 const startDate = original.submissionStartDate const endDate = original.submissionEndDate if (!startDate || !endDate) { value = "-" } else { const startObj = new Date(startDate) const endObj = new Date(endDate) // KST 변환 (UTC+9) const formatKst = (d: Date) => { const kstDate = new Date(d.getTime() + 9 * 60 * 60 * 1000) return kstDate.toISOString().slice(0, 16).replace('T', ' ') } value = `${formatKst(startObj)} ~ ${formatKst(endObj)}` } break // case "preQuoteDeadline": // // 사전견적 마감일: 날짜 형식 // if (!original.preQuoteDeadline) { // value = "-" // } else { // const deadline = new Date(original.preQuoteDeadline) // value = deadline.toISOString().slice(0, 16).replace('T', ' ') // } // break case "contractStartDate": // 계약기간: contractStartDate, contractEndDate 기준 const contractStart = original.contractStartDate const contractEnd = original.contractEndDate if (!contractStart || !contractEnd) { value = "-" } else { const startObj = new Date(contractStart) const endObj = new Date(contractEnd) value = `${formatDate(startObj, "KR")} ~ ${formatDate(endObj, "KR")}` } break case "bidPicName": // 입찰담당자: bidPicName value = original.bidPicName || "-" break case "supplyPicName": // 조달담당자: supplyPicName value = original.supplyPicName || "-" break case "updatedAt": // 최종수정일: 날짜 시간 형식 if (original.updatedAt) { const updated = new Date(original.updatedAt) value = updated.toISOString().slice(0, 16).replace('T', ' ') } else { value = "-" } break case "biddingNumber": // 입찰번호: 원입찰번호 포함 const biddingNumber = original.biddingNumber const originalBiddingNumber = original.originalBiddingNumber if (originalBiddingNumber) { value = `${biddingNumber} (원: ${originalBiddingNumber})` } else { value = biddingNumber } break default: // 기본값: row.getValue 사용 value = row.getValue(colId) // null/undefined 처리 if (value == null) { value = "" } // 객체인 경우 JSON 문자열로 변환 if (typeof value === "object") { value = JSON.stringify(value) } break } return value }) }) // 최종 sheetData const sheetData = [headerRow, ...dataRows] // ExcelJS로 파일 생성 및 다운로드 await createAndDownloadExcel(sheetData, columns.length, filename) } /** * Excel 파일 생성 및 다운로드 */ async function createAndDownloadExcel( sheetData: any[][], columnCount: number, filename: string ): Promise { // ExcelJS 워크북/시트 생성 const workbook = new ExcelJS.Workbook() const worksheet = workbook.addWorksheet("Sheet1") // 칼럼별 최대 길이 추적 const maxColumnLengths = Array(columnCount).fill(0) sheetData.forEach((row) => { row.forEach((cellValue, colIdx) => { const cellText = cellValue?.toString() ?? "" if (cellText.length > maxColumnLengths[colIdx]) { maxColumnLengths[colIdx] = cellText.length } }) }) // 시트에 데이터 추가 + 헤더 스타일 sheetData.forEach((arr, idx) => { const row = worksheet.addRow(arr) // 헤더 스타일 적용 (첫 번째 행) if (idx === 0) { row.font = { bold: true } row.alignment = { horizontal: "center" } row.eachCell((cell) => { cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFCCCCCC" }, } }) } }) // 칼럼 너비 자동 조정 maxColumnLengths.forEach((len, idx) => { // 최소 너비 10, +2 여백 worksheet.getColumn(idx + 1).width = Math.max(len + 2, 10) }) // 최종 파일 다운로드 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}.xlsx` link.click() URL.revokeObjectURL(url) }