diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..527389dfe --- /dev/null +++ b/.gitignore @@ -0,0 +1,216 @@ +# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij,macos + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +gradle/ +gradlew +gradlew.bat +HELP.md + +src/test/java/com/blackdog/springbootBoardJpa/global/JasyptTest.java +src/main/resources/jasypt-encryptor-password.txt + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +###jar 파일 생성 관련 META-INF### +META-INF/* + +.idea/* + +# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij,macos diff --git a/BOOT-INF/classes/static/docs/index.html b/BOOT-INF/classes/static/docs/index.html new file mode 100644 index 000000000..e2e1e0273 --- /dev/null +++ b/BOOT-INF/classes/static/docs/index.html @@ -0,0 +1,471 @@ + + + + + + + +Spring Boot JPA Board + + + + + +
+
+

Post API docs

+
+ +
+
+
+

User API docs

+
+ +
+
+
+ + + \ No newline at end of file diff --git a/BOOT-INF/classes/static/docs/post.html b/BOOT-INF/classes/static/docs/post.html new file mode 100644 index 000000000..68c47f8c2 --- /dev/null +++ b/BOOT-INF/classes/static/docs/post.html @@ -0,0 +1,894 @@ + + + + + + + + +API docs + + + + + +
+
+

Post

+
+
+

게시물 생성

