├── .github
├── CODEOWNERS
└── pull_request_template.md
├── .gitignore
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── readme-img
├── spring-rest-docs-example.png
├── spring-rest-docs-openapi-example.png
└── springdocs-example.png
├── settings.gradle
├── src
├── docs
│ └── asciidoc
│ │ ├── index.adoc
│ │ ├── order.adoc
│ │ └── product.adoc
├── main
│ ├── java
│ │ └── com
│ │ │ └── kurly
│ │ │ └── tet
│ │ │ └── guide
│ │ │ └── springrestdocs
│ │ │ ├── Application.java
│ │ │ ├── application
│ │ │ ├── order
│ │ │ │ └── OrderFacade.java
│ │ │ └── product
│ │ │ │ └── ProductFacade.java
│ │ │ ├── common
│ │ │ ├── exception
│ │ │ │ └── BusinessException.java
│ │ │ └── util
│ │ │ │ ├── JsonUtils.java
│ │ │ │ ├── LocalDateTimeUtils.java
│ │ │ │ ├── LocalDateUtils.java
│ │ │ │ └── LocalTimeUtils.java
│ │ │ ├── config
│ │ │ ├── Constant.java
│ │ │ └── SpringDocOpenApiConfig.java
│ │ │ ├── domain
│ │ │ ├── BusinessCode.java
│ │ │ ├── DescriptionEnum.java
│ │ │ ├── exception
│ │ │ │ ├── OrderNotFoundException.java
│ │ │ │ └── ProductNotFoundException.java
│ │ │ ├── order
│ │ │ │ ├── OrderDto.java
│ │ │ │ └── OrderStatus.java
│ │ │ └── product
│ │ │ │ ├── ProductDto.java
│ │ │ │ └── ProductStatus.java
│ │ │ └── infrastructure
│ │ │ ├── package-info.java
│ │ │ └── web
│ │ │ ├── common
│ │ │ ├── advisor
│ │ │ │ ├── ApiResponseJsonProcessingException.java
│ │ │ │ └── ApiResponseWrappingAdvisor.java
│ │ │ ├── component
│ │ │ │ ├── LocalDateJsonComponent.java
│ │ │ │ ├── LocalDateTimeJsonComponent.java
│ │ │ │ └── LocalTimeJsonComponent.java
│ │ │ ├── dto
│ │ │ │ ├── ApiResponse.java
│ │ │ │ ├── ApiResponseGenerator.java
│ │ │ │ ├── PageRequest.java
│ │ │ │ └── PageResponse.java
│ │ │ └── handler
│ │ │ │ └── GlobalRestControllerExceptionHandler.java
│ │ │ ├── order
│ │ │ ├── OrderRestController.java
│ │ │ ├── OrderSearchCondition.java
│ │ │ └── command
│ │ │ │ ├── OrderCreateCommand.java
│ │ │ │ └── OrderPaymentCommand.java
│ │ │ ├── package-info.java
│ │ │ └── product
│ │ │ ├── ProductCreateCommand.java
│ │ │ ├── ProductModifyCommand.java
│ │ │ ├── ProductRestController.java
│ │ │ └── ProductSearchCondition.java
│ └── resources
│ │ ├── application-springdoc.yml
│ │ └── application.yml
└── test
│ ├── java
│ └── com
│ │ └── kurly
│ │ └── tet
│ │ └── guide
│ │ └── springrestdocs
│ │ ├── ApplicationTests.java
│ │ ├── annotations
│ │ ├── MockTest.java
│ │ └── RestDocsTest.java
│ │ ├── application
│ │ └── order
│ │ │ └── OrderFacadeTest.java
│ │ ├── documenation
│ │ ├── DocumentFormatGenerator.java
│ │ ├── DocumentFormatGeneratorTest.java
│ │ ├── DocumentUtils.java
│ │ └── MockMvcFactory.java
│ │ └── infrastructure
│ │ └── web
│ │ ├── dto
│ │ └── PageResponseTest.java
│ │ ├── order
│ │ ├── OrderRestControllerDocsTest.java
│ │ └── OrderRestControllerTest.java
│ │ └── product
│ │ ├── ProductRestControllerDocsTest.java
│ │ └── ProductRestControllerTest.java
│ └── resources
│ ├── application.yml
│ └── org
│ └── springframework
│ └── restdocs
│ └── templates
│ └── asciidoctor
│ ├── README.md
│ ├── curl-request.snippet
│ ├── http-request.snippet
│ ├── http-response.snippet
│ ├── httpie-request.snippet
│ ├── request-fields.snippet
│ ├── request-parameters.snippet
│ └── response-fields.snippet
└── swagger-ui
├── favicon-16x16.png
├── favicon-32x32.png
├── index.css
├── swagger-initializer.js
├── swagger-ui-bundle.js
├── swagger-ui-bundle.js.map
├── swagger-ui-standalone-preset.js
├── swagger-ui-standalone-preset.js.map
├── swagger-ui.css
├── swagger-ui.css.map
├── swagger-ui.html
├── swagger-ui.js
└── swagger-ui.js.map
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | #
2 | # 코드소유자(Code Owner) 정보
3 | # @see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
4 | # 코드 변경 후 Pull Request(PR)이 생성되면 자동으로 코드리뷰요청을 받게되는 사람을 기재합니다.
5 | # CODEOWNERS 에 기재된 사람에게 자동으로 코드리뷰가 요청됩니다.
6 | #
7 |
8 | # Global owners: Repository 내 모든 것에 소유권한을 가지고 있는 사람
9 | * @honeymon-enterprise
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | ## 작업개요
3 |
4 |
5 | * see https://kurly0521.atlassian.net/browse/{Jira Project}-
6 |
7 | ## 작업내용
8 | *
9 |
10 | ## 코멘트
11 | *
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 | .envrc
8 |
9 | ### STS ###
10 | .apt_generated
11 | .classpath
12 | .factorypath
13 | .project
14 | .settings
15 | .springBeans
16 | .sts4-cache
17 | bin/
18 | !**/src/main/**/bin/
19 | !**/src/test/**/bin/
20 |
21 | ### IntelliJ IDEA ###
22 | .idea
23 | *.iws
24 | *.iml
25 | *.ipr
26 | out/
27 | !**/src/main/**/out/
28 | !**/src/test/**/out/
29 | BOOT-INF
30 |
31 | ### NetBeans ###
32 | /nbproject/private/
33 | /nbbuild/
34 | /dist/
35 | /nbdist/
36 | /.nb-gradle/
37 |
38 | ### VS Code ###
39 | .vscode/
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Spring Rest Docs 사용 가이드
2 | ==================================
3 |
4 |
5 |
6 |
7 |
8 | 목차(Table of Contents)
9 |
10 |
11 | 프로젝트 소개
12 |
13 | 시작하기
14 |
18 |
19 | 부록
20 |
21 |
22 |
23 |
24 | ## 프로젝트 소개
25 | 'Spring REST Docs 가이드'를 위한 **예제프로젝트** 입니다.
26 |
27 | ['Spring Boot'][url-spring-boot]를 기반으로 하고 있으며 간단하게 ["Spring REST Docs"](url-spring-rest-docs-project) 와 ["Swagger-UI"](url-swagger-ui) 를 활용한 API문서를 제공합니다.
28 |
29 |
(최상단 이동 )
30 |
31 |
32 | ## 시작하기
33 |
34 | 프로젝트를 로컬에서 시작하려면 다음 안내를 따라주시면 됩니다.
35 |
36 | ### 요구사항
37 | * Java 17 이상
38 | * Java 설치방법: [여러 개의 JDK를 설치하고 선택해서 사용하기](https://blog.benelog.net/installing-jdk.html)
39 |
40 | ### 설치
41 |
42 | 1. 리포지토리 복제(Clone the repo)
43 | ```sh
44 | git clone git@github.com:thefarmersfront/spring-rest-docs-guide.git
45 | cd spring-rest-docs-guide
46 | ```
47 |
48 | 2. 프로젝트 구성하기(Build project)
49 | ```sh
50 | ./gradlew clean build
51 | ```
52 |
53 | 3. API문서생성
54 | ```sh
55 | ./gradlew clean restDocsTest
56 | ```
57 |
58 | 4. 생성문서 확인
59 | 1. Spring REST Docs: `build/docs/index.html`
60 | 2. SwaggerUI: `api-spec/openapi3.yaml`
61 |
62 | 5. 애플리케이션 실행
63 | ```sh
64 | ./gradlew apiBuild
65 | cd build/libs
66 | java -jar application.jar
67 | ```
68 |
69 | ### 확인
70 | #### Spring REST Docs
71 | [http://localhost:8080/docs/index.html]()
72 | > 
73 |
74 | #### Spring REST Docs - OpenAPI Specification Integration
75 | [http://localhost:8080/swagger/swagger-ui.html]()
76 | > 
77 |
78 | #### Springdoc
79 | [http://localhost:8080/swagger-ui/index.html]()
80 | > 
81 |
82 |
83 | (최상단 이동 )
84 |
85 |
86 | ## 부록
87 |
88 | * [여러 개의 JDK를 설치하고 선택해서 사용하기](https://blog.benelog.net/installing-jdk.html)
89 | * [Spring REST Docs](https://spring.io/projects/spring-restdocs)
90 | * [Github - Spring REST Docs](https://github.com/spring-projects/spring-restdocs)
91 | * [Spring REST Docs - Reference Documentation](https://docs.spring.io/spring-restdocs/docs/current/reference/html5/)
92 | * [Asciidoctor](https://asciidoctor.org/)
93 | * [Gradle - org.asciidoctor.jvm.convert`](https://plugins.gradle.org/plugin/org.asciidoctor.jvm.convert)
94 | * [Springdoc - Openapi](https://springdoc.org/)
95 | * [Swagger annotation](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations)
96 |
97 |
98 |
99 | [url-spring-boot]: https://spring.io/projects/spring-boot/
100 | [url-spring-boot-ref-doc]: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
101 | [url-spring-rest-docs-project]: https://spring.io/projects/spring-restdocs/
102 | [url-spring-rest-docs-ref-doc]: https://docs.spring.io/spring-restdocs/docs/current/reference/html5/
103 | [url-swagger-io]: https://swagger.io/
104 | [url-swagger-ui]: https://swagger.io/tools/swagger-ui/
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "org.springframework.boot" version "2.7.3"
3 | id "io.spring.dependency-management" version "1.0.13.RELEASE"
4 | id "org.asciidoctor.jvm.convert" version "3.3.2"
5 | id "com.gorylenko.gradle-git-properties" version "2.4.1"
6 | id "com.epages.restdocs-api-spec" version "0.16.2"
7 | id "java"
8 | }
9 |
10 | group = "${projectGroup}"
11 | version = "${projectVersion}"
12 | sourceCompatibility = JavaVersion.VERSION_17
13 |
14 | configurations {
15 | compileOnly {
16 | extendsFrom annotationProcessor
17 | }
18 | asciidoctorExt
19 | }
20 |
21 | repositories {
22 | mavenCentral()
23 | }
24 |
25 | ext {
26 | set("snippetsDir", file("build/generated-snippets"))
27 | }
28 |
29 | dependencies {
30 | implementation("org.springframework.boot:spring-boot-starter-web")
31 | implementation("org.springframework.boot:spring-boot-starter-validation")
32 | implementation("com.google.code.findbugs:jsr305:3.0.2")
33 | //@see springdoc-openapi getting started. Spring REST Docs API specification Integration
44 | testImplementation("com.epages:restdocs-api-spec:0.16.2")
45 | testImplementation("com.epages:restdocs-api-spec-mockmvc:0.16.2")
46 |
47 | asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor")
48 | }
49 |
50 | tasks.named("test") {
51 | outputs.dir snippetsDir
52 | useJUnitPlatform()
53 | }
54 |
55 | tasks.register("restDocsTest", Test) {
56 | outputs.dir snippetsDir
57 | useJUnitPlatform {
58 | includeTags("restDocs")
59 | }
60 |
61 | finalizedBy "asciidoctor"
62 | finalizedBy "openapi3"
63 | }
64 |
65 | /**
66 | *
67 | * 개발환경에서만 API문서를 제공하고자 한다면 빌드태스크에 asciidoctor 를 추가합니다.
68 | * asciidoctor 가 수행될 때 통합테스트 실행 태스크 restDocsTest 를 수행하기에
69 | * restDocsTest 를 추가로 수행하지 않아도 됩니다.
70 | *
71 | * @see Asciidoctor Gradle Plugin Suite
72 | *
73 | * ex)./gradlew clean asciidoctor build
74 | */
75 | tasks.named("asciidoctor") {
76 | dependsOn restDocsTest
77 |
78 | inputs.dir snippetsDir
79 | configurations "asciidoctorExt"
80 | baseDirFollowsSourceDir() // 원본파일작업은 .adoc 디렉터리 기준
81 | }
82 |
83 | openapi3 {
84 | servers = [
85 | { url = 'http://localhost:8080' },
86 | ]
87 | title = 'spring-rest-docs-guide'
88 | description = 'Spring REST Docs 테스트 생성물 생성시 추가생성되는 OpenAPI 문서이용'
89 | version = "${project.version}"
90 | format = 'yaml'
91 | }
92 |
93 | tasks.register("apiBuild", GradleBuild) {
94 | tasks = ["clean", "restDocsTest", "build"]
95 | }
96 |
97 | springBoot {
98 | buildInfo()
99 | }
100 |
101 | gitProperties {
102 | dateFormat = "yyyy-MM-dd'T'HH:mm:ss.Zz"
103 | dateFormatTimeZone = "Asia/Seoul"
104 | failOnNoGitDirectory = false
105 | }
106 |
107 | bootJar {
108 | from("swagger-ui") {
109 | into "BOOT-INF/classes/static/swagger"
110 | }
111 | from("${asciidoctor.outputDir}") {
112 | into "BOOT-INF/classes/static/docs"
113 | }
114 | from("build/api-spec") {
115 | into "BOOT-INF/classes/static/swagger"
116 | }
117 |
118 | archiveFileName.set "application.jar"
119 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | projectGroup=com.kurly.tet
2 | projectVersion=1.0.0
3 |
4 | org.gradle.cache=false
5 | org.gradle.parallel=true
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thefarmersfront/spring-rest-docs-guide/7fd8b90007b12d1509b55c17bc4e06fd5292daee/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Stop when "xargs" is not available.
209 | if ! command -v xargs >/dev/null 2>&1
210 | then
211 | die "xargs is not available"
212 | fi
213 |
214 | # Use "xargs" to parse quoted args.
215 | #
216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
217 | #
218 | # In Bash we could simply go:
219 | #
220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
221 | # set -- "${ARGS[@]}" "$@"
222 | #
223 | # but POSIX shell has neither arrays nor command substitution, so instead we
224 | # post-process each arg (as a line of input to sed) to backslash-escape any
225 | # character that might be a shell metacharacter, then use eval to reverse
226 | # that process (while maintaining the separation between arguments), and wrap
227 | # the whole thing up as a single "set" statement.
228 | #
229 | # This will of course break if any of these variables contains a newline or
230 | # an unmatched quote.
231 | #
232 |
233 | eval "set -- $(
234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
235 | xargs -n1 |
236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
237 | tr '\n' ' '
238 | )" '"$@"'
239 |
240 | exec "$JAVACMD" "$@"
241 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if %ERRORLEVEL% equ 0 goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if %ERRORLEVEL% equ 0 goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | set EXIT_CODE=%ERRORLEVEL%
84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
86 | exit /b %EXIT_CODE%
87 |
88 | :mainEnd
89 | if "%OS%"=="Windows_NT" endlocal
90 |
91 | :omega
92 |
--------------------------------------------------------------------------------
/readme-img/spring-rest-docs-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thefarmersfront/spring-rest-docs-guide/7fd8b90007b12d1509b55c17bc4e06fd5292daee/readme-img/spring-rest-docs-example.png
--------------------------------------------------------------------------------
/readme-img/spring-rest-docs-openapi-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thefarmersfront/spring-rest-docs-guide/7fd8b90007b12d1509b55c17bc4e06fd5292daee/readme-img/spring-rest-docs-openapi-example.png
--------------------------------------------------------------------------------
/readme-img/springdocs-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thefarmersfront/spring-rest-docs-guide/7fd8b90007b12d1509b55c17bc4e06fd5292daee/readme-img/springdocs-example.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'spring-rest-docs-guide'
2 |
--------------------------------------------------------------------------------
/src/docs/asciidoc/index.adoc:
--------------------------------------------------------------------------------
1 | = Spring REST Docs 가이드
2 | 김지헌(팀 엔지니어링 팀),
3 | v2022.09.28, 2022-09-28
4 | :doctype: book
5 | :icons: font
6 | :source-highlighter: coderay
7 | :toc: left
8 | :toc-title: 목차
9 | :toclevels: 3
10 | :sectlinks:
11 | :sectnums:
12 |
13 | == 개요
14 | 이 API문서는 'Spring REST Docs 가이드' 프로젝트의 산출물입니다.
15 |
16 |
17 | === API 서버 경로
18 | [cols="2,5,3"]
19 | |====
20 | |환경 |DNS |비고
21 | |개발(dev) | link:[] |API 문서 제공
22 | |베타(beta) | link:[] |API 문서 제공
23 | |운영(prod) | link:[] |API 문서 미제공
24 | |====
25 |
26 | [NOTE]
27 | ====
28 | 해당 프로젝트 API문서는 개발환경까지 노출되는 것을 권장합니다. +
29 | ====
30 |
31 | [CAUTION]
32 | ====
33 | 운영환경에 노출될 경우 보안 관련 문제가 발생할 수 있습니다.
34 | ====
35 |
36 | === 응답형식
37 | 프로젝트는 다음과 같은 응답형식을 제공합니다.
38 |
39 | ==== 정상(200, OK)
40 |
41 | |====
42 | |응답데이터가 없는 경우|응답데이터가 있는 경우
43 |
44 | a|[source,json]
45 | ----
46 | {
47 | "code": "0000", // 정상인 경우 '0000'
48 | "message": "OK", // 정상인 경우 'OK'
49 | "data": null
50 | }
51 | ----
52 |
53 | a|[source,json]
54 | ----
55 | {
56 | "code": "0000", // 정상인 경우 '0000'
57 | "message": "OK", // 정상인 경우 'OK'
58 | "data": {
59 | "name": "honeymon-enterprise"
60 | }
61 | }
62 | ----
63 | |====
64 |
65 | ==== 상태코드(HttpStatus)
66 | 응답시 다음과 같은 응답상태 헤더, 응답코드 및 응답메시지를 제공합니다.
67 |
68 | [cols="3,1,3,3"]
69 | |====
70 | |HttpStatus |코드 |메시지 |설명
71 |
72 | |`OK(200)` |`0000` |"OK" |정상 응답
73 | |`INTERNAL_SERVER_ERROR(500)`|`S5XX` |"알 수 없는 에러가 발생했습니다. 관리자에게 문의하세요." |서버 내부 오류
74 | |`FORBIDDEN(403)`|`C403` |"[AccessDenied] 잘못된 접근입니다." |비인가 접속입니다.
75 | |`BAD_REQUEST(400)`|`C400` |"잘못된 요청입니다. 요청내용을 확인하세요." |요청값 누락 혹은 잘못된 기입
76 | |`NOT_FOUND(404)`|`C404` |"상황에 따라 다름" |요청값 누락 혹은 잘못된 기입
77 |
78 | |====
79 |
80 | == API
81 |
82 | //상품
83 | include::product.adoc[]
84 |
85 | //주문
86 | include::order.adoc[]
--------------------------------------------------------------------------------
/src/docs/asciidoc/order.adoc:
--------------------------------------------------------------------------------
1 | == 주문(Order)
2 | 주문을 등록하고 `결제(PAID) -> 출하(SHIPPED) -> 배송완료(COMPLETED)` 처리를 할 수 있습니다.
3 |
4 | === 주문검색
5 | 주문정보를 회원번호(memberNo)과 주문번호(orderNo)를 이용해서 검색할 수 있습니다.
6 | 페이징 처리된 목록을 제공합니다.
7 |
8 | |====
9 | |속성 |설명
10 |
11 | |`memberNo` |회원번호
12 | |`orderNo` |주문번호
13 | |`page` |페이지(0 부터 시작)
14 | |`size` |페이지당 조회건수(최소: 1, 최대: 1000)
15 |
16 | |====
17 |
18 |
19 | [discrete]
20 | ==== 요청
21 | include::{snippets}/get-v1-get-orders/curl-request.adoc[]
22 | include::{snippets}/get-v1-get-orders/httpie-request.adoc[]
23 | include::{snippets}/get-v1-get-orders/http-request.adoc[]
24 | include::{snippets}/get-v1-get-orders/request-parameters.adoc[]
25 |
26 |
27 | [discrete]
28 | ==== 응답
29 | include::{snippets}/get-v1-get-orders/http-response.adoc[]
30 | include::{snippets}/get-v1-get-orders/response-fields.adoc[]
31 |
32 |
33 | === 주문생성
34 | 주문을 새롭게 등록합니다.
35 |
36 | [discrete]
37 | ==== 요청
38 | include::{snippets}/post-v1-create-order/curl-request.adoc[]
39 | include::{snippets}/post-v1-create-order/httpie-request.adoc[]
40 | include::{snippets}/post-v1-create-order/http-request.adoc[]
41 | include::{snippets}/post-v1-create-order/request-fields.adoc[]
42 |
43 |
44 | [discrete]
45 | ==== 응답
46 | include::{snippets}/post-v1-create-order/http-response.adoc[]
47 | include::{snippets}/post-v1-create-order/response-fields.adoc[]
48 |
49 | === 주문결제
50 | 주문을결제합니다.
51 |
52 | [discrete]
53 | ==== 요청
54 | include::{snippets}/put-v1-payment-order/curl-request.adoc[]
55 | include::{snippets}/put-v1-payment-order/httpie-request.adoc[]
56 | include::{snippets}/put-v1-payment-order/http-request.adoc[]
57 | include::{snippets}/put-v1-payment-order/request-fields.adoc[]
58 | include::{snippets}/put-v1-payment-order/path-parameters.adoc[]
59 |
60 |
61 | [discrete]
62 | ==== 응답
63 | include::{snippets}/put-v1-payment-order/http-response.adoc[]
64 | include::{snippets}/put-v1-payment-order/response-fields.adoc[]
65 |
66 | === 주문상세조회
67 | 주문상세정보를 조회합니다.
68 |
69 | [discrete]
70 | ==== 요청
71 | include::{snippets}/get-v1-get-order/curl-request.adoc[]
72 | include::{snippets}/get-v1-get-order/httpie-request.adoc[]
73 | include::{snippets}/get-v1-get-order/http-request.adoc[]
74 | include::{snippets}/get-v1-get-order/path-parameters.adoc[]
75 |
76 |
77 | [discrete]
78 | ==== 응답
79 | include::{snippets}/get-v1-get-order/http-response.adoc[]
80 | include::{snippets}/get-v1-get-order/response-fields.adoc[]
81 |
82 |
83 | === 주문출하
84 | 주문상태를 출하로 변경합니다.
85 |
86 | [WARNING]
87 | ====
88 | 결제완료(`PAID`)상태여야 합니다.
89 | ====
90 |
91 | [discrete]
92 | ==== 요청
93 | include::{snippets}/put-v1-shipping-order/curl-request.adoc[]
94 | include::{snippets}/put-v1-shipping-order/httpie-request.adoc[]
95 | include::{snippets}/put-v1-shipping-order/http-request.adoc[]
96 | include::{snippets}/put-v1-shipping-order/path-parameters.adoc[]
97 |
98 |
99 | [discrete]
100 | ==== 응답
101 | include::{snippets}/put-v1-shipping-order/http-response.adoc[]
102 | include::{snippets}/put-v1-shipping-order/response-fields.adoc[]
103 |
104 | === 주문배송완료
105 | 주문상태를 완료상태로 변경합니다.
106 |
107 | [WARNING]
108 | ====
109 | 출하완료(`SHIPPED`)상태여야 합니다.
110 | ====
111 |
112 | [discrete]
113 | ==== 요청
114 | include::{snippets}/put-v1-complete-order/curl-request.adoc[]
115 | include::{snippets}/put-v1-complete-order/httpie-request.adoc[]
116 | include::{snippets}/put-v1-complete-order/http-request.adoc[]
117 | include::{snippets}/put-v1-complete-order/path-parameters.adoc[]
118 |
119 |
120 | [discrete]
121 | ==== 응답
122 | include::{snippets}/put-v1-complete-order/http-response.adoc[]
123 | include::{snippets}/put-v1-complete-order/response-fields.adoc[]
--------------------------------------------------------------------------------
/src/docs/asciidoc/product.adoc:
--------------------------------------------------------------------------------
1 | == 상품(Product)
2 | 상품정보에 대한 검색/등록/상세조회/변경/삭제 기능을 제공합니다.
3 |
4 |
5 | === 상품검색
6 | 상품정보를 상품명(productName)과 상품번호(productNo)를 이용해서 검색할 수 있습니다.
7 | 페이징 처리된 목록을 제공합니다.
8 |
9 | |====
10 | |속성 |설명
11 |
12 | |`page` |페이지(0 부터 시작)
13 | |`size` |페이지당 조회건수(최소: 1, 최대: 1000)
14 |
15 | |====
16 |
17 |
18 | [discrete]
19 | ==== 요청
20 | include::{snippets}/get-v1-get-products/curl-request.adoc[]
21 | include::{snippets}/get-v1-get-products/httpie-request.adoc[]
22 | include::{snippets}/get-v1-get-products/http-request.adoc[]
23 | include::{snippets}/get-v1-get-products/request-parameters.adoc[]
24 |
25 |
26 | [discrete]
27 | ==== 응답
28 | include::{snippets}/get-v1-get-products/http-response.adoc[]
29 | include::{snippets}/get-v1-get-products/response-fields.adoc[]
30 |
31 |
32 | === 상품생성
33 | 상품을 새롭게 등록합니다.
34 |
35 | [discrete]
36 | ==== 요청
37 | include::{snippets}/post-v1-create-product/curl-request.adoc[]
38 | include::{snippets}/post-v1-create-product/httpie-request.adoc[]
39 | include::{snippets}/post-v1-create-product/http-request.adoc[]
40 | include::{snippets}/post-v1-create-product/request-fields.adoc[]
41 |
42 |
43 | [discrete]
44 | ==== 응답
45 | include::{snippets}/post-v1-create-product/http-response.adoc[]
46 | include::{snippets}/post-v1-create-product/response-fields.adoc[]
47 |
48 |
49 | === 상품상세조회
50 | 상품세부정보를 조회합니다.
51 |
52 | [discrete]
53 | ==== 요청
54 | include::{snippets}/get-v1-get-product/curl-request.adoc[]
55 | include::{snippets}/get-v1-get-product/httpie-request.adoc[]
56 | include::{snippets}/get-v1-get-product/http-request.adoc[]
57 | include::{snippets}/get-v1-get-product/path-parameters.adoc[]
58 |
59 |
60 | [discrete]
61 | ==== 응답
62 | include::{snippets}/get-v1-get-product/http-response.adoc[]
63 | include::{snippets}/get-v1-get-product/response-fields.adoc[]
64 |
65 |
66 | === 상품상세변경
67 | 상품세부정보를 변경합니다.
68 |
69 | [discrete]
70 | ==== 요청
71 | include::{snippets}/put-v1-modify-product/curl-request.adoc[]
72 | include::{snippets}/put-v1-modify-product/httpie-request.adoc[]
73 | include::{snippets}/put-v1-modify-product/http-request.adoc[]
74 | include::{snippets}/put-v1-modify-product/path-parameters.adoc[]
75 | include::{snippets}/put-v1-modify-product/request-fields.adoc[]
76 |
77 |
78 | [discrete]
79 | ==== 응답
80 | include::{snippets}/put-v1-modify-product/http-response.adoc[]
81 | include::{snippets}/put-v1-modify-product/response-fields.adoc[]
82 |
83 |
84 |
85 | === 상품삭제
86 | 상품을 삭제합니다.
87 |
88 | [discrete]
89 | ==== 요청
90 | include::{snippets}/delete-v1-delete-product/curl-request.adoc[]
91 | include::{snippets}/delete-v1-delete-product/httpie-request.adoc[]
92 | include::{snippets}/delete-v1-delete-product/http-request.adoc[]
93 | include::{snippets}/put-v1-modify-product/path-parameters.adoc[]
94 |
95 |
96 | [discrete]
97 | ==== 응답
98 | include::{snippets}/delete-v1-delete-product/http-response.adoc[]
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/Application.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class Application {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(Application.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/application/order/OrderFacade.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.application.order;
2 |
3 | import com.kurly.tet.guide.springrestdocs.application.product.ProductFacade;
4 | import com.kurly.tet.guide.springrestdocs.domain.exception.OrderNotFoundException;
5 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderDto;
6 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageRequest;
7 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageResponse;
8 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.order.OrderSearchCondition;
9 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.order.command.OrderCreateCommand;
10 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.order.command.OrderPaymentCommand;
11 | import org.springframework.stereotype.Component;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 | import java.util.concurrent.atomic.AtomicLong;
16 | import java.util.stream.Collectors;
17 |
18 | @Component
19 | public class OrderFacade {
20 | private final AtomicLong orderIdCreator = new AtomicLong(0);
21 | private final List orders = new ArrayList<>();
22 | private final ProductFacade productFacade;
23 |
24 | public OrderFacade(ProductFacade productFacade) {
25 | this.productFacade = productFacade;
26 | }
27 |
28 | public PageResponse search(OrderSearchCondition searchCondition, PageRequest pageRequest) {
29 | var filteredOrders = this.orders.stream()
30 | .filter(searchCondition::filter)
31 | .collect(Collectors.toList());
32 |
33 | return new PageResponse<>(filteredOrders, pageRequest, this.orders.size());
34 | }
35 |
36 | public OrderDto create(OrderCreateCommand createCommand) {
37 | var findProducts = createCommand.getProductIds().stream().map(productFacade::getProduct).collect(Collectors.toList());
38 | var createOrder = createCommand.createOrder(orderIdCreator.incrementAndGet(), findProducts);
39 | orders.add(createOrder);
40 | return createOrder;
41 | }
42 |
43 | public OrderDto findByOrderNo(String orderNo) {
44 | return orders.stream()
45 | .filter(it -> it.getOrderNo().equals(orderNo))
46 | .findAny()
47 | .orElseThrow(() -> new OrderNotFoundException(orderNo));
48 | }
49 |
50 | public OrderDto payment(String orderNo, OrderPaymentCommand paymentCommand) {
51 | var order = findByOrderNo(orderNo);
52 | paymentCommand.payment(order);
53 | return order;
54 | }
55 |
56 | public OrderDto shipping(String orderNo) {
57 | var order = findByOrderNo(orderNo);
58 | order.shipping();
59 | return order;
60 | }
61 |
62 | public OrderDto complete(String orderNo) {
63 | var order = findByOrderNo(orderNo);
64 | order.complete();
65 | return order;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/application/product/ProductFacade.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.application.product;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.exception.ProductNotFoundException;
4 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
5 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageRequest;
6 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageResponse;
7 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.product.ProductCreateCommand;
8 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.product.ProductModifyCommand;
9 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.product.ProductSearchCondition;
10 | import org.springframework.stereotype.Component;
11 |
12 | import javax.annotation.PostConstruct;
13 | import java.util.ArrayList;
14 | import java.util.List;
15 | import java.util.concurrent.atomic.AtomicLong;
16 | import java.util.stream.Collectors;
17 |
18 | /**
19 | * 대충...
20 | */
21 | @Component
22 | public class ProductFacade {
23 | private final AtomicLong productIdCreator = new AtomicLong(0);
24 | private final List products = new ArrayList<>();
25 |
26 | @PostConstruct
27 | public void setUp() {
28 | products.add(new ProductDto(productIdCreator.incrementAndGet(), "TEST" + productIdCreator.incrementAndGet(), String.format("%05d", productIdCreator.incrementAndGet())));
29 | products.add(new ProductDto(productIdCreator.incrementAndGet(), "TEST" + productIdCreator.incrementAndGet(), String.format("%05d", productIdCreator.incrementAndGet())));
30 | products.add(new ProductDto(productIdCreator.incrementAndGet(), "TEST" + productIdCreator.incrementAndGet(), String.format("%05d", productIdCreator.incrementAndGet())));
31 | products.add(new ProductDto(productIdCreator.incrementAndGet(), "TEST" + productIdCreator.incrementAndGet(), String.format("%05d", productIdCreator.incrementAndGet())));
32 | products.add(new ProductDto(productIdCreator.incrementAndGet(), "TEST" + productIdCreator.incrementAndGet(), String.format("%05d", productIdCreator.incrementAndGet())));
33 | }
34 |
35 | public PageResponse search(ProductSearchCondition searchCondition, PageRequest pageRequest) {
36 | var filteredProducts = this.products.stream()
37 | .filter(searchCondition::filter)
38 | .collect(Collectors.toList());;
39 |
40 | return new PageResponse<>(filteredProducts, pageRequest, this.products.size());
41 | }
42 |
43 | public ProductDto create(ProductCreateCommand createCommand) {
44 | var createProduct = createCommand.createDto(productIdCreator.incrementAndGet());
45 |
46 | products.add(createProduct);
47 |
48 | return createProduct;
49 | }
50 |
51 | public ProductDto getProduct(Long productId) {
52 | return products.stream()
53 | .filter(it -> it.getId().equals(productId)).findFirst()
54 | .orElseThrow(() -> new ProductNotFoundException(productId));
55 | }
56 |
57 | public ProductDto modify(Long productId, ProductModifyCommand modifyCommand) {
58 | var targetProduct = getProduct(productId);
59 | modifyCommand.modify(targetProduct);
60 |
61 | return targetProduct;
62 | }
63 |
64 | public void remove(Long productId) {
65 | products.removeIf(it -> it.getId().equals(productId));
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/common/exception/BusinessException.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.common.exception;
2 |
3 | import com.kurly.tet.guide.springrestdocs.common.util.JsonUtils;
4 | import org.springframework.http.HttpStatus;
5 | import org.springframework.lang.NonNull;
6 |
7 | import java.util.Map;
8 |
9 | public abstract class BusinessException extends RuntimeException {
10 |
11 | protected BusinessException() {
12 | }
13 |
14 | protected BusinessException(String message) {
15 | super(message);
16 | }
17 |
18 | protected BusinessException(String message, Throwable cause) {
19 | super(message, cause);
20 | }
21 |
22 | protected BusinessException(Throwable cause) {
23 | super(cause);
24 | }
25 |
26 | protected BusinessException(@NonNull Map messageFields) {
27 | super(JsonUtils.toJson(messageFields));
28 | }
29 |
30 | protected BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
31 | super(message, cause, enableSuppression, writableStackTrace);
32 | }
33 |
34 | public abstract HttpStatus getHttpStatus();
35 |
36 | public abstract String getErrorCode();
37 |
38 | public abstract boolean isNecessaryToLog();
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/common/util/JsonUtils.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.common.util;
2 |
3 | import com.fasterxml.jackson.core.type.TypeReference;
4 | import com.fasterxml.jackson.databind.DeserializationFeature;
5 | import com.fasterxml.jackson.databind.JsonNode;
6 | import com.fasterxml.jackson.databind.ObjectMapper;
7 | import com.fasterxml.jackson.databind.SerializationFeature;
8 | import com.fasterxml.jackson.databind.type.CollectionType;
9 | import com.fasterxml.jackson.databind.type.TypeFactory;
10 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
11 | import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
12 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
13 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
14 | import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer;
15 |
16 | import java.io.IOException;
17 | import java.io.InputStream;
18 | import java.time.LocalDateTime;
19 | import java.time.ZonedDateTime;
20 | import java.time.format.DateTimeFormatter;
21 | import java.util.Collections;
22 | import java.util.List;
23 |
24 | import static java.util.Objects.isNull;
25 |
26 | public class JsonUtils {
27 |
28 | private static final ObjectMapper MAPPER;
29 |
30 | static {
31 | MAPPER = new ObjectMapper();
32 |
33 | JavaTimeModule javaTimeModule = new JavaTimeModule();
34 | javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
35 | javaTimeModule.addDeserializer(LocalDateTime.class,
36 | new LocalDateTimeDeserializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
37 | javaTimeModule.addSerializer(ZonedDateTime.class,
38 | new ZonedDateTimeSerializer(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
39 | javaTimeModule.addDeserializer(ZonedDateTime.class, InstantDeserializer.ZONED_DATE_TIME);
40 |
41 | MAPPER.registerModule(javaTimeModule);
42 | MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
43 | MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
44 | }
45 |
46 |
47 | private JsonUtils() {
48 | throw new UnsupportedOperationException();
49 | }
50 |
51 | public static ObjectMapper getMapper() {
52 | return MAPPER;
53 | }
54 |
55 | public static T fromJson(InputStream inputStream, Class clazz) {
56 | if (isNull(inputStream)) {
57 | return null;
58 | }
59 |
60 | try {
61 | return MAPPER.readValue(inputStream, clazz);
62 | } catch (IOException e) {
63 | throw new JsonDecodeException(e);
64 | }
65 | }
66 |
67 | public static T fromJson(String json, Class clazz) {
68 | if (isNull(json)) {
69 | return null;
70 | }
71 |
72 | try {
73 | return MAPPER.readValue(json, clazz);
74 | } catch (IOException e) {
75 | throw new JsonDecodeException(e);
76 | }
77 | }
78 |
79 | public static T fromJson(JsonNode jsonNode, Class clazz) {
80 | if (isNull(jsonNode)) {
81 | return null;
82 | }
83 |
84 | try {
85 | return MAPPER.readValue(jsonNode.traverse(), clazz);
86 | } catch (IOException e) {
87 | throw new JsonDecodeException(e);
88 | }
89 | }
90 |
91 | public static T fromJson(InputStream inputStream, TypeReference typeReference) {
92 | if (isNull(inputStream)) {
93 | return null;
94 | }
95 |
96 | try {
97 | return MAPPER.readValue(inputStream, typeReference);
98 | } catch (IOException e) {
99 | throw new JsonDecodeException(e);
100 | }
101 | }
102 |
103 | public static T fromJson(String json, TypeReference typeReference) {
104 | if (isNull(json)) {
105 | return null;
106 | }
107 |
108 | try {
109 | return MAPPER.readValue(json, typeReference);
110 | } catch (IOException e) {
111 | throw new JsonDecodeException(e);
112 | }
113 | }
114 |
115 | public static T fromJson(JsonNode jsonNode, TypeReference typeReference) {
116 | if (isNull(jsonNode)) {
117 | return null;
118 | }
119 |
120 | try {
121 | return MAPPER.readValue(jsonNode.traverse(), typeReference);
122 | } catch (IOException e) {
123 | throw new JsonDecodeException(e);
124 | }
125 | }
126 |
127 | public static T fromJson(byte[] bytes, TypeReference typeReference) {
128 | if (isNull(bytes) || bytes.length == 0) {
129 | return null;
130 | }
131 |
132 | try {
133 | return MAPPER.readValue(bytes, typeReference);
134 | } catch (IOException e) {
135 | throw new JsonDecodeException(e);
136 | }
137 | }
138 |
139 | public static T fromJson(byte[] bytes, Class clazz) {
140 | if (isNull(bytes) || bytes.length == 0) {
141 | return null;
142 | }
143 |
144 | try {
145 | return MAPPER.readValue(bytes, clazz);
146 | } catch (IOException e) {
147 | throw new JsonDecodeException(e);
148 | }
149 | }
150 |
151 | public static JsonNode fromJson(InputStream inputStream) {
152 | if (isNull(inputStream)) {
153 | return null;
154 | }
155 |
156 | try {
157 | return MAPPER.readTree(inputStream);
158 | } catch (IOException e) {
159 | throw new JsonDecodeException(e);
160 | }
161 | }
162 |
163 | public static JsonNode fromJson(String json) {
164 | if (isNull(json)) {
165 | return null;
166 | }
167 |
168 | try {
169 | return MAPPER.readTree(json);
170 | } catch (IOException e) {
171 | throw new JsonDecodeException(e);
172 | }
173 | }
174 |
175 | public static List fromJsonArray(InputStream inputStream, Class clazz) {
176 | if (isNull(inputStream)) {
177 | return Collections.emptyList();
178 | }
179 |
180 | CollectionType collectionType = TypeFactory.defaultInstance().constructCollectionType(List.class, clazz);
181 |
182 | try {
183 | return MAPPER.readValue(inputStream, collectionType);
184 | } catch (IOException e) {
185 | throw new JsonDecodeException(e);
186 | }
187 | }
188 |
189 | public static List fromJsonArray(String json, Class clazz) {
190 | if (isNull(json)) {
191 | return Collections.emptyList();
192 | }
193 |
194 | CollectionType collectionType = TypeFactory.defaultInstance().constructCollectionType(List.class, clazz);
195 |
196 | try {
197 | return MAPPER.readValue(json, collectionType);
198 | } catch (IOException e) {
199 | throw new JsonDecodeException(e);
200 | }
201 | }
202 |
203 | public static String toJson(final Object object) {
204 | try {
205 | return MAPPER.writeValueAsString(object);
206 | } catch (IOException e) {
207 | throw new JsonEncodeException(e);
208 | }
209 | }
210 |
211 | public static byte[] toJsonByte(final Object object) {
212 | try {
213 | return MAPPER.writeValueAsBytes(object);
214 | } catch (IOException e) {
215 | throw new JsonEncodeException(e);
216 | }
217 | }
218 |
219 | public static String toPrettyJson(final Object object) {
220 | try {
221 | return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(object);
222 | } catch (IOException e) {
223 | throw new JsonEncodeException(e);
224 | }
225 | }
226 |
227 | public static String get(JsonNode jsonNode) {
228 | if (jsonNode == null || jsonNode.isNull()) {
229 | return null;
230 | }
231 |
232 | return jsonNode.asText();
233 | }
234 |
235 | public static T convertValue(Object obj, TypeReference clazz) {
236 | return MAPPER.convertValue(obj, clazz);
237 | }
238 |
239 | public static class JsonEncodeException extends RuntimeException {
240 |
241 | private static final long serialVersionUID = 4975703115049362769L;
242 |
243 | public JsonEncodeException(Throwable cause) {
244 | super(cause);
245 | }
246 | }
247 |
248 | public static class JsonDecodeException extends RuntimeException {
249 |
250 | private static final long serialVersionUID = -2651564042039413190L;
251 |
252 | public JsonDecodeException(Throwable cause) {
253 | super(cause);
254 | }
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/common/util/LocalDateTimeUtils.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.common.util;
2 |
3 | import com.kurly.tet.guide.springrestdocs.config.Constant;
4 |
5 | import java.time.LocalDate;
6 | import java.time.LocalDateTime;
7 | import java.time.LocalTime;
8 | import java.time.format.DateTimeFormatter;
9 |
10 | import static com.kurly.tet.guide.springrestdocs.config.Constant.FORMAT_LOCAL_DATE_TIME;
11 | import static java.util.Objects.isNull;
12 |
13 | public class LocalDateTimeUtils {
14 | private LocalDateTimeUtils() {
15 | throw new UnsupportedOperationException("Utility class.");
16 | }
17 |
18 | /**
19 | * LocalDateTime 변환
20 | *
21 | * @param source 변환문자열
22 | * @return yyyy-MM-dd'T'HH:mm:ss 형식 {@link LocalDateTime}
23 | */
24 | public static LocalDateTime toLocalDateTime(String source) {
25 | return toLocalDateTime(source, FORMAT_LOCAL_DATE_TIME);
26 | }
27 |
28 | /**
29 | * LocalDateTime 변환
30 | *
31 | * @param source 변환문자열
32 | * @param dateTimeFormat pattern
33 | * @return pattern 에 맞춰 변환된 {@link LocalDateTime}
34 | */
35 | public static LocalDateTime toLocalDateTime(String source, String dateTimeFormat) {
36 | if (isNull(source) || source.isEmpty()) {
37 | return null;
38 | }
39 |
40 | return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(dateTimeFormat));
41 | }
42 |
43 | /**
44 | * LocalDateTime to 문자열 변환
45 | *
46 | * @param source 원천
47 | * @return "yyyy-MM-dd'T'HH:mm:ss" 형식으로 변환된 문자열
48 | */
49 | public static String toString(LocalDateTime source) {
50 | return toString(source, FORMAT_LOCAL_DATE_TIME);
51 | }
52 |
53 | /**
54 | * LocalDateTime to 문자열 변환
55 | *
56 | * @param source 원천
57 | * @param dateTimeFormat pattern
58 | * @return pattern 형식으로 변환된 문자열
59 | */
60 | public static String toString(LocalDateTime source, String dateTimeFormat) {
61 | if (isNull(source)) {
62 | return null;
63 | }
64 |
65 | return source.format(DateTimeFormatter.ofPattern(dateTimeFormat));
66 | }
67 |
68 | /**
69 | * 해당일 시작시
70 | *
71 | * @param source 원천일
72 | * @return 원천일 00:00:00
73 | */
74 | public static LocalDateTime ofFirst(LocalDate source) {
75 | return LocalDateTime.of(source, LocalTime.MIN);
76 | }
77 |
78 | /**
79 | * 해당일 종료시
80 | *
81 | * @param source 원천일
82 | * @return 원천일 23:59:59.999999999
83 | */
84 | public static LocalDateTime ofLast(LocalDate source) {
85 | return LocalDateTime.of(source, LocalTime.MAX);
86 | }
87 |
88 | /**
89 | * 해당월 시작일 시작시
90 | *
91 | * @param source 원천일
92 | * @return month-01 00:00:00
93 | */
94 | public static LocalDateTime ofMonthFirstDateTime(LocalDate source) {
95 | return ofFirst(LocalDateUtils.ofMonthFirstDay(source));
96 | }
97 |
98 | /**
99 | * 해당월 마지막날 마지막시
100 | *
101 | * @param source 원천일
102 | * @return month-lengthOfMonth 23:59:59.999999999
103 | */
104 | public static LocalDateTime ofMonthLastDateTime(LocalDate source) {
105 | return ofLast(LocalDateUtils.ofMonthLastDay(source));
106 | }
107 |
108 | /**
109 | * localDateTime을 Asia/Seoul ZoneDateTime 문자열로 변환합니다.
110 | *
111 | * @return Asia/Seoul ZoneDateTime 문자열
112 | */
113 | public static String convertZoneDateTimeFormat(final LocalDateTime source) {
114 | if (isNull(source)) {
115 | return null;
116 | }
117 |
118 | return source.atZone(Constant.DEFAULT_ZONE_ID)
119 | .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/common/util/LocalDateUtils.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.common.util;
2 |
3 | import java.time.LocalDate;
4 | import java.time.format.DateTimeFormatter;
5 |
6 | import static com.kurly.tet.guide.springrestdocs.config.Constant.FORMAT_LOCAL_DATE;
7 | import static java.util.Objects.isNull;
8 |
9 | public class LocalDateUtils {
10 | public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
11 | public static final int FIRST_DAY_OF_MONTH = 1;
12 |
13 | private LocalDateUtils() {
14 | throw new IllegalStateException("Util class.");
15 | }
16 |
17 | /**
18 | * {@link LocalDate} 변환
19 | *
20 | * @param source 원천 문자열
21 | * @return "yyyy-MM-dd" 형식으로 변환된 {@link LocalDate}
22 | */
23 | public static LocalDate toLocalDate(String source) {
24 | return toLocalDate(source, FORMAT_LOCAL_DATE);
25 | }
26 |
27 | /**
28 | * {@link LocalDate} 변환
29 | *
30 | * @param source 원천문자열
31 | * @param dateFormat 변환날짜형식
32 | * @return 지정형식으로 변환된 {@link LocalDate}
33 | */
34 | public static LocalDate toLocalDate(String source, String dateFormat) {
35 | if (isNull(source) || source.isEmpty()) {
36 | return null;
37 | }
38 |
39 | return LocalDate.parse(source, DateTimeFormatter.ofPattern(dateFormat));
40 | }
41 |
42 | /**
43 | * 문자열 변환
44 | *
45 | * @param source 원천일
46 | * @return yyyy-MM-dd 형식 문자열
47 | */
48 | public static String toString(LocalDate source) {
49 | if (isNull(source)) {
50 | return null;
51 | }
52 |
53 | return source.format(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
54 | }
55 |
56 | /**
57 | * 문자열 변환
58 | *
59 | * @param source 원천일
60 | * @param dateFormat 변환날짜형식
61 | * @return '변환날짜형식'으로 변환된 문자열
62 | */
63 | public static String toString(LocalDate source, String dateFormat) {
64 | return source.format(DateTimeFormatter.ofPattern(dateFormat));
65 | }
66 |
67 | /**
68 | * 해당월 첫번째 날
69 | *
70 | * @param source 원천일
71 | * @return 1 of source
72 | */
73 | public static LocalDate ofMonthFirstDay(LocalDate source) {
74 | return source.withDayOfMonth(FIRST_DAY_OF_MONTH);
75 | }
76 |
77 | /**
78 | * 해당월 마지막 날
79 | *
80 | * @param source 원천일
81 | * @return lengthOfMonth of source
82 | */
83 | public static LocalDate ofMonthLastDay(LocalDate source) {
84 | return source.withDayOfMonth(source.lengthOfMonth());
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/common/util/LocalTimeUtils.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.common.util;
2 |
3 | import java.time.LocalTime;
4 | import java.time.format.DateTimeFormatter;
5 | import java.util.Objects;
6 |
7 | import static com.kurly.tet.guide.springrestdocs.config.Constant.FORMAT_LOCAL_TIME;
8 | import static java.util.Objects.isNull;
9 |
10 | public class LocalTimeUtils {
11 | private LocalTimeUtils() {
12 | throw new IllegalStateException("Util class.");
13 | }
14 |
15 | /**
16 | * {@link LocalTime} 변환
17 | *
18 | * @param source 원천문자열
19 | * @return "HH:mm:ss" 형식으로 변환된 {@link LocalTime}
20 | */
21 | public static LocalTime toLocalTime(String source) {
22 | return toLocalTime(source, FORMAT_LOCAL_TIME);
23 | }
24 |
25 | /**
26 | * {@link LocalTime} 변환
27 | *
28 | * @param source 원천문자열
29 | * @param timeFormat 변환시간형식
30 | * @return null or "HH:mm:ss" 형식으로 변환된 {@link LocalTime}
31 | */
32 | public static LocalTime toLocalTime(String source, String timeFormat) {
33 | if (isNull(source) || source.isEmpty()) {
34 | return null;
35 | }
36 |
37 | return LocalTime.parse(source, DateTimeFormatter.ofPattern(timeFormat));
38 | }
39 |
40 | /**
41 | * 문자열 변환
42 | *
43 | * @param source 원천
44 | * @return "HH:mm:ss" 형식으로 변환된 문자열
45 | */
46 | public static String toString(LocalTime source) {
47 | return toString(source, FORMAT_LOCAL_TIME);
48 | }
49 |
50 | /**
51 | * 문자열 변환
52 | *
53 | * @param source 원천
54 | * @param timeFormat 변환시간형식
55 | * @return '변환시간형식'으로 변환된 문자열 or null
56 | */
57 | public static String toString(LocalTime source, String timeFormat) {
58 | if (Objects.isNull(source)) {
59 | return null;
60 | }
61 | return source.format(DateTimeFormatter.ofPattern(timeFormat));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/config/Constant.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.config;
2 |
3 | import java.time.ZoneId;
4 |
5 | /**
6 | * 애플리케이션에서 공통으로 사용되는 불변변수를 선언하는 목적으로 사용합니다.
7 | */
8 | public class Constant {
9 | public static final String PROFILE_TEST = "test";
10 | public static final String PROFILE_LOCAL = "local";
11 | public static final String PROFILE_DEV = "dev";
12 | public static final String PROFILE_STAGE = "stg";
13 | public static final String PROFILE_PERFORMANCE = "perf";
14 | public static final String PROFILE_PRODUCTION = "prod";
15 |
16 | public static final String HOST_LOCAL = "12.0.0.1";
17 | public static final String HOST_DEV = ""; // 개발DNS 는 배포에 따라 맞춰 진행
18 |
19 | public static final String FORMAT_LOCAL_DATE_TIME = "yyyy-MM-dd'T'HH:mm:ss";
20 | public static final String FORMAT_LOCAL_DATE = "yyyy-MM-dd";
21 | public static final String FORMAT_LOCAL_TIME = "HH:mm:ss";
22 |
23 | public static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("Asia/Seoul");
24 |
25 | private Constant() {
26 | throw new UnsupportedOperationException("불변변수용 클래스");
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/config/SpringDocOpenApiConfig.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.config;
2 |
3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition;
4 | import io.swagger.v3.oas.annotations.info.Contact;
5 | import io.swagger.v3.oas.annotations.info.Info;
6 | import org.springframework.context.annotation.Configuration;
7 |
8 | /**
9 | * springdoc-opeanpi 를 통해 생성되는 Swagger 정의
10 | *
11 | * @see OpenAPI Specification - v3.0.3
12 | * @see OpenAPI Annotations
13 | */
14 | @OpenAPIDefinition(
15 | info = @Info(
16 | title = "Spring REST Docs 가이드(Swagger 구성) 응?",
17 | version = "v2022.09.26",
18 | description = """
19 | Spring REST Docs 가이드를 작성하려고 했는데, 일이 너무 커저버린 건에 대해서 반성중....
20 | 생각외로 간단하게 적용가능해서 놀람""",
21 | contact = @Contact(url = "https://helloworld.kurly.com/", name = "김지헌(팀 엔지니어링 팀)", email = "jiheon.kim@kurlycorp.com")
22 | )
23 | )
24 | @Configuration
25 | public class SpringDocOpenApiConfig {
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/domain/BusinessCode.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.domain;
2 |
3 | import lombok.Getter;
4 |
5 | @Getter
6 | public enum BusinessCode {
7 | //COMMON
8 | SUCCESS("0000","정상"),
9 |
10 | //Client
11 | BAD_REQUEST("C400", "잘못된 요청입니다. 요청내용을 확인하세요."),
12 | NOT_FOUND("C404", "요청내용을 찾을 수 없습니다. 요청내용을 확인하세요."),
13 | UNAUTHORIZED("C401", "인증되지 않았습니다. 인증을 확인하세요."),
14 | FORBIDDEN("C403", "권한이 없습니다. 권한을 확인하세요."),
15 |
16 | //Application
17 | INTERNAL_SERVER_ERROR("S500", "시스템 내부오류가 발생했습니다. 담당자에게 문의바랍니다."),
18 |
19 |
20 | ;
21 |
22 | private final String code;
23 | private final String description;
24 |
25 | BusinessCode(String code, String description) {
26 | this.code = code;
27 | this.description = description;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/domain/DescriptionEnum.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.domain;
2 |
3 | /**
4 | * 외부 노출되는 enum 에 선언
5 | */
6 | public interface DescriptionEnum {
7 | String getCode();
8 | String getDescription();
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/domain/exception/OrderNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.domain.exception;
2 |
3 | import com.kurly.tet.guide.springrestdocs.common.exception.BusinessException;
4 | import org.springframework.http.HttpStatus;
5 |
6 | public class OrderNotFoundException extends BusinessException {
7 | public OrderNotFoundException(String orderNo) {
8 | super(String.format("주문(주문번호: %s)을 찾을 수 없습니다.", orderNo));
9 | }
10 |
11 | @Override
12 | public HttpStatus getHttpStatus() {
13 | return HttpStatus.NOT_FOUND;
14 | }
15 |
16 | @Override
17 | public String getErrorCode() {
18 | return "C404";
19 | }
20 |
21 | @Override
22 | public boolean isNecessaryToLog() {
23 | return true;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/domain/exception/ProductNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.domain.exception;
2 |
3 | import com.kurly.tet.guide.springrestdocs.common.exception.BusinessException;
4 | import org.springframework.http.HttpStatus;
5 |
6 | public class ProductNotFoundException extends BusinessException {
7 | public ProductNotFoundException(Long id) {
8 | super(String.format("상품(ID: %d)을 찾을 수 없습니다.", id));
9 | }
10 |
11 | @Override
12 | public HttpStatus getHttpStatus() {
13 | return HttpStatus.NOT_FOUND;
14 | }
15 |
16 | @Override
17 | public String getErrorCode() {
18 | return "C404";
19 | }
20 |
21 | @Override
22 | public boolean isNecessaryToLog() {
23 | return true;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/domain/order/OrderDto.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.domain.order;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
4 | import lombok.AccessLevel;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import org.springframework.util.Assert;
8 |
9 | import java.time.LocalDateTime;
10 | import java.util.List;
11 |
12 | @Getter
13 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
14 | public class OrderDto {
15 | private Long id;
16 | private String memberNo;
17 | private String orderNo;
18 | private OrderStatus orderStatus;
19 | private List products;
20 | private LocalDateTime orderDateTime; // 주문일시
21 |
22 | private Long paymentMoney; // 결제금액
23 | private LocalDateTime paymentDateTime; // 결제일시
24 |
25 | private LocalDateTime shipmentDateTime; // 출하일시
26 | private LocalDateTime completedDateTime; // 완료일시
27 |
28 | /**
29 | * 주문정보
30 | *
31 | * @param id 식별번호
32 | * @param memberNo 회원번호
33 | * @param orderNo 주문번호
34 | * @param products 주문상품목록
35 | */
36 | public OrderDto(Long id,
37 | String memberNo,
38 | String orderNo,
39 | List products) {
40 | this.id = id;
41 | this.memberNo = memberNo;
42 | this.orderNo = orderNo;
43 | this.products = products;
44 |
45 | this.orderStatus = OrderStatus.ORDERED;
46 | this.orderDateTime = LocalDateTime.now();
47 | }
48 |
49 | public String getOrderStatusDescription() {
50 | return getOrderStatus().getDescription();
51 | }
52 |
53 | /**
54 | * 결제
55 | *
56 | * @param paymentMoney 결제금액
57 | */
58 | public void payment(Long paymentMoney) {
59 | Assert.isTrue(this.orderStatus == OrderStatus.ORDERED, "주문상태가 '주문완료' 상태여야 합니다.");
60 | Assert.notNull(paymentMoney, "결제금액(paymentMoney)은 필수입력값입니다.");
61 | Assert.isTrue(paymentMoney > 0, "결제금액(paymentMoney)은 0보다 커야합니다.");
62 |
63 | this.orderStatus = OrderStatus.PAID;
64 | this.paymentMoney = paymentMoney;
65 | this.paymentDateTime = LocalDateTime.now();
66 | }
67 |
68 | public void shipping() {
69 | Assert.isTrue(this.orderStatus == OrderStatus.PAID, "주문상태가 '결제완료' 상태여야 합니다.");
70 |
71 | this.orderStatus = OrderStatus.SHIPPED;
72 | this.shipmentDateTime = LocalDateTime.now();
73 | }
74 |
75 | public void complete() {
76 | Assert.isTrue(this.orderStatus == OrderStatus.SHIPPED, "주문상태가 '출하완료' 상태여야 합니다.");
77 |
78 | this.orderStatus = OrderStatus.COMPLETED;
79 | this.completedDateTime = LocalDateTime.now();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/domain/order/OrderStatus.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.domain.order;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.DescriptionEnum;
4 | import lombok.Getter;
5 |
6 | @Getter
7 | public enum OrderStatus implements DescriptionEnum {
8 | ORDERED("주문완료"),
9 | PAID("결제완료"),
10 | SHIPPED("출하완료"),
11 | COMPLETED("배송완료"),
12 | ;
13 |
14 | private String description;
15 |
16 | OrderStatus(String description) {
17 | this.description = description;
18 | }
19 |
20 | @Override
21 | public String getCode() {
22 | return name();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/domain/product/ProductDto.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.domain.product;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | import javax.validation.constraints.NotBlank;
8 | import javax.validation.constraints.NotNull;
9 | import java.time.LocalDateTime;
10 |
11 | @Getter
12 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
13 | public class ProductDto {
14 | private Long id;
15 | private String productName;
16 | private String productNo;
17 | private ProductStatus productStatus;
18 | private LocalDateTime created;
19 | private LocalDateTime modified;
20 |
21 | public ProductDto(Long id, String productName, String productNo) {
22 | this.id = id;
23 | this.productName = productName;
24 | this.productNo = productNo;
25 | this.productStatus = ProductStatus.CREATED;
26 |
27 | LocalDateTime now = LocalDateTime.now();
28 | this.created = now;
29 | this.modified = now;
30 | }
31 |
32 | public String getProductStatusDescription() {
33 | return getProductStatus().getDescription();
34 | }
35 |
36 | public void modify(@NotBlank String productName, @NotBlank String productNo, @NotNull ProductStatus productStatus) {
37 | this.productName = productName;
38 | this.productNo = productNo;
39 | this.productStatus = productStatus;
40 | this.modified = LocalDateTime.now();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/domain/product/ProductStatus.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.domain.product;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.DescriptionEnum;
4 | import lombok.Getter;
5 |
6 | import java.util.Set;
7 |
8 | @Getter
9 | public enum ProductStatus implements DescriptionEnum {
10 | CREATED("생성"),
11 | ACTIVATED("활성화"),
12 | ARCHIVED("보관처리됨"),
13 | ;
14 |
15 | private String description;
16 |
17 | ProductStatus(String description) {
18 | this.description = description;
19 | }
20 |
21 | @Override
22 | public String getCode() {
23 | return name();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/package-info.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure;
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/advisor/ApiResponseJsonProcessingException.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.advisor;
2 |
3 | import com.kurly.tet.guide.springrestdocs.common.exception.BusinessException;
4 | import org.springframework.http.HttpStatus;
5 |
6 | public class ApiResponseJsonProcessingException extends BusinessException {
7 | private static final String ERROR_CODE = "S599";
8 |
9 | private static final HttpStatus HTTP_STATUS = HttpStatus.INTERNAL_SERVER_ERROR;
10 |
11 | public ApiResponseJsonProcessingException(Throwable cause) {
12 | super(cause);
13 | }
14 |
15 | @Override
16 | public HttpStatus getHttpStatus() {
17 | return HTTP_STATUS;
18 | }
19 |
20 | @Override
21 | public String getErrorCode() {
22 | return ERROR_CODE;
23 | }
24 |
25 | @Override
26 | public boolean isNecessaryToLog() {
27 | return true;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/advisor/ApiResponseWrappingAdvisor.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.advisor;
2 |
3 | import com.kurly.tet.guide.springrestdocs.common.util.JsonUtils;
4 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.ApiResponseGenerator;
5 | import lombok.extern.slf4j.Slf4j;
6 | import org.springframework.core.GenericTypeResolver;
7 | import org.springframework.core.MethodParameter;
8 | import org.springframework.core.ResolvableType;
9 | import org.springframework.http.HttpEntity;
10 | import org.springframework.http.HttpStatus;
11 | import org.springframework.http.MediaType;
12 | import org.springframework.http.converter.ByteArrayHttpMessageConverter;
13 | import org.springframework.http.converter.HttpMessageConverter;
14 | import org.springframework.http.converter.ResourceHttpMessageConverter;
15 | import org.springframework.http.converter.StringHttpMessageConverter;
16 | import org.springframework.http.server.ServerHttpRequest;
17 | import org.springframework.http.server.ServerHttpResponse;
18 | import org.springframework.http.server.ServletServerHttpResponse;
19 | import org.springframework.lang.NonNull;
20 | import org.springframework.lang.Nullable;
21 | import org.springframework.web.bind.annotation.RestControllerAdvice;
22 | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
23 |
24 | import java.lang.reflect.Type;
25 | import java.util.Objects;
26 |
27 | /**
28 | * 웹요청 처리결과 응답값을 봉투패턴(Envelop pattern)으로 일정한 데이터 형식으로 가공
29 | *
30 | * {
31 | * "code": "0000",
32 | * "message": "OK",
33 | * "data": { data }
34 | * }
35 | *
36 | */
37 | @Slf4j
38 | @RestControllerAdvice(basePackages = {
39 | "com.kurly.tet.guide.springrestdocs.infrastructure.web.product",
40 | "com.kurly.tet.guide.springrestdocs.infrastructure.web.order"
41 | })
42 | public class ApiResponseWrappingAdvisor implements ResponseBodyAdvice {
43 |
44 | private Type getGenericType(MethodParameter returnType) {
45 | if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
46 | return ResolvableType.forType(returnType.getGenericParameterType()).getGeneric().getType();
47 | } else {
48 | return returnType.getGenericParameterType();
49 | }
50 | }
51 |
52 | @Override
53 | public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) {
54 | Type type = GenericTypeResolver.resolveType(getGenericType(returnType),
55 | returnType.getContainingClass());
56 |
57 | if(returnType.getExecutable().getName().toUpperCase().contains("HEALTH")) { // healthCheck
58 | return false;
59 | }
60 |
61 | if (Void.class.getName().equals(type.getTypeName())) {
62 | return false;
63 | }
64 |
65 | return !converterType.isAssignableFrom(ByteArrayHttpMessageConverter.class) &&
66 | !converterType.isAssignableFrom(ResourceHttpMessageConverter.class);
67 | }
68 |
69 | @Override
70 | public Object beforeBodyWrite(@Nullable Object body,
71 | @NonNull MethodParameter returnType,
72 | @NonNull MediaType selectedContentType,
73 | @NonNull Class extends HttpMessageConverter>> selectedConverterType,
74 | @NonNull ServerHttpRequest request,
75 | @NonNull ServerHttpResponse response) {
76 |
77 | HttpStatus responseStatus = HttpStatus.valueOf(
78 | ((ServletServerHttpResponse) response).getServletResponse().getStatus()
79 | );
80 |
81 | if (Objects.isNull(body)) {
82 | return responseStatus.isError() ? ApiResponseGenerator.fail() : ApiResponseGenerator.success();
83 | }
84 |
85 | var apiResponse = responseStatus.isError() ? ApiResponseGenerator.fail(body) : ApiResponseGenerator.success(body);
86 | log.trace("[ApiResponse] {}", apiResponse);
87 |
88 | if (selectedConverterType.isAssignableFrom(StringHttpMessageConverter.class)) {
89 | try {
90 | response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
91 | return JsonUtils.toJson(apiResponse);
92 | } catch (JsonUtils.JsonEncodeException jpe) {
93 | log.warn("JSON 처리 중 오류 발생", jpe);
94 | throw new ApiResponseJsonProcessingException(jpe);
95 | }
96 | }
97 |
98 | return apiResponse;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/component/LocalDateJsonComponent.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.component;
2 |
3 | import com.fasterxml.jackson.core.JacksonException;
4 | import com.fasterxml.jackson.core.JsonGenerator;
5 | import com.fasterxml.jackson.core.JsonParser;
6 | import com.fasterxml.jackson.databind.DeserializationContext;
7 | import com.fasterxml.jackson.databind.JsonDeserializer;
8 | import com.fasterxml.jackson.databind.JsonSerializer;
9 | import com.fasterxml.jackson.databind.SerializerProvider;
10 | import com.kurly.tet.guide.springrestdocs.common.util.LocalDateUtils;
11 | import org.springframework.boot.jackson.JsonComponent;
12 |
13 | import java.io.IOException;
14 | import java.time.LocalDate;
15 |
16 | @JsonComponent
17 | public class LocalDateJsonComponent {
18 | public static class Serializer extends JsonSerializer {
19 | @Override
20 | public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
21 | gen.writeString(LocalDateUtils.toString(value));
22 | }
23 | }
24 |
25 | public static class Deserializer extends JsonDeserializer {
26 | @Override
27 | public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
28 | return LocalDateUtils.toLocalDate(p.getText());
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/component/LocalDateTimeJsonComponent.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.component;
2 |
3 | import com.fasterxml.jackson.core.JacksonException;
4 | import com.fasterxml.jackson.core.JsonGenerator;
5 | import com.fasterxml.jackson.core.JsonParser;
6 | import com.fasterxml.jackson.databind.DeserializationContext;
7 | import com.fasterxml.jackson.databind.JsonDeserializer;
8 | import com.fasterxml.jackson.databind.JsonSerializer;
9 | import com.fasterxml.jackson.databind.SerializerProvider;
10 | import com.kurly.tet.guide.springrestdocs.common.util.LocalDateTimeUtils;
11 | import com.kurly.tet.guide.springrestdocs.common.util.LocalDateUtils;
12 | import com.kurly.tet.guide.springrestdocs.common.util.LocalTimeUtils;
13 | import org.springframework.boot.jackson.JsonComponent;
14 |
15 | import java.io.IOException;
16 | import java.time.LocalDateTime;
17 |
18 | @JsonComponent
19 | public class LocalDateTimeJsonComponent {
20 | public static class Serializer extends JsonSerializer {
21 |
22 | @Override
23 | public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
24 | gen.writeString(LocalDateTimeUtils.toString(value));
25 | }
26 | }
27 |
28 | public static class Deserializer extends JsonDeserializer {
29 |
30 | @Override
31 | public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
32 | return LocalDateTimeUtils.toLocalDateTime(p.getText());
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/component/LocalTimeJsonComponent.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.component;
2 |
3 | import com.fasterxml.jackson.core.JacksonException;
4 | import com.fasterxml.jackson.core.JsonGenerator;
5 | import com.fasterxml.jackson.core.JsonParser;
6 | import com.fasterxml.jackson.databind.DeserializationContext;
7 | import com.fasterxml.jackson.databind.JsonDeserializer;
8 | import com.fasterxml.jackson.databind.JsonSerializer;
9 | import com.fasterxml.jackson.databind.SerializerProvider;
10 | import com.kurly.tet.guide.springrestdocs.common.util.LocalDateTimeUtils;
11 | import com.kurly.tet.guide.springrestdocs.common.util.LocalTimeUtils;
12 | import org.springframework.boot.jackson.JsonComponent;
13 |
14 | import java.io.IOException;
15 | import java.time.LocalTime;
16 |
17 | @JsonComponent
18 | public class LocalTimeJsonComponent {
19 | public static class Serializer extends JsonSerializer {
20 | @Override
21 | public void serialize(LocalTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
22 | gen.writeString(LocalTimeUtils.toString(value));
23 | }
24 | }
25 |
26 | public static class Deserializer extends JsonDeserializer {
27 |
28 | @Override
29 | public LocalTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
30 | return LocalTimeUtils.toLocalTime(p.getText());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/dto/ApiResponse.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.BusinessCode;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @Getter
8 | @NoArgsConstructor
9 | public class ApiResponse {
10 | private String code;
11 | private String message;
12 | private T data;
13 |
14 | public ApiResponse(BusinessCode businessCode) {
15 | this.code = businessCode.getCode();
16 | this.message = businessCode.getDescription();
17 | }
18 |
19 | public ApiResponse(String code, String message) {
20 | this.code = code;
21 | this.message = message;
22 | }
23 |
24 | public ApiResponse(String code, String message, T data) {
25 | this.code = code;
26 | this.message = message;
27 | this.data = data;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/dto/ApiResponseGenerator.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.BusinessCode;
4 |
5 | public class ApiResponseGenerator {
6 | private static final ApiResponse SUCCESS = new ApiResponse<>(BusinessCode.SUCCESS);
7 | private static final ApiResponse FAILURE = new ApiResponse<>(BusinessCode.INTERNAL_SERVER_ERROR);
8 |
9 | private ApiResponseGenerator() {
10 | throw new UnsupportedOperationException("Utility class.");
11 | }
12 |
13 | /**
14 | * 기본적인 성공응답
15 | *
16 | * @return new ApiResponse<>("0000", "OK");
17 | */
18 | public static ApiResponse success() {
19 | return SUCCESS;
20 | }
21 |
22 | /**
23 | * 성공 응답데이터 제공
24 | *
25 | * @param data 응답데이터
26 | * @return new ApiResponse<>("0000", "OK", data);
27 | */
28 | public static ApiResponse success(D data) {
29 | return new ApiResponse<>(SUCCESS.getCode(), SUCCESS.getMessage(), data);
30 | }
31 |
32 | /**
33 | * 기본적인 실패응답
34 | *
35 | * @return new ApiResponse<>("5000", "Internal Server Error");
36 | */
37 | public static ApiResponse fail() {
38 | return FAILURE;
39 | }
40 |
41 | /**
42 | * 실패 응답데이터 제공
43 | *
44 | * @param data 응답데이터
45 | * @return new ApiResponse<>("5000", "Internal Server Error", data);
46 | */
47 | public static ApiResponse fail(D data) {
48 | return new ApiResponse<>(FAILURE.getCode(), FAILURE.getMessage(), data);
49 | }
50 |
51 | /**
52 | * 실패응답
53 | *
54 | * @param code 실패코드
55 | * @param message 실패메시지
56 | * @return new ApiResponse<>(code, message, null);
57 | */
58 | public static ApiResponse fail(String code, String message) {
59 | return new ApiResponse<>(code, message, null);
60 | }
61 |
62 | /**
63 | * 응답값 처리(서버에서 응답한 경우는 요청성공(success == true)으로 판단한다)
64 | *
65 | * @param code 응답코드
66 | * @param message 응답메시지
67 | * @param data 응답데이터
68 | * @param 응답데이터 클래스
69 | * @return new ApiResponse<>(code, message, data);
70 | */
71 | public static ApiResponse of(String code, String message, D data) {
72 | return new ApiResponse<>(code, message, data);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/dto/PageRequest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import io.swagger.v3.oas.annotations.media.Schema;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import lombok.Setter;
8 |
9 | import javax.validation.constraints.Max;
10 | import javax.validation.constraints.Min;
11 |
12 | @Schema(description = "페이징요청")
13 | @Getter
14 | @Setter
15 | @NoArgsConstructor
16 | public class PageRequest {
17 | @Schema(description = "페이지번호", required = true, minimum = "0")
18 | @Min(value = 0)
19 | private int page;
20 | @Schema(description = "페이지 내 조회건수(1000건 이상 불가)", required = true, minimum = "0", maximum = "1000")
21 | @Min(value = 0)
22 | @Max(value = 1000)
23 | private int size;
24 |
25 | public PageRequest(int page, int size) {
26 | if (page < 0) {
27 | throw new IllegalArgumentException("페이지(page)는 0보다 커야합니다.");
28 | }
29 |
30 | if (size < 1) {
31 | throw new IllegalArgumentException("페이지크기(size)는 1보다 커야 합니다.");
32 | }
33 |
34 | this.page = page;
35 | this.size = size;
36 | }
37 |
38 | public static PageRequest of(int page, int size) {
39 | return new PageRequest(page, size);
40 | }
41 |
42 | @JsonIgnore
43 | public long getOffset() {
44 | return (long) page * (long) size;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/dto/PageResponse.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import lombok.AccessLevel;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import lombok.Setter;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 | import java.util.Optional;
12 |
13 | @Getter
14 | @NoArgsConstructor
15 | public class PageResponse {
16 | private List content = new ArrayList<>();
17 | private PageRequest pageRequest;
18 | private long total;
19 |
20 | public PageResponse(List content, PageRequest pageRequest, long total) {
21 | this.content.addAll(content);
22 | this.pageRequest = pageRequest;
23 | this.total = Optional.of(pageRequest)
24 | .filter(it -> !content.isEmpty())
25 | .filter(it -> pageRequest.getOffset() + it.getSize() > total)
26 | .map(it -> pageRequest.getOffset() + content.size())
27 | .orElse(total);
28 | }
29 |
30 | public int getTotalPages() {
31 | return this.pageRequest.getSize() == 0 ? 1 : (int) Math.ceil(getTotal() / (double) this.pageRequest.getSize());
32 | }
33 |
34 | @JsonProperty("hasPrevious")
35 | public boolean hasPrevious(){
36 | return this.pageRequest.getPage() > 0;
37 | }
38 |
39 | @JsonProperty("isFirst")
40 | public boolean isFirst() {
41 | return !hasPrevious();
42 | }
43 |
44 | @JsonProperty("hasNext")
45 | public boolean hasNext() {
46 | return this.pageRequest.getPage() + 1 < getTotalPages();
47 | }
48 |
49 | @JsonProperty("isLast")
50 | public boolean isLast() {
51 | return !hasNext();
52 | }
53 |
54 | @JsonProperty("hasContent")
55 | public boolean hasContent() {
56 | return !getContent().isEmpty();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/common/handler/GlobalRestControllerExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.common.handler;
2 |
3 | import com.kurly.tet.guide.springrestdocs.common.exception.BusinessException;
4 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.ApiResponse;
5 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.ApiResponseGenerator;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.springframework.core.convert.ConversionFailedException;
8 | import org.springframework.http.HttpStatus;
9 | import org.springframework.http.ResponseEntity;
10 | import org.springframework.http.converter.HttpMessageNotReadableException;
11 | import org.springframework.validation.BindException;
12 | import org.springframework.web.HttpMediaTypeNotAcceptableException;
13 | import org.springframework.web.HttpMediaTypeNotSupportedException;
14 | import org.springframework.web.bind.MethodArgumentNotValidException;
15 | import org.springframework.web.bind.MissingServletRequestParameterException;
16 | import org.springframework.web.bind.annotation.ExceptionHandler;
17 | import org.springframework.web.bind.annotation.ResponseStatus;
18 | import org.springframework.web.bind.annotation.RestControllerAdvice;
19 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
20 |
21 | import java.nio.file.AccessDeniedException;
22 |
23 | import static com.kurly.tet.guide.springrestdocs.domain.BusinessCode.*;
24 |
25 | @Slf4j
26 | @RestControllerAdvice
27 | public class GlobalRestControllerExceptionHandler {
28 | @ExceptionHandler(BusinessException.class)
29 | protected ResponseEntity> handle(BusinessException exception) {
30 | if (exception.isNecessaryToLog()) {
31 | log.error("[BusinessException] {}", exception.getMessage(), exception);
32 | }
33 |
34 | return ResponseEntity
35 | .status(exception.getHttpStatus())
36 | .body(ApiResponseGenerator.fail(exception.getErrorCode(), exception.getMessage()));
37 | }
38 |
39 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
40 | @ExceptionHandler(Throwable.class)
41 | protected ApiResponse handle(Throwable throwable) {
42 | log.error("[InternalServerError]{}", throwable.getMessage(), throwable);
43 |
44 | return ApiResponseGenerator.fail(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getDescription());
45 | }
46 |
47 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
48 | @ExceptionHandler(ConversionFailedException.class)
49 | protected ApiResponse handle(ConversionFailedException exception) {
50 | Throwable cause = exception.getCause();
51 | if (cause instanceof IllegalArgumentException illegalArgumentException) {
52 | return this.handle(illegalArgumentException);
53 | }
54 |
55 | log.error("[InternalServerError][ConversionFailed] {}", exception.getMessage(), exception);
56 |
57 | return ApiResponseGenerator.fail(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getDescription());
58 | }
59 |
60 | @ResponseStatus(HttpStatus.FORBIDDEN)
61 | @ExceptionHandler(AccessDeniedException.class)
62 | protected ApiResponse handle(AccessDeniedException exception) {
63 | log.info("[AccessDenied] {}", exception.getMessage());
64 | return ApiResponseGenerator.fail(FORBIDDEN.getCode(), FORBIDDEN.getDescription());
65 | }
66 |
67 | @ResponseStatus(HttpStatus.BAD_REQUEST)
68 | @ExceptionHandler({
69 | IllegalArgumentException.class,
70 | MethodArgumentNotValidException.class,
71 | MissingServletRequestParameterException.class,
72 | MethodArgumentTypeMismatchException.class,
73 | HttpMessageNotReadableException.class,
74 | HttpMediaTypeNotSupportedException.class,
75 | HttpMediaTypeNotAcceptableException.class,
76 | BindException.class
77 | })
78 | protected ApiResponse handle(Exception exception) {
79 | log.info("[BadRequest] {}", exception.getMessage(), exception);
80 |
81 | return ApiResponseGenerator.fail(BAD_REQUEST.getCode(), BAD_REQUEST.getDescription());
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/order/OrderRestController.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.order;
2 |
3 | import com.kurly.tet.guide.springrestdocs.application.order.OrderFacade;
4 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderDto;
5 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageRequest;
6 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageResponse;
7 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.order.command.OrderCreateCommand;
8 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.order.command.OrderPaymentCommand;
9 | import io.swagger.v3.oas.annotations.Operation;
10 | import io.swagger.v3.oas.annotations.Parameter;
11 | import io.swagger.v3.oas.annotations.media.Content;
12 | import io.swagger.v3.oas.annotations.media.Schema;
13 | import io.swagger.v3.oas.annotations.responses.ApiResponse;
14 | import io.swagger.v3.oas.annotations.responses.ApiResponses;
15 | import io.swagger.v3.oas.annotations.tags.Tag;
16 | import org.springframework.http.HttpStatus;
17 | import org.springframework.web.bind.annotation.*;
18 |
19 | import javax.validation.Valid;
20 |
21 | @Tag(name = "orders", description = "주문(Order)API")
22 | @RestController
23 | public class OrderRestController {
24 | private final OrderFacade orderFacade;
25 |
26 | public OrderRestController(OrderFacade orderFacade) {
27 | this.orderFacade = orderFacade;
28 | }
29 |
30 | /**
31 | * 주문검색
32 | *
33 | * @param searchCondition 검색조건[회원번호]
34 | * @param pageRequest 페이징조건(page=페이지번호,size=페이지 내 건수)
35 | * @return 페이징된 검색주문내역
36 | */
37 | @Operation(summary = "주문내역을 검색한다.")
38 | @GetMapping("/orders")
39 | public PageResponse search(@Parameter(description = "주문검색조건") OrderSearchCondition searchCondition,
40 | @Valid PageRequest pageRequest) {
41 | return orderFacade.search(searchCondition, pageRequest);
42 | }
43 |
44 | @Operation(summary = "주문을 생성한다.",
45 | requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "주문을 생성합니다.", required = true , content = @Content(schema = @Schema(implementation = OrderCreateCommand.class))),
46 | responses = {
47 | @ApiResponse(responseCode = "201", description = "정상적으로 생성완료"),
48 | @ApiResponse(responseCode = "400", description = "회원번호 혹은 상품번호를 누락한 경우 발생",
49 | content = {@Content(schema = @Schema(example = "{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"))}),
50 | @ApiResponse(responseCode = "403", description = "미구현"),
51 | }
52 | )
53 | @ResponseStatus(HttpStatus.CREATED)
54 | @PostMapping("/orders")
55 | public OrderDto create(@Valid @RequestBody OrderCreateCommand createCommand) {
56 | return orderFacade.create(createCommand);
57 | }
58 |
59 | @Operation(summary = "주문내역을 조회한다.",
60 | responses = {
61 | @ApiResponse(responseCode = "200", description = "정상처리"),
62 | @ApiResponse(responseCode = "400", description = "미구현"),
63 | @ApiResponse(responseCode = "403", description = "미구현"),
64 | @ApiResponse(responseCode = "404", description = "주문번호와 대응되는 주문내역 검색실패")
65 | })
66 | @GetMapping("/orders/{orderNo}")
67 | public OrderDto findByOrderNo(@Parameter(description = "주문번호") @PathVariable String orderNo) {
68 | return orderFacade.findByOrderNo(orderNo);
69 | }
70 |
71 | @Operation(summary = "주문내역을 결제처리한다.",
72 | responses = {
73 | @ApiResponse(responseCode = "200", description = "정상처리"),
74 | @ApiResponse(responseCode = "400", description = "회원번호 혹은 상품번호를 누락한 경우 발생",
75 | content = {@Content(schema = @Schema(example = "{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"))}),
76 | @ApiResponse(responseCode = "403", description = "미구현"),
77 | @ApiResponse(responseCode = "404", description = "주문번호와 대응되는 주문내역 검색실패",
78 | content = {@Content(schema = @Schema(example = "{\"code\":\"C404\",\"message\":\"주문(주문번호: ${주문번호})을 찾을 수 없습니다.\",\"data\":null}"))})
79 | }
80 | )
81 | @PutMapping("/orders/{orderNo}/payment")
82 | public OrderDto payment(@Parameter(description = "주문번호") @PathVariable String orderNo,
83 | @Valid @RequestBody OrderPaymentCommand paymentCommand) {
84 | return orderFacade.payment(orderNo, paymentCommand);
85 | }
86 |
87 | @Operation(summary = "주문내역을 출하한다.",
88 | responses = {
89 | @ApiResponse(responseCode = "200", description = "정상처리"),
90 | @ApiResponse(responseCode = "404", description = "주문번호와 대응되는 주문내역 검색실패",
91 | content = {@Content(schema = @Schema(example = "{\"code\":\"C404\",\"message\":\"주문(주문번호: ${주문번호})을 찾을 수 없습니다.\",\"data\":null}"))})
92 | })
93 | @PutMapping("/orders/{orderNo}/shipping")
94 | public OrderDto shipping(@Parameter(description = "주문번호") @PathVariable String orderNo) {
95 | return orderFacade.shipping(orderNo);
96 | }
97 |
98 | @Operation(summary = "주문내역을 배송완료처리한다.",
99 | responses = {
100 | @ApiResponse(responseCode = "200", description = "정상처리"),
101 | @ApiResponse(responseCode = "404", description = "주문번호와 대응되는 주문내역 검색실패",
102 | content = {@Content(schema = @Schema(example = "{\"code\":\"C404\",\"message\":\"주문(주문번호: ${주문번호})을 찾을 수 없습니다.\",\"data\":null}"))})
103 | })
104 | @PutMapping("/orders/{orderNo}/complete")
105 | public OrderDto complete(@Parameter(description = "주문번호") @PathVariable String orderNo) {
106 | return orderFacade.complete(orderNo);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/order/OrderSearchCondition.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.order;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderDto;
4 | import io.swagger.v3.oas.annotations.media.Schema;
5 | import lombok.Getter;
6 |
7 | @Schema(description = "주문검색조건")
8 | @Getter
9 | public class OrderSearchCondition {
10 | @Schema(description = "회원번호")
11 | private String memberNo;
12 | @Schema(description = "주문번호")
13 | private String orderNo;
14 |
15 | private boolean hasMemberNo() {
16 | return memberNo != null && !memberNo.isBlank();
17 | }
18 |
19 | private boolean hasOrderNo() {
20 | return orderNo != null && !orderNo.isBlank();
21 | }
22 |
23 | public boolean filter(OrderDto orderDto) {
24 | if (hasMemberNo()) {
25 | return orderDto.getMemberNo().equals(getMemberNo());
26 | }
27 | if (hasOrderNo()) {
28 | return orderDto.getOrderNo().equals(getOrderNo());
29 | }
30 |
31 | return true;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/order/command/OrderCreateCommand.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.order.command;
2 |
3 | import com.kurly.tet.guide.springrestdocs.common.util.LocalDateTimeUtils;
4 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderDto;
5 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
6 | import io.swagger.v3.oas.annotations.media.Schema;
7 | import lombok.AccessLevel;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 |
11 | import javax.validation.constraints.NotBlank;
12 | import javax.validation.constraints.NotEmpty;
13 | import java.time.LocalDateTime;
14 | import java.util.List;
15 |
16 | @Schema(description = "주문생성명령")
17 | @Getter
18 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
19 | public class OrderCreateCommand {
20 | @Schema(description = "회원번호",required = true)
21 | @NotBlank
22 | private String memberNo;
23 | @Schema(description = "상품식별번호목록", required = true, example = "[1,2,]")
24 | @NotEmpty
25 | private List productIds;
26 |
27 | public OrderCreateCommand(String memberNo, List productIds) {
28 | this.memberNo = memberNo;
29 | this.productIds = productIds;
30 | }
31 |
32 | public OrderDto createOrder(long id, List findProducts) {
33 | return new OrderDto(
34 | id,
35 | this.memberNo,
36 | LocalDateTimeUtils.toString(LocalDateTime.now(), "yyyyMMddHHmmss" + id),
37 | findProducts);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/order/command/OrderPaymentCommand.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.order.command;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderDto;
4 | import io.swagger.v3.oas.annotations.media.Schema;
5 | import lombok.AccessLevel;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 |
9 | import javax.validation.constraints.NotNull;
10 |
11 | @Schema(description = "주문결제명령")
12 | @Getter
13 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
14 | public class OrderPaymentCommand {
15 | @Schema(description = "결제금액", required = true, example = "1000")
16 | @NotNull
17 | private Long paymentMoney;
18 |
19 | public OrderPaymentCommand(Long paymentMoney) {
20 | this.paymentMoney = paymentMoney;
21 | }
22 |
23 | public void payment(OrderDto order) {
24 | order.payment(paymentMoney);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/package-info.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web;
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/product/ProductCreateCommand.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.product;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
4 | import lombok.AccessLevel;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 |
8 | import javax.validation.constraints.NotBlank;
9 |
10 | @Getter
11 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
12 | public class ProductCreateCommand {
13 | @NotBlank
14 | private String productName;
15 | @NotBlank
16 | private String productNo;
17 |
18 | public ProductCreateCommand(String productName, String productNo) {
19 | this.productName = productName;
20 | this.productNo = productNo;
21 | }
22 |
23 | public ProductDto createDto(Long id) {
24 | return new ProductDto(id, getProductName(), getProductNo());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/product/ProductModifyCommand.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.product;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
4 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductStatus;
5 | import lombok.AccessLevel;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 |
9 | import javax.validation.constraints.NotBlank;
10 | import javax.validation.constraints.NotNull;
11 |
12 | @Getter
13 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
14 | public class ProductModifyCommand {
15 | @NotBlank
16 | private String productName;
17 | @NotBlank
18 | private String productNo;
19 | @NotNull
20 | private ProductStatus productStatus;
21 |
22 | public ProductModifyCommand(String productName, String productNo, ProductStatus productStatus) {
23 | this.productName = productName;
24 | this.productNo = productNo;
25 | this.productStatus = productStatus;
26 | }
27 |
28 | public ProductDto modify(ProductDto source) {
29 | source.modify(getProductName(), getProductNo(), getProductStatus());
30 | return source;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/product/ProductRestController.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.product;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
4 | import com.kurly.tet.guide.springrestdocs.application.product.ProductFacade;
5 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageRequest;
6 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageResponse;
7 | import org.springframework.http.HttpStatus;
8 | import org.springframework.web.bind.annotation.*;
9 |
10 | import javax.validation.Valid;
11 |
12 | @RestController
13 | public class ProductRestController {
14 | private final ProductFacade productFacade;
15 |
16 | public ProductRestController(ProductFacade productFacade) {
17 | this.productFacade = productFacade;
18 | }
19 |
20 |
21 | @GetMapping("/products")
22 | public PageResponse search(ProductSearchCondition searchCondition, @Valid PageRequest pageRequest) {
23 | return productFacade.search(searchCondition, pageRequest);
24 | }
25 |
26 | @ResponseStatus(HttpStatus.CREATED)
27 | @PostMapping("/products")
28 | public ProductDto createProduct(@Valid @RequestBody ProductCreateCommand createCommand) {
29 | return productFacade.create(createCommand);
30 | }
31 |
32 | @GetMapping("/products/{productId}")
33 | public ProductDto getProduct(@PathVariable("productId") Long productId) {
34 | return productFacade.getProduct(productId);
35 | }
36 |
37 |
38 | @PutMapping("/products/{productId}")
39 | public ProductDto modifyProduct(@PathVariable Long productId, @Valid @RequestBody ProductModifyCommand modifyCommand) {
40 | return productFacade.modify(productId, modifyCommand);
41 | }
42 |
43 | @ResponseStatus(HttpStatus.NO_CONTENT)
44 | @DeleteMapping("/products/{productId}")
45 | public void deleteProduct(@PathVariable Long productId) {
46 | productFacade.remove(productId);
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/product/ProductSearchCondition.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.product;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
4 | import lombok.Getter;
5 |
6 | @Getter
7 | public class ProductSearchCondition {
8 | private String productName;
9 | private String productNo;
10 |
11 | public boolean hasProductName() {
12 | return getProductName() != null && !getProductName().isEmpty();
13 | }
14 |
15 | public boolean hasProductNo() {
16 | return getProductNo() != null && !getProductNo().isEmpty();
17 | }
18 |
19 | public boolean filter(ProductDto productDto) {
20 | if(hasProductName()) {
21 | return productDto.getProductName().equals(getProductNo());
22 | }
23 | if(hasProductNo()) {
24 | return productDto.getProductNo().equals(getProductNo());
25 | }
26 |
27 | return true;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/resources/application-springdoc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | spring:
3 | config:
4 | activate:
5 | on-profile: springdoc-local
6 |
7 | springdoc:
8 | api-docs:
9 | enabled: true
10 |
11 | ---
12 | spring:
13 | config:
14 | activate:
15 | on-profile: springdoc-test
16 |
17 | springdoc:
18 | api-docs:
19 | enabled: false
20 |
21 | ---
22 | spring:
23 | config:
24 | activate:
25 | on-profile: springdoc-dev
26 |
27 | springdoc:
28 | api-docs:
29 | enabled: true
30 |
31 | ---
32 | spring:
33 | config:
34 | activate:
35 | on-profile: springdoc-beta
36 |
37 | springdoc:
38 | api-docs:
39 | enabled: true
40 |
41 | ---
42 | spring:
43 | config:
44 | activate:
45 | on-profile: springdoc-prod
46 |
47 | springdoc:
48 | api-docs:
49 | enabled: false
50 |
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | profiles:
3 | include:
4 | - springdoc
5 | group:
6 | local: springdoc-local
7 | test: springdoc-test
8 | dev: springdoc-dev
9 | beta: springdoc-beta
10 | prod: springdoc-prod
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/ApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs;
2 |
3 | import org.junit.jupiter.api.Disabled;
4 | import org.junit.jupiter.api.Test;
5 | import org.springframework.boot.test.context.SpringBootTest;
6 |
7 | /**
8 | * 애플리케이션 ApplicationContext 정상적재 확인용
9 | */
10 | @Disabled("애플리케이션 ApplicationContext 정상적재: 확인됨")
11 | @SpringBootTest
12 | class ApplicationTests {
13 |
14 | @Test
15 | void contextLoads() {
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/annotations/MockTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.annotations;
2 |
3 | import org.junit.jupiter.api.extension.ExtendWith;
4 | import org.mockito.junit.jupiter.MockitoExtension;
5 | import org.springframework.restdocs.RestDocumentationExtension;
6 |
7 | import java.lang.annotation.*;
8 |
9 | @Target(ElementType.TYPE)
10 | @Retention(RetentionPolicy.RUNTIME)
11 | @Documented
12 | @ExtendWith({MockitoExtension.class})
13 | public @interface MockTest {
14 | }
15 |
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/annotations/RestDocsTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.annotations;
2 |
3 | import org.junit.jupiter.api.Tag;
4 | import org.junit.jupiter.api.extension.ExtendWith;
5 | import org.mockito.junit.jupiter.MockitoExtension;
6 | import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
8 | import org.springframework.restdocs.RestDocumentationExtension;
9 |
10 | import java.lang.annotation.*;
11 |
12 | /**
13 | * @see AutoConfigureMockMvc
14 | * @See AutoConfigureRestDocs
15 | */
16 | @Target(ElementType.TYPE)
17 | @Retention(RetentionPolicy.RUNTIME)
18 | @Documented
19 | @Tag("restDocs")
20 | @ExtendWith({MockitoExtension.class, RestDocumentationExtension.class})
21 | @AutoConfigureMockMvc
22 | @AutoConfigureRestDocs
23 | public @interface RestDocsTest {
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/application/order/OrderFacadeTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.application.order;
2 |
3 | import com.kurly.tet.guide.springrestdocs.annotations.MockTest;
4 | import com.kurly.tet.guide.springrestdocs.application.product.ProductFacade;
5 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderStatus;
6 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.order.command.OrderCreateCommand;
7 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.order.command.OrderPaymentCommand;
8 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.product.ProductCreateCommand;
9 | import org.assertj.core.api.SoftAssertions;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.DisplayName;
12 | import org.junit.jupiter.api.Test;
13 | import org.mockito.Spy;
14 |
15 | import java.util.List;
16 |
17 | import static org.assertj.core.api.Assertions.assertThat;
18 | import static org.assertj.core.api.Assertions.catchThrowable;
19 |
20 | @MockTest
21 | class OrderFacadeTest {
22 | @Spy
23 | ProductFacade productFacade;
24 | OrderFacade orderFacade;
25 |
26 | @BeforeEach
27 | void setUp() {
28 | productFacade = new ProductFacade();
29 | var createCommand = new ProductCreateCommand("TEST", "TEST01");
30 | productFacade.create(createCommand);
31 |
32 | orderFacade = new OrderFacade(productFacade);
33 | }
34 |
35 | @DisplayName("주문: 주문완료(생성)")
36 | @Test
37 | void testCreate() {
38 | //tiven
39 | long productId = 1L;
40 | var createCommand = new OrderCreateCommand("202209240001", List.of(productId));
41 |
42 | //when
43 | var createdOrder = orderFacade.create(createCommand);
44 |
45 | //then
46 | SoftAssertions.assertSoftly(softly -> {
47 | softly.assertThat(createdOrder.getMemberNo()).isEqualTo(createCommand.getMemberNo());
48 | softly.assertThat(createdOrder.getProducts().get(0).getId()).isEqualTo(productId);
49 | });
50 |
51 | }
52 |
53 | @DisplayName("주문: 결제완료")
54 | @Test
55 | void testPayment() {
56 | //given
57 | long productId = 1L;
58 | var createCommand = new OrderCreateCommand("202209240001", List.of(productId));
59 | var createdOrder = orderFacade.create(createCommand);
60 |
61 | //when
62 | OrderPaymentCommand paymentCommand = new OrderPaymentCommand(1000L);
63 | var paidOrder = orderFacade.payment(createdOrder.getOrderNo(), paymentCommand);
64 |
65 | //then
66 | SoftAssertions.assertSoftly(softly -> {
67 | softly.assertThat(paidOrder.getOrderStatus()).isEqualTo(OrderStatus.PAID);
68 | softly.assertThat(paidOrder.getPaymentMoney()).isEqualTo(paymentCommand.getPaymentMoney());
69 | softly.assertThat(paidOrder.getPaymentDateTime()).isNotNull();
70 | });
71 | }
72 |
73 | @DisplayName("주문: 출하완료 처리")
74 | @Test
75 | void testShipping() {
76 | //given
77 | long productId = 1L;
78 | var createCommand = new OrderCreateCommand("202209240001", List.of(productId));
79 | var createdOrder = orderFacade.create(createCommand);
80 |
81 | OrderPaymentCommand paymentCommand = new OrderPaymentCommand(1000L);
82 | var paidOrder = orderFacade.payment(createdOrder.getOrderNo(), paymentCommand);
83 |
84 | var shippingOrder = orderFacade.shipping(paidOrder.getOrderNo());
85 |
86 | //expect
87 | SoftAssertions.assertSoftly(softly -> {
88 | softly.assertThat(paidOrder.getOrderStatus()).isEqualTo(OrderStatus.SHIPPED);
89 | softly.assertThat(paidOrder.getShipmentDateTime()).isNotNull();
90 | });
91 | }
92 |
93 | @Test
94 | @DisplayName("생성상태에서 출하로 변경시 오류 발생")
95 | void testShippingWhenCreatedOccurException() {
96 | //given
97 | long productId = 1L;
98 | var createCommand = new OrderCreateCommand("202209240001", List.of(productId));
99 | var createdOrder = orderFacade.create(createCommand);
100 |
101 | //expect
102 | Throwable thrown = catchThrowable(() -> orderFacade.shipping(createdOrder.getOrderNo()));
103 | assertThat(thrown)
104 | .isInstanceOf(IllegalArgumentException.class)
105 | .hasMessage("주문상태가 '결제완료' 상태여야 합니다.");
106 | }
107 |
108 | @DisplayName("주문: 배송완료 처리")
109 | @Test
110 | void testComplete() {
111 | //given
112 | long productId = 1L;
113 | var createCommand = new OrderCreateCommand("202209240001", List.of(productId));
114 | var createdOrder = orderFacade.create(createCommand);
115 |
116 | OrderPaymentCommand paymentCommand = new OrderPaymentCommand(1000L);
117 | var paidOrder = orderFacade.payment(createdOrder.getOrderNo(), paymentCommand);
118 |
119 | var shippingOrder = orderFacade.shipping(paidOrder.getOrderNo());
120 |
121 | //when
122 | var completedOrder = orderFacade.complete(shippingOrder.getOrderNo());
123 |
124 | //then
125 | SoftAssertions.assertSoftly(softly -> {
126 | softly.assertThat(completedOrder.getOrderStatus()).isEqualTo(OrderStatus.COMPLETED);
127 | softly.assertThat(completedOrder.getCompletedDateTime()).isNotNull();
128 | });
129 | }
130 | }
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/documenation/DocumentFormatGenerator.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.documenation;
2 |
3 | import org.springframework.restdocs.snippet.Attributes;
4 |
5 | import java.util.Arrays;
6 | import java.util.List;
7 | import java.util.function.Function;
8 | import java.util.stream.Collectors;
9 |
10 | import static org.springframework.restdocs.snippet.Attributes.key;
11 |
12 | public class DocumentFormatGenerator {
13 | public DocumentFormatGenerator() {
14 | throw new UnsupportedOperationException("Util class.");
15 | }
16 |
17 | public static Attributes.Attribute required() {
18 | return key("required").value("true");
19 | }
20 |
21 | public static Attributes.Attribute optional() {
22 | return key("required").value("false");
23 | }
24 |
25 | public static Attributes.Attribute customFormat(String format) {
26 | return key("format").value(format);
27 | }
28 |
29 | public static Attributes.Attribute emptyFormat() {
30 | return customFormat("");
31 | }
32 |
33 | public static Attributes.Attribute dateTimeFormat() {
34 | return key("format").value("yyyy-MM-dd HH:mm:ss");
35 | }
36 |
37 | public static Attributes.Attribute dateFormat() {
38 | return key("format").value("yyyy-MM-dd");
39 | }
40 |
41 | public static Attributes.Attribute timeFormat() {
42 | return key("format").value("HH:mm:ss");
43 | }
44 |
45 | /**
46 | * Enum 타입 문자열 출력
47 | *
48 | * @param enumClass enum클래스
49 | * @param 대상타입
50 | * @return "* `A`"
51 | */
52 | public static > String generatedEnums(Class enumClass) {
53 | return Arrays.stream(enumClass.getEnumConstants())
54 | .map(el -> "* `" + el.name() + "`")
55 | .collect(Collectors.joining("\n"));
56 | }
57 |
58 | /**
59 | * Enum 타입 문자열 출력
60 | *
61 | * @param enumClass enum클래스
62 | * @param detailFun 문자열 출력 함수 (ex: Enum::getDescription)
63 | * @param 변환하고자 하는 Enum 타입
64 | * @return "A(a 설명),\n B(b 설명)"
65 | */
66 | public static > Attributes.Attribute generateEnumAttrs(Class enumClass, Function detailFun) {
67 | var value = Arrays.stream(enumClass.getEnumConstants())
68 | .map(el -> "* `" + el.name() + "`(" + detailFun.apply(el) + ")")
69 | .collect(Collectors.joining("\n"));
70 | return key("format").value(value);
71 | }
72 |
73 | /**
74 | * Enum 타입 중 일부만 목록으로 노출하고 싶을 떄 사용
75 | *
76 | * @param enumClassList 대상 enum클래스 목록
77 | * @param detailFun 문자열 출력함수
78 | * @param 변환하고자 하는 enum 타입
79 | * @return "* `A`(a 설명),\n* `B`(b 설명)"
80 | */
81 | public static > Attributes.Attribute generateEnumListFormatAttribute(List enumClassList, Function detailFun) {
82 | var value = enumClassList.stream()
83 | .map(el -> "* `" + el.name() + "`(" + detailFun.apply(el) + ")")
84 | .collect(Collectors.joining("\n"));
85 |
86 | return key("format").value(value);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/documenation/DocumentFormatGeneratorTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.documenation;
2 |
3 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductStatus;
4 | import org.junit.jupiter.api.DisplayName;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.util.List;
8 |
9 | import static com.kurly.tet.guide.springrestdocs.domain.product.ProductStatus.ACTIVATED;
10 | import static com.kurly.tet.guide.springrestdocs.domain.product.ProductStatus.CREATED;
11 | import static org.assertj.core.api.Assertions.assertThat;
12 |
13 | /**
14 | * enum 타입 아스키독 목록 스니펫 사용 문자형식 생성
15 | */
16 | @DisplayName("지정된 enum 타입을 아스키독문법 목록() 형태로 출력")
17 | class DocumentFormatGeneratorTest {
18 | @DisplayName("속성값만 나열함")
19 | @Test
20 | void testGeneratedEnums() {
21 | var result = DocumentFormatGenerator.generatedEnums(ProductStatus.class);
22 |
23 | assertThat(result).isEqualTo("""
24 | * `CREATED`
25 | * `ACTIVATED`
26 | * `ARCHIVED`""");
27 | }
28 |
29 | @DisplayName("속성값(설명) 나열함")
30 | @Test
31 | void testGeneratedEnumAttr() {
32 | var attrs = DocumentFormatGenerator.generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription);
33 |
34 | assertThat(attrs.getValue()).isEqualTo("""
35 | * `CREATED`(생성)
36 | * `ACTIVATED`(활성화)
37 | * `ARCHIVED`(보관처리됨)""");
38 | }
39 |
40 | @DisplayName("지정된 속성값(설명) 나열함")
41 | @Test
42 | void testGeneratedEnumListFormat() {
43 | var failed = List.of(CREATED, ACTIVATED);
44 |
45 | var formatAttr = DocumentFormatGenerator.generateEnumListFormatAttribute(failed, ProductStatus::getDescription);
46 |
47 | assertThat(formatAttr.getValue()).isEqualTo(
48 | """
49 | * `CREATED`(생성)
50 | * `ACTIVATED`(활성화)""");
51 | }
52 | }
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/documenation/DocumentUtils.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.documenation;
2 |
3 | import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
4 | import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;
5 |
6 | import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
7 |
8 | public class DocumentUtils {
9 | public static OperationRequestPreprocessor getDocumentRequest() {
10 | return preprocessRequest(
11 | prettyPrint());
12 | }
13 |
14 | public static OperationResponsePreprocessor getDocumentResponse() {
15 | return preprocessResponse(prettyPrint());
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/documenation/MockMvcFactory.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.documenation;
2 |
3 | import com.kurly.tet.guide.springrestdocs.common.util.JsonUtils;
4 | import com.kurly.tet.guide.springrestdocs.common.util.LocalDateTimeUtils;
5 | import com.kurly.tet.guide.springrestdocs.common.util.LocalDateUtils;
6 | import com.kurly.tet.guide.springrestdocs.common.util.LocalTimeUtils;
7 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.advisor.ApiResponseWrappingAdvisor;
8 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.handler.GlobalRestControllerExceptionHandler;
9 | import org.springframework.core.convert.converter.Converter;
10 | import org.springframework.format.support.DefaultFormattingConversionService;
11 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
12 | import org.springframework.restdocs.RestDocumentationContextProvider;
13 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
14 | import org.springframework.test.web.servlet.MockMvc;
15 | import org.springframework.test.web.servlet.setup.MockMvcBuilders;
16 | import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
17 | import org.springframework.web.filter.CharacterEncodingFilter;
18 |
19 | import java.nio.charset.StandardCharsets;
20 | import java.time.LocalDate;
21 | import java.time.LocalDateTime;
22 | import java.time.LocalTime;
23 | import java.time.format.DateTimeFormatter;
24 |
25 | public class MockMvcFactory {
26 | public static MockMvc getMockMvc(Object... controllers) {
27 | return getMockMvcBuilder(controllers).build();
28 | }
29 |
30 | public static MockMvc getRestDocsMockMvc(RestDocumentationContextProvider restDocumentationContextProvider, String host, Object... controllers) {
31 | var documentationConfigurer = MockMvcRestDocumentation.documentationConfiguration(restDocumentationContextProvider);
32 | documentationConfigurer.uris().withScheme("https").withHost(host).withPort(443);
33 |
34 | return getMockMvcBuilder(controllers).apply(documentationConfigurer).build();
35 | }
36 |
37 | private static StandaloneMockMvcBuilder getMockMvcBuilder(Object... controllers) {
38 | var conversionService = new DefaultFormattingConversionService();
39 | conversionService.addConverter(new LocalDateTimeConverter());
40 | conversionService.addConverter(new LocalDateConverter());
41 | conversionService.addConverter(new LocalTimeConverter());
42 |
43 | return MockMvcBuilders.standaloneSetup(controllers)
44 | .setControllerAdvice(
45 | new GlobalRestControllerExceptionHandler(),
46 | new ApiResponseWrappingAdvisor())
47 | .setConversionService(conversionService)
48 | .setMessageConverters(new MappingJackson2HttpMessageConverter(JsonUtils.getMapper()))
49 | .addFilter(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true));
50 | }
51 |
52 | public static class LocalDateTimeConverter implements Converter {
53 | @Override
54 | public LocalDateTime convert(String source) {
55 | return LocalDateTimeUtils.toLocalDateTime(source);
56 | }
57 | }
58 |
59 | public static class LocalDateConverter implements Converter {
60 |
61 | @Override
62 | public LocalDate convert(String source) {
63 | return LocalDateUtils.toLocalDate(source);
64 | }
65 | }
66 |
67 | public static class LocalTimeConverter implements Converter {
68 | @Override
69 | public LocalTime convert(String source) {
70 | return LocalTimeUtils.toLocalTime(source);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/dto/PageResponseTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.dto;
2 |
3 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageRequest;
4 | import org.junit.jupiter.api.DisplayName;
5 | import org.junit.jupiter.params.ParameterizedTest;
6 | import org.junit.jupiter.params.provider.Arguments;
7 | import org.junit.jupiter.params.provider.MethodSource;
8 |
9 | import java.util.Collections;
10 | import java.util.List;
11 | import java.util.Optional;
12 | import java.util.stream.Stream;
13 |
14 | import static org.assertj.core.api.Assertions.assertThat;
15 |
16 | class PageResponseTest {
17 | @DisplayName("총갯수 계산")
18 | @MethodSource("caseCalculateTotal")
19 | @ParameterizedTest
20 | void test(List content, PageRequest pageRequest, long total, long expectTotalCount) {
21 | var totalCount = Optional.of(pageRequest)
22 | .filter(it -> !content.isEmpty())
23 | .filter(it -> pageRequest.getOffset() + it.getSize() > total)
24 | .map(it -> pageRequest.getOffset() + content.size())
25 | .orElse(total);
26 |
27 | assertThat(totalCount).isEqualTo(expectTotalCount);
28 | }
29 |
30 | private static Stream caseCalculateTotal() {
31 | return Stream.of(
32 | Arguments.of(Collections.emptyList(), PageRequest.of(0, 10), 0L, 0L),
33 | Arguments.of(List.of("1"), PageRequest.of(0, 10), 1L, 1L),
34 | Arguments.of(List.of("1", "2", "3", "4", "5", "6", "7", "8", "9"), PageRequest.of(0, 10), 9L, 9L),
35 | Arguments.of(List.of("11"), PageRequest.of(1, 10), 11L, 11L),
36 | Arguments.of(List.of("11", "12"), PageRequest.of(1, 10), 12L, 12L)
37 | );
38 | }
39 | }
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/order/OrderRestControllerDocsTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.order;
2 |
3 | import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
4 | import com.epages.restdocs.apispec.ResourceSnippetParameters;
5 | import com.kurly.tet.guide.springrestdocs.annotations.RestDocsTest;
6 | import com.kurly.tet.guide.springrestdocs.application.order.OrderFacade;
7 | import com.kurly.tet.guide.springrestdocs.common.util.JsonUtils;
8 | import com.kurly.tet.guide.springrestdocs.documenation.MockMvcFactory;
9 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderDto;
10 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderStatus;
11 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductStatus;
12 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageResponse;
13 | import org.junit.jupiter.api.DisplayName;
14 | import org.junit.jupiter.api.Test;
15 | import org.mockito.InjectMocks;
16 | import org.mockito.Mock;
17 | import org.mockito.Mockito;
18 | import org.springframework.http.MediaType;
19 | import org.springframework.restdocs.RestDocumentationContextProvider;
20 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
21 | import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
22 | import org.springframework.restdocs.payload.FieldDescriptor;
23 | import org.springframework.restdocs.request.ParameterDescriptor;
24 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
25 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
26 |
27 | import java.util.UUID;
28 |
29 | import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
30 | import static com.kurly.tet.guide.springrestdocs.config.Constant.*;
31 | import static com.kurly.tet.guide.springrestdocs.documenation.DocumentFormatGenerator.customFormat;
32 | import static com.kurly.tet.guide.springrestdocs.documenation.DocumentFormatGenerator.generateEnumAttrs;
33 | import static com.kurly.tet.guide.springrestdocs.documenation.DocumentUtils.getDocumentRequest;
34 | import static com.kurly.tet.guide.springrestdocs.documenation.DocumentUtils.getDocumentResponse;
35 | import static org.springframework.restdocs.payload.JsonFieldType.*;
36 | import static org.springframework.restdocs.payload.PayloadDocumentation.*;
37 | import static org.springframework.restdocs.request.RequestDocumentation.*;
38 |
39 | @DisplayName("주문API 문서화")
40 | @RestDocsTest
41 | class OrderRestControllerDocsTest {
42 | @Mock
43 | private OrderFacade orderFacade;
44 | @InjectMocks
45 | private OrderRestController controller;
46 |
47 | @DisplayName("검색: 주문정보 조회")
48 | @Test
49 | void test01(RestDocumentationContextProvider contextProvider) throws Exception {
50 | var responseSource = """
51 | {
52 | "content":[
53 | {
54 | "id":1,
55 | "memberNo":"1111",
56 | "orderNo":"202209251659441",
57 | "orderStatus":"COMPLETED",
58 | "orderStatusDescription":"배송완료",
59 | "products":[
60 | {
61 | "id":1,
62 | "productName":"TEST2",
63 | "productNo":"00003",
64 | "productStatus":"CREATED",
65 | "created":"2022-09-25T16:59:23",
66 | "modified":"2022-09-25T16:59:23",
67 | "productStatusDescription":"생성"
68 | },
69 | {
70 | "id":4,
71 | "productName":"TEST5",
72 | "productNo":"00006",
73 | "productStatus":"CREATED",
74 | "created":"2022-09-25T16:59:23",
75 | "modified":"2022-09-25T16:59:23",
76 | "productStatusDescription":"생성"
77 | }
78 | ],
79 | "orderDateTime":"2022-09-25T16:59:44",
80 | "paymentMoney":1000,
81 | "paymentDateTime":"2022-09-25T17:00:46",
82 | "shipmentDateTime":"2022-09-25T17:00:59",
83 | "completedDateTime":"2022-09-25T17:01:17"
84 | },
85 | {
86 | "id":2,
87 | "memberNo":"1111",
88 | "orderNo":"202209251659492",
89 | "orderStatus":"SHIPPED",
90 | "orderStatusDescription":"출하완료 ",
91 | "products":[
92 | {
93 | "id":4,
94 | "productName":"TEST5",
95 | "productNo":"00006",
96 | "productStatus":"CREATED",
97 | "created":"2022-09-25T16:59:23",
98 | "modified":"2022-09-25T16:59:23",
99 | "productStatusDescription":"생성"
100 | },
101 | {
102 | "id":7,
103 | "productName":"TEST8",
104 | "productNo":"00009",
105 | "productStatus":"CREATED",
106 | "created":"2022-09-25T16:59:23",
107 | "modified":"2022-09-25T16:59:23",
108 | "productStatusDescription":"생성"
109 | }
110 | ],
111 | "orderDateTime":"2022-09-25T16:59:49",
112 | "paymentMoney":10001,
113 | "paymentDateTime":"2022-09-25T17:01:37",
114 | "shipmentDateTime":"2022-09-25T17:01:44",
115 | "completedDateTime":null
116 | },
117 | {
118 | "id":3,
119 | "memberNo":"1111",
120 | "orderNo":"202209251659563",
121 | "orderStatus":"PAID",
122 | "orderStatusDescription":"결제완료",
123 | "products":[
124 | {
125 | "id":7,
126 | "productName":"TEST8",
127 | "productNo":"00009",
128 | "productStatus":"CREATED",
129 | "created":"2022-09-25T16:59:23",
130 | "modified":"2022-09-25T16:59:23",
131 | "productStatusDescription":"생성"
132 | },
133 | {
134 | "id":10,
135 | "productName":"TEST11",
136 | "productNo":"00012",
137 | "productStatus":"CREATED",
138 | "created":"2022-09-25T16:59:23",
139 | "modified":"2022-09-25T16:59:23",
140 | "productStatusDescription":"생성"
141 | }
142 | ],
143 | "orderDateTime":"2022-09-25T16:59:56",
144 | "paymentMoney":1003,
145 | "paymentDateTime":"2022-09-25T17:02:20",
146 | "shipmentDateTime":null,
147 | "completedDateTime":null
148 | },
149 | {
150 | "id":4,
151 | "memberNo":"1111",
152 | "orderNo":"202209251700034",
153 | "orderStatus":"ORDERED",
154 | "orderStatusDescription":"주문완료",
155 | "products":[
156 | {
157 | "id":4,
158 | "productName":"TEST5",
159 | "productNo":"00006",
160 | "productStatus":"CREATED",
161 | "created":"2022-09-25T16:59:23",
162 | "modified":"2022-09-25T16:59:23",
163 | "productStatusDescription":"생성"
164 | },
165 | {
166 | "id":7,
167 | "productName":"TEST8",
168 | "productNo":"00009",
169 | "productStatus":"CREATED",
170 | "created":"2022-09-25T16:59:23",
171 | "modified":"2022-09-25T16:59:23",
172 | "productStatusDescription":"생성"
173 | },
174 | {
175 | "id":10,
176 | "productName":"TEST11",
177 | "productNo":"00012",
178 | "productStatus":"CREATED",
179 | "created":"2022-09-25T16:59:23",
180 | "modified":"2022-09-25T16:59:23",
181 | "productStatusDescription":"생성"
182 | }
183 | ],
184 | "orderDateTime":"2022-09-25T17:00:03",
185 | "paymentMoney":null,
186 | "paymentDateTime":null,
187 | "shipmentDateTime":null,
188 | "completedDateTime":null
189 | }
190 | ],
191 | "pageRequest":{
192 | "page":0,
193 | "size":1000
194 | },
195 | "total":4,
196 | "totalPages":1,
197 | "hasNext":false,
198 | "hasPrevious":false,
199 | "isFirst":true,
200 | "isLast":true,
201 | "hasContent":true
202 | }""";
203 |
204 | Mockito.when(orderFacade.search(Mockito.any(), Mockito.any()))
205 | .thenReturn(JsonUtils.fromJson(responseSource, PageResponse.class));
206 |
207 | var parameterDescriptors = new ParameterDescriptor[]{
208 | parameterWithName("page").description("페이지번호"),
209 | parameterWithName("size").description("페이지 건수"),
210 | parameterWithName("memberNo").description("회원번호"),
211 | parameterWithName("orderNo").description("주문번호")
212 | };
213 |
214 | var responseFieldDescriptors = new FieldDescriptor[]{
215 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
216 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
217 | fieldWithPath("data.content[].id").type(NUMBER).description("주문번호"),
218 | fieldWithPath("data.content[].memberNo").type(STRING).description("회원번호"),
219 | fieldWithPath("data.content[].orderNo").type(STRING).description("주문번호"),
220 | fieldWithPath("data.content[].orderStatus").type(STRING).attributes(generateEnumAttrs(OrderStatus.class, OrderStatus::getDescription)).description("주문상태"),
221 | fieldWithPath("data.content[].orderStatusDescription").type(STRING).description("주문상태설명"),
222 | fieldWithPath("data.content[].products[].id").type(NUMBER).description("상품일련번호"),
223 | fieldWithPath("data.content[].products[].productName").type(STRING).description("상품명"),
224 | fieldWithPath("data.content[].products[].productNo").type(STRING).description("상품번호"),
225 | fieldWithPath("data.content[].products[].productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
226 | fieldWithPath("data.content[].products[].productStatusDescription").type(STRING).description("상품상태설명"),
227 | fieldWithPath("data.content[].products[].created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
228 | fieldWithPath("data.content[].products[].modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시"),
229 | fieldWithPath("data.content[].orderDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("주문접수일시"),
230 | fieldWithPath("data.content[].paymentMoney").type(NUMBER).description("결제금액").optional(),
231 | fieldWithPath("data.content[].paymentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
232 | fieldWithPath("data.content[].shipmentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("출하완료일시").optional(),
233 | fieldWithPath("data.content[].completedDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
234 | fieldWithPath("data.pageRequest.page").type(NUMBER).description("페이지번호"),
235 | fieldWithPath("data.pageRequest.size").type(NUMBER).description("페이지크기"),
236 | fieldWithPath("data.total").type(NUMBER).description("데이터 전체갯수"),
237 | fieldWithPath("data.totalPages").type(NUMBER).description("전체 페이지갯수"),
238 | fieldWithPath("data.isFirst").type(BOOLEAN).description("시작페이지인가?"),
239 | fieldWithPath("data.isLast").type(BOOLEAN).description("마지막페이지인가?"),
240 | fieldWithPath("data.hasPrevious").type(BOOLEAN).description("앞페이지가있는가?"),
241 | fieldWithPath("data.hasNext").type(BOOLEAN).description("다음페이지가있는가?"),
242 | fieldWithPath("data.hasContent").type(BOOLEAN).description("컨텐츠가있는가?"),
243 | };
244 |
245 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
246 | .perform(RestDocumentationRequestBuilders.get("/orders")
247 | .param("page", "0")
248 | .param("size", "20")
249 | .param("memberNo", "20220925163201")
250 | .param("orderNo", UUID.randomUUID().toString())
251 | .contentType(MediaType.APPLICATION_JSON))
252 | .andDo(MockMvcResultHandlers.print())
253 | .andExpect(MockMvcResultMatchers.status().isOk())
254 | //REST Docs 용
255 | .andDo(MockMvcRestDocumentation.document("get-v1-get-orders",
256 | getDocumentRequest(),
257 | getDocumentResponse(),
258 | requestParameters(
259 | parameterDescriptors
260 | ),
261 | responseFields(
262 | responseFieldDescriptors
263 | )
264 | ))
265 | //OAS 3.0 - Swagger
266 | .andDo(MockMvcRestDocumentationWrapper.document("get-v1-get-orders",
267 | getDocumentRequest(),
268 | getDocumentResponse(),
269 | resource(ResourceSnippetParameters.builder()
270 | .requestParameters(
271 | parameterDescriptors
272 | )
273 | .responseFields(
274 | responseFieldDescriptors
275 | )
276 | .build())));
277 | }
278 |
279 | @DisplayName("주문생성")
280 | @Test
281 | void test02(RestDocumentationContextProvider contextProvider) throws Exception {
282 | var createCommandJson = """
283 | {
284 | "memberNo":"1111",
285 | "productIds":[1,4]
286 | }
287 | """;
288 |
289 | var requestFieldDescription = new FieldDescriptor[]{
290 | fieldWithPath("memberNo").type(STRING).description("회원번호"),
291 | fieldWithPath("productIds").type(ARRAY).description("상품번호목록")
292 | };
293 |
294 | var createdOrderJson = """
295 | {
296 | "id":1,
297 | "memberNo":"1111",
298 | "orderNo":"202209251659441",
299 | "orderStatus":"ORDERED",
300 | "orderStatusDescription":"주문완료",
301 | "products":[
302 | {
303 | "id":1,
304 | "productName":"TEST2",
305 | "productNo":"00003",
306 | "productStatus":"CREATED",
307 | "created":"2022-09-25T16:59:23",
308 | "modified":"2022-09-25T16:59:23",
309 | "productStatusDescription":"생성"
310 | },
311 | {
312 | "id":4,
313 | "productName":"TEST5",
314 | "productNo":"00006",
315 | "productStatus":"CREATED",
316 | "created":"2022-09-25T16:59:23",
317 | "modified":"2022-09-25T16:59:23",
318 | "productStatusDescription":"생성"
319 | }
320 | ],
321 | "orderDateTime":"2022-09-25T16:59:44",
322 | "paymentMoney":null,
323 | "paymentDateTime":null,
324 | "shipmentDateTime":null,
325 | "completedDateTime":null
326 | }""";
327 |
328 | Mockito.when(orderFacade.create(Mockito.any()))
329 | .thenReturn(JsonUtils.fromJson(createdOrderJson, OrderDto.class));
330 |
331 | var responseFieldDescription = new FieldDescriptor[]{
332 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
333 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
334 | fieldWithPath("data.id").type(NUMBER).description("주문번호"),
335 | fieldWithPath("data.memberNo").type(STRING).description("회원번호"),
336 | fieldWithPath("data.orderNo").type(STRING).description("주문번호"),
337 | fieldWithPath("data.orderStatus").type(STRING).attributes(generateEnumAttrs(OrderStatus.class, OrderStatus::getDescription)).description("주문상태"),
338 | fieldWithPath("data.orderStatusDescription").type(STRING).description("주문상태설명"),
339 | fieldWithPath("data.products[].id").type(NUMBER).description("상품일련번호"),
340 | fieldWithPath("data.products[].productName").type(STRING).description("상품명"),
341 | fieldWithPath("data.products[].productNo").type(STRING).description("상품번호"),
342 | fieldWithPath("data.products[].productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
343 | fieldWithPath("data.products[].productStatusDescription").type(STRING).description("상품상태설명"),
344 | fieldWithPath("data.products[].created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
345 | fieldWithPath("data.products[].modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시"),
346 | fieldWithPath("data.orderDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("주문접수일시"),
347 | fieldWithPath("data.paymentMoney").type(NUMBER).description("결제금액").optional(),
348 | fieldWithPath("data.paymentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
349 | fieldWithPath("data.shipmentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("출하완료일시").optional(),
350 | fieldWithPath("data.completedDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
351 | };
352 |
353 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
354 | .perform(RestDocumentationRequestBuilders.post("/orders")
355 | .contentType(MediaType.APPLICATION_JSON)
356 | .content(createCommandJson)
357 | )
358 | .andDo(MockMvcResultHandlers.print())
359 | .andExpect(MockMvcResultMatchers.status().isCreated())
360 | //REST Docs 용
361 | .andDo(MockMvcRestDocumentation.document("post-v1-create-order",
362 | getDocumentRequest(),
363 | getDocumentResponse(),
364 | requestFields(
365 | requestFieldDescription
366 | ),
367 | responseFields(
368 | responseFieldDescription
369 | )
370 | ))
371 | //OAS 3.0 - Swagger
372 | .andDo(MockMvcRestDocumentationWrapper.document("post-v1-create-order",
373 | getDocumentRequest(),
374 | getDocumentResponse(),
375 | resource(ResourceSnippetParameters.builder()
376 | .requestFields(
377 | requestFieldDescription
378 | )
379 | .responseFields(
380 | responseFieldDescription
381 | )
382 | .build())));
383 | }
384 |
385 | @DisplayName("결제완료")
386 | @Test
387 | void test03(RestDocumentationContextProvider contextProvider) throws Exception {
388 | var orderNo = "797a1eb9-d6b8-4875-9ac6-4cbeac65ba40";
389 | var commandJson = """
390 | {
391 | "paymentMoney": 1000
392 | }
393 | """;
394 | var expectedContent = "{\"code\":\"0000\",\"message\":\"정상\",\"data\":{\"id\":1,\"memberNo\":\"1111\",\"orderNo\":\"202209251659441\",\"orderStatus\":\"PAID\",\"products\":[{\"id\":1,\"productName\":\"TEST2\",\"productNo\":\"00003\",\"productStatus\":\"CREATED\",\"created\":\"2022-09-25T16:59:23\",\"modified\":\"2022-09-25T16:59:23\",\"productStatusDescription\":\"생성\"},{\"id\":4,\"productName\":\"TEST5\",\"productNo\":\"00006\",\"productStatus\":\"CREATED\",\"created\":\"2022-09-25T16:59:23\",\"modified\":\"2022-09-25T16:59:23\",\"productStatusDescription\":\"생성\"}],\"orderDateTime\":\"2022-09-25T16:59:44\",\"paymentMoney\":1000,\"paymentDateTime\":\"2022-09-25T17:00:46\",\"shipmentDateTime\":null,\"completedDateTime\":null,\"orderStatusDescription\":\"결제완료\"}}";
395 | var parameterDescriptor = parameterWithName("orderNo").description("주문번호");
396 | var requestFieldDescription = new FieldDescriptor[]{
397 | fieldWithPath("paymentMoney").type(NUMBER).description("결제금액")
398 | };
399 |
400 | var orderJson = """
401 | {
402 | "id":1,
403 | "memberNo":"1111",
404 | "orderNo":"202209251659441",
405 | "orderStatus":"PAID",
406 | "orderStatusDescription":"결제완료",
407 | "products":[
408 | {
409 | "id":1,
410 | "productName":"TEST2",
411 | "productNo":"00003",
412 | "productStatus":"CREATED",
413 | "created":"2022-09-25T16:59:23",
414 | "modified":"2022-09-25T16:59:23",
415 | "productStatusDescription":"생성"
416 | },
417 | {
418 | "id":4,
419 | "productName":"TEST5",
420 | "productNo":"00006",
421 | "productStatus":"CREATED",
422 | "created":"2022-09-25T16:59:23",
423 | "modified":"2022-09-25T16:59:23",
424 | "productStatusDescription":"생성"
425 | }
426 | ],
427 | "orderDateTime":"2022-09-25T16:59:44",
428 | "paymentMoney":1000,
429 | "paymentDateTime":"2022-09-25T17:00:46",
430 | "shipmentDateTime":null,
431 | "completedDateTime":null
432 | }""";
433 |
434 | Mockito.when(orderFacade.payment(Mockito.any(), Mockito.any()))
435 | .thenReturn(JsonUtils.fromJson(orderJson, OrderDto.class));
436 |
437 | var responseFieldDescription = new FieldDescriptor[]{
438 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
439 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
440 | fieldWithPath("data.id").type(NUMBER).description("주문번호"),
441 | fieldWithPath("data.memberNo").type(STRING).description("회원번호"),
442 | fieldWithPath("data.orderNo").type(STRING).description("주문번호"),
443 | fieldWithPath("data.orderStatus").type(STRING).attributes(generateEnumAttrs(OrderStatus.class, OrderStatus::getDescription)).description("주문상태"),
444 | fieldWithPath("data.orderStatusDescription").type(STRING).description("주문상태설명"),
445 | fieldWithPath("data.products[].id").type(NUMBER).description("상품일련번호"),
446 | fieldWithPath("data.products[].productName").type(STRING).description("상품명"),
447 | fieldWithPath("data.products[].productNo").type(STRING).description("상품번호"),
448 | fieldWithPath("data.products[].productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
449 | fieldWithPath("data.products[].productStatusDescription").type(STRING).description("상품상태설명"),
450 | fieldWithPath("data.products[].created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
451 | fieldWithPath("data.products[].modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시"),
452 | fieldWithPath("data.orderDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("주문접수일시"),
453 | fieldWithPath("data.paymentMoney").type(NUMBER).description("결제금액").optional(),
454 | fieldWithPath("data.paymentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
455 | fieldWithPath("data.shipmentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("출하완료일시").optional(),
456 | fieldWithPath("data.completedDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
457 | };
458 |
459 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
460 | .perform(RestDocumentationRequestBuilders.put("/orders/{orderNo}/payment", orderNo)
461 | .contentType(MediaType.APPLICATION_JSON)
462 | .content(commandJson)
463 | )
464 | .andDo(MockMvcResultHandlers.print())
465 | .andExpect(MockMvcResultMatchers.status().isOk())
466 | .andExpect(MockMvcResultMatchers.content().string(expectedContent))
467 | //REST Docs 용
468 | .andDo(MockMvcRestDocumentation.document("put-v1-payment-order",
469 | getDocumentRequest(),
470 | getDocumentResponse(),
471 | pathParameters(
472 | parameterDescriptor
473 | ),
474 | requestFields(
475 | requestFieldDescription
476 | ),
477 | responseFields(
478 | responseFieldDescription
479 | )
480 | ))
481 | //OAS 3.0 - Swagger
482 | .andDo(MockMvcRestDocumentationWrapper.document("put-v1-payment-order",
483 | getDocumentRequest(),
484 | getDocumentResponse(),
485 | resource(ResourceSnippetParameters.builder()
486 | .pathParameters(
487 | parameterDescriptor
488 | )
489 | .requestFields(
490 | requestFieldDescription
491 | )
492 | .responseFields(
493 | responseFieldDescription
494 | )
495 | .build())));
496 | }
497 |
498 | @DisplayName("주문상세조회")
499 | @Test
500 | void test06(RestDocumentationContextProvider contextProvider) throws Exception {
501 | var orderNo = "797a1eb9-d6b8-4875-9ac6-4cbeac65ba40";
502 | var expectedContent = "{\"code\":\"0000\",\"message\":\"정상\",\"data\":{\"id\":1,\"memberNo\":\"1111\",\"orderNo\":\"202209251659441\",\"orderStatus\":\"PAID\",\"products\":[{\"id\":1,\"productName\":\"TEST2\",\"productNo\":\"00003\",\"productStatus\":\"CREATED\",\"created\":\"2022-09-25T16:59:23\",\"modified\":\"2022-09-25T16:59:23\",\"productStatusDescription\":\"생성\"},{\"id\":4,\"productName\":\"TEST5\",\"productNo\":\"00006\",\"productStatus\":\"CREATED\",\"created\":\"2022-09-25T16:59:23\",\"modified\":\"2022-09-25T16:59:23\",\"productStatusDescription\":\"생성\"}],\"orderDateTime\":\"2022-09-25T16:59:44\",\"paymentMoney\":1000,\"paymentDateTime\":\"2022-09-25T17:00:46\",\"shipmentDateTime\":null,\"completedDateTime\":null,\"orderStatusDescription\":\"결제완료\"}}";
503 | var parameterDescriptor = parameterWithName("orderNo").description("주문번호");
504 |
505 | var orderJson = """
506 | {
507 | "id":1,
508 | "memberNo":"1111",
509 | "orderNo":"202209251659441",
510 | "orderStatus":"PAID",
511 | "orderStatusDescription":"결제완료",
512 | "products":[
513 | {
514 | "id":1,
515 | "productName":"TEST2",
516 | "productNo":"00003",
517 | "productStatus":"CREATED",
518 | "created":"2022-09-25T16:59:23",
519 | "modified":"2022-09-25T16:59:23",
520 | "productStatusDescription":"생성"
521 | },
522 | {
523 | "id":4,
524 | "productName":"TEST5",
525 | "productNo":"00006",
526 | "productStatus":"CREATED",
527 | "created":"2022-09-25T16:59:23",
528 | "modified":"2022-09-25T16:59:23",
529 | "productStatusDescription":"생성"
530 | }
531 | ],
532 | "orderDateTime":"2022-09-25T16:59:44",
533 | "paymentMoney":1000,
534 | "paymentDateTime":"2022-09-25T17:00:46",
535 | "shipmentDateTime":null,
536 | "completedDateTime":null
537 | }""";
538 |
539 | Mockito.when(orderFacade.findByOrderNo(Mockito.any()))
540 | .thenReturn(JsonUtils.fromJson(orderJson, OrderDto.class));
541 |
542 | var responseFieldDescription = new FieldDescriptor[]{
543 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
544 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
545 | fieldWithPath("data.id").type(NUMBER).description("주문번호"),
546 | fieldWithPath("data.memberNo").type(STRING).description("회원번호"),
547 | fieldWithPath("data.orderNo").type(STRING).description("주문번호"),
548 | fieldWithPath("data.orderStatus").type(STRING).attributes(generateEnumAttrs(OrderStatus.class, OrderStatus::getDescription)).description("주문상태"),
549 | fieldWithPath("data.orderStatusDescription").type(STRING).description("주문상태설명"),
550 | fieldWithPath("data.products[].id").type(NUMBER).description("상품일련번호"),
551 | fieldWithPath("data.products[].productName").type(STRING).description("상품명"),
552 | fieldWithPath("data.products[].productNo").type(STRING).description("상품번호"),
553 | fieldWithPath("data.products[].productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
554 | fieldWithPath("data.products[].productStatusDescription").type(STRING).description("상품상태설명"),
555 | fieldWithPath("data.products[].created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
556 | fieldWithPath("data.products[].modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시"),
557 | fieldWithPath("data.orderDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("주문접수일시"),
558 | fieldWithPath("data.paymentMoney").type(NUMBER).description("결제금액").optional(),
559 | fieldWithPath("data.paymentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
560 | fieldWithPath("data.shipmentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("출하완료일시").optional(),
561 | fieldWithPath("data.completedDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
562 | };
563 |
564 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
565 | .perform(RestDocumentationRequestBuilders.get("/orders/{orderNo}", orderNo)
566 | .contentType(MediaType.APPLICATION_JSON)
567 | )
568 | .andDo(MockMvcResultHandlers.print())
569 | .andExpect(MockMvcResultMatchers.status().isOk())
570 | .andExpect(MockMvcResultMatchers.content().string(expectedContent))
571 | //REST Docs 용
572 | .andDo(MockMvcRestDocumentation.document("get-v1-get-order",
573 | getDocumentRequest(),
574 | getDocumentResponse(),
575 | pathParameters(
576 | parameterDescriptor
577 | ),
578 | responseFields(
579 | responseFieldDescription
580 | )
581 | ))
582 | //OAS 3.0 - Swagger
583 | .andDo(MockMvcRestDocumentationWrapper.document("get-v1-get-order",
584 | getDocumentRequest(),
585 | getDocumentResponse(),
586 | resource(ResourceSnippetParameters.builder()
587 | .pathParameters(
588 | parameterDescriptor
589 | )
590 | .responseFields(
591 | responseFieldDescription
592 | )
593 | .build())));
594 | }
595 |
596 | @DisplayName("출하완료")
597 | @Test
598 | void test04(RestDocumentationContextProvider contextProvider) throws Exception {
599 | var orderNo = "797a1eb9-d6b8-4875-9ac6-4cbeac65ba40";
600 |
601 | var orderJson = """
602 | {
603 | "id":1,
604 | "memberNo":"1111",
605 | "orderNo":"202209251659441",
606 | "orderStatus":"SHIPPED",
607 | "orderStatusDescription":"출하완료",
608 | "products":[
609 | {
610 | "id":1,
611 | "productName":"TEST2",
612 | "productNo":"00003",
613 | "productStatus":"CREATED",
614 | "created":"2022-09-25T16:59:23",
615 | "modified":"2022-09-25T16:59:23",
616 | "productStatusDescription":"생성"
617 | },
618 | {
619 | "id":4,
620 | "productName":"TEST5",
621 | "productNo":"00006",
622 | "productStatus":"CREATED",
623 | "created":"2022-09-25T16:59:23",
624 | "modified":"2022-09-25T16:59:23",
625 | "productStatusDescription":"생성"
626 | }
627 | ],
628 | "orderDateTime":"2022-09-25T16:59:44",
629 | "paymentMoney":1000,
630 | "paymentDateTime":"2022-09-25T17:00:46",
631 | "shipmentDateTime":"2022-09-25T17:00:50",
632 | "completedDateTime":null
633 | }""";
634 |
635 | Mockito.when(orderFacade.shipping(Mockito.any()))
636 | .thenReturn(JsonUtils.fromJson(orderJson, OrderDto.class));
637 |
638 | var parameterDescriptor = parameterWithName("orderNo").description("주문번호");
639 |
640 | var responseFieldDescription = new FieldDescriptor[]{
641 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
642 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
643 | fieldWithPath("data.id").type(NUMBER).description("주문번호"),
644 | fieldWithPath("data.memberNo").type(STRING).description("회원번호"),
645 | fieldWithPath("data.orderNo").type(STRING).description("주문번호"),
646 | fieldWithPath("data.orderStatus").type(STRING).attributes(generateEnumAttrs(OrderStatus.class, OrderStatus::getDescription)).description("주문상태"),
647 | fieldWithPath("data.orderStatusDescription").type(STRING).description("주문상태설명"),
648 | fieldWithPath("data.products[].id").type(NUMBER).description("상품일련번호"),
649 | fieldWithPath("data.products[].productName").type(STRING).description("상품명"),
650 | fieldWithPath("data.products[].productNo").type(STRING).description("상품번호"),
651 | fieldWithPath("data.products[].productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
652 | fieldWithPath("data.products[].productStatusDescription").type(STRING).description("상품상태설명"),
653 | fieldWithPath("data.products[].created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
654 | fieldWithPath("data.products[].modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시"),
655 | fieldWithPath("data.orderDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("주문접수일시"),
656 | fieldWithPath("data.paymentMoney").type(NUMBER).description("결제금액").optional(),
657 | fieldWithPath("data.paymentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
658 | fieldWithPath("data.shipmentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("출하완료일시").optional(),
659 | fieldWithPath("data.completedDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
660 | };
661 |
662 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
663 | .perform(RestDocumentationRequestBuilders.put("/orders/{orderNo}/shipping", orderNo)
664 | .contentType(MediaType.APPLICATION_JSON)
665 | )
666 | .andDo(MockMvcResultHandlers.print())
667 | .andExpect(MockMvcResultMatchers.status().isOk())
668 | //REST Docs 용
669 | .andDo(MockMvcRestDocumentation.document("put-v1-shipping-order",
670 | getDocumentRequest(),
671 | getDocumentResponse(),
672 | pathParameters(
673 | parameterDescriptor
674 | ),
675 | responseFields(
676 | responseFieldDescription
677 | )
678 | ))
679 | //OAS 3.0 - Swagger
680 | .andDo(MockMvcRestDocumentationWrapper.document("put-v1-shipping-order",
681 | getDocumentRequest(),
682 | getDocumentResponse(),
683 | resource(ResourceSnippetParameters.builder()
684 | .pathParameters(
685 | parameterDescriptor
686 | )
687 | .responseFields(
688 | responseFieldDescription
689 | )
690 | .build())));
691 | }
692 |
693 | @DisplayName("배송완료")
694 | @Test
695 | void test05(RestDocumentationContextProvider contextProvider) throws Exception {
696 | var orderNo = "797a1eb9-d6b8-4875-9ac6-4cbeac65ba40";
697 |
698 | var orderJson = """
699 | {
700 | "id":1,
701 | "memberNo":"1111",
702 | "orderNo":"202209251659441",
703 | "orderStatus":"COMPLETED",
704 | "orderStatusDescription":"배송완료",
705 | "products":[
706 | {
707 | "id":1,
708 | "productName":"TEST2",
709 | "productNo":"00003",
710 | "productStatus":"CREATED",
711 | "created":"2022-09-25T16:59:23",
712 | "modified":"2022-09-25T16:59:23",
713 | "productStatusDescription":"생성"
714 | },
715 | {
716 | "id":4,
717 | "productName":"TEST5",
718 | "productNo":"00006",
719 | "productStatus":"CREATED",
720 | "created":"2022-09-25T16:59:23",
721 | "modified":"2022-09-25T16:59:23",
722 | "productStatusDescription":"생성"
723 | }
724 | ],
725 | "orderDateTime":"2022-09-25T16:59:44",
726 | "paymentMoney":1000,
727 | "paymentDateTime":"2022-09-25T17:00:46",
728 | "shipmentDateTime":"2022-09-25T17:00:50",
729 | "completedDateTime":"2022-09-26T17:00:50"
730 | }""";
731 |
732 | Mockito.when(orderFacade.complete(Mockito.any()))
733 | .thenReturn(JsonUtils.fromJson(orderJson, OrderDto.class));
734 |
735 | var parameterDescriptor = parameterWithName("orderNo").description("주문번호");
736 |
737 | var responseFieldDescription = new FieldDescriptor[]{
738 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
739 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
740 | fieldWithPath("data.id").type(NUMBER).description("주문번호"),
741 | fieldWithPath("data.memberNo").type(STRING).description("회원번호"),
742 | fieldWithPath("data.orderNo").type(STRING).description("주문번호"),
743 | fieldWithPath("data.orderStatus").type(STRING).attributes(generateEnumAttrs(OrderStatus.class, OrderStatus::getDescription)).description("주문상태"),
744 | fieldWithPath("data.orderStatusDescription").type(STRING).description("주문상태설명"),
745 | fieldWithPath("data.products[].id").type(NUMBER).description("상품일련번호"),
746 | fieldWithPath("data.products[].productName").type(STRING).description("상품명"),
747 | fieldWithPath("data.products[].productNo").type(STRING).description("상품번호"),
748 | fieldWithPath("data.products[].productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
749 | fieldWithPath("data.products[].productStatusDescription").type(STRING).description("상품상태설명"),
750 | fieldWithPath("data.products[].created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
751 | fieldWithPath("data.products[].modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시"),
752 | fieldWithPath("data.orderDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("주문접수일시"),
753 | fieldWithPath("data.paymentMoney").type(NUMBER).description("결제금액").optional(),
754 | fieldWithPath("data.paymentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
755 | fieldWithPath("data.shipmentDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("출하완료일시").optional(),
756 | fieldWithPath("data.completedDateTime").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("결제완료일시").optional(),
757 | };
758 |
759 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
760 | .perform(RestDocumentationRequestBuilders.put("/orders/{orderNo}/complete", orderNo)
761 | .contentType(MediaType.APPLICATION_JSON)
762 | )
763 | .andDo(MockMvcResultHandlers.print())
764 | .andExpect(MockMvcResultMatchers.status().isOk())
765 | //REST Docs 용
766 | .andDo(MockMvcRestDocumentation.document("put-v1-complete-order",
767 | getDocumentRequest(),
768 | getDocumentResponse(),
769 | pathParameters(
770 | parameterDescriptor
771 | ),
772 | responseFields(
773 | responseFieldDescription
774 | )
775 | ))
776 | //OAS 3.0 - Swagger
777 | .andDo(MockMvcRestDocumentationWrapper.document("put-v1-complete-order",
778 | getDocumentRequest(),
779 | getDocumentResponse(),
780 | resource(ResourceSnippetParameters.builder()
781 | .pathParameters(
782 | parameterDescriptor
783 | )
784 | .responseFields(
785 | responseFieldDescription
786 | )
787 | .build())));
788 | }
789 | }
790 |
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/order/OrderRestControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.order;
2 |
3 | import com.kurly.tet.guide.springrestdocs.application.order.OrderFacade;
4 | import com.kurly.tet.guide.springrestdocs.domain.exception.OrderNotFoundException;
5 | import com.kurly.tet.guide.springrestdocs.domain.exception.ProductNotFoundException;
6 | import com.kurly.tet.guide.springrestdocs.domain.order.OrderDto;
7 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.Test;
10 | import org.mockito.Mockito;
11 | import org.springframework.beans.factory.annotation.Autowired;
12 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
13 | import org.springframework.boot.test.mock.mockito.MockBean;
14 | import org.springframework.http.MediaType;
15 | import org.springframework.test.web.servlet.MockMvc;
16 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
17 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
18 |
19 | import java.util.List;
20 | import java.util.UUID;
21 |
22 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
23 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
25 |
26 | @DisplayName("주문API")
27 | @WebMvcTest({OrderRestController.class})
28 | class OrderRestControllerTest {
29 | @Autowired
30 | private MockMvc mockMvc;
31 | @MockBean
32 | private OrderFacade orderFacade;
33 |
34 | @DisplayName("검색: 최대조회크기(size, 1000) 초과하는 경우 400 오류")
35 | @Test
36 | void testSearchWhenOverMax() throws Exception {
37 | this.mockMvc.perform(
38 | get("/orders")
39 | .contentType(MediaType.APPLICATION_JSON)
40 | .param("size", "1001")
41 | )
42 | .andDo(MockMvcResultHandlers.print())
43 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
44 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"));
45 | }
46 |
47 | @DisplayName("생성: 회원번호(memberNo)를 누락한 경우 400 오류")
48 | @Test
49 | void testCreateOrderWhenMissingMemberNo() throws Exception {
50 | var requestContent = """
51 | {
52 | "memberNo":"",
53 | "ids":[1]
54 | }
55 | """;
56 | this.mockMvc.perform(
57 | post("/orders")
58 | .contentType(MediaType.APPLICATION_JSON)
59 | .content(requestContent)
60 | )
61 | .andDo(MockMvcResultHandlers.print())
62 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
63 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"));
64 | }
65 |
66 | @DisplayName("생성: 회원번호(memberNo)를 누락한 경우 400 오류")
67 | @Test
68 | void testCreateOrderWhenEmptyids() throws Exception {
69 | var requestContent = """
70 | {
71 | "memberNo":"202209251508",
72 | "ids":[]
73 | }
74 | """;
75 | this.mockMvc.perform(
76 | post("/orders")
77 | .contentType(MediaType.APPLICATION_JSON)
78 | .content(requestContent)
79 | )
80 | .andDo(MockMvcResultHandlers.print())
81 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
82 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"));
83 | }
84 |
85 | @DisplayName("생성: 주문 정상생성")
86 | @Test
87 | void testCreate() throws Exception {
88 | var products = List.of(new ProductDto(1L, "Product1", "P0001"));
89 | var orderDto = new OrderDto(1L, "202209251508", "TX-00001", products);
90 | Mockito.when(orderFacade.create(Mockito.any()))
91 | .thenReturn(orderDto);
92 |
93 | var requestContent = """
94 | {
95 | "memberNo":"202209251508",
96 | "productIds":[1]
97 | }
98 | """;
99 | this.mockMvc.perform(
100 | post("/orders")
101 | .contentType(MediaType.APPLICATION_JSON)
102 | .content(requestContent)
103 | )
104 | .andDo(MockMvcResultHandlers.print())
105 | .andExpect(MockMvcResultMatchers.status().isCreated());
106 | }
107 |
108 | @DisplayName("조회: 요청정보를 찾지 못하는 경우 404 오류")
109 | @Test
110 | void testFindByOrderNoWhenNotFound() throws Exception {
111 | var orderNo = "797a1eb9-d6b8-4875-9ac6-4cbeac65ba40";
112 |
113 | Mockito.when(orderFacade.findByOrderNo(orderNo))
114 | .thenThrow(new OrderNotFoundException(orderNo));
115 |
116 | this.mockMvc.perform(
117 | get("/orders/{orderNo}", orderNo)
118 | .contentType(MediaType.APPLICATION_JSON)
119 | )
120 | .andDo(MockMvcResultHandlers.print())
121 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
122 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C404\",\"message\":\"주문(주문번호: 797a1eb9-d6b8-4875-9ac6-4cbeac65ba40)을 찾을 수 없습니다.\",\"data\":null}"));
123 | }
124 |
125 | @DisplayName("결제: 결제금액이 누락된 경우")
126 | @Test
127 | void testPayment() throws Exception {
128 | var orderNo = "797a1eb9-d6b8-4875-9ac6-4cbeac65ba40";
129 |
130 | String modifyContent = """
131 | {
132 | "paymentMoney":null // 상품명 누락
133 | }
134 | """;
135 |
136 | this.mockMvc.perform(
137 | put("/orders/{orderNo}/payment", orderNo)
138 | .contentType(MediaType.APPLICATION_JSON)
139 | .content(modifyContent)
140 | )
141 | .andDo(MockMvcResultHandlers.print())
142 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
143 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"));
144 | }
145 |
146 | @DisplayName("출하: 주문번호 누락된 경우")
147 | @Test
148 | void testShipping() throws Exception {
149 | var orderNo = "797a1eb9-d6b8-4875-9ac6-4cbeac65ba40";
150 |
151 | Mockito.when(orderFacade.shipping(orderNo))
152 | .thenThrow(new OrderNotFoundException(orderNo));
153 |
154 | this.mockMvc.perform(
155 | put("/orders/{orderNo}/shipping", orderNo)
156 | .contentType(MediaType.APPLICATION_JSON)
157 | )
158 | .andDo(MockMvcResultHandlers.print())
159 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
160 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C404\",\"message\":\"주문(주문번호: 797a1eb9-d6b8-4875-9ac6-4cbeac65ba40)을 찾을 수 없습니다.\",\"data\":null}"));
161 | }
162 | }
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/product/ProductRestControllerDocsTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.product;
2 |
3 | import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
4 | import com.epages.restdocs.apispec.ResourceSnippetParameters;
5 | import com.kurly.tet.guide.springrestdocs.annotations.RestDocsTest;
6 | import com.kurly.tet.guide.springrestdocs.application.product.ProductFacade;
7 | import com.kurly.tet.guide.springrestdocs.common.util.JsonUtils;
8 | import com.kurly.tet.guide.springrestdocs.documenation.MockMvcFactory;
9 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductDto;
10 | import com.kurly.tet.guide.springrestdocs.domain.product.ProductStatus;
11 | import com.kurly.tet.guide.springrestdocs.infrastructure.web.common.dto.PageResponse;
12 | import org.junit.jupiter.api.DisplayName;
13 | import org.junit.jupiter.api.Test;
14 | import org.mockito.InjectMocks;
15 | import org.mockito.Mock;
16 | import org.mockito.Mockito;
17 | import org.springframework.http.MediaType;
18 | import org.springframework.restdocs.RestDocumentationContextProvider;
19 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
20 | import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
21 | import org.springframework.restdocs.payload.FieldDescriptor;
22 | import org.springframework.restdocs.request.ParameterDescriptor;
23 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
24 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
25 |
26 | import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
27 | import static com.kurly.tet.guide.springrestdocs.config.Constant.FORMAT_LOCAL_DATE_TIME;
28 | import static com.kurly.tet.guide.springrestdocs.config.Constant.HOST_LOCAL;
29 | import static com.kurly.tet.guide.springrestdocs.documenation.DocumentFormatGenerator.customFormat;
30 | import static com.kurly.tet.guide.springrestdocs.documenation.DocumentFormatGenerator.generateEnumAttrs;
31 | import static com.kurly.tet.guide.springrestdocs.documenation.DocumentUtils.getDocumentRequest;
32 | import static com.kurly.tet.guide.springrestdocs.documenation.DocumentUtils.getDocumentResponse;
33 | import static org.springframework.restdocs.payload.JsonFieldType.*;
34 | import static org.springframework.restdocs.payload.PayloadDocumentation.*;
35 | import static org.springframework.restdocs.request.RequestDocumentation.*;
36 |
37 | @DisplayName("상품API 문서화")
38 | @RestDocsTest
39 | class ProductRestControllerDocsTest {
40 | @Mock
41 | private ProductFacade productFacade;
42 | @InjectMocks
43 | private ProductRestController controller;
44 |
45 | @DisplayName("검색: 상품정보")
46 | @Test
47 | void testSearchProduct(RestDocumentationContextProvider contextProvider) throws Exception {
48 |
49 | var responseSource = """
50 | {
51 | "content": [
52 | {
53 | "id": 1,
54 | "productName": "TEST2",
55 | "productNo": "00003",
56 | "productStatus": "CREATED",
57 | "productStatusDescription": "생성",
58 | "created": "2022-09-22T18:41:52",
59 | "modified": "2022-09-22T18:41:52"
60 | },
61 | {
62 | "id": 4,
63 | "productName": "TEST5",
64 | "productNo": "00006",
65 | "productStatus": "CREATED",
66 | "productStatusDescription": "생성",
67 | "created": "2022-09-22T18:41:52",
68 | "modified": "2022-09-22T18:41:52"
69 | },
70 | {
71 | "id": 7,
72 | "productName": "TEST8",
73 | "productNo": "00009",
74 | "productStatus": "CREATED",
75 | "productStatusDescription": "생성",
76 | "created": "2022-09-22T18:41:52",
77 | "modified": "2022-09-22T18:41:52"
78 | },
79 | {
80 | "id": 10,
81 | "productName": "TEST11",
82 | "productNo": "00012",
83 | "productStatus": "ACTIVATED",
84 | "productStatusDescription": "활성화됨",
85 | "created": "2022-09-22T18:41:52",
86 | "modified": "2022-09-22T18:41:52"
87 | },
88 | {
89 | "id": 13,
90 | "productName": "TEST14",
91 | "productNo": "00015",
92 | "productStatus": "ARCHIVED",
93 | "productStatusDescription": "보관처리됨",
94 | "created": "2022-09-22T18:41:52",
95 | "modified": "2022-09-22T18:41:52"
96 | }
97 | ],
98 | "pageRequest": {
99 | "page": 0,
100 | "size": 1000
101 | },
102 | "total": 5,
103 | "totalPages": 1,
104 | "hasNext": false,
105 | "hasPrevious": false,
106 | "hasContent": true,
107 | "isFirst": true,
108 | "isLast": true
109 | }""";
110 |
111 | Mockito.when(productFacade.search(Mockito.any(), Mockito.any()))
112 | .thenReturn(JsonUtils.fromJson(responseSource, PageResponse.class));
113 |
114 | var parameterDescriptors = new ParameterDescriptor[]{
115 | parameterWithName("page").description("조회페이지"),
116 | parameterWithName("size").description("페이지내 건수"),
117 | parameterWithName("productName").description("상품명"),
118 | parameterWithName("productNo").description("상품번호")
119 | };
120 |
121 | var fieldDescriptors = new FieldDescriptor[]{
122 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
123 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
124 | fieldWithPath("data.content[].id").type(NUMBER).description("상품일련번호"),
125 | fieldWithPath("data.content[].productName").type(STRING).description("상품명"),
126 | fieldWithPath("data.content[].productNo").type(STRING).description("상품번호"),
127 | fieldWithPath("data.content[].productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
128 | fieldWithPath("data.content[].productStatusDescription").type(STRING).description("상품상태설명"),
129 | fieldWithPath("data.content[].created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
130 | fieldWithPath("data.content[].modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시"),
131 | fieldWithPath("data.pageRequest.page").type(NUMBER).description("페이지번호"),
132 | fieldWithPath("data.pageRequest.size").type(NUMBER).description("페이지크기"),
133 | fieldWithPath("data.total").type(NUMBER).description("데이터 전체갯수"),
134 | fieldWithPath("data.totalPages").type(NUMBER).description("전체 페이지갯수"),
135 | fieldWithPath("data.isFirst").type(BOOLEAN).description("시작페이지인가?"),
136 | fieldWithPath("data.isLast").type(BOOLEAN).description("마지막페이지인가?"),
137 | fieldWithPath("data.hasPrevious").type(BOOLEAN).description("앞페이지가있는가?"),
138 | fieldWithPath("data.hasNext").type(BOOLEAN).description("다음페이지가있는가?"),
139 | fieldWithPath("data.hasContent").type(BOOLEAN).description("컨텐츠가있는가?"),
140 | };
141 |
142 |
143 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
144 | .perform(RestDocumentationRequestBuilders.get("/products")
145 | .param("page", "0")
146 | .param("size", "20")
147 | .param("productName", "테스트상품")
148 | .param("productNo", "PRODUCT01")
149 | .contentType(MediaType.APPLICATION_JSON))
150 | .andDo(MockMvcResultHandlers.print())
151 | .andExpect(MockMvcResultMatchers.status().isOk())
152 | //REST Docs 용
153 | .andDo(MockMvcRestDocumentation.document("get-v1-get-products",
154 | getDocumentRequest(),
155 | getDocumentResponse(),
156 | requestParameters(
157 | parameterDescriptors
158 | ),
159 | responseFields(
160 | fieldDescriptors
161 | )
162 | ))
163 | //OAS 3.0 - Swagger
164 | .andDo(MockMvcRestDocumentationWrapper.document("get-v1-get-products",
165 | getDocumentRequest(),
166 | getDocumentResponse(),
167 | resource(ResourceSnippetParameters.builder()
168 | .requestParameters(
169 | parameterDescriptors
170 | )
171 | .responseFields(
172 | fieldDescriptors
173 | )
174 | .build())));
175 | }
176 |
177 | @DisplayName("생성: 상품")
178 | @Test
179 | void testCreateProduct(RestDocumentationContextProvider contextProvider) throws Exception {
180 | var createCommandJson = """
181 | {
182 | "productName": "테스트상품",
183 | "productNo": "TEST-1111"
184 | }
185 | """;
186 | var createdProductJson = """
187 | {
188 | "id": 1,
189 | "productName": "테스트상품",
190 | "productNo": "TEST01",
191 | "productStatus": "CREATED",
192 | "created": "2022-09-23T05:46:10",
193 | "modified": "2022-09-23T05:46:10"
194 | }""";
195 |
196 | Mockito.when(productFacade.create(Mockito.any()))
197 | .thenReturn(JsonUtils.fromJson(createdProductJson, ProductDto.class));
198 |
199 | var requestFieldDescription = new FieldDescriptor[]{
200 | fieldWithPath("productName").type(STRING).description("상품명"),
201 | fieldWithPath("productNo").type(STRING).description("상품번호")
202 | };
203 |
204 | var responseFieldDescription = new FieldDescriptor[]{
205 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
206 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
207 | fieldWithPath("data.id").type(NUMBER).description("상품일련번호"),
208 | fieldWithPath("data.productName").type(STRING).description("상품명"),
209 | fieldWithPath("data.productNo").type(STRING).description("상품번호"),
210 | fieldWithPath("data.productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
211 | fieldWithPath("data.productStatusDescription").type(STRING).description("상품상태설명"),
212 | fieldWithPath("data.created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
213 | fieldWithPath("data.modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시")
214 | };
215 |
216 | var expectedContent = "{\"code\":\"0000\",\"message\":\"정상\",\"data\":{\"id\":1,\"productName\":\"테스트상품\",\"productNo\":\"TEST01\",\"productStatus\":\"CREATED\",\"created\":\"2022-09-23T05:46:10\",\"modified\":\"2022-09-23T05:46:10\",\"productStatusDescription\":\"생성\"}}";
217 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
218 | .perform(RestDocumentationRequestBuilders.post("/products")
219 | .contentType(MediaType.APPLICATION_JSON)
220 | .content(createCommandJson)
221 | )
222 | .andDo(MockMvcResultHandlers.print())
223 | .andExpect(MockMvcResultMatchers.status().isCreated())
224 | .andExpect(MockMvcResultMatchers.content().string(expectedContent))
225 | //REST Docs 용
226 | .andDo(MockMvcRestDocumentation.document("post-v1-create-product",
227 | getDocumentRequest(),
228 | getDocumentResponse(),
229 | requestFields(
230 | requestFieldDescription
231 | ),
232 | responseFields(
233 | responseFieldDescription
234 | )
235 | ))
236 | //OAS 3.0 - Swagger
237 | .andDo(MockMvcRestDocumentationWrapper.document("post-v1-create-product",
238 | getDocumentRequest(),
239 | getDocumentResponse(),
240 | resource(ResourceSnippetParameters.builder()
241 | .requestFields(
242 | requestFieldDescription
243 | )
244 | .responseFields(
245 | responseFieldDescription
246 | )
247 | .build())));
248 | }
249 |
250 | @DisplayName("조회: 상품상세")
251 | @Test
252 | void testGetProduct(RestDocumentationContextProvider contextProvider) throws Exception {
253 | var sourceJson = """
254 | {
255 | "id": 1,
256 | "productName": "테스트상품",
257 | "productNo": "TEST01",
258 | "productStatus": "CREATED",
259 | "created": "2022-09-23T05:46:10",
260 | "modified": "2022-09-23T05:46:10"
261 | }""";
262 |
263 | Mockito.when(productFacade.getProduct(Mockito.anyLong()))
264 | .thenReturn(JsonUtils.fromJson(sourceJson, ProductDto.class));
265 |
266 | String expectedContent = "{\"code\":\"0000\",\"message\":\"정상\",\"data\":{\"id\":1,\"productName\":\"테스트상품\",\"productNo\":\"TEST01\",\"productStatus\":\"CREATED\",\"created\":\"2022-09-23T05:46:10\",\"modified\":\"2022-09-23T05:46:10\",\"productStatusDescription\":\"생성\"}}";
267 |
268 | var parameterDescriptor = parameterWithName("id").description("상품번호");
269 |
270 | var fieldDescriptors = new FieldDescriptor[]{
271 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
272 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
273 | fieldWithPath("data.id").type(NUMBER).description("상품일련번호"),
274 | fieldWithPath("data.productName").type(STRING).description("상품명"),
275 | fieldWithPath("data.productNo").type(STRING).description("상품번호"),
276 | fieldWithPath("data.productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
277 | fieldWithPath("data.productStatusDescription").type(STRING).description("상품상태설명"),
278 | fieldWithPath("data.created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
279 | fieldWithPath("data.modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시")
280 | };
281 |
282 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
283 | .perform(RestDocumentationRequestBuilders.get("/products/{id}", 1L)
284 | .contentType(MediaType.APPLICATION_JSON))
285 | .andDo(MockMvcResultHandlers.print())
286 | .andExpect(MockMvcResultMatchers.status().isOk())
287 | .andExpect(MockMvcResultMatchers.content().string(expectedContent))
288 | //REST Docs 용
289 | .andDo(MockMvcRestDocumentation.document("get-v1-get-product",
290 | getDocumentRequest(),
291 | getDocumentResponse(),
292 | pathParameters(
293 | parameterDescriptor
294 | ),
295 | responseFields(
296 | fieldDescriptors
297 | )
298 | ))
299 | //OAS 3.0 - Swagger
300 | .andDo(MockMvcRestDocumentationWrapper.document("get-v1-get-product",
301 | getDocumentRequest(),
302 | getDocumentResponse(),
303 | resource(ResourceSnippetParameters.builder()
304 | .pathParameters(
305 | parameterDescriptor
306 | )
307 | .responseFields(
308 | fieldDescriptors
309 | )
310 | .build())));
311 | }
312 |
313 | @DisplayName("변경: 상품")
314 | @Test
315 | void testModifyProduct(RestDocumentationContextProvider contextProvider) throws Exception {
316 | var modifyCommandJson = """
317 | {
318 | "productName": "테스트상품",
319 | "productNo": "TEST-1111",
320 | "productStatus": "ACTIVATED"
321 | }
322 | """;
323 |
324 | var modifiedProductJson = """
325 | {
326 | "id": 1,
327 | "productName": "테스트상품",
328 | "productNo": "TEST01",
329 | "productStatus": "ACTIVATED",
330 | "created": "2022-09-23T05:46:10",
331 | "modified": "2022-09-23T05:46:10"
332 | }""";
333 |
334 | var expectedContent = "{\"code\":\"0000\",\"message\":\"정상\",\"data\":{\"id\":1,\"productName\":\"테스트상품\",\"productNo\":\"TEST01\",\"productStatus\":\"ACTIVATED\",\"created\":\"2022-09-23T05:46:10\",\"modified\":\"2022-09-23T05:46:10\",\"productStatusDescription\":\"활성화\"}}";
335 |
336 | Mockito.when(productFacade.modify(Mockito.anyLong(), Mockito.any()))
337 | .thenReturn(JsonUtils.fromJson(modifiedProductJson, ProductDto.class));
338 |
339 | var parameterDescriptor = parameterWithName("id").description("상품번호");
340 |
341 | var requestFieldDescription = new FieldDescriptor[]{
342 | fieldWithPath("productName").type(STRING).description("상품명"),
343 | fieldWithPath("productNo").type(STRING).description("상품번호"),
344 | fieldWithPath("productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태")
345 | };
346 |
347 | var responseFieldDescription = new FieldDescriptor[]{
348 | fieldWithPath("code").type(STRING).description("응답코드(정상: 0000)"),
349 | fieldWithPath("message").type(STRING).description("응답메시지(정상: OK)"),
350 | fieldWithPath("data.id").type(NUMBER).description("상품일련번호"),
351 | fieldWithPath("data.productName").type(STRING).description("상품명"),
352 | fieldWithPath("data.productNo").type(STRING).description("상품번호"),
353 | fieldWithPath("data.productStatus").type(STRING).attributes(generateEnumAttrs(ProductStatus.class, ProductStatus::getDescription)).description("상품상태"),
354 | fieldWithPath("data.productStatusDescription").type(STRING).description("상품상태설명"),
355 | fieldWithPath("data.created").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("생성일시"),
356 | fieldWithPath("data.modified").type(STRING).attributes(customFormat(FORMAT_LOCAL_DATE_TIME)).description("변경일시")
357 | };
358 |
359 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
360 | .perform(RestDocumentationRequestBuilders.put("/products/{id}", 1L)
361 | .contentType(MediaType.APPLICATION_JSON)
362 | .content(modifyCommandJson)
363 | )
364 | .andDo(MockMvcResultHandlers.print())
365 | .andExpect(MockMvcResultMatchers.status().isOk())
366 | .andExpect(MockMvcResultMatchers.content().string(expectedContent))
367 | //REST Docs 용
368 | .andDo(MockMvcRestDocumentation.document("put-v1-modify-product",
369 | getDocumentRequest(),
370 | getDocumentResponse(),
371 | pathParameters(
372 | parameterDescriptor
373 | ),
374 | requestFields(
375 | requestFieldDescription
376 | ),
377 | responseFields(
378 | responseFieldDescription
379 | )
380 | ))
381 | //OAS 3.0 - Swagger
382 | .andDo(MockMvcRestDocumentationWrapper.document("put-v1-modify-product",
383 | getDocumentRequest(),
384 | getDocumentResponse(),
385 | resource(ResourceSnippetParameters.builder()
386 | .pathParameters(
387 | parameterDescriptor
388 | )
389 | .requestFields(
390 | requestFieldDescription
391 | )
392 | .responseFields(
393 | responseFieldDescription
394 | )
395 | .build())));
396 | }
397 |
398 | @DisplayName("삭제: 상품")
399 | @Test
400 | void testDeleteProduct(RestDocumentationContextProvider contextProvider) throws Exception {
401 | var expectedContent = "";
402 |
403 | var parameterDescriptor = parameterWithName("id").description("상품번호");
404 |
405 | MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
406 | .perform(RestDocumentationRequestBuilders.delete("/products/{id}", 1L)
407 | .contentType(MediaType.APPLICATION_JSON)
408 | )
409 | .andDo(MockMvcResultHandlers.print())
410 | .andExpect(MockMvcResultMatchers.status().isNoContent())
411 | .andExpect(MockMvcResultMatchers.content().string(expectedContent))
412 | //REST Docs 용
413 | .andDo(MockMvcRestDocumentation.document("delete-v1-delete-product",
414 | getDocumentRequest(),
415 | getDocumentResponse(),
416 | pathParameters(
417 | parameterDescriptor
418 | )
419 | ))
420 | //OAS 3.0 - Swagger
421 | .andDo(MockMvcRestDocumentationWrapper.document("delete-v1-delete-product",
422 | getDocumentRequest(),
423 | getDocumentResponse(),
424 | resource(ResourceSnippetParameters.builder()
425 | .pathParameters(
426 | parameterDescriptor
427 | )
428 | .build())));
429 | }
430 | }
--------------------------------------------------------------------------------
/src/test/java/com/kurly/tet/guide/springrestdocs/infrastructure/web/product/ProductRestControllerTest.java:
--------------------------------------------------------------------------------
1 | package com.kurly.tet.guide.springrestdocs.infrastructure.web.product;
2 |
3 | import com.kurly.tet.guide.springrestdocs.application.product.ProductFacade;
4 | import com.kurly.tet.guide.springrestdocs.domain.exception.ProductNotFoundException;
5 | import org.junit.jupiter.api.DisplayName;
6 | import org.junit.jupiter.api.Test;
7 | import org.mockito.Mockito;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
10 | import org.springframework.boot.test.mock.mockito.MockBean;
11 | import org.springframework.http.MediaType;
12 | import org.springframework.test.web.servlet.MockMvc;
13 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
14 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
15 |
16 | import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
18 |
19 | @DisplayName("상품API")
20 | @WebMvcTest({ProductRestController.class})
21 | class ProductRestControllerTest {
22 | @Autowired
23 | private MockMvc mockMvc;
24 | @MockBean
25 | private ProductFacade productFacade;
26 |
27 | @DisplayName("검색: 최대조회크기(size, 1000) 초과하는 경우 400 오류")
28 | @Test
29 | void testSearchWhenOverMax() throws Exception {
30 | this.mockMvc.perform(
31 | get("/products")
32 | .contentType(MediaType.APPLICATION_JSON)
33 | .param("size", "1001")
34 | )
35 | .andDo(MockMvcResultHandlers.print())
36 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
37 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"));
38 | }
39 |
40 | @DisplayName("생성: 상품명을 누락한 경우 400 오류")
41 | @Test
42 | void testCreateProduct() throws Exception {
43 | var requestContent = """
44 | {
45 | "productName":"",
46 | "productNo":"1234"
47 | }
48 | """;
49 | this.mockMvc.perform(
50 | post("/products")
51 | .contentType(MediaType.APPLICATION_JSON)
52 | .content(requestContent)
53 | )
54 | .andDo(MockMvcResultHandlers.print())
55 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
56 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"));
57 | }
58 |
59 | @DisplayName("조회: 요청정보를 찾지 못하는 경우 404 오류")
60 | @Test
61 | void testGetProductWhenNotFound() throws Exception {
62 | Mockito.when(productFacade.getProduct(Long.MAX_VALUE))
63 | .thenThrow(new ProductNotFoundException(Long.MAX_VALUE));
64 |
65 | this.mockMvc.perform(
66 | get("/products/{id}", Long.MAX_VALUE)
67 | .contentType(MediaType.APPLICATION_JSON)
68 | )
69 | .andDo(MockMvcResultHandlers.print())
70 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
71 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C404\",\"message\":\"상품(ID: 9223372036854775807)을 찾을 수 없습니다.\",\"data\":null}"));
72 | }
73 |
74 | @DisplayName("변경: 요청정보를 찾지 못하는 경우 404 오류")
75 | @Test
76 | void testModifyProductWhenNotFound() throws Exception {
77 | Mockito.when(productFacade.modify(Mockito.anyLong(), Mockito.any()))
78 | .thenThrow(new ProductNotFoundException(Long.MAX_VALUE));
79 |
80 | String modifyContent = """
81 | {
82 | "productName":"1234",
83 | "productNo":"1234",
84 | "productStatus":"ACTIVATED"
85 | }
86 | """;
87 | this.mockMvc.perform(
88 | put("/products/{id}", Long.MAX_VALUE)
89 | .contentType(MediaType.APPLICATION_JSON)
90 | .content(modifyContent)
91 | )
92 | .andDo(MockMvcResultHandlers.print())
93 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
94 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C404\",\"message\":\"상품(ID: 9223372036854775807)을 찾을 수 없습니다.\",\"data\":null}"));
95 | }
96 |
97 | @DisplayName("변경: 상품명을 누락한 경우 404 오류")
98 | @Test
99 | void testModify02() throws Exception {
100 | String modifyContent = """
101 | {
102 | "productName":"", // 상품명 누락
103 | "productNo":"1234",
104 | "productStatus":"ACTIVATED"
105 | }
106 | """;
107 |
108 | this.mockMvc.perform(
109 | put("/products/{id}", Long.MAX_VALUE)
110 | .contentType(MediaType.APPLICATION_JSON)
111 | .content(modifyContent)
112 | )
113 | .andDo(MockMvcResultHandlers.print())
114 | .andExpect(MockMvcResultMatchers.status().is4xxClientError())
115 | .andExpect(MockMvcResultMatchers.content().string("{\"code\":\"C400\",\"message\":\"잘못된 요청입니다. 요청내용을 확인하세요.\",\"data\":null}"));
116 | }
117 |
118 | @DisplayName("삭제: 별도 처리 없음. NOT_CONTENT 반환")
119 | @Test
120 | void testDeleteWhenNotFound() throws Exception {
121 | this.mockMvc.perform(
122 | delete("/products/{id}", Long.MAX_VALUE)
123 | .contentType(MediaType.APPLICATION_JSON)
124 | )
125 | .andDo(MockMvcResultHandlers.print())
126 | .andExpect(MockMvcResultMatchers.status().isNoContent());
127 | }
128 | }
--------------------------------------------------------------------------------
/src/test/resources/application.yml:
--------------------------------------------------------------------------------
1 | server:
2 | servlet:
3 | encoding:
4 | force: true
--------------------------------------------------------------------------------
/src/test/resources/org/springframework/restdocs/templates/asciidoctor/README.md:
--------------------------------------------------------------------------------
1 | Spring REST Docs 사용자정의 스니펫 적용시 주의사항
2 | ==============================================
3 |
4 | * Spring REST Docs 스니펫이 적용되지 않은 이유: 스니펫 폴더이름이 잘못 되었음
5 | * AS-IS: `src/test/resources/org.springframework.restdocs.templates.asciidoctor`
6 | * TO-BE: `src/test/resources/org/springframework/restdocs/templates/asciidoctor`
7 | * 인텔리제이에서는 동일하게 보이지만, 실제로 스니펫 폴더 경로는 달랐음
8 |
--------------------------------------------------------------------------------
/src/test/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet:
--------------------------------------------------------------------------------
1 | .link:https://curl.se/docs/manual.html[`curl`] 명령어
2 | [source,bash]
3 | ----
4 | $ curl {{url}} {{options}}
5 | ----
6 |
--------------------------------------------------------------------------------
/src/test/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet:
--------------------------------------------------------------------------------
1 | .HTTP 요청예제
2 | [source,http,options="nowrap"]
3 | ----
4 | {{method}} {{path}} HTTP/1.1
5 | {{#headers}}
6 | {{name}}: {{value}}
7 | {{/headers}}
8 | {{requestBody}}
9 | ----
--------------------------------------------------------------------------------
/src/test/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet:
--------------------------------------------------------------------------------
1 | .HTTP 응답예제
2 | [source,http,options="nowrap"]
3 | ----
4 | HTTP/1.1{{statusCode}}{{statusReason}}
5 | {{#headers}}
6 | {{name}}:{{value}}
7 | {{/headers}}
8 | {{responseBody}}
9 | ----
--------------------------------------------------------------------------------
/src/test/resources/org/springframework/restdocs/templates/asciidoctor/httpie-request.snippet:
--------------------------------------------------------------------------------
1 | .link:https://httpie.io/docs/cli/usage[`httpie`] 명령어
2 | [source,bash]
3 | ----
4 | $ {{echoContent}}http {{options}} {{url}}{{requestItems}}
5 | ----
--------------------------------------------------------------------------------
/src/test/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet:
--------------------------------------------------------------------------------
1 | .요청필드
2 | |===
3 | |필드|타입|필수값|설명|형식
4 |
5 | {{#fields}}
6 | |{{path}}
7 | |{{type}}
8 | |{{^optional}}true{{/optional}}
9 | a|{{description}}
10 | a|{{#format}}{{format}}{{/format}}{{^format}}{{/format}}
11 | {{/fields}}
12 | |===
13 |
--------------------------------------------------------------------------------
/src/test/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet:
--------------------------------------------------------------------------------
1 | .요청파라미터
2 | |===
3 | |Parameter|Description
4 |
5 | {{#parameters}}
6 | |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
7 | |{{#tableCellContent}}{{description}}{{/tableCellContent}}
8 |
9 | {{/parameters}}
10 | |===
--------------------------------------------------------------------------------
/src/test/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet:
--------------------------------------------------------------------------------
1 | .응답필드
2 | |===
3 | |필드|타입|필수값|설명|형식
4 |
5 | {{#fields}}
6 | |{{path}}
7 | |{{type}}
8 | |{{^optional}}true{{/optional}}
9 | a|{{description}}
10 | a|{{#format}}{{format}}{{/format}}{{^format}}{{/format}}
11 | {{/fields}}
12 | |===
13 |
--------------------------------------------------------------------------------
/swagger-ui/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thefarmersfront/spring-rest-docs-guide/7fd8b90007b12d1509b55c17bc4e06fd5292daee/swagger-ui/favicon-16x16.png
--------------------------------------------------------------------------------
/swagger-ui/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thefarmersfront/spring-rest-docs-guide/7fd8b90007b12d1509b55c17bc4e06fd5292daee/swagger-ui/favicon-32x32.png
--------------------------------------------------------------------------------
/swagger-ui/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | overflow: -moz-scrollbars-vertical;
4 | overflow-y: scroll;
5 | }
6 |
7 | *,
8 | *:before,
9 | *:after {
10 | box-sizing: inherit;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | background: #fafafa;
16 | }
17 |
--------------------------------------------------------------------------------
/swagger-ui/swagger-initializer.js:
--------------------------------------------------------------------------------
1 | window.onload = function() {
2 | //
3 |
4 | // the following lines will be replaced by docker/configurator, when it runs in a docker-container
5 | window.ui = SwaggerUIBundle({
6 | url: "openapi3.yaml",
7 | dom_id: '#swagger-ui',
8 | deepLinking: true,
9 | presets: [
10 | SwaggerUIBundle.presets.apis,
11 | SwaggerUIStandalonePreset
12 | ],
13 | plugins: [
14 | SwaggerUIBundle.plugins.DownloadUrl
15 | ],
16 | layout: "StandaloneLayout"
17 | });
18 |
19 | //
20 | };
21 |
--------------------------------------------------------------------------------
/swagger-ui/swagger-ui.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Swagger UI for Spring REST Docs 작성 가이드
8 |
9 |
10 |
11 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
57 |
58 |
59 |
--------------------------------------------------------------------------------