프로젝트

[SpringBoot Excel] 엑셀 업로드 공통 서비스 가이드

nineDeveloper 2020. 6. 20.
728x90

엑셀 업로드 공통 서비스

클래스 파일 경로: com.project.component.excel.service

ExcelReader

엑셀 파일로 부터 데이터를 읽어 들여 요청한 객체 타입에 맞게 데이터를 맵핑 한다음 요청 객체 리스트로 리턴 해준다

필수 셋팅

  • 엑셀 업로드용 DTO 객체를 생성할때 엑셀 업로드용 DTO 임을 알 수 있도록 반드시 suffixExcelDto 를 붙여서 생성한다
    • ex) SampleExcelDto, ParkinglotInfoExcelDto

Sample 객체 소스코드 from 메서드

엑셀 업로드 데이터를 셋팅할 오브젝트에 static from 메서드를 반드시 생성해야 한다

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SampleExcelDto {

    ...

    /**
     * 엑셀 업로드 처리를 위한 객체
     * @param row
     * @return
     */
    public static SampleExcelDto from(Row row) {
        return ExcelUtils.setObjectMapping(new SampleExcelDto(), row);
    }
}

사용법

static 메서드이므로 그냥 바로 호출해서 사용 하면된다

  • Sample
  • List<SampleExcelDto> dataList = ExcelReader.getObjectList(multipartFile, SampleExcelDto::from, 1);
  • 요청 데이터
    • multipartFile타입의 엑셀 파일
    • 요청객체의 static from 메서드
    • 헤더를 제외한 데이터 시작 ROW(만약 헤더가 있다면 시작 ROW는 1)

시작 ROW 가 1이라면 기본값이므로 아래와 같이 넘겨주지 않아도 무방하다

List<SampleExcelDto> dataList = ExcelReader.getObjectList(multipartFile, SampleExcelDto::from);

Sample 코드

엑셀 업로드 로직 구현 시 Sample 코드를 참조해 비지니스 로직을 작성 하면 된다

Sample 엑셀 업로드 DTO 객체

Validation 체크를 위해서 Validation 체크가 필요한 값은 반드시 Validation 어노테이션을 붙여줘야 한다
엑셀 업로드시 해당 Validation을 체크하여 검증을 통과하지 못하면 Validation message를 리턴한다

※Validation 어노테이션 추가시 @NotEmpty, @NotNull에 대해서는 별도의 메세지 처리를 하지 말아야 한다
별도의 메세지 처리시 Validation 검증을 할 수 없다

package com.iparking.api.sample.dto;

import com.iparking.component.excel.utils.ExcelUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import org.apache.poi.ss.usermodel.Row;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

/**
 * 엑셀 업로드 Sample Excel DTO
 * Created by KMS on 02/09/2019.
 */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@ApiModel("Sample 엑셀 업로드 객체")
public class SampleExcelDto {
    @NotEmpty
    @ApiModelProperty("이름")
    private String name;
    @ApiModelProperty("이메일")
    private String email;
    @NotEmpty
    @ApiModelProperty("전화번호")
    private String phone;
    @NotEmpty
    @ApiModelProperty("소속부서")
    @Size(min = 1, max = 10)
    private String dept;
    @NotNull
    @ApiModelProperty("업무코드")
    private Integer workCode;
    @NotNull
    @ApiModelProperty("부서코드")
    private Integer deptCode;
    @Size(min = 1, max = 500)
    @ApiModelProperty("내용")
    private String content;
    @ApiModelProperty("소수")
    private Double percent;
    @ApiModelProperty("날짜")
    private String createDate;
    @ApiModelProperty("일시")
    private String updateDatetime;
    @ApiModelProperty("빈데이터")
    private String emptyData;

    /**
     * 엑셀 업로드 처리를 위한 객체
     * @param row
     * @return
     */
    public static SampleExcelDto from(Row row) {
        return ExcelUtils.setObjectMapping(new SampleExcelDto(), row);
    }
}

SampleExcelController 소스코드

엑셀 업로드 테스트를 해볼 수 있는 Sample 컨트롤러
Swagger에서 Sample 엑셀 양식을 다운로드 해서 Sample 엑셀 업로드 테스트를 해볼 수 있다

package com.iparking.api.sample.controller;

