diff --git a/.gitignore b/.gitignore index da5a4a90..6ebf9ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -39,5 +39,8 @@ out/ ### MAC ### .DS_Store +### rest docs ### +!**/src/main/**/static/docs + ### log ### *.log diff --git a/module-api/build.gradle b/module-api/build.gradle index 803ca447..eda87d95 100644 --- a/module-api/build.gradle +++ b/module-api/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.2.0' id 'io.spring.dependency-management' version '1.1.4' + id "org.asciidoctor.jvm.convert" version "3.3.2" } group = 'com.kernel360' @@ -11,7 +12,12 @@ java { sourceCompatibility = '17' } +ext { + snippetsDir = file('build/generated-snippets') +} + configurations { + asciidoctorExt compileOnly { extendsFrom annotationProcessor } @@ -30,32 +36,63 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //implementation 'org.springframework.boot:spring-boot-starter-security' + // jasypt implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5' + // flyway implementation 'org.flywaydb:flyway-core' + // postgresql runtimeOnly 'org.postgresql:postgresql' + //lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' + // validataion implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.4' + // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + // test - fixture Monkey testImplementation 'net.jqwik:jqwik:1.7.3' testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.0") testImplementation("com.navercorp.fixturemonkey:fixture-monkey-jakarta-validation:1.0.0") + + // rest docs + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' } tasks.named('test') { + outputs.dir snippetsDir useJUnitPlatform() } -tasks.register("prepareKotlinBuildScriptModel") {} +asciidoctor { + dependsOn test + configurations 'asciidoctorExt' + baseDirFollowsSourceFile() + inputs.dir snippetsDir +} + +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') +} + +task copyDocument(type: Copy) { + dependsOn asciidoctor + from file("build/docs/asciidoc") + into file("src/main/resources/static/docs") +} +bootJar { + dependsOn copyDocument +} +tasks.register("prepareKotlinBuildScriptModel") {} \ No newline at end of file diff --git a/module-api/src/docs/asciidoc/index.adoc b/module-api/src/docs/asciidoc/index.adoc new file mode 100644 index 00000000..e1dddb94 --- /dev/null +++ b/module-api/src/docs/asciidoc/index.adoc @@ -0,0 +1,11 @@ += API Document +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + +include::overview.adoc[] +include::sample-api.adoc[] +// FIXME :: 위 라인 1줄은 추후 삭제 예정입니다 \ No newline at end of file diff --git a/module-api/src/docs/asciidoc/overview.adoc b/module-api/src/docs/asciidoc/overview.adoc new file mode 100644 index 00000000..268ce84d --- /dev/null +++ b/module-api/src/docs/asciidoc/overview.adoc @@ -0,0 +1,14 @@ +[[overview]] +== Overview + +[[overview-host]] +=== Domain + +[cols="3,7"] +|=== +| 환경 | Domain +| 개발 서버 +| `http://washpedia.my-project.life` +| 운영 서버 +| +|=== \ No newline at end of file diff --git a/module-api/src/docs/asciidoc/sample-api.adoc b/module-api/src/docs/asciidoc/sample-api.adoc new file mode 100644 index 00000000..63e58ddd --- /dev/null +++ b/module-api/src/docs/asciidoc/sample-api.adoc @@ -0,0 +1,23 @@ +// FIXME :: 삭제 예정 파일입니다 +== 샘플 API + +// [[]] 안에는 a 태그 이름 들어갑니다 (http://localhost:8080/docs/index#공통코드-조회) +[[공통코드-조회]] +=== 공통코드 조회 + +// [Request] 실제로 API 에서 필요한 내용들만 아래 내용을 추가합니다 +==== Request +include::{snippets}/commoncode/get-common-codes/path-parameters.adoc[] +// include::{snippets}/commoncode/get-common-codes/query-parameters.adoc[] +// include::{snippets}/commoncode/get-common-codes/request-fields.adoc[] + +===== HTTP Request 예시 +include::{snippets}/commoncode/get-common-codes/http-request.adoc[] + +// [Response] 실제로 API 에서 필요한 내용들만 아래 내용을 추가합니다 +==== Response +// include::{snippets}/commoncode/get-common-codes/response-fields.adoc[] +include::{snippets}/commoncode/get-common-codes/response-fields-value.adoc[] + +===== HTTP Response 예시 +include::{snippets}/commoncode/get-common-codes/http-response.adoc[] \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/commoncode/controller/CommonCodeController.java b/module-api/src/main/java/com/kernel360/commoncode/controller/CommonCodeController.java index 5af0c9c4..e822e1ff 100644 --- a/module-api/src/main/java/com/kernel360/commoncode/controller/CommonCodeController.java +++ b/module-api/src/main/java/com/kernel360/commoncode/controller/CommonCodeController.java @@ -2,7 +2,10 @@ import com.kernel360.commoncode.service.CommonCodeService; import com.kernel360.commoncode.dto.CommonCodeDto; +import com.kernel360.response.ApiResponse; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -10,6 +13,8 @@ import java.util.List; +import static com.kernel360.commoncode.code.CommonCodeBusinessCode.GET_COMMON_CODE_SUCCESS; + @RestController @RequestMapping("/commoncode") public class CommonCodeController { @@ -26,4 +31,13 @@ public List getCommonCode (@PathVariable String codeName){ return commonCodeService.getCodes(codeName); } + + // FIXME :: 아래 메서드는 추후 삭제 예정입니다 + @GetMapping("/test/{codeName}") + public ResponseEntity getCommonCode_1 (@PathVariable String codeName){ + List codes = commonCodeService.getCodes(codeName); + ApiResponse> response = ApiResponse.of(GET_COMMON_CODE_SUCCESS, codes); + + return new ResponseEntity<>(response, HttpStatus.OK); + } } diff --git a/module-api/src/test/java/com/kernel360/common/ControllerTest.java b/module-api/src/test/java/com/kernel360/common/ControllerTest.java new file mode 100644 index 00000000..9c98722e --- /dev/null +++ b/module-api/src/test/java/com/kernel360/common/ControllerTest.java @@ -0,0 +1,38 @@ +package com.kernel360.common; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kernel360.commoncode.controller.CommonCodeController; +import com.kernel360.commoncode.service.CommonCodeService; +import com.kernel360.member.controller.MemberController; +import com.kernel360.member.service.MemberService; +import com.kernel360.product.controller.ProductController; +import com.kernel360.product.service.ProductService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest({ + CommonCodeController.class, + MemberController.class, + ProductController.class +}) +@AutoConfigureRestDocs +public abstract class ControllerTest { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + protected CommonCodeService commonCodeService; + + @MockBean + protected MemberService memberService; + + @MockBean + protected ProductService productService; +} diff --git a/module-api/src/test/java/com/kernel360/common/utils/RestDocumentUtils.java b/module-api/src/test/java/com/kernel360/common/utils/RestDocumentUtils.java new file mode 100644 index 00000000..2e03072b --- /dev/null +++ b/module-api/src/test/java/com/kernel360/common/utils/RestDocumentUtils.java @@ -0,0 +1,19 @@ +package com.kernel360.common.utils; + +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; + +public interface RestDocumentUtils { + + static OperationRequestPreprocessor getDocumentRequest() { + return preprocessRequest(modifyUris().scheme("http") + .host("washpedia.my-project.life") + .removePort(), prettyPrint()); + } + + static OperationResponsePreprocessor getDocumentResponse() { + return preprocessResponse(prettyPrint()); + } +} diff --git a/module-api/src/test/java/com/kernel360/commoncode/controller/CommonCodeControllerRestDocsTest.java b/module-api/src/test/java/com/kernel360/commoncode/controller/CommonCodeControllerRestDocsTest.java new file mode 100644 index 00000000..eb506fd7 --- /dev/null +++ b/module-api/src/test/java/com/kernel360/commoncode/controller/CommonCodeControllerRestDocsTest.java @@ -0,0 +1,100 @@ +package com.kernel360.commoncode.controller; + +import com.kernel360.common.ControllerTest; +import com.kernel360.commoncode.dto.CommonCodeDto; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; + +import static com.kernel360.common.utils.RestDocumentUtils.getDocumentRequest; +import static com.kernel360.common.utils.RestDocumentUtils.getDocumentResponse; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +// FIXME :: 삭제 예정 파일입니다 +class CommonCodeControllerRestDocsTest extends ControllerTest { + + @Test + void commmonCodeSearch() throws Exception { + //given + List responseList = Arrays.asList( + CommonCodeDto.of( + 11L, + "Sedan", + 10, + "cartype", + 1, + true, + "세단", + LocalDate.now(), + "admin", + null, + null), + CommonCodeDto.of( + 12L, + "Hatchback", + 10, + "cartype", + 2, + true, + "해치백", + LocalDate.now(), + "admin", + null, + null) + ); + + String pathVariable = "color"; + given(commonCodeService.getCodes(pathVariable)).willReturn(responseList); + + + //when + ResultActions result = this.mockMvc.perform( + get("/commoncode/test/{codeName}", pathVariable)); + + + //then + //pathParameters, pathParameters, requestFields, responseFields는 필요 시 각각 작성 + result.andExpect(status().isOk()) + .andDo(document( + "commoncode/get-common-codes", + getDocumentRequest(), + getDocumentResponse(), + pathParameters( + parameterWithName("codeName").description("코드명") + ), +// queryParameters( +// parameterWithName("size").description("size").optional(), +// parameterWithName("page").description("page").optional() +// ), +// requestFields( +// fieldWithPath("codeName").type(JsonFieldType.STRING).description("코드명"), +// fieldWithPath("upperName").type(JsonFieldType.STRING).description("상위 코드명").optional() +// ), +// responseFields( +// fieldWithPath("codeNo").type(JsonFieldType.NUMBER).description("코드번호"), +// ) + responseFields(beneathPath("value").withSubsectionId("value"), + fieldWithPath("codeNo").type(JsonFieldType.NUMBER).description("코드번호"), + fieldWithPath("codeName").type(JsonFieldType.STRING).description("코드명"), + fieldWithPath("upperNo").type(JsonFieldType.NUMBER).description("상위 코드번호").optional(), + fieldWithPath("upperName").type(JsonFieldType.STRING).description("상위 코드명").optional(), + fieldWithPath("sortOrder").type(JsonFieldType.NUMBER).description("정렬순서"), + fieldWithPath("isUsed").type(JsonFieldType.BOOLEAN).description("사용여부"), + fieldWithPath("description").type(JsonFieldType.STRING).description("설명"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("생성일시"), + fieldWithPath("createdBy").type(JsonFieldType.STRING).description("생성자"), + fieldWithPath("modifiedAt").type(JsonFieldType.STRING).description("수정일시").optional(), + fieldWithPath("modifiedBy").type(JsonFieldType.STRING).description("수정자").optional() + ) + )); + } +} \ No newline at end of file diff --git a/module-api/src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet b/module-api/src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet new file mode 100644 index 00000000..c985e678 --- /dev/null +++ b/module-api/src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet @@ -0,0 +1,14 @@ +===== End Point +{{path}} + +===== Path Parameters +|=== + +|파라미터|설명 + +{{#parameters}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/parameters}} + +|=== \ No newline at end of file diff --git a/module-api/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet b/module-api/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet new file mode 100644 index 00000000..c3f7621e --- /dev/null +++ b/module-api/src/test/resources/org/springframework/restdocs/templates/query-parameters.snippet @@ -0,0 +1,12 @@ +===== Query Parameters +|=== + +|파라미터|필수값|설명 + +{{#parameters}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/parameters}} + +|=== \ No newline at end of file diff --git a/module-api/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/module-api/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet new file mode 100644 index 00000000..af391640 --- /dev/null +++ b/module-api/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet @@ -0,0 +1,13 @@ +===== Request Fields +|=== + +|필드명|타입|필수값|설명 + +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/fields}} + +|=== \ No newline at end of file diff --git a/module-api/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet b/module-api/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet new file mode 100644 index 00000000..8ad4694c --- /dev/null +++ b/module-api/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet @@ -0,0 +1,12 @@ +===== Response Fields +|=== + +|필드명|타입|설명 + +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/fields}} + +|=== \ No newline at end of file