From b9ec080cdfdd0f1af9161ebaacd5d89659c62fbf Mon Sep 17 00:00:00 2001 From: Hyunjin-Jeong Date: Sun, 17 Nov 2024 22:47:32 +0900 Subject: [PATCH] (WIP) Excel uploader (#64) * Move: Application out of domain * Feat: Driver * Feat: DriverRepostory, DriverEntity * File: excel-uploader * Update: validation result * Feat: ExcelValidator empty result * Test: (disabled) valid excel * File: excel-uploader * Feat: ExcelReader --- tdd/excel-uploader/build.gradle | 1 + tdd/excel-uploader/excel-uploader.drawio | 112 ++++++++++++++++++ .../com/tdd/{domain => }/Application.java | 2 +- .../src/main/java/com/tdd/domain/Driver.java | 35 ++++++ .../java/com/tdd/domain/DriverRepository.java | 8 ++ .../java/com/tdd/util/excel/ExcelReader.java | 74 ++++++++++++ .../com/tdd/util/excel/ExcelValidator.java | 12 ++ .../util/excel/result/ValidationResult.java | 11 ++ .../com/tdd/domain/DriverRepositoryTest.java | 44 +++++++ .../test/java/com/tdd/domain/DriverTest.java | 15 +++ .../com/tdd/util/excel/ExcelReaderTest.java | 54 +++++++++ .../com/tdd/util/excel/ExcelTestHelper.java | 70 +++++++++++ .../tdd/util/excel/ExcelValidatorTest.java | 46 +++++++ 13 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 tdd/excel-uploader/excel-uploader.drawio rename tdd/excel-uploader/src/main/java/com/tdd/{domain => }/Application.java (92%) create mode 100644 tdd/excel-uploader/src/main/java/com/tdd/domain/Driver.java create mode 100644 tdd/excel-uploader/src/main/java/com/tdd/domain/DriverRepository.java create mode 100644 tdd/excel-uploader/src/main/java/com/tdd/util/excel/ExcelReader.java create mode 100644 tdd/excel-uploader/src/main/java/com/tdd/util/excel/ExcelValidator.java create mode 100644 tdd/excel-uploader/src/main/java/com/tdd/util/excel/result/ValidationResult.java create mode 100644 tdd/excel-uploader/src/test/java/com/tdd/domain/DriverRepositoryTest.java create mode 100644 tdd/excel-uploader/src/test/java/com/tdd/domain/DriverTest.java create mode 100644 tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelReaderTest.java create mode 100644 tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelTestHelper.java create mode 100644 tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelValidatorTest.java diff --git a/tdd/excel-uploader/build.gradle b/tdd/excel-uploader/build.gradle index 1beb407..f531985 100644 --- a/tdd/excel-uploader/build.gradle +++ b/tdd/excel-uploader/build.gradle @@ -22,6 +22,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.apache.poi:poi-ooxml:5.2.5' runtimeOnly 'com.h2database:h2' diff --git a/tdd/excel-uploader/excel-uploader.drawio b/tdd/excel-uploader/excel-uploader.drawio new file mode 100644 index 0000000..9f8d62c --- /dev/null +++ b/tdd/excel-uploader/excel-uploader.drawio @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tdd/excel-uploader/src/main/java/com/tdd/domain/Application.java b/tdd/excel-uploader/src/main/java/com/tdd/Application.java similarity index 92% rename from tdd/excel-uploader/src/main/java/com/tdd/domain/Application.java rename to tdd/excel-uploader/src/main/java/com/tdd/Application.java index 4626d10..6084aa7 100644 --- a/tdd/excel-uploader/src/main/java/com/tdd/domain/Application.java +++ b/tdd/excel-uploader/src/main/java/com/tdd/Application.java @@ -1,4 +1,4 @@ -package com.tdd.domain; +package com.tdd; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/tdd/excel-uploader/src/main/java/com/tdd/domain/Driver.java b/tdd/excel-uploader/src/main/java/com/tdd/domain/Driver.java new file mode 100644 index 0000000..a163a29 --- /dev/null +++ b/tdd/excel-uploader/src/main/java/com/tdd/domain/Driver.java @@ -0,0 +1,35 @@ +package com.tdd.domain; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Driver { + public Driver() {} + + public Driver(String name, String phoneNumber) { + this.name = name; + this.phoneNumber = phoneNumber; + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + private String name = ""; + + private String phoneNumber = ""; + + public Long getId() { + return id; + } + + public String getName() { + return this.name; + } + + public String getPhoneNumber() { + return this.phoneNumber; + } +} diff --git a/tdd/excel-uploader/src/main/java/com/tdd/domain/DriverRepository.java b/tdd/excel-uploader/src/main/java/com/tdd/domain/DriverRepository.java new file mode 100644 index 0000000..bd886e6 --- /dev/null +++ b/tdd/excel-uploader/src/main/java/com/tdd/domain/DriverRepository.java @@ -0,0 +1,8 @@ +package com.tdd.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DriverRepository extends JpaRepository { +} diff --git a/tdd/excel-uploader/src/main/java/com/tdd/util/excel/ExcelReader.java b/tdd/excel-uploader/src/main/java/com/tdd/util/excel/ExcelReader.java new file mode 100644 index 0000000..73fa417 --- /dev/null +++ b/tdd/excel-uploader/src/main/java/com/tdd/util/excel/ExcelReader.java @@ -0,0 +1,74 @@ +package com.tdd.util.excel; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.web.multipart.MultipartFile; + +public class ExcelReader { + public static Map> read(MultipartFile file) throws IOException { + Map> result = new HashMap<>(); + + try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) { + Sheet sheet = workbook.getSheetAt(0); + + Row headerRow = sheet.getRow(0); + + String[] headers = new String[headerRow.getLastCellNum()]; + for (int i = 0; i < headerRow.getLastCellNum(); i++) { + Cell cell = headerRow.getCell(i); + headers[i] = getCellValue(cell); + } + + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + Row row = sheet.getRow(i); + + Map rowData = new HashMap<>(); + + for (int j = 0; j < headers.length; j++) { + Cell cell = row.getCell(j); + rowData.put(headers[j], getCellValue(cell)); + } + + result.put(i, rowData); + } + } + + return result; + } + + private static String getCellValue(Cell cell) { + if (cell == null) return ""; + + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getLocalDateTimeCellValue().toString(); + } + double numericValue = cell.getNumericCellValue(); + if (numericValue == (long) numericValue) { + return String.format("%d", (long) numericValue); + } + return String.valueOf(numericValue); + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + try { + return String.valueOf(cell.getNumericCellValue()); + } catch (IllegalStateException e) { + return cell.getStringCellValue(); + } + default: + return ""; + } + } +} diff --git a/tdd/excel-uploader/src/main/java/com/tdd/util/excel/ExcelValidator.java b/tdd/excel-uploader/src/main/java/com/tdd/util/excel/ExcelValidator.java new file mode 100644 index 0000000..3a75960 --- /dev/null +++ b/tdd/excel-uploader/src/main/java/com/tdd/util/excel/ExcelValidator.java @@ -0,0 +1,12 @@ +package com.tdd.util.excel; + +import org.springframework.web.multipart.MultipartFile; + +import com.tdd.util.excel.result.ValidationResult; + +public class ExcelValidator { + + public static ValidationResult validate(MultipartFile file) { + return new ValidationResult(); + } +} diff --git a/tdd/excel-uploader/src/main/java/com/tdd/util/excel/result/ValidationResult.java b/tdd/excel-uploader/src/main/java/com/tdd/util/excel/result/ValidationResult.java new file mode 100644 index 0000000..d331f1f --- /dev/null +++ b/tdd/excel-uploader/src/main/java/com/tdd/util/excel/result/ValidationResult.java @@ -0,0 +1,11 @@ +package com.tdd.util.excel.result; + +public class ValidationResult { + public int getValidResults() { + return 0; + } + + public int getInvalidResults() { + return 0; + } +} diff --git a/tdd/excel-uploader/src/test/java/com/tdd/domain/DriverRepositoryTest.java b/tdd/excel-uploader/src/test/java/com/tdd/domain/DriverRepositoryTest.java new file mode 100644 index 0000000..0fa2231 --- /dev/null +++ b/tdd/excel-uploader/src/test/java/com/tdd/domain/DriverRepositoryTest.java @@ -0,0 +1,44 @@ +package com.tdd.domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +@DataJpaTest +public class DriverRepositoryTest { + private final Driver driver = new Driver("name", "010-1234-1234"); + + @Autowired + private DriverRepository repository; + + @Test + void save_test() { + long beforeCount = repository.count(); + + repository.save(driver); + long afterCount = repository.count(); + + assertEquals(beforeCount + 1, afterCount); + } + + @Test + void save_entity_value_test() { + Driver actual = repository.save(driver); + + assertEquals(driver.getName(), actual.getName()); + assertEquals(driver.getPhoneNumber(), actual.getPhoneNumber()); + } + + @Test + void find_by_id_test() { + repository.save(driver); + + Driver actual = repository.findById(driver.getId()) + .orElseThrow(() -> new IllegalArgumentException("Driver not found")); + + assertEquals(driver.getName(), actual.getName()); + assertEquals(driver.getPhoneNumber(), actual.getPhoneNumber()); + } +} diff --git a/tdd/excel-uploader/src/test/java/com/tdd/domain/DriverTest.java b/tdd/excel-uploader/src/test/java/com/tdd/domain/DriverTest.java new file mode 100644 index 0000000..0fd249a --- /dev/null +++ b/tdd/excel-uploader/src/test/java/com/tdd/domain/DriverTest.java @@ -0,0 +1,15 @@ +package com.tdd.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class DriverTest { + @Test + void create() { + Driver actual = new Driver("name", "010-1234-1234"); + + assertEquals(actual.getName(), "name"); + assertEquals(actual.getPhoneNumber(), "010-1234-1234"); + } +} \ No newline at end of file diff --git a/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelReaderTest.java b/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelReaderTest.java new file mode 100644 index 0000000..e450b62 --- /dev/null +++ b/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelReaderTest.java @@ -0,0 +1,54 @@ +package com.tdd.util.excel; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.multipart.MultipartFile; + +class ExcelReaderTest { + public static final String HEADER_NAME = "기사명"; + public static final String FIRST_NAME = "홍길동"; + public static final String SECOND_NAME = "김철수"; + public static final String HEADER_PHONE_NUMBER = "전화번호"; + public static final String FIRST_PHONE_NUMBER = "010-1234-5678"; + public static final String SECOND_PHONE_NUMBER = "010-8765-4321"; + private MultipartFile VALID_EXCEL; + private MultipartFile EMPTY_EXCEL; + + @BeforeEach + void setUp() throws IOException { + List testData = Arrays.asList( + new String[]{FIRST_NAME, FIRST_PHONE_NUMBER}, + new String[]{SECOND_NAME, SECOND_PHONE_NUMBER} + ); + + VALID_EXCEL = ExcelTestHelper.createExcelFile(testData); + EMPTY_EXCEL = ExcelTestHelper.createEmptyExcelFile(); + } + + @Test + void readExcel() throws IOException { + Map> actual = ExcelReader.read(VALID_EXCEL); + + Map firstActual = actual.get(1); + assertEquals(FIRST_NAME, firstActual.get(HEADER_NAME)); + assertEquals(FIRST_PHONE_NUMBER, firstActual.get(HEADER_PHONE_NUMBER)); + + Map secondActual = actual.get(2); + assertEquals(SECOND_NAME, secondActual.get(HEADER_NAME)); + assertEquals(SECOND_PHONE_NUMBER, secondActual.get(HEADER_PHONE_NUMBER)); + } + + @Test + void readEmptyExcel() throws IOException { + Map> actual = ExcelReader.read(EMPTY_EXCEL); + + assertTrue(actual.isEmpty()); + } +} \ No newline at end of file diff --git a/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelTestHelper.java b/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelTestHelper.java new file mode 100644 index 0000000..5a74f59 --- /dev/null +++ b/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelTestHelper.java @@ -0,0 +1,70 @@ +package com.tdd.util.excel; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +public class ExcelTestHelper { + public static MultipartFile createEmptyExcelFile() throws IOException { + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = createSheet(workbook); + + setHeaderRow(sheet); + + byte[] bytes = toByteArray(workbook); + + return convertMockMultipartFile(bytes); + } + } + + public static MultipartFile createExcelFile(List data) throws IOException { + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = createSheet(workbook); + + setHeaderRow(sheet); + + for (int i = 0; i < data.size(); i++) { + XSSFRow dataRow = sheet.createRow(i + 1); + String[] rowData = data.get(i); + for (int j = 0; j < rowData.length; j++) { + dataRow.createCell(j).setCellValue(rowData[j]); + } + } + + byte[] bytes = toByteArray(workbook); + + return convertMockMultipartFile(bytes); + } + } + + private static MockMultipartFile convertMockMultipartFile(byte[] bytes) { + return new MockMultipartFile( + "file", + "test.xlsx", + "multipart/form-data", + bytes + ); + } + + private static byte[] toByteArray(XSSFWorkbook workbook) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + workbook.write(bos); + return bos.toByteArray(); + } + + private static XSSFSheet createSheet(XSSFWorkbook workbook) { + return workbook.createSheet("Sheet1"); + } + + private static void setHeaderRow(XSSFSheet sheet) { + XSSFRow headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("기사명"); + headerRow.createCell(1).setCellValue("전화번호"); + } +} diff --git a/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelValidatorTest.java b/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelValidatorTest.java new file mode 100644 index 0000000..4057e60 --- /dev/null +++ b/tdd/excel-uploader/src/test/java/com/tdd/util/excel/ExcelValidatorTest.java @@ -0,0 +1,46 @@ +package com.tdd.util.excel; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.web.multipart.MultipartFile; + +import com.tdd.util.excel.result.ValidationResult; + +class ExcelValidatorTest { + private MultipartFile EMPTY_EXCEL; + + @BeforeEach + void setUp() throws IOException { + EMPTY_EXCEL = ExcelTestHelper.createEmptyExcelFile(); + } + + @Test + void validate_has_valid_and_invalid_results() { + ValidationResult actual = ExcelValidator.validate(EMPTY_EXCEL); + + assertEquals(0, actual.getValidResults()); + assertEquals(0, actual.getInvalidResults()); + } + + @Test + @Disabled + void validate_for_valid_excel() throws IOException { + List testData = Arrays.asList( + new String[]{"홍길동", "010-1234-5678"}, + new String[]{"김철수", "010-8765-4321"} + ); + MultipartFile validTwoRowExcel = ExcelTestHelper.createExcelFile(testData); + + ValidationResult actual = ExcelValidator.validate(validTwoRowExcel); + + assertEquals(2, actual.getValidResults()); + assertEquals(0, actual.getInvalidResults()); + } +} \ No newline at end of file