import com.iparking.api.sample.dto.SampleExcelDto;
import com.iparking.api.sample.service.SampleExcelService;
import com.iparking.common.domain.CommonResult;
import com.iparking.component.excel.model.ExcelReaderErrorField;
import io.swagger.annotations.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

/**
 * Sample Excel 컨트롤러
 * Created by KMS on 18/03/2020.
 */
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/sample")
@Api(value = "Sample Excel API 컨트롤러", tags = "Sample Excel API")
// @ApiIgnore
public class SampleExcelController {

    private final SampleExcelService sampleExcelService;

    @ApiOperation(value = "Sample 엑셀 양식 다운로드")
    @GetMapping("/excel")
    public ResponseEntity<Resource> getSampleExcel() throws IOException {
        return sampleExcelService.getSampleExcel();
    }

    @ApiOperation(value = "Sample 엑셀 업로드", nickname = "KMS", notes = "# Sample 엑셀 업로드를 테스트 해볼 수 있는 API\n" +
            "## 엑셀 업로드 공통 서비스 가이드\n" +
            "---\n" +
            "https://parkingcloud.dooray.com/project/2525192394467198586?contentsType=wiki&pageId=2697094616920924080\n\n" +
            "## 사용예시\n" +
            "---\n" +
            "`List<SampleExcelDto> dataList = ExcelReader.getObjectList(multipartFile, SampleExcelDto::from);`")
    @ApiResponses({
            @ApiResponse(code = 4010, message = "읽을 수 없는 엑셀 파일 입니다 (DRM 적용 또는 다른이유)"),
            @ApiResponse(code = 4011, message = "엑셀 업로드 FIELD ERROR :: 입력 데이터를 확인하세요\n" +
                    "## Sample Data\n" +
                    "### TYPE\n" +
                    "___\n" +
                    "`TYPE`: 잘못된 데이터 타입\n" +
                    "`EMPTY`: 필수 입력값 누락\n" +
                    "`VALID`: 유효성 검증 실패\n" +
                    "`UNKNOWN`: 알수 없는 에러\n" +
                    "```json" +
                    "" +
                    "'[\n" +
                    "[\n"+
                    "  {\n" +
                    "    \"type\": \"EMPTY\",\n" +
                    "    \"row\": 2,\n" +
                    "    \"field\": \"name\",\n" +
                    "    \"fieldHeader\": \"이름\",\n" +
                    "    \"inputData\": \"\",\n" +
                    "    \"message\": \"필수 입력값 누락\",\n" +
                    "    \"exceptionMessage\": \"이름은 필수 입력값입니다\"\n" +
                    "  }\n" +
                    "]"+
                    "",
                    response = ExcelReaderErrorField.class, responseContainer = "List",
                    examples = @Example(value = {
                            @ExampleProperty(
                                    value = "[\n"+
                                            "  {\n" +
                                            "    \"type\": \"EMPTY\",\n" +
                                            "    \"row\": 2,\n" +
                                            "    \"field\": \"name\",\n" +
                                            "    \"fieldHeader\": \"이름\",\n" +
                                            "    \"inputData\": \"\",\n" +
                                            "    \"message\": \"필수 입력값 누락\",\n" +
                                            "    \"exceptionMessage\": \"이름은 필수 입력값입니다\"\n" +
                                            "  }\n" +
                                            "]",
                                    mediaType = MediaType.APPLICATION_JSON_VALUE
                            )
                    })
            ),
    })
    @PostMapping("/excel")
    public CommonResult<List<SampleExcelDto>> insertEquipment(@ApiParam("JWT 토큰 - 요청시 전달 받아 ID를 추출한다")
                                                              @RequestHeader(value = "jwt", defaultValue = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbktleSI6IjIxMEJGMDY1LUFDRjMtNEUxQS04QTM4LThFRjQ0Mzg1NTM5QSIsImlkIjoieXNraW0iLCJuYW1lIjoiXHVhZTQwXHVjNzIwXHVjMTFkIiwiZGVwYXJ0bWVudF9jb2RlIjoiMTUwMTIiLCJkZXBhcnRtZW50X25hbWUiOiJcdWM2ZjlcdWQ1MDRcdWI4NjBcdWQyYjhcdWM1ZDRcdWI0ZGNcdWQzMGNcdWQyYjgiLCJlbXBsb3llZV9udW1iZXIiOiIyMDE3MDAyNCIsImVtYWlsIjoieXNraW1AaXBhcmtpbmcuY28ua3IiLCJwb3NpdGlvbl9jb2RlIjoiSjEiLCJwb3NpdGlvbl9uYW1lIjoiXHViOWU0XHViMmM4XHVjODAwIiwiZHV0eV9jb2RlIjoiTDIiLCJkdXR5X25hbWUiOiJcdWQzMDBcdWM2ZDAiLCJ0ZWFtX25hbWUiOm51bGwsInBhdGgiOiIxMDAwfDEwMDAwfDE1MDExfDE1MDEyIiwicGF0aF9uYW1lIjoiXHVkMzBjXHVkMGI5XHVkMDc0XHViNzdjXHVjNmIwXHViNGRjfFImRHxcdWQ1MDRcdWI4NjBcdWQyYjhcdWM1ZDRcdWI0ZGNcdWQzMDB8XHVjNmY5XHVkNTA0XHViODYwXHVkMmI4XHVjNWQ0XHViNGRjXHVkMzBjXHVkMmI4In0.bxBABM7tSoHyxMyfp2P89rQSGS_cWpAqc-1bcGDrbrY") String jwt,
                                                              @ApiParam("업로드 엑셀파일 - 업무에 해당하는 엑셀 업로드 양식을 다운 받아 양식에 맞게 작성한뒤 업로드해야함")
                                                              @RequestPart(value = "file") MultipartFile file) throws Exception {
        return sampleExcelService.excelUpload(jwt, file);
    }
}

SampleExcelService 소스코드

package com.iparking.api.sample.service;

import com.iparking.api.sample.dto.SampleExcelDto;
import com.iparking.common.domain.CommonResult;
import com.iparking.component.excel.service.ExcelReader;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import static com.iparking.common.constant.ResCode.SUCCESS;

/**
 * Sample Excel 서비스
 * Created by KMS on 18/03/2020.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SampleExcelService {

    private final ResourceLoader resourceLoader;

    /**
     * Sample 엑셀 업로드 테스트
     * @param jwt
     * @param file
     * @return
     * @throws Exception
     */
    public CommonResult<List<SampleExcelDto>> excelUpload(String jwt, MultipartFile file) throws Exception {

        List<SampleExcelDto> sampleExcelDtoList = ExcelReader.getObjectList(file, SampleExcelDto::from);
        for(SampleExcelDto dto: sampleExcelDtoList) {
            log.info("Excel Upload Sample Data = {}", dto);
        }
        return new CommonResult<>(SUCCESS, SUCCESS.getMessage(), sampleExcelDtoList);
    }

