diff --git a/src/main/java/uno/fastcampus/testdata/controller/TableSchemaController.java b/src/main/java/uno/fastcampus/testdata/controller/TableSchemaController.java index 02be04e..5ee52ad 100644 --- a/src/main/java/uno/fastcampus/testdata/controller/TableSchemaController.java +++ b/src/main/java/uno/fastcampus/testdata/controller/TableSchemaController.java @@ -21,6 +21,7 @@ import uno.fastcampus.testdata.dto.response.SimpleTableSchemaResponse; import uno.fastcampus.testdata.dto.response.TableSchemaResponse; import uno.fastcampus.testdata.dto.security.GithubUser; +import uno.fastcampus.testdata.service.SchemaExportService; import uno.fastcampus.testdata.service.TableSchemaService; import java.util.Arrays; @@ -31,6 +32,7 @@ public class TableSchemaController { private final TableSchemaService tableSchemaService; + private final SchemaExportService schemaExportService; private final ObjectMapper mapper; @GetMapping("/table-schema") @@ -89,12 +91,20 @@ public String deleteMySchema( } @GetMapping("/table-schema/export") - public ResponseEntity exportTableSchema(TableSchemaExportRequest tableSchemaExportRequest) { + public ResponseEntity exportTableSchema( + @AuthenticationPrincipal GithubUser githubUser, + TableSchemaExportRequest tableSchemaExportRequest + ) { + String body = schemaExportService.export( + tableSchemaExportRequest.getFileType(), + tableSchemaExportRequest.toDto(githubUser != null ? githubUser.id() : null), + tableSchemaExportRequest.getRowCount() + ); String filename = tableSchemaExportRequest.getSchemaName() + "." + tableSchemaExportRequest.getFileType().name().toLowerCase(); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename) - .body(json(tableSchemaExportRequest)); // TODO: 나중에 데이터 바꿔야 함 + .body(body); } @@ -111,12 +121,4 @@ private TableSchemaResponse defaultTableSchema(String schemaName) { ); } - private String json(Object object) { - try { - return mapper.writeValueAsString(object); - } catch (JsonProcessingException jpe) { - throw new RuntimeException(jpe); - } - } - } diff --git a/src/main/java/uno/fastcampus/testdata/domain/constant/ExportFileType.java b/src/main/java/uno/fastcampus/testdata/domain/constant/ExportFileType.java index f525fa9..0918599 100644 --- a/src/main/java/uno/fastcampus/testdata/domain/constant/ExportFileType.java +++ b/src/main/java/uno/fastcampus/testdata/domain/constant/ExportFileType.java @@ -1,5 +1,9 @@ package uno.fastcampus.testdata.domain.constant; public enum ExportFileType { - CSV, TSV, JSON, SQL_INSERT; + CSV, + TSV, +// JSON, // TODO: 강의 시간 상 구현을 생략함. 구현하면 TODO 삭제 +// SQL_INSERT, // TODO: 강의 시간 상 구현을 생략함. 구현하면 TODO 삭제 + ; } diff --git a/src/main/java/uno/fastcampus/testdata/service/SchemaExportService.java b/src/main/java/uno/fastcampus/testdata/service/SchemaExportService.java new file mode 100644 index 0000000..d47adcd --- /dev/null +++ b/src/main/java/uno/fastcampus/testdata/service/SchemaExportService.java @@ -0,0 +1,27 @@ +package uno.fastcampus.testdata.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import uno.fastcampus.testdata.domain.TableSchema; +import uno.fastcampus.testdata.domain.constant.ExportFileType; +import uno.fastcampus.testdata.dto.TableSchemaDto; +import uno.fastcampus.testdata.repository.TableSchemaRepository; +import uno.fastcampus.testdata.service.exporter.MockDataFileExporterContext; + +@RequiredArgsConstructor +@Service +public class SchemaExportService { + + private final MockDataFileExporterContext mockDataFileExporterContext; + private final TableSchemaRepository tableSchemaRepository; + + public String export(ExportFileType fileType, TableSchemaDto dto, Integer rowCount) { + if (dto.userId() != null) { + tableSchemaRepository.findByUserIdAndSchemaName(dto.userId(), dto.schemaName()) + .ifPresent(TableSchema::markExported); + } + + return mockDataFileExporterContext.export(fileType, dto, rowCount); + } + +} diff --git a/src/main/java/uno/fastcampus/testdata/service/exporter/CSVFileExporter.java b/src/main/java/uno/fastcampus/testdata/service/exporter/CSVFileExporter.java new file mode 100644 index 0000000..19a37fe --- /dev/null +++ b/src/main/java/uno/fastcampus/testdata/service/exporter/CSVFileExporter.java @@ -0,0 +1,19 @@ +package uno.fastcampus.testdata.service.exporter; + +import org.springframework.stereotype.Component; +import uno.fastcampus.testdata.domain.constant.ExportFileType; + +@Component +public class CSVFileExporter extends DelimiterBasedFileExporter implements MockDataFileExporter { + + @Override + public String getDelimiter() { + return ","; + } + + @Override + public ExportFileType getType() { + return ExportFileType.CSV; + } + +} diff --git a/src/main/java/uno/fastcampus/testdata/service/exporter/DelimiterBasedFileExporter.java b/src/main/java/uno/fastcampus/testdata/service/exporter/DelimiterBasedFileExporter.java new file mode 100644 index 0000000..452657a --- /dev/null +++ b/src/main/java/uno/fastcampus/testdata/service/exporter/DelimiterBasedFileExporter.java @@ -0,0 +1,45 @@ +package uno.fastcampus.testdata.service.exporter; + +import uno.fastcampus.testdata.dto.SchemaFieldDto; +import uno.fastcampus.testdata.dto.TableSchemaDto; + +import java.util.Comparator; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public abstract class DelimiterBasedFileExporter implements MockDataFileExporter { + + /** + * 파일 열 구분자로 사용할 문자열을 반환한다. + * + * @return 파일 열 구분자 + */ + public abstract String getDelimiter(); + + @Override + public String export(TableSchemaDto dto, Integer rowCount) { + StringBuilder sb = new StringBuilder(); + + // 헤더 만들기 + sb.append(dto.schemaFields().stream() + .sorted(Comparator.comparing(SchemaFieldDto::fieldOrder)) + .map(SchemaFieldDto::fieldName) + .collect(Collectors.joining(getDelimiter())) + ); + sb.append("\n"); + + // 데이터 부분 + IntStream.range(0, rowCount).forEach(i -> { + sb.append(dto.schemaFields().stream() + .sorted(Comparator.comparing(SchemaFieldDto::fieldOrder)) + .map(field -> "가짜-데이터") // TODO: 구현할 것 + .map(v -> v == null ? "" : v) + .collect(Collectors.joining(getDelimiter())) + ); + sb.append("\n"); + }); + + return sb.toString(); + } + +} diff --git a/src/main/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporter.java b/src/main/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporter.java new file mode 100644 index 0000000..cfde10e --- /dev/null +++ b/src/main/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporter.java @@ -0,0 +1,27 @@ +package uno.fastcampus.testdata.service.exporter; + +import uno.fastcampus.testdata.domain.constant.ExportFileType; +import uno.fastcampus.testdata.dto.TableSchemaDto; + +/** + * 특정 파일 유형({@link ExportFileType})에 맞는 데이터 출력 인터페이스. + */ +public interface MockDataFileExporter { + + /** + * 구현체가 처리하는 출력 파일 유형을 반환하는 메소드. + * + * @return 이 구현체가 다루는 출력 파일 유형 + */ + ExportFileType getType(); + + /** + * 파일 형식에 맞는 문자열 데이터를 출력하는 메소드. + * + * @param dto 출력 데이터의 테이블 스키마 정보 + * @param rowCount 출력할 데이터 행 수 + * @return 적절한 파일 형식으로 변환된 문자열 데이터 + */ + String export(TableSchemaDto dto, Integer rowCount); + +} diff --git a/src/main/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporterContext.java b/src/main/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporterContext.java new file mode 100644 index 0000000..0e631f3 --- /dev/null +++ b/src/main/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporterContext.java @@ -0,0 +1,28 @@ +package uno.fastcampus.testdata.service.exporter; + +import org.springframework.stereotype.Service; +import uno.fastcampus.testdata.domain.constant.ExportFileType; +import uno.fastcampus.testdata.dto.TableSchemaDto; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class MockDataFileExporterContext { + + private final Map mockDataFileExporterMap; + + public MockDataFileExporterContext(List mockDataFileExporters) { + this.mockDataFileExporterMap = mockDataFileExporters.stream() + .collect(Collectors.toMap(MockDataFileExporter::getType, Function.identity())); + } + + public String export(ExportFileType fileType, TableSchemaDto dto, Integer rowCount) { + MockDataFileExporter fileExporter = mockDataFileExporterMap.get(fileType); + + return fileExporter.export(dto, rowCount); + } + +} diff --git a/src/main/java/uno/fastcampus/testdata/service/exporter/TSVFileExporter.java b/src/main/java/uno/fastcampus/testdata/service/exporter/TSVFileExporter.java new file mode 100644 index 0000000..d37bfc9 --- /dev/null +++ b/src/main/java/uno/fastcampus/testdata/service/exporter/TSVFileExporter.java @@ -0,0 +1,19 @@ +package uno.fastcampus.testdata.service.exporter; + +import org.springframework.stereotype.Component; +import uno.fastcampus.testdata.domain.constant.ExportFileType; + +@Component +public class TSVFileExporter extends DelimiterBasedFileExporter implements MockDataFileExporter { + + @Override + public String getDelimiter() { + return "\t"; + } + + @Override + public ExportFileType getType() { + return ExportFileType.TSV; + } + +} diff --git a/src/test/java/uno/fastcampus/testdata/controller/TableSchemaControllerTest.java b/src/test/java/uno/fastcampus/testdata/controller/TableSchemaControllerTest.java index 15ede75..e1730c7 100644 --- a/src/test/java/uno/fastcampus/testdata/controller/TableSchemaControllerTest.java +++ b/src/test/java/uno/fastcampus/testdata/controller/TableSchemaControllerTest.java @@ -18,6 +18,7 @@ import uno.fastcampus.testdata.dto.request.TableSchemaExportRequest; import uno.fastcampus.testdata.dto.request.TableSchemaRequest; import uno.fastcampus.testdata.dto.security.GithubUser; +import uno.fastcampus.testdata.service.SchemaExportService; import uno.fastcampus.testdata.service.TableSchemaService; import uno.fastcampus.testdata.util.FormDataEncoder; @@ -42,6 +43,7 @@ class TableSchemaControllerTest { @Autowired private ObjectMapper mapper; @MockBean private TableSchemaService tableSchemaService; + @MockBean private SchemaExportService schemaExportService; @DisplayName("[GET] 테이블 스키마 조회, 비로그인 최초 진입 (정상)") @Test @@ -162,7 +164,7 @@ void givenAuthenticatedUserAndSchemaName_whenDeleting_thenRedirectsToTableSchema then(tableSchemaService).should().deleteTableSchema(githubUser.id(), schemaName); } - @DisplayName("[GET] 테이블 스키마 파일 다운로드 (정상)") + @DisplayName("[GET] 테이블 스키마 파일 다운로드, 비로그인 (정상)") @Test void givenTableSchema_whenDownloading_thenReturnsFile() throws Exception { // Given @@ -177,13 +179,49 @@ void givenTableSchema_whenDownloading_thenReturnsFile() throws Exception { ) ); String queryParam = formDataEncoder.encode(request, false); + String expectedBody = "id,name,age\n1,uno,20"; + given(schemaExportService.export(request.getFileType(), request.toDto(null), request.getRowCount())) + .willReturn(expectedBody); // When & Then mvc.perform(get("/table-schema/export?" + queryParam)) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_PLAIN)) .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=test.csv")) - .andExpect(content().json(mapper.writeValueAsString(request))); // TODO: 나중에 데이터 바꿔야 함 + .andExpect(content().string(expectedBody)); + then(schemaExportService).should().export(request.getFileType(), request.toDto(null), request.getRowCount()); + } + + @DisplayName("[GET] 테이블 스키마 파일 다운로드, 로그인 (정상)") + @Test + void givenAuthenticatedUserAndTableSchema_whenDownloading_thenReturnsFile() throws Exception { + // Given + var githubUser = new GithubUser("test-id", "test-name", "test@email.com"); + TableSchemaExportRequest request = TableSchemaExportRequest.of( + "test", + 77, + ExportFileType.CSV, + List.of( + SchemaFieldRequest.of("id", MockDataType.ROW_NUMBER, 1, 0, null, null), + SchemaFieldRequest.of("name", MockDataType.STRING, 1, 0, "option", "well"), + SchemaFieldRequest.of("age", MockDataType.NUMBER, 3, 20, null, null) + ) + ); + String queryParam = formDataEncoder.encode(request, false); + String expectedBody = "id,name,age\n1,uno,20"; + given(schemaExportService.export(request.getFileType(), request.toDto(githubUser.id()), request.getRowCount())) + .willReturn(expectedBody); + + // When & Then + mvc.perform( + get("/table-schema/export?" + queryParam) + .with(oauth2Login().oauth2User(githubUser)) + ) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_PLAIN)) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=test.csv")) + .andExpect(content().string(expectedBody)); + then(schemaExportService).should().export(request.getFileType(), request.toDto(githubUser.id()), request.getRowCount()); } } diff --git a/src/test/java/uno/fastcampus/testdata/service/SchemaExportServiceTest.java b/src/test/java/uno/fastcampus/testdata/service/SchemaExportServiceTest.java new file mode 100644 index 0000000..346218d --- /dev/null +++ b/src/test/java/uno/fastcampus/testdata/service/SchemaExportServiceTest.java @@ -0,0 +1,71 @@ +package uno.fastcampus.testdata.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uno.fastcampus.testdata.domain.TableSchema; +import uno.fastcampus.testdata.domain.constant.ExportFileType; +import uno.fastcampus.testdata.dto.TableSchemaDto; +import uno.fastcampus.testdata.repository.TableSchemaRepository; +import uno.fastcampus.testdata.service.exporter.MockDataFileExporterContext; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +@DisplayName("[Service] 스키마 파일 출력 서비스 테스트") +@ExtendWith(MockitoExtension.class) +class SchemaExportServiceTest { + + @InjectMocks private SchemaExportService sut; + + @Mock private MockDataFileExporterContext mockDataFileExporterContext; + @Mock private TableSchemaRepository tableSchemaRepository; + + @DisplayName("출력 파일 유형과 스키마 정보와 행 수가 주어지면, 엔티티 출력 여부를 마킹하고 알맞은 파일 유형으로 가짜 테이터 파일을 반환한다.") + @Test + void givenFileTypeAndSchemaAndRowCount_whenExporting_thenMarksEntityExportedAndReturnsFileFormattedString() { + // Given + ExportFileType exportFileType = ExportFileType.CSV; + TableSchemaDto dto = TableSchemaDto.of("test_schema", "uno", null, null); + int rowCount = 5; + TableSchema exectedTableSchema = TableSchema.of(dto.schemaName(), dto.userId()); + given(tableSchemaRepository.findByUserIdAndSchemaName(dto.userId(), dto.schemaName())) + .willReturn(Optional.of(exectedTableSchema)); + given(mockDataFileExporterContext.export(exportFileType, dto, rowCount)).willReturn("test,file,format"); + + // When + String result = sut.export(exportFileType, dto, rowCount); + + // Then + assertThat(result).isEqualTo("test,file,format"); + assertThat(exectedTableSchema.isExported()).isTrue(); + then(tableSchemaRepository).should().findByUserIdAndSchemaName(dto.userId(), dto.schemaName()); + then(mockDataFileExporterContext).should().export(exportFileType, dto, rowCount); + } + + @DisplayName("입력 파라미터 중에 유저 식별 정보가 없으면, 스키마 테이블 조회를 시도하지 않는다.") + @Test + void givenNoUserIdInParams_whenExporting_thenDoesNotTrySelectingTableSchema() { + // Given + ExportFileType exportFileType = ExportFileType.CSV; + TableSchemaDto dto = TableSchemaDto.of("test_schema", null, null, null); + int rowCount = 5; + given(mockDataFileExporterContext.export(exportFileType, dto, rowCount)).willReturn("test,file,format"); + + // When + String result = sut.export(exportFileType, dto, rowCount); + + // Then + assertThat(result).isEqualTo("test,file,format"); + then(tableSchemaRepository).shouldHaveNoInteractions(); + then(mockDataFileExporterContext).should().export(exportFileType, dto, rowCount); + } + +} diff --git a/src/test/java/uno/fastcampus/testdata/service/exporter/CSVFileExporterTest.java b/src/test/java/uno/fastcampus/testdata/service/exporter/CSVFileExporterTest.java new file mode 100644 index 0000000..bc9558d --- /dev/null +++ b/src/test/java/uno/fastcampus/testdata/service/exporter/CSVFileExporterTest.java @@ -0,0 +1,45 @@ +package uno.fastcampus.testdata.service.exporter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uno.fastcampus.testdata.domain.constant.MockDataType; +import uno.fastcampus.testdata.dto.SchemaFieldDto; +import uno.fastcampus.testdata.dto.TableSchemaDto; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("[Logic] CSV 파일 출력기 테스트") +class CSVFileExporterTest { + + private CSVFileExporter sut = new CSVFileExporter(); + + + @DisplayName("테이블 스키마 정보와 행 수가 주어지면, CSV 형식의 문자열을 생성한다.") + @Test + void givenSchemaAndRowCount_whenExporting_thenReturnsCSVFormattedString() { + // Given + TableSchemaDto dto = TableSchemaDto.of( + "test_schema", + "uno", + null, + Set.of( + SchemaFieldDto.of("id", MockDataType.ROW_NUMBER, 1, 0, null, null), + SchemaFieldDto.of("name", MockDataType.NAME, 2, 0, null, null), + SchemaFieldDto.of("created_at", MockDataType.DATETIME, 5, 0, null, null), + SchemaFieldDto.of("age", MockDataType.NUMBER, 3, 0, null, null), + SchemaFieldDto.of("car", MockDataType.CAR, 4, 0, null, null) + ) + ); + int rowCount = 10; + + // When + String result = sut.export(dto, rowCount); + + // Then + System.out.println(result); // 관찰용 + assertThat(result).startsWith("id,name,age,car,created_at"); + } + +} \ No newline at end of file diff --git a/src/test/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporterContextTest.java b/src/test/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporterContextTest.java new file mode 100644 index 0000000..dc4ae87 --- /dev/null +++ b/src/test/java/uno/fastcampus/testdata/service/exporter/MockDataFileExporterContextTest.java @@ -0,0 +1,49 @@ +package uno.fastcampus.testdata.service.exporter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import uno.fastcampus.testdata.domain.constant.ExportFileType; +import uno.fastcampus.testdata.domain.constant.MockDataType; +import uno.fastcampus.testdata.dto.SchemaFieldDto; +import uno.fastcampus.testdata.dto.TableSchemaDto; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@DisplayName("[IntegrationTest] 파일 출력기 컨텍스트 테스트") +@SpringBootTest +record MockDataFileExporterContextTest(@Autowired MockDataFileExporterContext sut) { + + @DisplayName("파일 형식과 테이블 스키마와 행 수가 주어지면, 파일 형식에 맞게 변환한 문자열을 리턴한다.") + @Test + void givenFileTypeAndTableSchemaAndRowCount_whenExporting_thenReturnsFileFormattedString() { + // Given + ExportFileType exportFileType = ExportFileType.CSV; + TableSchemaDto dto = TableSchemaDto.of( + "test_schema", + "uno", + null, + Set.of( + SchemaFieldDto.of("id", MockDataType.ROW_NUMBER, 1, 0, null, null), + SchemaFieldDto.of("name", MockDataType.NAME, 2, 0, null, null), + SchemaFieldDto.of("age", MockDataType.NUMBER, 3, 0, null, null), + SchemaFieldDto.of("car", MockDataType.CAR, 4, 0, null, null), + SchemaFieldDto.of("created_at", MockDataType.DATETIME, 5, 0, null, null) + ) + ); + int rowCount = 10; + + // When + String result = sut.export(exportFileType, dto, rowCount); + + // Then + System.out.println(result); // 관찰용 + assertThat(result).startsWith("id,name,age,car,created_at"); + } + +} diff --git a/src/test/java/uno/fastcampus/testdata/service/exporter/TSVFileExporterTest.java b/src/test/java/uno/fastcampus/testdata/service/exporter/TSVFileExporterTest.java new file mode 100644 index 0000000..678f619 --- /dev/null +++ b/src/test/java/uno/fastcampus/testdata/service/exporter/TSVFileExporterTest.java @@ -0,0 +1,46 @@ +package uno.fastcampus.testdata.service.exporter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import uno.fastcampus.testdata.domain.constant.MockDataType; +import uno.fastcampus.testdata.dto.SchemaFieldDto; +import uno.fastcampus.testdata.dto.TableSchemaDto; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("[Logic] TSV 파일 출력기 테스트") +class TSVFileExporterTest { + + private TSVFileExporter sut = new TSVFileExporter(); + + + @DisplayName("테이블 스키마 정보와 행 수가 주어지면, TSV 형식의 문자열을 생성한다.") + @Test + void givenSchemaAndRowCount_whenExporting_thenReturnsTSVFormattedString() { + // Given + TableSchemaDto dto = TableSchemaDto.of( + "test_schema", + "uno", + null, + Set.of( + SchemaFieldDto.of("id", MockDataType.ROW_NUMBER, 1, 0, null, null), + SchemaFieldDto.of("name", MockDataType.NAME, 2, 0, null, null), + SchemaFieldDto.of("created_at", MockDataType.DATETIME, 5, 0, null, null), + SchemaFieldDto.of("age", MockDataType.NUMBER, 3, 0, null, null), + SchemaFieldDto.of("car", MockDataType.CAR, 4, 0, null, null) + ) + ); + int rowCount = 10; + + // When + String result = sut.export(dto, rowCount); + + // Then + System.out.println(result); // 관찰용 + assertThat(result).startsWith("id\tname\tage\tcar\tcreated_at"); + } + +}