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
|
// lib/excelUtils.ts
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";
import { toast } from "sonner";
// Define the column type enum
export type ColumnType = "STRING" | "NUMBER" | "LIST" | string;
// Define the column structure
export interface DataTableColumnJSON {
key: string;
label: string;
type: ColumnType;
options?: string[];
shi?: boolean; // SHI-only field indicator
// Add any other properties that might be in columnsJSON
}
// Define a generic data interface
export interface GenericData {
[key: string]: any;
TAG_NO?: string; // Since TAG_NO seems important in the code
}
// Define the options interface for the export function
export interface ExportExcelOptions {
tableData: GenericData[];
columnsJSON: DataTableColumnJSON[];
formCode: string;
onPendingChange?: (isPending: boolean) => void;
}
// Define the return type
export interface ExportExcelResult {
success: boolean;
error?: any;
}
/**
* Export table data to Excel with data validation for select columns
* @param options Configuration options for Excel export
* @returns Promise with success/error information
*/
export async function exportExcelData({
tableData,
columnsJSON,
formCode,
onPendingChange
}: ExportExcelOptions): Promise<ExportExcelResult> {
try {
if (onPendingChange) onPendingChange(true);
// Create a new workbook
const workbook = new ExcelJS.Workbook();
// 데이터 시트 생성
const worksheet = workbook.addWorksheet("Data");
// 유효성 검사용 숨김 시트 생성
const validationSheet = workbook.addWorksheet("ValidationData");
validationSheet.state = "hidden"; // 시트 숨김 처리
// 1. 유효성 검사 시트에 select 옵션 추가
const selectColumns = columnsJSON.filter(
(col) => col.type === "LIST" && col.options && col.options.length > 0
);
// 유효성 검사 범위 저장 맵 (컬럼 키 -> 유효성 검사 범위)
const validationRanges = new Map<string, string>();
selectColumns.forEach((col, idx) => {
const colIndex = idx + 1;
const colLetter = validationSheet.getColumn(colIndex).letter;
// 헤더 추가 (컬럼 레이블)
validationSheet.getCell(`${colLetter}1`).value = col.label;
// 옵션 추가
if (col.options) {
col.options.forEach((option, optIdx) => {
validationSheet.getCell(`${colLetter}${optIdx + 2}`).value = option;
});
// 유효성 검사 범위 저장 (ValidationData!$A$2:$A$4 형식)
validationRanges.set(
col.key,
`ValidationData!${colLetter}$2:${colLetter}${
col.options.length + 1
}`
);
}
});
// 2. 데이터 시트에 헤더 추가
const headers = columnsJSON.map((col) => col.label);
worksheet.addRow(headers);
// 헤더 스타일 적용
const headerRow = worksheet.getRow(1);
headerRow.font = { bold: true };
headerRow.alignment = { horizontal: "center" };
// 각 헤더 셀에 스타일 적용
headerRow.eachCell((cell, colNumber) => {
const columnIndex = colNumber - 1;
const column = columnsJSON[columnIndex];
if (column?.shi === true) {
// SHI-only 필드는 더 진한 음영으로 표시 (헤더 라벨은 원본 유지)
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFF9999" }, // 연한 빨간색 배경
};
cell.font = { bold: true, color: { argb: "FF800000" } }; // 진한 빨간색 글자
} else {
// 일반 필드는 기존 스타일
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFCCCCCC" }, // 연한 회색 배경
};
}
});
// 3. 데이터 행 추가
tableData.forEach((rowData, rowIndex) => {
const rowValues = columnsJSON.map((col) => {
const value = rowData[col.key];
return value !== undefined && value !== null ? value : "";
});
const dataRow = worksheet.addRow(rowValues);
// SHI-only 컬럼의 데이터 셀에도 음영 적용
dataRow.eachCell((cell, colNumber) => {
const columnIndex = colNumber - 1;
const column = columnsJSON[columnIndex];
if (column?.shi === true) {
// SHI-only 필드의 데이터 셀에 연한 음영 적용
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFFCCCC" }, // 매우 연한 빨간색 배경
};
// 읽기 전용임을 나타내기 위해 이탤릭 적용
cell.font = { italic: true, color: { argb: "FF666666" } };
}
});
});
// 4. 데이터 유효성 검사 적용
const maxRows = 5000; // 데이터 유효성 검사를 적용할 최대 행 수
columnsJSON.forEach((col, idx) => {
const colLetter = worksheet.getColumn(idx + 1).letter;
// SHI-only 필드가 아닌 LIST 타입에만 유효성 검사 적용
if (col.type === "LIST" && validationRanges.has(col.key) && col.shi !== true) {
const validationRange = validationRanges.get(col.key)!;
// 유효성 검사 정의
const validation = {
type: "list" as const,
allowBlank: true,
formulae: [validationRange],
showErrorMessage: true,
errorStyle: "warning" as const,
errorTitle: "유효하지 않은 값",
error: "목록에서 값을 선택해주세요.",
};
// 모든 데이터 행에 유효성 검사 적용 (최대 maxRows까지)
for (
let rowIdx = 2;
rowIdx <= Math.min(tableData.length + 1, maxRows);
rowIdx++
) {
worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation =
validation;
}
// 빈 행에도 적용 (최대 maxRows까지)
if (tableData.length + 1 < maxRows) {
for (
let rowIdx = tableData.length + 2;
rowIdx <= maxRows;
rowIdx++
) {
worksheet.getCell(`${colLetter}${rowIdx}`).dataValidation =
validation;
}
}
}
// SHI-only 필드의 빈 행들에도 음영 처리 적용
if (col.shi === true) {
for (let rowIdx = tableData.length + 2; rowIdx <= maxRows; rowIdx++) {
const cell = worksheet.getCell(`${colLetter}${rowIdx}`);
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFFFCCCC" },
};
cell.font = { italic: true, color: { argb: "FF666666" } };
}
}
});
// 5. 컬럼 너비 자동 조정
columnsJSON.forEach((col, idx) => {
const column = worksheet.getColumn(idx + 1);
// 최적 너비 계산
let maxLength = col.label.length;
tableData.forEach((row) => {
const value = row[col.key];
if (value !== undefined && value !== null) {
const valueLength = String(value).length;
if (valueLength > maxLength) {
maxLength = valueLength;
}
}
});
// 너비 설정 (최소 10, 최대 50)
column.width = Math.min(Math.max(maxLength + 2, 10), 50);
});
// 6. 범례 추가 (별도 시트)
const legendSheet = workbook.addWorksheet("Legend");
legendSheet.addRow(["Excel Template Legend"]);
legendSheet.addRow([]);
legendSheet.addRow(["Symbol", "Description"]);
legendSheet.addRow(["Red background header", "SHI-only fields that cannot be edited"]);
legendSheet.addRow(["Gray background header", "Regular editable fields"]);
legendSheet.addRow(["Light red background cells", "Data in SHI-only fields (read-only)"]);
legendSheet.addRow(["Red text color", "SHI-only field headers"]);
// 범례 스타일 적용
const legendHeaderRow = legendSheet.getRow(1);
legendHeaderRow.font = { bold: true, size: 14 };
const legendTableHeader = legendSheet.getRow(3);
legendTableHeader.font = { bold: true };
legendTableHeader.eachCell((cell) => {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFCCCCCC" },
};
});
// 7. 파일 다운로드
const buffer = await workbook.xlsx.writeBuffer();
saveAs(
new Blob([buffer]),
`${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx`
);
toast.success("Excel 내보내기 완료!");
return { success: true };
} catch (err) {
console.error("Excel export error:", err);
toast.error("Excel 내보내기 실패.");
return { success: false, error: err };
} finally {
if (onPendingChange) onPendingChange(false);
}
}
|