summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor/export-partners-biddings-to-excel.ts
blob: 9e99eeeca880a96f0e373077f0e746710a719e9f (plain)
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
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<PartnersBiddingListItem>,
  {
    filename = "협력업체입찰목록",
    onlySelected = false,
  }: {
    filename?: string
    onlySelected?: boolean
  } = {}
): Promise<void> {
  // 테이블에서 실제 사용 중인 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<string, string> = {
    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<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)
}