    /**
     * Sample 엑셀 다운로드
     * @return
     * @throws IOException
     */
    public ResponseEntity<Resource>  getSampleExcel() throws IOException {

        String fileName = "sampleUpload.xlsx";

        // 리소스 파일 획득하기
        // /src/main/resources/something.txt 파일을 읽어 온다.
        // String something = IOUtils.toString(getClass().getResourceAsStream("/something.txt"), "UTF-8");

        //Path filePath = Paths.get("src", "resources", "file", File.separatorChar + fileName);
        // Resource resource = resourceLoader.getResource(filePath.toString());
        Path filePath = Paths.get(File.separatorChar + "file", File.separatorChar + fileName);
        Resource resource = new InputStreamResource(getClass().getResourceAsStream(filePath.toString()));

        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .cacheControl(CacheControl.noCache())
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName + "")
                .body(resource);
    }
}

SampleExcelControllerTest 테스트 소스코드

Sample 엑셀 업로드 API 를 테스트 할 수 있는 소스코드

package com.iparking.api.sample.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.iparking.api.sample.dto.SampleExcelDto;
import com.iparking.common.BaseControllerTest;
import com.iparking.common.domain.CommonResult;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.context.WebApplicationContext;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * Created by KMS on 18/03/2020.
 */
class SampleExcelControllerTest extends BaseControllerTest {

    private static Validator validator;

    public SampleExcelControllerTest(ObjectMapper mapper, MockMvc mockMvc, WebApplicationContext ctx) {
        super(mapper, mockMvc, ctx);
    }

