feat: Excel batch registration (equipment + employees)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package com.srsystem.util;
|
||||
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ExcelParser {
|
||||
|
||||
public static <T> ParseResult<T> parse(MultipartFile file, RowMapper<T> mapper, List<Validator<T>> validators) throws IOException {
|
||||
List<T> data = new ArrayList<>();
|
||||
List<ParseFailure> failures = new ArrayList<>();
|
||||
|
||||
try (InputStream is = file.getInputStream(); Workbook wb = new XSSFWorkbook(is)) {
|
||||
Sheet sheet = wb.getSheetAt(0);
|
||||
if (sheet.getPhysicalNumberOfRows() < 2) {
|
||||
return new ParseResult<>(List.of(), List.of(new ParseFailure(0, "데이터 행이 없습니다")));
|
||||
}
|
||||
|
||||
Row headerRow = sheet.getRow(0);
|
||||
int colCount = headerRow.getPhysicalNumberOfCells();
|
||||
String[] headers = new String[colCount];
|
||||
for (int i = 0; i < colCount; i++) {
|
||||
headers[i] = getCellValue(headerRow.getCell(i)).trim().toLowerCase();
|
||||
}
|
||||
|
||||
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null) continue;
|
||||
|
||||
try {
|
||||
Map<String, String> rowData = new HashMap<>();
|
||||
for (int c = 0; c < colCount; c++) {
|
||||
String val = getCellValue(row.getCell(c));
|
||||
if (!val.isEmpty()) {
|
||||
rowData.put(headers[c], val);
|
||||
}
|
||||
}
|
||||
|
||||
if (rowData.isEmpty()) continue;
|
||||
|
||||
T item = mapper.map(rowData);
|
||||
for (Validator<T> v : validators) {
|
||||
String err = v.validate(item, rowData);
|
||||
if (err != null) {
|
||||
throw new ValidationException(err);
|
||||
}
|
||||
}
|
||||
data.add(item);
|
||||
} catch (Exception e) {
|
||||
failures.add(new ParseFailure(r + 1, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ParseResult<>(data, failures);
|
||||
}
|
||||
|
||||
public static byte[] generateTemplate(String[] headers, String sheetName, Map<Integer, String[]> dropdowns) throws IOException {
|
||||
try (Workbook wb = new XSSFWorkbook(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = wb.createSheet(sheetName);
|
||||
Row headerRow = sheet.createRow(0);
|
||||
|
||||
CellStyle headerStyle = wb.createCellStyle();
|
||||
Font headerFont = wb.createFont();
|
||||
headerFont.setBold(true);
|
||||
headerFont.setColor(IndexedColors.WHITE.getIndex());
|
||||
headerStyle.setFont(headerFont);
|
||||
headerStyle.setFillForegroundColor(IndexedColors.INDIGO.getIndex());
|
||||
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
headerStyle.setBorderBottom(BorderStyle.THIN);
|
||||
headerStyle.setBorderLeft(BorderStyle.THIN);
|
||||
headerStyle.setBorderRight(BorderStyle.THIN);
|
||||
headerStyle.setBorderTop(BorderStyle.THIN);
|
||||
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell cell = headerRow.createCell(i);
|
||||
cell.setCellValue(headers[i]);
|
||||
cell.setCellStyle(headerStyle);
|
||||
sheet.autoSizeColumn(i);
|
||||
}
|
||||
|
||||
if (dropdowns != null) {
|
||||
for (Map.Entry<Integer, String[]> entry : dropdowns.entrySet()) {
|
||||
int col = entry.getKey();
|
||||
String[] values = entry.getValue();
|
||||
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||
DataValidationConstraint constraint = helper.createExplicitListConstraint(values);
|
||||
CellRangeAddressList range = new CellRangeAddressList(1, 1000, col, col);
|
||||
DataValidation validation = helper.createValidation(constraint, range);
|
||||
validation.setShowErrorBox(true);
|
||||
validation.createErrorBox("유효성 검사", "허용된 값만 입력 가능합니다.");
|
||||
sheet.addValidationData(validation);
|
||||
}
|
||||
}
|
||||
|
||||
wb.write(bos);
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getCellValue(Cell cell) {
|
||||
if (cell == null) return "";
|
||||
return switch (cell.getCellType()) {
|
||||
case STRING -> cell.getStringCellValue().trim();
|
||||
case NUMERIC -> {
|
||||
double val = cell.getNumericCellValue();
|
||||
if (val == Math.floor(val) && !Double.isInfinite(val)) {
|
||||
yield String.valueOf((long) val);
|
||||
}
|
||||
yield String.valueOf(val);
|
||||
}
|
||||
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
|
||||
case FORMULA -> {
|
||||
try { yield String.valueOf((int) cell.getNumericCellValue()); }
|
||||
catch (Exception e) { yield cell.getStringCellValue(); }
|
||||
}
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
public record ParseResult<T>(List<T> data, List<ParseFailure> failures) {
|
||||
public boolean hasFailures() { return !failures.isEmpty(); }
|
||||
}
|
||||
|
||||
public record ParseFailure(int row, String reason) {}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RowMapper<T> {
|
||||
T map(Map<String, String> rowData) throws Exception;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Validator<T> {
|
||||
String validate(T item, Map<String, String> rawData);
|
||||
}
|
||||
|
||||
private static class ValidationException extends RuntimeException {
|
||||
public ValidationException(String message) { super(message); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user