diff --git a/.github/workflows/deploy-all-ci-cd-push.yml b/.github/workflows/deploy-all-ci-cd-push.yml new file mode 100644 index 000000000..8a829fcdb --- /dev/null +++ b/.github/workflows/deploy-all-ci-cd-push.yml @@ -0,0 +1,80 @@ +name: All deploy CI + CD on push + +on: + push: + branches: [ "deploy" ] + +jobs: + deploy-ci-be: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + token: ${{ secrets.SUBMODULE_BE_TOKEN }} + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'corretto' + + - name: Build with Gradle + uses: gradle/gradle-build-action@v2.6.0 + - name: Execute Gradle build + run: | + cd backend/baton + ./gradlew build + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2.9.1 + + - name: Login to Docker Hub + uses: docker/login-action@v2.2.0 + with: + username: ${{ secrets.DOCKERHUB_DEPLOY_USERNAME }} + password: ${{ secrets.DOCKERHUB_DEPLOY_TOKEN }} + + - name: Docker Image Build + run: | + cd backend/baton + docker build --platform linux/arm64/v8 -t 2023batondeploy/2023-baton-deploy -f Dockerfile-deploy . + + - name: Docker Hub Push + run: docker push 2023batondeploy/2023-baton-deploy + + deploy-ci-fe: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: 의존성을 설치한다 + run: npm install + + - name: 테스트를 수행한다 + run: npm run test + + deploy-cd-be: + needs: deploy-ci-be + runs-on: [self-hosted, Linux, ARM64, deploy] + + steps: + - name: Pull Latest Docker Image + run: | + sudo docker login --username ${{ secrets.DOCKERHUB_DEPLOY_USERNAME }} --password ${{ secrets.DOCKERHUB_DEPLOY_TOKEN }} + if sudo docker inspect spring-baton &>/dev/null; then + sudo docker stop spring-baton + sudo docker rm -f spring-baton + sudo docker image prune -af + fi + sudo docker pull 2023batondeploy/2023-baton-deploy:latest + + - name: Docker Compose + run: | + sudo docker run --name spring-baton -p 8080:8080 2023batondeploy/2023-baton-deploy:latest 1>> build.log 2>> error.log & diff --git a/.github/workflows/deploy-be-ci-pr.yml b/.github/workflows/deploy-be-ci-pr.yml new file mode 100644 index 000000000..13d11066b --- /dev/null +++ b/.github/workflows/deploy-be-ci-pr.yml @@ -0,0 +1,26 @@ +name: BE deploy CI on Pull Request + +on: + pull_request: + branches: [ "deploy" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'corretto' + - name: Build with Gradle + uses: gradle/gradle-build-action@v2.6.0 + - name: Execute Gradle build + run: | + cd backend/baton + ./gradlew build diff --git a/.github/workflows/deploy-fe-ci-pr.yml b/.github/workflows/deploy-fe-ci-pr.yml new file mode 100644 index 000000000..7bf483c46 --- /dev/null +++ b/.github/workflows/deploy-fe-ci-pr.yml @@ -0,0 +1,21 @@ +name: FE deploy CI on Pull Request + +on: + pull_request: + branches: [ "deploy" ] + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: 의존성을 설치한다 + run: npm install + + - name: 테스트를 수행한다 + run: npm run test diff --git a/.github/workflows/dev-be-ci-cd-push.yml b/.github/workflows/dev-be-ci-cd-push.yml new file mode 100644 index 000000000..fcf078b3d --- /dev/null +++ b/.github/workflows/dev-be-ci-cd-push.yml @@ -0,0 +1,65 @@ +name: dev/BE CD on Push + +on: + push: + branches: [ "dev/BE" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + token: ${{ secrets.SUBMODULE_BE_TOKEN }} + submodules: recursive + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'corretto' + + - name: Build with Gradle + uses: gradle/gradle-build-action@v2.6.0 + - name: Execute Gradle build + run: | + cd backend/baton + ./gradlew build + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2.9.1 + + - name: Login to Docker Hub + uses: docker/login-action@v2.2.0 + with: + username: ${{ secrets.DOCKERHUB_DEV_USERNAME }} + password: ${{ secrets.DOCKERHUB_DEV_TOKEN }} + + - name: Docker Image Build + run: | + cd backend/baton + docker build --platform linux/arm64/v8 -t 2023baton/2023baton -f Dockerfile-dev . + + - name: Docker Hub Push + run: docker push 2023baton/2023baton + + deploy: + runs-on: [self-hosted, Linux, ARM64] + needs: build + + steps: + - name: Pull Latest Docker Image + run: | + sudo docker login --username ${{ secrets.DOCKERHUB_DEV_USERNAME }} --password ${{ secrets.DOCKERHUB_DEV_TOKEN }} + if sudo docker inspect spring-baton &>/dev/null; then + sudo docker stop spring-baton + sudo docker rm -f spring-baton + sudo docker image prune -af + fi + sudo docker pull 2023baton/2023baton:latest + + - name: Docker Compose + run: | + sudo docker run --name spring-baton --network=baton -p 8080:8080 2023baton/2023baton:latest 1>> build.log 2>> error.log & diff --git a/.github/workflows/dev-be-ci-pr.yml b/.github/workflows/dev-be-ci-pr.yml new file mode 100644 index 000000000..b337a3cbd --- /dev/null +++ b/.github/workflows/dev-be-ci-pr.yml @@ -0,0 +1,26 @@ +name: dev/BE CI on Pull Request + +on: + pull_request: + branches: [ "dev/BE" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'corretto' + - name: Build with Gradle + uses: gradle/gradle-build-action@v2.6.0 + - name: Execute Gradle build + run: | + cd backend/baton + ./gradlew build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..2d48c233c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "backend/baton/secret"] + path = backend/baton/secret + url = https://github.com/2023-baton/sub-be.git diff --git a/backend/baton/.gitignore b/backend/baton/.gitignore new file mode 100644 index 000000000..e9f2877d2 --- /dev/null +++ b/backend/baton/.gitignore @@ -0,0 +1,182 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,java,gradle,intellij+all +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,java,gradle,intellij+all + +### Intellij+all ### +# 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 +HELP.md + +# 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+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### 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 + +# End of https://www.toptal.com/developers/gitignore/api/macos,java,gradle,intellij+all + +src/main/resources/application-prod.yml +src/main/resources/application-dev.yml diff --git a/backend/baton/Dockerfile-deploy b/backend/baton/Dockerfile-deploy new file mode 100644 index 000000000..c3ebdc441 --- /dev/null +++ b/backend/baton/Dockerfile-deploy @@ -0,0 +1,7 @@ +FROM arm64v8/amazoncorretto:17 + +WORKDIR /app + +COPY ./build/libs/baton-0.0.1-SNAPSHOT.jar /app/baton.jar + +CMD ["java", "-jar", "-Dspring.profiles.active=deploy", "baton.jar"] diff --git a/backend/baton/Dockerfile-dev b/backend/baton/Dockerfile-dev new file mode 100644 index 000000000..9c8d784ff --- /dev/null +++ b/backend/baton/Dockerfile-dev @@ -0,0 +1,7 @@ +FROM arm64v8/amazoncorretto:17 + +WORKDIR /app + +COPY ./build/libs/baton-0.0.1-SNAPSHOT.jar /app/baton.jar + +CMD ["java", "-jar", "-Dspring.profiles.active=dev", "baton.jar"] diff --git a/backend/baton/build.gradle b/backend/baton/build.gradle new file mode 100644 index 000000000..cc3e58976 --- /dev/null +++ b/backend/baton/build.gradle @@ -0,0 +1,73 @@ +plugins { + id 'java' + id 'java-test-fixtures' + id 'org.springframework.boot' version '3.1.1' + id 'io.spring.dependency-management' version '1.1.0' + id 'org.asciidoctor.jvm.convert' version '3.3.2' +} + +group = 'touch' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + asciidoctorExt +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +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-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' + developmentOnly 'org.springframework.boot:spring-boot-docker-compose' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.rest-assured:rest-assured' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testImplementation 'org.testcontainers:mysql' + + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' +} + +tasks.named('test') { + outputs.dir snippetsDir + useJUnitPlatform() +} + +processResources.dependsOn('copySecret') + +tasks.register('copySecret', Copy) { + from './secret' + include 'application*.yml' + into 'src/main/resources' +} + +tasks.named('asciidoctor') { + inputs.dir snippetsDir + configurations 'asciidoctorExt' + dependsOn test +} diff --git a/backend/baton/docker-compose.yaml b/backend/baton/docker-compose.yaml new file mode 100644 index 000000000..3f0d6fd01 --- /dev/null +++ b/backend/baton/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + mysql: + image: 'mysql:latest' + environment: + - 'MYSQL_DATABASE=mydatabase' + - 'MYSQL_PASSWORD=secret' + - 'MYSQL_ROOT_PASSWORD=verysecret' + - 'MYSQL_USER=myuser' + ports: + - '3307:3306' diff --git a/backend/baton/gradle/wrapper/gradle-wrapper.jar b/backend/baton/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..ccebba771 Binary files /dev/null and b/backend/baton/gradle/wrapper/gradle-wrapper.jar differ diff --git a/backend/baton/gradle/wrapper/gradle-wrapper.properties b/backend/baton/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..42defcc94 --- /dev/null +++ b/backend/baton/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/baton/gradlew b/backend/baton/gradlew new file mode 100755 index 000000000..79a61d421 --- /dev/null +++ b/backend/baton/gradlew @@ -0,0 +1,244 @@ +#!/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 + +# 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"' + +# 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 + which java >/dev/null 2>&1 || 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 + +# 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 + +# 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/backend/baton/gradlew.bat b/backend/baton/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/backend/baton/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/backend/baton/secret b/backend/baton/secret new file mode 160000 index 000000000..34e8a2f69 --- /dev/null +++ b/backend/baton/secret @@ -0,0 +1 @@ +Subproject commit 34e8a2f6989c8040526b023fbf854a341d8dc63f diff --git a/backend/baton/settings.gradle b/backend/baton/settings.gradle new file mode 100644 index 000000000..12f2c9107 --- /dev/null +++ b/backend/baton/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'baton' diff --git a/backend/baton/src/main/java/touch/baton/BatonApplication.java b/backend/baton/src/main/java/touch/baton/BatonApplication.java new file mode 100644 index 000000000..5face043a --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/BatonApplication.java @@ -0,0 +1,13 @@ +package touch.baton; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BatonApplication { + + public static void main(String[] args) { + SpringApplication.run(BatonApplication.class, args); + } + +} diff --git a/backend/baton/src/main/java/touch/baton/config/JpaConfig.java b/backend/baton/src/main/java/touch/baton/config/JpaConfig.java new file mode 100644 index 000000000..94748c9d0 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/config/JpaConfig.java @@ -0,0 +1,9 @@ +package touch.baton.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration(proxyBeanMethods = false) +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/backend/baton/src/main/java/touch/baton/config/WebMvcConfig.java b/backend/baton/src/main/java/touch/baton/config/WebMvcConfig.java new file mode 100644 index 000000000..fb1ecc9ee --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/config/WebMvcConfig.java @@ -0,0 +1,25 @@ +package touch.baton.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import static org.springframework.http.HttpHeaders.LOCATION; +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.OPTIONS; +import static org.springframework.http.HttpMethod.PATCH; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.http.HttpMethod.PUT; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(final CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000") + .allowCredentials(false) + .allowedMethods(GET.name(), POST.name(), PUT.name(), PATCH.name(), DELETE.name(), OPTIONS.name()) + .exposedHeaders(LOCATION) + .maxAge(3600); + } +} diff --git a/backend/baton/src/main/java/touch/baton/config/converter/ConverterConfig.java b/backend/baton/src/main/java/touch/baton/config/converter/ConverterConfig.java new file mode 100644 index 000000000..7c0577823 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/config/converter/ConverterConfig.java @@ -0,0 +1,36 @@ +package touch.baton.config.converter; + +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; + +@Configuration +public class ConverterConfig implements WebMvcConfigurer { + + private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; + private static final TimeZone KOREA_TIME_ZONE = TimeZone.getTimeZone("Asia/Seoul"); + + @Override + public void addFormatters(final FormatterRegistry registry) { + registry.addConverter(new StringDateToLocalDateTimeConverter(DEFAULT_DATE_TIME_FORMAT, KOREA_TIME_ZONE)); + } + + @Bean + public Jackson2ObjectMapperBuilderCustomizer localDateTimeConverter() { + return jacksonObjectMapperBuilder -> { + jacksonObjectMapperBuilder.timeZone(KOREA_TIME_ZONE); + jacksonObjectMapperBuilder.simpleDateFormat(DEFAULT_DATE_TIME_FORMAT); + + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT); + jacksonObjectMapperBuilder.serializers(new LocalDateTimeSerializer(formatter)); + jacksonObjectMapperBuilder.deserializers(new LocalDateTimeDeserializer(formatter)); + }; + } +} diff --git a/backend/baton/src/main/java/touch/baton/config/converter/StringDateToLocalDateTimeConverter.java b/backend/baton/src/main/java/touch/baton/config/converter/StringDateToLocalDateTimeConverter.java new file mode 100644 index 000000000..bcc28f8a6 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/config/converter/StringDateToLocalDateTimeConverter.java @@ -0,0 +1,26 @@ +package touch.baton.config.converter; + +import org.springframework.core.convert.converter.Converter; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; + +public class StringDateToLocalDateTimeConverter implements Converter { + + private final TimeZone timeZone; + private final String dateTimeFormat; + + public StringDateToLocalDateTimeConverter(final String dateTimeFormat, final TimeZone timeZone) { + this.timeZone = timeZone; + this.dateTimeFormat = dateTimeFormat; + } + + @Override + public LocalDateTime convert(final String source) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateTimeFormat) + .withZone(timeZone.toZoneId()); + + return LocalDateTime.parse(source, formatter); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/BaseEntity.java b/backend/baton/src/main/java/touch/baton/domain/common/BaseEntity.java new file mode 100644 index 000000000..b7fc3f251 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/BaseEntity.java @@ -0,0 +1,28 @@ +package touch.baton.domain.common; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @Column(updatable = false, nullable = false) + @CreatedDate + private LocalDateTime createdAt; + + @Column(nullable = false) + @LastModifiedDate + private LocalDateTime updatedAt; + + @Column(nullable = true) + private LocalDateTime deletedAt; +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/GlobalControllerAdvice.java b/backend/baton/src/main/java/touch/baton/domain/common/GlobalControllerAdvice.java new file mode 100644 index 000000000..55a67a09f --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/GlobalControllerAdvice.java @@ -0,0 +1,16 @@ +package touch.baton.domain.common; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import touch.baton.domain.common.exception.ClientRequestException; +import touch.baton.domain.common.response.ErrorResponse; + +@RestControllerAdvice +public class GlobalControllerAdvice { + + @ExceptionHandler(ClientRequestException.class) + public ResponseEntity handleClientRequest(ClientRequestException e) { + return ResponseEntity.status(e.getHttpStatus().value()).body(ErrorResponse.from(e)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/BaseException.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/BaseException.java new file mode 100644 index 000000000..dfda53abf --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/BaseException.java @@ -0,0 +1,25 @@ +package touch.baton.domain.common.exception; + +import org.springframework.http.HttpStatus; + +public abstract class BaseException extends RuntimeException { + + private final ErrorCode errorCode; + + public BaseException(final ErrorCode errorCode) { + this.errorCode = errorCode; + } + + @Override + public String getMessage() { + return errorCode.getMessage(); + } + + public String getErrorCode() { + return errorCode.getErrorCode(); + } + + public HttpStatus getHttpStatus() { + return errorCode.getHttpStatus(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/BusinessException.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/BusinessException.java new file mode 100644 index 000000000..4852cd0d7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/BusinessException.java @@ -0,0 +1,8 @@ +package touch.baton.domain.common.exception; + +public abstract class BusinessException extends BaseException { + + public BusinessException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientErrorCode.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientErrorCode.java new file mode 100644 index 000000000..8ab11e4d8 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientErrorCode.java @@ -0,0 +1,40 @@ +package touch.baton.domain.common.exception; + +import org.springframework.http.HttpStatus; + +public enum ClientErrorCode implements ErrorCode { + + TITLE_IS_NULL(HttpStatus.BAD_REQUEST, "RP001", "제목을 입력해주세요."), + PULL_REQUEST_URL_IS_NULL(HttpStatus.BAD_REQUEST, "RP002", "PR 주소를 입력해주세요."), + DEADLINE_IS_NULL(HttpStatus.BAD_REQUEST, "RP003", "마감일을 입력해주세요."), + CONTENTS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP004", "내용을 입력해주세요."), + CONTENTS_OVERFLOW(HttpStatus.BAD_REQUEST, "RP005", "내용은 1000자 까지 입력해주세요."), + PAST_DEADLINE(HttpStatus.BAD_REQUEST, "RP006", "마감일은 오늘보다 과거일 수 없습니다."), + CONTENTS_NOT_FOUND(HttpStatus.NOT_FOUND, "RP007", "존재하지 않는 게시물입니다."), + TAGS_ARE_NULL(HttpStatus.BAD_REQUEST, "RP008", "태그 목록을 빈 값이라도 입력해주세요."); + + private final HttpStatus httpStatus; + private final String errorCode; + private final String message; + + ClientErrorCode(final HttpStatus httpStatus, final String code, final String message) { + this.httpStatus = httpStatus; + this.errorCode = code; + this.message = message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientRequestException.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientRequestException.java new file mode 100644 index 000000000..9c83378c4 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/ClientRequestException.java @@ -0,0 +1,8 @@ +package touch.baton.domain.common.exception; + +public class ClientRequestException extends BaseException { + + public ClientRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/DomainException.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/DomainException.java new file mode 100644 index 000000000..8d4131755 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/DomainException.java @@ -0,0 +1,8 @@ +package touch.baton.domain.common.exception; + +public abstract class DomainException extends BaseException { + + public DomainException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/ErrorCode.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/ErrorCode.java new file mode 100644 index 000000000..78a4c1724 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/ErrorCode.java @@ -0,0 +1,12 @@ +package touch.baton.domain.common.exception; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + HttpStatus getHttpStatus(); + + String getMessage(); + + String getErrorCode(); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/ServerErrorCode.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/ServerErrorCode.java new file mode 100644 index 000000000..5a82add59 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/ServerErrorCode.java @@ -0,0 +1,32 @@ +package touch.baton.domain.common.exception; + +import org.springframework.http.HttpStatus; + +public enum ServerErrorCode implements ErrorCode { + ; + + private final HttpStatus httpStatus; + private final String errorCode; + private final String message; + + ServerErrorCode(final HttpStatus httpStatus, final String code, final String message) { + this.httpStatus = httpStatus; + this.errorCode = code; + this.message = message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/validator/NotNullValidator.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/validator/NotNullValidator.java new file mode 100644 index 000000000..bb3ea1f71 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/validator/NotNullValidator.java @@ -0,0 +1,24 @@ +package touch.baton.domain.common.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class NotNullValidator implements ConstraintValidator { + + private ClientErrorCode errorCode; + + @Override + public void initialize(final ValidNotNull constraintAnnotation) { + errorCode = constraintAnnotation.clientErrorCode(); + } + + @Override + public boolean isValid(final Object value, final ConstraintValidatorContext context) { + if (value == null) { + throw new ClientRequestException(errorCode); + } + return true; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/exception/validator/ValidNotNull.java b/backend/baton/src/main/java/touch/baton/domain/common/exception/validator/ValidNotNull.java new file mode 100644 index 000000000..571b66590 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/exception/validator/ValidNotNull.java @@ -0,0 +1,38 @@ +package touch.baton.domain.common.exception.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import touch.baton.domain.common.exception.ClientErrorCode; + +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, PARAMETER }) +@Retention(RUNTIME) +@Repeatable(ValidNotNull.List.class) +@Documented +@Constraint(validatedBy = NotNullValidator.class) +public @interface ValidNotNull { + + String message() default "null 값이 존재합니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + ClientErrorCode clientErrorCode(); + + @Target({ FIELD, PARAMETER }) + @Retention(RUNTIME) + @Documented + @interface List { + + ValidNotNull[] value(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/response/ErrorResponse.java b/backend/baton/src/main/java/touch/baton/domain/common/response/ErrorResponse.java new file mode 100644 index 000000000..5cf4c6060 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/response/ErrorResponse.java @@ -0,0 +1,20 @@ +package touch.baton.domain.common.response; + +import lombok.Getter; +import touch.baton.domain.common.exception.BaseException; + +@Getter +public class ErrorResponse { + + private final String errorCode; + private final String message; + + private ErrorResponse(final String errorCode, final String message) { + this.errorCode = errorCode; + this.message = message; + } + + public static ErrorResponse from(final BaseException e) { + return new ErrorResponse(e.getErrorCode(), e.getMessage()); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/ChattingCount.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/ChattingCount.java new file mode 100644 index 000000000..072f9d222 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/ChattingCount.java @@ -0,0 +1,31 @@ +package touch.baton.domain.common.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class ChattingCount { + + private static final String DEFAULT_VALUE = "0"; + + @ColumnDefault(DEFAULT_VALUE) + @Column(name = "chatting_room_count", nullable = false) + private int value; + + public ChattingCount(final int value) { + this.value = value; + } + + public static ChattingCount zero() { + return new ChattingCount(Integer.parseInt(DEFAULT_VALUE)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/Contents.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/Contents.java new file mode 100644 index 000000000..d4cf58619 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/Contents.java @@ -0,0 +1,32 @@ +package touch.baton.domain.common.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class Contents { + + @Column(name = "contents", nullable = false, columnDefinition = "TEXT") + private String value; + + public Contents(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("contents 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/Grade.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/Grade.java new file mode 100644 index 000000000..7348430f2 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/Grade.java @@ -0,0 +1,6 @@ +package touch.baton.domain.common.vo; + +public enum Grade { + + BARE_FOOT +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/Introduction.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/Introduction.java new file mode 100644 index 000000000..76eaea4b1 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/Introduction.java @@ -0,0 +1,23 @@ +package touch.baton.domain.common.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class Introduction { + + @Column(name = "introduction", nullable = true) + private String value; + + public Introduction(final String value) { + this.value = value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/TagName.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/TagName.java new file mode 100644 index 000000000..5247fbadc --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/TagName.java @@ -0,0 +1,32 @@ +package touch.baton.domain.common.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class TagName { + + @Column(name = "name", nullable = false, unique = true) + private String value; + + public TagName(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("tagName 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/Title.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/Title.java new file mode 100644 index 000000000..5cfecd5b7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/Title.java @@ -0,0 +1,32 @@ +package touch.baton.domain.common.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class Title { + + @Column(name = "title", nullable = false) + private String value; + + public Title(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("title 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/TotalRating.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/TotalRating.java new file mode 100644 index 000000000..8db33b994 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/TotalRating.java @@ -0,0 +1,27 @@ +package touch.baton.domain.common.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class TotalRating { + + private static final String DEFAULT_VALUE = "0"; + + @ColumnDefault(DEFAULT_VALUE) + @Column(name = "total_rating", nullable = false) + private int value; + + public TotalRating(final int value) { + this.value = value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/common/vo/WatchedCount.java b/backend/baton/src/main/java/touch/baton/domain/common/vo/WatchedCount.java new file mode 100644 index 000000000..39683032b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/common/vo/WatchedCount.java @@ -0,0 +1,35 @@ +package touch.baton.domain.common.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class WatchedCount { + + private static final String DEFAULT_VALUE = "0"; + + @ColumnDefault(DEFAULT_VALUE) + @Column(name = "watch_count", nullable = false) + private int value; + + public WatchedCount(final int value) { + this.value = value; + } + + public static WatchedCount zero() { + return new WatchedCount(Integer.parseInt(DEFAULT_VALUE)); + } + + public WatchedCount increase() { + return new WatchedCount(value + 1); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/Member.java b/backend/baton/src/main/java/touch/baton/domain/member/Member.java new file mode 100644 index 000000000..a1310d10f --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/Member.java @@ -0,0 +1,111 @@ +package touch.baton.domain.member; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.member.exception.OldMemberException; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; + +import java.util.Objects; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Member extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private MemberName memberName; + + @Embedded + private Email email; + + @Embedded + private OauthId oauthId; + + @Embedded + private GithubUrl githubUrl; + + @Embedded + private Company company; + + @Embedded + private ImageUrl imageUrl; + + @Builder + private Member(final MemberName memberName, + final Email email, + final OauthId oauthId, + final GithubUrl githubUrl, + final Company company, + final ImageUrl imageUrl + ) { + this(null, memberName, email, oauthId, githubUrl, company, imageUrl); + } + + private Member(final Long id, + final MemberName memberName, + final Email email, + final OauthId oauthId, + final GithubUrl githubUrl, + final Company company, + final ImageUrl imageUrl + ) { + validateNotNull(memberName, email, oauthId, githubUrl, company, imageUrl); + this.id = id; + this.memberName = memberName; + this.email = email; + this.oauthId = oauthId; + this.githubUrl = githubUrl; + this.company = company; + this.imageUrl = imageUrl; + } + + private void validateNotNull(final MemberName memberName, + final Email email, + final OauthId oauthId, + final GithubUrl githubUrl, + final Company company, + final ImageUrl imageUrl + ) { + if (Objects.isNull(memberName)) { + throw new OldMemberException.NotNull("name 는 null 일 수 없습니다."); + } + + if (Objects.isNull(email)) { + throw new OldMemberException.NotNull("email 는 null 일 수 없습니다."); + } + + if (Objects.isNull(oauthId)) { + throw new OldMemberException.NotNull("oauthId 는 null 일 수 없습니다."); + } + + if (Objects.isNull(githubUrl)) { + throw new OldMemberException.NotNull("githubUrl 는 null 일 수 없습니다."); + } + + if (Objects.isNull(company)) { + throw new OldMemberException.NotNull("company 는 null 일 수 없습니다."); + } + + if (Objects.isNull(imageUrl)) { + throw new OldMemberException.NotNull("imageUrl 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberBusinessException.java new file mode 100644 index 000000000..db146b331 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberBusinessException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.BusinessException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class MemberBusinessException extends BusinessException { + + public MemberBusinessException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberDomainException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberDomainException.java new file mode 100644 index 000000000..17947e8a3 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberDomainException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.DomainException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class MemberDomainException extends DomainException { + + public MemberDomainException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberRequestException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberRequestException.java new file mode 100644 index 000000000..01aa1d11d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/MemberRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.member.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class MemberRequestException extends ClientRequestException { + + public MemberRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/exception/OldMemberException.java b/backend/baton/src/main/java/touch/baton/domain/member/exception/OldMemberException.java new file mode 100644 index 000000000..bdabae5f2 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/exception/OldMemberException.java @@ -0,0 +1,15 @@ +package touch.baton.domain.member.exception; + +public class OldMemberException extends RuntimeException { + + public OldMemberException(final String message) { + super(message); + } + + public static class NotNull extends OldMemberException { + + public NotNull(final String message) { + super(message); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/repository/MemberRepository.java b/backend/baton/src/main/java/touch/baton/domain/member/repository/MemberRepository.java new file mode 100644 index 000000000..83f47b92d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/repository/MemberRepository.java @@ -0,0 +1,7 @@ +package touch.baton.domain.member.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.member.Member; + +public interface MemberRepository extends JpaRepository { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/vo/Company.java b/backend/baton/src/main/java/touch/baton/domain/member/vo/Company.java new file mode 100644 index 000000000..ed4e3b074 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/vo/Company.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class Company { + + @Column(name = "company", nullable = false) + private String value; + + public Company(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("company 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/vo/Email.java b/backend/baton/src/main/java/touch/baton/domain/member/vo/Email.java new file mode 100644 index 000000000..bdf26580e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/vo/Email.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class Email { + + @Column(name = "email", nullable = false) + private String value; + + public Email(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("email 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/vo/GithubUrl.java b/backend/baton/src/main/java/touch/baton/domain/member/vo/GithubUrl.java new file mode 100644 index 000000000..19ddb20a9 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/vo/GithubUrl.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class GithubUrl { + + @Column(name = "github_url", nullable = false) + private String value; + + public GithubUrl(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("githubUrl 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/vo/ImageUrl.java b/backend/baton/src/main/java/touch/baton/domain/member/vo/ImageUrl.java new file mode 100644 index 000000000..95f787cd1 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/vo/ImageUrl.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class ImageUrl { + + @Column(name = "image_url", nullable = false) + private String value; + + public ImageUrl(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("imageUrl 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/vo/MemberName.java b/backend/baton/src/main/java/touch/baton/domain/member/vo/MemberName.java new file mode 100644 index 000000000..39c2ac6eb --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/vo/MemberName.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class MemberName { + + @Column(name = "name", nullable = false) + private String value; + + public MemberName(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("name 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/member/vo/OauthId.java b/backend/baton/src/main/java/touch/baton/domain/member/vo/OauthId.java new file mode 100644 index 000000000..6cab4758e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/member/vo/OauthId.java @@ -0,0 +1,32 @@ +package touch.baton.domain.member.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class OauthId { + + @Column(name = "oauth_id", nullable = false) + private String value; + + public OauthId(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("oauthId 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runner/Runner.java b/backend/baton/src/main/java/touch/baton/domain/runner/Runner.java new file mode 100644 index 000000000..1522c2a84 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runner/Runner.java @@ -0,0 +1,88 @@ +package touch.baton.domain.runner; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.Introduction; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.exception.OldRunnerException; + +import java.util.Objects; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Runner extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private TotalRating totalRating; + + @Enumerated(STRING) + @Column(nullable = false) + private Grade grade; + + @Embedded + private Introduction introduction; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "fk_runner_to_member"), nullable = false) + private Member member; + + @Builder + private Runner(final TotalRating totalRating, + final Grade grade, + final Introduction introduction, + final Member member + ) { + this(null, totalRating, grade, introduction, member); + } + + private Runner(final Long id, + final TotalRating totalRating, + final Grade grade, + final Introduction introduction, + final Member member + ) { + validateNotNull(totalRating, grade, member); + this.id = id; + this.totalRating = totalRating; + this.grade = grade; + this.introduction = introduction; + this.member = member; + } + + private void validateNotNull(final TotalRating totalRating, final Grade grade, final Member member) { + if (Objects.isNull(totalRating)) { + throw new OldRunnerException.NotNull("totalRating 은 null 일 수 없습니다."); + } + + if (Objects.isNull(grade)) { + throw new OldRunnerException.NotNull("grade 는 null 일 수 없습니다."); + } + + if (Objects.isNull(member)) { + throw new OldRunnerException.NotNull("member 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runner/exception/OldRunnerException.java b/backend/baton/src/main/java/touch/baton/domain/runner/exception/OldRunnerException.java new file mode 100644 index 000000000..800325968 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runner/exception/OldRunnerException.java @@ -0,0 +1,22 @@ +package touch.baton.domain.runner.exception; + +public class OldRunnerException extends RuntimeException { + + public OldRunnerException(final String message) { + super(message); + } + + public static class NotNull extends OldRunnerException { + + public NotNull(final String message) { + super(message); + } + } + + public static class NotFound extends OldRunnerException { + + public NotFound(final String message) { + super(message); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerBusinessException.java new file mode 100644 index 000000000..333666c7f --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerBusinessException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.runner.exception; + +import touch.baton.domain.common.exception.BusinessException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class RunnerBusinessException extends BusinessException { + + public RunnerBusinessException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerDomainException.java b/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerDomainException.java new file mode 100644 index 000000000..987eb1cfa --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerDomainException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.runner.exception; + +import touch.baton.domain.common.exception.DomainException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class RunnerDomainException extends DomainException { + + public RunnerDomainException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerRequestException.java b/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerRequestException.java new file mode 100644 index 000000000..a259334ca --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runner/exception/RunnerRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.runner.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class RunnerRequestException extends ClientRequestException { + + public RunnerRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runner/repository/RunnerRepository.java b/backend/baton/src/main/java/touch/baton/domain/runner/repository/RunnerRepository.java new file mode 100644 index 000000000..c5e8998b8 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runner/repository/RunnerRepository.java @@ -0,0 +1,19 @@ +package touch.baton.domain.runner.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import touch.baton.domain.runner.Runner; + +import java.util.Optional; + +public interface RunnerRepository extends JpaRepository { + + @Query(""" + select r + from Runner r + join fetch Member m on m.id = r.member.id + where r.id = :runnerId + """) + Optional joinMemberByRunnerId(@Param("runnerId") Long runnerId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runner/service/RunnerService.java b/backend/baton/src/main/java/touch/baton/domain/runner/service/RunnerService.java new file mode 100644 index 000000000..d1fa565bb --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runner/service/RunnerService.java @@ -0,0 +1,21 @@ +package touch.baton.domain.runner.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.exception.OldRunnerException; +import touch.baton.domain.runner.repository.RunnerRepository; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class RunnerService { + + private final RunnerRepository runnerRepository; + + public Runner readRunnerWithMember(final Long runnerId) { + return runnerRepository.joinMemberByRunnerId(runnerId) + .orElseThrow(() -> new OldRunnerException.NotFound("해당하는 식별자의 Runner 를 찾을 수 없습니다.")); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/RunnerPost.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/RunnerPost.java new file mode 100644 index 000000000..1a7ac575c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/RunnerPost.java @@ -0,0 +1,232 @@ +package touch.baton.domain.runnerpost; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.exception.OldRunnerPostException; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.RunnerPostTags; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class RunnerPost extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private Title title; + + @Embedded + private Contents contents; + + @Embedded + private PullRequestUrl pullRequestUrl; + + @Embedded + private Deadline deadline; + + @Embedded + private WatchedCount watchedCount; + + @Embedded + private ChattingCount chattingCount; + + @Enumerated(STRING) + @Column(nullable = false) + private ReviewStatus reviewStatus = ReviewStatus.NOT_STARTED; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "runner_id", foreignKey = @ForeignKey(name = "fk_runner_post_to_runner"), nullable = false) + private Runner runner; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "supporter_id", foreignKey = @ForeignKey(name = "fk_runner_post_to_supporter"), nullable = true) + private Supporter supporter; + + @Embedded + private RunnerPostTags runnerPostTags; + + @Builder + private RunnerPost(final Title title, + final Contents contents, + final PullRequestUrl pullRequestUrl, + final Deadline deadline, + final WatchedCount watchedCount, + final ChattingCount chattingCount, + final ReviewStatus reviewStatus, + final Runner runner, + final Supporter supporter, + final RunnerPostTags runnerPostTags + ) { + this(null, title, contents, pullRequestUrl, deadline, watchedCount, chattingCount, reviewStatus, runner, supporter, runnerPostTags); + } + + private RunnerPost(final Long id, + final Title title, + final Contents contents, + final PullRequestUrl pullRequestUrl, + final Deadline deadline, + final WatchedCount watchedCount, + final ChattingCount chattingCount, + final ReviewStatus reviewStatus, + final Runner runner, + final Supporter supporter, + final RunnerPostTags runnerPostTags + ) { + validateNotNull(title, contents, pullRequestUrl, deadline, watchedCount, chattingCount, reviewStatus, runner, runnerPostTags); + this.id = id; + this.title = title; + this.contents = contents; + this.pullRequestUrl = pullRequestUrl; + this.deadline = deadline; + this.watchedCount = watchedCount; + this.chattingCount = chattingCount; + this.reviewStatus = reviewStatus; + this.runner = runner; + this.supporter = supporter; + this.runnerPostTags = runnerPostTags; + } + + private void validateNotNull(final Title title, + final Contents contents, + final PullRequestUrl pullRequestUrl, + final Deadline deadline, + final WatchedCount watchedCount, + final ChattingCount chattingCount, + final ReviewStatus reviewStatus, + final Runner runner, + final RunnerPostTags runnerPostTags + ) { + if (Objects.isNull(title)) { + throw new OldRunnerPostException.NotNull("title 는 null 일 수 없습니다."); + } + + if (Objects.isNull(contents)) { + throw new OldRunnerPostException.NotNull("contents 는 null 일 수 없습니다."); + } + + if (Objects.isNull(pullRequestUrl)) { + throw new OldRunnerPostException.NotNull("pullRequestUrl 는 null 일 수 없습니다."); + } + + if (Objects.isNull(deadline)) { + throw new OldRunnerPostException.NotNull("deadline 는 null 일 수 없습니다."); + } + + if (Objects.isNull(watchedCount)) { + throw new OldRunnerPostException.NotNull("watchedCount 는 null 일 수 없습니다."); + } + + if (Objects.isNull(chattingCount)) { + throw new OldRunnerPostException.NotNull("chattingCount 는 null 일 수 없습니다."); + } + + if (Objects.isNull(reviewStatus)) { + throw new OldRunnerPostException.NotNull("reviewStatus 는 null 일 수 없습니다."); + } + + if (Objects.isNull(runner)) { + throw new OldRunnerPostException.NotNull("runner 는 null 일 수 없습니다."); + } + + if (Objects.isNull(runnerPostTags)) { + throw new OldRunnerPostException.NotNull("runnerPostTags 는 null 일 수 없습니다."); + } + } + + public static RunnerPost newInstance(final String title, + final String contents, + final String pullRequestUrl, + final LocalDateTime deadline, + final Runner runner + ) { + return RunnerPost.builder() + .title(new Title(title)) + .contents(new Contents(contents)) + .pullRequestUrl(new PullRequestUrl(pullRequestUrl)) + .deadline(new Deadline(deadline)) + .runner(runner) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .watchedCount(WatchedCount.zero()) + .reviewStatus(ReviewStatus.NOT_STARTED) + .chattingCount(ChattingCount.zero()) + .build(); + } + + public void addAllRunnerPostTags(final List postTags) { + runnerPostTags.addAll(postTags); + } + + public void appendRunnerPostTag(RunnerPostTag postTag) { + runnerPostTags.add(postTag); + } + + public void updateTitle(final Title title) { + this.title = title; + } + + public void updateContents(final Contents contents) { + this.contents = contents; + } + + public void updatePullRequestUrl(final PullRequestUrl pullRequestUrl) { + this.pullRequestUrl = pullRequestUrl; + } + + public void updateDeadLine(final Deadline deadline) { + this.deadline = deadline; + } + + public void assignSupporter(final Supporter supporter) { + this.supporter = supporter; + } + + public void increaseWatchedCount() { + this.watchedCount = watchedCount.increase(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RunnerPost that = (RunnerPost) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostController.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostController.java new file mode 100644 index 000000000..8e51835fc --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/RunnerPostController.java @@ -0,0 +1,115 @@ +package touch.baton.domain.runnerpost.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import org.springframework.web.util.UriComponentsBuilder; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.service.RunnerService; +import touch.baton.domain.runnerpost.controller.response.RunnerPostReadResponses; +import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; +import touch.baton.domain.runnerpost.service.RunnerPostService; +import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateTestRequest; +import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; + +import java.net.URI; +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/posts/runner") +@RestController +public class RunnerPostController { + + private final RunnerPostService runnerPostService; + private final RunnerService runnerService; + + @PostMapping + public ResponseEntity createRunnerPost(@Valid @RequestBody RunnerPostCreateRequest request) { + // TODO 07/19 로그인 기능 개발시 1L 변경 요망 + Runner runner = runnerService.readRunnerWithMember(1L); + + final Long savedId = runnerPostService.createRunnerPost(runner, request); + + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{id}") + .buildAndExpand(savedId) + .toUri(); + return ResponseEntity.created(redirectUri).build(); + } + + @PostMapping("/test") + public ResponseEntity createRunnerPostVersionTest(@Valid @RequestBody RunnerPostCreateTestRequest request) { + // TODO 07/19 로그인 기능 개발시 1L 변경 요망 + Runner runner = runnerService.readRunnerWithMember(1L); + + final Long savedId = runnerPostService.createRunnerPostTest(runner, request); + + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{id}") + .buildAndExpand(savedId) + .toUri(); + return ResponseEntity.created(redirectUri).build(); + } + + @GetMapping("/{runnerPostId}") + public ResponseEntity readByRunnerPostId(@PathVariable final Long runnerPostId) { + final RunnerPostResponse.Detail response + = RunnerPostResponse.Detail.from(runnerPostService.readByRunnerPostId(runnerPostId)); + + return ResponseEntity.ok(response); + } + + @GetMapping("/{runnerPostId}/test") + public ResponseEntity readByRunnerPostIdVersionTest(@PathVariable final Long runnerPostId) { + final RunnerPostResponse.DetailVersionTest response + = RunnerPostResponse.DetailVersionTest.fromVersionTest(runnerPostService.readByRunnerPostId(runnerPostId)); + + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{runnerPostId}") + public ResponseEntity deleteByRunnerPostId(@PathVariable final Long runnerPostId) { + runnerPostService.deleteByRunnerPostId(runnerPostId); + + return ResponseEntity.noContent().build(); + } + + @PutMapping("/{runnerPostId}") + public ResponseEntity update(@PathVariable final Long runnerPostId, + @Valid @RequestBody final RunnerPostUpdateRequest request + ) { + final Long updatedId = runnerPostService.updateRunnerPost(runnerPostId, request); + final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/posts/runner") + .path("/{runnerPostId}") + .buildAndExpand(updatedId) + .toUri(); + return ResponseEntity.created(redirectUri).build(); + } + + @GetMapping + public ResponseEntity readAllRunnerPosts() { + final List responses = runnerPostService.readAllRunnerPosts().stream() + .map(RunnerPostResponse.Simple::from) + .toList(); + + return ResponseEntity.ok(RunnerPostReadResponses.NoFiltering.from(responses)); + } + + @GetMapping("/test") + public ResponseEntity readAllRunnerPostsVersionTest() { + final List responses = runnerPostService.readAllRunnerPosts().stream() + .map(RunnerPostResponse.Simple::from) + .toList(); + + return ResponseEntity.ok(RunnerPostReadResponses.NoFiltering.from(responses)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostReadResponses.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostReadResponses.java new file mode 100644 index 000000000..fbf200187 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostReadResponses.java @@ -0,0 +1,13 @@ +package touch.baton.domain.runnerpost.controller.response; + +import java.util.List; + +public record RunnerPostReadResponses() { + + public record NoFiltering(List data) { + + public static NoFiltering from(final List data) { + return new NoFiltering(data); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostResponse.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostResponse.java new file mode 100644 index 000000000..44190f605 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerPostResponse.java @@ -0,0 +1,104 @@ +package touch.baton.domain.runnerpost.controller.response; + +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.ReviewStatus; + +import java.time.LocalDateTime; +import java.util.List; + +public record RunnerPostResponse() { + + public record Detail(Long runnerPostId, + String title, + String contents, + String pullRequestUrl, + LocalDateTime deadline, + Integer watchedCount, + Integer chattingCount, + ReviewStatus reviewStatus, + boolean isOwner, + RunnerProfileResponse.Detail runnerProfile, + List tags + ) { + + public static Detail from(final RunnerPost runnerPost) { + return new Detail( + runnerPost.getId(), + runnerPost.getTitle().getValue(), + runnerPost.getContents().getValue(), + runnerPost.getPullRequestUrl().getValue(), + runnerPost.getDeadline().getValue(), + runnerPost.getWatchedCount().getValue(), + runnerPost.getChattingCount().getValue(), + runnerPost.getReviewStatus(), + true, + RunnerProfileResponse.Detail.from(runnerPost.getRunner()), + convertToTags(runnerPost) + ); + } + } + + + public record DetailVersionTest(Long runnerPostId, + String title, + String contents, + String pullRequestUrl, + LocalDateTime deadline, + Integer watchedCount, + Integer chattingCount, + ReviewStatus reviewStatus, + RunnerProfileResponse.Detail runnerProfile, + SupporterResponseTestVersion.Simple supporterProfile, + boolean isOwner, + List tags + ) { + public static DetailVersionTest fromVersionTest(final RunnerPost runnerPost) { + return new DetailVersionTest( + runnerPost.getId(), + runnerPost.getTitle().getValue(), + runnerPost.getContents().getValue(), + runnerPost.getPullRequestUrl().getValue(), + runnerPost.getDeadline().getValue(), + runnerPost.getWatchedCount().getValue(), + runnerPost.getChattingCount().getValue(), + runnerPost.getReviewStatus(), + RunnerProfileResponse.Detail.from(runnerPost.getRunner()), + SupporterResponseTestVersion.Simple.fromTestVersion(runnerPost.getSupporter()), + true, + convertToTags(runnerPost) + ); + } + } + + public record Simple(Long runnerPostId, + String title, + LocalDateTime deadline, + int watchedCount, + int chattingCount, + String reviewStatus, + RunnerProfileResponse.Simple runnerProfile, + List tags + ) { + + public static Simple from(final RunnerPost runnerPost) { + return new Simple( + runnerPost.getId(), + runnerPost.getTitle().getValue(), + runnerPost.getDeadline().getValue(), + runnerPost.getWatchedCount().getValue(), + runnerPost.getChattingCount().getValue(), + runnerPost.getReviewStatus().name(), + RunnerProfileResponse.Simple.from(runnerPost.getRunner()), + convertToTags(runnerPost) + ); + } + } + + private static List convertToTags(final RunnerPost runnerPost) { + return runnerPost.getRunnerPostTags() + .getRunnerPostTags() + .stream() + .map(runnerPostTag -> runnerPostTag.getTag().getTagName().getValue()) + .toList(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerProfileResponse.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerProfileResponse.java new file mode 100644 index 000000000..e41dff18d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/RunnerProfileResponse.java @@ -0,0 +1,32 @@ +package touch.baton.domain.runnerpost.controller.response; + +import touch.baton.domain.runner.Runner; + +public record RunnerProfileResponse() { + + public record Detail(Long runnerId, + String name, + String company, + String imageUrl + ) { + + public static Detail from(final Runner runner) { + return new Detail( + runner.getId(), + runner.getMember().getMemberName().getValue(), + runner.getMember().getCompany().getValue(), + runner.getMember().getImageUrl().getValue() + ); + } + } + + public record Simple(String name, String imageUrl) { + + public static Simple from(final Runner runner) { + return new Simple( + runner.getMember().getMemberName().getValue(), + runner.getMember().getImageUrl().getValue() + ); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/SupporterResponseTestVersion.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/SupporterResponseTestVersion.java new file mode 100644 index 000000000..7d1d8f085 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/controller/response/SupporterResponseTestVersion.java @@ -0,0 +1,17 @@ +package touch.baton.domain.runnerpost.controller.response; + +import touch.baton.domain.supporter.Supporter; + +public record SupporterResponseTestVersion() { + + public record Simple(Long supporterId, String name) { + + public static Simple fromTestVersion(final Supporter supporter) { + return new Simple( + supporter.getId(), + supporter.getMember().getMemberName().getValue() + ); + } + } + +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/OldRunnerPostBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/OldRunnerPostBusinessException.java new file mode 100644 index 000000000..814fd2151 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/OldRunnerPostBusinessException.java @@ -0,0 +1,17 @@ +package touch.baton.domain.runnerpost.exception; + +import touch.baton.domain.runner.exception.OldRunnerException; + +public class OldRunnerPostBusinessException extends OldRunnerException { + + public OldRunnerPostBusinessException(final String message) { + super(message); + } + + public static class NotFound extends OldRunnerPostBusinessException { + + public NotFound(final String message) { + super(message); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/OldRunnerPostException.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/OldRunnerPostException.java new file mode 100644 index 000000000..0a6ad3908 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/OldRunnerPostException.java @@ -0,0 +1,15 @@ +package touch.baton.domain.runnerpost.exception; + +public class OldRunnerPostException extends RuntimeException { + + public OldRunnerPostException(final String message) { + super(message); + } + + public static class NotNull extends OldRunnerPostException { + + public NotNull(final String message) { + super(message); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostBusinessException.java new file mode 100644 index 000000000..da0d44096 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostBusinessException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.runnerpost.exception; + +import touch.baton.domain.common.exception.BusinessException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class RunnerPostBusinessException extends BusinessException { + + public RunnerPostBusinessException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostDomainException.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostDomainException.java new file mode 100644 index 000000000..4aa5d0663 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostDomainException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.runnerpost.exception; + +import touch.baton.domain.common.exception.DomainException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class RunnerPostDomainException extends DomainException { + + public RunnerPostDomainException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostRequestException.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostRequestException.java new file mode 100644 index 000000000..8418f5f49 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/RunnerPostRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.runnerpost.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class RunnerPostRequestException extends ClientRequestException { + + public RunnerPostRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/FutureValidator.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/FutureValidator.java new file mode 100644 index 000000000..64239752b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/FutureValidator.java @@ -0,0 +1,26 @@ +package touch.baton.domain.runnerpost.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +import java.time.LocalDateTime; + +public class FutureValidator implements ConstraintValidator { + + private ClientErrorCode errorCode; + + @Override + public void initialize(final ValidFuture constraintAnnotation) { + errorCode = constraintAnnotation.clientErrorCode(); + } + + @Override + public boolean isValid(final LocalDateTime value, final ConstraintValidatorContext context) { + if (value.isBefore(LocalDateTime.now())) { + throw new ClientRequestException(errorCode); + } + return true; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/MaxLengthValidator.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/MaxLengthValidator.java new file mode 100644 index 000000000..2acb27d41 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/MaxLengthValidator.java @@ -0,0 +1,26 @@ +package touch.baton.domain.runnerpost.exception.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class MaxLengthValidator implements ConstraintValidator { + + private ClientErrorCode errorCode; + private int max; + + @Override + public void initialize(final ValidMaxLength constraintAnnotation) { + errorCode = constraintAnnotation.clientErrorCode(); + max = constraintAnnotation.max(); + } + + @Override + public boolean isValid(final String value, final ConstraintValidatorContext context) { + if (value.length() > max) { + throw new ClientRequestException(errorCode); + } + return true; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/ValidFuture.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/ValidFuture.java new file mode 100644 index 000000000..2ff689d7b --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/ValidFuture.java @@ -0,0 +1,28 @@ +package touch.baton.domain.runnerpost.exception.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import touch.baton.domain.common.exception.ClientErrorCode; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, PARAMETER }) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = FutureValidator.class) +public @interface ValidFuture { + + String message() default "마감일은 오늘보다 과거일 수 없습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + ClientErrorCode clientErrorCode(); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/ValidMaxLength.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/ValidMaxLength.java new file mode 100644 index 000000000..7aaf02477 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/exception/validator/ValidMaxLength.java @@ -0,0 +1,30 @@ +package touch.baton.domain.runnerpost.exception.validator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import touch.baton.domain.common.exception.ClientErrorCode; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, PARAMETER}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = MaxLengthValidator.class) +public @interface ValidMaxLength { + + String message() default "길이가 잘못되었습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + + ClientErrorCode clientErrorCode(); + + int max(); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostRepository.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostRepository.java new file mode 100644 index 000000000..6ac0d8f49 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/repository/RunnerPostRepository.java @@ -0,0 +1,27 @@ +package touch.baton.domain.runnerpost.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.tag.RunnerPostTags; + +import java.util.List; +import java.util.Optional; + +public interface RunnerPostRepository extends JpaRepository { + + @Query(value = """ + select rp + from RunnerPost rp + join fetch Runner r on r.id = rp.runner.id + join fetch Member m on m.id = r.member.id + where rp.id = :runnerPostId + """) + Optional joinMemberByRunnerPostId(@Param("runnerPostId") final Long runnerPostId); + + List readByRunnerId(Long runnerId); + List readBySupporterId(Long supporterId); + Optional readByTitle(Title title); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostService.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostService.java new file mode 100644 index 000000000..86fab704e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/RunnerPostService.java @@ -0,0 +1,204 @@ +package touch.baton.domain.runnerpost.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.exception.OldRunnerPostBusinessException; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; +import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateTestRequest; +import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.exception.OldSupporterException; +import touch.baton.domain.supporter.repository.SupporterRepository; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.repository.RunnerPostTagRepository; +import touch.baton.domain.tag.repository.TagRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class RunnerPostService { + + private final RunnerPostRepository runnerPostRepository; + private final RunnerPostTagRepository runnerPostTagRepository; + private final TagRepository tagRepository; + private final SupporterRepository supporterRepository; + + @Transactional + public Long createRunnerPost(final Runner runner, final RunnerPostCreateRequest request) { + final RunnerPost runnerPost = toDomain(runner, request); + runnerPostRepository.save(runnerPost); + + List toSaveTags = new ArrayList<>(); + for (final String tagName : request.tags()) { + final Optional maybeTag = tagRepository.findByTagName(new TagName(tagName)); + + if (maybeTag.isEmpty()) { + final Tag savedTag = tagRepository.save(Tag.newInstance(tagName)); + toSaveTags.add(savedTag); + continue; + } + + final Tag presentTag = maybeTag.get(); + presentTag.increaseCount(); + toSaveTags.add(presentTag); + } + + final List postTags = toSaveTags.stream() + .map(tag -> RunnerPostTag.builder() + .tag(tag) + .runnerPost(runnerPost) + .build()) + .toList(); + + runnerPost.addAllRunnerPostTags(postTags); + return runnerPost.getId(); + } + + private RunnerPost toDomain(final Runner runner, final RunnerPostCreateRequest request) { + return RunnerPost.newInstance(request.title(), + request.contents(), + request.pullRequestUrl(), + request.deadline(), + runner); + } + + @Transactional + public Long createRunnerPostTest(final Runner runner, final RunnerPostCreateTestRequest request) { + final RunnerPost runnerPost = RunnerPost.newInstance(request.title(), + request.contents(), + request.pullRequestUrl(), + request.deadline(), + runner); + + if (Objects.nonNull(request.supporterId())) { + final Supporter supporter = supporterRepository.findById(request.supporterId()) + .orElseThrow(() -> new OldSupporterException.NotNull("서포터가 존재하지 않습니다.")); + runnerPost.assignSupporter(supporter); + } + + runnerPostRepository.save(runnerPost); + + List toSaveTags = new ArrayList<>(); + for (final String tagName : request.tags()) { + final Optional maybeTag = tagRepository.findByTagName(new TagName(tagName)); + + if (maybeTag.isEmpty()) { + final Tag savedTag = tagRepository.save(Tag.newInstance(tagName)); + toSaveTags.add(savedTag); + continue; + } + + final Tag presentTag = maybeTag.get(); + presentTag.increaseCount(); + toSaveTags.add(presentTag); + } + + final List postTags = toSaveTags.stream() + .map(tag -> RunnerPostTag.builder() + .tag(tag) + .runnerPost(runnerPost) + .build()) + .toList(); + + runnerPost.addAllRunnerPostTags(postTags); + return runnerPost.getId(); + } + + public RunnerPost readByRunnerPostId(final Long runnerPostId) { + runnerPostTagRepository.joinTagByRunnerPostId(runnerPostId); + final RunnerPost findRunnerPost = runnerPostRepository.joinMemberByRunnerPostId(runnerPostId) + .orElseThrow(() -> new OldRunnerPostBusinessException.NotFound("러너 게시글 식별자값으로 러너 게시글을 조회할 수 없습니다.")); + + findRunnerPost.increaseWatchedCount(); + + return findRunnerPost; + } + + @Transactional + public void deleteByRunnerPostId(final Long runnerPostId) { + final Optional maybeRunnerPost = runnerPostRepository.findById(runnerPostId); + if (maybeRunnerPost.isEmpty()) { + throw new OldRunnerPostBusinessException.NotFound("러너 게시글 식별자값으로 삭제할 러너 게시글이 존재하지 않습니다."); + } + + runnerPostTagRepository.joinTagByRunnerPostId(runnerPostId) + .stream() + .map(RunnerPostTag::getTag) + .forEach(Tag::decreaseCount); + + runnerPostRepository.deleteById(runnerPostId); + } + + @Transactional + public Long updateRunnerPost(final Long runnerPostId, final RunnerPostUpdateRequest request) { + // TODO: 메소드 분리 + final RunnerPost runnerPost = runnerPostRepository.findById(runnerPostId) + .orElseThrow(() -> new IllegalArgumentException("잘못된 runnerPostId 입니다.")); + runnerPost.updateTitle(new Title(request.title())); + runnerPost.updateContents(new Contents(request.contents())); + runnerPost.updatePullRequestUrl(new PullRequestUrl(request.pullRequestUrl())); + runnerPost.updateDeadLine(new Deadline(request.deadline())); + + final List presentRunnerPostTags = + runnerPostTagRepository.joinTagByRunnerPostId(runnerPost.getId()); + // TODO: tag 개수 차감 메소드 분리 + final List presentTags = presentRunnerPostTags.stream() + .map(RunnerPostTag::getTag) + .toList(); + presentTags.forEach(Tag::decreaseCount); + + // TODO: 새로운 tag 로 교체 메소드 분리 + final List removedRunnerPostTags = new ArrayList<>(presentRunnerPostTags); + for (String tagName : request.tags()) { + final Optional existRunnerPostTag = presentRunnerPostTags.stream() + .filter(presentRunnerPostTag -> presentRunnerPostTag.isSameTagName(tagName)) + .findFirst(); + if (existRunnerPostTag.isPresent()) { + removedRunnerPostTags.remove(existRunnerPostTag.get()); + existRunnerPostTag.get().getTag().increaseCount(); + } + if (existRunnerPostTag.isEmpty()) { + // TODO: tag 찾기 메소드 분리 + final Optional tag = tagRepository.findByTagName(new TagName(tagName)); + if (tag.isEmpty()) { + final Tag newTag = tagRepository.save(Tag.newInstance(tagName)); + final RunnerPostTag newRunnerPostTag = runnerPostTagRepository.save(RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(newTag) + .build()); + runnerPost.appendRunnerPostTag(newRunnerPostTag); + } + if (tag.isPresent()) { + tag.get().increaseCount(); + final RunnerPostTag newRunnerPostTag = runnerPostTagRepository.save(RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag.get()) + .build()); + runnerPost.appendRunnerPostTag(newRunnerPostTag); + } + } + } + runnerPostTagRepository.deleteAll(removedRunnerPostTags); + + return runnerPost.getId(); + } + + public List readAllRunnerPosts() { + return runnerPostRepository.findAll(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateRequest.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateRequest.java new file mode 100644 index 000000000..d97cd787c --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateRequest.java @@ -0,0 +1,24 @@ +package touch.baton.domain.runnerpost.service.dto; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.validator.ValidNotNull; +import touch.baton.domain.runnerpost.exception.validator.ValidFuture; +import touch.baton.domain.runnerpost.exception.validator.ValidMaxLength; + +import java.time.LocalDateTime; +import java.util.List; + +public record RunnerPostCreateRequest(@ValidNotNull(clientErrorCode = ClientErrorCode.TITLE_IS_NULL) + String title, + @ValidNotNull(clientErrorCode = ClientErrorCode.TAGS_ARE_NULL) + List tags, + @ValidNotNull(clientErrorCode = ClientErrorCode.PULL_REQUEST_URL_IS_NULL) + String pullRequestUrl, + @ValidNotNull(clientErrorCode = ClientErrorCode.DEADLINE_IS_NULL) + @ValidFuture(clientErrorCode = ClientErrorCode.PAST_DEADLINE) + LocalDateTime deadline, + @ValidNotNull(clientErrorCode = ClientErrorCode.CONTENTS_ARE_NULL) + @ValidMaxLength(clientErrorCode = ClientErrorCode.CONTENTS_OVERFLOW, max = 1000) + String contents +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateTestRequest.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateTestRequest.java new file mode 100644 index 000000000..2ccf42d03 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostCreateTestRequest.java @@ -0,0 +1,13 @@ +package touch.baton.domain.runnerpost.service.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public record RunnerPostCreateTestRequest(String title, + List tags, + String pullRequestUrl, + LocalDateTime deadline, + String contents, + Long supporterId +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostUpdateRequest.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostUpdateRequest.java new file mode 100644 index 000000000..9bfd636d2 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/service/dto/RunnerPostUpdateRequest.java @@ -0,0 +1,24 @@ +package touch.baton.domain.runnerpost.service.dto; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.validator.ValidNotNull; +import touch.baton.domain.runnerpost.exception.validator.ValidFuture; +import touch.baton.domain.runnerpost.exception.validator.ValidMaxLength; + +import java.time.LocalDateTime; +import java.util.List; + +public record RunnerPostUpdateRequest(@ValidNotNull(clientErrorCode = ClientErrorCode.TITLE_IS_NULL) + String title, + @ValidNotNull(clientErrorCode = ClientErrorCode.TAGS_ARE_NULL) + List tags, + @ValidNotNull(clientErrorCode = ClientErrorCode.PULL_REQUEST_URL_IS_NULL) + String pullRequestUrl, + @ValidNotNull(clientErrorCode = ClientErrorCode.DEADLINE_IS_NULL) + @ValidFuture(clientErrorCode = ClientErrorCode.PAST_DEADLINE) + LocalDateTime deadline, + @ValidNotNull(clientErrorCode = ClientErrorCode.CONTENTS_ARE_NULL) + @ValidMaxLength(clientErrorCode = ClientErrorCode.CONTENTS_OVERFLOW, max = 1000) + String contents +) { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/Deadline.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/Deadline.java new file mode 100644 index 000000000..81b7bc72e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/Deadline.java @@ -0,0 +1,33 @@ +package touch.baton.domain.runnerpost.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class Deadline { + + @Column(name = "deadline", nullable = false) + private LocalDateTime value; + + public Deadline(final LocalDateTime value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final LocalDateTime value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("deadline 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/PullRequestUrl.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/PullRequestUrl.java new file mode 100644 index 000000000..0c399c265 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/PullRequestUrl.java @@ -0,0 +1,34 @@ +package touch.baton.domain.runnerpost.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class PullRequestUrl { + + private static final int MAXIMUM_URL_LENGTH = 2083; + + @Column(name = "pull_request_url", nullable = false, length = MAXIMUM_URL_LENGTH) + private String value; + + public PullRequestUrl(final String value) { + validateNotNull(value); + this.value = value; + } + + private void validateNotNull(final String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("pull request url 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/ReviewStatus.java b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/ReviewStatus.java new file mode 100644 index 000000000..b39037766 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/runnerpost/vo/ReviewStatus.java @@ -0,0 +1,8 @@ +package touch.baton.domain.runnerpost.vo; + +public enum ReviewStatus { + + NOT_STARTED, + IN_PROGRESS, + DONE +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/Supporter.java b/backend/baton/src/main/java/touch/baton/domain/supporter/Supporter.java new file mode 100644 index 000000000..a54625e85 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/Supporter.java @@ -0,0 +1,133 @@ +package touch.baton.domain.supporter; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.BaseEntity; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.Introduction; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.supporter.exception.OldSupporterException; +import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.supporter.vo.StarCount; +import touch.baton.domain.technicaltag.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.SupporterTechnicalTags; + +import java.util.List; +import java.util.Objects; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Supporter extends BaseEntity { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private ReviewCount reviewCount; + + @Embedded + private StarCount starCount; + + @Embedded + private TotalRating totalRating; + + @Enumerated(STRING) + @Column(nullable = false) + private Grade grade; + + @Embedded + private Introduction introduction; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "fk_supporter_to_member"), nullable = false) + private Member member; + + @Embedded + private SupporterTechnicalTags supporterTechnicalTags; + + @Builder + private Supporter(final ReviewCount reviewCount, + final StarCount starCount, + final TotalRating totalRating, + final Grade grade, + final Introduction introduction, + final Member member, + final SupporterTechnicalTags supporterTechnicalTags + ) { + this(null, reviewCount, starCount, totalRating, grade, introduction, member, supporterTechnicalTags); + } + + private Supporter(final Long id, + final ReviewCount reviewCount, + final StarCount starCount, + final TotalRating totalRating, + final Grade grade, + final Introduction introduction, + final Member member, + final SupporterTechnicalTags supporterTechnicalTags + ) { + validateNotNull(reviewCount, starCount, totalRating, grade, member, supporterTechnicalTags); + this.id = id; + this.reviewCount = reviewCount; + this.starCount = starCount; + this.totalRating = totalRating; + this.grade = grade; + this.introduction = introduction; + this.member = member; + this.supporterTechnicalTags = supporterTechnicalTags; + } + + private void validateNotNull(final ReviewCount reviewCount, + final StarCount starCount, + final TotalRating totalRating, + final Grade grade, + final Member member, + final SupporterTechnicalTags supporterTechnicalTags + ) { + if (Objects.isNull(reviewCount)) { + throw new OldSupporterException.NotNull("reviewCount 는 null 일 수 없습니다."); + } + + if (Objects.isNull(starCount)) { + throw new OldSupporterException.NotNull("starCount 는 null 일 수 없습니다."); + } + + if (Objects.isNull(totalRating)) { + throw new OldSupporterException.NotNull("totalRating 은 null 일 수 없습니다."); + } + + if (Objects.isNull(grade)) { + throw new OldSupporterException.NotNull("grade 는 null 일 수 없습니다."); + } + + if (Objects.isNull(member)) { + throw new OldSupporterException.NotNull("member 는 null 일 수 없습니다."); + } + + if (Objects.isNull(supporterTechnicalTags)) { + throw new OldSupporterException.NotNull("supporterTechnicalTags 는 null 일 수 없습니다."); + } + } + + public void addAllSupporterTechnicalTags(final List supporterTechnicalTags) { + this.supporterTechnicalTags.addAll(supporterTechnicalTags); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/controller/SupporterController.java b/backend/baton/src/main/java/touch/baton/domain/supporter/controller/SupporterController.java new file mode 100644 index 000000000..2de7b66df --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/controller/SupporterController.java @@ -0,0 +1,29 @@ +package touch.baton.domain.supporter.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import touch.baton.domain.supporter.controller.response.SupporterReadResponses; +import touch.baton.domain.supporter.controller.response.SupporterResponse; +import touch.baton.domain.supporter.service.SupporterService; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/supporters") +@RestController +public class SupporterController { + + private final SupporterService supporterService; + + @GetMapping("/test") + public ResponseEntity readAll() { + final List responses = supporterService.readAllSupporters().stream() + .map(SupporterResponse.Detail::from) + .toList(); + + return ResponseEntity.ok(SupporterReadResponses.NoFiltering.from(responses)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/controller/response/SupporterReadResponses.java b/backend/baton/src/main/java/touch/baton/domain/supporter/controller/response/SupporterReadResponses.java new file mode 100644 index 000000000..b27231463 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/controller/response/SupporterReadResponses.java @@ -0,0 +1,13 @@ +package touch.baton.domain.supporter.controller.response; + +import java.util.List; + +public record SupporterReadResponses() { + + public record NoFiltering(List data) { + + public static NoFiltering from(final List data) { + return new SupporterReadResponses.NoFiltering(data); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/controller/response/SupporterResponse.java b/backend/baton/src/main/java/touch/baton/domain/supporter/controller/response/SupporterResponse.java new file mode 100644 index 000000000..ceea007dd --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/controller/response/SupporterResponse.java @@ -0,0 +1,26 @@ +package touch.baton.domain.supporter.controller.response; + +import touch.baton.domain.supporter.Supporter; + +public record SupporterResponse() { + + public record Detail(Long supporterId, + String name, + String company, + int reviewCount, + String githubUrl, + String introduction + ) { + + public static Detail from(final Supporter supporter) { + return new Detail( + supporter.getId(), + supporter.getMember().getMemberName().getValue(), + supporter.getMember().getCompany().getValue(), + supporter.getReviewCount().getValue(), + supporter.getMember().getGithubUrl().getValue(), + supporter.getIntroduction().getValue() + ); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/exception/OldSupporterException.java b/backend/baton/src/main/java/touch/baton/domain/supporter/exception/OldSupporterException.java new file mode 100644 index 000000000..af9897ad8 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/exception/OldSupporterException.java @@ -0,0 +1,15 @@ +package touch.baton.domain.supporter.exception; + +public class OldSupporterException extends RuntimeException { + + public OldSupporterException(final String message) { + super(message); + } + + public static class NotNull extends OldSupporterException { + + public NotNull(final String message) { + super(message); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterBusinessException.java new file mode 100644 index 000000000..3c26c35b5 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterBusinessException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.supporter.exception; + +import touch.baton.domain.common.exception.BusinessException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class SupporterBusinessException extends BusinessException { + + public SupporterBusinessException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterDomainException.java b/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterDomainException.java new file mode 100644 index 000000000..22cda7820 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterDomainException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.supporter.exception; + +import touch.baton.domain.common.exception.DomainException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class SupporterDomainException extends DomainException { + + public SupporterDomainException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterRequestException.java b/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterRequestException.java new file mode 100644 index 000000000..67b19cec9 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/exception/SupporterRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.supporter.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class SupporterRequestException extends ClientRequestException { + + public SupporterRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/repository/SupporterRepository.java b/backend/baton/src/main/java/touch/baton/domain/supporter/repository/SupporterRepository.java new file mode 100644 index 000000000..cd48a42f7 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/repository/SupporterRepository.java @@ -0,0 +1,7 @@ +package touch.baton.domain.supporter.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.supporter.Supporter; + +public interface SupporterRepository extends JpaRepository { +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/service/SupporterService.java b/backend/baton/src/main/java/touch/baton/domain/supporter/service/SupporterService.java new file mode 100644 index 000000000..be8eed7bd --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/service/SupporterService.java @@ -0,0 +1,21 @@ +package touch.baton.domain.supporter.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.repository.SupporterRepository; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class SupporterService { + + private final SupporterRepository supporterRepository; + + public List readAllSupporters() { + return supporterRepository.findAll(); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/vo/ReviewCount.java b/backend/baton/src/main/java/touch/baton/domain/supporter/vo/ReviewCount.java new file mode 100644 index 000000000..fe75db747 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/vo/ReviewCount.java @@ -0,0 +1,27 @@ +package touch.baton.domain.supporter.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class ReviewCount { + + private static final String DEFAULT_VALUE = "0"; + + @ColumnDefault(DEFAULT_VALUE) + @Column(name = "review_count") + private int value; + + public ReviewCount(final int value) { + this.value = value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/supporter/vo/StarCount.java b/backend/baton/src/main/java/touch/baton/domain/supporter/vo/StarCount.java new file mode 100644 index 000000000..0a3deff52 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/supporter/vo/StarCount.java @@ -0,0 +1,27 @@ +package touch.baton.domain.supporter.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class StarCount { + + private static final String DEFAULT_VALUE = "0"; + + @ColumnDefault(DEFAULT_VALUE) + @Column(name = "star_count", nullable = false) + private int value; + + public StarCount(final int value) { + this.value = value; + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTag.java b/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTag.java new file mode 100644 index 000000000..54c91c1ff --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTag.java @@ -0,0 +1,65 @@ +package touch.baton.domain.tag; + +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.tag.exception.OldTagException; + +import java.util.Objects; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class RunnerPostTag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "runner_post_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_runner_post_tag_to_runner_post")) + private RunnerPost runnerPost; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "tag_id", nullable = false, foreignKey = @ForeignKey(name = "fk_runner_post_tag_to_tag")) + private Tag tag; + + @Builder + private RunnerPostTag(final RunnerPost runnerPost, final Tag tag) { + this(null, runnerPost, tag); + } + + private RunnerPostTag(final Long id, final RunnerPost runnerPost, final Tag tag) { + validateNotNull(runnerPost, tag); + this.id = id; + this.runnerPost = runnerPost; + this.tag = tag; + } + + private void validateNotNull(final RunnerPost runnerPost, final Tag tag) { + if (Objects.isNull(runnerPost)) { + throw new OldTagException.NotNull("runnerPost 은 null 일 수 없습니다."); + } + + if (Objects.isNull(tag)) { + throw new OldTagException.NotNull("tag 은 null 일 수 없습니다."); + } + } + + public boolean isSameTagName(final String tagName) { + return tag.isSameTagName(tagName); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTags.java b/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTags.java new file mode 100644 index 000000000..bca856d89 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/RunnerPostTags.java @@ -0,0 +1,33 @@ +package touch.baton.domain.tag; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.CascadeType.PERSIST; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class RunnerPostTags { + + @OneToMany(mappedBy = "runnerPost", cascade = PERSIST, orphanRemoval = true) + private List runnerPostTags = new ArrayList<>(); + + public RunnerPostTags(final List runnerPostTags) { + this.runnerPostTags = runnerPostTags; + } + + public void add(final RunnerPostTag runnerPostTag) { + runnerPostTags.add(runnerPostTag); + } + + public void addAll(final List runnerPostTags) { + this.runnerPostTags.addAll(runnerPostTags); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/Tag.java b/backend/baton/src/main/java/touch/baton/domain/tag/Tag.java new file mode 100644 index 000000000..fed713660 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/Tag.java @@ -0,0 +1,74 @@ +package touch.baton.domain.tag; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.exception.OldTagException; +import touch.baton.domain.tag.vo.TagCount; + +import java.util.Objects; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Tag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private TagName tagName; + + @Embedded + private TagCount tagCount; + + @Builder + private Tag(final TagName tagName, final TagCount tagCount) { + this(null, tagName, tagCount); + } + + private Tag(final Long id, final TagName tagName, final TagCount tagCount) { + validateNotNull(tagName, tagCount); + this.id = id; + this.tagName = tagName; + this.tagCount = tagCount; + } + + private void validateNotNull(final TagName tagName, final TagCount tagCount) { + if (Objects.isNull(tagName)) { + throw new OldTagException.NotNull("tagName 은 null 일 수 없습니다."); + } + + if (Objects.isNull(tagCount)) { + throw new OldTagException.NotNull("tagCount 은 null 일 수 없습니다."); + } + } + + public static Tag newInstance(final String tagName) { + return Tag.builder() + .tagName(new TagName(tagName)) + .tagCount(TagCount.init()) + .build(); + } + + public void increaseCount() { + this.tagCount = tagCount.increase(); + } + + public void decreaseCount() { + this.tagCount = tagCount.decrease(); + } + + public boolean isSameTagName(final String tagName) { + return this.tagName.equals(new TagName(tagName)); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/exception/OldTagException.java b/backend/baton/src/main/java/touch/baton/domain/tag/exception/OldTagException.java new file mode 100644 index 000000000..c14a49fa0 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/exception/OldTagException.java @@ -0,0 +1,15 @@ +package touch.baton.domain.tag.exception; + +public class OldTagException extends RuntimeException { + + public OldTagException(final String message) { + super(message); + } + + public static class NotNull extends OldTagException { + + public NotNull(final String message) { + super(message); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagBusinessException.java b/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagBusinessException.java new file mode 100644 index 000000000..da9c41f83 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagBusinessException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.tag.exception; + +import touch.baton.domain.common.exception.BusinessException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class TagBusinessException extends BusinessException { + + public TagBusinessException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagDomainException.java b/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagDomainException.java new file mode 100644 index 000000000..807c499e2 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagDomainException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.tag.exception; + +import touch.baton.domain.common.exception.DomainException; +import touch.baton.domain.common.exception.ServerErrorCode; + +public class TagDomainException extends DomainException { + + public TagDomainException(final ServerErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagRequestException.java b/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagRequestException.java new file mode 100644 index 000000000..25b28d85d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/exception/TagRequestException.java @@ -0,0 +1,11 @@ +package touch.baton.domain.tag.exception; + +import touch.baton.domain.common.exception.ClientErrorCode; +import touch.baton.domain.common.exception.ClientRequestException; + +public class TagRequestException extends ClientRequestException { + + public TagRequestException(final ClientErrorCode errorCode) { + super(errorCode); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/repository/RunnerPostTagRepository.java b/backend/baton/src/main/java/touch/baton/domain/tag/repository/RunnerPostTagRepository.java new file mode 100644 index 000000000..a72bd070a --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/repository/RunnerPostTagRepository.java @@ -0,0 +1,19 @@ +package touch.baton.domain.tag.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import touch.baton.domain.tag.RunnerPostTag; + +import java.util.List; + +public interface RunnerPostTagRepository extends JpaRepository { + + @Query(""" + select rpt + from RunnerPostTag rpt + join fetch Tag tag on rpt.tag.id = tag.id + where rpt.runnerPost.id = :runnerPostId + """) + List joinTagByRunnerPostId(@Param("runnerPostId") final Long runnerPostId); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/repository/TagRepository.java b/backend/baton/src/main/java/touch/baton/domain/tag/repository/TagRepository.java new file mode 100644 index 000000000..958e52954 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/repository/TagRepository.java @@ -0,0 +1,12 @@ +package touch.baton.domain.tag.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.Tag; + +import java.util.Optional; + +public interface TagRepository extends JpaRepository { + + Optional findByTagName(final TagName tagName); +} diff --git a/backend/baton/src/main/java/touch/baton/domain/tag/vo/TagCount.java b/backend/baton/src/main/java/touch/baton/domain/tag/vo/TagCount.java new file mode 100644 index 000000000..7ab90528d --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/tag/vo/TagCount.java @@ -0,0 +1,39 @@ +package touch.baton.domain.tag.vo; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; + +import static lombok.AccessLevel.PROTECTED; + +@EqualsAndHashCode +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class TagCount { + + private static final String DEFAULT_VALUE = "1"; + + @ColumnDefault(DEFAULT_VALUE) + @Column(name = "tag_count", nullable = false) + private int value; + + public TagCount(final int value) { + this.value = value; + } + + public static TagCount init() { + return new TagCount(Integer.parseInt(DEFAULT_VALUE)); + } + + public TagCount increase() { + return new TagCount(value + 1); + } + + public TagCount decrease() { + return new TagCount(value - 1); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/SupporterTechnicalTag.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/SupporterTechnicalTag.java new file mode 100644 index 000000000..12568aebd --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/SupporterTechnicalTag.java @@ -0,0 +1,63 @@ +package touch.baton.domain.technicaltag; + +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.tag.exception.OldTagException; + +import java.util.Objects; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class SupporterTechnicalTag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "supporter_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_supporter_technical_tag_to_supporter")) + private Supporter supporter; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "technical_tag_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_supporter_technical_tag_to_technical_tag")) + private TechnicalTag technicalTag; + + @Builder + public SupporterTechnicalTag(final Supporter supporter, final TechnicalTag technicalTag) { + this(null, supporter, technicalTag); + } + + private SupporterTechnicalTag(final Long id, final Supporter supporter, final TechnicalTag technicalTag) { + validateNotNull(supporter, technicalTag); + this.id = id; + this.supporter = supporter; + this.technicalTag = technicalTag; + } + + private void validateNotNull(final Supporter supporter, final TechnicalTag technicalTag) { + if (Objects.isNull(supporter)) { + throw new OldTagException.NotNull("supporter 는 null 일 수 없습니다."); + } + + if (Objects.isNull(technicalTag)) { + throw new OldTagException.NotNull("technicalTag 는 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/SupporterTechnicalTags.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/SupporterTechnicalTags.java new file mode 100644 index 000000000..6b9e72ca2 --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/SupporterTechnicalTags.java @@ -0,0 +1,29 @@ +package touch.baton.domain.technicaltag; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.CascadeType.PERSIST; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Embeddable +public class SupporterTechnicalTags { + + @OneToMany(mappedBy = "supporter", cascade = PERSIST, orphanRemoval = true) + private List supporterTechnicalTags = new ArrayList<>(); + + public SupporterTechnicalTags(final List supporterTechnicalTags) { + this.supporterTechnicalTags = supporterTechnicalTags; + } + + public void addAll(final List supporterTechnicalTags) { + this.supporterTechnicalTags.addAll(supporterTechnicalTags); + } +} diff --git a/backend/baton/src/main/java/touch/baton/domain/technicaltag/TechnicalTag.java b/backend/baton/src/main/java/touch/baton/domain/technicaltag/TechnicalTag.java new file mode 100644 index 000000000..3daa9794e --- /dev/null +++ b/backend/baton/src/main/java/touch/baton/domain/technicaltag/TechnicalTag.java @@ -0,0 +1,46 @@ +package touch.baton.domain.technicaltag; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.exception.OldTagException; + +import java.util.Objects; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class TechnicalTag { + + @GeneratedValue(strategy = IDENTITY) + @Id + private Long id; + + @Embedded + private TagName tagName; + + @Builder + private TechnicalTag(final TagName tagName) { + this(null, tagName); + } + + private TechnicalTag(final Long id, final TagName tagName) { + validateNotNull(tagName); + this.id = id; + this.tagName = tagName; + } + + private void validateNotNull(final TagName tagName) { + if (Objects.isNull(tagName)) { + throw new OldTagException.NotNull("tagName 은 null 일 수 없습니다."); + } + } +} diff --git a/backend/baton/src/main/resources/application.yml b/backend/baton/src/main/resources/application.yml new file mode 100644 index 000000000..01ddffd26 --- /dev/null +++ b/backend/baton/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + flyway: + enabled: false + jpa: + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: create +logging: + level: + org.hibernate.SQL: debug + org.hibernate.orm.jdbc.bind: trace diff --git a/backend/baton/src/test/java/touch/baton/BatonApplicationTests.java b/backend/baton/src/test/java/touch/baton/BatonApplicationTests.java new file mode 100644 index 000000000..e0fd8f936 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/BatonApplicationTests.java @@ -0,0 +1,13 @@ +package touch.baton; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BatonApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/baton/src/test/java/touch/baton/TestBatonApplication.java b/backend/baton/src/test/java/touch/baton/TestBatonApplication.java new file mode 100644 index 000000000..3ef21f53e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/TestBatonApplication.java @@ -0,0 +1,22 @@ +package touch.baton; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.MySQLContainer; + +@TestConfiguration(proxyBeanMethods = false) +public class TestBatonApplication { + + @Bean + @ServiceConnection + MySQLContainer mysqlContainer() { + return new MySQLContainer<>("mysql:latest"); + } + + public static void main(String[] args) { + SpringApplication.from(BatonApplication::main).with(TestBatonApplication.class).run(args); + } + +} diff --git a/backend/baton/src/test/java/touch/baton/assure/common/AssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/common/AssuredSupport.java new file mode 100644 index 000000000..565d88e23 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/common/AssuredSupport.java @@ -0,0 +1,31 @@ +package touch.baton.assure.common; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +public class AssuredSupport { + + public static ExtractableResponse get(final String uri, final String pathParamName, final Long id) { + return RestAssured + .given().log().ifValidationFails() + .when().log().ifValidationFails() + .pathParam(pathParamName, id) + .get(uri) + .then().log().ifError() + .extract(); + } + + public static ExtractableResponse delete(final String uri, final String pathParamName, final Long id) { + return RestAssured + .given().log().ifValidationFails() + .when().log().ifValidationFails() + .contentType(APPLICATION_JSON_VALUE) + .pathParam(pathParamName, id) + .delete(uri) + .then().log().ifError() + .extract(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/fixture/MemberFixture.java b/backend/baton/src/test/java/touch/baton/assure/fixture/MemberFixture.java new file mode 100644 index 000000000..385b8a4e1 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/fixture/MemberFixture.java @@ -0,0 +1,32 @@ +package touch.baton.assure.fixture; + +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; + +public abstract class MemberFixture { + + private MemberFixture() { + } + + public static Member from(final String memberName, + final String email, + final String oauthId, + final String githubUrl, + final String company, + final String imageUrl + ) { + return Member.builder() + .memberName(new MemberName(memberName)) + .email(new Email(email)) + .oauthId(new OauthId(oauthId)) + .githubUrl(new GithubUrl(githubUrl)) + .company(new Company(company)) + .imageUrl(new ImageUrl(imageUrl)) + .build(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/fixture/RunnerFixture.java b/backend/baton/src/test/java/touch/baton/assure/fixture/RunnerFixture.java new file mode 100644 index 000000000..725cd06f4 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/fixture/RunnerFixture.java @@ -0,0 +1,23 @@ +package touch.baton.assure.fixture; + +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; + +public abstract class RunnerFixture { + + private RunnerFixture() { + } + + public static Runner from(final Member member, + final TotalRating totalRating, + final Grade grade + ) { + return Runner.builder() + .totalRating(totalRating) + .grade(grade) + .member(member) + .build(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/fixture/RunnerPostFixture.java b/backend/baton/src/test/java/touch/baton/assure/fixture/RunnerPostFixture.java new file mode 100644 index 000000000..1d9dee3d6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/fixture/RunnerPostFixture.java @@ -0,0 +1,45 @@ +package touch.baton.assure.fixture; + +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.tag.RunnerPostTags; + +import java.time.LocalDateTime; + +public abstract class RunnerPostFixture { + + private RunnerPostFixture() { + } + + public static RunnerPost from(final Runner runner, + final Supporter supporter, + final String title, + final String contents, + final String pullRequestUrl, + final LocalDateTime deadline, + final Integer watchedCount, + final Integer chattingRoomCount, + final RunnerPostTags runnerPostTags + ) { + return RunnerPost.builder() + .title(new Title(title)) + .contents(new Contents(contents)) + .pullRequestUrl(new PullRequestUrl(pullRequestUrl)) + .deadline(new Deadline(deadline)) + .watchedCount(new WatchedCount(watchedCount)) + .chattingCount(new ChattingCount(chattingRoomCount)) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(runner) + .supporter(supporter) + .runnerPostTags(runnerPostTags) + .build(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredDeleteTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredDeleteTest.java new file mode 100644 index 000000000..0d128df18 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredDeleteTest.java @@ -0,0 +1,52 @@ +package touch.baton.assure.runnerpost; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.fixture.MemberFixture; +import touch.baton.assure.fixture.RunnerFixture; +import touch.baton.assure.fixture.RunnerPostFixture; +import touch.baton.config.AssuredTestConfig; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.tag.RunnerPostTags; + +import java.time.LocalDateTime; +import java.util.Collections; + +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static touch.baton.fixture.vo.TotalRatingFixture.totalRating; + +class RunnerPostAssuredDeleteTest extends AssuredTestConfig { + + @Test + void 러너의_게시글_식별자값으로_러너_게시글_상세_정보_삭제에_성공한다() { + final Member member = MemberFixture.from("헤나", + "test@test.com", + "1jgiwng9213n0f1", + "https://", + "우아한테크코스", + "https://" + ); + memberRepository.save(member); + + final Runner runner = RunnerFixture.from(member, totalRating(0), Grade.BARE_FOOT); + runnerRepository.save(runner); + + final RunnerPost runnerPost = RunnerPostFixture.from(runner, + null, + "제 코드를 리뷰해주세요", + "제 코드의 내용은 이렇습니다.", + "https://", + LocalDateTime.now(), + 0, + 0, + new RunnerPostTags(Collections.emptyList()) + ); + runnerPostRepository.save(runnerPost); + + RunnerPostAssuredSupport + .클라이언트_요청().러너_게시글_식별자값으로_러너_게시글을_삭제한다(runnerPost.getId()) + .서버_응답().러너_게시글_삭제_성공을_검증한다(NO_CONTENT); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredReadTest.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredReadTest.java new file mode 100644 index 000000000..4c9c9d73e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredReadTest.java @@ -0,0 +1,49 @@ +package touch.baton.assure.runnerpost; + +import org.junit.jupiter.api.Test; +import touch.baton.assure.fixture.RunnerFixture; +import touch.baton.config.AssuredTestConfig; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; +import touch.baton.domain.runnerpost.controller.response.RunnerProfileResponse; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerPostFixture; + +import java.time.LocalDateTime; +import java.util.Collections; + +import static touch.baton.fixture.vo.ChattingCountFixture.chattingCount; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.TotalRatingFixture.totalRating; +import static touch.baton.fixture.vo.WatchedCountFixture.watchedCount; + +@SuppressWarnings("NonAsciiCharacters") +class RunnerPostAssuredReadTest extends AssuredTestConfig { + + @Test + void 러너의_게시글_식별자값으로_러너_게시글_상세_정보_조회에_성공한다() { + final Member memberHyena = memberRepository.save(MemberFixture.createHyena()); + final Runner runnerHyena = runnerRepository.save(RunnerFixture.from(memberHyena, totalRating(0), Grade.BARE_FOOT)); + final RunnerPost runnerPost = runnerPostRepository.save(RunnerPostFixture.create(runnerHyena, deadline(LocalDateTime.now().plusHours(100)))); + + RunnerPostAssuredSupport + .클라이언트_요청().러너_게시글_식별자값으로_러너_게시글을_조회한다(runnerPost.getId()) + .서버_응답().러너_게시글_단건_조회_성공을_검증한다(new RunnerPostResponse.Detail( + runnerPost.getId(), + runnerPost.getTitle().getValue(), + runnerPost.getContents().getValue(), + runnerPost.getPullRequestUrl().getValue(), + runnerPost.getDeadline().getValue(), + watchedCount(1).getValue(), + chattingCount(0).getValue(), + ReviewStatus.NOT_STARTED, + true, + RunnerProfileResponse.Detail.from(runnerHyena), + Collections.emptyList() + )); + } +} diff --git a/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredSupport.java b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredSupport.java new file mode 100644 index 000000000..20a381afb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/assure/runnerpost/RunnerPostAssuredSupport.java @@ -0,0 +1,72 @@ +package touch.baton.assure.runnerpost; + +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.HttpStatus; +import touch.baton.assure.common.AssuredSupport; +import touch.baton.domain.runnerpost.controller.response.RunnerPostResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +public class RunnerPostAssuredSupport { + + private RunnerPostAssuredSupport() { + } + + public static RunnerPostClientRequestBuilder 클라이언트_요청() { + return new RunnerPostClientRequestBuilder(); + } + + public static class RunnerPostClientRequestBuilder { + + private ExtractableResponse response; + + public RunnerPostClientRequestBuilder 러너_게시글_식별자값으로_러너_게시글을_조회한다(final Long 러너_게시글_식별자값) { + response = AssuredSupport.get("/api/v1/posts/runner/{runnerPostId}", "runnerPostId", 러너_게시글_식별자값); + return this; + } + + public RunnerPostClientRequestBuilder 러너_게시글_식별자값으로_러너_게시글을_삭제한다(final Long 러너_게시글_식별자값) { + response = AssuredSupport.delete("/api/v1/posts/runner/{runnerPostId}", "runnerPostId", 러너_게시글_식별자값); + return this; + } + + public RunnerPostServerResponseBuilder 서버_응답() { + return new RunnerPostServerResponseBuilder(response); + } + } + + public static class RunnerPostServerResponseBuilder { + + private final ExtractableResponse response; + + public RunnerPostServerResponseBuilder(final ExtractableResponse response) { + this.response = response; + } + + public void 러너_게시글_단건_조회_성공을_검증한다(final RunnerPostResponse.Detail 러너_게시글_응답) { + final RunnerPostResponse.Detail actual = this.response.as(RunnerPostResponse.Detail.class); + + assertSoftly(softly -> { + softly.assertThat(actual.title()).isEqualTo(러너_게시글_응답.title()); + softly.assertThat(actual.contents()).isEqualTo(러너_게시글_응답.contents()); + softly.assertThat(actual.tags()).isEqualTo(러너_게시글_응답.tags()); + softly.assertThat(actual.deadline()).isEqualToIgnoringSeconds(러너_게시글_응답.deadline()); + softly.assertThat(actual.runnerProfile().name()).isEqualTo(러너_게시글_응답.runnerProfile().name()); + softly.assertThat(actual.runnerProfile().company()).isEqualTo(러너_게시글_응답.runnerProfile().company()); + softly.assertThat(actual.runnerProfile().imageUrl()).isEqualTo(러너_게시글_응답.runnerProfile().imageUrl()); + softly.assertThat(actual.runnerProfile().runnerId()).isEqualTo(러너_게시글_응답.runnerProfile().runnerId()); + softly.assertThat(actual.chattingCount()).isEqualTo(러너_게시글_응답.chattingCount()); + softly.assertThat(actual.watchedCount()).isEqualTo(러너_게시글_응답.watchedCount()); + softly.assertThat(actual.runnerPostId()).isEqualTo(러너_게시글_응답.runnerPostId()); + } + ); + } + + public void 러너_게시글_삭제_성공을_검증한다(final HttpStatus HTTP_STATUS) { + assertThat(response.statusCode()) + .isEqualTo(HTTP_STATUS.value()); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java b/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java new file mode 100644 index 000000000..336ef9481 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/AssuredTestConfig.java @@ -0,0 +1,31 @@ +package touch.baton.config; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public abstract class AssuredTestConfig { + + @Autowired + protected MemberRepository memberRepository; + + @Autowired + protected RunnerRepository runnerRepository; + + @Autowired + protected RunnerPostRepository runnerPostRepository; + + @BeforeEach + void assuredTestSetUp(@LocalServerPort int port) { + RestAssured.port = port; + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/RepositoryTestConfig.java b/backend/baton/src/test/java/touch/baton/config/RepositoryTestConfig.java new file mode 100644 index 000000000..a366cfec7 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/RepositoryTestConfig.java @@ -0,0 +1,9 @@ +package touch.baton.config; + +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@Import(JpaConfig.class) +@DataJpaTest +public abstract class RepositoryTestConfig { +} diff --git a/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java b/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java new file mode 100644 index 000000000..105ae3770 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/RestdocsConfig.java @@ -0,0 +1,57 @@ +package touch.baton.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@ExtendWith(MockitoExtension.class) +@Import(RestDocsResultConfig.class) +@ExtendWith(RestDocumentationExtension.class) +public abstract class RestdocsConfig { + + protected MockMvc mockMvc; + + @Autowired + protected RestDocumentationResultHandler restDocs; + + @Autowired + protected ObjectMapper objectMapper; + + @BeforeEach + void restdocsSetUp(final WebApplicationContext webApplicationContext, + final RestDocumentationContextProvider restDocumentation + ) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation)) + .build(); + } +} + +@TestConfiguration +class RestDocsResultConfig { + + @Bean + RestDocumentationResultHandler restDocumentationResultHandler() { + return MockMvcRestDocumentation.document("{class-name}/{method-name}", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java b/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java new file mode 100644 index 000000000..88d257c96 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/ServiceTestConfig.java @@ -0,0 +1,30 @@ +package touch.baton.config; + +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; +import touch.baton.domain.supporter.repository.SupporterRepository; +import touch.baton.domain.tag.repository.RunnerPostTagRepository; +import touch.baton.domain.tag.repository.TagRepository; + +public abstract class ServiceTestConfig extends RepositoryTestConfig { + + @Autowired + protected MemberRepository memberRepository; + + @Autowired + protected RunnerRepository runnerRepository; + + @Autowired + protected RunnerPostRepository runnerPostRepository; + + @Autowired + protected RunnerPostTagRepository runnerPostTagRepository; + + @Autowired + protected TagRepository tagRepository; + + @Autowired + protected SupporterRepository supporterRepository; +} diff --git a/backend/baton/src/test/java/touch/baton/config/converter/ConverterConfigTest.java b/backend/baton/src/test/java/touch/baton/config/converter/ConverterConfigTest.java new file mode 100644 index 000000000..8ac5df301 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/converter/ConverterConfigTest.java @@ -0,0 +1,64 @@ +package touch.baton.config.converter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@WebMvcTest(ConverterConfig.class) +class ConverterConfigTest { + + @Autowired + private ObjectMapper objectMapper; + + @DisplayName("직렬화 할 때 LocalDateTime yyyy-MM-dd'T'HH:mm 형식으로 변경된다.") + @Test + void serializeStringDateToLocalDateTime() throws JsonProcessingException { + // given + final LocalDateTime expected = LocalDateTime.of(2023, 8, 15, 12, 13); + final LocalDateTime expectedWithSecond = LocalDateTime.of(2023, 8, 15, 12, 13, 12); + + // when + final String actual = objectMapper.writeValueAsString(expected); + final String actualWithSecond = objectMapper.writeValueAsString(expectedWithSecond); + + // then + assertAll( + () -> assertThat(actual).isEqualTo("\"2023-08-15T12:13\""), + () -> assertThat(actualWithSecond).isEqualTo("\"2023-08-15T12:13\"") + ); + } + + @DisplayName("역직렬화 할 때, yyyy-MM-dd'T'HH:mm 형식이 LocalDateTime 으로 변경된다.") + @Test + void success_deserializeLocalDateTimeToStringDate() throws JsonProcessingException { + // given + final String expected = "\"2023-08-15T12:13\""; + + // when + final LocalDateTime actual = objectMapper.readValue(expected, LocalDateTime.class); + + // then + assertThat(actual).isEqualTo(LocalDateTime.of(2023, 8, 15, 12, 13)); + } + + @DisplayName("역직렬화 할 때, 맞지 않는 형식의 날짜 String 이 들어오면 LocalDateTime 으로 변환이 실패한다.") + @ValueSource(strings = {"\"2023-08-15T12:13:45\"", "\"2023-08-15 12:13\"", "\"2023/08/15T12:13\"", "\"2023/08/15 12:13\""}) + @ParameterizedTest + void fail_deserializeLocalDateTimeToStringDate(final String expected) { + // when, then + assertThatThrownBy(() -> objectMapper.readValue(expected, LocalDateTime.class)) + .isInstanceOf(InvalidFormatException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/config/converter/StringDateToLocalDateTimeConverterTest.java b/backend/baton/src/test/java/touch/baton/config/converter/StringDateToLocalDateTimeConverterTest.java new file mode 100644 index 000000000..52309dafa --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/config/converter/StringDateToLocalDateTimeConverterTest.java @@ -0,0 +1,44 @@ +package touch.baton.config.converter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.TimeZone; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class StringDateToLocalDateTimeConverterTest { + + private static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm"; + private static final TimeZone KOREA_TIME_ZONE = TimeZone.getTimeZone("Asia/Seoul"); + + @DisplayName("String 값 Date 가 LocalDateTime 으로 변경되는지 확인한다.") + @Test + void success_convertStringDateToLocalDateTime() { + // given + final String expect = "2023-08-18T09:45"; + final StringDateToLocalDateTimeConverter converter = new StringDateToLocalDateTimeConverter(DEFAULT_DATE_TIME_FORMAT, KOREA_TIME_ZONE); + + // when + final LocalDateTime actual = converter.convert(expect); + + // then + assertThat(actual).isEqualTo(LocalDateTime.of(2023, 8, 18, 9, 45)); + } + + @DisplayName("형식에 맞지 않는 String 값 Date 는 convert 할 때 예외를 던진다.") + @ValueSource(strings = {"2023-08-18T09:45:12", "2023-08-18 09:45", "2023/08/18 09:45", "2023.08.18 09:45"}) + @ParameterizedTest + void fail_convertInvalidStringDateToLocalDateTime(final String expect) { + // given + final StringDateToLocalDateTimeConverter converter = new StringDateToLocalDateTimeConverter(DEFAULT_DATE_TIME_FORMAT, KOREA_TIME_ZONE); + + // when, then + assertThatThrownBy(() -> converter.convert(expect)).isInstanceOf(DateTimeParseException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadApiTest.java b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadApiTest.java new file mode 100644 index 000000000..1a67455f7 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/document/runnerpost/read/RunnerPostReadApiTest.java @@ -0,0 +1,79 @@ +package touch.baton.document.runnerpost.read; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import touch.baton.config.RestdocsConfig; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.service.RunnerService; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.controller.RunnerPostController; +import touch.baton.domain.runnerpost.service.RunnerPostService; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.tag.Tag; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.RunnerFixture; +import touch.baton.fixture.domain.RunnerPostFixture; +import touch.baton.fixture.domain.TagFixture; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static touch.baton.fixture.vo.DeadlineFixture.deadline; +import static touch.baton.fixture.vo.TagCountFixture.tagCount; +import static touch.baton.fixture.vo.TagNameFixture.tagName; + +@WebMvcTest(RunnerPostController.class) +class RunnerPostReadApiTest extends RestdocsConfig { + + @MockBean + private RunnerPostService runnerPostService; + + @MockBean + private RunnerService runnerService; + + @DisplayName("러너 게시글 전체 조회 API") + @Test + void readAllRunnerPosts() throws Exception { + // given + final Runner runner = RunnerFixture.createRunner(MemberFixture.createHyena()); + final Deadline deadline = deadline(LocalDateTime.now().plusHours(100)); + final Tag javaTag = TagFixture.create(tagName("자바"), tagCount(10)); + final RunnerPost runnerPost = RunnerPostFixture.create(runner, deadline, List.of(javaTag)); + final RunnerPost spyRunnerPost = spy(runnerPost); + + // when + given(spyRunnerPost.getId()).willReturn(1L); + given(runnerPostService.readAllRunnerPosts()).willReturn(List.of(spyRunnerPost)); + + // then + mockMvc.perform(get("/api/v1/posts/runner")) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andDo(restDocs.document( + responseFields( + fieldWithPath("data.[].runnerPostId").type(NUMBER).description("러너 게시글 식별자값(id)"), + fieldWithPath("data.[].title").type(STRING).description("러너 게시글의 제목"), + fieldWithPath("data.[].deadline").type(STRING).description("러너 게시글의 마감 기한"), + fieldWithPath("data.[].watchedCount").type(NUMBER).description("러너 게시글의 조회수"), + fieldWithPath("data.[].chattingCount").type(NUMBER).description("러너 게시글의 채팅수"), + fieldWithPath("data.[].reviewStatus").type(STRING).description("러너 게시글의 리뷰 상태"), + fieldWithPath("data.[].runnerProfile.name").type(STRING).description("러너 게시글의 러너 프로필 이름"), + fieldWithPath("data.[].runnerProfile.imageUrl").type(STRING).description("러너 게시글의 러너 프로필 이미지"), + fieldWithPath("data.[].tags.[]").type(ARRAY).description("러너 게시글의 태그 목록") + )) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/common/vo/ChattingCountTest.java b/backend/baton/src/test/java/touch/baton/domain/common/vo/ChattingCountTest.java new file mode 100644 index 000000000..15326bc52 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/common/vo/ChattingCountTest.java @@ -0,0 +1,19 @@ +package touch.baton.domain.common.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ChattingCountTest { + + @DisplayName("기본 채팅수를 가진 ChattingRoomCount 를 생성할 수 있다.") + @Test + void createDefaultChattingRoomCount() { + // given + final ChattingCount expected = ChattingCount.zero(); + + // when, then + assertThat(expected.getValue()).isEqualTo(0); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/common/vo/ContentsTest.java b/backend/baton/src/test/java/touch/baton/domain/common/vo/ContentsTest.java new file mode 100644 index 000000000..312f7a5ce --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/common/vo/ContentsTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.common.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ContentsTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new Contents(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/common/vo/TitleTest.java b/backend/baton/src/test/java/touch/baton/domain/common/vo/TitleTest.java new file mode 100644 index 000000000..a27af7c9f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/common/vo/TitleTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.common.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TitleTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new Title(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/common/vo/WatchedCountTest.java b/backend/baton/src/test/java/touch/baton/domain/common/vo/WatchedCountTest.java new file mode 100644 index 000000000..00cff711d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/common/vo/WatchedCountTest.java @@ -0,0 +1,32 @@ +package touch.baton.domain.common.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class WatchedCountTest { + + @DisplayName("기본 조회수를 가진 WatchedCount 를 생성할 수 있다.") + @Test + void createDefaultWatchedCount() { + // given + final WatchedCount expected = WatchedCount.zero(); + + // when, then + assertThat(expected.getValue()).isEqualTo(0); + } + + @DisplayName("조회수를 증가시킨다.") + @Test + void increase() { + // given + final WatchedCount watchedCount = WatchedCount.zero(); + + // when + final WatchedCount increasedWatchedCount = watchedCount.increase(); + + // then + assertThat(increasedWatchedCount).isEqualTo(new WatchedCount(1)); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/MemberTest.java b/backend/baton/src/test/java/touch/baton/domain/member/MemberTest.java new file mode 100644 index 000000000..0e7664850 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/member/MemberTest.java @@ -0,0 +1,121 @@ +package touch.baton.domain.member; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.member.exception.OldMemberException; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class MemberTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("https://")) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("이름에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_name_is_null() { + assertThatThrownBy(() -> Member.builder() + .memberName(null) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build() + ).isInstanceOf(OldMemberException.NotNull.class); + } + + @DisplayName("이메일에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_email_is_null() { + assertThatThrownBy(() -> Member.builder() + .memberName(new MemberName("에단")) + .email(null) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build() + ).isInstanceOf(OldMemberException.NotNull.class); + } + + @DisplayName("oauth id 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_oauth_id_is_null() { + assertThatThrownBy(() -> Member.builder() + .memberName(new MemberName("에단")) + .email(new Email("test@test.co.kr")) + .oauthId(null) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build() + ).isInstanceOf(OldMemberException.NotNull.class); + } + + @DisplayName("github url 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_github_url_is_null() { + assertThatThrownBy(() -> Member.builder() + .memberName(new MemberName("에단")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(null) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build() + ).isInstanceOf(OldMemberException.NotNull.class); + } + + @DisplayName("company 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_company_is_null() { + assertThatThrownBy(() -> Member.builder() + .memberName(new MemberName("에단")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(null) + .imageUrl(new ImageUrl("imageUrl")) + .build() + ).isInstanceOf(OldMemberException.NotNull.class); + } + + @DisplayName("imageUrl 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_imageUrl_is_null() { + assertThatThrownBy(() -> Member.builder() + .memberName(new MemberName("에단")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(null) + .build() + ).isInstanceOf(OldMemberException.NotNull.class); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/CompanyTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/CompanyTest.java new file mode 100644 index 000000000..e9312518e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/CompanyTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.member.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CompanyTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new Company(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/EmailTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/EmailTest.java new file mode 100644 index 000000000..6873af9f8 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/EmailTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.member.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class EmailTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new Email(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/GithubUrlTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/GithubUrlTest.java new file mode 100644 index 000000000..28c73ca0f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/GithubUrlTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.member.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GithubUrlTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new GithubUrl(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/ImageUrlTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/ImageUrlTest.java new file mode 100644 index 000000000..511c551c4 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/ImageUrlTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.member.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ImageUrlTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new ImageUrl(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/NameTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/NameTest.java new file mode 100644 index 000000000..062e90c46 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/NameTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.member.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class NameTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new MemberName(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/member/vo/OauthIdTest.java b/backend/baton/src/test/java/touch/baton/domain/member/vo/OauthIdTest.java new file mode 100644 index 000000000..20582fcc6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/member/vo/OauthIdTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.member.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OauthIdTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new OauthId(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runner/RunnerTest.java b/backend/baton/src/test/java/touch/baton/domain/runner/RunnerTest.java new file mode 100644 index 000000000..2cd1265f3 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runner/RunnerTest.java @@ -0,0 +1,79 @@ +package touch.baton.domain.runner; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.exception.OldRunnerException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RunnerTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + private final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build(); + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(member) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("totalRating 이 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_totalRating_is_null() { + assertThatThrownBy(() -> Runner.builder() + .totalRating(null) + .grade(Grade.BARE_FOOT) + .member(member) + .build() + ).isInstanceOf(OldRunnerException.NotNull.class); + } + + @DisplayName("grade 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_grade_is_null() { + assertThatThrownBy(() -> Runner.builder() + .totalRating(new TotalRating(100)) + .grade(null) + .member(member) + .build() + ).isInstanceOf(OldRunnerException.NotNull.class); + } + + @DisplayName("member 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_member_is_null() { + assertThatThrownBy(() -> Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(null) + .build() + ).isInstanceOf(OldRunnerException.NotNull.class); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runner/repository/RunnerRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/runner/repository/RunnerRepositoryTest.java new file mode 100644 index 000000000..763b9e2e4 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runner/repository/RunnerRepositoryTest.java @@ -0,0 +1,99 @@ +package touch.baton.domain.runner.repository; + +import org.junit.jupiter.api.BeforeEach; +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import touch.baton.config.JpaConfig; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@Import(JpaConfig.class) +@DataJpaTest +class RunnerRepositoryTest { + + private static final MemberName memberName = new MemberName("헤에디주"); + private static final Email email = new Email("test@test.co.kr"); + private static final OauthId oauthId = new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j"); + private static final GithubUrl githubUrl = new GithubUrl("github.com/hyena0608"); + private static final Company company = new Company("우아한형제들"); + private static final ImageUrl imageUrl = new ImageUrl("김석호"); + private static final TotalRating totalRating = new TotalRating(100); + private static final Grade grade = Grade.BARE_FOOT; + + @Autowired + private RunnerRepository runnerRepository; + + @Autowired + private MemberRepository memberRepository; + + private Member member; + private Runner runner; + + @BeforeEach + void setUp() { + member = Member.builder() + .memberName(memberName) + .email(email) + .oauthId(oauthId) + .githubUrl(githubUrl) + .company(company) + .imageUrl(imageUrl) + .build(); + memberRepository.save(member); + + runner = Runner.builder() + .totalRating(totalRating) + .grade(grade) + .member(member) + .build(); + } + + @DisplayName("Runner 를 Member 와 조인해서 조회할 수 있다.") + @Test + void findByIdJoinMember() { + // given + final Runner expected = runnerRepository.save(runner); + + // when + final Optional actual = runnerRepository.joinMemberByRunnerId(expected.getId()); + + // then + assertThat(actual).isPresent(); + final Member actualMember = actual.get().getMember(); + assertAll( + () -> assertThat(actualMember.getId()).isNotNull(), + () -> assertThat(actualMember.getMemberName()).isEqualTo(memberName), + () -> assertThat(actualMember.getCompany()).isEqualTo(company), + () -> assertThat(actualMember.getEmail()).isEqualTo(email), + () -> assertThat(actualMember.getOauthId()).isEqualTo(oauthId), + () -> assertThat(actualMember.getGithubUrl()).isEqualTo(githubUrl) + ); + } + + @DisplayName("식별자가 없으면 Optional.empty()가 반환된다.") + @Test + void findByIdJoinMember_if_id_is_not_exists() { + // when + final Optional actual = runnerRepository.joinMemberByRunnerId(999L); + + // then + assertThat(actual).isEmpty(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerServiceReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerServiceReadTest.java new file mode 100644 index 000000000..cb2936612 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runner/service/RunnerServiceReadTest.java @@ -0,0 +1,88 @@ +package touch.baton.domain.runner.service; + +import org.junit.jupiter.api.BeforeEach; +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import touch.baton.config.JpaConfig; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.exception.OldRunnerException; +import touch.baton.domain.runner.repository.RunnerRepository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Import(JpaConfig.class) +@DataJpaTest +class RunnerServiceReadTest { + + private RunnerService runnerService; + + @Autowired + private RunnerRepository runnerRepository; + + @Autowired + private MemberRepository memberRepository; + private static final MemberName memberName = new MemberName("헤에디주"); + private static final Email email = new Email("test@test.co.kr"); + private static final OauthId oauthId = new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j"); + private static final GithubUrl githubUrl = new GithubUrl("github.com/hyena0608"); + private static final Company company = new Company("우아한형제들"); + private static final ImageUrl imageUrl = new ImageUrl("홍혁준"); + private static final TotalRating totalRating = new TotalRating(100); + private static final Grade grade = Grade.BARE_FOOT; + + private Runner runner; + + @BeforeEach + void setUp() { + runnerService = new RunnerService(runnerRepository); + + final Member member = Member.builder() + .memberName(memberName) + .email(email) + .oauthId(oauthId) + .githubUrl(githubUrl) + .company(company) + .imageUrl(imageUrl) + .build(); + memberRepository.save(member); + + runner = Runner.builder() + .totalRating(totalRating) + .grade(grade) + .member(member) + .build(); + runnerRepository.save(runner); + } + + @DisplayName("Runner 를 Member 와 조인해서 조회할 수 있다.") + @Test + void success_readRunnerWithMember() { + // when + final Runner actual = runnerService.readRunnerWithMember(runner.getId()); + + // then + assertThat(actual).isEqualTo(runner); + } + + @DisplayName("식별자로 Runner 와 Member 를 조인해서 조회할 때, 식별자에 해당하는 데이터가 없으면 예외를 던진다.") + @Test + void fail_readRunnerWithMember_if_id_is_invalid() { + // when, then + assertThatThrownBy(() -> runnerService.readRunnerWithMember(999L)) + .isInstanceOf(OldRunnerException.NotFound.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostTest.java new file mode 100644 index 000000000..a31de36f2 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/RunnerPostTest.java @@ -0,0 +1,297 @@ +package touch.baton.domain.runnerpost; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.exception.OldRunnerPostException; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.supporter.vo.StarCount; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.RunnerPostTags; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.technicaltag.SupporterTechnicalTags; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RunnerPostTest { + + private final Member runnerMember = Member.builder() + .memberName(new MemberName("러너 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("ads7821iuqjkrhadsioh1f1r4efsoi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한테크코스")) + .imageUrl(new ImageUrl("김석호")) + .build(); + + private final Member supporterMember = Member.builder() + .memberName(new MemberName("서포터 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/pobi")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("김석호")) + .build(); + + private final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(runnerMember) + .build(); + + private final Supporter supporter = Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(new StarCount(10)) + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(supporterMember) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build(); + + @DisplayName("생성 테스트") + @Nested + class Create { + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> RunnerPost.builder() + .title(new Title("JPA 정복")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("supporterProfile 에 null 이 들어간 경우 아직 리뷰어 할당이 되지 않은 것이다.") + @Test + void success_if_supporter_is_null() { + assertThatCode(() -> RunnerPost.builder() + .title(new Title("아이")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .reviewStatus(ReviewStatus.NOT_STARTED ) + .runner(runner) + .supporter(null) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("title 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_title_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(null) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).isInstanceOf(OldRunnerPostException.NotNull.class); + } + + @DisplayName("contents 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_contents_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(new Title("헤나")) + .contents(null) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).isInstanceOf(OldRunnerPostException.NotNull.class); + } + + @DisplayName("pull request url 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_pullRequestUrl_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(new Title("하이하이")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(null) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).isInstanceOf(OldRunnerPostException.NotNull.class); + } + + @DisplayName("deadline 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_deadline_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(new Title("아이")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(null) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).isInstanceOf(OldRunnerPostException.NotNull.class); + } + + @DisplayName("watched count 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_watchedCount_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(new Title("아이")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(null) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).isInstanceOf(OldRunnerPostException.NotNull.class); + } + + @DisplayName("chatting room count 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_chattingRoomCount_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(new Title("아이")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(null) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).isInstanceOf(OldRunnerPostException.NotNull.class); + } + + @DisplayName("runner 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_runner_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(new Title("아이")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(null) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build() + ).isInstanceOf(OldRunnerPostException.NotNull.class); + } + + @DisplayName("runnerPostTags 에 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_runnerPostTags_is_null() { + assertThatThrownBy(() -> RunnerPost.builder() + .title(new Title("아이")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(supporter) + .runnerPostTags(null) + .build() + ).isInstanceOf(OldRunnerPostException.NotNull.class); + } + + @DisplayName("태그, 조회수, 채팅수가 초기화된 RunnerPost 를 생성할 수 있다.") + @Test + void createDefaultRunnerPost() { + // given + final String title = "JPA 리뷰 부탁 드려요."; + final String contents = "넘나 어려워요."; + final String pullRequestUrl = "https://github.com/cookienc"; + final LocalDateTime deadline = LocalDateTime.of(2099, 12, 12, 0, 0); + final RunnerPost runnerPost = RunnerPost.newInstance(title, contents, pullRequestUrl, deadline, runner); + + // when, then + assertAll( + () -> assertThat(runnerPost.getTitle()).isEqualTo(new Title(title)), + () -> assertThat(runnerPost.getContents()).isEqualTo(new Contents(contents)), + () -> assertThat(runnerPost.getPullRequestUrl()).isEqualTo(new PullRequestUrl(pullRequestUrl)), + () -> assertThat(runnerPost.getDeadline()).isEqualTo(new Deadline(deadline)), + () -> assertThat(runnerPost.getRunnerPostTags()).isNotNull(), + () -> assertThat(runnerPost.getChattingCount()).isEqualTo(new ChattingCount(0)), + () -> assertThat(runnerPost.getWatchedCount()).isEqualTo(new WatchedCount(0)) + ); + } + } + + @DisplayName("runnerPostTags 전체를 추가할 수 있다.") + @Test + void addAllRunnerPostTags() { + // given + final String title = "JPA 리뷰 부탁 드려요."; + final String contents = "넘나 어려워요."; + final String pullRequestUrl = "https://github.com/cookienc"; + final LocalDateTime deadline = LocalDateTime.of(2099, 12, 12, 0, 0); + final RunnerPost runnerPost = RunnerPost.newInstance(title, contents, pullRequestUrl, deadline, runner); + final RunnerPostTag java = RunnerPostTag.builder() + .tag(Tag.newInstance("Java")) + .runnerPost(runnerPost) + .build(); + final RunnerPostTag spring = RunnerPostTag.builder() + .tag(Tag.newInstance("Spring")) + .runnerPost(runnerPost) + .build(); + + // when + runnerPost.addAllRunnerPostTags(List.of(java, spring)); + + // then + assertThat(runnerPost.getRunnerPostTags().getRunnerPostTags()).hasSize(2); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/controller/RunnerPostControllerCreateTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/controller/RunnerPostControllerCreateTest.java new file mode 100644 index 000000000..07cca5e9f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/controller/RunnerPostControllerCreateTest.java @@ -0,0 +1,90 @@ +package touch.baton.domain.runnerpost.controller; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; + +import java.time.LocalDateTime; +import java.util.List; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +class RunnerPostControllerCreateTest { + + @Autowired + private RunnerRepository runnerRepository; + @Autowired + private MemberRepository memberRepository; + + @BeforeEach + void setUp(@LocalServerPort int port) { + RestAssured.port = port; + + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("홍혁준")) + .build(); + memberRepository.save(member); + final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(member) + .build(); + runnerRepository.save(runner); + } + + @Test + void 러너_게시글_등록에_성공한다() { + // given + final RunnerPostCreateRequest request = new RunnerPostCreateRequest("코드 리뷰 해주세요.", + List.of("Java", "Spring"), + "https://github.com/cookienc", + LocalDateTime.of(2099, 12, 12, 0, 0), + "싸게 부탁드려요." + ); + + // when + final ExtractableResponse response = given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when() + .post("/api/v1/posts/runner") + .then() + .log().all().extract(); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()), + () -> assertThat(response.header(HttpHeaders.LOCATION)).contains("/api/v1/posts/runner") + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/controller/read/RunnerPostControllerReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/controller/read/RunnerPostControllerReadTest.java new file mode 100644 index 000000000..89a3c80ba --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/controller/read/RunnerPostControllerReadTest.java @@ -0,0 +1,128 @@ +package touch.baton.domain.runnerpost.controller.read; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import touch.baton.config.JpaConfig; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.controller.RunnerPostController; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; +import touch.baton.domain.runnerpost.service.RunnerPostService; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.repository.SupporterRepository; +import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.supporter.vo.StarCount; +import touch.baton.domain.technicaltag.SupporterTechnicalTags; + +import java.util.ArrayList; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Import(JpaConfig.class) +@SpringBootTest +@AutoConfigureMockMvc +class RunnerPostControllerReadTest { + + @Autowired + ObjectMapper mapper; + + @Autowired + MockMvc mockMvc; + + @Autowired + private RunnerPostService runnerPostService; + + @Autowired + private RunnerPostController runnerPostController; + + @Autowired + private RunnerPostRepository runnerPostRepository; + @Autowired + private RunnerRepository runnerRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private SupporterRepository supporterRepository; + + private final Member runnerMember = Member.builder() + .memberName(new MemberName("러너 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("ads7821iuqjkrhadsioh1f1r4efsoi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한테크코스")) + .imageUrl(new ImageUrl("imageUrl")) + .build(); + + private final Member supporterMember = Member.builder() + .memberName(new MemberName("서포터 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/pobi")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build(); + + private final Runner runner = Runner.builder() + .totalRating(new TotalRating(2)) + .grade(Grade.BARE_FOOT) + .member(runnerMember) + .build(); + + private final Supporter supporter = Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(new StarCount(10)) + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(supporterMember) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build(); + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + mockMvc = MockMvcBuilders.standaloneSetup(runnerPostController).build(); + + memberRepository.save(runnerMember); + memberRepository.save(supporterMember); + runnerRepository.save(runner); + supporterRepository.save(supporter); + } + + @DisplayName("response 가 json 형식인지 테스트 한다.") + @Test + void read() throws Exception { + // given, when + MvcResult mvcResult = mockMvc.perform(get("/api/v1/posts/runner")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andReturn(); + + //then + String content = mvcResult.getResponse().getContentAsString(); + Assertions.assertNotNull(content); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostData.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostData.java new file mode 100644 index 000000000..b319341e9 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostData.java @@ -0,0 +1,150 @@ +package touch.baton.domain.runnerpost.repository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import touch.baton.config.JpaConfig; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.repository.SupporterRepository; +import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.supporter.vo.StarCount; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.RunnerPostTags; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.repository.RunnerPostTagRepository; +import touch.baton.domain.tag.repository.TagRepository; +import touch.baton.domain.technicaltag.SupporterTechnicalTags; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +@Import(JpaConfig.class) +@DataJpaTest +public class RunnerPostData { + + @Autowired + private RunnerPostTagRepository runnerPostTagRepository; + @Autowired + private RunnerPostRepository runnerPostRepository; + @Autowired + private TagRepository tagRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private RunnerRepository runnerRepository; + @Autowired + private SupporterRepository supporterRepository; + + protected Member runnerMember; + protected Member supporterMember; + protected Runner runner; + protected Supporter supporter; + protected RunnerPost runnerPost; + protected Tag tagJava; + protected Tag tagSpring; + protected RunnerPostTag runnerPostTagJava; + protected RunnerPostTag runnerPostTagSpring; + + private Member setRunnerMember() { + return Member.builder() + .memberName(new MemberName("러너 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("ads7821iuqjkrhadsioh1f1r4efsoi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한테크코스")) + .imageUrl(new ImageUrl("김석호")) + .build(); + } + + private Member setSupporterMember() { + return Member.builder() + .memberName(new MemberName("서포터 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/pobi")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("김석호")) + .build(); + } + + private Runner setRunner() { + return Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(runnerMember) + .build(); + } + + private Supporter setSupporter() { + return Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(new StarCount(10)) + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(supporterMember) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build(); + } + + private Tag setTag(final String name) { + return Tag.newInstance(name); + } + + private RunnerPost setRunnerPost() { + return RunnerPost.builder() + .title(new Title("JPA 정복")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.of(2023, 9, 1, 10, 10))) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build(); + } + + private RunnerPostTag setRunnerPostTag(final RunnerPost runnerPost, final Tag tag) { + return RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag) + .build(); + } + + protected void setData() { + runnerMember = memberRepository.save(setRunnerMember()); + supporterMember = memberRepository.save(setSupporterMember()); + runner = runnerRepository.save(setRunner()); + supporter = supporterRepository.save(setSupporter()); + runnerPost = runnerPostRepository.save(setRunnerPost()); + tagJava = tagRepository.save(setTag("java")); + tagSpring = tagRepository.save(setTag("spring")); + + runnerPostTagJava = runnerPostTagRepository.save(setRunnerPostTag(runnerPost, tagJava)); + runnerPostTagSpring = runnerPostTagRepository.save(setRunnerPostTag(runnerPost, tagSpring)); + runnerPost.appendRunnerPostTag(runnerPostTagJava); + runnerPost.appendRunnerPostTag(runnerPostTagSpring); + runnerPostRepository.saveAndFlush(runnerPost); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryDeleteTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryDeleteTest.java new file mode 100644 index 000000000..c70917346 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryDeleteTest.java @@ -0,0 +1,89 @@ +package touch.baton.domain.runnerpost.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.tag.RunnerPostTags; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class RunnerPostRepositoryDeleteTest extends RepositoryTestConfig { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RunnerRepository runnerRepository; + + @Autowired + private RunnerPostRepository runnerPostRepository; + + @DisplayName("RunnerPost 식별자값으로 RunnerPost 을 삭제한다.") + @Test + void success_deleteByRunnerPostId() { + // given + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("홍혁준")) + .build(); + final Member saveMember = memberRepository.saveAndFlush(member); + + final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(saveMember) + .build(); + final Runner saveRunner = runnerRepository.saveAndFlush(runner); + + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("제 코드 리뷰 좀 해주세요!!")) + .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .deadline(new Deadline(LocalDateTime.now())) + .pullRequestUrl(new PullRequestUrl("https://")) + .watchedCount(new WatchedCount(1)) + .chattingCount(new ChattingCount(1)) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(saveRunner) + .supporter(null) + .build(); + final Long saveRunnerPostId = runnerPostRepository.saveAndFlush(runnerPost).getId(); + + // when + runnerPostRepository.deleteById(saveRunnerPostId); + + final Optional maybeRunnerPost = runnerPostRepository.findById(saveRunnerPostId); + + // then + assertThat(maybeRunnerPost).isNotPresent(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryReadTest.java new file mode 100644 index 000000000..9200626d8 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/RunnerPostRepositoryReadTest.java @@ -0,0 +1,89 @@ +package touch.baton.domain.runnerpost.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.tag.RunnerPostTags; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class RunnerPostRepositoryReadTest extends RepositoryTestConfig { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RunnerRepository runnerRepository; + + @Autowired + private RunnerPostRepository runnerPostRepository; + + @DisplayName("RunnerPost 식별자값으로 RunnerPost 을 삭제한다.") + @Test + void success_deleteByRunnerPostId() { + // given + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("홍혁준")) + .build(); + final Member saveMember = memberRepository.saveAndFlush(member); + + final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(saveMember) + .build(); + final Runner saveRunner = runnerRepository.saveAndFlush(runner); + + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("제 코드 리뷰 좀 해주세요!!")) + .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .deadline(new Deadline(LocalDateTime.now())) + .pullRequestUrl(new PullRequestUrl("https://")) + .watchedCount(new WatchedCount(1)) + .chattingCount(new ChattingCount(1)) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(saveRunner) + .supporter(null) + .build(); + final Long saveRunnerPostId = runnerPostRepository.saveAndFlush(runnerPost).getId(); + + // when + runnerPostRepository.deleteById(saveRunnerPostId); + + final Optional maybeRunnerPost = runnerPostRepository.findById(saveRunnerPostId); + + // then + assertThat(maybeRunnerPost).isNotPresent(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/read/RunnerPostRepositoryReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/read/RunnerPostRepositoryReadTest.java new file mode 100644 index 000000000..c04f7495e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/repository/read/RunnerPostRepositoryReadTest.java @@ -0,0 +1,33 @@ +package touch.baton.domain.runnerpost.repository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.repository.RunnerPostTagRepository; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RunnerPostRepositoryTest extends RunnerPostData { + + @Autowired + private RunnerPostTagRepository runnerPostTagRepository; + + @BeforeEach + void setUp() { + super.setData(); + } + + @DisplayName("RunnerPost 식별자로 RunnerPostTag 목록을 조회할 때 Tag 가 있으면 조회된다.") + @Test + void findRunnerPostTagsById_exist() { + // when + final List expected = runnerPostTagRepository.joinTagByRunnerPostId(runnerPost.getId()); + + // then + assertThat(expected).containsExactly(runnerPostTagJava, runnerPostTagSpring); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerFixture.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerFixture.java new file mode 100644 index 000000000..9e957e38c --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerFixture.java @@ -0,0 +1,53 @@ +package touch.baton.domain.runnerpost.service; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import touch.baton.config.JpaConfig; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; + +@Import(JpaConfig.class) +@DataJpaTest +public abstract class RunnerFixture { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RunnerRepository runnerRepository; + + protected Member member; + protected Runner runner; + + @BeforeEach + void setUpFixture() { + member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("김석호")) + .build(); + runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(member) + .build(); + + memberRepository.save(member); + runnerRepository.save(runner); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceCreateTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceCreateTest.java new file mode 100644 index 000000000..dc124f381 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceCreateTest.java @@ -0,0 +1,80 @@ +package touch.baton.domain.runnerpost.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; +import touch.baton.domain.runnerpost.service.dto.RunnerPostCreateRequest; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.supporter.repository.SupporterRepository; +import touch.baton.domain.tag.repository.RunnerPostTagRepository; +import touch.baton.domain.tag.repository.TagRepository; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RunnerPostServiceCreateTest extends RunnerFixture { + + private static final String TITLE = "코드 리뷰 해주세요."; + private static final String TAG = "Java"; + private static final String OTHER_TAG = "Spring"; + private static final String PULL_REQUEST_URL = "https://github.com/cookienc"; + private static final LocalDateTime DEADLINE = LocalDateTime.of(2099, 12, 12, 0, 0); + private static final String CONTENTS = "싸게 부탁드려요."; + + private RunnerPostService runnerPostService; + + @Autowired + private RunnerPostRepository runnerPostRepository; + + @Autowired + private RunnerPostTagRepository runnerPostTagRepository; + + @Autowired + private TagRepository tagRepository; + + @Autowired + private SupporterRepository supporterRepository; + + @BeforeEach + void setUp() { + runnerPostService = new RunnerPostService(runnerPostRepository, runnerPostTagRepository, tagRepository, supporterRepository); + } + + @DisplayName("Runner post 저장에 성공한다.") + @Test + void success() { + // given + final RunnerPostCreateRequest request = new RunnerPostCreateRequest(TITLE, + List.of(TAG, OTHER_TAG), + PULL_REQUEST_URL, + DEADLINE, + CONTENTS); + + // when + final Long savedId = runnerPostService.createRunnerPost(runner, request); + + // then + assertThat(savedId).isNotNull(); + RunnerPost actual = runnerPostRepository.findById(savedId).get(); + assertAll( + () -> assertThat(actual.getTitle()).isEqualTo(new Title(TITLE)), + () -> assertThat(actual.getContents()).isEqualTo(new Contents(CONTENTS)), + () -> assertThat(actual.getPullRequestUrl()).isEqualTo(new PullRequestUrl(PULL_REQUEST_URL)), + () -> assertThat(actual.getDeadline()).isEqualTo(new Deadline(DEADLINE)), + () -> assertThat(actual.getWatchedCount()).isEqualTo(new WatchedCount(0)), + () -> assertThat(actual.getChattingCount()).isEqualTo(new ChattingCount(0)), + () -> assertThat(actual.getRunner()).isEqualTo(runner) + ); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceDeleteTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceDeleteTest.java new file mode 100644 index 000000000..1d946ed0b --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceDeleteTest.java @@ -0,0 +1,105 @@ +package touch.baton.domain.runnerpost.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.ServiceTestConfig; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.exception.OldRunnerPostBusinessException; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.RunnerPostTags; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagCount; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RunnerPostServiceDeleteTest extends ServiceTestConfig { + + private RunnerPostService runnerPostService; + + @BeforeEach + void setUp() { + runnerPostService = new RunnerPostService(runnerPostRepository, runnerPostTagRepository, tagRepository, supporterRepository); + } + + @Disabled + @DisplayName("RunnerPost 식별자값으로 RunnerPost 을 삭제한다.") + @Test + void success_deleteByRunnerPostId() { + // given + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .build(); + memberRepository.saveAndFlush(member); + + final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(member) + .build(); + runnerRepository.saveAndFlush(runner); + + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("제 코드 리뷰 좀 해주세요!!")) + .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .deadline(new Deadline(LocalDateTime.now())) + .pullRequestUrl(new PullRequestUrl("https://")) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .runner(runner) + .supporter(null) + .build(); + final Long saveRunnerPostId = runnerPostRepository.saveAndFlush(runnerPost).getId(); + + final Tag tag = Tag.builder() + .tagName(new TagName("자바")) + .tagCount(new TagCount(1)) + .build(); + tagRepository.save(tag); + + final RunnerPostTag runnerPostTag = RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag) + .build(); + runnerPostTagRepository.save(runnerPostTag); + + // when + runnerPostService.deleteByRunnerPostId(saveRunnerPostId); + + // then + assertThatThrownBy(() -> runnerPostService.readByRunnerPostId(saveRunnerPostId)) + .isInstanceOf(OldRunnerPostBusinessException.NotFound.class); + } + + @DisplayName("RunnerPost 식별자값으로 존재하지 않는 RunnerPost 을 삭제 시도할 경우 예외가 발생한다.") + @Test + void fail_deleteByRunnerPostId_if_runnerPost_is_null() { + assertThatThrownBy(() -> runnerPostService.readByRunnerPostId(0L)) + .isInstanceOf(OldRunnerPostBusinessException.NotFound.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceReadTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceReadTest.java new file mode 100644 index 000000000..720693550 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceReadTest.java @@ -0,0 +1,112 @@ +package touch.baton.domain.runnerpost.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.config.ServiceTestConfig; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.exception.OldRunnerPostBusinessException; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.RunnerPostTags; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagCount; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RunnerPostServiceReadTest extends ServiceTestConfig { + + private RunnerPostService runnerPostService; + + @BeforeEach + void setUp() { + runnerPostService = new RunnerPostService(runnerPostRepository, runnerPostTagRepository, tagRepository, supporterRepository); + } + + @DisplayName("RunnerPost 식별자값으로 RunnerPost 를 조회한다.") + @Test + void success_findByRunnerPostId() { + // given + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("홍혁준")) + .build(); + memberRepository.save(member); + + final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(member) + .build(); + runnerRepository.save(runner); + + final LocalDateTime deadline = LocalDateTime.now(); + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("제 코드 리뷰 좀 해주세요!!")) + .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .deadline(new Deadline(deadline)) + .pullRequestUrl(new PullRequestUrl("https://")) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(runner) + .supporter(null) + .build(); + runnerPostRepository.save(runnerPost); + + final Tag tag = Tag.builder() + .tagName(new TagName("자바")) + .tagCount(new TagCount(1)) + .build(); + tagRepository.save(tag); + + final RunnerPostTag runnerPostTag = RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag) + .build(); + runnerPost.addAllRunnerPostTags(List.of(runnerPostTag)); + + // when + final RunnerPost findRunnerPost = runnerPostService.readByRunnerPostId(runnerPost.getId()); + + // then + assertThat(findRunnerPost) + .usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(runnerPost); + } + + @DisplayName("RunnerPost 식별자값으로 존재하지 않는 RunnerPost 을 조회할 경우 예외가 발생한다.") + @Test + void fail_findByRunnerPostId_if_runner_post_is_null() { + assertThatThrownBy(() -> runnerPostService.readByRunnerPostId(0L)) + .isInstanceOf(OldRunnerPostBusinessException.NotFound.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceUpdateTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceUpdateTest.java new file mode 100644 index 000000000..e8b387b6a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/service/RunnerPostServiceUpdateTest.java @@ -0,0 +1,82 @@ +package touch.baton.domain.runnerpost.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.RunnerPostData; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; +import touch.baton.domain.runnerpost.service.dto.RunnerPostUpdateRequest; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.supporter.repository.SupporterRepository; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.repository.RunnerPostTagRepository; +import touch.baton.domain.tag.repository.TagRepository; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RunnerPostServiceUpdateTest extends RunnerPostData { + + private static final String TITLE = "코드 리뷰 해주세요."; + private static final String TAG = "java"; + private static final String OTHER_TAG = "spring"; + private static final String PULL_REQUEST_URL = "https://github.com/shb03323"; + private static final LocalDateTime DEADLINE = LocalDateTime.now().plusHours(100); + private static final String CONTENTS = "싸게 부탁드려요."; + + private RunnerPostService runnerPostService; + + @Autowired + private RunnerPostRepository runnerPostRepository; + + @Autowired + private RunnerPostTagRepository runnerPostTagRepository; + + @Autowired + private TagRepository tagRepository; + + @Autowired + private SupporterRepository supporterRepository; + + @BeforeEach + void setUp() { + super.setData(); + runnerPostService = new RunnerPostService(runnerPostRepository, runnerPostTagRepository, tagRepository, supporterRepository); + } + + @DisplayName("Runner Post 수정에 성공한다.") + @Test + void success() { + // given + final RunnerPostUpdateRequest request = new RunnerPostUpdateRequest( + TITLE, List.of(TAG, OTHER_TAG), PULL_REQUEST_URL, DEADLINE, CONTENTS); + + // when + final Long savedId = runnerPostService.updateRunnerPost(runnerPost.getId(), request); + + // then + assertThat(savedId).isNotNull(); + RunnerPost actual = runnerPostRepository.findById(savedId).get(); + assertAll( + () -> assertThat(actual.getTitle()).isEqualTo(new Title(TITLE)), + () -> assertThat(actual.getContents()).isEqualTo(new Contents(CONTENTS)), + () -> assertThat(actual.getPullRequestUrl()).isEqualTo(new PullRequestUrl(PULL_REQUEST_URL)), + () -> assertThat(actual.getDeadline()).isEqualTo(new Deadline(request.deadline())) + ); + + final List runnerPostTags = runnerPostTagRepository.joinTagByRunnerPostId(savedId); + assertThat( + runnerPostTags.stream() + .map(runnerPostTag -> runnerPostTag.getTag().getTagName().getValue()) + .toList() + ).containsExactly(TAG, OTHER_TAG); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/DeadlineTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/DeadlineTest.java new file mode 100644 index 000000000..644b60bbb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/DeadlineTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DeadlineTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new Deadline(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/PullRequestUrlTest.java b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/PullRequestUrlTest.java new file mode 100644 index 000000000..86f583eff --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/runnerpost/vo/PullRequestUrlTest.java @@ -0,0 +1,16 @@ +package touch.baton.domain.runnerpost.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PullRequestUrlTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new PullRequestUrl(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/supporter/SupporterTest.java b/backend/baton/src/test/java/touch/baton/domain/supporter/SupporterTest.java new file mode 100644 index 000000000..19268e8de --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/supporter/SupporterTest.java @@ -0,0 +1,106 @@ +package touch.baton.domain.supporter; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.supporter.exception.OldSupporterException; +import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.supporter.vo.StarCount; +import touch.baton.domain.technicaltag.SupporterTechnicalTags; + +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SupporterTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + private final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build(); + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(new StarCount(10)) + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(member) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("startCount 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_startCount_is_null() { + assertThatThrownBy(() -> Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(null) + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(member) + .build() + ).isInstanceOf(OldSupporterException.NotNull.class); + } + + @DisplayName("totalRating 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_totalRating_is_null() { + assertThatThrownBy(() -> Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(new StarCount(10)) + .totalRating(null) + .grade(Grade.BARE_FOOT) + .member(member) + .build() + ).isInstanceOf(OldSupporterException.NotNull.class); + } + + @DisplayName("grade 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_grade_is_null() { + assertThatThrownBy(() -> Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(new StarCount(10)) + .totalRating(new TotalRating(100)) + .grade(null) + .member(member) + .build() + ).isInstanceOf(OldSupporterException.NotNull.class); + } + + @DisplayName("member 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_member_is_null() { + assertThatThrownBy(() -> Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(new StarCount(10)) + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(null) + .build() + ).isInstanceOf(OldSupporterException.NotNull.class); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagTest.java new file mode 100644 index 000000000..02b162ff0 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagTest.java @@ -0,0 +1,125 @@ +package touch.baton.domain.tag; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.supporter.vo.StarCount; +import touch.baton.domain.tag.exception.OldTagException; +import touch.baton.domain.tag.vo.TagCount; +import touch.baton.domain.technicaltag.SupporterTechnicalTags; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RunnerPostTagTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + private final Member runnerMember = Member.builder() + .memberName(new MemberName("러너 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("ads7821iuqjkrhadsioh1f1r4efsoi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한테크코스")) + .imageUrl(new ImageUrl("imageUrl")) + .build(); + + private final Member supporterMember = Member.builder() + .memberName(new MemberName("서포터 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/pobi")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("imageUrl")) + .build(); + + private final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(runnerMember) + .build(); + + private final Supporter supporter = Supporter.builder() + .reviewCount(new ReviewCount(10)) + .starCount(new StarCount(10)) + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(supporterMember) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build(); + + private final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("JPA 정복")) + .contents(new Contents("김영한 짱짱맨")) + .pullRequestUrl(new PullRequestUrl("https://github.com/woowacourse-teams/2023-baton/pull/17")) + .deadline(new Deadline(LocalDateTime.now())) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build(); + + private Tag tag = Tag.builder() + .tagName(new TagName("자바")) + .tagCount(new TagCount(0)) + .build(); + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("runner post 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_runnerPost_is_null() { + assertThatThrownBy(() -> RunnerPostTag.builder() + .runnerPost(null) + .tag(tag) + .build() + ).isInstanceOf(OldTagException.NotNull.class); + } + + @DisplayName("tag 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_tag_is_null() { + assertThatThrownBy(() -> RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(null) + .build() + ).isInstanceOf(OldTagException.NotNull.class); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagsTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagsTest.java new file mode 100644 index 000000000..210e0a3fb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/RunnerPostTagsTest.java @@ -0,0 +1,55 @@ +package touch.baton.domain.tag; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RunnerPostTagsTest { + + @DisplayName("RunnerPostTags 에 runnerPostTag 를 추가할 수 있다.") + @Test + void addAllRunnerPostTags() { + // given + RunnerPostTags postTags = new RunnerPostTags(); + Member member = Member.builder() + .memberName(new MemberName("러너 사용자")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("ads7821iuqjkrhadsioh1f1r4efsoi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한테크코스")) + .imageUrl(new ImageUrl("김석호")) + .build(); + Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(member) + .build(); + final RunnerPost runnerpost = RunnerPost.newInstance("리뷰해주세요.", "제발요.", "https://github.com/cookienc", LocalDateTime.of(2099, 12, 12, 0, 0), runner); + + final RunnerPostTag runnerPostTag = RunnerPostTag.builder() + .runnerPost(runnerpost) + .tag(Tag.newInstance("Java")) + .build(); + + // when + postTags.addAll(List.of(runnerPostTag)); + + // then + assertThat(postTags.getRunnerPostTags()).hasSize(1); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/TagTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/TagTest.java new file mode 100644 index 000000000..2498f7890 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/TagTest.java @@ -0,0 +1,80 @@ +package touch.baton.domain.tag; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.exception.OldTagException; +import touch.baton.domain.tag.vo.TagCount; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; + +class TagTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> Tag.builder() + .tagName(new TagName("자바")) + .tagCount(new TagCount(0)) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("tag name 이 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_tagName_is_null() { + assertThatThrownBy(() -> Tag.builder() + .tagName(null) + .tagCount(new TagCount(0)) + .build() + ).isInstanceOf(OldTagException.NotNull.class); + } + + @DisplayName("tag count 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_tagCount_is_null() { + assertThatThrownBy(() -> Tag.builder() + .tagName(new TagName("자바")) + .tagCount(null) + .build() + ).isInstanceOf(OldTagException.NotNull.class); + } + } + + @DisplayName("기본 count 를 가진 tag 를 생성할 수 있다.") + @Test + void createDefaultTag() { + // given + final String tagName = "Java"; + final Tag tag = Tag.newInstance(tagName); + + // when, then + assertAll( + () -> assertThat(tag.getTagName()).isEqualTo(new TagName(tagName)), + () -> assertThat(tag.getTagCount()).isEqualTo(new TagCount(1)) + ); + } + + @DisplayName("Tag 의 count는 1개씩 증가한다.") + @Test + void increaseCount() { + // given + final Tag tag = Tag.newInstance("Java"); + + // when + tag.increaseCount(); + + // then + assertAll( + () -> assertThat(tag.getTagName()).isEqualTo(new TagName("Java")), + () -> assertThat(tag.getTagCount()).isEqualTo(new TagCount(2)) + ); + + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/repository/RunnerPostTagRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/repository/RunnerPostTagRepositoryTest.java new file mode 100644 index 000000000..b98e590ee --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/repository/RunnerPostTagRepositoryTest.java @@ -0,0 +1,156 @@ +package touch.baton.domain.tag.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import touch.baton.config.RepositoryTestConfig; +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.RunnerPostTags; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagCount; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RunnerPostTagRepositoryTest extends RepositoryTestConfig { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private RunnerRepository runnerRepository; + + @Autowired + private RunnerPostRepository runnerPostRepository; + + @Autowired + private RunnerPostTagRepository runnerPostTagRepository; + + @Autowired + private TagRepository tagRepository; + + @DisplayName("RunnerPostTag 의 식별자값 목록으로 Tag 목록을 조회한다.") + @Test + void success_joinTagByRunnerPostIds() { + // given + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("김석호")) + .build(); + final Member saveMember = memberRepository.saveAndFlush(member); + + final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(saveMember) + .build(); + final Runner saveRunner = runnerRepository.saveAndFlush(runner); + + final LocalDateTime deadline = LocalDateTime.now(); + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("제 코드 리뷰 좀 해주세요!!")) + .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .deadline(new Deadline(deadline)) + .pullRequestUrl(new PullRequestUrl("https://")) + .watchedCount(new WatchedCount(1)) + .chattingCount(new ChattingCount(1)) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(saveRunner) + .supporter(null) + .build(); + runnerPostRepository.saveAndFlush(runnerPost); + + final Tag tag = Tag.builder() + .tagName(new TagName("자바")) + .tagCount(new TagCount(1)) + .build(); + tagRepository.save(tag); + + final RunnerPostTag runnerPostTag = RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag) + .build(); + runnerPostTagRepository.save(runnerPostTag); + + // when + final List joinRunnerPostTags + = runnerPostTagRepository.joinTagByRunnerPostId(runnerPost.getId()); + + // then + assertThat(joinRunnerPostTags).containsExactly(runnerPostTag); + } + + @DisplayName("RunnerPostTag 의 식별자값 목록이 비어있을 때 빈 컬렉션을 반환한다.") + @Test + void success_joinTagByRunnerPostIds_if_tag_is_empty() { + // given + final Member member = Member.builder() + .memberName(new MemberName("헤에디주")) + .email(new Email("test@test.co.kr")) + .oauthId(new OauthId("dsigjh98gh230gn2oinv913bcuo23nqovbvu93b12voi3bc31j")) + .githubUrl(new GithubUrl("github.com/hyena0608")) + .company(new Company("우아한형제들")) + .imageUrl(new ImageUrl("김석호")) + .build(); + final Member saveMember = memberRepository.saveAndFlush(member); + + final Runner runner = Runner.builder() + .totalRating(new TotalRating(100)) + .grade(Grade.BARE_FOOT) + .member(saveMember) + .build(); + final Runner saveRunner = runnerRepository.saveAndFlush(runner); + + final LocalDateTime deadline = LocalDateTime.now(); + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("제 코드 리뷰 좀 해주세요!!")) + .contents(new Contents("제 코드는 클린코드가 맞을까요?")) + .deadline(new Deadline(deadline)) + .pullRequestUrl(new PullRequestUrl("https://")) + .watchedCount(new WatchedCount(1)) + .chattingCount(new ChattingCount(1)) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(saveRunner) + .supporter(null) + .build(); + runnerPostRepository.saveAndFlush(runnerPost); + + // when + final List joinRunnerPostTags + = runnerPostTagRepository.joinTagByRunnerPostId(runnerPost.getId()); + + // then + assertThat(joinRunnerPostTags).isEmpty(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/repository/TagRepositoryTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/repository/TagRepositoryTest.java new file mode 100644 index 000000000..47d163ac5 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/repository/TagRepositoryTest.java @@ -0,0 +1,41 @@ +package touch.baton.domain.tag.repository; + +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import touch.baton.config.JpaConfig; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagCount; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@Import(JpaConfig.class) +@DataJpaTest +class TagRepositoryTest { + + @Autowired + private TagRepository tagRepository; + + @DisplayName("이름으로 단건 검색한다.") + @Test + void findByName() { + // given + final String newTagName = "Java"; + final Tag newTag = Tag.builder() + .tagName(new TagName(newTagName)) + .tagCount(new TagCount(0)) + .build(); + final Tag expected = tagRepository.save(newTag); + + // when + final Optional actual = tagRepository.findByTagName(new TagName(newTagName)); + + // then + assertThat(actual).contains(expected); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/tag/vo/TagNameTest.java b/backend/baton/src/test/java/touch/baton/domain/tag/vo/TagNameTest.java new file mode 100644 index 000000000..59f13042e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/tag/vo/TagNameTest.java @@ -0,0 +1,17 @@ +package touch.baton.domain.tag.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.TagName; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TagNameTest { + + @DisplayName("value 가 null 이면 예외가 발생한다.") + @Test + void fail_if_value_is_null() { + assertThatThrownBy(() -> new TagName(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagTest.java b/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagTest.java new file mode 100644 index 000000000..0fce45a08 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/technicaltag/SupporterTechnicalTagTest.java @@ -0,0 +1,70 @@ +package touch.baton.domain.technicaltag; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.supporter.vo.StarCount; +import touch.baton.domain.tag.exception.OldTagException; +import touch.baton.fixture.domain.MemberFixture; +import touch.baton.fixture.domain.SupporterFixture; +import touch.baton.fixture.domain.TechnicalTagFixture; + +import java.util.ArrayList; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SupporterTechnicalTagTest { + + @DisplayName("생성 테스트") + @Nested + class Create { + + private final Member member = MemberFixture.createHyena(); + + private final TechnicalTag technicalTag = TechnicalTagFixture.createJava(); + + private final Supporter supporter = SupporterFixture.create( + new ReviewCount(0), + new StarCount(0), + new TotalRating(10), + Grade.BARE_FOOT, + member, + new ArrayList<>()); + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> SupporterTechnicalTag.builder() + .supporter(supporter) + .technicalTag(technicalTag) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("supporter 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_supporter_is_null() { + assertThatThrownBy(() -> SupporterTechnicalTag.builder() + .supporter(null) + .technicalTag(technicalTag) + .build() + ).isInstanceOf(OldTagException.NotNull.class); + } + + @DisplayName("technical tag 가 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_technical_tag_is_null() { + assertThatThrownBy(() -> SupporterTechnicalTag.builder() + .supporter(supporter) + .technicalTag(null) + .build() + ).isInstanceOf(OldTagException.NotNull.class); + } + } +} diff --git a/backend/baton/src/test/java/touch/baton/domain/technicaltag/TechnicalTagTest.java b/backend/baton/src/test/java/touch/baton/domain/technicaltag/TechnicalTagTest.java new file mode 100644 index 000000000..774e8f50c --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/domain/technicaltag/TechnicalTagTest.java @@ -0,0 +1,30 @@ +package touch.baton.domain.technicaltag; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.exception.OldTagException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TechnicalTagTest { + + @DisplayName("성공한다.") + @Test + void success() { + assertThatCode(() -> TechnicalTag.builder() + .tagName(new TagName("자바")) + .build() + ).doesNotThrowAnyException(); + } + + @DisplayName("tag name 이 null 이 들어갈 경우 예외가 발생한다.") + @Test + void fail_if_tagName_is_null() { + assertThatThrownBy(() -> TechnicalTag.builder() + .tagName(null) + .build() + ).isInstanceOf(OldTagException.NotNull.class); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/MemberFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/MemberFixture.java new file mode 100644 index 000000000..22e0a7a05 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/MemberFixture.java @@ -0,0 +1,79 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.member.Member; +import touch.baton.domain.member.vo.Company; +import touch.baton.domain.member.vo.Email; +import touch.baton.domain.member.vo.GithubUrl; +import touch.baton.domain.member.vo.ImageUrl; +import touch.baton.domain.member.vo.MemberName; +import touch.baton.domain.member.vo.OauthId; + +import static touch.baton.fixture.vo.CompanyFixture.company; +import static touch.baton.fixture.vo.EmailFixture.email; +import static touch.baton.fixture.vo.GithubUrlFixture.githubUrl; +import static touch.baton.fixture.vo.ImageUrlFixture.imageUrl; +import static touch.baton.fixture.vo.MemberNameFixture.memberName; +import static touch.baton.fixture.vo.OauthIdFixture.oauthId; + +public abstract class MemberFixture { + + private MemberFixture() { + } + + public static Member create(final MemberName memberName, + final Email email, + final OauthId oauthId, + final GithubUrl githubUrl, + final Company company, + final ImageUrl imageUrl + ) { + return Member.builder() + .memberName(memberName) + .email(email) + .oauthId(oauthId) + .githubUrl(githubUrl) + .company(company) + .imageUrl(imageUrl) + .build(); + } + + public static Member createHyena() { + return create( + memberName("헤나"), + email("email_hyena@test.com"), + oauthId("oauth_hyena"), + githubUrl("https://github.com/"), + company("우아한테크코스 5기 백엔드"), + imageUrl("https://")); + } + + public static Member createEthan() { + return create( + memberName("에단"), + email("email_ethan@test.com"), + oauthId("oauth_ethan"), + githubUrl("https://github.com/"), + company("우아한테크코스 5기 백엔드"), + imageUrl("https://")); + } + + public static Member createDitoo() { + return create( + memberName("디투"), + email("email_ditoo@test.com"), + oauthId("oauth_ditoo"), + githubUrl("https://github.com/"), + company("우아한테크코스 5기 백엔드"), + imageUrl("https://")); + } + + public static Member createJudy() { + return create( + memberName("주디"), + email("email_judy@test.com"), + oauthId("oauth_judy"), + githubUrl("https://github.com/"), + company("우아한테크코스 5기 백엔드"), + imageUrl("https://")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerFixture.java new file mode 100644 index 000000000..f2a3679fa --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerFixture.java @@ -0,0 +1,26 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.runner.Runner; + +import static touch.baton.fixture.vo.TotalRatingFixture.totalRating; + +public abstract class RunnerFixture { + + private RunnerFixture() { + } + + public static Runner create(final TotalRating totalRating, final Grade grade, final Member member) { + return Runner.builder() + .totalRating(totalRating) + .grade(grade) + .member(member) + .build(); + } + + public static Runner createRunner(final Member member) { + return create(totalRating(5000), Grade.BARE_FOOT, member); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostFixture.java new file mode 100644 index 000000000..ba4d673d9 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostFixture.java @@ -0,0 +1,130 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.common.vo.ChattingCount; +import touch.baton.domain.common.vo.Contents; +import touch.baton.domain.common.vo.Title; +import touch.baton.domain.common.vo.WatchedCount; +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.vo.Deadline; +import touch.baton.domain.runnerpost.vo.PullRequestUrl; +import touch.baton.domain.runnerpost.vo.ReviewStatus; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.RunnerPostTags; +import touch.baton.domain.tag.Tag; + +import java.util.ArrayList; +import java.util.List; + +public abstract class RunnerPostFixture { + + private RunnerPostFixture() { + } + + public static RunnerPost create(final Title title, + final Contents contents, + final PullRequestUrl pullRequestUrl, + final Deadline deadline, + final WatchedCount watchedCount, + final ChattingCount chattingCount, + final ReviewStatus reviewStatus, + final Runner runner, + final Supporter supporter, + final RunnerPostTags runnerPostTags + ) { + return RunnerPost.builder() + .title(title) + .contents(contents) + .pullRequestUrl(pullRequestUrl) + .deadline(deadline) + .watchedCount(watchedCount) + .chattingCount(chattingCount) + .reviewStatus(reviewStatus) + .runner(runner) + .supporter(supporter) + .runnerPostTags(runnerPostTags) + .build(); + } + + public static RunnerPost create(final Runner runner, final Deadline deadline) { + return RunnerPost.builder() + .title(new Title("테스트 제목")) + .contents(new Contents("테스트 내용")) + .pullRequestUrl(new PullRequestUrl("https://테스트")) + .deadline(deadline) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(runner) + .supporter(null) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build(); + } + + public static RunnerPost create(final Runner runner, final Deadline deadline, List tags) { + final RunnerPost runnerPost = RunnerPost.builder() + .title(new Title("테스트 제목")) + .contents(new Contents("테스트 내용")) + .pullRequestUrl(new PullRequestUrl("https://테스트")) + .deadline(deadline) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .reviewStatus(ReviewStatus.NOT_STARTED) + .runner(runner) + .supporter(null) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build(); + + final List runnerPostTags = tags.stream() + .map(tag -> RunnerPostTagFixture.create(runnerPost, tag)) + .toList(); + + runnerPost.addAllRunnerPostTags(runnerPostTags); + + return runnerPost; + } + + + public static RunnerPost create(final Runner runner, final RunnerPostTags runnerPostTags, final Deadline deadline) { + return RunnerPost.builder() + .title(new Title("테스트 제목")) + .contents(new Contents("테스트 내용")) + .pullRequestUrl(new PullRequestUrl("https://테스트")) + .deadline(deadline) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(null) + .runnerPostTags(runnerPostTags) + .build(); + } + + public static RunnerPost create(final Runner runner, final Supporter supporter, final Deadline deadline) { + return RunnerPost.builder() + .title(new Title("테스트 제목")) + .contents(new Contents("테스트 내용")) + .pullRequestUrl(new PullRequestUrl("https://테스트")) + .deadline(deadline) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(supporter) + .runnerPostTags(new RunnerPostTags(new ArrayList<>())) + .build(); + } + + public static RunnerPost create(final Runner runner, final Supporter supporter, final RunnerPostTags runnerPostTags, final Deadline deadline) { + return RunnerPost.builder() + .title(new Title("테스트 제목")) + .contents(new Contents("테스트 내용")) + .pullRequestUrl(new PullRequestUrl("https://테스트")) + .deadline(deadline) + .watchedCount(new WatchedCount(0)) + .chattingCount(new ChattingCount(0)) + .runner(runner) + .supporter(supporter) + .runnerPostTags(runnerPostTags) + .build(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagFixture.java new file mode 100644 index 000000000..b244ce1bb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagFixture.java @@ -0,0 +1,18 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.Tag; + +public abstract class RunnerPostTagFixture { + + private RunnerPostTagFixture() { + } + + public static RunnerPostTag create(final RunnerPost runnerPost, final Tag tag) { + return RunnerPostTag.builder() + .runnerPost(runnerPost) + .tag(tag) + .build(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagsFixture.java new file mode 100644 index 000000000..0c7023970 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/RunnerPostTagsFixture.java @@ -0,0 +1,16 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.tag.RunnerPostTag; +import touch.baton.domain.tag.RunnerPostTags; + +import java.util.List; + +public abstract class RunnerPostTagsFixture { + + private RunnerPostTagsFixture() { + } + + public static RunnerPostTags runnerPostTags(final List runnerPostTags){ + return new RunnerPostTags(runnerPostTags); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFixture.java new file mode 100644 index 000000000..ac9118567 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterFixture.java @@ -0,0 +1,64 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.common.vo.Grade; +import touch.baton.domain.common.vo.TotalRating; +import touch.baton.domain.member.Member; +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.vo.ReviewCount; +import touch.baton.domain.supporter.vo.StarCount; +import touch.baton.domain.technicaltag.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.SupporterTechnicalTags; +import touch.baton.domain.technicaltag.TechnicalTag; + +import java.util.ArrayList; +import java.util.List; + +public abstract class SupporterFixture { + + private SupporterFixture() { + } + + public static Supporter create(final ReviewCount reviewCount, + final StarCount starCount, + final TotalRating totalRating, + final Grade grade, + final Member member, + final SupporterTechnicalTags supporterTechnicalTags + ) { + return Supporter.builder() + .reviewCount(reviewCount) + .starCount(starCount) + .totalRating(totalRating) + .grade(grade) + .member(member) + .supporterTechnicalTags(supporterTechnicalTags) + .build(); + } + + public static Supporter create(final ReviewCount reviewCount, + final StarCount starCount, + final TotalRating totalRating, + final Grade grade, + final Member member, + final List technicalTags + ) { + final Supporter supporter = Supporter.builder() + .reviewCount(reviewCount) + .starCount(starCount) + .totalRating(totalRating) + .grade(grade) + .member(member) + .supporterTechnicalTags(new SupporterTechnicalTags(new ArrayList<>())) + .build(); + + final List supporterTechnicalTags = technicalTags.stream() + .map(technicalTag -> SupporterTechnicalTag.builder() + .supporter(supporter) + .technicalTag(technicalTag) + .build()) + .toList(); + + supporter.addAllSupporterTechnicalTags(supporterTechnicalTags); + return supporter; + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagFixture.java new file mode 100644 index 000000000..b67a77f5a --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagFixture.java @@ -0,0 +1,18 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.technicaltag.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.TechnicalTag; + +public abstract class SupporterTechnicalTagFixture { + + private SupporterTechnicalTagFixture() { + } + + public static SupporterTechnicalTag create(final Supporter supporter, final TechnicalTag technicalTag) { + return SupporterTechnicalTag.builder() + .supporter(supporter) + .technicalTag(technicalTag) + .build(); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagsFixture.java new file mode 100644 index 000000000..da7600f6e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/SupporterTechnicalTagsFixture.java @@ -0,0 +1,16 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.technicaltag.SupporterTechnicalTag; +import touch.baton.domain.technicaltag.SupporterTechnicalTags; + +import java.util.List; + +public abstract class SupporterTechnicalTagsFixture { + + private SupporterTechnicalTagsFixture() { + } + + public static SupporterTechnicalTags create(final List supporterTechnicalTags) { + return new SupporterTechnicalTags(supporterTechnicalTags); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/TagFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/TagFixture.java new file mode 100644 index 000000000..59915ba2d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/TagFixture.java @@ -0,0 +1,33 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.vo.TagCount; +import touch.baton.fixture.vo.TagNameFixture; + +import static touch.baton.fixture.vo.TagCountFixture.tagCount; + +public abstract class TagFixture { + + private TagFixture() { + } + + public static Tag create(final TagName tagName, final TagCount tagCount) { + return Tag.builder() + .tagName(tagName) + .tagCount(tagCount) + .build(); + } + + public static Tag createJava() { + return create(TagNameFixture.tagName("Java"), tagCount(1)); + } + + public static Tag createSpring() { + return create(TagNameFixture.tagName("Spring"), tagCount(1)); + } + + public static Tag createReact() { + return create(TagNameFixture.tagName("React"), tagCount(1)); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/domain/TechnicalTagFixture.java b/backend/baton/src/test/java/touch/baton/fixture/domain/TechnicalTagFixture.java new file mode 100644 index 000000000..3464d9c7d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/domain/TechnicalTagFixture.java @@ -0,0 +1,30 @@ +package touch.baton.fixture.domain; + +import touch.baton.domain.common.vo.TagName; +import touch.baton.domain.technicaltag.TechnicalTag; + +import static touch.baton.fixture.vo.TagNameFixture.tagName; + +public abstract class TechnicalTagFixture { + + private TechnicalTagFixture() { + } + + public static TechnicalTag create(final TagName tagName) { + return TechnicalTag.builder() + .tagName(tagName) + .build(); + } + + public static TechnicalTag createJava() { + return create(tagName("Java")); + } + + public static TechnicalTag createSpring() { + return create(tagName("Spring")); + } + + public static TechnicalTag createReact() { + return create(tagName("React")); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/repository/MemberRepositoryFixture.java b/backend/baton/src/test/java/touch/baton/fixture/repository/MemberRepositoryFixture.java new file mode 100644 index 000000000..071861505 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/repository/MemberRepositoryFixture.java @@ -0,0 +1,17 @@ +package touch.baton.fixture.repository; + +import touch.baton.domain.member.Member; +import touch.baton.domain.member.repository.MemberRepository; + +public class MemberRepositoryFixture { + + private final MemberRepository memberRepository; + + public MemberRepositoryFixture(final MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public Member save(final Member member) { + return memberRepository.save(member); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/repository/RunnerPostRepositoryFixture.java b/backend/baton/src/test/java/touch/baton/fixture/repository/RunnerPostRepositoryFixture.java new file mode 100644 index 000000000..e295d60e6 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/repository/RunnerPostRepositoryFixture.java @@ -0,0 +1,17 @@ +package touch.baton.fixture.repository; + +import touch.baton.domain.runnerpost.RunnerPost; +import touch.baton.domain.runnerpost.repository.RunnerPostRepository; + +public class RunnerPostRepositoryFixture { + + private final RunnerPostRepository runnerPostRepository; + + public RunnerPostRepositoryFixture(final RunnerPostRepository runnerPostRepository) { + this.runnerPostRepository = runnerPostRepository; + } + + public RunnerPost save(final RunnerPost runnerPost) { + return runnerPostRepository.save(runnerPost); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/repository/RunnerRepositoryFixture.java b/backend/baton/src/test/java/touch/baton/fixture/repository/RunnerRepositoryFixture.java new file mode 100644 index 000000000..394b6f3e0 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/repository/RunnerRepositoryFixture.java @@ -0,0 +1,17 @@ +package touch.baton.fixture.repository; + +import touch.baton.domain.runner.Runner; +import touch.baton.domain.runner.repository.RunnerRepository; + +public class RunnerRepositoryFixture { + + private final RunnerRepository runnerRepository; + + public RunnerRepositoryFixture(final RunnerRepository runnerRepository) { + this.runnerRepository = runnerRepository; + } + + public Runner save(final Runner runner) { + return runnerRepository.save(runner); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/repository/SupporterRepositoryFixture.java b/backend/baton/src/test/java/touch/baton/fixture/repository/SupporterRepositoryFixture.java new file mode 100644 index 000000000..4b8e4dc55 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/repository/SupporterRepositoryFixture.java @@ -0,0 +1,17 @@ +package touch.baton.fixture.repository; + +import touch.baton.domain.supporter.Supporter; +import touch.baton.domain.supporter.repository.SupporterRepository; + +public class SupporterRepositoryFixture { + + private final SupporterRepository supporterRepository; + + public SupporterRepositoryFixture(final SupporterRepository supporterRepository) { + this.supporterRepository = supporterRepository; + } + + public Supporter save(final Supporter supporter) { + return supporterRepository.save(supporter); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/repository/TagRepositoryFixture.java b/backend/baton/src/test/java/touch/baton/fixture/repository/TagRepositoryFixture.java new file mode 100644 index 000000000..51b18a316 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/repository/TagRepositoryFixture.java @@ -0,0 +1,17 @@ +package touch.baton.fixture.repository; + +import touch.baton.domain.tag.Tag; +import touch.baton.domain.tag.repository.TagRepository; + +public class TagRepositoryFixture { + + private final TagRepository tagRepository; + + public TagRepositoryFixture(final TagRepository tagRepository) { + this.tagRepository = tagRepository; + } + + public Tag save(final Tag tag) { + return tagRepository.save(tag); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/ChattingCountFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/ChattingCountFixture.java new file mode 100644 index 000000000..3402fb05e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ChattingCountFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.common.vo.ChattingCount; + +public abstract class ChattingCountFixture { + + private ChattingCountFixture() { + } + + public static ChattingCount chattingCount(final int value) { + return new ChattingCount(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/CompanyFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/CompanyFixture.java new file mode 100644 index 000000000..44532716e --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/CompanyFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.member.vo.Company; + +public abstract class CompanyFixture { + + private CompanyFixture() { + } + + public static Company company(final String value) { + return new Company(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/ContentsFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/ContentsFixture.java new file mode 100644 index 000000000..fce95cf6f --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ContentsFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.common.vo.Contents; + +public abstract class ContentsFixture { + + private ContentsFixture() { + } + + public static Contents contents(final String value) { + return new Contents(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/DeadlineFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/DeadlineFixture.java new file mode 100644 index 000000000..a706c4283 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/DeadlineFixture.java @@ -0,0 +1,15 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.runnerpost.vo.Deadline; + +import java.time.LocalDateTime; + +public abstract class DeadlineFixture { + + private DeadlineFixture() { + } + + public static Deadline deadline(final LocalDateTime value) { + return new Deadline(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/EmailFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/EmailFixture.java new file mode 100644 index 000000000..f720a84d7 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/EmailFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.member.vo.Email; + +public abstract class EmailFixture { + + private EmailFixture() { + } + + public static Email email(final String value) { + return new Email(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/GithubUrlFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/GithubUrlFixture.java new file mode 100644 index 000000000..6eedbdc2d --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/GithubUrlFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.member.vo.GithubUrl; + +public abstract class GithubUrlFixture { + + private GithubUrlFixture() { + } + + public static GithubUrl githubUrl(final String value) { + return new GithubUrl(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/ImageUrlFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/ImageUrlFixture.java new file mode 100644 index 000000000..3f3667ffe --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ImageUrlFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.member.vo.ImageUrl; + +public abstract class ImageUrlFixture { + + private ImageUrlFixture() { + } + + public static ImageUrl imageUrl(final String value) { + return new ImageUrl(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/MemberNameFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/MemberNameFixture.java new file mode 100644 index 000000000..2160545eb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/MemberNameFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.member.vo.MemberName; + +public abstract class MemberNameFixture { + + private MemberNameFixture() { + } + + public static MemberName memberName(final String value) { + return new MemberName(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/OauthIdFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/OauthIdFixture.java new file mode 100644 index 000000000..519ba6766 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/OauthIdFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.member.vo.OauthId; + +public abstract class OauthIdFixture { + + private OauthIdFixture() { + } + + public static OauthId oauthId(final String value) { + return new OauthId(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/PullRequestUrlFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/PullRequestUrlFixture.java new file mode 100644 index 000000000..a28447b46 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/PullRequestUrlFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.runnerpost.vo.PullRequestUrl; + +public abstract class PullRequestUrlFixture { + + private PullRequestUrlFixture() { + } + + public static PullRequestUrl pullRequestUrl(final String value) { + return new PullRequestUrl(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/ReviewCountFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/ReviewCountFixture.java new file mode 100644 index 000000000..59a70efa8 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/ReviewCountFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.supporter.vo.ReviewCount; + +public abstract class ReviewCountFixture { + + private ReviewCountFixture() { + } + + public static ReviewCount reviewCount(final int value) { + return new ReviewCount(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/StarCountFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/StarCountFixture.java new file mode 100644 index 000000000..3b32213d1 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/StarCountFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.supporter.vo.StarCount; + +public abstract class StarCountFixture { + + private StarCountFixture() { + } + + public static StarCount starCount(final int value) { + return new StarCount(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/TagCountFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/TagCountFixture.java new file mode 100644 index 000000000..f68862f05 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/TagCountFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.tag.vo.TagCount; + +public abstract class TagCountFixture { + + private TagCountFixture() { + } + + public static TagCount tagCount(final int value) { + return new TagCount(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/TagNameFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/TagNameFixture.java new file mode 100644 index 000000000..6fcc79eb2 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/TagNameFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.common.vo.TagName; + +public abstract class TagNameFixture { + + private TagNameFixture() { + } + + public static TagName tagName(final String value) { + return new TagName(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/TitleFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/TitleFixture.java new file mode 100644 index 000000000..1feb686cf --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/TitleFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.common.vo.Title; + +public abstract class TitleFixture { + + private TitleFixture() { + } + + public static Title title(final String value) { + return new Title(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/TotalRatingFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/TotalRatingFixture.java new file mode 100644 index 000000000..40747a593 --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/TotalRatingFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.common.vo.TotalRating; + +public abstract class TotalRatingFixture { + + private TotalRatingFixture() { + } + + public static TotalRating totalRating(final int value) { + return new TotalRating(value); + } +} diff --git a/backend/baton/src/test/java/touch/baton/fixture/vo/WatchedCountFixture.java b/backend/baton/src/test/java/touch/baton/fixture/vo/WatchedCountFixture.java new file mode 100644 index 000000000..54b5551cb --- /dev/null +++ b/backend/baton/src/test/java/touch/baton/fixture/vo/WatchedCountFixture.java @@ -0,0 +1,13 @@ +package touch.baton.fixture.vo; + +import touch.baton.domain.common.vo.WatchedCount; + +public abstract class WatchedCountFixture { + + private WatchedCountFixture() { + } + + public static WatchedCount watchedCount(final int value) { + return new WatchedCount(value); + } +}