    @BeforeAll
    public static void setUpValidator() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    @DisplayName("Sample 엑셀 다운로드")
    void getSampleExcel() throws Exception {
        mockMvc.perform(get("/sample/excel"))
                .andExpect(status().isOk());
    }

    @Test
    @DisplayName("Sample 엑셀 업로드")
    void insertEquipment() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.multipart("/sample/excel")
                .file(getMultipartFile("sampleUpload.xlsx", "file"))
                .header("jwt",jwtData);

        ResultActions result = mockMvc.perform(builder)
                .andExpect(status().isOk())
                .andExpect(jsonPath("code").value(100))
                .andDo(print());

        String responseBody = result.andReturn().getResponse().getContentAsString();

        CommonResult commonResult = mapper.readValue(responseBody, CommonResult.class);
        List<Map<String, Object>> sampleExcelList = (List<Map<String, Object>>) commonResult.getResult();
        System.out.println(commonResult.getResult().getClass().getSimpleName());
        for(Map<String, Object> dto : sampleExcelList) {
            System.out.println(dto);
        }

    }

    @Test
    @DisplayName("Sample 엑셀 업로드 에러")
    void insertEquipmentError() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.multipart("/sample/excel")
                .file(getMultipartFile("sampleUpload_error.xlsx", "file"))
                .header("jwt",jwtData);

        ResultActions result = mockMvc.perform(builder)
                .andExpect(status().isOk())
                .andExpect(jsonPath("code").value(4011))
                .andDo(print());

        String responseBody = result.andReturn().getResponse().getContentAsString();

        CommonResult commonResult = mapper.readValue(responseBody, CommonResult.class);
        List<Map<String, Object>> sampleExcelList = (List<Map<String, Object>>) commonResult.getResult();
        System.out.println(commonResult.getResult().getClass().getSimpleName());
        for(Map<String, Object> dto : sampleExcelList) {
            System.out.println(dto);
        }

    }

    @Test
    @DisplayName("객체 Validation 체크")
    void objectValidationCheck() {
        SampleExcelDto dto = new SampleExcelDto();
        Set<ConstraintViolation<SampleExcelDto>> constraintValidations = validator.validate(dto);

        ConstraintViolation<SampleExcelDto> validData = constraintValidations.stream()
                .filter(data -> data.getPropertyPath().toString().equals("name"))
                .findFirst().orElse(null);

        if(Objects.nonNull(validData)){
            System.out.println(validData.getMessageTemplate().contains("NotEmpty"));
            // System.out.println(validData.getPropertyPath()+" : "+validData.getMessage());
        }


        // for(ConstraintViolation data : constraintValidations) {
        //     System.out.println(data.getPropertyPath());
        // }


        // System.out.println("## constraintValidations = "+constraintValidations);
        // System.out.println("## size = "+constraintValidations.size());
        // System.out.println("## message = "+constraintValidations.iterator().next().getMessage());
    }

}

ExcelReaderTest 소스 코드

클래스 파일 경로: test.java.com.iparking.component.excel
엑셀 파일을 읽어 객체에 맵핑하는 것 까지 테스트 해볼 수 있다

package com.iparking.component.excel;

import com.iparking.component.excel.service.ExcelReader;
import com.iparking.component.excel.tutorial.domain.SampleExcelUpload;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

/**
 * Created by KMS on 02/09/2019.
 */
public class ExcelReaderTest {

    @Test
    @DisplayName("엑셀 데이터 읽어서 오브젝트에 담기 테스트")
    public void getObjectListTest() throws IOException, InvalidFormatException, IllegalAccessException {
        String fileName = "dev_doc.xlsx";
        Path filePath = Paths.get("src", "test", "resources",File.separatorChar + fileName);
        System.out.println("## filePath = "+filePath);

        String originalFileName = fileName;
        String contentType = "";

        byte[] content = Files.readAllBytes(filePath);

        MockMultipartFile multipartFile = new MockMultipartFile(fileName, originalFileName, contentType, content);

        List<SampleExcelDto> products = ExcelReader.getObjectList(multipartFile, SampleExcelDto::from, 2);
        products.forEach(data -> System.out.println("## data = "+data));
    }

}

엑셀 업로드 소스코드

