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 ParseResult parse(MultipartFile file, RowMapper mapper, List> validators) throws IOException { List data = new ArrayList<>(); List 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 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 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 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 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(List data, List failures) { public boolean hasFailures() { return !failures.isEmpty(); } } public record ParseFailure(int row, String reason) {} @FunctionalInterface public interface RowMapper { T map(Map rowData) throws Exception; } @FunctionalInterface public interface Validator { String validate(T item, Map rawData); } private static class ValidationException extends RuntimeException { public ValidationException(String message) { super(message); } } }