diff --git a/hehe-board/.gitignore b/hehe-board/.gitignore new file mode 100644 index 000000000..c2065bc26 --- /dev/null +++ b/hehe-board/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/hehe-board/build.gradle b/hehe-board/build.gradle new file mode 100644 index 000000000..3d9372f5a --- /dev/null +++ b/hehe-board/build.gradle @@ -0,0 +1,59 @@ +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.programmers' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +asciidoctor { + dependsOn test +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +bootJar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}") { + into 'static/docs' + } +} + +repositories { + mavenCentral() +} + +ext { + set('snippetsDir', file("build/generated-snippets")) +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'jakarta.validation:jakarta.validation-api:3.0.2' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' +} + +tasks.named('test') { + outputs.dir snippetsDir + useJUnitPlatform() +} + +tasks.named('asciidoctor') { + inputs.dir snippetsDir + dependsOn test +} \ No newline at end of file diff --git a/hehe-board/gradle/wrapper/gradle-wrapper.jar b/hehe-board/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..033e24c4c Binary files /dev/null and b/hehe-board/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hehe-board/gradle/wrapper/gradle-wrapper.properties b/hehe-board/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..9f4197d5f --- /dev/null +++ b/hehe-board/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hehe-board/gradlew b/hehe-board/gradlew new file mode 100644 index 000000000..fcb6fca14 --- /dev/null +++ b/hehe-board/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hehe-board/gradlew.bat b/hehe-board/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/hehe-board/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hehe-board/settings.gradle b/hehe-board/settings.gradle new file mode 100644 index 000000000..dc35c29a7 --- /dev/null +++ b/hehe-board/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'hehe-board' diff --git a/hehe-board/src/docs/asciidoc/index.adoc b/hehe-board/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..6e7cb2e8d --- /dev/null +++ b/hehe-board/src/docs/asciidoc/index.adoc @@ -0,0 +1,82 @@ += Spring REST Docs 가이드 +:doctype: book +:icons: font +:source-highlighter: coderay +:toc: left +:toc-title: 목차 +:toclevels: 3 +:sectlinks: +:sectnums: + +== 개요 +이 API문서는 'Spring REST Docs 가이드' 프로젝트의 산출물입니다. + + +=== API 서버 경로 +[cols="2,5,3"] +|==== +|환경 |DNS |비고 +|개발(dev) | link:[] |API 문서 제공 +|베타(beta) | link:[] |API 문서 제공 +|운영(prod) | link:[] |API 문서 미제공 +|==== + +[NOTE] +==== +해당 프로젝트 API문서는 개발환경까지 노출되는 것을 권장합니다. + +==== + +[CAUTION] +==== +운영환경에 노출될 경우 보안 관련 문제가 발생할 수 있습니다. +==== + +=== 응답형식 +프로젝트는 다음과 같은 응답형식을 제공합니다. + +==== 정상(200, OK) + +|==== +|응답데이터가 없는 경우|응답데이터가 있는 경우 + +a|[source,json] +---- +{ + "code": "0000", // 정상인 경우 '0000' + "data": null +} +---- + +a|[source,json] +---- +{ + "code": "0000", // 정상인 경우 '0000' + "data": { + "name": "honeymon-enterprise" + } +} +---- +|==== + +==== 상태코드(HttpStatus) +응답시 다음과 같은 응답상태 헤더, 응답코드 및 응답메시지를 제공합니다. + +[cols="3,1,3,3"] +|==== +|HttpStatus |코드 |메시지 |설명 + +|`OK(200)` |`0000` |"OK" |정상 응답 +|`INTERNAL_SERVER_ERROR(500)`|`S5XX` |"알 수 없는 에러가 발생했습니다. 관리자에게 문의하세요." |서버 내부 오류 +|`FORBIDDEN(403)`|`C403` |"[AccessDenied] 잘못된 접근입니다." |비인가 접속입니다. +|`BAD_REQUEST(400)`|`C400` |"잘못된 요청입니다. 요청내용을 확인하세요." |요청값 누락 혹은 잘못된 기입 +|`NOT_FOUND(404)`|`C404` |"상황에 따라 다름" |요청값 누락 혹은 잘못된 기입 + +|==== + +== API + +// 유저 +include::user.adoc[] + +// 게시글 +include::post.adoc[] \ No newline at end of file diff --git a/hehe-board/src/docs/asciidoc/post.adoc b/hehe-board/src/docs/asciidoc/post.adoc new file mode 100644 index 000000000..b8a1340bb --- /dev/null +++ b/hehe-board/src/docs/asciidoc/post.adoc @@ -0,0 +1,66 @@ +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + +== 게시글(Post) +Post 에 대한 등록 기능을 제공합니다. + +=== 게시글 등록 +게시글을 새롭게 등록합다. + +|==== +|속성 |설명 + +|`title` |게시글 제목(문자열 입력) +|`content` |게시글 내용(문자열 입력) + +|==== + +=== 생성 + +[discrete] +==== 요청 +include::{snippets}/post-save/curl-request.adoc[] +include::{snippets}/post-save/httpie-request.adoc[] +include::{snippets}/post-save/http-request.adoc[] +include::{snippets}/post-save/request-fields.adoc[] + +[discrete] +==== 응답 +include::{snippets}/post-save/http-response.adoc[] +include::{snippets}/post-save/response-fields.adoc[] + +=== 단일 조회 +==== 요청 +include::{snippets}/post-find/curl-request.adoc[] +include::{snippets}/post-find/httpie-request.adoc[] +include::{snippets}/post-find/http-request.adoc[] + +[discrete] +==== 응답 +include::{snippets}/post-find/http-response.adoc[] +include::{snippets}/post-find/response-fields.adoc[] + +=== 리스트 조회 +==== 요청 +include::{snippets}/post-get/curl-request.adoc[] +include::{snippets}/post-get/httpie-request.adoc[] +include::{snippets}/post-get/http-request.adoc[] + +[discrete] +==== 응답 +include::{snippets}/post-get/http-response.adoc[] +include::{snippets}/post-get/response-fields.adoc[] + +=== 수정 +==== 요청 +include::{snippets}/post-update/curl-request.adoc[] +include::{snippets}/post-update/httpie-request.adoc[] +include::{snippets}/post-update/http-request.adoc[] +include::{snippets}/post-update/request-fields.adoc[] + + +[discrete] +==== 응답 +include::{snippets}/post-update/http-response.adoc[] +include::{snippets}/post-update/response-fields.adoc[] \ No newline at end of file diff --git a/hehe-board/src/docs/asciidoc/user.adoc b/hehe-board/src/docs/asciidoc/user.adoc new file mode 100644 index 000000000..2e7b53a8a --- /dev/null +++ b/hehe-board/src/docs/asciidoc/user.adoc @@ -0,0 +1,31 @@ +ifndef::snippets[] +:snippets: ./build/generated-snippets +endif::[] + +== 유저(User) +User 에 대한 등록 기능을 제공합니다. + +=== 유저 등록 +유저를 새롭게 등록합다. + +|==== +|속성 |설명 + +|`name` |사용자 이름(문자열 입력) +|`age` |사용자 나이(최소: 1, 최대: 120) +|`hobby` |사용자 취미(문자열 입력) + +|==== + + +[discrete] +==== 요청 +include::{snippets}/member-save/curl-request.adoc[] +include::{snippets}/member-save/httpie-request.adoc[] +include::{snippets}/member-save/http-request.adoc[] +include::{snippets}/member-save/request-fields.adoc[] + +[discrete] +==== 응답 +include::{snippets}/member-save/http-response.adoc[] +include::{snippets}/member-save/response-fields.adoc[] \ No newline at end of file diff --git a/hehe-board/src/main/java/com/programmers/heheboard/HeheBoardApplication.java b/hehe-board/src/main/java/com/programmers/heheboard/HeheBoardApplication.java new file mode 100644 index 000000000..134f92efc --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/HeheBoardApplication.java @@ -0,0 +1,13 @@ +package com.programmers.heheboard; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HeheBoardApplication { + + public static void main(String[] args) { + SpringApplication.run(HeheBoardApplication.class, args); + } + +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/post/CreatePostRequestDto.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/CreatePostRequestDto.java new file mode 100644 index 000000000..9be3e8f60 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/CreatePostRequestDto.java @@ -0,0 +1,40 @@ +package com.programmers.heheboard.domain.post; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public final class CreatePostRequestDto { + + @NotBlank + private final String title; + + @NotBlank + private final String content; + + private final Long userId; + + public CreatePostRequestDto(String title, String content, Long userId) { + this.title = title; + this.content = content; + this.userId = userId; + } + + public Post toEntity() { + return Post.builder() + .title(title) + .content(content) + .build(); + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public Long getUserId() { + return userId; + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/post/Post.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/Post.java new file mode 100644 index 000000000..bb1d586db --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/Post.java @@ -0,0 +1,92 @@ +package com.programmers.heheboard.domain.post; + +import java.util.Objects; +import java.util.regex.Pattern; + +import com.programmers.heheboard.domain.user.User; +import com.programmers.heheboard.global.BaseEntity; +import com.programmers.heheboard.global.codes.ErrorCode; +import com.programmers.heheboard.global.exception.GlobalRuntimeException; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "posts") +@Entity +@Getter +@NoArgsConstructor +public class Post extends BaseEntity { + private static final int TITLE_MAX_LEN = 30; + private static final int TITLE_MIN_LEN = 1; + private static final int CONTENT_MAX_LEN = 5000; + + private static final String NOT_VALID_TITLE_REG_EXP = "//gm"; + private static final String NOT_VALID_CONTENT_REG_EXP = "//gm"; + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + @Column(nullable = false) + private String title; + @Column(columnDefinition = "TEXT", length = 5000) + private String content; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Builder + public Post(String title, String content) { + validateTitle(title); + validateContent(content); + this.title = title; + this.content = content; + } + + private void validateTitle(String title) { + if (title.length() < TITLE_MIN_LEN || + title.length() > TITLE_MAX_LEN || + Pattern.matches(NOT_VALID_TITLE_REG_EXP, title)) { + throw new GlobalRuntimeException(ErrorCode.POST_TITLE_VALIDATION_FAIL); + } + } + + private void validateContent(String content) { + if (content.length() > CONTENT_MAX_LEN || + Pattern.matches(NOT_VALID_CONTENT_REG_EXP, content)) { + throw new GlobalRuntimeException(ErrorCode.POST_CONTENT_VALIDATION_FAIL); + } + } + + public void attachUser(User user) { + if (Objects.nonNull(this.user)) { + this.user.removePost(this); + } + + this.user = user; + } + + public void updatePost(String newTitle, String newContent) { + changeTitle(newTitle); + changeContents(newContent); + } + + private void changeTitle(String newTitle) { + validateTitle(newTitle); + this.title = newTitle; + } + + private void changeContents(String newContent) { + validateContent(newContent); + this.content = newContent; + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostController.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostController.java new file mode 100644 index 000000000..60c5e37d5 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostController.java @@ -0,0 +1,56 @@ +package com.programmers.heheboard.domain.post; + +import org.springframework.data.domain.Slice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.programmers.heheboard.global.response.ApiResponse; +import com.programmers.heheboard.global.response.ApiSliceResponse; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "/posts") +public class PostController { + + private final PostService postService; + + @ExceptionHandler(Exception.class) + public ApiResponse internalServerErrorHandler(Exception e) { + return ApiResponse.fail(500, e.getMessage()); + } + + @PostMapping + public ApiResponse save(@Valid @RequestBody CreatePostRequestDto createPostRequestDto) { + PostResponseDto postDto = postService.createPost(createPostRequestDto); + return ApiResponse.ok(postDto); + } + + @PutMapping("/{post-id}") + public ApiResponse updatePost(@Valid @RequestBody UpdatePostRequestDto updatePostRequestDto, + @PathVariable("post-id") Long postId) { + PostResponseDto postResponseDTO = postService.updatePost(postId, updatePostRequestDto); + return ApiResponse.ok(postResponseDTO); + } + + @GetMapping("/{post-id}") + public ApiResponse getOnePost(@PathVariable("post-id") Long postId) { + PostResponseDto postDto = postService.findPost(postId); + return ApiResponse.ok(postDto); + } + + @GetMapping + public ApiResponse getPostBySlice(@RequestParam int page, @RequestParam int size) { + Slice postSliceResponseDtos = postService.getPosts(page, size); + return ApiSliceResponse.ok(postSliceResponseDtos); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostRepository.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostRepository.java new file mode 100644 index 000000000..c33a37d17 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostRepository.java @@ -0,0 +1,11 @@ +package com.programmers.heheboard.domain.post; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { + + Slice findSliceBy(Pageable pageable); + +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostResponseDto.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostResponseDto.java new file mode 100644 index 000000000..ad8cca7ac --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostResponseDto.java @@ -0,0 +1,17 @@ +package com.programmers.heheboard.domain.post; + +import java.time.LocalDateTime; + +import lombok.Builder; + +@Builder +public record PostResponseDto(String title, String content, LocalDateTime createdAt, LocalDateTime modifiedAt) { + public static PostResponseDto toResponse(Post post) { + return PostResponseDto.builder() + .title(post.getTitle()) + .content(post.getContent()) + .createdAt(post.getCreatedAt()) + .modifiedAt(post.getModifiedAt()) + .build(); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostService.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostService.java new file mode 100644 index 000000000..b2e79feeb --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/PostService.java @@ -0,0 +1,60 @@ +package com.programmers.heheboard.domain.post; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.programmers.heheboard.domain.user.User; +import com.programmers.heheboard.domain.user.UserRepository; +import com.programmers.heheboard.global.codes.ErrorCode; +import com.programmers.heheboard.global.exception.GlobalRuntimeException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class PostService { + private final UserRepository userRepository; + private final PostRepository postRepository; + + @Transactional + public PostResponseDto createPost(CreatePostRequestDto createPostRequestDto) { + User user = userRepository.findById(createPostRequestDto.getUserId()) + .orElseThrow(() -> new GlobalRuntimeException(ErrorCode.USER_NOT_FOUND)); + + Post post = createPostRequestDto.toEntity(); + post.attachUser(user); + + return PostResponseDto.toResponse(postRepository.save(post)); + } + + @Transactional(readOnly = true) + public PostResponseDto findPost(Long postId) { + Post retrievedPost = postRepository.findById(postId) + .orElseThrow(() -> new GlobalRuntimeException(ErrorCode.POST_NOT_FOUND)); + + return PostResponseDto.toResponse(retrievedPost); + } + + @Transactional(readOnly = true) + public Slice getPosts(int page, int size) { + PageRequest pageRequest = PageRequest.of(page, size); + + return postRepository.findSliceBy(pageRequest) + .map(PostResponseDto::toResponse); + } + + @Transactional + public PostResponseDto updatePost(Long postId, UpdatePostRequestDto updatePostRequestDto) { + Post retrievedPost = postRepository.findById(postId) + .orElseThrow(() ->new GlobalRuntimeException(ErrorCode.POST_NOT_FOUND)); + + String newTitle = updatePostRequestDto.getTitle(); + String newContent = updatePostRequestDto.getContent(); + + retrievedPost.updatePost(newTitle, newContent); + + return PostResponseDto.toResponse(retrievedPost); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/post/UpdatePostRequestDto.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/UpdatePostRequestDto.java new file mode 100644 index 000000000..532662fe5 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/post/UpdatePostRequestDto.java @@ -0,0 +1,25 @@ +package com.programmers.heheboard.domain.post; + +import jakarta.validation.constraints.NotBlank; + +public final class UpdatePostRequestDto { + @NotBlank + private final String title; + + @NotBlank + private final String content; + + public UpdatePostRequestDto(String title, String content) { + this.title = title; + this.content = content; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/user/CreateUserRequestDto.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/CreateUserRequestDto.java new file mode 100644 index 000000000..c849ae85c --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/CreateUserRequestDto.java @@ -0,0 +1,44 @@ +package com.programmers.heheboard.domain.user; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public final class CreateUserRequestDto { + + @NotBlank + private final String name; + + @Min(0) + @Max(100) + private final int age; + + @NotBlank + private final String hobby; + + public CreateUserRequestDto(String name, int age, String hobby) { + this.name = name; + this.age = age; + this.hobby = hobby; + } + + public User toEntity() { + return User.builder() + .name(name) + .age(age) + .hobby(hobby) + .build(); + } + + public String name() { + return name; + } + + public int age() { + return age; + } + + public String hobby() { + return hobby; + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/user/User.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/User.java new file mode 100644 index 000000000..6d72dff80 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/User.java @@ -0,0 +1,82 @@ +package com.programmers.heheboard.domain.user; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import com.programmers.heheboard.domain.post.Post; +import com.programmers.heheboard.global.BaseEntity; +import com.programmers.heheboard.global.codes.ErrorCode; +import com.programmers.heheboard.global.exception.GlobalRuntimeException; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor +@Table(name = "users") +public class User extends BaseEntity { + + private static final int MIN_AGE = 0; + private static final int MAX_AGE = 100; + private static final String NAME_REG_EXP = "[a-zA-Z0-9가-힣]{2,10}"; + private static final String HOBBY_REG_EXP = "[가-힣]{2,10}"; + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + @Column(length = 10, nullable = false) + private String name; + @Column(nullable = false) + private int age; + @Column(nullable = false) + private String hobby; + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + private final List posts = new ArrayList<>(); + + @Builder + public User(String name, int age, String hobby) { + validateAge(age); + validateName(name); + validateHobby(hobby); + this.name = name; + this.age = age; + this.hobby = hobby; + } + + public void addPost(Post post) { + posts.add(post); + post.attachUser(this); + } + + public void removePost(Post post) { + this.posts.remove(post); + } + + private void validateAge(int age) { + if (age < MIN_AGE || age > MAX_AGE) { + throw new GlobalRuntimeException(ErrorCode.USER_AGE_VALIDATION_FAIL); + } + } + + private void validateName(String name) { + if (!Pattern.matches(NAME_REG_EXP, name)) { + throw new GlobalRuntimeException(ErrorCode.USER_AGE_VALIDATION_FAIL); + } + } + + private void validateHobby(String hobby) { + if (!Pattern.matches(HOBBY_REG_EXP, hobby)) { + throw new GlobalRuntimeException(ErrorCode.USER_HOBBY_VALIDATION_FAIL); + } + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserController.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserController.java new file mode 100644 index 000000000..f2554c30b --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserController.java @@ -0,0 +1,23 @@ +package com.programmers.heheboard.domain.user; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.programmers.heheboard.global.response.ApiResponse; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + private final UserService userService; + + @PostMapping + public ApiResponse createUser(@Valid @RequestBody CreateUserRequestDto createUserRequestDto) { + return ApiResponse.ok(userService.createUser(createUserRequestDto)); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserRepository.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserRepository.java new file mode 100644 index 000000000..30226ad28 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserRepository.java @@ -0,0 +1,7 @@ +package com.programmers.heheboard.domain.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserResponseDto.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserResponseDto.java new file mode 100644 index 000000000..60fe5b537 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserResponseDto.java @@ -0,0 +1,19 @@ +package com.programmers.heheboard.domain.user; + +import java.time.LocalDateTime; + +import lombok.Builder; + +@Builder +public record UserResponseDto(String name, int age, String hobby, LocalDateTime createdAt, LocalDateTime modifiedAt) { + + public static UserResponseDto toResponse(User user) { + return UserResponseDto.builder() + .name(user.getName()) + .age(user.getAge()) + .hobby(user.getHobby()) + .createdAt(user.getCreatedAt()) + .modifiedAt(user.getModifiedAt()) + .build(); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserService.java b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserService.java new file mode 100644 index 000000000..b927b5cbd --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/domain/user/UserService.java @@ -0,0 +1,17 @@ +package com.programmers.heheboard.domain.user; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserService { + private final UserRepository userRepository; + + public UserResponseDto createUser(CreateUserRequestDto createUserRequestDto) { + User user = createUserRequestDto.toEntity(); + + return UserResponseDto.toResponse(userRepository.save(user)); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/BaseEntity.java b/hehe-board/src/main/java/com/programmers/heheboard/global/BaseEntity.java new file mode 100644 index 000000000..fcb0ca4e5 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/BaseEntity.java @@ -0,0 +1,22 @@ +package com.programmers.heheboard.global; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseEntity { + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime modifiedAt; +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/JpaAuditingConfiguration.java b/hehe-board/src/main/java/com/programmers/heheboard/global/JpaAuditingConfiguration.java new file mode 100644 index 000000000..cc95eb23d --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/JpaAuditingConfiguration.java @@ -0,0 +1,10 @@ +package com.programmers.heheboard.global; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@Configuration +public class JpaAuditingConfiguration { + +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/codes/ErrorCode.java b/hehe-board/src/main/java/com/programmers/heheboard/global/codes/ErrorCode.java new file mode 100644 index 000000000..83527edad --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/codes/ErrorCode.java @@ -0,0 +1,31 @@ +package com.programmers.heheboard.global.codes; + +import lombok.Getter; + +@Getter +public enum ErrorCode { + POST_CREATE_FAIL("P10001", "게시글 저장 실패", 400), + POST_NOT_FOUND("P10003", "게시글 찾기 실패", 400), + POST_TITLE_VALIDATION_FAIL("P10004", "게시글 제목 검증 실패", 400), + POST_CONTENT_VALIDATION_FAIL("P10005", "게시글 내용 검증 실패", 400), + + USER_CREATE_FAIL("U10001", "사용자 저장 실패", 400), + USER_NOT_FOUND("U10003", "사용자 찾기 실패", 400), + USER_AGE_VALIDATION_FAIL("U10004", "사용자 나이 검증 실패", 400), + USER_NAME_VALIDATION_FAIL("U10005", "사용자 이름 검증 실패", 400), + USER_HOBBY_VALIDATION_FAIL("U10006", "사용자 취미 검증 실패", 400), + + VALIDATION_FAIL("V10001", "유효성 검사 실패", 400); + + private final String code; + + private final String message; + + private final int httpStatusCode; + + ErrorCode(String code, String message, int httpStatusCode) { + this.code = code; + this.message = message; + this.httpStatusCode = httpStatusCode; + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/exception/GlobalExceptionHandler.java b/hehe-board/src/main/java/com/programmers/heheboard/global/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..0ecd13fd4 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,21 @@ +package com.programmers.heheboard.global.exception; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.programmers.heheboard.global.codes.ErrorCode; +import com.programmers.heheboard.global.response.ErrorResponse; + +import lombok.extern.slf4j.Slf4j; + +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(GlobalRuntimeException.class) + public ErrorResponse handleRuntimeException(GlobalRuntimeException ex) { + ErrorCode errorCode = ex.getErrorCode(); + log.error("code: {} \n stack trace: {}", errorCode, ex); + return new ErrorResponse(errorCode.getHttpStatusCode()); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/exception/GlobalRuntimeException.java b/hehe-board/src/main/java/com/programmers/heheboard/global/exception/GlobalRuntimeException.java new file mode 100644 index 000000000..deb5b7ed7 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/exception/GlobalRuntimeException.java @@ -0,0 +1,15 @@ +package com.programmers.heheboard.global.exception; + +import com.programmers.heheboard.global.codes.ErrorCode; + +import lombok.Getter; + +@Getter +public class GlobalRuntimeException extends RuntimeException { + + private final ErrorCode errorCode; + + public GlobalRuntimeException(ErrorCode errorCode) { + this.errorCode = errorCode; + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/exception/ValidationExceptionHandler.java b/hehe-board/src/main/java/com/programmers/heheboard/global/exception/ValidationExceptionHandler.java new file mode 100644 index 000000000..945505f3a --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/exception/ValidationExceptionHandler.java @@ -0,0 +1,22 @@ +package com.programmers.heheboard.global.exception; + +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.programmers.heheboard.global.codes.ErrorCode; +import com.programmers.heheboard.global.response.ErrorResponse; + +import lombok.extern.slf4j.Slf4j; + +@RestControllerAdvice +@Slf4j +public class ValidationExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ErrorResponse handleRuntimeException(MethodArgumentNotValidException ex) { + ErrorCode errorCode = ErrorCode.VALIDATION_FAIL; + log.info("code: {} \n", errorCode); + return new ErrorResponse(errorCode.getHttpStatusCode()); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/response/ApiResponse.java b/hehe-board/src/main/java/com/programmers/heheboard/global/response/ApiResponse.java new file mode 100644 index 000000000..a3832be90 --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/response/ApiResponse.java @@ -0,0 +1,24 @@ +package com.programmers.heheboard.global.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ApiResponse { + private int statusCode; + private T data; + + public ApiResponse(int statusCode, T data) { + this.statusCode = statusCode; + this.data = data; + } + + public static ApiResponse ok(T data) { + return new ApiResponse<>(200, data); + } + + public static ApiResponse fail(int statusCode, T data) { + return new ApiResponse<>(statusCode, data); + } +} diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/response/ApiSliceResponse.java b/hehe-board/src/main/java/com/programmers/heheboard/global/response/ApiSliceResponse.java new file mode 100644 index 000000000..cc533a8cb --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/response/ApiSliceResponse.java @@ -0,0 +1,49 @@ +package com.programmers.heheboard.global.response; + +import java.util.List; + +import org.springframework.data.domain.Slice; + +import com.programmers.heheboard.domain.post.PostResponseDto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ApiSliceResponse { + private final int size; + private final int number; + private final boolean first; + private final boolean last; + private final List content; + + @Builder + public ApiSliceResponse(int size, int number, boolean first, boolean last, + List content) { + this.size = size; + this.number = number; + this.first = first; + this.last = last; + this.content = content; + } + + public static ApiResponse ok(Slice postSliceResponseDtos) { + ApiSliceResponse apiSliceResponse = getApiSliceResponse(postSliceResponseDtos); + return ApiResponse.ok(apiSliceResponse); + } + + public static ApiResponse fail(int statusCode, Slice postSliceResponseDtos) { + ApiSliceResponse apiSliceResponse = getApiSliceResponse(postSliceResponseDtos); + return ApiResponse.fail(statusCode, apiSliceResponse); + } + + private static ApiSliceResponse getApiSliceResponse(Slice postSliceResponseDtos) { + return ApiSliceResponse.builder() + .size(postSliceResponseDtos.getSize()) + .number(postSliceResponseDtos.getNumber()) + .first(postSliceResponseDtos.isFirst()) + .last(postSliceResponseDtos.isLast()) + .content(postSliceResponseDtos.getContent()) + .build(); + } +} \ No newline at end of file diff --git a/hehe-board/src/main/java/com/programmers/heheboard/global/response/ErrorResponse.java b/hehe-board/src/main/java/com/programmers/heheboard/global/response/ErrorResponse.java new file mode 100644 index 000000000..9b258133d --- /dev/null +++ b/hehe-board/src/main/java/com/programmers/heheboard/global/response/ErrorResponse.java @@ -0,0 +1,13 @@ +package com.programmers.heheboard.global.response; + +public final class ErrorResponse { + private final int httpStatusCode; + + public ErrorResponse(int httpStatusCode) { + this.httpStatusCode = httpStatusCode; + } + + public int httpStatusCode() { + return httpStatusCode; + } +} diff --git a/hehe-board/src/main/resources/application.yaml b/hehe-board/src/main/resources/application.yaml new file mode 100644 index 000000000..614788328 --- /dev/null +++ b/hehe-board/src/main/resources/application.yaml @@ -0,0 +1,23 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/board + username: root + password: root1234! + + jpa: + database: mysql + show-sql: true + open-in-view: false + + properties: + hibernate: + format_sql: true + hbm2ddl.auto: update + +server: + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true \ No newline at end of file diff --git a/hehe-board/src/test/java/com/programmers/heheboard/HeheBoardApplicationTests.java b/hehe-board/src/test/java/com/programmers/heheboard/HeheBoardApplicationTests.java new file mode 100644 index 000000000..378f157ac --- /dev/null +++ b/hehe-board/src/test/java/com/programmers/heheboard/HeheBoardApplicationTests.java @@ -0,0 +1,13 @@ +package com.programmers.heheboard; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class HeheBoardApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/hehe-board/src/test/java/com/programmers/heheboard/domain/post/PostTest.java b/hehe-board/src/test/java/com/programmers/heheboard/domain/post/PostTest.java new file mode 100644 index 000000000..57f6cc2af --- /dev/null +++ b/hehe-board/src/test/java/com/programmers/heheboard/domain/post/PostTest.java @@ -0,0 +1,209 @@ +package com.programmers.heheboard.domain.post; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +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.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@WebMvcTest(PostController.class) +@AutoConfigureRestDocs(uriScheme = "https") +class PostTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private PostService postService; + + @Autowired + private ObjectMapper objectMapper; + + private static List postResponseDtos; + + @BeforeAll + public static void setUP() { + postResponseDtos = Arrays.asList( + PostResponseDto.builder() + .title("title1") + .content("content1") + .createdAt(LocalDateTime.of(2000, 1, 1, 1, 1)) + .modifiedAt(LocalDateTime.of(2000, 1, 1, 1, 1)) + .build(), + + PostResponseDto.builder() + .title("title2") + .content("content2") + .createdAt(LocalDateTime.of(2000, 1, 1, 1, 1)) + .modifiedAt(LocalDateTime.of(2000, 1, 1, 1, 1)) + .build() + ); + } + + @Test + void create() throws Exception { + // given + PostResponseDto responseDTO = PostResponseDto.builder() + .title("title") + .content("content") + .createdAt(LocalDateTime.of(2000, 1, 1, 1, 1)) + .modifiedAt(LocalDateTime.of(2000, 1, 1, 1, 1)) + .build(); + + when(postService.createPost(any(CreatePostRequestDto.class))).thenReturn(responseDTO); + + // when + CreatePostRequestDto requestDto = new CreatePostRequestDto("title", "content", 1L); + + ResultActions result = this.mockMvc.perform( + post("/posts") + .contentType((MediaType.APPLICATION_JSON)) + .content(objectMapper.writeValueAsString(requestDto)) + ); + + // then + result.andExpect(status().isOk()) + .andDo(print()) + .andDo( + document("post-save", + requestFields( + fieldWithPath("userId").type(JsonFieldType.NUMBER).description("User Id"), + fieldWithPath("title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("내용") + ), + responseFields( + fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("상태코드"), + fieldWithPath("data.title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("data.content").type(JsonFieldType.STRING).description("내용"), + fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("생성일"), + fieldWithPath("data.modifiedAt").type(JsonFieldType.STRING).description("수정일") + ) + ) + ); + } + + @Test + void findSinglePost() throws Exception { + // given + Long postId = 1L; + + PostResponseDto responseDTO = postResponseDtos.get(0); + + when(postService.findPost(postId)).thenReturn(responseDTO); + + // when + ResultActions result = this.mockMvc.perform( + get("/posts/%,d".formatted(postId)) + ); + + // then + result.andExpect(status().isOk()) + .andDo(print()) + .andDo(document("post-find", + responseFields( + fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("상태코드"), + fieldWithPath("data.title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("data.content").type(JsonFieldType.STRING).description("내용"), + fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("생성일"), + fieldWithPath("data.modifiedAt").type(JsonFieldType.STRING).description("수정일") + ) + ) + ); + } + + @Test + void getPosts() throws Exception { + // given + Slice responseSlice = new SliceImpl<>(postResponseDtos); + + int page = 1; + int size = 2; + + when(postService.getPosts(page, size)).thenReturn(responseSlice); + + // when + ResultActions result = this.mockMvc.perform( + get("/posts") + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + ); + + // then + result.andExpect(status().isOk()) + .andDo(print()) + .andDo(document("post-get", + responseFields( + fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("상태코드"), + + fieldWithPath("data.content[].title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("data.content[].content").type(JsonFieldType.STRING).description("내용"), + fieldWithPath("data.content[].createdAt").type(JsonFieldType.STRING).description("생성일"), + fieldWithPath("data.content[].modifiedAt").type(JsonFieldType.STRING).description("수정일"), + + fieldWithPath("data.size").type(JsonFieldType.NUMBER).description("size"), + fieldWithPath("data.number").type(JsonFieldType.NUMBER).description("number"), + fieldWithPath("data.first").type(JsonFieldType.BOOLEAN).description("first"), + fieldWithPath("data.last").type(JsonFieldType.BOOLEAN).description("last") + ) + ) + ); + } + + @Test + void update() throws Exception { + Long postId = 1L; + + PostResponseDto responseDTO = postResponseDtos.get(0); + + when(postService.updatePost(eq(postId), any(UpdatePostRequestDto.class))).thenReturn(responseDTO); + + UpdatePostRequestDto updatePostRequestDto = new UpdatePostRequestDto("updated content", "updated content"); + + // when + ResultActions result = this.mockMvc.perform( + put("/posts/%,d".formatted(postId)) + .contentType((MediaType.APPLICATION_JSON)) + .content(objectMapper.writeValueAsString(updatePostRequestDto)) + ); + + // then + result.andExpect(status().isOk()) + .andDo(print()) + .andDo( + document("post-update", + requestFields( + fieldWithPath("title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("content").type(JsonFieldType.STRING).description("내용") + ), + responseFields( + fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("상태코드"), + fieldWithPath("data.title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("data.content").type(JsonFieldType.STRING).description("내용"), + fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("생성일"), + fieldWithPath("data.modifiedAt").type(JsonFieldType.STRING).description("수정일") + ) + ) + ); + } +} \ No newline at end of file diff --git a/hehe-board/src/test/java/com/programmers/heheboard/domain/user/UserTest.java b/hehe-board/src/test/java/com/programmers/heheboard/domain/user/UserTest.java new file mode 100644 index 000000000..9a76cba5e --- /dev/null +++ b/hehe-board/src/test/java/com/programmers/heheboard/domain/user/UserTest.java @@ -0,0 +1,80 @@ +package com.programmers.heheboard.domain.user; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDateTime; + +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.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@WebMvcTest(UserController.class) +@AutoConfigureRestDocs(uriScheme = "https") +class UserTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + public void create() throws Exception { + // given + UserResponseDto response = UserResponseDto.builder() + .name("heehee") + .age(10) + .hobby("coding") + .createdAt(LocalDateTime.of(2000, 1, 1, 1, 1)) + .modifiedAt(LocalDateTime.of(2000, 1, 1, 1, 1)) + .build(); + + when(userService.createUser(any(CreateUserRequestDto.class))).thenReturn(response); + + // when + CreateUserRequestDto requestDto = new CreateUserRequestDto("heehee", 10, "coding"); + + ResultActions result = this.mockMvc.perform( + post("/users") + .contentType((MediaType.APPLICATION_JSON)) + .content(objectMapper.writeValueAsString(requestDto)) + ); + + // then + result.andExpect(status().isOk()) + .andDo(print()) + .andDo(document("member-save", + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("이름"), + fieldWithPath("age").type(JsonFieldType.NUMBER).description("나이"), + fieldWithPath("hobby").type(JsonFieldType.STRING).description("취미") + ), + responseFields( + fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("상태코드"), + fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"), + fieldWithPath("data.age").type(JsonFieldType.NUMBER).description("나이"), + fieldWithPath("data.hobby").type(JsonFieldType.STRING).description("취미"), + fieldWithPath("data.createdAt").type(JsonFieldType.STRING).description("생성 시간"), + fieldWithPath("data.modifiedAt").type(JsonFieldType.STRING).description("수정 시간") + ) + ) + ); + } +} \ No newline at end of file