ExcelReader 소스 코드

  • getObjectList: 엑셀 파일의 데이터를 읽어서 요청한 오브젝트 타입 리스트에 값을 순서대로 담아 준다
    처리 중 에러가 발생하거나 처리 후 errorFieldListField 에러 내역이 있을 경우 에러 내역을 리턴한다
  • isPass: ROW 데이터를 체크해서 데이터가 모두 비어있으면 Trash 데이터라고 판단하고 PASS 한다
  • getHeader: 엑셀 파일의 데이터 중 가장 첫번 째 ROWHeader 데이터를 가져온다
  • getTitle: 엑셀 파일의 데이터 중 TITLE 데이터가 있다면 가져온다
package com.iparking.component.excel.service;

import com.iparking.component.excel.model.ExcelReaderErrorField;
import com.iparking.component.excel.utils.ExcelUtils;
import com.iparking.exception.excel.ExcelReaderFileException;
import com.iparking.exception.excel.ExcelReaderFieldException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 *엑셀 업로드를 유틸리티
 * Created by KMS on 02/09/2019.
 */
@Slf4j
public class ExcelReader {

    /** 엑셀 업로드 에러 필드 리스트 **/
    public static List<ExcelReaderErrorField> errorFieldList;
    /** 엑셀 업로드 HEADER 리스트 **/
    public static List<String> headerList;

    /**
     * SAMPLE
     * 사용하기 위해서는 아래와 같이 엑셀 업로드 객체 생성 후 해당 객체
     * from static Constructor 를 생성하고
     * ExcelUtils.setObjectMapping(new Object(), row); 로 리턴 해야 함
     *
     * Sample Product 객체
     */
    /*
    public static Product from(Row row) {
        return ExcelUtils.setObjectMapping(new Product(), row);
    }
    */

    /**
     * 엑셀 파일의 데이터를 읽어서 요청한 오브젝트 타입 리스트에 담아 준다
     * @param multipartFile 엑셀 파일 데이터
     * @param rowFunc cell 데이터를 객체에 셋팅해주는 함수
     * @param <T> 요청 객체 타입
     * @return List<T> 요청 객체 타입의 리스트로 리턴
     * @throws IOException
     */
    public static <T> List<T> getObjectList(final MultipartFile multipartFile,
                                            final Function<Row, T> rowFunc) throws Exception {
        //헤더 데이터가 ROW가 0에 있고 실제 데이터의 시작 ROW가 1번째 부터인 것으로 판단
        return getObjectList(multipartFile, rowFunc, 1);
    }

    /**
     * 엑셀 파일의 데이터를 읽어서 요청한 오브젝트 타입 리스트에 담아 준다
     * @param multipartFile 엑셀 파일 데이터
     * @param rowFunc cell 데이터를 객체에 셋팅해주는 함수
     * @param startRow 데이터 시작 ROW (Default: 1)
     * @param <T> 요청 객체 타입
     * @return List<T> 요청 객체 타입의 리스트로 리턴
     * @throws IOException
     */
    public static <T> List<T> getObjectList(final MultipartFile multipartFile,
                                      final Function<Row, T> rowFunc, Integer startRow) {

        errorFieldList = new ArrayList<>();
        headerList = new ArrayList<>();

        // rownum 이 입력되지 않으면 default로 1 번째 라인을 데이터 시작 ROW로 판단
        if(Objects.isNull(startRow)) startRow = 1;

        // 엑셀 파일을 Workbook에 담는다
        final Workbook workbook;
        try {
            workbook = WorkbookFactory.create(multipartFile.getInputStream());
        } catch (IOException e) {
            throw new ExcelReaderFileException(e.getMessage(), e);
        }
        // 시트 수 (첫번째에만 존재시 0)
        final Sheet sheet = workbook.getSheetAt(0);
        // 전체 행 수
        final int rowCount = sheet.getPhysicalNumberOfRows();
        // log.debug("## rowCount = "+rowCount);
        // 헤더 셋팅
        headerList = getHeader(multipartFile);

        List<T> objectList = IntStream
                .range(startRow, rowCount)
                .filter(rowIndex -> isPass(sheet.getRow(rowIndex)))
                .mapToObj(rowIndex -> rowFunc.apply(sheet.getRow(rowIndex)))
                .collect(Collectors.toList());

        if(!ListUtils.emptyIfNull(errorFieldList).isEmpty())
            throw new ExcelReaderFieldException();

        return objectList;
    }

