Skip to content

Commit

Permalink
(WIP) Excel uploader (#64)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
currenjin authored Nov 17, 2024
1 parent bb1f393 commit b9ec080
Show file tree
Hide file tree
Showing 13 changed files with 483 additions and 1 deletion.
1 change: 1 addition & 0 deletions tdd/excel-uploader/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
112 changes: 112 additions & 0 deletions tdd/excel-uploader/excel-uploader.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" version="24.8.6">
<diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">
<mxGraphModel dx="1266" dy="650" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="WIyWlLk6GJQsqaUBKTNV-0" />
<mxCell id="WIyWlLk6GJQsqaUBKTNV-1" parent="WIyWlLk6GJQsqaUBKTNV-0" />
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--17" target="Hc8vjFMk4_dKpQ89hqnR-0" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-6" value="&amp;lt;T&amp;gt; parse(validResult: ValidResult): T" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="Hc8vjFMk4_dKpQ89hqnR-5" vertex="1" connectable="0">
<mxGeometry x="0.6043" y="-1" relative="1" as="geometry">
<mxPoint x="31" y="-21" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="BPdMix6tNrOgbUj1U_3O-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--17" target="BPdMix6tNrOgbUj1U_3O-0">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="BPdMix6tNrOgbUj1U_3O-3" value="read(file: MultipartFile): Map&amp;lt;String, String&amp;gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="BPdMix6tNrOgbUj1U_3O-2">
<mxGeometry x="0.5593" y="2" relative="1" as="geometry">
<mxPoint x="10" y="-18" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="zkfFHV4jXpPFQw0GAbJ--17" value="ExcelUploader" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="50" y="200" width="160" height="60" as="geometry">
<mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="zkfFHV4jXpPFQw0GAbJ--23" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
<mxGeometry y="26" width="160" height="8" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-0" value="ExcelParser" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="630" y="440" width="160" height="60" as="geometry">
<mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-1" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="Hc8vjFMk4_dKpQ89hqnR-0" vertex="1">
<mxGeometry y="26" width="160" height="8" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-2" value="ExcelValidator" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="630" y="200" width="160" height="60" as="geometry">
<mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-3" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="Hc8vjFMk4_dKpQ89hqnR-2" vertex="1">
<mxGeometry y="26" width="160" height="8" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--23" target="Hc8vjFMk4_dKpQ89hqnR-2" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-7" value="validate(file: MultipartFile): ValidatationResult" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="Hc8vjFMk4_dKpQ89hqnR-4" vertex="1" connectable="0">
<mxGeometry x="0.2348" relative="1" as="geometry">
<mxPoint x="41" y="-20" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="Hc8vjFMk4_dKpQ89hqnR-8" target="Hc8vjFMk4_dKpQ89hqnR-12" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-8" value="ValidResult" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="940" y="140" width="160" height="60" as="geometry">
<mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-9" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="Hc8vjFMk4_dKpQ89hqnR-8" vertex="1">
<mxGeometry y="26" width="160" height="8" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-10" value="InvalidResult" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="940" y="290" width="160" height="60" as="geometry">
<mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-11" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="Hc8vjFMk4_dKpQ89hqnR-10" vertex="1">
<mxGeometry y="26" width="160" height="8" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-12" value="&lt;&lt;interface&gt;&gt;&#xa;ExcelResult" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=50;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
<mxGeometry x="1180" y="200" width="160" height="104" as="geometry">
<mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-13" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="Hc8vjFMk4_dKpQ89hqnR-12" vertex="1">
<mxGeometry y="50" width="160" height="44" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="Hc8vjFMk4_dKpQ89hqnR-11" target="Hc8vjFMk4_dKpQ89hqnR-12" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="Hc8vjFMk4_dKpQ89hqnR-3" target="Hc8vjFMk4_dKpQ89hqnR-8" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-22" value="1...n" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="Hc8vjFMk4_dKpQ89hqnR-16" vertex="1" connectable="0">
<mxGeometry x="0.6" y="-2" relative="1" as="geometry">
<mxPoint y="-2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="Hc8vjFMk4_dKpQ89hqnR-3" target="Hc8vjFMk4_dKpQ89hqnR-10" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Hc8vjFMk4_dKpQ89hqnR-23" value="1...n" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="Hc8vjFMk4_dKpQ89hqnR-17" vertex="1" connectable="0">
<mxGeometry x="0.7167" y="4" relative="1" as="geometry">
<mxPoint x="4" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="BPdMix6tNrOgbUj1U_3O-0" value="ExcelReader" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" vertex="1" parent="WIyWlLk6GJQsqaUBKTNV-1">
<mxGeometry x="630" y="10" width="160" height="60" as="geometry">
<mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="BPdMix6tNrOgbUj1U_3O-1" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" vertex="1" parent="BPdMix6tNrOgbUj1U_3O-0">
<mxGeometry y="26" width="160" height="8" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tdd.domain;
package com.tdd;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
35 changes: 35 additions & 0 deletions tdd/excel-uploader/src/main/java/com/tdd/domain/Driver.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Driver, Long> {
}
Original file line number Diff line number Diff line change
@@ -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<Integer, Map<String, String>> read(MultipartFile file) throws IOException {
Map<Integer, Map<String, String>> 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<String, String> 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 "";
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tdd.util.excel.result;

public class ValidationResult {
public int getValidResults() {
return 0;
}

public int getInvalidResults() {
return 0;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
15 changes: 15 additions & 0 deletions tdd/excel-uploader/src/test/java/com/tdd/domain/DriverTest.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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<String[]> 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<Integer, Map<String, String>> actual = ExcelReader.read(VALID_EXCEL);

Map<String, String> firstActual = actual.get(1);
assertEquals(FIRST_NAME, firstActual.get(HEADER_NAME));
assertEquals(FIRST_PHONE_NUMBER, firstActual.get(HEADER_PHONE_NUMBER));

Map<String, String> 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<Integer, Map<String, String>> actual = ExcelReader.read(EMPTY_EXCEL);

assertTrue(actual.isEmpty());
}
}
Loading

0 comments on commit b9ec080

Please sign in to comment.