+
+
Request
+
+
POST /posts/1 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 52
+Host: localhost:8080
+
+{
+  "title" : "subject2",
+  "content" : "content2"
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

title

String

게시물 제목

content

String

게시물 내용

+
+
Response
+
+
HTTP/1.1 201 Created
+Content-Type: application/json
+Content-Length: 163
+
+{
+  "id" : 2,
+  "title" : "subject2",
+  "content" : "content2",
+  "name" : "둘리",
+  "createdAt" : "2023-08-08 15:26:14",
+  "updatedAt" : "2023-08-08 15:26:14"
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

게시물 ID

title

String

게시물 제목

content

String

게시물 내용

name

String

작성자 이름

createdAt

String

게시물 작성 시간

updatedAt

String

게시물 수정 시간

+
+
+
+

게시물 전체 조회

+
+
Request
+
+
GET /posts?page=0&size=5 HTTP/1.1
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 724
+
+{
+  "postResponses" : {
+    "content" : [ {
+      "id" : 1,
+      "title" : "subject1",
+      "content" : "content1",
+      "name" : "둘리",
+      "createdAt" : "2023-08-08 15:26:14",
+      "updatedAt" : "2023-08-08 15:26:14"
+    }, {
+      "id" : 2,
+      "title" : "subject2",
+      "content" : "content2",
+      "name" : "둘리",
+      "createdAt" : "2023-08-08 15:26:14",
+      "updatedAt" : "2023-08-08 15:26:14"
+    } ],
+    "pageable" : "INSTANCE",
+    "totalPages" : 1,
+    "totalElements" : 2,
+    "last" : true,
+    "size" : 2,
+    "number" : 0,
+    "sort" : {
+      "empty" : true,
+      "sorted" : false,
+      "unsorted" : true
+    },
+    "numberOfElements" : 2,
+    "first" : true,
+    "empty" : false
+  }
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

postResponses

Object

게시물 응답

postResponses.content

Array

게시물 정보 배열

postResponses.content[].id

Number

게시물 ID

postResponses.content[].title

String

게시물 제목

postResponses.content[].content

String

게시물 내용

postResponses.content[].name

String

게시물 작성자 이름

postResponses.content[].createdAt

String

게시물 생성일

postResponses.content[].updatedAt

String

게시물 갱신일

postResponses.totalElements

Number

totalElements

postResponses.totalPages

Number

totalPages

+
+
+
+

게시글 단건 조회

+
+
Request
+
+
GET /posts/1 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 163
+
+{
+  "id" : 2,
+  "title" : "subject2",
+  "content" : "content2",
+  "name" : "둘리",
+  "createdAt" : "2023-08-08 15:26:14",
+  "updatedAt" : "2023-08-08 15:26:14"
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

게시글 아이디

title

String

게시글 제목

content

String

게시글 본문

name

String

게시글 작성자 이름

createdAt

String

게시글 작성일

updatedAt

String

게시글 갱신일

+
+
+

게시물 작성자로 조회

+
+
Request
+
+
GET /posts/user/1?page=0&size=5 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 724
+
+{
+  "postResponses" : {
+    "content" : [ {
+      "id" : 1,
+      "title" : "subject1",
+      "content" : "content1",
+      "name" : "둘리",
+      "createdAt" : "2023-08-08 15:26:14",
+      "updatedAt" : "2023-08-08 15:26:14"
+    }, {
+      "id" : 2,
+      "title" : "subject2",
+      "content" : "content2",
+      "name" : "둘리",
+      "createdAt" : "2023-08-08 15:26:14",
+      "updatedAt" : "2023-08-08 15:26:14"
+    } ],
+    "pageable" : "INSTANCE",
+    "totalPages" : 1,
+    "totalElements" : 2,
+    "last" : true,
+    "size" : 2,
+    "number" : 0,
+    "sort" : {
+      "empty" : true,
+      "sorted" : false,
+      "unsorted" : true
+    },
+    "numberOfElements" : 2,
+    "first" : true,
+    "empty" : false
+  }
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

postResponses

Object

게시글 응답

postResponses.content

Array

게시글 정보 배열

postResponses.content[].id

Number

게시글 아이디

postResponses.content[].title

String

게시글 이름

postResponses.content[].content

String

게시글 나이

postResponses.content[].name

String

게시글 작성자 이름

postResponses.content[].createdAt

String

게시글 생성일

postResponses.content[].updatedAt

String

게시글 갱신일

postResponses.totalElements

Number

totalElements

postResponses.totalPages

Number

totalPages

+
+
+
+
+ + + \ No newline at end of file diff --git a/BOOT-INF/classes/static/docs/user.html b/BOOT-INF/classes/static/docs/user.html new file mode 100644 index 000000000..7119d04e3 --- /dev/null +++ b/BOOT-INF/classes/static/docs/user.html @@ -0,0 +1,790 @@ + + + + + + + + +API docs + + + + + +
+
+

유저

+
+
+
+

유저 생성

+
+
Request
+
+
POST /users HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Accept: application/json
+Content-Length: 40
+Host: localhost:8080
+
+{"name":"Kim","age":23,"hobby":"축구"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

회원 이름

age

Number

나이

hobby

String

취미

+
+
Response
+
+
HTTP/1.1 201 Created
+Content-Type: application/json
+Content-Length: 115
+
+{"id":1,"name":"Kim","age":23,"hobby":"축구","createdAt":"2023-08-08 15:26:15","updatedAt":"2023-08-08 15:26:15"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

회원 ID

name

String

회원 이름

age

Number

나이

hobby

String

취미

createdAt

String

회원 가입 시간

updatedAt

String

회원 수정 시간

+
+
+
+

유저 삭제

+
+
Request
+
+
DELETE /users/1 HTTP/1.1
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 204 No Content
+
+
+
+
+
+

유저 단건 조회

+
+
Request
+
+
GET /users/1 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Accept: application/json
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 147
+
+{
+  "id" : 1,
+  "name" : "Park",
+  "age" : 26,
+  "hobby" : "여행",
+  "createdAt" : "2023-08-08 15:26:15",
+  "updatedAt" : "2023-08-08 15:26:15"
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

유저 아이디

name

String

유저 이름

age

Number

유저 나이

hobby

String

유저 취미

createdAt

String

유저 생성일

updatedAt

String

유저 갱신일

+
+
+
+

유저 전체 조회

+
+
Request
+
+
GET /users?page=0&size=5 HTTP/1.1
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 692
+
+{
+  "userResponses" : {
+    "content" : [ {
+      "id" : 1,
+      "name" : "Park",
+      "age" : 26,
+      "hobby" : "여행",
+      "createdAt" : "2023-08-08 15:26:15",
+      "updatedAt" : "2023-08-08 15:26:15"
+    }, {
+      "id" : 2,
+      "name" : "Park",
+      "age" : 26,
+      "hobby" : "여행",
+      "createdAt" : "2023-08-08 15:26:15",
+      "updatedAt" : "2023-08-08 15:26:15"
+    } ],
+    "pageable" : "INSTANCE",
+    "totalPages" : 1,
+    "totalElements" : 2,
+    "last" : true,
+    "size" : 2,
+    "number" : 0,
+    "sort" : {
+      "empty" : true,
+      "sorted" : false,
+      "unsorted" : true
+    },
+    "numberOfElements" : 2,
+    "first" : true,
+    "empty" : false
+  }
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

userResponses

Object

유저 응답

userResponses.content[]

Array

유저 정보 배열

userResponses.content[].id

Number

유저 아이디

userResponses.content[].name

String

유저 이름

userResponses.content[].age

Number

유저 나이

userResponses.content[].hobby

String

유저 취미

userResponses.content[].createdAt

String

유저 생성일

userResponses.content[].updatedAt

String

유저 갱신일

userResponses.totalElements

Number

totalElements

userResponses.totalPages

Number

totalPages

+
+
+
+
+ + + \ No newline at end of file diff --git a/board.mermaid b/board.mermaid new file mode 100644 index 000000000..4d68980a8 --- /dev/null +++ b/board.mermaid @@ -0,0 +1,19 @@ +classDiagram + + class Post { + Long id + String title; + String content; + } + + class User { + Long id + Name name + Age age + String hobby + } + + class BaseEntity { + LocalDateTime createdAt + Long createdBy + } diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..f8759bb1b --- /dev/null +++ b/build.gradle @@ -0,0 +1,80 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.2' + id 'io.spring.dependency-management' version '1.1.2' + id 'org.asciidoctor.jvm.convert' version '3.3.2' +} + +group = 'com.blackdog' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +ext { + snippetsDir = file('build/generated-snippets') +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + + // jasypt + implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4' + + // lombok + compileOnly 'org.projectlombok:lombok:1.18.28' + + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + runtimeOnly 'com.mysql:mysql-connector-j' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + +} + +tasks.named('test') { + useJUnitPlatform() + outputs.dir snippetsDir +} + +asciidoctor { + dependsOn test + inputs.dir snippetsDir +} + +//기존에 존재하는 docs 삭제 +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') +} + +//bootJar 설정을 통해 html 파일을 BOOT-INF/classes/static/docs로 복사 +bootJar { + dependsOn asciidoctor + copy { + from "${asciidoctor.outputDir}" + into 'BOOT-INF/classes/static/docs' + } +} + +// build/docs/asciidocs 파일을 src/main/resources/static/docs로 복사 +task copyDocument(type: Copy) { + dependsOn asciidoctor + from file("build/docs/asciidoc") + into file("src/main/resources/static/doc") +} + +build { + dependsOn copyDocument +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..7a7db22cc --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'springbootBoardJpa' diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..89347b248 --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,9 @@ += Spring Boot JPA Board + +== Post API docs + +xref:post.adoc[Post API] + +== User API docs + +xref:user.adoc[User API] diff --git a/src/docs/asciidoc/post.adoc b/src/docs/asciidoc/post.adoc new file mode 100644 index 000000000..95a65dd6b --- /dev/null +++ b/src/docs/asciidoc/post.adoc @@ -0,0 +1,54 @@ +:hardbreaks: +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + += API docs +:doctype: book +:source-highlighter:: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + +== Post + +=== 게시물 생성 + +.Request +include::{snippets}/post-save/http-request.adoc[] +include::{snippets}/post-save/request-fields.adoc[] + +.Response +include::{snippets}/post-save/http-response.adoc[] +include::{snippets}/post-save/response-fields.adoc[] + +--- + +=== 게시물 전체 조회 + +.Request +include::{snippets}/posts - get/http-request.adoc[] + +.Response +include::{snippets}/posts - get/http-response.adoc[] +include::{snippets}/posts - get/response-fields.adoc[] + +--- + +=== 게시글 단건 조회 + +.Request +include::{snippets}/post-get/http-request.adoc[] + +.Response +include::{snippets}/post-get/http-response.adoc[] +include::{snippets}/post-get/response-fields.adoc[] + +=== 게시물 작성자로 조회 + +.Request +include::{snippets}/post-get-by-user/http-request.adoc[] + +.Response +include::{snippets}/post-get-by-user/http-response.adoc[] +include::{snippets}/post-get-by-user/response-fields.adoc[] diff --git a/src/docs/asciidoc/user.adoc b/src/docs/asciidoc/user.adoc new file mode 100644 index 000000000..1c4c31d04 --- /dev/null +++ b/src/docs/asciidoc/user.adoc @@ -0,0 +1,56 @@ +:hardbreaks: +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + += API docs +:doctype: book +:source-highlighter:: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + +== 유저 + +--- +=== 유저 생성 + +.Request +include::{snippets}/user-save/http-request.adoc[] +include::{snippets}/user-save/request-fields.adoc[] + +.Response +include::{snippets}/user-save/http-response.adoc[] +include::{snippets}/user-save/response-fields.adoc[] + +--- + +=== 유저 삭제 + +.Request +include::{snippets}/user-delete/http-request.adoc[] + +.Response +include::{snippets}/user-delete/http-response.adoc[] + +--- + +=== 유저 단건 조회 + +.Request +include::{snippets}/user-get/http-request.adoc[] + +.Response +include::{snippets}/user-get/http-response.adoc[] +include::{snippets}/user-get/response-fields.adoc[] + +--- + +=== 유저 전체 조회 + +.Request +include::{snippets}/users-get/http-request.adoc[] + +.Response +include::{snippets}/users-get/http-response.adoc[] +include::{snippets}/users-get/response-fields.adoc[] diff --git a/src/main/java/com/blackdog/springbootBoardJpa/SpringbootBoardJpaApplication.java b/src/main/java/com/blackdog/springbootBoardJpa/SpringbootBoardJpaApplication.java new file mode 100644 index 000000000..ea43f9cc5 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/SpringbootBoardJpaApplication.java @@ -0,0 +1,13 @@ +package com.blackdog.springbootBoardJpa; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringbootBoardJpaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringbootBoardJpaApplication.class, args); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/PostController.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/PostController.java new file mode 100644 index 000000000..ae42124f9 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/PostController.java @@ -0,0 +1,114 @@ +package com.blackdog.springbootBoardJpa.domain.post.controller; + +import com.blackdog.springbootBoardJpa.domain.post.controller.converter.PostControllerConverter; +import com.blackdog.springbootBoardJpa.domain.post.controller.dto.PostCreateDto; +import com.blackdog.springbootBoardJpa.domain.post.controller.dto.PostUpdateDto; +import com.blackdog.springbootBoardJpa.domain.post.service.PostService; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponse; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponses; +import com.blackdog.springbootBoardJpa.global.response.SuccessResponse; +import jakarta.validation.Valid; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.blackdog.springbootBoardJpa.global.response.SuccessCode.POST_DELETE_SUCCESS; + +@RestController +@RequestMapping(path = "/posts", produces = MediaType.APPLICATION_JSON_VALUE) +public class PostController { + + private final PostService postService; + private final PostControllerConverter controllerConverter; + + public PostController( + final PostService postService, + final PostControllerConverter controllerConverter + ) { + this.postService = postService; + this.controllerConverter = controllerConverter; + } + + /** + * [게시글 저장 API] + * + * @param userId + * @param createDto + * @return ResponseEntity + */ + @PostMapping(path = "/{userId}", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity savePost(@PathVariable Long userId, @Valid @RequestBody PostCreateDto createDto) { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(postService.savePost( + userId, + controllerConverter.toCreateRequest(createDto))); + } + + /** + * [게시글 수정 API] + * + * @param postId + * @param userId + * @param updateDto + * @return ResponseEntity + */ + @PatchMapping(path = "/{postId}/user/{userId}", consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity updatePost(@PathVariable Long postId, @PathVariable Long userId, @Valid @RequestBody PostUpdateDto updateDto) { + return ResponseEntity + .status(HttpStatus.OK) + .body(postService.updatePost( + postId, + userId, + controllerConverter.toUpdateRequest(updateDto))); + } + + /** + * [게시글 삭제 API] + * + * @param postId + * @param userId + * @return ResponseEntity + */ + @DeleteMapping(path = "/{postId}/user/{userId}") + public ResponseEntity deletePost(@PathVariable Long postId, @PathVariable Long userId) { + postService.deletePostById(userId, postId); + return ResponseEntity + .status(HttpStatus.OK) + .body(SuccessResponse.of(POST_DELETE_SUCCESS)); + } + + /** + * [게시글 상세 조회 API] + * + * @param postId + * @return ResponseEntity + */ + @GetMapping(path = "/{postId}") + public ResponseEntity getPostById(@PathVariable Long postId) { + return ResponseEntity + .status(HttpStatus.OK) + .body(postService.findPostById(postId)); + } + + /** + * [게시글 전체 조회 API] + * + * @param userId + * @param pageable + * @return ResponseEntity + */ + @GetMapping + public ResponseEntity getPosts(@RequestParam(defaultValue = "-1") Long userId, Pageable pageable) { + PostResponses postResponses = (userId == -1) + ? postService.findAllPosts(pageable) + : postService.findPostsByUserId(userId, pageable); + + return ResponseEntity + .status(HttpStatus.OK) + .body(postResponses); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/converter/PostControllerConverter.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/converter/PostControllerConverter.java new file mode 100644 index 000000000..8237ba242 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/converter/PostControllerConverter.java @@ -0,0 +1,25 @@ +package com.blackdog.springbootBoardJpa.domain.post.controller.converter; + +import com.blackdog.springbootBoardJpa.domain.post.controller.dto.PostCreateDto; +import com.blackdog.springbootBoardJpa.domain.post.controller.dto.PostUpdateDto; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostCreateRequest; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostUpdateRequest; +import org.springframework.stereotype.Component; + +@Component +public class PostControllerConverter { + + public PostCreateRequest toCreateRequest(PostCreateDto dto) { + return new PostCreateRequest( + dto.title(), + dto.content() + ); + } + + public PostUpdateRequest toUpdateRequest(PostUpdateDto dto) { + return new PostUpdateRequest( + dto.title(), + dto.content() + ); + } +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/dto/PostCreateDto.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/dto/PostCreateDto.java new file mode 100644 index 000000000..191d34dab --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/dto/PostCreateDto.java @@ -0,0 +1,15 @@ +package com.blackdog.springbootBoardJpa.domain.post.controller.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record PostCreateDto( + @NotBlank(message = "title은 공백일 수 없습니다.") + String title, + + @NotNull + @Size(max = 255, message = "내용은 최대 255자 입니다.") + String content +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/dto/PostUpdateDto.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/dto/PostUpdateDto.java new file mode 100644 index 000000000..34d1d82a8 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/controller/dto/PostUpdateDto.java @@ -0,0 +1,15 @@ +package com.blackdog.springbootBoardJpa.domain.post.controller.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record PostUpdateDto( + @NotBlank(message = "title은 공백일 수 없습니다.") + String title, + + @NotNull + @Size(max = 255, message = "내용은 최대 255자 입니다.") + String content +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/model/Post.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/model/Post.java new file mode 100644 index 000000000..af1067d7f --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/model/Post.java @@ -0,0 +1,99 @@ +package com.blackdog.springbootBoardJpa.domain.post.model; + +import com.blackdog.springbootBoardJpa.domain.user.model.User; +import com.blackdog.springbootBoardJpa.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.Getter; +import org.springframework.util.Assert; + +@Getter +@Entity +@Table(name = "posts") +public class Post extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "users_id", referencedColumnName = "id", nullable = false) + private User user; + + protected Post() { + } + + public static Post builder() { + return new Post(); + } + + public Post title(String title) { + this.title = title; + return this; + } + + public Post content(String content) { + this.content = content; + return this; + } + + public Post user(User user) { + this.user = user; + return this; + } + + public Post build() { + return new Post( + this.title, + this.content, + this.user + ); + } + + private Post( + String title, + String content, + User user + ) { + Assert.notNull(title, "title은 공백일 수 없습니다."); + Assert.notNull(content, "content는 공백일 수 없습니다."); + + this.title = title; + this.content = content; + this.user = user; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public User getUser() { + return user; + } + + private void changeTitle(String title) { + this.title = title; + } + + private void changeContent(String content) { + this.content = content; + } + + public void changePost(String title, String content) { + changeTitle(title); + changeContent(content); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/repository/PostRepository.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/repository/PostRepository.java new file mode 100644 index 000000000..5636fa20c --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/repository/PostRepository.java @@ -0,0 +1,14 @@ +package com.blackdog.springbootBoardJpa.domain.post.repository; + +import com.blackdog.springbootBoardJpa.domain.post.model.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { + + Page findPostsByUserId(Long userId, Pageable pageable); + + void deleteByIdAndUserId(Long id, Long userId); + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/PostService.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/PostService.java new file mode 100644 index 000000000..48b6c655a --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/PostService.java @@ -0,0 +1,90 @@ +package com.blackdog.springbootBoardJpa.domain.post.service; + +import com.blackdog.springbootBoardJpa.domain.post.model.Post; +import com.blackdog.springbootBoardJpa.domain.post.repository.PostRepository; +import com.blackdog.springbootBoardJpa.domain.post.service.converter.PostServiceConverter; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostCreateRequest; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponse; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponses; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostUpdateRequest; +import com.blackdog.springbootBoardJpa.domain.user.model.User; +import com.blackdog.springbootBoardJpa.domain.user.repository.UserRepository; +import com.blackdog.springbootBoardJpa.global.exception.PermissionDeniedException; +import com.blackdog.springbootBoardJpa.global.exception.PostNotFoundException; +import com.blackdog.springbootBoardJpa.global.exception.UserNotFoundException; +import jakarta.validation.Valid; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +import static com.blackdog.springbootBoardJpa.global.response.ErrorCode.*; + +@Service +@Transactional(readOnly = true) +public class PostService { + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final PostServiceConverter converter; + + public PostService( + final PostRepository postRepository, + final UserRepository userRepository, + final PostServiceConverter converter + ) { + this.postRepository = postRepository; + this.userRepository = userRepository; + this.converter = converter; + } + + @Transactional + public PostResponse savePost(Long userId, @Valid PostCreateRequest request) { + User user = userRepository + .findById(userId) + .orElseThrow(() -> new UserNotFoundException(NOT_FOUND_USER)); + + Post post = converter.toEntity(request, user); + + return converter.toResponse( + postRepository.save(post)); + } + + @Transactional + public PostResponse updatePost(Long userId, Long postId, @Valid PostUpdateRequest dto) { + Post targetPost = postRepository + .findById(postId) + .orElseThrow(() -> new PostNotFoundException(NOT_FOUND_POST)); + + boolean isOwner = Objects.equals(targetPost.getUser().getId(), userId); + if (!isOwner) { + throw new PermissionDeniedException(PERMISSION_DENIED); + } + + targetPost.changePost(dto.title(), dto.content()); + return converter.toResponse(targetPost); + } + + @Transactional + public void deletePostById(Long userId, Long id) { + postRepository.deleteByIdAndUserId(id, userId); + } + + public PostResponses findAllPosts(Pageable pageable) { + return converter.toResponses( + postRepository.findAll(pageable)); + } + + public PostResponse findPostById(Long id) { + return converter.toResponse( + postRepository.findById(id) + .orElseThrow(() -> new PostNotFoundException(NOT_FOUND_POST))); + } + + public PostResponses findPostsByUserId(Long userId, Pageable pageable) { + return converter.toResponses( + postRepository.findPostsByUserId(userId, pageable)); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/converter/PostServiceConverter.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/converter/PostServiceConverter.java new file mode 100644 index 000000000..21f4db8f4 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/converter/PostServiceConverter.java @@ -0,0 +1,36 @@ +package com.blackdog.springbootBoardJpa.domain.post.service.converter; + +import com.blackdog.springbootBoardJpa.domain.post.model.Post; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostCreateRequest; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponse; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponses; +import com.blackdog.springbootBoardJpa.domain.user.model.User; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +@Component +public class PostServiceConverter { + + public Post toEntity(PostCreateRequest dto, User user) { + return Post.builder() + .title(dto.title()) + .content(dto.content()) + .user(user) + .build(); + } + + public PostResponse toResponse(Post post) { + return new PostResponse( + post.getId(), + post.getTitle(), + post.getContent(), + post.getUser().getName().getNameValue(), + post.getCreatedAt(), + post.getUpdatedAt() + ); + } + + public PostResponses toResponses(Page posts) { + return new PostResponses(posts.map(this::toResponse)); + } +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostCreateRequest.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostCreateRequest.java new file mode 100644 index 000000000..b8ff3bd58 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostCreateRequest.java @@ -0,0 +1,15 @@ +package com.blackdog.springbootBoardJpa.domain.post.service.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record PostCreateRequest( + @NotBlank + String title, + + @NotNull + @Size(max = 255) + String content +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostResponse.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostResponse.java new file mode 100644 index 000000000..73a602985 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostResponse.java @@ -0,0 +1,25 @@ +package com.blackdog.springbootBoardJpa.domain.post.service.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.time.LocalDateTime; + +public record PostResponse( + Long id, + + String title, + + String content, + + String name, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdAt, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostResponses.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostResponses.java new file mode 100644 index 000000000..edaa5dea8 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostResponses.java @@ -0,0 +1,8 @@ +package com.blackdog.springbootBoardJpa.domain.post.service.dto; + +import org.springframework.data.domain.Page; + +public record PostResponses( + Page postResponses +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostUpdateRequest.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostUpdateRequest.java new file mode 100644 index 000000000..7248fa8cc --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/post/service/dto/PostUpdateRequest.java @@ -0,0 +1,15 @@ +package com.blackdog.springbootBoardJpa.domain.post.service.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record PostUpdateRequest( + @NotBlank + String title, + + @NotNull + @Size(max = 255) + String content +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/UserController.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/UserController.java new file mode 100644 index 000000000..cdc7de337 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/UserController.java @@ -0,0 +1,91 @@ +package com.blackdog.springbootBoardJpa.domain.user.controller; + +import com.blackdog.springbootBoardJpa.domain.user.controller.converter.UserControllerConverter; +import com.blackdog.springbootBoardJpa.domain.user.controller.dto.UserCreateDto; +import com.blackdog.springbootBoardJpa.domain.user.service.UserService; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponse; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponses; +import com.blackdog.springbootBoardJpa.global.response.SuccessResponse; +import jakarta.validation.Valid; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.blackdog.springbootBoardJpa.global.response.SuccessCode.USER_DELETE_SUCCESS; + +@RestController +@RequestMapping(path = "/users", produces = MediaType.APPLICATION_JSON_VALUE) +public class UserController { + + private final UserService service; + private final UserControllerConverter converter; + + public UserController( + final UserService service, + final UserControllerConverter converter + ) { + this.service = service; + this.converter = converter; + } + + /** + * register or update user data + * + * @param createDto + * @return ResponseEntity + * HttpStatus 201 + */ + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity saveUser(@Valid @RequestBody UserCreateDto createDto) { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(service.saveUser( + converter.toRequest(createDto))); + } + + /** + * delete user by userId + * + * @param userId + * @return ResponseEntity + * HttpStatus 200 + */ + @DeleteMapping(path = "/{userId}") + public ResponseEntity deleteUser(@PathVariable long userId) { + service.deleteUserById(userId); + return ResponseEntity + .status(HttpStatus.OK) + .body(SuccessResponse.of(USER_DELETE_SUCCESS)); + } + + /** + * search all users with pagination + * + * @param pageable + * @return ResponseEntity + * HttpStatus 200 + */ + @GetMapping + public ResponseEntity getAllUsers(Pageable pageable) { + return ResponseEntity + .status(HttpStatus.OK) + .body(service.findAllUsers(pageable)); + } + + /** + * search user by userId + * + * @param userId + * @return ResponseEntity + * HttpStatus 200 + */ + @GetMapping(path = "/{userId}") + public ResponseEntity getUser(@PathVariable Long userId) { + return ResponseEntity + .status(HttpStatus.OK) + .body(service.findUserById(userId)); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/converter/UserControllerConverter.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/converter/UserControllerConverter.java new file mode 100644 index 000000000..0d78e64ac --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/converter/UserControllerConverter.java @@ -0,0 +1,20 @@ +package com.blackdog.springbootBoardJpa.domain.user.controller.converter; + +import com.blackdog.springbootBoardJpa.domain.user.controller.dto.UserCreateDto; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Age; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Name; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserCreateRequest; +import org.springframework.stereotype.Component; + +@Component +public class UserControllerConverter { + + public UserCreateRequest toRequest(UserCreateDto createDto) { + return new UserCreateRequest( + new Name(createDto.name()), + new Age(createDto.age()), + createDto.hobby() + ); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/dto/UserCreateDto.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/dto/UserCreateDto.java new file mode 100644 index 000000000..c62450928 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/controller/dto/UserCreateDto.java @@ -0,0 +1,16 @@ +package com.blackdog.springbootBoardJpa.domain.user.controller.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public record UserCreateDto( + @NotBlank(message = "이름은 공백일 수 없습니다.") + String name, + + @Min(value = 0, message = "나이는 최소 0세 이상이어야 합니다") + int age, + + @NotBlank(message = "이름은 공백일 수 없습니다.") + String hobby +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/User.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/User.java new file mode 100644 index 000000000..56daa912c --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/User.java @@ -0,0 +1,80 @@ +package com.blackdog.springbootBoardJpa.domain.user.model; + +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Age; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Name; +import com.blackdog.springbootBoardJpa.global.entity.BaseEntity; +import jakarta.persistence.*; + +@Entity +@Table(name = "users") +public class User extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private Name name; + + @Embedded + private Age age; + + @Column + private String hobby; + + protected User() { + } + + public static User builder() { + return new User(); + } + + public User name(final Name name) { + this.name = name; + return this; + } + + public User age(final Age age) { + this.age = age; + return this; + } + + public User hobby(final String hobby) { + this.hobby = hobby; + return this; + } + + public User build() { + return new User( + this.name, + this.age, + this.hobby + ); + } + + public User( + Name name, + Age age, + String hobby + ) { + this.name = name; + this.age = age; + this.hobby = hobby; + } + + public Long getId() { + return id; + } + + public Name getName() { + return name; + } + + public Age getAge() { + return age; + } + + public String getHobby() { + return hobby; + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/vo/Age.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/vo/Age.java new file mode 100644 index 000000000..36484aa96 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/vo/Age.java @@ -0,0 +1,27 @@ +package com.blackdog.springbootBoardJpa.domain.user.model.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.validation.constraints.Positive; +import org.springframework.util.Assert; + +@Embeddable +public class Age { + + @Column(nullable = false) + @Positive + private int ageValue; + + protected Age() { + } + + public Age(final int ageValue) { + Assert.isTrue(ageValue > 0, "나이는 1살 이상이어야 합니다."); + this.ageValue = ageValue; + } + + public int getAgeValue() { + return ageValue; + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/vo/Name.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/vo/Name.java new file mode 100644 index 000000000..8d3f673d2 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/model/vo/Name.java @@ -0,0 +1,28 @@ +package com.blackdog.springbootBoardJpa.domain.user.model.vo; + +import io.micrometer.common.util.StringUtils; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.validation.constraints.NotBlank; +import org.springframework.util.Assert; + +@Embeddable +public class Name { + + @NotBlank + @Column(nullable = false) + private String nameValue; + + protected Name() { + } + + public Name(final String nameValue) { + Assert.isTrue(StringUtils.isNotBlank(nameValue), "name must be given"); + this.nameValue = nameValue; + } + + public String getNameValue() { + return nameValue; + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/repository/UserRepository.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/repository/UserRepository.java new file mode 100644 index 000000000..cc07e767c --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/repository/UserRepository.java @@ -0,0 +1,7 @@ +package com.blackdog.springbootBoardJpa.domain.user.repository; + +import com.blackdog.springbootBoardJpa.domain.user.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/UserService.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/UserService.java new file mode 100644 index 000000000..5d100f13d --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/UserService.java @@ -0,0 +1,52 @@ +package com.blackdog.springbootBoardJpa.domain.user.service; + +import com.blackdog.springbootBoardJpa.domain.user.repository.UserRepository; +import com.blackdog.springbootBoardJpa.domain.user.service.converter.UserServiceConverter; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserCreateRequest; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponse; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponses; +import com.blackdog.springbootBoardJpa.global.exception.UserNotFoundException; +import jakarta.validation.Valid; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.blackdog.springbootBoardJpa.global.response.ErrorCode.NOT_FOUND_USER; + +@Service +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository repository; + private final UserServiceConverter converter; + + public UserService( + final UserRepository repository, + final UserServiceConverter converter + ) { + this.repository = repository; + this.converter = converter; + } + + @Transactional + public UserResponse saveUser(@Valid UserCreateRequest dto) { + return converter.toResponse( + repository.save(converter.toEntity(dto))); + } + + @Transactional + public void deleteUserById(Long id) { + repository.deleteById(id); + } + + public UserResponses findAllUsers(Pageable pageable) { + return converter.toResponses(repository.findAll(pageable)); + } + + public UserResponse findUserById(Long id) { + return converter.toResponse( + repository.findById(id) + .orElseThrow(() -> new UserNotFoundException(NOT_FOUND_USER))); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/converter/UserServiceConverter.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/converter/UserServiceConverter.java new file mode 100644 index 000000000..1fa03bf98 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/converter/UserServiceConverter.java @@ -0,0 +1,36 @@ +package com.blackdog.springbootBoardJpa.domain.user.service.converter; + +import com.blackdog.springbootBoardJpa.domain.user.model.User; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserCreateRequest; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponse; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponses; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +@Component +public class UserServiceConverter { + + public User toEntity(UserCreateRequest dto) { + return User.builder() + .name(dto.name()) + .age(dto.age()) + .hobby(dto.hobby()) + .build(); + } + + public UserResponse toResponse(User user) { + return new UserResponse( + user.getId(), + user.getName().getNameValue(), + user.getAge().getAgeValue(), + user.getHobby(), + user.getCreatedAt(), + user.getUpdatedAt() + ); + } + + public UserResponses toResponses(Page users) { + return new UserResponses(users.map(this::toResponse)); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserCreateRequest.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserCreateRequest.java new file mode 100644 index 000000000..c8b63c2b9 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserCreateRequest.java @@ -0,0 +1,18 @@ +package com.blackdog.springbootBoardJpa.domain.user.service.dto; + +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Age; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Name; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record UserCreateRequest( + @NotNull + Name name, + + @NotNull + Age age, + + @NotBlank + String hobby +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserResponse.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserResponse.java new file mode 100644 index 000000000..0f760ff8c --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserResponse.java @@ -0,0 +1,22 @@ +package com.blackdog.springbootBoardJpa.domain.user.service.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +public record UserResponse( + Long id, + + String name, + + int age, + + String hobby, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdAt, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserResponses.java b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserResponses.java new file mode 100644 index 000000000..5cf787757 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/domain/user/service/dto/UserResponses.java @@ -0,0 +1,8 @@ +package com.blackdog.springbootBoardJpa.domain.user.service.dto; + +import org.springframework.data.domain.Page; + +public record UserResponses( + Page userResponses +) { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/config/JasyptConfig.java b/src/main/java/com/blackdog/springbootBoardJpa/global/config/JasyptConfig.java new file mode 100644 index 000000000..bf4db2ba9 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/config/JasyptConfig.java @@ -0,0 +1,80 @@ +package com.blackdog.springbootBoardJpa.global.config; + +import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties; +import org.jasypt.encryption.StringEncryptor; +import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; +import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +@Configuration +@ConfigurationProperties("jasypt.encryptor") +@EnableEncryptableProperties +public class JasyptConfig { + + private String algorithm; + private int poolSize; + private String stringOutputType; + private int keyObtentionIterations; + + @Bean("jasyptStringEncryptor") + public StringEncryptor jasyptStringEncryptor() { + PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); + SimpleStringPBEConfig configuration = new SimpleStringPBEConfig(); + configuration.setAlgorithm(algorithm); + configuration.setPoolSize(poolSize); + configuration.setStringOutputType(stringOutputType); + configuration.setKeyObtentionIterations(keyObtentionIterations); + configuration.setPassword(getJasyptEncryptorPassword()); + encryptor.setConfig(configuration); + return encryptor; + } + + private String getJasyptEncryptorPassword() { + try { + ClassPathResource resource = new ClassPathResource("src/main/resources/jasypt-encryptor-password.txt"); + return String.join("", Files.readAllLines(Paths.get(resource.getPath()))); + } catch (IOException e) { + throw new RuntimeException("jasypt password file not found"); + } + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public int getPoolSize() { + return poolSize; + } + + public void setPoolSize(int poolSize) { + this.poolSize = poolSize; + } + + public String getStringOutputType() { + return stringOutputType; + } + + public void setStringOutputType(String stringOutputType) { + this.stringOutputType = stringOutputType; + } + + public int getKeyObtentionIterations() { + return keyObtentionIterations; + } + + public void setKeyObtentionIterations(int keyObtentionIterations) { + this.keyObtentionIterations = keyObtentionIterations; + } +} + diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/config/JpaConfig.java b/src/main/java/com/blackdog/springbootBoardJpa/global/config/JpaConfig.java new file mode 100644 index 000000000..f8c1db801 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/config/JpaConfig.java @@ -0,0 +1,9 @@ +package com.blackdog.springbootBoardJpa.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/entity/BaseEntity.java b/src/main/java/com/blackdog/springbootBoardJpa/global/entity/BaseEntity.java new file mode 100644 index 000000000..722008ebd --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/entity/BaseEntity.java @@ -0,0 +1,32 @@ +package com.blackdog.springbootBoardJpa.global.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseEntity { + + @CreatedDate + @Column(updatable = false, name = "created_at", columnDefinition = "DATETIME") + protected LocalDateTime createdAt; + + @LastModifiedDate + @Column(updatable = false, name = "updated_at", columnDefinition = "DATETIME") + protected LocalDateTime updatedAt; + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/exception/GlobalExceptionHandler.java b/src/main/java/com/blackdog/springbootBoardJpa/global/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..6ecab357d --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,81 @@ +package com.blackdog.springbootBoardJpa.global.exception; + +import com.blackdog.springbootBoardJpa.global.response.ErrorCode; +import com.blackdog.springbootBoardJpa.global.response.ErrorResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import static com.blackdog.springbootBoardJpa.global.response.ErrorCode.*; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { + ErrorResponse errorResponse = ErrorResponse.of(INVALID_METHOD_ERROR); + log.warn("{}", errorResponse); + log.warn("{}", e.getCause()); + e.printStackTrace(); + return ResponseEntity + .status(HttpStatus.METHOD_NOT_ALLOWED) + .body(errorResponse); + } + + @ExceptionHandler(PermissionDeniedException.class) + public ResponseEntity permissionDeniedExceptionHandler(PermissionDeniedException e) { + ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.PERMISSION_DENIED); + log.warn("{}", errorResponse); + log.warn("{}", e.getCause()); + return ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body(errorResponse); + } + + @ExceptionHandler(PostNotFoundException.class) + public ResponseEntity postNotFoundExceptionHandler(PostNotFoundException e) { + ErrorResponse errorResponse = ErrorResponse.of(NOT_FOUND_POST); + log.warn("{}", errorResponse); + log.warn("{}", e.getCause()); + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(errorResponse); + } + + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity userNotFoundExceptionHandler(UserNotFoundException e) { + ErrorResponse errorResponse = ErrorResponse.of(NOT_FOUND_USER); + log.warn("{}", errorResponse); + log.warn("{}", e.getCause()); + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(errorResponse); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException e) { + ErrorResponse errorResponse = ErrorResponse.of(INTERNAL_SERVER_ERROR); + log.warn("{}", errorResponse); + log.warn("{}", e.getCause()); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleAllException(Exception e) { + ErrorResponse errorResponse = ErrorResponse.of(INTERNAL_SERVER_ERROR); + log.warn("{}", errorResponse); + log.warn("{}", e.getCause()); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/exception/PermissionDeniedException.java b/src/main/java/com/blackdog/springbootBoardJpa/global/exception/PermissionDeniedException.java new file mode 100644 index 000000000..4a8d75500 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/exception/PermissionDeniedException.java @@ -0,0 +1,12 @@ +package com.blackdog.springbootBoardJpa.global.exception; + +import com.blackdog.springbootBoardJpa.global.response.ErrorCode; + +public class PermissionDeniedException extends RuntimeException { + private final ErrorCode errorCode; + + public PermissionDeniedException(final ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/exception/PostNotFoundException.java b/src/main/java/com/blackdog/springbootBoardJpa/global/exception/PostNotFoundException.java new file mode 100644 index 000000000..08d445562 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/exception/PostNotFoundException.java @@ -0,0 +1,12 @@ +package com.blackdog.springbootBoardJpa.global.exception; + +import com.blackdog.springbootBoardJpa.global.response.ErrorCode; + +public class PostNotFoundException extends RuntimeException { + private final ErrorCode errorCode; + + public PostNotFoundException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/exception/UserNotFoundException.java b/src/main/java/com/blackdog/springbootBoardJpa/global/exception/UserNotFoundException.java new file mode 100644 index 000000000..908445564 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/exception/UserNotFoundException.java @@ -0,0 +1,12 @@ +package com.blackdog.springbootBoardJpa.global.exception; + +import com.blackdog.springbootBoardJpa.global.response.ErrorCode; + +public class UserNotFoundException extends RuntimeException { + private final ErrorCode errorCode; + + public UserNotFoundException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/response/ErrorCode.java b/src/main/java/com/blackdog/springbootBoardJpa/global/response/ErrorCode.java new file mode 100644 index 000000000..f39d02f9c --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/response/ErrorCode.java @@ -0,0 +1,41 @@ +package com.blackdog.springbootBoardJpa.global.response; + +import org.springframework.http.HttpStatus; + +public enum ErrorCode { + //global + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "G001", "Internal Server Error"), + INVALID_METHOD_ERROR(HttpStatus.METHOD_NOT_ALLOWED, "G002", "Method Argument가 적절하지 않습니다."), + + //user + NOT_FOUND_USER(HttpStatus.NOT_FOUND, "U001", "존재하지 않는 유저입니다."), + + //post + NOT_FOUND_POST(HttpStatus.NOT_FOUND, "P001", "존재하지 않는 게시글입니다."), + PERMISSION_DENIED(HttpStatus.NON_AUTHORITATIVE_INFORMATION, "P002", "권한 없는 유저입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + ErrorCode(final HttpStatus httpStatus, + final String code, + final String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } + +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/response/ErrorResponse.java b/src/main/java/com/blackdog/springbootBoardJpa/global/response/ErrorResponse.java new file mode 100644 index 000000000..eebfdaff4 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/response/ErrorResponse.java @@ -0,0 +1,37 @@ +package com.blackdog.springbootBoardJpa.global.response; + +public class ErrorResponse { + private final String code; + private final String message; + + public ErrorResponse( + final String code, + final String message + ) { + this.code = code; + this.message = message; + } + + public static ErrorResponse of(ErrorCode errorCode) { + return new ErrorResponse( + errorCode.getCode(), + errorCode.getMessage() + ); + } + + @Override + public String toString() { + return "ErrorResponse{" + + "code='" + code + '\'' + + ", message='" + message + '\'' + + '}'; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/response/SuccessCode.java b/src/main/java/com/blackdog/springbootBoardJpa/global/response/SuccessCode.java new file mode 100644 index 000000000..353014e82 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/response/SuccessCode.java @@ -0,0 +1,23 @@ +package com.blackdog.springbootBoardJpa.global.response; + +public enum SuccessCode { + /** + * Post - 게시글 관련 Code + */ + POST_DELETE_SUCCESS("게시글이 성공적으로 삭제되었습니다."), + + /** + * User - 유저 관련 성공 Code + */ + USER_DELETE_SUCCESS("유저가 성공적으로 삭제되었습니다."); + + private final String message; + + SuccessCode(final String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/blackdog/springbootBoardJpa/global/response/SuccessResponse.java b/src/main/java/com/blackdog/springbootBoardJpa/global/response/SuccessResponse.java new file mode 100644 index 000000000..933f983b0 --- /dev/null +++ b/src/main/java/com/blackdog/springbootBoardJpa/global/response/SuccessResponse.java @@ -0,0 +1,17 @@ +package com.blackdog.springbootBoardJpa.global.response; + +public class SuccessResponse { + private String message; + + private SuccessResponse(String message) { + this.message = message; + } + + public static SuccessResponse of(SuccessCode code) { + return new SuccessResponse(code.getMessage()); + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..f428a2590 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,34 @@ +jasypt: + encryptor: + bean: jasyptStringEncryptor + algorithm: PBEWithMD5AndDES + provider-name: SunJCE + pool-size: 2 + key-obtention-iterations: 1000 + string-output-type: base64 + +spring: + datasource: + url: ENC(NG45wSV57jwBic94bKgq+HcjwGxbDE7Mht8GAr5SI4byEIZ6GT+ieKinVbZk2FC5keTWRJVqsFM=) + username: ENC(VzS/i/YnWH1aKINeKZElTw==) + password: ENC(4ZgFCYri6aT5EMvyXPdAwWPXDkrYIZHO) + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + show_sql: true + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + open-in-view: false + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + + logging: + level: + org: + hibernate: + type: + descriptor: + sql: trace diff --git a/src/main/resources/jasypt-encryptor-password.txt b/src/main/resources/jasypt-encryptor-password.txt new file mode 100644 index 000000000..a3f3f49b3 --- /dev/null +++ b/src/main/resources/jasypt-encryptor-password.txt @@ -0,0 +1 @@ +key1234! diff --git a/src/main/resources/static/doc/index.html b/src/main/resources/static/doc/index.html new file mode 100644 index 000000000..e2e1e0273 --- /dev/null +++ b/src/main/resources/static/doc/index.html @@ -0,0 +1,471 @@ + + + + + + + +Spring Boot JPA Board + + + + + +
+
+

Post API docs

+
+ +
+
+
+

User API docs

+
+ +
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/doc/post.html b/src/main/resources/static/doc/post.html new file mode 100644 index 000000000..68c47f8c2 --- /dev/null +++ b/src/main/resources/static/doc/post.html @@ -0,0 +1,894 @@ + + + + + + + + +API docs + + + + + +
+
+

Post

+
+
+

게시물 생성

+
+
Request
+
+
POST /posts/1 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 52
+Host: localhost:8080
+
+{
+  "title" : "subject2",
+  "content" : "content2"
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

title

String

게시물 제목

content

String

게시물 내용

+
+
Response
+
+
HTTP/1.1 201 Created
+Content-Type: application/json
+Content-Length: 163
+
+{
+  "id" : 2,
+  "title" : "subject2",
+  "content" : "content2",
+  "name" : "둘리",
+  "createdAt" : "2023-08-08 15:26:14",
+  "updatedAt" : "2023-08-08 15:26:14"
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

게시물 ID

title

String

게시물 제목

content

String

게시물 내용

name

String

작성자 이름

createdAt

String

게시물 작성 시간

updatedAt

String

게시물 수정 시간

+
+
+
+

게시물 전체 조회

+
+
Request
+
+
GET /posts?page=0&size=5 HTTP/1.1
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 724
+
+{
+  "postResponses" : {
+    "content" : [ {
+      "id" : 1,
+      "title" : "subject1",
+      "content" : "content1",
+      "name" : "둘리",
+      "createdAt" : "2023-08-08 15:26:14",
+      "updatedAt" : "2023-08-08 15:26:14"
+    }, {
+      "id" : 2,
+      "title" : "subject2",
+      "content" : "content2",
+      "name" : "둘리",
+      "createdAt" : "2023-08-08 15:26:14",
+      "updatedAt" : "2023-08-08 15:26:14"
+    } ],
+    "pageable" : "INSTANCE",
+    "totalPages" : 1,
+    "totalElements" : 2,
+    "last" : true,
+    "size" : 2,
+    "number" : 0,
+    "sort" : {
+      "empty" : true,
+      "sorted" : false,
+      "unsorted" : true
+    },
+    "numberOfElements" : 2,
+    "first" : true,
+    "empty" : false
+  }
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

postResponses

Object

게시물 응답

postResponses.content

Array

게시물 정보 배열

postResponses.content[].id

Number

게시물 ID

postResponses.content[].title

String

게시물 제목

postResponses.content[].content

String

게시물 내용

postResponses.content[].name

String

게시물 작성자 이름

postResponses.content[].createdAt

String

게시물 생성일

postResponses.content[].updatedAt

String

게시물 갱신일

postResponses.totalElements

Number

totalElements

postResponses.totalPages

Number

totalPages

+
+
+
+

게시글 단건 조회

+
+
Request
+
+
GET /posts/1 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 163
+
+{
+  "id" : 2,
+  "title" : "subject2",
+  "content" : "content2",
+  "name" : "둘리",
+  "createdAt" : "2023-08-08 15:26:14",
+  "updatedAt" : "2023-08-08 15:26:14"
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

게시글 아이디

title

String

게시글 제목

content

String

게시글 본문

name

String

게시글 작성자 이름

createdAt

String

게시글 작성일

updatedAt

String

게시글 갱신일

+
+
+

게시물 작성자로 조회

+
+
Request
+
+
GET /posts/user/1?page=0&size=5 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 724
+
+{
+  "postResponses" : {
+    "content" : [ {
+      "id" : 1,
+      "title" : "subject1",
+      "content" : "content1",
+      "name" : "둘리",
+      "createdAt" : "2023-08-08 15:26:14",
+      "updatedAt" : "2023-08-08 15:26:14"
+    }, {
+      "id" : 2,
+      "title" : "subject2",
+      "content" : "content2",
+      "name" : "둘리",
+      "createdAt" : "2023-08-08 15:26:14",
+      "updatedAt" : "2023-08-08 15:26:14"
+    } ],
+    "pageable" : "INSTANCE",
+    "totalPages" : 1,
+    "totalElements" : 2,
+    "last" : true,
+    "size" : 2,
+    "number" : 0,
+    "sort" : {
+      "empty" : true,
+      "sorted" : false,
+      "unsorted" : true
+    },
+    "numberOfElements" : 2,
+    "first" : true,
+    "empty" : false
+  }
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

postResponses

Object

게시글 응답

postResponses.content

Array

게시글 정보 배열

postResponses.content[].id

Number

게시글 아이디

postResponses.content[].title

String

게시글 이름

postResponses.content[].content

String

게시글 나이

postResponses.content[].name

String

게시글 작성자 이름

postResponses.content[].createdAt

String

게시글 생성일

postResponses.content[].updatedAt

String

게시글 갱신일

postResponses.totalElements

Number

totalElements

postResponses.totalPages

Number

totalPages

+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/doc/user.html b/src/main/resources/static/doc/user.html new file mode 100644 index 000000000..7119d04e3 --- /dev/null +++ b/src/main/resources/static/doc/user.html @@ -0,0 +1,790 @@ + + + + + + + + +API docs + + + + + +
+
+

유저

+
+
+
+

유저 생성

+
+
Request
+
+
POST /users HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Accept: application/json
+Content-Length: 40
+Host: localhost:8080
+
+{"name":"Kim","age":23,"hobby":"축구"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

name

String

회원 이름

age

Number

나이

hobby

String

취미

+
+
Response
+
+
HTTP/1.1 201 Created
+Content-Type: application/json
+Content-Length: 115
+
+{"id":1,"name":"Kim","age":23,"hobby":"축구","createdAt":"2023-08-08 15:26:15","updatedAt":"2023-08-08 15:26:15"}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

회원 ID

name

String

회원 이름

age

Number

나이

hobby

String

취미

createdAt

String

회원 가입 시간

updatedAt

String

회원 수정 시간

+
+
+
+

유저 삭제

+
+
Request
+
+
DELETE /users/1 HTTP/1.1
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 204 No Content
+
+
+
+
+
+

유저 단건 조회

+
+
Request
+
+
GET /users/1 HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Accept: application/json
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 147
+
+{
+  "id" : 1,
+  "name" : "Park",
+  "age" : 26,
+  "hobby" : "여행",
+  "createdAt" : "2023-08-08 15:26:15",
+  "updatedAt" : "2023-08-08 15:26:15"
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

id

Number

유저 아이디

name

String

유저 이름

age

Number

유저 나이

hobby

String

유저 취미

createdAt

String

유저 생성일

updatedAt

String

유저 갱신일

+
+
+
+

유저 전체 조회

+
+
Request
+
+
GET /users?page=0&size=5 HTTP/1.1
+Host: localhost:8080
+
+
+
+
Response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 692
+
+{
+  "userResponses" : {
+    "content" : [ {
+      "id" : 1,
+      "name" : "Park",
+      "age" : 26,
+      "hobby" : "여행",
+      "createdAt" : "2023-08-08 15:26:15",
+      "updatedAt" : "2023-08-08 15:26:15"
+    }, {
+      "id" : 2,
+      "name" : "Park",
+      "age" : 26,
+      "hobby" : "여행",
+      "createdAt" : "2023-08-08 15:26:15",
+      "updatedAt" : "2023-08-08 15:26:15"
+    } ],
+    "pageable" : "INSTANCE",
+    "totalPages" : 1,
+    "totalElements" : 2,
+    "last" : true,
+    "size" : 2,
+    "number" : 0,
+    "sort" : {
+      "empty" : true,
+      "sorted" : false,
+      "unsorted" : true
+    },
+    "numberOfElements" : 2,
+    "first" : true,
+    "empty" : false
+  }
+}
+
+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

userResponses

Object

유저 응답

userResponses.content[]

Array

유저 정보 배열

userResponses.content[].id

Number

유저 아이디

userResponses.content[].name

String

유저 이름

userResponses.content[].age

Number

유저 나이

userResponses.content[].hobby

String

유저 취미

userResponses.content[].createdAt

String

유저 생성일

userResponses.content[].updatedAt

String

유저 갱신일

userResponses.totalElements

Number

totalElements

userResponses.totalPages

Number

totalPages

+
+
+
+
+ + + \ No newline at end of file diff --git a/src/test/java/com/blackdog/springbootBoardJpa/SpringbootBoardJpaApplicationTests.java b/src/test/java/com/blackdog/springbootBoardJpa/SpringbootBoardJpaApplicationTests.java new file mode 100644 index 000000000..41904e0e5 --- /dev/null +++ b/src/test/java/com/blackdog/springbootBoardJpa/SpringbootBoardJpaApplicationTests.java @@ -0,0 +1,13 @@ +package com.blackdog.springbootBoardJpa; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringbootBoardJpaApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/blackdog/springbootBoardJpa/global/JasyptTest.java b/src/test/java/com/blackdog/springbootBoardJpa/global/JasyptTest.java new file mode 100644 index 000000000..81b01982f --- /dev/null +++ b/src/test/java/com/blackdog/springbootBoardJpa/global/JasyptTest.java @@ -0,0 +1,43 @@ +package com.blackdog.springbootBoardJpa.global; + +import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class JasyptTest { + + @Test + void jasypt() { + String url = "jdbc:mysql://localhost:3306/jpa_board_mission"; + String username = "root"; + String password = "root1234!"; + + String encryptUrl = jasyptEncrypt(url); + String encryptUsername = jasyptEncrypt(username); + String encryptPassword = jasyptEncrypt(password); + + System.out.println("encrypt url : " + encryptUrl); + System.out.println("encrypt username: " + encryptUsername); + System.out.println("encrypt password: " + encryptPassword); + + assertThat(url).isEqualTo(jasyptDecrypt(encryptUrl)); + } + + private String jasyptEncrypt(String input) { + String key = "key1234!"; + StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); + encryptor.setAlgorithm("PBEWithMD5AndDES"); + encryptor.setPassword(key); + return encryptor.encrypt(input); + } + + private String jasyptDecrypt(String input) { + String key = "key1234!"; + StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); + encryptor.setAlgorithm("PBEWithMD5AndDES"); + encryptor.setPassword(key); + return encryptor.decrypt(input); + } + +} diff --git a/src/test/java/com/blackdog/springbootBoardJpa/post/controller/PostControllerTest.java b/src/test/java/com/blackdog/springbootBoardJpa/post/controller/PostControllerTest.java new file mode 100644 index 000000000..6498b97f1 --- /dev/null +++ b/src/test/java/com/blackdog/springbootBoardJpa/post/controller/PostControllerTest.java @@ -0,0 +1,351 @@ +package com.blackdog.springbootBoardJpa.post.controller; + +import com.blackdog.springbootBoardJpa.domain.post.controller.PostController; +import com.blackdog.springbootBoardJpa.domain.post.controller.converter.PostControllerConverter; +import com.blackdog.springbootBoardJpa.domain.post.controller.dto.PostCreateDto; +import com.blackdog.springbootBoardJpa.domain.post.service.PostService; +import com.blackdog.springbootBoardJpa.domain.post.service.converter.PostServiceConverter; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostCreateRequest; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponse; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponses; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostUpdateRequest; +import com.blackdog.springbootBoardJpa.domain.user.model.User; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Age; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Name; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@AutoConfigureRestDocs +@WebMvcTest(PostController.class) +@Import({PostServiceConverter.class, PostControllerConverter.class, PostService.class}) +class PostControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + PostControllerConverter ControllerConverter; + + @Autowired + PostServiceConverter serviceConverter; + + @Autowired + ObjectMapper objectMapper; + + @MockBean + PostService service; + + @ParameterizedTest + @DisplayName("존재하는 유저로 게시글을 생성하면 성공한다.") + @MethodSource("provideTestData") + void savePost_Dto_SaveReturnResponse(PostCreateDto dto, PostResponse response) throws Exception { + // given + given(service.savePost(any(Long.class), any(PostCreateRequest.class))) + .willReturn(response); + + // when + mockMvc.perform(RestDocumentationRequestBuilders.post("/posts/{userId}", 1L) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isCreated()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.title").value(dto.title())) + .andDo(document("post-save", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("userId").description("유저 아이디") + ), + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("게시물 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시물 내용")), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시물 ID"), + fieldWithPath("title").type(JsonFieldType.STRING).description("게시물 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시물 내용"), + fieldWithPath("name").type(JsonFieldType.STRING).description("작성자 이름"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시물 작성 시간"), + fieldWithPath("updatedAt").type(JsonFieldType.STRING).description("게시물 수정 시간") + ) + )); + + // then + verify(service, times(1)).savePost(any(), any()); + } + + @Test + @DisplayName("Dto의 필드가 valid하지 않다면 유효성 검사에 실패한다.") + void savePost_Dto_ThrowMethodArgumentNotValidException() throws Exception { + PostCreateDto dto = new PostCreateDto("", "내용"); + MockHttpServletRequestBuilder builder = post("/posts/{userId}", 1L) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(dto)); + + MvcResult result = mockMvc.perform(builder) + .andDo(print()) + .andReturn(); + + String message = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + + Assertions.assertThat(message).contains("Method Argument가 적절하지 않습니다."); + + } + + + @ParameterizedTest + @DisplayName("존재하는 게시글을 수정하면 성공한다.") + @MethodSource("provideTestData") + void updatePost_Dto_UpdateReturnResponse(PostCreateDto dto, PostResponse response) throws Exception { + // given + given(service.updatePost(any(Long.class), any(Long.class), any(PostUpdateRequest.class))) + .willReturn(response); + + // when + mockMvc.perform(RestDocumentationRequestBuilders.patch("/posts/{postId}/user/{userId}", 1L, 1L) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(dto))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.title").value(dto.title())) + .andDo(document("post-update", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("postId").description("게시글 아이디"), + parameterWithName("userId").description("유저 아이디") + ), + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("게시물 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시물 내용")), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시물 ID"), + fieldWithPath("title").type(JsonFieldType.STRING).description("게시물 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시물 내용"), + fieldWithPath("name").type(JsonFieldType.STRING).description("작성자 이름"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시물 작성 시간"), + fieldWithPath("updatedAt").type(JsonFieldType.STRING).description("게시물 수정 시간") + ) + )); + + // then + verify(service, times(1)).updatePost(any(), any(), any()); + } + + @ParameterizedTest + @DisplayName("존재하는 게시글을 삭제하면 성공한다.") + @MethodSource("provideTestData") + void deletePostById_Dto_Delete() throws Exception { + // given + doNothing().when(service) + .deletePostById(any(Long.class), any(Long.class)); + + // when + mockMvc.perform(RestDocumentationRequestBuilders.delete("/posts/{postId}/user/{userId}", 1L, 1L)) + .andExpect(status().isOk()) + .andDo(document("post - delete", + pathParameters( + parameterWithName("postId").description("게시글 아이디"), + parameterWithName("userId").description("회원 아이디") + ) + )); + + // then + verify(service, times(1)).deletePostById(any(), any()); + } + + @ParameterizedTest + @DisplayName("존재하는 게시글을 조회하면 성공한다.") + @MethodSource("provideTestData") + void getPostById_id_ReturnResponse(PostCreateDto dto, PostResponse response) throws Exception { + // given + given(service.findPostById(any(Long.class))).willReturn(response); + + // when + mockMvc.perform(RestDocumentationRequestBuilders.get("/posts/{postId}", 1L) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.title").value(dto.title())) + .andDo(print()) + .andDo(document("post-get", + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("postId").description("게시글 아이디") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("게시글 아이디"), + fieldWithPath("title").type(JsonFieldType.STRING).description("게시글 제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("게시글 본문"), + fieldWithPath("name").type(JsonFieldType.STRING).description("게시글 작성자 이름"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("게시글 작성일"), + fieldWithPath("updatedAt").type(JsonFieldType.STRING).description("게시글 갱신일") + ) + )); + + // then + verify(service, times(1)).findPostById(any()); + } + + @ParameterizedTest + @DisplayName("모든 게시글을 조회하면 성공한다.") + @MethodSource("provideTestData") + void getAllPosts_Void_ReturnResponses() throws Exception { + // given + given(service.findAllPosts(pageable)).willReturn(responses); + + // when + mockMvc.perform(get("/posts") + .param("page", "0") + .param("size", "5")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.postResponses.content", Matchers.hasSize(2))) + .andDo(print()) + .andDo(document("posts - get", preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath("postResponses").type(JsonFieldType.OBJECT).description("게시물 응답"), + fieldWithPath("postResponses.content").type(JsonFieldType.ARRAY).description("게시물 정보 배열"), + fieldWithPath("postResponses.content[].id").type(JsonFieldType.NUMBER).description("게시물 ID"), + fieldWithPath("postResponses.content[].title").type(JsonFieldType.STRING).description("게시물 제목"), + fieldWithPath("postResponses.content[].content").type(JsonFieldType.STRING).description("게시물 내용"), + fieldWithPath("postResponses.content[].name").type(JsonFieldType.STRING).description("게시물 작성자 이름"), + fieldWithPath("postResponses.content[].createdAt").type(JsonFieldType.STRING).description("게시물 생성일"), + fieldWithPath("postResponses.content[].updatedAt").type(JsonFieldType.STRING).description("게시물 갱신일"), + + fieldWithPath("postResponses.pageable").type(JsonFieldType.OBJECT).description("pageable").ignored(), + fieldWithPath("postResponses.last").type(JsonFieldType.BOOLEAN).description("last").ignored(), + fieldWithPath("postResponses.totalElements").type(JsonFieldType.NUMBER).description("totalElements"), + fieldWithPath("postResponses.totalPages").type(JsonFieldType.NUMBER).description("totalPages"), + fieldWithPath("postResponses.size").type(JsonFieldType.NUMBER).description("size").ignored(), + fieldWithPath("postResponses.number").type(JsonFieldType.NUMBER).description("number").ignored(), + fieldWithPath("postResponses.sort.empty").type(JsonFieldType.BOOLEAN).description("sort.empty").ignored(), + fieldWithPath("postResponses.sort.sorted").type(JsonFieldType.BOOLEAN).description("sort.sorted").ignored(), + fieldWithPath("postResponses.sort.unsorted").type(JsonFieldType.BOOLEAN).description("sort.unsorted").ignored(), + fieldWithPath("postResponses.first").type(JsonFieldType.BOOLEAN).description("first").ignored(), + fieldWithPath("postResponses.numberOfElements").type(JsonFieldType.NUMBER).description("numberOfElements").ignored(), + fieldWithPath("postResponses.empty").type(JsonFieldType.BOOLEAN).description("empty").ignored() + ))); + + // then + verify(service, times(1)).findAllPosts(pageable); + } + + @ParameterizedTest + @DisplayName("존재하는 게시글을 유저로 조회하면 성공한다.") + @MethodSource("provideTestData") + void getPostsByUserId_id_ReturnResponse(PostCreateDto dto, PostResponse response) throws Exception { + // given + given(service.findPostsByUserId(any(), any())).willReturn(responses); + + // when + mockMvc.perform(get("/posts") + .param("page", "0") + .param("size", "5") + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.postResponses.content", Matchers.hasSize(2))) + .andDo(print()) + .andDo(document("post-get-by-user", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("page").description("페이지"), + parameterWithName("size").description("사이즈"), + parameterWithName("userId").description("회원ID") + ), + responseFields( + fieldWithPath("postResponses").type(JsonFieldType.OBJECT).description("게시글 응답"), + fieldWithPath("postResponses.content").type(JsonFieldType.ARRAY).description("게시글 정보 배열"), + fieldWithPath("postResponses.content[].id").type(JsonFieldType.NUMBER).description("게시글 아이디"), + fieldWithPath("postResponses.content[].title").type(JsonFieldType.STRING).description("게시글 이름"), + fieldWithPath("postResponses.content[].content").type(JsonFieldType.STRING).description("게시글 나이"), + fieldWithPath("postResponses.content[].name").type(JsonFieldType.STRING).description("게시글 작성자 이름"), + fieldWithPath("postResponses.content[].createdAt").type(JsonFieldType.STRING).description("게시글 생성일"), + fieldWithPath("postResponses.content[].updatedAt").type(JsonFieldType.STRING).description("게시글 갱신일"), + + fieldWithPath("postResponses.pageable").type(JsonFieldType.OBJECT).description("pageable").ignored(), + fieldWithPath("postResponses.last").type(JsonFieldType.BOOLEAN).description("last").ignored(), + fieldWithPath("postResponses.totalElements").type(JsonFieldType.NUMBER).description("totalElements"), + fieldWithPath("postResponses.totalPages").type(JsonFieldType.NUMBER).description("totalPages"), + fieldWithPath("postResponses.size").type(JsonFieldType.NUMBER).description("size").ignored(), + fieldWithPath("postResponses.number").type(JsonFieldType.NUMBER).description("number").ignored(), + fieldWithPath("postResponses.sort.empty").type(JsonFieldType.BOOLEAN).description("sort.empty").ignored(), + fieldWithPath("postResponses.sort.sorted").type(JsonFieldType.BOOLEAN).description("sort.sorted").ignored(), + fieldWithPath("postResponses.sort.unsorted").type(JsonFieldType.BOOLEAN).description("sort.unsorted").ignored(), + fieldWithPath("postResponses.first").type(JsonFieldType.BOOLEAN).description("first").ignored(), + fieldWithPath("postResponses.numberOfElements").type(JsonFieldType.NUMBER).description("numberOfElements").ignored(), + fieldWithPath("postResponses.empty").type(JsonFieldType.BOOLEAN).description("empty").ignored() + ) + )); + + // then + verify(service, times(1)).findPostsByUserId(any(), any()); + } + + static User user = User.builder() + .name(new Name("둘리")) + .age(new Age(1200)) + .hobby("고길동 등골 빼먹기") + .build(); + + static List postCreateDto = List.of( + new PostCreateDto("subject1", "content1"), + new PostCreateDto("subject2", "content2") + ); + + static List postResponses = List.of( + new PostResponse(1L, "subject1", "content1", user.getName().getNameValue(), LocalDateTime.now(), LocalDateTime.now()), + new PostResponse(2L, "subject2", "content2", user.getName().getNameValue(), LocalDateTime.now(), LocalDateTime.now()) + ); + + static Pageable pageable = PageRequest.of(0, 5); + + static PostResponses responses = new PostResponses( + new PageImpl<>(postResponses) + ); + + static Stream provideTestData() { + return IntStream.range(0, 2) + .mapToObj(i -> Arguments.of(postCreateDto.get(i), postResponses.get(i))); + } +} diff --git a/src/test/java/com/blackdog/springbootBoardJpa/post/service/PostServiceTest.java b/src/test/java/com/blackdog/springbootBoardJpa/post/service/PostServiceTest.java new file mode 100644 index 000000000..64c97a7b8 --- /dev/null +++ b/src/test/java/com/blackdog/springbootBoardJpa/post/service/PostServiceTest.java @@ -0,0 +1,184 @@ +package com.blackdog.springbootBoardJpa.post.service; + +import com.blackdog.springbootBoardJpa.domain.post.service.PostService; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostCreateRequest; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponse; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostResponses; +import com.blackdog.springbootBoardJpa.domain.post.service.dto.PostUpdateRequest; +import com.blackdog.springbootBoardJpa.domain.user.model.User; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Age; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Name; +import com.blackdog.springbootBoardJpa.domain.user.repository.UserRepository; +import com.blackdog.springbootBoardJpa.global.exception.PostNotFoundException; +import com.blackdog.springbootBoardJpa.global.exception.UserNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; + +@SpringBootTest +@Transactional +class PostServiceTest { + + @Autowired + PostService postService; + + @Autowired + UserRepository userRepository; + + static User savedUser; + + @BeforeEach + void setup() { + User user = User.builder() + .name(new Name("홍길동")) + .age(new Age(1200)) + .hobby("동에번쩍하는거") + .build(); + savedUser = userRepository.save(user); + } + + @ParameterizedTest + @DisplayName("존재하는 유저로 게시글을 생성하면 성공한다.") + @ArgumentsSource(value = PostCreateRequestDataProvider.class) + void savePost_Dto_SaveReturnResponse(PostCreateRequest request) { + // when + PostResponse savedPost = postService.savePost(savedUser.getId(), request); + + // then + PostResponse result = postService.findPostById(savedPost.id()); + assertThat(result.title()).isEqualTo(savedPost.title()); + } + + @ParameterizedTest + @DisplayName("존재하지 않는 유저로 게시글을 생성하면 실패한다.") + @ArgumentsSource(value = PostCreateRequestDataProvider.class) + void savePost_Dto_Exception(PostCreateRequest request) { + // given + Long userId = Long.MAX_VALUE; + + // when + Exception exception = catchException(() -> postService.savePost(userId, request)); + + // then + assertThat(exception).isInstanceOf(UserNotFoundException.class); + } + + @ParameterizedTest + @DisplayName("존재하는 게시글을 수정하면 성공한다.") + @ArgumentsSource(value = PostCreateRequestDataProvider.class) + void updatePost_Dto_UpdateReturnResponse(PostCreateRequest request) { + // given + PostResponse savedPost = postService.savePost(savedUser.getId(), request); + PostUpdateRequest updateRequest = new PostUpdateRequest("수정제목", "수정본문"); + + // when + PostResponse updatePost = postService.updatePost(savedUser.getId(), savedPost.id(), updateRequest); + + // then + PostResponse result = postService.findPostById(updatePost.id()); + assertThat(result.title()).isEqualTo(updatePost.title()); + } + + @ParameterizedTest + @DisplayName("존재하는 게시글을 삭제하면 성공한다.") + @ArgumentsSource(value = PostCreateRequestDataProvider.class) + void deletePostById_Dto_Delete(PostCreateRequest request) { + // given + PostResponse savedPost = postService.savePost(savedUser.getId(), request); + + // when + postService.deletePostById(savedUser.getId(), savedPost.id()); + + // then + assertThatThrownBy(() -> postService.findPostById(savedPost.id())) + .isInstanceOf(PostNotFoundException.class); + } + + @ParameterizedTest + @DisplayName("모든 게시글을 조회하면 성공한다.") + @ArgumentsSource(value = PostCreateRequestDataProvider.class) + void findAllPosts_Void_ReturnResponses(PostCreateRequest request) { + // given + postService.savePost(savedUser.getId(), request); + Pageable pageable = PageRequest.of(0, 10); + + // when + PostResponses result = postService.findAllPosts(pageable); + + // then + assertThat(result).isNotNull(); + assertThat(result.postResponses()).isNotEmpty(); + } + + @ParameterizedTest + @DisplayName("존재하는 게시글을 조회하면 성공한다.") + @ArgumentsSource(value = PostCreateRequestDataProvider.class) + void findPostById_id_ReturnResponse(PostCreateRequest request) { + // given + PostResponse savedPost = postService.savePost(savedUser.getId(), request); + + // when + PostResponse result = postService.findPostById(savedPost.id()); + + // then + assertThat(result.title()).isEqualTo(savedPost.title()); + } + + @ParameterizedTest + @DisplayName("존재하지 않는 게시글을 조회하면 실패한다.") + @ArgumentsSource(value = PostCreateRequestDataProvider.class) + void findPostById_id_Exception(PostCreateRequest request) { + + // when + Exception exception = catchException(() -> postService.findPostById(100L)); + + // then + assertThat(exception).isInstanceOf(PostNotFoundException.class); + } + + @ParameterizedTest + @DisplayName("존재하는 게시글을 조회하면 성공한다.") + @ArgumentsSource(value = PostCreateRequestDataProvider.class) + void findPostsByUserId_id_ReturnResponse(PostCreateRequest request) { + // given + PostResponse savedPost = postService.savePost(savedUser.getId(), request); + Pageable pageable = PageRequest.of(0, 10); + + // when + PostResponses result = postService.findPostsByUserId(savedUser.getId(), pageable); + + // then + assertThat(result).isNotNull(); + assertThat(result.postResponses()).isNotEmpty(); + } + + static class PostCreateRequestDataProvider implements ArgumentsProvider { + + static List postCreateRequests = List.of( + new PostCreateRequest("subject1", "content1"), + new PostCreateRequest("subject2", "content2") + ); + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return postCreateRequests.stream() + .map(Arguments::of); + } + + } + +} diff --git a/src/test/java/com/blackdog/springbootBoardJpa/user/controller/UserControllerTest.java b/src/test/java/com/blackdog/springbootBoardJpa/user/controller/UserControllerTest.java new file mode 100644 index 000000000..7ceb9fcce --- /dev/null +++ b/src/test/java/com/blackdog/springbootBoardJpa/user/controller/UserControllerTest.java @@ -0,0 +1,201 @@ +package com.blackdog.springbootBoardJpa.user.controller; + +import com.blackdog.springbootBoardJpa.domain.user.controller.UserController; +import com.blackdog.springbootBoardJpa.domain.user.controller.converter.UserControllerConverter; +import com.blackdog.springbootBoardJpa.domain.user.controller.dto.UserCreateDto; +import com.blackdog.springbootBoardJpa.domain.user.service.UserService; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponse; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponses; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureRestDocs +@WebMvcTest(UserController.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserControllerConverter controllerConverter; + + @MockBean + private UserService userService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("유저를 생성한다.") + void saveUser_Dto_ReturnResponse() throws Exception { + //given + UserCreateDto createDto = new UserCreateDto("Kim", 23, "축구"); + UserResponse response = new UserResponse(1L, "Kim", 23, "축구", LocalDateTime.now(), LocalDateTime.now()); + given(userService.saveUser(any())).willReturn(response); + + //when & then + mockMvc.perform(post("/users") + .accept(MediaType.APPLICATION_JSON_VALUE) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(createDto))) + .andExpect(status().isCreated()) + .andDo(print()) + .andDo(document("user-save", + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("회원 이름"), + fieldWithPath("age").type(JsonFieldType.NUMBER).description("나이"), + fieldWithPath("hobby").type(JsonFieldType.STRING).description("취미")), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("회원 ID"), + fieldWithPath("name").type(JsonFieldType.STRING).description("회원 이름"), + fieldWithPath("age").type(JsonFieldType.NUMBER).description("나이"), + fieldWithPath("hobby").type(JsonFieldType.STRING).description("취미"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("회원 가입 시간"), + fieldWithPath("updatedAt").type(JsonFieldType.STRING).description("회원 수정 시간") + ) + )); + } + + @Test + @DisplayName("유효하지 않은 유저는 생성 못한다.") + void saveUser_Dto_ReturnFailResponse() throws Exception { + //given + UserCreateDto createDto = new UserCreateDto("", -1, ""); + UserResponse response = new UserResponse(1L, "", -1, "", LocalDateTime.now(), LocalDateTime.now()); + given(userService.saveUser(any())).willReturn(response); + + //when & then + mockMvc.perform(post("/users") + .accept(MediaType.APPLICATION_JSON_VALUE) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(createDto))) + .andExpect(status().is4xxClientError()) + .andDo(print()); + } + + @Test + @DisplayName("유저를 삭제한다.") + void deleteUser_Id_ReturnMessage() throws Exception { + //given + doNothing().when(userService).deleteUserById(anyLong()); + + //when & then + mockMvc.perform(RestDocumentationRequestBuilders.delete("/users/{userId}", 1L)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document("user-delete", + pathParameters( + parameterWithName("userId").description("회원 ID") + ))); + } + + @Test + @DisplayName("유저를 단건 조회한다.") + void getUser_Id_ReturnResponse() throws Exception { + //given + UserResponse response = new UserResponse(1L, "Park", 26, "여행", LocalDateTime.now(), LocalDateTime.now()); + given(userService.findUserById(anyLong())).willReturn(response); + + // when + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/{userId}", 1L) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document("user-get", + preprocessResponse(prettyPrint()), + pathParameters( + parameterWithName("userId").description("회원 ID") + ), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("유저 아이디"), + fieldWithPath("name").type(JsonFieldType.STRING).description("유저 이름"), + fieldWithPath("age").type(JsonFieldType.NUMBER).description("유저 나이"), + fieldWithPath("hobby").type(JsonFieldType.STRING).description("유저 취미"), + fieldWithPath("createdAt").type(JsonFieldType.STRING).description("유저 생성일"), + fieldWithPath("updatedAt").type(JsonFieldType.STRING).description("유저 갱신일") + ) + )); + + // then + verify(userService, times(1)).findUserById(anyLong()); + } + + @Test + @DisplayName("유저를 전체 조회한다.") + void getAllUsers_Pageable_ReturnResponses() throws Exception { + //given + UserResponse response1 = new UserResponse(1L, "Park", 26, "여행", LocalDateTime.now(), LocalDateTime.now()); + UserResponse response2 = new UserResponse(2L, "Park", 26, "여행", LocalDateTime.now(), LocalDateTime.now()); + UserResponses responses = new UserResponses(new PageImpl<>(List.of(response1, response2))); + given(userService.findAllUsers(PageRequest.of(0, 5))).willReturn(responses); + + //when & then + mockMvc.perform(get("/users") + .param("page", "0") + .param("size", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userResponses.content", Matchers.hasSize(2))) + .andDo(print()) + .andDo(document("users-get", + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("page").description("페이지"), + parameterWithName("size").description("사이즈") + ), + responseFields( + fieldWithPath("userResponses").type(JsonFieldType.OBJECT).description("유저 응답"), + fieldWithPath("userResponses.content[]").type(JsonFieldType.ARRAY).description("유저 정보 배열"), + fieldWithPath("userResponses.content[].id").type(JsonFieldType.NUMBER).description("유저 아이디"), + fieldWithPath("userResponses.content[].name").type(JsonFieldType.STRING).description("유저 이름"), + fieldWithPath("userResponses.content[].age").type(JsonFieldType.NUMBER).description("유저 나이"), + fieldWithPath("userResponses.content[].hobby").type(JsonFieldType.STRING).description("유저 취미"), + fieldWithPath("userResponses.content[].createdAt").type(JsonFieldType.STRING).description("유저 생성일"), + fieldWithPath("userResponses.content[].updatedAt").type(JsonFieldType.STRING).description("유저 갱신일"), + fieldWithPath("userResponses.pageable").type(JsonFieldType.OBJECT).description("pageable").ignored(), + fieldWithPath("userResponses.last").type(JsonFieldType.BOOLEAN).description("last").ignored(), + fieldWithPath("userResponses.totalElements").type(JsonFieldType.NUMBER).description("totalElements"), + fieldWithPath("userResponses.totalPages").type(JsonFieldType.NUMBER).description("totalPages"), + fieldWithPath("userResponses.size").type(JsonFieldType.NUMBER).description("size").ignored(), + fieldWithPath("userResponses.number").type(JsonFieldType.NUMBER).description("number").ignored(), + fieldWithPath("userResponses.sort.empty").type(JsonFieldType.BOOLEAN).description("sort.empty").ignored(), + fieldWithPath("userResponses.sort.sorted").type(JsonFieldType.BOOLEAN).description("sort.sorted").ignored(), + fieldWithPath("userResponses.sort.unsorted").type(JsonFieldType.BOOLEAN).description("sort.unsorted").ignored(), + fieldWithPath("userResponses.first").type(JsonFieldType.BOOLEAN).description("first").ignored(), + fieldWithPath("userResponses.numberOfElements").type(JsonFieldType.NUMBER).description("numberOfElements").ignored(), + fieldWithPath("userResponses.empty").type(JsonFieldType.BOOLEAN).description("empty").ignored() + ) + )); + } + +} diff --git a/src/test/java/com/blackdog/springbootBoardJpa/user/service/UserServiceTest.java b/src/test/java/com/blackdog/springbootBoardJpa/user/service/UserServiceTest.java new file mode 100644 index 000000000..eb7fd6286 --- /dev/null +++ b/src/test/java/com/blackdog/springbootBoardJpa/user/service/UserServiceTest.java @@ -0,0 +1,142 @@ +package com.blackdog.springbootBoardJpa.user.service; + +import com.blackdog.springbootBoardJpa.domain.user.model.User; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Age; +import com.blackdog.springbootBoardJpa.domain.user.model.vo.Name; +import com.blackdog.springbootBoardJpa.domain.user.repository.UserRepository; +import com.blackdog.springbootBoardJpa.domain.user.service.UserService; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserCreateRequest; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponse; +import com.blackdog.springbootBoardJpa.domain.user.service.dto.UserResponses; +import com.blackdog.springbootBoardJpa.global.exception.UserNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static com.blackdog.springbootBoardJpa.global.response.ErrorCode.NOT_FOUND_USER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +public class UserServiceTest { + + @Autowired + private UserService userService; + + @Autowired + private UserRepository userRepository; + + private User user; + + @BeforeEach + void setUp() { + user = User.builder() + .name(new Name("park")) + .age(new Age(12)) + .hobby("야구") + .build(); + } + + @Test + @DisplayName("유저를 생성한다.") + void saveUser_Dto_ReturnUserResponse() { + //given + UserCreateRequest request = new UserCreateRequest( + new Name("park"), + new Age(26), + "축구" + ); + + //when + UserResponse response = userService.saveUser(request); + + //then + User savedUser = userRepository.findById(response.id()).get(); + assertThat(savedUser.getId()).isEqualTo(savedUser.getId()); + } + + @Test + @DisplayName("ID를 이용해 유저를 삭제한다.") + void deleteUserById_Id_Success() { + //given + User savedUser = userRepository.save(user); + + //when + userService.deleteUserById(savedUser.getId()); + + //then + Optional optionalUser = userRepository.findById(savedUser.getId()); + assertThat(optionalUser).isEmpty(); + } + + @Test + @DisplayName("ID를 이용하여 특정 유저를 조회한다.") + void findUserById_Id_ReturnUserResponse() { + //given + User savedUser = userRepository.save(user); + + //when + UserResponse userResponse = userService.findUserById(savedUser.getId()); + + //then + assertThat(userResponse).isNotNull(); + assertThat(userResponse.age()).isEqualTo(savedUser.getAge().getAgeValue()); + assertThat(userResponse.name()).isEqualTo(savedUser.getName().getNameValue()); + assertThat(userResponse.hobby()).isEqualTo(savedUser.getHobby()); + } + + @Test + @DisplayName("유효하지 않은 Id로 단건 조회시 조회에 실패한다.") + void findUserById_Id_ThrowUserNotFoundException() { + //given + User savedUser = userRepository.save(user); + + //when & then + assertThatThrownBy(() -> userService.findUserById(1000L)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage(NOT_FOUND_USER.getMessage()); + } + + + @ParameterizedTest + @DisplayName("페이징을 이용하여 유저를 전체 조회한다.") + @MethodSource("user_Data") + void findAllUsers_Pageable_UserResponses(List users) { + //given + users.forEach(user -> userRepository.save(user)); + + //when + UserResponses responses = userService.findAllUsers(PageRequest.of(0, 2)); + + //then + assertThat(responses).isNotNull(); + assertThat(responses.userResponses()).isNotEmpty(); + assertThat(responses.userResponses().getSize()).isEqualTo(2); + } + + private static Stream> user_Data() { + User user1 = User.builder() + .name(new Name("Park")) + .age(new Age(12)) + .hobby("야구") + .build(); + User user2 = User.builder() + .name(new Name("Kim")) + .age(new Age(21)) + .hobby("농구") + .build(); + return Stream.of(List.of(user1, user2)); + } + +}