    /**
     * 해당 ROW에 있는 데이터가 모두 비어있으면 빈 ROW로 판단하고 해당 ROW는 PASS 시킨다
     * @param row
     * @return
     */
    private static boolean isPass(Row row) {
        int i =0;
        boolean isPass = false;
        while (i < row.getPhysicalNumberOfCells()) {
            if(StringUtils.isNotEmpty(ExcelUtils.getValue(row.getCell(i++))))
                isPass = true;
        }
        // log.debug("## row.getPhysicalNumberOfCells() = {}, isPass = {}",row.getPhysicalNumberOfCells(), isPass);
        return isPass;

    }


    /**
     * 헤더 가져오기
     * 가장 상단에 헤더가 있다면 헤더 정보를 List<String> 에 담아준다
     * @param multipartFile 엑셀파일
     * @return List<String> 헤더 리스트
     * @throws IOException
     */
    public static List<String> getHeader(final MultipartFile multipartFile) {
        return getHeader(multipartFile, 0); //헤더가 가장 첫번째 라인에 있다고 판단함
    }

    /**
     * 헤더 가져오기
     * 가장 상단에 헤더가 있다면 헤더 정보를 List<String> 에 담아준다
     * @param multipartFile 엑셀파일
     * @param rownum 헤더가 있는 row number 값
     * @return List<String> 헤더 리스트
     * @throws IOException
     */
    public static List<String> getHeader(final MultipartFile multipartFile, Integer rownum) {

        // rownum 이 입력되지 않으면 default로 0 번째 라인을 header 로 판단
        if(Objects.isNull(rownum)) rownum = 0;

        final Workbook workbook;
        try {
            workbook = WorkbookFactory.create(multipartFile.getInputStream());
        } catch (IOException e) {
            throw new ExcelReaderFileException(e.getMessage(), e);
        }
        // 시트 수 (첫번째에만 존재시 0)
        final Sheet sheet = workbook.getSheetAt(0);

        // 타이틀 가져오기
        Row title = sheet.getRow(rownum);
        return IntStream
                .range(0, title.getPhysicalNumberOfCells())
                .mapToObj(cellIndex -> title.getCell(cellIndex).getStringCellValue())
                .collect(Collectors.toList());
    }

    /**
     * 타이틀 가져오기
     * 가장 상단에 타이틀이 있다면 타이틀 정보를 List<String> 에 담아준다
     * @param multipartFile 엑셀파일
     * @return List<String> 타이틀 리스트
     * @throws IOException
     */
    public static List<String> getTitle(final MultipartFile multipartFile) {

        final Workbook workbook;
        try {
            workbook = WorkbookFactory.create(multipartFile.getInputStream());
        } catch (IOException e) {
            throw new ExcelReaderFileException(e.getMessage(), e);
        }
        // 시트 수 (첫번째에만 존재시 0)
        final Sheet sheet = workbook.getSheetAt(0);

        // 타이틀 가져오기
        Row title = sheet.getRow(0);
        return IntStream
                .range(0, title.getPhysicalNumberOfCells())
                .mapToObj(cellIndex -> title.getCell(cellIndex).getStringCellValue())
                .collect(Collectors.toList());
    }

}

ExcelUtils 소스코드

  • getValue: Cell 데이터를 타입에 맞게 받아와서 String 값으로 변환하여 리턴
  • setObjectMapping: 객체와 엑셀의 ROW 데이터를 받아서 ROW 데이터의 값을 객체에 셋팅 해준다
    처리 중 Field 에 대한 에러가 발생하거나 Validation 검증을 통과 하지 못할 경우 내역을 errorFieldList에 담는다
  • checkValidation: 객체의 Validation을 검증해서 검증을 통과 하지 못한 값을 에러 리스트에 담는다
package com.iparking.component.excel.utils;

import com.github.drapostolos.typeparser.TypeParser;
import com.github.drapostolos.typeparser.TypeParserException;
import com.iparking.component.excel.constant.ExcelReaderFieldError;
import com.iparking.component.excel.model.ExcelReaderErrorField;
import com.iparking.component.excel.service.ExcelReader;
import eu.bitwalker.useragentutils.Browser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.ss.formula.eval.ErrorEval;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

