1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
|
import { type Table } from "@tanstack/react-table"
import ExcelJS from "exceljs"
import { BiddingListItem } from "@/db/schema"
import {
biddingStatusLabels,
contractTypeLabels,
biddingTypeLabels,
} from "@/db/schema"
import { formatDate } from "@/lib/utils"
// BiddingListItem 확장 타입 (manager 정보 포함)
type BiddingListItemWithManagerCode = BiddingListItem & {
bidPicName?: string | null
supplyPicName?: string | null
}
/**
* 입찰 목록을 Excel로 내보내기
* - 계약구분, 진행상태, 입찰유형은 라벨(명칭)로 변환
* - 입찰서 제출기간은 submissionStartDate, submissionEndDate 기준
* - 등록일시는 년, 월, 일 형식
*/
export async function exportBiddingsToExcel(
table: Table<BiddingListItemWithManagerCode>,
{
filename = "입찰목록",
onlySelected = false,
}: {
filename?: string
onlySelected?: boolean
} = {}
): Promise<void> {
// 테이블에서 실제 사용 중인 leaf columns 가져오기
const allColumns = table.getAllLeafColumns()
// select, actions 컬럼 제외
const columns = allColumns.filter(
(col) => !["select", "actions"].includes(col.id)
)
// 헤더 행 생성 (excelHeader 사용)
const headerRow = columns.map((col) => {
const excelHeader = (col.columnDef.meta as any)?.excelHeader
return typeof excelHeader === "string" ? excelHeader : 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 "biddingType":
// 입찰유형: 라벨로 변환
value = biddingTypeLabels[original.biddingType as keyof typeof biddingTypeLabels] || original.biddingType
break
case "submissionPeriod":
// 입찰서 제출기간: 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 "updatedAt":
// 등록일시: 년, 월, 일 형식만
if (original.updatedAt) {
value = formatDate(original.updatedAt, "KR")
} else {
value = "-"
}
break
case "biddingRegistrationDate":
// 입찰등록일: 년, 월, 일 형식만
if (original.biddingRegistrationDate) {
value = formatDate(original.biddingRegistrationDate, "KR")
} else {
value = "-"
}
break
case "projectName":
// 프로젝트: 코드와 이름 조합
const code = original.projectCode
const name = original.projectName
value = code && name ? `${code} (${name})` : (code || name || "-")
break
case "hasSpecificationMeeting":
// 사양설명회: Yes/No
value = original.hasSpecificationMeeting ? "Yes" : "No"
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<void> {
// 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)
}
|