/**
 * 공통 엑셀 컴포넌트 유틸
 * Created by KMS on 26/09/2019.
 */
@Slf4j
public class ExcelUtils {

   ...

    /**
     * Cell 데이터를 Type 별로 체크 하여 String 데이터로 변환함
     * String 데이터로 우선 변환해야 함
     * @param cell 요청 엑셀 파일의 cell 데이터
     * @return String 형으로 변환된 cell 데이터
     */
    public static String getValue(Cell cell) {

        String value = null;
        // 셀 내용의 유형 판별
        switch (cell.getCellType()) {
            case STRING: // getRichStringCellValue() 메소드를 사용하여 컨텐츠를 읽음
                value = cell.getRichStringCellValue().getString();
                break;
            case NUMERIC: // 날짜 또는 숫자를 포함 할 수 있으며 아래와 같이 읽음
                if (DateUtil.isCellDateFormatted(cell))
                    value = cell.getLocalDateTimeCellValue().toString();
                else
                    value = String.valueOf(cell.getNumericCellValue());
                    if (value.endsWith(".0"))
                        value = value.substring(0, value.length() - 2);
                break;
            case BOOLEAN:
                value = String.valueOf(cell.getBooleanCellValue());
                break;
            case FORMULA:
                value = String.valueOf(cell.getCellFormula());
                break;
            case ERROR:
                value = ErrorEval.getText(cell.getErrorCellValue());
                break;
            case BLANK:
            case _NONE:
            default:
                value = "";
        }
        //log.debug("## cellType = {}, value = {}",cell.getCellType(),value);
        return value;
    }

    /**
     * TypeParser 로 String으로 변환된 Cell 데이터를 객체 필드 타입에 맞게 변환하여 셋팅해줌
     * @param object 요청 객체
     * @param <T>
     * @param row 엑셀 ROW 데이터
     * @return Cell 데이터를 맵핑한 오브젝트
     */
    public static<T> T setObjectMapping(T object, Row row) {

        int i = 0;

        if(Objects.isNull(object)) return null;

        for (Field field : object.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            String cellValue = null;
            TypeParser typeParser = TypeParser.newBuilder().build();

            try {
                if( i  < row.getPhysicalNumberOfCells() ) { //유효한 Cell 영역 까지만
                    cellValue = ExcelUtils.getValue(row.getCell(i));
                    Object setData = null;
                    if(!StringUtils.isEmpty(cellValue))
                        setData = typeParser.parseType(cellValue, field.getType());
                    field.set(object, setData);
                    checkValidation(object, row, i, cellValue, field.getName());
                }
            } catch (TypeParserException e) {
                ExcelReaderFieldError error = ExcelReaderFieldError.TYPE;
                ExcelReader.errorFieldList.add(ExcelReaderErrorField.builder()
                        .type(error.name())
                        .row(row.getRowNum()+1)
                        .field(field.getName())
                        .fieldHeader(ExcelReader.headerList.get(i))
                        .inputData(cellValue)
                        .message(error.getMessage()+
                                "데이터 필드타입 - "+field.getType().getSimpleName()+
                                ", 입력값 필드타입 - "+cellValue.getClass().getSimpleName())
                        .exceptionMessage(ExceptionUtils.getRootCauseMessage(e))
                        .build());
            } catch (Exception e) {
                ExcelReaderFieldError error = ExcelReaderFieldError.UNKNOWN;
                ExcelReader.errorFieldList.add(ExcelReaderErrorField.builder()
                        .type(error.name())
                        .row(row.getRowNum()+1)
                        .field(field.getName())
                        .fieldHeader(ExcelReader.headerList.get(i))
                        .inputData(cellValue)
                        .message(error.getMessage())
                        .exceptionMessage(ExceptionUtils.getRootCauseMessage(e))
                        .build());
            }
            i++;

        }

        return object;
    }

    /**
     * 객체에 대한 Validation 을 검증해서 검증을 통과 하지 못한 내역이 있을 경우 에러 리스트에 담는다
     * @param object
     * @param row
     * @param i
     * @param <T>
     */
    private static<T> void checkValidation(T object, Row row, int i, String cellValue, String fieldName) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<T>> constraintValidations = validator.validate(object);
        ConstraintViolation<T> validData = constraintValidations.stream()
                .filter(data -> data.getPropertyPath().toString().equals(fieldName))
                .findFirst().orElse(null);

        if(Objects.isNull(validData)) return;

        String fieldHeader = ExcelReader.headerList.get(i);
        ExcelReaderFieldError error = ExcelReaderFieldError.VALID;
        String exceptionMessage = validData.getMessage();

        if(validData.getMessageTemplate().contains("NotEmpty") || validData.getMessageTemplate().contains("NotNull")) {
            error = ExcelReaderFieldError.EMPTY;
            exceptionMessage = fieldHeader+"은 필수 입력값입니다";
        }

        ExcelReader.errorFieldList.add(ExcelReaderErrorField.builder()
                .type(error.name())
                .row(row.getRowNum()+1)
                .field(validData.getPropertyPath().toString())
                .fieldHeader(fieldHeader)
                .inputData(cellValue)
                .message(error.getMessage())
                .exceptionMessage(exceptionMessage)
                .build());
    }
}

ExcelReaderFieldError 소스코드

엑셀 업로드시 Field 에러에 대한 구분 상수

package com.iparking.component.excel.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;

/**
 * 엑셀 업로드 Field 에러 상수
 * Created by KMS on 18/03/2020.
 */
@Getter
@AllArgsConstructor
public enum ExcelReaderFieldError {
    TYPE("잘못된 데이터 타입: "),
    EMPTY("필수 입력값 누락"),
    VALID("유효성 검증 실패"),
    UNKNOWN("알수 없는 에러")
    ;

    private static Map<String, ExcelReaderFieldError> messageToMap;

    /** 메세지 */
    private final String message;

    /**
     * 에러명으로 ExcelReaderErrorConstant 맵으로 맵핑
     * @param name
     * @return
     */
    public static ExcelReaderFieldError getExcelReaderErrorConstant(String name) {
        if(messageToMap == null) {
            initMapping();
        }
        return messageToMap.get(name);
    }

    /**
     * 맵 초기화
     */
    private static void initMapping() {
        messageToMap = new HashMap<>();
        for (ExcelReaderFieldError excelReaderFieldError : values()) {
            messageToMap.put(excelReaderFieldError.name(), excelReaderFieldError);
        }
    }
}

ExcelReaderErrorField 소스코드

엑셀 업로드 필드 에러를 리턴 해줄 때 사용하는 객체
여러 필드에 대한 에러를 아래의 내용으로 배열에 담아 한번에 리턴해준다

package com.iparking.component.excel.model;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;

/**
 * 엑셀 업로드 에러 필드
 * Created by KMS on 18/03/2020.
 *
 * Sample
 [
   {
     "errorType": "TYPE",
     "errorRow": 2,
     "errorFieldName": "test",
     "errorFieldHeaderName": "테스트",
     "errorInputData": "안녕",
     "errorMessage": "잘못된 데이터 타입: 데이터 필드타입 - Integer, 입력된 필드타입 - String",
     "exceptionMessage": "NumberFormatException: For input string: \"안녕\""
   }
 ]
 */
@Getter
@Setter
@NoArgsConstructor
@ToString
@ApiModel("엑셀 업로드 에러 필드 객체")
public class ExcelReaderErrorField {
    @ApiModelProperty("ERROR 타입 (TYPE: 잘못된 데이터 타입, EMPTY: 필수 입력값 누락, VALID: 유효성 검증 실패, UNKNOWN: 알수 없는 에러)")
    private String type;
    @ApiModelProperty("ERROR 행 번호")
    private Integer row;
    @ApiModelProperty("ERROR 필드명")
    private String field;
    @ApiModelProperty("ERROR 필드 Header명")
    private String fieldHeader;
    @ApiModelProperty("ERROR 입력값")
    private String inputData;
    @ApiModelProperty("ERROR 메세지")
    private String message;
    @ApiModelProperty("EXCEPTION MESSAGE")
    private String exceptionMessage;

    @Builder
    public ExcelReaderErrorField(String type, Integer row, String field, String fieldHeader, String inputData, String message, String exceptionMessage) {
        this.type = type;
        this.row = row;
        this.field = field;
        this.fieldHeader = fieldHeader;
        this.inputData = inputData;
        this.message = message;
        this.exceptionMessage = exceptionMessage;
    }
}
728x90

댓글

💲 추천 글