diff --git a/.github/auto_assign.yml b/.github/auto-assign.yml similarity index 94% rename from .github/auto_assign.yml rename to .github/auto-assign.yml index 5e1a75c..4c5109a 100644 --- a/.github/auto_assign.yml +++ b/.github/auto-assign.yml @@ -11,4 +11,4 @@ reviewers: # A number of reviewers added to the pull request # Set 0 to add all the reviewers (default: 0) -numberOfReviewers: 1 +numberOfReviewers: 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f264046 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: ci + +on: + pull_request: + branches: + - weekly # 'weekly' 브랜치로 PR이 생성될 때만 실행 + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + + - name: Cache Gradle dependencies + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + + - name: Build and Test + run: ./gradlew clean build + - name: Run Spring Boot App for 30 seconds + run: | + ./gradlew bootRun & + APP_PID=$! + sleep 30 + kill $APP_PID + continue-on-error: false \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..985e218 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: lint + +on: + pull_request: + branches: [ '**' ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Test with Spotless + run: ./gradlew --info :app:spotlessJavaCheck \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/README.md b/README.md index 9a62b2e..5737540 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,19 @@ 최종 배포는 크램폴린으로 배포해야 합니다. -하지만 배포 환경의 불편함이 있는 경우를 고려하여 +하지만 배포 환경의 불편함이 있는 경우를 고려하여 임의의 배포를 위해 타 배포 환경을 자유롭게 이용해도 됩니다. (단, 금액적인 지원은 어렵습니다.) 아래는 추가적인 설정을 통해 (체험판, 혹은 프리 티어 등)무료로 클라우드 배포가 가능한 서비스입니다. -ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype +ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype ``` ## Notice ``` -필요 산출물들은 수료 기준에 영향을 주는 것은 아니지만, +필요 산출물들은 수료 기준에 영향을 주는 것은 아니지만, 주차 별 산출물을 기반으로 평가가 이루어 집니다. 주차 별 평가 점수는 추 후 최종 평가에 최종 합산 점수로 포함됩니다. @@ -40,9 +40,9 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-1
- + ✅**1주차** - + ``` - 5 Whys - 마켓 리서치 @@ -50,7 +50,7 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype - 와이어 프레임 - 칸반보드 ``` - +
@@ -59,15 +59,15 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-2
- + ✅**2주차** - + ``` - ERD 설계서 - + - API 명세서 ``` - +
@@ -76,13 +76,13 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-3
- + ✅**3주차** - + ``` - 최종 기획안 ``` - +
@@ -91,15 +91,15 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-4
- + ✅**4주차** - + ``` - 4주차 github - + - 4주차 노션 ``` - +
@@ -107,15 +107,15 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-5
- + ✅**5주차** - + ``` - 5주차 github - + - 5주차 노션 ``` - +
@@ -124,17 +124,17 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-6
- + ✅**6주차** - + ``` - 6주차 github - + - 중간발표자료 - + - 피어리뷰시트 ``` - +
@@ -143,15 +143,15 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-7
- + ✅**7주차** - + ``` - 7주차 github - + - 7주차 노션 ``` - +
@@ -160,14 +160,14 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-8
- + ✅**8주차** - + ``` - 중간고사 - + ``` - +
@@ -176,15 +176,15 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-9
- + ✅**9주차** - + ``` - 9주차 github - + - 9주차 노션 ``` - +
@@ -193,17 +193,17 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-10
- + ✅**10주차** - + ``` - 10주차 github - + - 테스트 시나리오 명세서 - + - 테스트 결과 보고서 ``` - +
@@ -212,15 +212,15 @@ ex ) AWS(아마존), GCP(구글), Azure(마이크로소프트), Cloudtype
Step3. Week-11
- + ✅**11주차** - + ``` - 최종 기획안 - + - 배포 인스턴스 링크 ``` - +
@@ -244,7 +244,7 @@ UI 컴포넌트의 명칭과 이를 구현하는 능력은 필수적인 커뮤 **1. PR 제목과 내용을 아래와 같이 작성 해주세요.** > PR 제목 : 부산대_0조_아이템명_0주차 -> +>
diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..1482c2f --- /dev/null +++ b/build.gradle @@ -0,0 +1,77 @@ +buildscript { + ext { + projectName = 'Overflow' + projectVersion = '1.0.1' + springBootVersion = '2.7.5' + dependencyManagementVersion = '1.0.15.RELEASE' + spotlessVersion = '6.8.0' + + // dependencies + jsonwebtokenVersion = '0.11.5' + flywayVersion = '9.16.0' + } +} +plugins { + id 'java' + id 'org.springframework.boot' version "${springBootVersion}" + id 'io.spring.dependency-management' version "${dependencyManagementVersion}" + + id "com.diffplug.spotless" version "${spotlessVersion}" +} + +group = 'com.kakao' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '1.8' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +/** apply tasks */ +apply from: './tasks/formatting-task.gradle' +apply from: './tasks/install-git-hooks.gradle' + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // database + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'mysql:mysql-connector-java' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + + // encryption + implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4' + + // jwt + implementation "io.jsonwebtoken:jjwt-api:${jsonwebtokenVersion}" + implementation "io.jsonwebtoken:jjwt-impl:${jsonwebtokenVersion}" + implementation "io.jsonwebtoken:jjwt-jackson:${jsonwebtokenVersion}" + +} + +test { + useJUnitPlatform() + + testLogging { + events "failed" + exceptionFormat "full" + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..033e24c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9f4197d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..fcb6fca --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/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/resources/local-develop-environment/README.md b/resources/local-develop-environment/README.md new file mode 100644 index 0000000..4dec3cd --- /dev/null +++ b/resources/local-develop-environment/README.md @@ -0,0 +1,31 @@ +# docker-compose 를 통한 local 개발 환경 구축 +## docker-compose 띄우기 +``` +cd local-develop-environment +docker-compose up -d + +# 완전 종료 : 모든 데이터 사라짐 +docker-compose down + +# 일시 정지 : 데이터 유지 +docker-compose stop +``` + +## 초기화 파일 +* `mysql-init.d` : `*.sql` 파일들로 database 생성 등을 수행한다. + +* 초기화 파일 수정시에는 `docker-compose down` 으로 완전 초기화를 해야한다. + +## mysql client +* 서비스용 계정 : `golajuma-local` +* 서비스용 비밀번호 : `golajuma-local` +* [docker localhost adminer](http://localhost:18080) 로 접속하면 [adminer](https://www.adminer.org) Web DB Client 로 DB조회 조작 가능. + +```shell +docker exec -it golajuma-mysql8 mysql -uroot -p +# 비밀번호 root 로 접속 + +mysql> show databases; +mysql> use master; +mysql> show tables; +``` diff --git a/resources/local-develop-environment/docker-compose.yml b/resources/local-develop-environment/docker-compose.yml new file mode 100644 index 0000000..806adfc --- /dev/null +++ b/resources/local-develop-environment/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.1' +services: + overflow-mysql: + container_name: golajuma-mysql8 + image: mysql/mysql-server:8.0.27 + environment: + - MYSQL_ROOT_PASSWORD=root + - MYSQL_ROOT_HOST=% + - TZ=Asia/Seoul + command: [ "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", "--lower_case_table_names=1", "--max_connections=2048", "--wait_timeout=3600" ] + ports: + - "13306:3306" + volumes: + - ./mysql-init.d:/docker-entrypoint-initdb.d + + overflow-adminer: # mysql web admin + container_name: golajuma-adminer + image: adminer:4 + ports: + - "18080:8080" + environment: + - ADMINER_DEFAULT_SERVER=golajuma-mysql8 + - ADMINER_DESIGN=nette + - ADMINER_PLUGINS=tables-filter tinymce + + redis-docker: + container_name: golajuma-redis + image: redis:latest + ports: + - "16379:6379" diff --git a/resources/local-develop-environment/mysql-init.d/00_init.sql b/resources/local-develop-environment/mysql-init.d/00_init.sql new file mode 100644 index 0000000..7b4bcc7 --- /dev/null +++ b/resources/local-develop-environment/mysql-init.d/00_init.sql @@ -0,0 +1,12 @@ +CREATE + USER 'golajuma-local'@'localhost' IDENTIFIED BY 'golajuma-local'; +CREATE + USER 'golajuma-local'@'%' IDENTIFIED BY 'golajuma-local'; + +GRANT ALL PRIVILEGES ON *.* TO + 'golajuma-local'@'localhost'; +GRANT ALL PRIVILEGES ON *.* TO + 'golajuma-local'@'%'; + +CREATE + DATABASE golajuma DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/scripts/local-develop-env-reset b/scripts/local-develop-env-reset new file mode 100644 index 0000000..2de46fe --- /dev/null +++ b/scripts/local-develop-env-reset @@ -0,0 +1,6 @@ +#!/bin/sh + +cd ../resources/local-develop-environment +docker-compose down +docker-compose up -d +sleep 10 \ No newline at end of file diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100644 index 0000000..bce650f --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,11 @@ +# 변경된 파일들 이름만 추출하여 저장 +stagedFiles=$(git diff --staged --name-only) +# SpotlessApply 실행 +echo "Running spotlessApply. Formatting code..." +./gradlew spotlessApply --daemon +# 변경사항이 발생한 파일들 다시 git add +for file in $stagedFiles; do + if test -f "$file"; then + git add "$file" + fi +done \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..fc95567 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'goalajuma' diff --git a/src/main/java/com/kakao/golajuma/GolajumaApplication.java b/src/main/java/com/kakao/golajuma/GolajumaApplication.java new file mode 100644 index 0000000..2d3c1d3 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/GolajumaApplication.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GolajumaApplication { + public static void main(String[] args) { + SpringApplication.run(GolajumaApplication.class, args); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/exception/AuthorizationException.java b/src/main/java/com/kakao/golajuma/auth/domain/exception/AuthorizationException.java new file mode 100644 index 0000000..f97781c --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/exception/AuthorizationException.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma.auth.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class AuthorizationException extends BusinessException { + + public AuthorizationException(String message) { + super(message, HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/exception/DuplicateException.java b/src/main/java/com/kakao/golajuma/auth/domain/exception/DuplicateException.java new file mode 100644 index 0000000..aa1ea05 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/exception/DuplicateException.java @@ -0,0 +1,10 @@ +package com.kakao.golajuma.auth.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class DuplicateException extends BusinessException { + public DuplicateException(String message) { + super(message, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/exception/NotFoundException.java b/src/main/java/com/kakao/golajuma/auth/domain/exception/NotFoundException.java new file mode 100644 index 0000000..a6c47ad --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma.auth.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class NotFoundException extends BusinessException { + + public NotFoundException(String message) { + super(message, HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/exception/NotValidToken.java b/src/main/java/com/kakao/golajuma/auth/domain/exception/NotValidToken.java new file mode 100644 index 0000000..a7c5103 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/exception/NotValidToken.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma.auth.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class NotValidToken extends BusinessException { + + public NotValidToken(String message) { + super(message, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/helper/Encoder.java b/src/main/java/com/kakao/golajuma/auth/domain/helper/Encoder.java new file mode 100644 index 0000000..1e779e9 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/helper/Encoder.java @@ -0,0 +1,15 @@ +package com.kakao.golajuma.auth.domain.helper; + +import org.mindrot.jbcrypt.BCrypt; +import org.springframework.stereotype.Component; + +@Component +public class Encoder { + public String encode(String raw) { + return BCrypt.hashpw(raw, BCrypt.gensalt()); + } + + public boolean matches(String raw, String hashed) { + return BCrypt.checkpw(raw, hashed); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/model/RefreshToken.java b/src/main/java/com/kakao/golajuma/auth/domain/model/RefreshToken.java new file mode 100644 index 0000000..0e829bb --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/model/RefreshToken.java @@ -0,0 +1,17 @@ +package com.kakao.golajuma.auth.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class RefreshToken { + private String refreshToken; + private Long userId; +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/service/LoginUserService.java b/src/main/java/com/kakao/golajuma/auth/domain/service/LoginUserService.java new file mode 100644 index 0000000..1c548c1 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/service/LoginUserService.java @@ -0,0 +1,55 @@ +package com.kakao.golajuma.auth.domain.service; + +import com.kakao.golajuma.auth.domain.exception.NotFoundException; +import com.kakao.golajuma.auth.domain.helper.Encoder; +import com.kakao.golajuma.auth.domain.token.TokenProvider; +import com.kakao.golajuma.auth.domain.token.TokenResolver; +import com.kakao.golajuma.auth.infra.entity.UserEntity; +import com.kakao.golajuma.auth.infra.repository.UserRepository; +import com.kakao.golajuma.auth.web.dto.converter.TokenConverter; +import com.kakao.golajuma.auth.web.dto.request.LoginUserRequest; +import com.kakao.golajuma.auth.web.dto.response.TokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class LoginUserService { + + private final TokenProvider tokenProvider; + private final UserRepository userRepository; + private final TokenConverter tokenConverter; + private final TokenResolver tokenResolver; + private final TokenService tokenService; + + @Transactional + public TokenResponse execute(final LoginUserRequest request) { + UserEntity userEntity = + userRepository + .findByEmail(request.getEmail()) + .orElseThrow(() -> new NotFoundException("존재하지 않는 이메일입니다.")); + + validPassword(request.getPassword(), userEntity); + + String accessToken = tokenProvider.createAccessToken(userEntity.getId()); + String refreshToken = tokenProvider.createRefreshToken(userEntity.getId()); + + tokenService.execute(userEntity.getId(), refreshToken); + + return tokenConverter.from( + accessToken, tokenResolver.getExpiredDate(accessToken), refreshToken); + } + + private void validPassword(final String requestPassword, final UserEntity userEntity) { + if (!matchPassword(requestPassword, userEntity.getPassword())) { + throw new NotFoundException("존재하지 않는 비밀번호입니다"); + } + } + + private boolean matchPassword(final String requestPassword, final String password) { + return encoder.matches(requestPassword, password); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/service/SaveUserService.java b/src/main/java/com/kakao/golajuma/auth/domain/service/SaveUserService.java new file mode 100644 index 0000000..44322de --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/service/SaveUserService.java @@ -0,0 +1,30 @@ +package com.kakao.golajuma.auth.domain.service; + +import com.kakao.golajuma.auth.infra.converter.UserEntityConverter; +import com.kakao.golajuma.auth.infra.repository.UserRepository; +import com.kakao.golajuma.auth.web.dto.request.SaveUserRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class SaveUserService { + + private final UserRepository userRepository; + private final UserEntityConverter entityConverter; + private final ValidEmailService validEmailService; + private final ValidNicknameService validNicknameService; + + @Transactional + public void execute(final SaveUserRequest source) { + valid(source); + userRepository.save(entityConverter.toEntity(source)); + } + + private void valid(final SaveUserRequest source) { + validEmailService.execute(source); + validNicknameService.execute(source); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/service/TokenService.java b/src/main/java/com/kakao/golajuma/auth/domain/service/TokenService.java new file mode 100644 index 0000000..fc9b463 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/service/TokenService.java @@ -0,0 +1,19 @@ +package com.kakao.golajuma.auth.domain.service; + +import com.kakao.golajuma.auth.domain.model.RefreshToken; +import com.kakao.golajuma.auth.infra.repository.RefreshTokenRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class TokenService { + private final RefreshTokenRepository refreshTokenRepository; + + @Transactional + public void execute(Long userId, String token) { + RefreshToken refreshToken = RefreshToken.builder().refreshToken(token).userId(userId).build(); + refreshTokenRepository.save(refreshToken); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/service/ValidEmailService.java b/src/main/java/com/kakao/golajuma/auth/domain/service/ValidEmailService.java new file mode 100644 index 0000000..eface28 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/service/ValidEmailService.java @@ -0,0 +1,22 @@ +package com.kakao.golajuma.auth.domain.service; + +import com.kakao.golajuma.auth.domain.exception.DuplicateException; +import com.kakao.golajuma.auth.infra.repository.UserRepository; +import com.kakao.golajuma.auth.web.supplier.EmailSupplier; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ValidEmailService { + private final UserRepository userRepository; + + public void execute(EmailSupplier supplier) { + boolean exists = userRepository.existsByEmail(supplier.getEmail()); + if (exists) { + throw new DuplicateException("중복되는 이메일입니다"); + } + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/service/ValidNicknameService.java b/src/main/java/com/kakao/golajuma/auth/domain/service/ValidNicknameService.java new file mode 100644 index 0000000..792f442 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/service/ValidNicknameService.java @@ -0,0 +1,22 @@ +package com.kakao.golajuma.auth.domain.service; + +import com.kakao.golajuma.auth.domain.exception.DuplicateException; +import com.kakao.golajuma.auth.infra.repository.UserRepository; +import com.kakao.golajuma.auth.web.supplier.NicknameSupplier; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ValidNicknameService { + private final UserRepository userRepository; + + public void execute(NicknameSupplier supplier) { + boolean exists = userRepository.existsByNickname(supplier.getNickname()); + if (exists) { + throw new DuplicateException("중복되는 닉네임입니다"); + } + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/token/TokenProvider.java b/src/main/java/com/kakao/golajuma/auth/domain/token/TokenProvider.java new file mode 100644 index 0000000..b97053f --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/token/TokenProvider.java @@ -0,0 +1,52 @@ +package com.kakao.golajuma.auth.domain.token; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class TokenProvider { + + private static final String USER_ID_CLAIM_KEY = "userId"; + private final SecretKey secretKey; + private final long accessValidTime; + private final long refreshValidTime; + + public TokenProvider( + @Value("${security.jwt.token.secretKey}") String secretKey, + @Value("${security.jwt.token.access.validTime}") long accessValidTime, + @Value("${security.jwt.token.refresh.validTime}") long refreshValidTime) { + this.secretKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + this.accessValidTime = accessValidTime; + this.refreshValidTime = refreshValidTime; + } + + public String createAccessToken(final Long userId) { + Date now = new Date(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .claim(USER_ID_CLAIM_KEY, userId) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + accessValidTime)) + .signWith(secretKey) + .compact(); + } + + public String createRefreshToken(final Long userId) { + final Date now = new Date(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .claim(USER_ID_CLAIM_KEY, userId) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + refreshValidTime)) + .signWith(secretKey) + .compact(); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/token/TokenResolver.java b/src/main/java/com/kakao/golajuma/auth/domain/token/TokenResolver.java new file mode 100644 index 0000000..b3d082a --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/token/TokenResolver.java @@ -0,0 +1,43 @@ +package com.kakao.golajuma.auth.domain.token; + +import com.kakao.golajuma.auth.domain.exception.NotValidToken; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Objects; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class TokenResolver { + + private static final String USER_ID_CLAIM_KEY = "userId"; + private final SecretKey secretKey; + + public TokenResolver(@Value("${security.jwt.token.secretKey}") String accessSecretKey) { + this.secretKey = Keys.hmacShaKeyFor(accessSecretKey.getBytes(StandardCharsets.UTF_8)); + } + + private Claims getClaims(final String token) { + try { + return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody(); + } catch (ExpiredJwtException e) { + throw new NotValidToken("만료된 토큰입니다"); + } + } + + public Date getExpiredDate(final String token) { + Objects.requireNonNull(token); + return getClaims(token).getExpiration(); + } + + public Long getUserInfo(final String token) { + Objects.requireNonNull(token); + + return Long.valueOf(String.valueOf(getClaims(token).get(USER_ID_CLAIM_KEY))); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/domain/token/TokenValidator.java b/src/main/java/com/kakao/golajuma/auth/domain/token/TokenValidator.java new file mode 100644 index 0000000..99a0c66 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/domain/token/TokenValidator.java @@ -0,0 +1,20 @@ +package com.kakao.golajuma.auth.domain.token; + +import com.kakao.golajuma.auth.domain.exception.AuthorizationException; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TokenValidator { + private final TokenResolver tokenResolver; + + public void valid(String token) { + Date expiredDate = tokenResolver.getExpiredDate(token); + + if (expiredDate.before(new Date())) { + throw new AuthorizationException("토큰이 만료되었습니다"); + } + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/infra/converter/UserEntityConverter.java b/src/main/java/com/kakao/golajuma/auth/infra/converter/UserEntityConverter.java new file mode 100644 index 0000000..2660c69 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/infra/converter/UserEntityConverter.java @@ -0,0 +1,24 @@ +package com.kakao.golajuma.auth.infra.converter; + +import com.kakao.golajuma.auth.domain.helper.Encoder; +import com.kakao.golajuma.auth.infra.entity.UserEntity; +import com.kakao.golajuma.auth.web.dto.request.SaveUserRequest; +import com.kakao.golajuma.common.support.converter.AbstractEntityConverter; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserEntityConverter implements AbstractEntityConverter { + + private final Encoder encoder; + + @Override + public UserEntity toEntity(SaveUserRequest source) { + return UserEntity.builder() + .nickname(source.getNickname()) + .email(source.getEmail()) + .password(encoder.encode(source.getPassword())) + .build(); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/infra/entity/AuthInfoEntity.java b/src/main/java/com/kakao/golajuma/auth/infra/entity/AuthInfoEntity.java new file mode 100644 index 0000000..6fdce6f --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/infra/entity/AuthInfoEntity.java @@ -0,0 +1,47 @@ +package com.kakao.golajuma.auth.infra.entity; + +import com.kakao.golajuma.common.BaseEntity; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString +@SuperBuilder(toBuilder = true) +@Entity(name = AuthInfoEntity.ENTITY_PREFIX + "_entity") +public class AuthInfoEntity extends BaseEntity { + + public static final String ENTITY_PREFIX = "auth_info"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = ENTITY_PREFIX + "_id", nullable = false) + private Long id; + + @Builder.Default + @Enumerated(EnumType.STRING) + @Column(name = ENTITY_PREFIX + "_type", nullable = false) + private LoginType type = LoginType.SERVICE; + + @Column(name = ENTITY_PREFIX + "_token", nullable = false) + private String token; + + @OneToOne + @JoinColumn(name = "user_id") + private UserEntity userEntity; +} diff --git a/src/main/java/com/kakao/golajuma/auth/infra/entity/LoginType.java b/src/main/java/com/kakao/golajuma/auth/infra/entity/LoginType.java new file mode 100644 index 0000000..4e0614b --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/infra/entity/LoginType.java @@ -0,0 +1,24 @@ +package com.kakao.golajuma.auth.infra.entity; + +import java.util.Arrays; +import java.util.Optional; + +public enum LoginType { + SERVICE("service"); + + private final String typeName; + + LoginType(String typeName) { + this.typeName = typeName; + } + + public String getTypeName() { + return typeName; + } + + public Optional findLoginType(String type) { + return Arrays.stream(LoginType.values()) + .filter(loginType -> loginType.getTypeName().equals(type)) + .findFirst(); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/infra/entity/UserEntity.java b/src/main/java/com/kakao/golajuma/auth/infra/entity/UserEntity.java new file mode 100644 index 0000000..a1998f4 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/infra/entity/UserEntity.java @@ -0,0 +1,38 @@ +package com.kakao.golajuma.auth.infra.entity; + +import com.kakao.golajuma.common.BaseEntity; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString +@SuperBuilder(toBuilder = true) +@Entity(name = UserEntity.ENTITY_PREFIX + "_entity") +public class UserEntity extends BaseEntity { + public static final String ENTITY_PREFIX = "user"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = ENTITY_PREFIX + "_id", nullable = false) + private Long id; + + @Column(name = ENTITY_PREFIX + "_nickname", nullable = false) + private String nickname; + + @Column(name = ENTITY_PREFIX + "_email", nullable = false) + private String email; + + @Column(name = ENTITY_PREFIX + "_password", nullable = false) + private String password; +} diff --git a/src/main/java/com/kakao/golajuma/auth/infra/repository/RefreshTokenRepository.java b/src/main/java/com/kakao/golajuma/auth/infra/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..f4bda1d --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/infra/repository/RefreshTokenRepository.java @@ -0,0 +1,42 @@ +package com.kakao.golajuma.auth.infra.repository; + +import com.kakao.golajuma.auth.domain.model.RefreshToken; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Repository; + +@Repository +@Slf4j +public class RefreshTokenRepository { + + private RedisTemplate redisTemplate; + + @Value("${redis.timeToLive}") + private long ttl; + + public RefreshTokenRepository(final RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void save(final RefreshToken refreshToken) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(refreshToken.getUserId(), refreshToken.getRefreshToken()); + redisTemplate.expire(refreshToken.getUserId(), ttl, TimeUnit.SECONDS); + } + + public Optional findById(final Long userId) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + String refreshToken = valueOperations.get(userId); + + if (Objects.isNull(refreshToken)) { + return Optional.empty(); + } + + return Optional.of(new RefreshToken(refreshToken, userId)); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/infra/repository/UserRepository.java b/src/main/java/com/kakao/golajuma/auth/infra/repository/UserRepository.java new file mode 100644 index 0000000..354fae5 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/infra/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.kakao.golajuma.auth.infra.repository; + +import com.kakao.golajuma.auth.infra.entity.UserEntity; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); + + boolean existsByNickname(String nickname); + + Optional findByEmail(String email); +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/controller/AuthInterceptor.java b/src/main/java/com/kakao/golajuma/auth/web/controller/AuthInterceptor.java new file mode 100644 index 0000000..c0e8b2b --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/controller/AuthInterceptor.java @@ -0,0 +1,40 @@ +package com.kakao.golajuma.auth.web.controller; + +import com.kakao.golajuma.auth.domain.token.TokenValidator; +import com.kakao.golajuma.auth.web.support.AuthorizationExtractor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@RequiredArgsConstructor +public class AuthInterceptor implements HandlerInterceptor { + private final TokenValidator tokenValidator; + + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) { + if (CorsUtils.isPreFlightRequest(request)) { + return true; + } + + if (isGetMethod(request)) { + return true; + } + + String token = extractToken(request); + tokenValidator.valid(token); + return true; + } + + private boolean isGetMethod(HttpServletRequest request) { + return request.getMethod().equalsIgnoreCase("GET"); + } + + private String extractToken(HttpServletRequest request) { + return AuthorizationExtractor.extract(request); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/controller/LoginController.java b/src/main/java/com/kakao/golajuma/auth/web/controller/LoginController.java new file mode 100644 index 0000000..3e4c500 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/controller/LoginController.java @@ -0,0 +1,45 @@ +package com.kakao.golajuma.auth.web.controller; + +import com.kakao.golajuma.auth.domain.service.LoginUserService; +import com.kakao.golajuma.auth.web.dto.request.LoginUserRequest; +import com.kakao.golajuma.auth.web.dto.response.TokenResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponseBody.SuccessBody; +import com.kakao.golajuma.common.support.respnose.ApiResponseGenerator; +import com.kakao.golajuma.common.support.respnose.MessageCode; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +public class LoginController { + private static final String REFRESH_TOKEN = "refreshToken"; + private static final int REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60; + private final LoginUserService loginUserUseCase; + + @PostMapping("/login") + public ApiResponse> signIn( + @RequestBody @Valid LoginUserRequest request) { + final TokenResponse tokenResponse = loginUserUseCase.execute(request); + final ResponseCookie cookie = putTokenInCookie(tokenResponse); + return ApiResponseGenerator.success( + tokenResponse, HttpStatus.OK, MessageCode.CREATE, cookie.toString()); + } + + private ResponseCookie putTokenInCookie(final TokenResponse tokenResponse) { + return ResponseCookie.from(REFRESH_TOKEN, tokenResponse.getRefreshToken()) + .maxAge(REFRESH_TOKEN_EXPIRATION) + .path("/") + .sameSite("None") + .secure(true) + .httpOnly(true) + .build(); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/controller/SignUpController.java b/src/main/java/com/kakao/golajuma/auth/web/controller/SignUpController.java new file mode 100644 index 0000000..7be9268 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/controller/SignUpController.java @@ -0,0 +1,28 @@ +package com.kakao.golajuma.auth.web.controller; + +import com.kakao.golajuma.auth.domain.service.SaveUserService; +import com.kakao.golajuma.auth.web.dto.request.SaveUserRequest; +import com.kakao.golajuma.common.support.respnose.ApiResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponseBody.SuccessBody; +import com.kakao.golajuma.common.support.respnose.ApiResponseGenerator; +import com.kakao.golajuma.common.support.respnose.MessageCode; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +public class SignUpController { + private final SaveUserService saveUserService; + + @PostMapping("/signup") + public ApiResponse> signUp(@RequestBody @Valid SaveUserRequest request) { + saveUserService.execute(request); + return ApiResponseGenerator.success(HttpStatus.CREATED, MessageCode.CREATE); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/controller/ValidController.java b/src/main/java/com/kakao/golajuma/auth/web/controller/ValidController.java new file mode 100644 index 0000000..c0e1efd --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/controller/ValidController.java @@ -0,0 +1,38 @@ +package com.kakao.golajuma.auth.web.controller; + +import com.kakao.golajuma.auth.domain.service.ValidEmailService; +import com.kakao.golajuma.auth.domain.service.ValidNicknameService; +import com.kakao.golajuma.auth.web.dto.request.ValidEmailRequest; +import com.kakao.golajuma.auth.web.dto.request.ValidNicknameRequest; +import com.kakao.golajuma.common.support.respnose.ApiResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponseBody.SuccessBody; +import com.kakao.golajuma.common.support.respnose.ApiResponseGenerator; +import com.kakao.golajuma.common.support.respnose.MessageCode; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +public class ValidController { + private final ValidEmailService validEmailService; + private final ValidNicknameService validNicknameService; + + @PostMapping("/email-check") + public ApiResponse> validEmail(@RequestBody @Valid ValidEmailRequest request) { + validEmailService.execute(request); + return ApiResponseGenerator.success(HttpStatus.CREATED, MessageCode.CREATE); + } + + @PostMapping("/nickname-check") + public ApiResponse> validNickname( + @RequestBody @Valid ValidNicknameRequest request) { + validNicknameService.execute(request); + return ApiResponseGenerator.success(HttpStatus.CREATED, MessageCode.CREATE); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/dto/converter/TokenConverter.java b/src/main/java/com/kakao/golajuma/auth/web/dto/converter/TokenConverter.java new file mode 100644 index 0000000..e5a3fc0 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/dto/converter/TokenConverter.java @@ -0,0 +1,19 @@ +package com.kakao.golajuma.auth.web.dto.converter; + +import com.kakao.golajuma.auth.web.dto.response.TokenResponse; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TokenConverter { + + public TokenResponse from(String accessToken, Date expiredTime, String refreshToken) { + return TokenResponse.builder() + .accessToken(accessToken) + .expiredTime(expiredTime) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/dto/request/LoginUserRequest.java b/src/main/java/com/kakao/golajuma/auth/web/dto/request/LoginUserRequest.java new file mode 100644 index 0000000..1a8231d --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/dto/request/LoginUserRequest.java @@ -0,0 +1,24 @@ +package com.kakao.golajuma.auth.web.dto.request; + +import com.kakao.golajuma.auth.web.supplier.EmailSupplier; +import com.kakao.golajuma.common.marker.AbstractRequestDto; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class LoginUserRequest implements AbstractRequestDto, EmailSupplier { + + @NotNull @Email private String email; + + @NotNull + @Size(min = 8) + private String password; +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/dto/request/SaveUserRequest.java b/src/main/java/com/kakao/golajuma/auth/web/dto/request/SaveUserRequest.java new file mode 100644 index 0000000..94e8911 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/dto/request/SaveUserRequest.java @@ -0,0 +1,21 @@ +package com.kakao.golajuma.auth.web.dto.request; + +import com.kakao.golajuma.auth.web.supplier.EmailSupplier; +import com.kakao.golajuma.auth.web.supplier.NicknameSupplier; +import com.kakao.golajuma.common.marker.AbstractRequestDto; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class SaveUserRequest implements AbstractRequestDto, EmailSupplier, NicknameSupplier { + @NotNull private String nickname; + @NotNull @Email private String email; + @NotNull private String password; +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/dto/request/ValidEmailRequest.java b/src/main/java/com/kakao/golajuma/auth/web/dto/request/ValidEmailRequest.java new file mode 100644 index 0000000..d065eb8 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/dto/request/ValidEmailRequest.java @@ -0,0 +1,16 @@ +package com.kakao.golajuma.auth.web.dto.request; + +import com.kakao.golajuma.auth.web.supplier.EmailSupplier; +import com.kakao.golajuma.common.marker.AbstractRequestDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class ValidEmailRequest implements AbstractRequestDto, EmailSupplier { + String email; +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/dto/request/ValidNicknameRequest.java b/src/main/java/com/kakao/golajuma/auth/web/dto/request/ValidNicknameRequest.java new file mode 100644 index 0000000..47754ce --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/dto/request/ValidNicknameRequest.java @@ -0,0 +1,16 @@ +package com.kakao.golajuma.auth.web.dto.request; + +import com.kakao.golajuma.auth.web.supplier.NicknameSupplier; +import com.kakao.golajuma.common.marker.AbstractRequestDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class ValidNicknameRequest implements AbstractRequestDto, NicknameSupplier { + String nickname; +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/dto/response/TokenResponse.java b/src/main/java/com/kakao/golajuma/auth/web/dto/response/TokenResponse.java new file mode 100644 index 0000000..60d631b --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/dto/response/TokenResponse.java @@ -0,0 +1,20 @@ +package com.kakao.golajuma.auth.web.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kakao.golajuma.common.marker.AbstractResponseDto; +import java.util.Date; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class TokenResponse implements AbstractResponseDto { + + private String accessToken; + private Date expiredTime; + @JsonIgnore private String refreshToken; +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/supplier/EmailSupplier.java b/src/main/java/com/kakao/golajuma/auth/web/supplier/EmailSupplier.java new file mode 100644 index 0000000..ce26bf2 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/supplier/EmailSupplier.java @@ -0,0 +1,6 @@ +package com.kakao.golajuma.auth.web.supplier; + +public interface EmailSupplier { + + String getEmail(); +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/supplier/NicknameSupplier.java b/src/main/java/com/kakao/golajuma/auth/web/supplier/NicknameSupplier.java new file mode 100644 index 0000000..57caa8e --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/supplier/NicknameSupplier.java @@ -0,0 +1,5 @@ +package com.kakao.golajuma.auth.web.supplier; + +public interface NicknameSupplier { + String getNickname(); +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/support/AuthenticationPrincipalArgumentResolver.java b/src/main/java/com/kakao/golajuma/auth/web/support/AuthenticationPrincipalArgumentResolver.java new file mode 100644 index 0000000..09259b4 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/support/AuthenticationPrincipalArgumentResolver.java @@ -0,0 +1,33 @@ +package com.kakao.golajuma.auth.web.support; + +import com.kakao.golajuma.auth.domain.token.TokenResolver; +import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@RequiredArgsConstructor +public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + private final TokenResolver tokenResolver; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Login.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) + throws Exception { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + String token = AuthorizationExtractor.extract(Objects.requireNonNull(request)); + return tokenResolver.getUserInfo(token); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/support/AuthorizationExtractor.java b/src/main/java/com/kakao/golajuma/auth/web/support/AuthorizationExtractor.java new file mode 100644 index 0000000..cac9d33 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/support/AuthorizationExtractor.java @@ -0,0 +1,26 @@ +package com.kakao.golajuma.auth.web.support; + +import com.kakao.golajuma.auth.domain.exception.AuthorizationException; +import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; + +public class AuthorizationExtractor { + private static final String BEARER_TYPE = "Bearer"; + + public static String extract(final HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + if (Objects.isNull(authorization)) { + throw new AuthorizationException("인증 토큰이 존재하지 않습니다"); + } + validateAuthorizationFormat(authorization); + return authorization.substring(BEARER_TYPE.length()); + } + + private static void validateAuthorizationFormat(String authorization) { + if (authorization.toLowerCase().startsWith(BEARER_TYPE.toLowerCase())) { + return; + } + throw new AuthorizationException("token 형식이 알맞지 않습니다"); + } +} diff --git a/src/main/java/com/kakao/golajuma/auth/web/support/Login.java b/src/main/java/com/kakao/golajuma/auth/web/support/Login.java new file mode 100644 index 0000000..dd533f5 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/support/Login.java @@ -0,0 +1,10 @@ +package com.kakao.golajuma.auth.web.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Login {} diff --git a/src/main/java/com/kakao/golajuma/auth/web/support/TokenExtractor.java b/src/main/java/com/kakao/golajuma/auth/web/support/TokenExtractor.java new file mode 100644 index 0000000..ee9c49b --- /dev/null +++ b/src/main/java/com/kakao/golajuma/auth/web/support/TokenExtractor.java @@ -0,0 +1,7 @@ +package com.kakao.golajuma.auth.web.support; + +import javax.servlet.http.HttpServletRequest; + +public interface TokenExtractor { + String extract(HttpServletRequest request); +} diff --git a/src/main/java/com/kakao/golajuma/comment/domain/exception/NoDecisionException.java b/src/main/java/com/kakao/golajuma/comment/domain/exception/NoDecisionException.java new file mode 100644 index 0000000..35ca230 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/domain/exception/NoDecisionException.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma.comment.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class NoDecisionException extends BusinessException { + + public NoDecisionException(String message, HttpStatus httpStatus) { + super(message, httpStatus); + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/domain/exception/NoOwnershipException.java b/src/main/java/com/kakao/golajuma/comment/domain/exception/NoOwnershipException.java new file mode 100644 index 0000000..ec8bc3a --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/domain/exception/NoOwnershipException.java @@ -0,0 +1,10 @@ +package com.kakao.golajuma.comment.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class NoOwnershipException extends BusinessException { + public NoOwnershipException(String message, HttpStatus httpStatus) { + super(message, httpStatus); + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/domain/exception/NullPointerException.java b/src/main/java/com/kakao/golajuma/comment/domain/exception/NullPointerException.java new file mode 100644 index 0000000..608d8d9 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/domain/exception/NullPointerException.java @@ -0,0 +1,10 @@ +package com.kakao.golajuma.comment.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class NullPointerException extends BusinessException { + public NullPointerException(String message, HttpStatus httpStatus) { + super(message, httpStatus); + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/domain/service/CommentService.java b/src/main/java/com/kakao/golajuma/comment/domain/service/CommentService.java new file mode 100644 index 0000000..ea0ae01 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/domain/service/CommentService.java @@ -0,0 +1,112 @@ +package com.kakao.golajuma.comment.domain.service; + +import com.kakao.golajuma.comment.domain.exception.NoOwnershipException; +import com.kakao.golajuma.comment.domain.exception.NullPointerException; +import com.kakao.golajuma.comment.infra.entity.CommentEntity; +import com.kakao.golajuma.comment.infra.repository.CommentRepository; +import com.kakao.golajuma.comment.web.dto.request.SaveCommentRequest; +import com.kakao.golajuma.comment.web.dto.request.UpdateCommentRequest; +import com.kakao.golajuma.comment.web.dto.response.ReadCommentDto; +import com.kakao.golajuma.comment.web.dto.response.ReadCommentListResponse; +import com.kakao.golajuma.comment.web.dto.response.SaveCommentResponse; +import com.kakao.golajuma.comment.web.dto.response.UpdateCommentResponse; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class CommentService { + + private final CommentRepository commentRepository; + + @Transactional + public SaveCommentResponse create(SaveCommentRequest requestDto, Long voteId, Long userId) { + // 1. 투표한 유저인지 확인 -decision이 나와야함 + // decisionRepository.findByUserIdVoteId(voteId,userId).orElseThrow(new NoDecisionException("투표 + // 후에 가능합니다.", HttpStatus.UNAUTHORIZED)); + + // 저장 + CommentEntity commentEntity = requestDto.toEntity(voteId, userId); + commentRepository.save(commentEntity); + + // return + SaveCommentResponse response = new SaveCommentResponse(commentEntity, true, 1); + return response; + } + + // 페이지 구현하기 안해둠 + public ReadCommentListResponse readList(Long voteId, Long userId) { + // 1. 투표한 유저인지 확인 -decision이 나와야함 + // decisionRepository.findByUserIdVoteId(voteId,userId).orElseThrow(new NoDecisionException("투표 + // 후에 가능합니다.", HttpStatus.UNAUTHORIZED)); + + // 가져오기 + List commentEntityList = commentRepository.findByVoteId(voteId); + + // 2. 유저이름 가져오기 로직 + List readCommentDtoList = new ArrayList<>(); + + for (CommentEntity commentEntity : commentEntityList) { + // for문 안에서만 사용하는 변수 선언 + boolean isOwner; + + Long id = commentEntity.getUserId(); + System.out.println(id); + String username = "asdf"; // 데이터베이스에서 유저 닉네임 가져오기 위한 레포지토리가 들어갈 부분 - 미완성 + + // 3. 주인 판별 로직 + isOwner = userId.equals(id); + ReadCommentDto readCommentDto = new ReadCommentDto(commentEntity, isOwner, username); + readCommentDtoList.add(readCommentDto); + } + + ReadCommentListResponse response = new ReadCommentListResponse(readCommentDtoList); + + return response; + } + + @Transactional + public UpdateCommentResponse update( + UpdateCommentRequest requestDto, Long commentId, Long userId) { + // 1. 존재하는 댓글인지 확인 + CommentEntity commentEntity = + commentRepository + .findById(commentId) + .orElseThrow(() -> new NullPointerException("댓글이 존재하지 않습니다.", HttpStatus.BAD_REQUEST)); + + // 2. 본인의 comment인지 확인 + if (!commentEntity.isUser(userId)) { + throw new NoOwnershipException("접근할 수 없습니다.", HttpStatus.FORBIDDEN); + } + + // repository update >> entity update + String newContent = requestDto.getContent(); + // setter는 명확하지 않기 때문에 댓글을 업데이트를 한다는걸 명시한 새로운 메서드 정의 + commentEntity.updateContent(newContent); + String username = "asdf"; + + UpdateCommentResponse response = new UpdateCommentResponse(commentEntity, true, username); + return response; + } + + @Transactional + public void delete(Long commentId, Long userId) { + // 1. 존재하는 댓글인지 확인 + CommentEntity commentEntity = + commentRepository + .findById(commentId) + .orElseThrow(() -> new NullPointerException("댓글이 존재하지 않습니다.", HttpStatus.BAD_REQUEST)); + + // 2. 본인의 comment인지 확인 + if (!commentEntity.isUser(userId)) { + throw new NoOwnershipException("접근할 수 없습니다.", HttpStatus.FORBIDDEN); + } + // 삭제로직 + commentEntity.delete(); + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/domain/utils/CommentCommomLogic.java b/src/main/java/com/kakao/golajuma/comment/domain/utils/CommentCommomLogic.java new file mode 100644 index 0000000..8956051 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/domain/utils/CommentCommomLogic.java @@ -0,0 +1,29 @@ +// package com.kakao.golajuma.comment.domain.utils; +// +// import com.kakao.golajuma.comment.infra.entity.CommentEntity; +// import com.kakao.golajuma.comment.web.dto.response.ReadCommentDto; +// +// import java.util.ArrayList; +// import java.util.List; +// +// public class CommentCommomLogic { +// boolean isOwner; +// //공통 dto채워넣기 +// public List entityToDtoListWithUserData(){ +// List readCommentDtoList = new ArrayList<>(); +// +// for (CommentEntity commentEntity : commentEntityList) { +// Long id = commentEntity.getUserId(); +// String username = "asdf"; // 데이터베이스에서 유저 닉네임 가져오기 위한 레포지토리가 들어갈 부분 - 미완성 +// //주인 판별 로직 +// if (userId == id) +// isOwner = true; +// else +// isOwner = false; +// readCommentDtoList.add(new ReadCommentDto(commentEntity, isOwner, username)); +// } +// return readCommentDtoList; +// } +// } + +// 유저 데이터를 끌고와 자신이 맞는지 확인하거나, 투표인증은 우리 서비스의 거의 대부분에 쓰임 따로 util로 마련하면 어떨까 하는 생각 diff --git a/src/main/java/com/kakao/golajuma/comment/domain/utils/CommentValidators.java b/src/main/java/com/kakao/golajuma/comment/domain/utils/CommentValidators.java new file mode 100644 index 0000000..8148c62 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/domain/utils/CommentValidators.java @@ -0,0 +1,16 @@ +// package com.kakao.golajuma.comment.domain.utils; +// +// import com.kakao.golajuma.common.marker.AbstractRequestDto; +// +// public class CommentValidators{ +//// 투표했는지 안했는지 검증기 +// public static AbstractRequestDto voter(AbstractRequestDto requestDto){ +// //현수형 레포지토리에서 뺏어오기 +// return requestDto; +// } +//// 본인 소유의 댓글이 맞는지 검증기 +// public static AbstractRequestDto commentHost(AbstractRequestDto requestDto){ +// //유저id == 댓글의 유저id인지 검사 +// return requestDto; +// } +// } diff --git a/src/main/java/com/kakao/golajuma/comment/domain/utils/SearchUserData.java b/src/main/java/com/kakao/golajuma/comment/domain/utils/SearchUserData.java new file mode 100644 index 0000000..662d937 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/domain/utils/SearchUserData.java @@ -0,0 +1,24 @@ +package com.kakao.golajuma.comment.domain.utils; + +// 유저id로 판별하고 불러와야하는 데이터들이 있음 +// public class SearchUserData { +// //넣고자 하는 dto +// private AbstractDto dto; +// +// //유저 데이터 가져오기 로직 +// public List start(AbstractDto dto, List entityList) { +// List DtoList = new ArrayList<>(); +// +// for (CommentEntity entity : entityList) { +// Long id = entity.getUserId(); +// String username = "asdf"; // 데이터베이스에서 유저 닉네임 가져오기 위한 레포지토리가 들어갈 부분 - 미완성 +// //주인 판별 로직 +// if (userId == id) +// isOwner = true; +// else +// isOwner = false; +// readCommentDtoList.add(new ReadCommentDto(commentEntity, isOwner, username)); +// } +// return DtoList; +// } +// } diff --git a/src/main/java/com/kakao/golajuma/comment/infra/entity/CommentEntity.java b/src/main/java/com/kakao/golajuma/comment/infra/entity/CommentEntity.java new file mode 100644 index 0000000..c14067b --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/infra/entity/CommentEntity.java @@ -0,0 +1,47 @@ +package com.kakao.golajuma.comment.infra.entity; + +import static com.kakao.golajuma.comment.infra.entity.CommentEntity.ENTITY_PREFIX; + +import com.kakao.golajuma.common.BaseEntity; +import javax.persistence.*; +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +@Table(name = ENTITY_PREFIX + "_tb") +public class CommentEntity extends BaseEntity { + + public static final String ENTITY_PREFIX = "comment"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = ENTITY_PREFIX + "_id", unique = true, nullable = false) + private Long id; + + @Column(name = ENTITY_PREFIX + "_vote_id", length = 15, nullable = false) + private Long voteId; + + @Column(name = ENTITY_PREFIX + "_user_id", length = 15, nullable = false) + private Long userId; + + @Column(name = ENTITY_PREFIX + "_content", length = 255, nullable = false) + private String content; + + @Builder + public CommentEntity(long voteId, long userId, String content) { + this.voteId = voteId; + this.userId = userId; + this.content = content; + } + public void updateContent(String content) { + this.content = content; + } + + public Boolean isUser(Long userId) { + return this.userId.equals(userId); + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/infra/repository/CommentRepository.java b/src/main/java/com/kakao/golajuma/comment/infra/repository/CommentRepository.java new file mode 100644 index 0000000..33efaf9 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/infra/repository/CommentRepository.java @@ -0,0 +1,19 @@ +package com.kakao.golajuma.comment.infra.repository; + +import com.kakao.golajuma.comment.infra.entity.CommentEntity; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface CommentRepository extends JpaRepository { + // 개개인의 댓글을 가져오기 + @Query("Select c from CommentEntity c where c.id = :commentId and c.deleted = false") + Optional findById(@Param("commentId") Long commentId); + // 투표에 따른 댓글 리스트 가져오기 + @Query("select c from CommentEntity c where c.voteId = :voteId and c.deleted = false") + List findByVoteId(@Param("voteId") long voteId); +} diff --git a/src/main/java/com/kakao/golajuma/comment/web/controller/CommentController.java b/src/main/java/com/kakao/golajuma/comment/web/controller/CommentController.java new file mode 100644 index 0000000..83aa88b --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/web/controller/CommentController.java @@ -0,0 +1,57 @@ +package com.kakao.golajuma.comment.web.controller; + +import com.kakao.golajuma.auth.web.support.Login; +import com.kakao.golajuma.comment.domain.service.CommentService; +import com.kakao.golajuma.comment.web.dto.request.SaveCommentRequest; +import com.kakao.golajuma.comment.web.dto.request.UpdateCommentRequest; +import com.kakao.golajuma.comment.web.dto.response.ReadCommentListResponse; +import com.kakao.golajuma.comment.web.dto.response.SaveCommentResponse; +import com.kakao.golajuma.comment.web.dto.response.UpdateCommentResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponseBody; +import com.kakao.golajuma.common.support.respnose.ApiResponseGenerator; +import com.kakao.golajuma.common.support.respnose.MessageCode; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/votes/{voteId}/comments") +public class CommentController { + + private final CommentService commentService; + + @GetMapping + public ApiResponse> readList( + @PathVariable Long voteId, @Login Long userId) { + ReadCommentListResponse responseData = commentService.readList(voteId, userId); + return ApiResponseGenerator.success(responseData, HttpStatus.OK, MessageCode.CREATE); + } + + @PostMapping + public ApiResponse> create( + @PathVariable Long voteId, + @Valid @RequestBody SaveCommentRequest requestDto, + @Login Long userId) { + SaveCommentResponse responseData = commentService.create(requestDto, voteId, userId); + return ApiResponseGenerator.success(responseData, HttpStatus.OK, MessageCode.CREATE); + } + + @PutMapping("/{commentId}") + public ApiResponse> update( + @PathVariable Long commentId, + @Valid @RequestBody UpdateCommentRequest requestDto, + @Login Long userId) { + UpdateCommentResponse responseData = commentService.update(requestDto, commentId, userId); + return ApiResponseGenerator.success(responseData, HttpStatus.OK, MessageCode.CREATE); + } + + @DeleteMapping("/{commentId}") + public ApiResponse> delete( + @PathVariable Long commentId, @Login Long userId) { + commentService.delete(commentId, userId); + return ApiResponseGenerator.success(HttpStatus.OK, MessageCode.CREATE); + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/web/dto/request/SaveCommentRequest.java b/src/main/java/com/kakao/golajuma/comment/web/dto/request/SaveCommentRequest.java new file mode 100644 index 0000000..231833a --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/web/dto/request/SaveCommentRequest.java @@ -0,0 +1,22 @@ +package com.kakao.golajuma.comment.web.dto.request; + +import com.kakao.golajuma.comment.infra.entity.CommentEntity; +import com.kakao.golajuma.common.marker.AbstractRequestDto; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class SaveCommentRequest implements AbstractRequestDto { + + @NotBlank // null, 빈 문자열, 스페이스만 포함한 문자열 불가 + @Size(min = 1, max = 255) // 최소 길이, 최대 길이 제한 + private String content; + + public CommentEntity toEntity(long voteId, long userId) { + return CommentEntity.builder().voteId(voteId).userId(userId).content(this.content).build(); + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/web/dto/request/UpdateCommentRequest.java b/src/main/java/com/kakao/golajuma/comment/web/dto/request/UpdateCommentRequest.java new file mode 100644 index 0000000..3021f19 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/web/dto/request/UpdateCommentRequest.java @@ -0,0 +1,17 @@ +package com.kakao.golajuma.comment.web.dto.request; + +import com.kakao.golajuma.common.marker.AbstractRequestDto; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class UpdateCommentRequest implements AbstractRequestDto { + + @NotBlank // null, 빈 문자열, 스페이스만 포함한 문자열 불가 + @Size(min = 1, max = 255) // 최소 길이, 최대 길이 제한 + private String content; +} diff --git a/src/main/java/com/kakao/golajuma/comment/web/dto/response/DeleteCommentResponse.java b/src/main/java/com/kakao/golajuma/comment/web/dto/response/DeleteCommentResponse.java new file mode 100644 index 0000000..af7b100 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/web/dto/response/DeleteCommentResponse.java @@ -0,0 +1,5 @@ +package com.kakao.golajuma.comment.web.dto.response; + +import com.kakao.golajuma.common.marker.AbstractResponseDto; + +public class DeleteCommentResponse implements AbstractResponseDto {} diff --git a/src/main/java/com/kakao/golajuma/comment/web/dto/response/ReadCommentDto.java b/src/main/java/com/kakao/golajuma/comment/web/dto/response/ReadCommentDto.java new file mode 100644 index 0000000..e6a8b19 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/web/dto/response/ReadCommentDto.java @@ -0,0 +1,27 @@ +package com.kakao.golajuma.comment.web.dto.response; + +import com.kakao.golajuma.comment.infra.entity.CommentEntity; +import com.kakao.golajuma.common.marker.AbstractResponseDto; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ReadCommentDto implements AbstractResponseDto { + private Long id; + private Boolean isOwner; + private String username; + private String content; + private LocalDateTime createTime; + + public ReadCommentDto(CommentEntity entity, Boolean isOwner, String username) { + this.id = entity.getId(); + this.isOwner = isOwner; + this.username = username; + this.content = entity.getContent(); + this.createTime = entity.getUpdatedDate(); + // 다른 필드 복사 + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/web/dto/response/ReadCommentListResponse.java b/src/main/java/com/kakao/golajuma/comment/web/dto/response/ReadCommentListResponse.java new file mode 100644 index 0000000..c336ba5 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/web/dto/response/ReadCommentListResponse.java @@ -0,0 +1,12 @@ +package com.kakao.golajuma.comment.web.dto.response; + +import com.kakao.golajuma.common.marker.AbstractResponseDto; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class ReadCommentListResponse implements AbstractResponseDto { + private List readCommentDtoList; +} diff --git a/src/main/java/com/kakao/golajuma/comment/web/dto/response/SaveCommentResponse.java b/src/main/java/com/kakao/golajuma/comment/web/dto/response/SaveCommentResponse.java new file mode 100644 index 0000000..a2975d9 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/web/dto/response/SaveCommentResponse.java @@ -0,0 +1,9 @@ +package com.kakao.golajuma.comment.web.dto.response; + +import com.kakao.golajuma.comment.infra.entity.CommentEntity; + +public class SaveCommentResponse extends ReadCommentDto { + public SaveCommentResponse(CommentEntity commentEntity, Boolean isOwner, String username) { + super(commentEntity, isOwner, username); + } +} diff --git a/src/main/java/com/kakao/golajuma/comment/web/dto/response/UpdateCommentResponse.java b/src/main/java/com/kakao/golajuma/comment/web/dto/response/UpdateCommentResponse.java new file mode 100644 index 0000000..0aac64e --- /dev/null +++ b/src/main/java/com/kakao/golajuma/comment/web/dto/response/UpdateCommentResponse.java @@ -0,0 +1,9 @@ +package com.kakao.golajuma.comment.web.dto.response; + +import com.kakao.golajuma.comment.infra.entity.CommentEntity; + +public class UpdateCommentResponse extends ReadCommentDto { + public UpdateCommentResponse(CommentEntity commentEntity, Boolean isOwner, String username) { + super(commentEntity, isOwner, username); + } +} diff --git a/src/main/java/com/kakao/golajuma/common/BaseEntity.java b/src/main/java/com/kakao/golajuma/common/BaseEntity.java new file mode 100644 index 0000000..a5665ae --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/BaseEntity.java @@ -0,0 +1,40 @@ +package com.kakao.golajuma.common; + +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@MappedSuperclass +@EntityListeners({AuditingEntityListener.class, SoftDeleteListener.class}) +@SuperBuilder(toBuilder = true) +public class BaseEntity { + + @CreatedDate + @Column(nullable = false, updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedDate; + + @Builder.Default + @Column(nullable = false) + private Boolean deleted = false; + + public void delete() { + this.deleted = true; + } +} diff --git a/src/main/java/com/kakao/golajuma/common/SoftDeleteListener.java b/src/main/java/com/kakao/golajuma/common/SoftDeleteListener.java new file mode 100644 index 0000000..b6d4908 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/SoftDeleteListener.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma.common; + +import javax.persistence.PreRemove; + +public class SoftDeleteListener { + + @PreRemove + private void preRemove(BaseEntity entity) { + entity.delete(); + } +} diff --git a/src/main/java/com/kakao/golajuma/common/exception/BusinessException.java b/src/main/java/com/kakao/golajuma/common/exception/BusinessException.java new file mode 100644 index 0000000..dbf0b99 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/exception/BusinessException.java @@ -0,0 +1,14 @@ +package com.kakao.golajuma.common.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class BusinessException extends RuntimeException { + private final HttpStatus httpStatus; + + public BusinessException(String message, HttpStatus httpStatus) { + super(message); + this.httpStatus = httpStatus; + } +} diff --git a/src/main/java/com/kakao/golajuma/common/exception/GlobalExceptionHandler.java b/src/main/java/com/kakao/golajuma/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..df64fed --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,54 @@ +package com.kakao.golajuma.common.exception; + +import com.kakao.golajuma.common.support.respnose.ApiResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponseBody.FailureBody; +import com.kakao.golajuma.common.support.respnose.ApiResponseGenerator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** javax.validation.Valid 또는 @Validated binding error가 발생할 경우 */ + @ExceptionHandler(BindException.class) + protected ApiResponse handleBindException(BindException e) { + log.error("handleBindException", e); + return ApiResponseGenerator.fail(e.getMessage(), HttpStatus.BAD_REQUEST); + } + + /** 주로 @RequestParam enum으로 binding 못했을 경우 발생 */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + protected ApiResponse handleMethodArgumentTypeMismatchException( + MethodArgumentTypeMismatchException e) { + log.error("handleMethodArgumentTypeMismatchException", e); + return ApiResponseGenerator.fail(e.getMessage(), HttpStatus.BAD_REQUEST); + } + + /** 지원하지 않은 HTTP method 호출 할 경우 발생 */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + protected ApiResponse handleHttpRequestMethodNotSupportedException( + HttpRequestMethodNotSupportedException e) { + log.error("handleHttpRequestMethodNotSupportedException", e); + return ApiResponseGenerator.fail(e.getMessage(), HttpStatus.METHOD_NOT_ALLOWED); + } + + /** 비즈니스 로직 실행 중 오류 발생 */ + @ExceptionHandler(value = {BusinessException.class}) + protected ApiResponse handleConflict(BusinessException e) { + log.error("BusinessException", e); + return ApiResponseGenerator.fail(e.getMessage(), e.getHttpStatus()); + } + + /** 나머지 예외 발생 */ + @ExceptionHandler(Exception.class) + protected ApiResponse handleException(Exception e) { + log.error("Exception", e); + return ApiResponseGenerator.fail(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/kakao/golajuma/common/marker/AbstractDto.java b/src/main/java/com/kakao/golajuma/common/marker/AbstractDto.java new file mode 100644 index 0000000..a779491 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/marker/AbstractDto.java @@ -0,0 +1,3 @@ +package com.kakao.golajuma.common.marker; + +public interface AbstractDto {} diff --git a/src/main/java/com/kakao/golajuma/common/marker/AbstractRequestDto.java b/src/main/java/com/kakao/golajuma/common/marker/AbstractRequestDto.java new file mode 100644 index 0000000..f19366e --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/marker/AbstractRequestDto.java @@ -0,0 +1,3 @@ +package com.kakao.golajuma.common.marker; + +public interface AbstractRequestDto extends AbstractDto {} diff --git a/src/main/java/com/kakao/golajuma/common/marker/AbstractResponseDto.java b/src/main/java/com/kakao/golajuma/common/marker/AbstractResponseDto.java new file mode 100644 index 0000000..cb77628 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/marker/AbstractResponseDto.java @@ -0,0 +1,3 @@ +package com.kakao.golajuma.common.marker; + +public interface AbstractResponseDto extends AbstractDto {} diff --git a/src/main/java/com/kakao/golajuma/common/support/converter/AbstractEntityConverter.java b/src/main/java/com/kakao/golajuma/common/support/converter/AbstractEntityConverter.java new file mode 100644 index 0000000..81287c8 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/support/converter/AbstractEntityConverter.java @@ -0,0 +1,8 @@ +package com.kakao.golajuma.common.support.converter; + +import com.kakao.golajuma.common.BaseEntity; +import com.kakao.golajuma.common.marker.AbstractDto; + +public interface AbstractEntityConverter { + T toEntity(R t); +} diff --git a/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponse.java b/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponse.java new file mode 100644 index 0000000..3912bc9 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponse.java @@ -0,0 +1,22 @@ +package com.kakao.golajuma.common.support.respnose; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; + +@Getter +public class ApiResponse extends ResponseEntity { + + public ApiResponse(final HttpStatus status) { + super(status); + } + + public ApiResponse(final B body, final HttpStatus status) { + super(body, status); + } + + public ApiResponse(final B body, MultiValueMap headers, HttpStatus status) { + super(body, headers, status); + } +} diff --git a/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponseBody.java b/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponseBody.java new file mode 100644 index 0000000..8c8ae11 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponseBody.java @@ -0,0 +1,23 @@ +package com.kakao.golajuma.common.support.respnose; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +public class ApiResponseBody { + @Getter + @AllArgsConstructor + public static class FailureBody implements Serializable { + + private String code; + private String message; + } + + @Getter + @AllArgsConstructor + public static class SuccessBody implements Serializable { + private D data; + private String message; + private String code; + } +} diff --git a/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponseGenerator.java b/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponseGenerator.java new file mode 100644 index 0000000..000224e --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/support/respnose/ApiResponseGenerator.java @@ -0,0 +1,61 @@ +package com.kakao.golajuma.common.support.respnose; + +import lombok.experimental.UtilityClass; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@UtilityClass +public class ApiResponseGenerator { + public static ApiResponse> success( + final HttpStatus status, MessageCode code) { + return new ApiResponse<>( + new ApiResponseBody.SuccessBody<>(null, code.getMessage(), code.getCode()), status); + } + + public static ApiResponse> success( + final D data, final HttpStatus status, MessageCode code) { + return new ApiResponse<>( + new ApiResponseBody.SuccessBody<>(data, code.getMessage(), code.getCode()), status); + } + + public static ApiResponse> success( + final D data, final HttpStatus status, MessageCode code, String cookieValue) { + return new ApiResponse<>( + new ApiResponseBody.SuccessBody<>(data, code.getMessage(), code.getCode()), + setCookie(cookieValue), + status); + } + + public static ApiResponse>> success( + final Page data, final HttpStatus status, final MessageCode code) { + return new ApiResponse<>( + new ApiResponseBody.SuccessBody<>( + new PageResponse<>(data), code.getMessage(), code.getCode()), + status); + } + + public static ApiResponse fail( + final ApiResponseBody.FailureBody body, final HttpStatus status) { + return new ApiResponse<>(body, status); + } + + public static ApiResponse fail( + final String message, final HttpStatus status) { + return new ApiResponse<>( + new ApiResponseBody.FailureBody(String.valueOf(status.value()), message), status); + } + + public static ApiResponse fail( + final String code, final String message, final HttpStatus status) { + return new ApiResponse<>(new ApiResponseBody.FailureBody(code, message), status); + } + + private MultiValueMap setCookie(String cookieValue) { + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("Set-Cookie", cookieValue); + + return map; + } +} diff --git a/src/main/java/com/kakao/golajuma/common/support/respnose/MessageCode.java b/src/main/java/com/kakao/golajuma/common/support/respnose/MessageCode.java new file mode 100644 index 0000000..2eb6809 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/support/respnose/MessageCode.java @@ -0,0 +1,20 @@ +package com.kakao.golajuma.common.support.respnose; + +public enum MessageCode { + CREATE("200", "생성 성공"); + private final String code; + private final String message; + + MessageCode(String code, String message) { + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/kakao/golajuma/common/support/respnose/PageResponse.java b/src/main/java/com/kakao/golajuma/common/support/respnose/PageResponse.java new file mode 100644 index 0000000..6d84aa8 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/support/respnose/PageResponse.java @@ -0,0 +1,30 @@ +package com.kakao.golajuma.common.support.respnose; + +import java.util.List; +import lombok.Getter; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +@Getter +public class PageResponse { + + /** 페이지를 구성하는 일정 수의 크기 */ + private final int pageSize; + /** 데이터를 가져온 페이지 번호 */ + private final int pageNumber; + /** size 크기에 맞춰 페이징했을 때 나오는 총 페이지 개수 */ + private final int totalPageCount; + /** 전체 데이터 개수 */ + private final Long totalCount; + + private final List data; + + public PageResponse(final Page source) { + final Pageable pageable = source.getPageable(); + this.pageSize = pageable.getPageSize(); + this.pageNumber = pageable.getPageNumber(); + this.totalPageCount = source.getTotalPages(); + this.totalCount = source.getTotalElements(); + this.data = source.getContent(); + } +} diff --git a/src/main/java/com/kakao/golajuma/common/support/respnose/SliceResponse.java b/src/main/java/com/kakao/golajuma/common/support/respnose/SliceResponse.java new file mode 100644 index 0000000..c98b0c5 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/common/support/respnose/SliceResponse.java @@ -0,0 +1,28 @@ +package com.kakao.golajuma.common.support.respnose; + +import java.util.List; +import lombok.Getter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +@Getter +public class SliceResponse { + + /** 현 페이지 데이터 개수 */ + private final int pageSize; + + /** 다음 슬라이스 존재 여부 */ + private final boolean hasNext; + /** 이전 슬라이스 존재 여부 */ + private final boolean hasPrevious; + + private final List data; + + public SliceResponse(final Slice source) { + final Pageable pageable = source.getPageable(); + this.pageSize = pageable.getPageSize(); + this.hasNext = source.hasNext(); + this.hasPrevious = source.hasPrevious(); + this.data = source.getContent(); + } +} diff --git a/src/main/java/com/kakao/golajuma/config/MysqlConfig.java b/src/main/java/com/kakao/golajuma/config/MysqlConfig.java new file mode 100644 index 0000000..510a540 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/config/MysqlConfig.java @@ -0,0 +1,8 @@ +package com.kakao.golajuma.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class MysqlConfig {} diff --git a/src/main/java/com/kakao/golajuma/config/RedisConfig.java b/src/main/java/com/kakao/golajuma/config/RedisConfig.java new file mode 100644 index 0000000..6e144d1 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/config/RedisConfig.java @@ -0,0 +1,45 @@ +package com.kakao.golajuma.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + private final String host; + private final int port; + + public RedisConfig( + @Value("${spring.redis.host}") final String host, + @Value("${spring.redis.port}") final int port) { + this.host = host; + this.port = port; + } + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + redisTemplate.setKeySerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + + redisTemplate.setHashKeySerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setHashValueSerializer(new StringRedisSerializer()); + + redisTemplate.setDefaultSerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} diff --git a/src/main/java/com/kakao/golajuma/config/SecurityConfig.java b/src/main/java/com/kakao/golajuma/config/SecurityConfig.java new file mode 100644 index 0000000..8a24b94 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/config/SecurityConfig.java @@ -0,0 +1,43 @@ +package com.kakao.golajuma.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity +public class SecurityConfig { + private final AuthenticationConfiguration authenticationConfiguration; + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager() throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf().disable(); + http.formLogin().disable(); + http.httpBasic().disable(); + http.cors(); + http.authorizeRequests().antMatchers(HttpMethod.POST, "/users/auth/**").permitAll(); + + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + return http.build(); + } +} diff --git a/src/main/java/com/kakao/golajuma/config/WebMvcConfig.java b/src/main/java/com/kakao/golajuma/config/WebMvcConfig.java new file mode 100644 index 0000000..c4095fe --- /dev/null +++ b/src/main/java/com/kakao/golajuma/config/WebMvcConfig.java @@ -0,0 +1,52 @@ +package com.kakao.golajuma.config; + +import com.kakao.golajuma.auth.domain.token.TokenResolver; +import com.kakao.golajuma.auth.web.controller.AuthInterceptor; +import com.kakao.golajuma.auth.web.support.AuthenticationPrincipalArgumentResolver; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final long MAX_AGE_SECS = 3600; + private final AuthInterceptor authInterceptor; + private final TokenResolver tokenResolver; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry + .addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("*") + .exposedHeaders("Authorization") + .allowCredentials(true) + .maxAge(MAX_AGE_SECS); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry + .addInterceptor(authInterceptor) + .addPathPatterns("/**") + .excludePathPatterns("/api/auth/**"); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(authenticationPrincipalArgumentResolver()); + } + + @Bean + public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() { + return new AuthenticationPrincipalArgumentResolver(tokenResolver); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/domain/exception/CategoryException.java b/src/main/java/com/kakao/golajuma/vote/domain/exception/CategoryException.java new file mode 100644 index 0000000..c1fea16 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/domain/exception/CategoryException.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma.vote.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class CategoryException extends BusinessException { + + public CategoryException(String message) { + super(message, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/domain/exception/NullException.java b/src/main/java/com/kakao/golajuma/vote/domain/exception/NullException.java new file mode 100644 index 0000000..5a7faa6 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/domain/exception/NullException.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma.vote.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class NullException extends BusinessException { + + public NullException(String message) { + super(message, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/domain/exception/OptionNumException.java b/src/main/java/com/kakao/golajuma/vote/domain/exception/OptionNumException.java new file mode 100644 index 0000000..9d6f92c --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/domain/exception/OptionNumException.java @@ -0,0 +1,10 @@ +package com.kakao.golajuma.vote.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class OptionNumException extends BusinessException { + public OptionNumException(String message) { + super(message, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/domain/exception/RequestParamException.java b/src/main/java/com/kakao/golajuma/vote/domain/exception/RequestParamException.java new file mode 100644 index 0000000..51d6756 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/domain/exception/RequestParamException.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma.vote.domain.exception; + +import com.kakao.golajuma.common.exception.BusinessException; +import org.springframework.http.HttpStatus; + +public class RequestParamException extends BusinessException { + + public RequestParamException(String message) { + super(message, HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/domain/service/CreateVoteService.java b/src/main/java/com/kakao/golajuma/vote/domain/service/CreateVoteService.java new file mode 100644 index 0000000..94ecda6 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/domain/service/CreateVoteService.java @@ -0,0 +1,54 @@ +package com.kakao.golajuma.vote.domain.service; + +import com.kakao.golajuma.vote.domain.exception.NullException; +import com.kakao.golajuma.vote.domain.exception.OptionNumException; +import com.kakao.golajuma.vote.infra.entity.Category; +import com.kakao.golajuma.vote.infra.entity.VoteEntity; +import com.kakao.golajuma.vote.infra.repository.OptionJPARepository; +import com.kakao.golajuma.vote.infra.repository.VoteJPARepository; +import com.kakao.golajuma.vote.web.dto.request.CreateVoteRequest; +import com.kakao.golajuma.vote.web.dto.response.CreateVoteResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class CreateVoteService { + + private final VoteJPARepository voteJPARepository; + private final OptionJPARepository optionJPARepository; + + @Transactional + public CreateVoteResponse createVote(CreateVoteRequest requestDto) { + boolean exit; + // 1. 투표 제목이 있는지 확인 후 예외처리 + if (requestDto.getTitle() == null) { + throw new NullException("제목을 입력해주세요"); + } + + // 2. 옵션 개수가 2개 ~ 6개 인지 확인 후 예외처리 + int size = requestDto.getOptions().size(); + if (size < 2 || size > 6) { + throw new OptionNumException("선택지의 개수는 2개 이하 6개 이상이어야 합니다."); + } + // 3. 옵션 이름이 있는지 확인 + for (CreateVoteRequest.OptionDTO option : requestDto.getOptions()) { + if (option.getName() == null) { + throw new NullException("옵션 이름을 입력해주세요."); + } + } + + // 4. 카테고리가 맞는지 확인 + Category.findCategory(requestDto.getCategory()); + + System.out.println(requestDto); + VoteEntity vote = voteJPARepository.save(requestDto.toEntity()); + long voteId = vote.getId(); + for (CreateVoteRequest.OptionDTO option : requestDto.getOptions()) { + optionJPARepository.save(option.toEntity(voteId)); + } + return new CreateVoteResponse(voteId); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/domain/service/GetVoteListService.java b/src/main/java/com/kakao/golajuma/vote/domain/service/GetVoteListService.java new file mode 100644 index 0000000..76ddff4 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/domain/service/GetVoteListService.java @@ -0,0 +1,150 @@ +package com.kakao.golajuma.vote.domain.service; + +import com.kakao.golajuma.vote.domain.exception.RequestParamException; +import com.kakao.golajuma.vote.infra.entity.Category; +import com.kakao.golajuma.vote.infra.entity.OptionEntity; +import com.kakao.golajuma.vote.infra.entity.VoteEntity; +import com.kakao.golajuma.vote.infra.repository.OptionJPARepository; +import com.kakao.golajuma.vote.infra.repository.VoteJPARepository; +import com.kakao.golajuma.vote.web.dto.response.GetVoteListResponse; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class GetVoteListService { + + private final VoteJPARepository voteJPARepository; + private final OptionJPARepository optionJPARepository; + // private final DesicionJPARepository desicionJPARepository; + + static int page = 0; + static int size = 5; + + public GetVoteListResponse.MainAndFinishPage getVoteList( + long idx, long totalCount, String sort, String active, String category) { + /* + 투표 중 active = continue 이고, createdDate가 최신순으로 정렬하여 가져와서 보여준다 + 사용자의 id를 가져와서 참여한 투표와 참여하지 않은 투표를 다른 데이터 형식으로 반환한다. + */ + long userId = 1; + + // 응답 dto + GetVoteListResponse.MainAndFinishPage responseBody = + new GetVoteListResponse.MainAndFinishPage(); + + // 진행중인 투표(on) or 완료된 투표 요청 판단 + boolean on = checkActive(active); + + // 1. vote list 를 가져온다 + Slice voteList = + findByRepository(idx, totalCount, sort, active, checkCategory(category)); + + // 마지막 페이지인지 검사 + responseBody.isLast(voteList.isLast()); + + // 2. 각 vote 별로 vote option 을 찾는다 - slice 방식 + for (VoteEntity vote : voteList) { + List options = optionJPARepository.findAllByVoteId(vote.getId()); + boolean isOwner = isOwner(userId, vote); + // boolean participate = desicionJPARepository.findByUserIdAndVoteId(userId, vote.getId()); + boolean participate = true; + + // 참여했다면 어느 옵션 id에 투표했는지 알아야함 + List choiceList = checkChoiceOption(options, userId); + + long voteTotalCount = vote.getVoteTotalCount(); + + // 투표 작성자를 보여준다면?? + // vote.getType?? 으로 익명인지 판단 + // 1. 익명이 아닌 경우 user repo 에서 voteUserId로 작성자의 닉네임을 가져온다 + // 2. 익명인 경우 작성자명을 비공개처리 + + // 여기서 문제 완료된 페이지 요청인 경우 투표 옵션 count를 무조건 보여줘야함 + responseBody.toDto(vote, on, isOwner, participate, voteTotalCount, options, choiceList); + } + + return responseBody; + } + + public List checkChoiceOption(List options, long userId) { + List choiceList = new ArrayList<>(); + // + for (OptionEntity option : options) { + long optionId = option.getId(); + // decision repo 에서 확인해야함 + // if(decisionJPARepository.checkByUserIdAndOptionId(userId, optionId)) + // choiceList.add(true); + // else choiceList.add(false); + choiceList.add(true); // dummy data + } + return choiceList; + } + + public boolean isOwner(long userId, VoteEntity vote) { + return userId == vote.getUserId(); + } + + public boolean checkActive(String active) { + if (active.equals("continue")) { + return true; + } + if (active.equals("finish")) { + return false; + } + throw new RequestParamException("잘못된 요청입니다.(active)"); + } + + public Category checkCategory(String category) { + return Category.findCategory(category); + } + + public Slice findByRepository( + long idx, long totalCount, String sort, String active, Category category) { + // 어디서부터 몇개씩 가져올건지 + Pageable pageable = PageRequest.of(page, size); + + if (sort.equals("current")) { + return voteJPARepository.findAllByActiveAndCategoryOrderByCreatedDate( + idx, active, category, pageable); + } + if (sort.equals("popular")) { + return voteJPARepository.findAllByActiveAndCategoryOrderByVoteTotalCount( + totalCount, active, category, pageable); + } + + throw new RequestParamException("잘못된 요청입니다.(sort)"); + } + + public GetVoteListResponse.MyPage getVoteListInMyPageByParticipate() { + // 임의 유저값 가져옴 나중에 유효성 처리 해야함 + long userId = 1; + GetVoteListResponse.MyPage responseBody = new GetVoteListResponse.MyPage(); + + // userId가 투표한 투표 리스트를 decision 레포에서 찾아야함 + // List voteList = decisionJPARepository.findAllUserId(userId); + List voteList = new ArrayList<>(); + responseBody.toDto(voteList); + + return responseBody; + } + + public GetVoteListResponse.MyPage getVoteListInMyPageByAsk() { + // 임의 유저값 가져옴 나중에 유효성 처리 해야함 + long userId = 1; + GetVoteListResponse.MyPage responseBody = new GetVoteListResponse.MyPage(); + + // userId가 올린 투표를 가져옴 + List voteList = voteJPARepository.findAllByUserId(userId); + responseBody.toDto(voteList); + + return responseBody; + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/domain/service/VoteService.java b/src/main/java/com/kakao/golajuma/vote/domain/service/VoteService.java new file mode 100644 index 0000000..0d7847e --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/domain/service/VoteService.java @@ -0,0 +1,56 @@ +package com.kakao.golajuma.vote.domain.service; + +import com.kakao.golajuma.vote.domain.exception.NullException; +import com.kakao.golajuma.vote.domain.exception.OptionNumException; +import com.kakao.golajuma.vote.infra.entity.VoteEntity; +import com.kakao.golajuma.vote.infra.repository.OptionJPARepository; +import com.kakao.golajuma.vote.infra.repository.VoteJPARepository; +import com.kakao.golajuma.vote.web.dto.request.CreateVoteRequest; +import com.kakao.golajuma.vote.web.dto.response.CreateVoteResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class VoteService { + + private final VoteJPARepository voteJPARepository; + private final OptionJPARepository optionJPARepository; + + @Transactional + public CreateVoteResponse createVote(CreateVoteRequest requestDto) { + boolean exit; + // 1. 투표 제목이 있는지 확인 후 예외처리 + if (requestDto.getTitle() == null) { + throw new NullException("제목을 입력해주세요"); + } + + // 2. 옵션 개수가 2개 ~ 6개 인지 확인 후 예외처리 + int size = requestDto.getOptions().size(); + if (size < 2 || size > 6) { + throw new OptionNumException("선택지의 개수는 2개 이하 6개 이상이어야 합니다."); + } + // 3. 옵션 이름이 있는지 확인 + for (CreateVoteRequest.OptionDTO option : requestDto.getOptions()) { + if (option.getName() == null) { + throw new NullException("옵션 이름을 입력해주세요."); + } + } + + // 4. 카테고리가 맞는지 확인 + // Optional category = Category.findCategory(requestDto.getCategory()); + // if(!category.isPresent()){ + // throw new CategoryException("해당 카테고리를 찾을 수 없습니다."); + // } + + System.out.println(requestDto); + VoteEntity vote = voteJPARepository.save(requestDto.toEntity()); + long voteId = vote.getId(); + for (CreateVoteRequest.OptionDTO option : requestDto.getOptions()) { + optionJPARepository.save(option.toEntity(voteId)); + } + return new CreateVoteResponse(voteId); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/infra/entity/Category.java b/src/main/java/com/kakao/golajuma/vote/infra/entity/Category.java new file mode 100644 index 0000000..2bed499 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/infra/entity/Category.java @@ -0,0 +1,27 @@ +package com.kakao.golajuma.vote.infra.entity; + +import com.kakao.golajuma.vote.domain.exception.CategoryException; +import java.util.Arrays; + +public enum Category { + TOTAL("total"), + CLOTHES("clothes"), + FOOD("food"); + + private final String category; + + Category(String category) { + this.category = category; + } + + public String getCategory() { + return this.category; + } + + public static Category findCategory(String category) { + return Arrays.stream(Category.values()) + .filter(Category -> Category.getCategory().equals(category)) + .findAny() + .orElseThrow(() -> new CategoryException("해당 카테고리는 존재하지 않습니다.")); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/infra/entity/OptionEntity.java b/src/main/java/com/kakao/golajuma/vote/infra/entity/OptionEntity.java new file mode 100644 index 0000000..6c05aa0 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/infra/entity/OptionEntity.java @@ -0,0 +1,45 @@ +package com.kakao.golajuma.vote.infra.entity; + +import com.kakao.golajuma.common.BaseEntity; +import javax.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = OptionEntity.ENTITY_PREFIX + "_tb") +public class OptionEntity extends BaseEntity { + + public static final String ENTITY_PREFIX = "option"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = ENTITY_PREFIX + "_id") + private long id; + + @Column(name = ENTITY_PREFIX + "_vote_id", nullable = false) + private long voteId; + + @Column(name = ENTITY_PREFIX + "_name", nullable = false) + private String optionName; + + @Column(name = ENTITY_PREFIX + "_image") + private String optionImage; + + @Column(name = ENTITY_PREFIX + "_count") + private long optionCount; + + @Builder + public OptionEntity(long id, long voteId, String optionName, String optionImage) { + this.id = id; + this.voteId = voteId; + this.optionName = optionName; + this.optionImage = optionImage; + this.optionCount = 0; + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/infra/entity/VoteEntity.java b/src/main/java/com/kakao/golajuma/vote/infra/entity/VoteEntity.java new file mode 100644 index 0000000..fecd8a1 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/infra/entity/VoteEntity.java @@ -0,0 +1,69 @@ +package com.kakao.golajuma.vote.infra.entity; + +import com.kakao.golajuma.common.BaseEntity; +import java.time.LocalDateTime; +import javax.persistence.*; +import lombok.*; +import lombok.experimental.SuperBuilder; + +@ToString +@SuperBuilder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = VoteEntity.ENTITY_PREFIX + "_tb") +public class VoteEntity extends BaseEntity { + + public static final String ENTITY_PREFIX = "vote"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = ENTITY_PREFIX + "_id", nullable = false) + private long id; + + @Column(name = "user_id", nullable = false) + private long userId; + + @Column(name = ENTITY_PREFIX + "_total_count", nullable = false) + private long voteTotalCount; + + @Column(name = ENTITY_PREFIX + "_title", length = 256, nullable = false) + private String voteTitle; + + @Column(name = ENTITY_PREFIX + "_content", length = 1000) + private String voteContent; + + @Enumerated(EnumType.STRING) + @Column(name = ENTITY_PREFIX + "_category", nullable = false) + private Category category; + + @Column(name = ENTITY_PREFIX + "_end_date", nullable = false) + private LocalDateTime voteEndDate; + + @Builder.Default + @Column(name = ENTITY_PREFIX + "_active", nullable = false) + private String voteActive = "continue"; + + @Column(name = ENTITY_PREFIX + "_type") + private String voteType; + + @Builder + public VoteEntity( + long id, + long userId, + long voteTotalCount, + Category category, + String voteTitle, + String voteContent, + LocalDateTime voteEndDate, + String voteType) { + this.id = id; + this.userId = userId; + this.voteTotalCount = voteTotalCount; + this.category = category; + this.voteTitle = voteTitle; + this.voteContent = voteContent; + this.voteEndDate = voteEndDate; + this.voteType = voteType; + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/infra/repository/OptionJPARepository.java b/src/main/java/com/kakao/golajuma/vote/infra/repository/OptionJPARepository.java new file mode 100644 index 0000000..c850180 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/infra/repository/OptionJPARepository.java @@ -0,0 +1,13 @@ +package com.kakao.golajuma.vote.infra.repository; + +import com.kakao.golajuma.vote.infra.entity.OptionEntity; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface OptionJPARepository extends JpaRepository { + + @Query("select o from OptionEntity o where o.voteId = :voteId") + List findAllByVoteId(@Param("voteId") long voteId); +} diff --git a/src/main/java/com/kakao/golajuma/vote/infra/repository/VoteJPARepository.java b/src/main/java/com/kakao/golajuma/vote/infra/repository/VoteJPARepository.java new file mode 100644 index 0000000..b76c791 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/infra/repository/VoteJPARepository.java @@ -0,0 +1,40 @@ +package com.kakao.golajuma.vote.infra.repository; + +import com.kakao.golajuma.vote.infra.entity.Category; +import com.kakao.golajuma.vote.infra.entity.VoteEntity; +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface VoteJPARepository extends JpaRepository { + + @Query( + "select v from VoteEntity v where v.deleted = false and v.id < :idx and v.voteActive = :active and v.category = :category ORDER BY v.createdDate desc ") + Slice findAllByActiveAndCategoryOrderByCreatedDate( + @Param("idx") long idx, + @Param("active") String active, + @Param("category") Category category, + Pageable pageable); + + @Query( + "select v from VoteEntity v where v.deleted = false and v.voteTotalCount < :totalCount and v.voteActive = :active and v.category = :category ORDER BY v.voteTotalCount desc ") + Slice findAllByActiveAndCategoryOrderByVoteTotalCount( + @Param("totalCount") long idx, + @Param("active") String active, + @Param("category") Category category, + Pageable pageable); + + @Query( + "select v from VoteEntity v where v.deleted = false and v.userId = :userId order by v.createdDate desc ") + List findAllByUserId(@Param("userId") long userId); + + // // 검색 기능 + // @Query("select v from VoteEntity v where v.deleted = false and v.voteTitle like %:keyword% + // order by v.createdDate desc ") + // Slice searchVotes(@Param("keyword") String keyword, Pageable pageable); + +} + diff --git a/src/main/java/com/kakao/golajuma/vote/web/controller/CreateVoteController.java b/src/main/java/com/kakao/golajuma/vote/web/controller/CreateVoteController.java new file mode 100644 index 0000000..a98dbfd --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/web/controller/CreateVoteController.java @@ -0,0 +1,26 @@ +package com.kakao.golajuma.vote.web.controller; + +import com.kakao.golajuma.common.support.respnose.ApiResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponseBody.SuccessBody; +import com.kakao.golajuma.common.support.respnose.ApiResponseGenerator; +import com.kakao.golajuma.common.support.respnose.MessageCode; +import com.kakao.golajuma.vote.domain.service.CreateVoteService; +import com.kakao.golajuma.vote.web.dto.request.CreateVoteRequest; +import com.kakao.golajuma.vote.web.dto.response.CreateVoteResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +public class CreateVoteController { + private final CreateVoteService createVoteService; + + // 투표 생성 + @PostMapping("/votes") + public ApiResponse> createVote( + @RequestBody CreateVoteRequest voteDTO) { + CreateVoteResponse responseDto = createVoteService.createVote(voteDTO); + return ApiResponseGenerator.success(responseDto, HttpStatus.OK, MessageCode.CREATE); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/web/controller/GetVoteListController.java b/src/main/java/com/kakao/golajuma/vote/web/controller/GetVoteListController.java new file mode 100644 index 0000000..9ff9c0a --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/web/controller/GetVoteListController.java @@ -0,0 +1,53 @@ +package com.kakao.golajuma.vote.web.controller; + +import com.kakao.golajuma.common.support.respnose.ApiResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponseBody; +import com.kakao.golajuma.common.support.respnose.ApiResponseGenerator; +import com.kakao.golajuma.common.support.respnose.MessageCode; +import com.kakao.golajuma.vote.domain.service.GetVoteListService; +import com.kakao.golajuma.vote.web.dto.response.GetVoteListResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class GetVoteListController { + + private final GetVoteListService getVoteListService; + + // 투표 조회 - 메인페이지, 완료된 페이지 + @GetMapping("/votes") + public ApiResponse> + getVoteList( + @RequestParam(defaultValue = "99999999999999999") long idx, + @RequestParam(defaultValue = "99999999999999999") long totalCount, + @RequestParam(defaultValue = "current") String sort, + @RequestParam(defaultValue = "continue") String active, + @RequestParam(defaultValue = "total") String category) { + GetVoteListResponse.MainAndFinishPage responseDto = + getVoteListService.getVoteList(idx, totalCount, sort, active, category); + + return ApiResponseGenerator.success(responseDto, HttpStatus.OK, MessageCode.CREATE); + } + + // 투표 리스트 조회 - 마이페이지 참여한 투표 + @GetMapping("/users/votes/participate") + public ApiResponse> + getVoteListInMyPageByParticipate() { + GetVoteListResponse.MyPage responseDto = getVoteListService.getVoteListInMyPageByParticipate(); + + return ApiResponseGenerator.success(responseDto, HttpStatus.OK, MessageCode.CREATE); + } + + // 투표 리스트 조회 - 마이페이지 올린 투표 + @GetMapping("/users/votes/ask") + public ApiResponse> + getVoteListInMyPageByAsk() { + GetVoteListResponse.MyPage responseDto = getVoteListService.getVoteListInMyPageByAsk(); + + return ApiResponseGenerator.success(responseDto, HttpStatus.OK, MessageCode.CREATE); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/web/controller/VoteRestController.java b/src/main/java/com/kakao/golajuma/vote/web/controller/VoteRestController.java new file mode 100644 index 0000000..6881496 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/web/controller/VoteRestController.java @@ -0,0 +1,28 @@ +package com.kakao.golajuma.vote.web.controller; + +import com.kakao.golajuma.common.support.respnose.ApiResponse; +import com.kakao.golajuma.common.support.respnose.ApiResponseBody.SuccessBody; +import com.kakao.golajuma.common.support.respnose.ApiResponseGenerator; +import com.kakao.golajuma.common.support.respnose.MessageCode; +import com.kakao.golajuma.vote.domain.service.VoteService; +import com.kakao.golajuma.vote.web.dto.request.CreateVoteRequest; +import com.kakao.golajuma.vote.web.dto.response.CreateVoteResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class VoteRestController { + private final VoteService voteService; + + // 투표 생성 + @PostMapping("/votes") + public ApiResponse> createVote( + @RequestBody CreateVoteRequest voteDTO) { + CreateVoteResponse responseDto = voteService.createVote(voteDTO); + return ApiResponseGenerator.success(responseDto, HttpStatus.OK, MessageCode.CREATE); + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/web/dto/request/CreateVoteRequest.java b/src/main/java/com/kakao/golajuma/vote/web/dto/request/CreateVoteRequest.java new file mode 100644 index 0000000..74f9273 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/web/dto/request/CreateVoteRequest.java @@ -0,0 +1,68 @@ +package com.kakao.golajuma.vote.web.dto.request; + +import com.kakao.golajuma.common.marker.AbstractRequestDto; +import com.kakao.golajuma.vote.infra.entity.Category; +import com.kakao.golajuma.vote.infra.entity.OptionEntity; +import com.kakao.golajuma.vote.infra.entity.VoteEntity; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@NoArgsConstructor +@ToString +@Getter +public class CreateVoteRequest implements AbstractRequestDto { + + private String title; + private String content; + List options; + private String category; + private int timeLimit; // 시간 제한 받아서 연산해야함 + + public CreateVoteRequest( + String voteTitle, + String category, + String voteContent, + int timeLimit, + List options) { + this.title = voteTitle; + this.category = category; + this.content = voteContent; + this.timeLimit = timeLimit; + this.options = options; + } + + public VoteEntity toEntity() { + VoteEntity vote = + VoteEntity.builder() + .userId(1) + .voteTotalCount(0) + .category(Category.findCategory(category)) + .voteTitle(title) + .voteContent(content) + .voteType("null") + .voteEndDate(LocalDateTime.now().plusMinutes(timeLimit)) + .build(); + System.out.println(vote); + return vote; + } + + @Getter + @Setter + public static class OptionDTO { + private String name; + private String image; + + public OptionDTO(String name, String image) { + this.name = name; + this.image = image; + } + + public OptionEntity toEntity(long voteId) { + return OptionEntity.builder().voteId(voteId).optionName(name).optionImage(image).build(); + } + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/web/dto/response/CreateVoteResponse.java b/src/main/java/com/kakao/golajuma/vote/web/dto/response/CreateVoteResponse.java new file mode 100644 index 0000000..999ee26 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/web/dto/response/CreateVoteResponse.java @@ -0,0 +1,15 @@ +package com.kakao.golajuma.vote.web.dto.response; + +import com.kakao.golajuma.common.marker.AbstractResponseDto; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class CreateVoteResponse implements AbstractResponseDto { + private long id; + + public CreateVoteResponse(long id) { + this.id = id; + } +} diff --git a/src/main/java/com/kakao/golajuma/vote/web/dto/response/GetVoteListResponse.java b/src/main/java/com/kakao/golajuma/vote/web/dto/response/GetVoteListResponse.java new file mode 100644 index 0000000..8fce434 --- /dev/null +++ b/src/main/java/com/kakao/golajuma/vote/web/dto/response/GetVoteListResponse.java @@ -0,0 +1,174 @@ +package com.kakao.golajuma.vote.web.dto.response; + +import com.kakao.golajuma.common.marker.AbstractResponseDto; +import com.kakao.golajuma.vote.infra.entity.OptionEntity; +import com.kakao.golajuma.vote.infra.entity.VoteEntity; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.*; + +@NoArgsConstructor +@Getter +@Setter +public class GetVoteListResponse implements AbstractResponseDto { + + @Getter + public static class MainAndFinishPage { + List votes = new ArrayList<>(); + Boolean isLast; + + public void isLast(boolean isLast) { + this.isLast = isLast; + } + + public void toDto( + VoteEntity vote, + boolean on, + boolean isOwner, + boolean participate, + long totalCount, + List options, + List choiceList) { + VoteDto voteDto = VoteToDto(vote, isOwner, participate, totalCount); + this.votes.add(voteDto); + // case 1 : 질문자, isOwner : true, participate : false, 옵션 카운트 표시 + // case 2 : 응답자 참여 O, isOwner : false, participate : true, 옵션 카운트 표시 + // case 3 : 응답자 참여 X, isOwner : false, participate : false, 옵션 카운트 미표시 + + // 투표가 진행되고 있는 상태에서(on) 주인이 아니고, 참여하지 않았을때만 옵션 Count를 보여주지 않음 + if (noParticipateCase(on, isOwner, participate)) { + voteDto.addOption(options); + } else { + voteDto.addCountOption(options, choiceList); // 투표 카운트 표시 + } + } + + // 투표가 진행되고 있는 상태에서(on) 주인이 아니고, 참여하지 않았을때 + public boolean noParticipateCase(boolean on, boolean isOwner, boolean participate) { + return on == true && isOwner == false && participate == false; + } + + @Getter + @Setter + @Builder + public static class VoteDto { + private long id; + private Boolean isOwner; + private long totalCount; + private LocalDateTime createdDate; + private LocalDateTime endDate; + private String active; + private boolean participate; + private String title; + private String content; + private List options; + + public void addCountOption(List options, List choiceList) { + this.options = new ArrayList<>(); + for (int i = 0; i < options.size(); i++) { + this.options.add(toCountOptionDto(options.get(i), choiceList.get(i), totalCount)); + } + } + + public void addOption(List options) { + for (OptionEntity option : options) { + this.options.add(toOptionDto(option)); + } + } + } + + public VoteDto VoteToDto( + VoteEntity vote, boolean isOwner, boolean participate, long totalCount) { + return VoteDto.builder() + .id(vote.getId()) + .isOwner(isOwner) + .totalCount(totalCount) + .createdDate(vote.getCreatedDate()) + .endDate(vote.getVoteEndDate()) + .active(vote.getVoteActive()) + .participate(participate) + .title(vote.getVoteTitle()) + .content(vote.getVoteContent()) + .build(); + } + + @Builder + @Getter + @Setter + public static class OptionDto { + private long id; + private String name; + private String image; + + public OptionDto(long id, String name, String image) { + this.id = id; + this.name = name; + this.image = image; + } + } + + @Getter + @Setter + public static class CountOptionDto extends OptionDto { + private boolean choiced; + private long count; + private int ratio; + + public CountOptionDto( + long id, String name, String image, boolean choiced, long count, int ratio) { + super(id, name, image); + this.choiced = choiced; + this.count = count; + this.ratio = ratio; + } + } + + public static CountOptionDto toCountOptionDto( + OptionEntity option, boolean choiced, long totalCount) { + if (totalCount == 0) totalCount = 1; + return new CountOptionDto( + option.getId(), + option.getOptionName(), + option.getOptionImage(), + choiced, + option.getOptionCount(), + Math.round(option.getOptionCount() / totalCount)); + } + + public static OptionDto toOptionDto(OptionEntity option) { + return OptionDto.builder() + .id(option.getId()) + .name(option.getOptionName()) + .image(option.getOptionImage()) + .build(); + } + } + + @Getter + public static class MyPage { + List votes = new ArrayList<>(); + + @Builder + @Getter + public static class VoteDto { + long id; + String active; + String title; + } + + public void toDto(List votes) { + for (VoteEntity vote : votes) { + this.votes.add(voteToDto(vote)); + } + } + + public VoteDto voteToDto(VoteEntity vote) { + return VoteDto.builder() + .id(vote.getId()) + .active(vote.getVoteActive()) + .title(vote.getVoteTitle()) + .build(); + } + } +} diff --git a/src/main/resources/application-local-mysql.yml b/src/main/resources/application-local-mysql.yml new file mode 100644 index 0000000..200f829 --- /dev/null +++ b/src/main/resources/application-local-mysql.yml @@ -0,0 +1,20 @@ +spring: + config: + activate: + on-profile: local-mysql + datasource: + url: jdbc:mysql://localhost:13306/golajuma?allowPublicKeyRetrieval=true&rewriteBatchedStatements=true + username: root + password: root + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + url: + +logging: + level: + sql: debug \ No newline at end of file diff --git a/src/main/resources/application-local-redis.yml b/src/main/resources/application-local-redis.yml new file mode 100644 index 0000000..36a3021 --- /dev/null +++ b/src/main/resources/application-local-redis.yml @@ -0,0 +1,7 @@ +spring: + redis: + host: localhost + port: 16379 + +redis: + timeToLive : 60 \ No newline at end of file diff --git a/src/main/resources/application-local-security.yml b/src/main/resources/application-local-security.yml new file mode 100644 index 0000000..c031ac6 --- /dev/null +++ b/src/main/resources/application-local-security.yml @@ -0,0 +1,13 @@ +spring: + config: + activate: + on-profile: local-security + +security: + jwt: + token: + secretKey: asccesssecretkeyoverflowsecrekey + access: + validTime: 1800000 + refresh: + validTime: 3600000 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..cc51800 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,8 @@ +spring: + profiles: + group: + local: + - local-mysql + - local-security + - local-redis + active: local diff --git a/src/test/java/com/kakao/golajuma/GoalajumaApplicationTests.java b/src/test/java/com/kakao/golajuma/GoalajumaApplicationTests.java new file mode 100644 index 0000000..ad23796 --- /dev/null +++ b/src/test/java/com/kakao/golajuma/GoalajumaApplicationTests.java @@ -0,0 +1,11 @@ +package com.kakao.golajuma; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GoalajumaApplicationTests { + + @Test + void contextLoads() {} +} diff --git a/src/test/java/com/kakao/golajuma/auth/domain/service/SaveUserServiceTest.java b/src/test/java/com/kakao/golajuma/auth/domain/service/SaveUserServiceTest.java new file mode 100644 index 0000000..8980a3c --- /dev/null +++ b/src/test/java/com/kakao/golajuma/auth/domain/service/SaveUserServiceTest.java @@ -0,0 +1,70 @@ +package com.kakao.golajuma.auth.domain.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.kakao.golajuma.auth.infra.converter.UserEntityConverter; +import com.kakao.golajuma.auth.infra.entity.UserEntity; +import com.kakao.golajuma.auth.infra.repository.UserRepository; +import com.kakao.golajuma.auth.web.dto.request.SaveUserRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SaveUserServiceTest { + @Mock private ValidEmailService validEmailService; + + @Mock private ValidNicknameService validNicknameService; + + @Mock private UserEntityConverter entityConverter; + @Mock private UserRepository userRepository; + + @InjectMocks private SaveUserService saveUserService; + + @Test + @DisplayName("회원가입 시 이메일 중복체크를 한다.") + void email_duplicated_check_when_signup() { + // given + SaveUserRequest request = SaveUserRequest.builder().build(); + + // when + saveUserService.execute(request); + + // then + verify(validEmailService).execute(request); + } + + @Test + @DisplayName("회원가입 시 닉네임 중복체크를 한다.") + void nickname_duplicated_check_when_signup() { + // given + SaveUserRequest request = SaveUserRequest.builder().build(); + + // when + saveUserService.execute(request); + + // then + verify(validNicknameService).execute(request); + } + + @Test + @DisplayName("회원가입 시 repository에게 save 메시지를 전달한다.") + void save_message_when_signup() { + // given + SaveUserRequest request = SaveUserRequest.builder().build(); + UserEntity entity = UserEntity.builder().build(); + + when(entityConverter.toEntity(request)).thenReturn(entity); + + // when + saveUserService.execute(request); + + // then + verify(userRepository).save(entity); + } +} diff --git a/src/test/java/com/kakao/golajuma/auth/domain/service/ValidEmailServiceTest.java b/src/test/java/com/kakao/golajuma/auth/domain/service/ValidEmailServiceTest.java new file mode 100644 index 0000000..c0baf30 --- /dev/null +++ b/src/test/java/com/kakao/golajuma/auth/domain/service/ValidEmailServiceTest.java @@ -0,0 +1,45 @@ +package com.kakao.golajuma.auth.domain.service; + +import static org.junit.jupiter.api.Assertions.*; + +import com.kakao.golajuma.auth.domain.exception.DuplicateException; +import com.kakao.golajuma.auth.infra.repository.UserRepository; +import com.kakao.golajuma.auth.web.dto.request.SaveUserRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ValidEmailUseCaseTest { + @Mock private UserRepository userRepository; + + @InjectMocks private ValidEmailService validEmailService; + + @Test + @DisplayName("이메일 중복되면 예외가 발생한다.") + public void duplicated_email() { + // given + SaveUserRequest request = SaveUserRequest.builder().email("email@email").build(); + + Mockito.when(userRepository.existsByEmail(request.getEmail())).thenReturn(Boolean.TRUE); + + // when & then + assertThrows(DuplicateException.class, () -> validEmailService.execute(request)); + } + + @Test + @DisplayName("이메일 중복이 되지 않으면 예외가 발생하지 않는다.") + public void not_duplicated_email() { + // given + SaveUserRequest request = SaveUserRequest.builder().email("email@email").build(); + + Mockito.when(userRepository.existsByEmail(request.getEmail())).thenReturn(Boolean.FALSE); + + // when & then + assertDoesNotThrow(() -> validEmailService.execute(request)); + } +} diff --git a/src/test/java/com/kakao/golajuma/auth/domain/service/ValidNicknameServiceTest.java b/src/test/java/com/kakao/golajuma/auth/domain/service/ValidNicknameServiceTest.java new file mode 100644 index 0000000..f82e92e --- /dev/null +++ b/src/test/java/com/kakao/golajuma/auth/domain/service/ValidNicknameServiceTest.java @@ -0,0 +1,45 @@ +package com.kakao.golajuma.auth.domain.service; + +import static org.junit.jupiter.api.Assertions.*; + +import com.kakao.golajuma.auth.domain.exception.DuplicateException; +import com.kakao.golajuma.auth.infra.repository.UserRepository; +import com.kakao.golajuma.auth.web.dto.request.SaveUserRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ValidNicknameUseCaseTest { + @Mock private UserRepository userRepository; + + @InjectMocks private ValidNicknameService validNicknameService; + + @Test + @DisplayName("닉네임 중복되면 예외가 발생한다.") + public void duplicated_email() { + // given + SaveUserRequest request = SaveUserRequest.builder().nickname("nickname").build(); + + Mockito.when(userRepository.existsByNickname(request.getNickname())).thenReturn(Boolean.TRUE); + + // when & then + assertThrows(DuplicateException.class, () -> validNicknameService.execute(request)); + } + + @Test + @DisplayName("닉네임 중복이 되지 않으면 예외가 발생하지 않는다.") + public void not_duplicated_email() { + // given + SaveUserRequest request = SaveUserRequest.builder().nickname("nickname").build(); + + Mockito.when(userRepository.existsByNickname(request.getNickname())).thenReturn(Boolean.FALSE); + + // when & then + assertDoesNotThrow(() -> validNicknameService.execute(request)); + } +} diff --git a/src/test/java/com/kakao/golajuma/auth/domain/token/TokenProviderTest.java b/src/test/java/com/kakao/golajuma/auth/domain/token/TokenProviderTest.java new file mode 100644 index 0000000..5c7d6d4 --- /dev/null +++ b/src/test/java/com/kakao/golajuma/auth/domain/token/TokenProviderTest.java @@ -0,0 +1,41 @@ +package com.kakao.golajuma.auth.domain.token; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class TokenProviderTest { + private static final String JWT_SECRET_KEY = "A".repeat(32); + private static final int ACCESS_EXPIRED_TIME = 3600; + private static final int REFRESH_EXPIRED_TIME = 3600; + private final TokenProvider tokenProvider = + new TokenProvider(JWT_SECRET_KEY, ACCESS_EXPIRED_TIME, REFRESH_EXPIRED_TIME); + + @Test + @DisplayName("엑세스 토큰을 생성한다.") + void create_accessToken() { + // given + Long userId = 1L; + + // when + String token = tokenProvider.createAccessToken(userId); + + // then + assertThat(token.split("\\.")).hasSize(3); + } + + @Test + @DisplayName("리프레시 토큰을 생성한다.") + void create_refreshToken() { + // given + Long userId = 1L; + + // when + String token = tokenProvider.createRefreshToken(userId); + + // then + assertThat(token.split("\\.")).hasSize(3); + } +} diff --git a/src/test/java/com/kakao/golajuma/auth/domain/token/TokenResolverTest.java b/src/test/java/com/kakao/golajuma/auth/domain/token/TokenResolverTest.java new file mode 100644 index 0000000..3ae0f2c --- /dev/null +++ b/src/test/java/com/kakao/golajuma/auth/domain/token/TokenResolverTest.java @@ -0,0 +1,56 @@ +package com.kakao.golajuma.auth.domain.token; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import com.kakao.golajuma.auth.domain.exception.NotValidToken; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class TokenResolverTest { + private static final String JWT_SECRET_KEY = "A".repeat(32); + private static final int ACCESS_EXPIRED_TIME = 3600; + private static final int REFRESH_EXPIRED_TIME = 3600; + private final TokenProvider tokenProvider = + new TokenProvider(JWT_SECRET_KEY, ACCESS_EXPIRED_TIME, REFRESH_EXPIRED_TIME); + private final TokenResolver tokenResolver = new TokenResolver(JWT_SECRET_KEY); + + @Test + @DisplayName("토큰이 만료되지 않았을 경우 유저 정보를 가져온다") + void get_userInfo() { + // given + Long userId = 1L; + String token = tokenProvider.createAccessToken(userId); + + // when + Long userInfo = tokenResolver.getUserInfo(token); + + // then + assertThat(userInfo).isEqualTo(userId); + } + + @Test + @DisplayName("엑세스 토큰이 만료되면 예외를 터트린다.") + void get_userInfo_by_access_exception() { + // given + TokenProvider expiredTokenProvider = new TokenProvider(JWT_SECRET_KEY, 0, 0); + Long userId = 1L; + String token = expiredTokenProvider.createAccessToken(userId); + + // when & then + assertThatThrownBy(() -> tokenResolver.getUserInfo(token)).isInstanceOf(NotValidToken.class); + } + + @Test + @DisplayName("리프레시 토큰이 만료되면 예외를 터트린다.") + void get_userInfo_by_refresh_exception() { + // given + TokenProvider expiredTokenProvider = new TokenProvider(JWT_SECRET_KEY, 0, 0); + Long userId = 1L; + String token = expiredTokenProvider.createRefreshToken(userId); + + // when & then + assertThatThrownBy(() -> tokenResolver.getUserInfo(token)).isInstanceOf(NotValidToken.class); + } +} diff --git a/src/test/java/com/kakao/golajuma/comment/api/CreateCommentApiTest.java b/src/test/java/com/kakao/golajuma/comment/api/CreateCommentApiTest.java new file mode 100644 index 0000000..362107d --- /dev/null +++ b/src/test/java/com/kakao/golajuma/comment/api/CreateCommentApiTest.java @@ -0,0 +1,40 @@ +package com.kakao.golajuma.comment.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kakao.golajuma.comment.web.dto.request.SaveCommentRequest; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +public class CreateCommentApiTest { + @Autowired private ObjectMapper om; + + @Autowired private MockMvc mvc; + + @DisplayName("comment-create-success-case") + @Test + public void createTest() throws Exception { + // given + SaveCommentRequest requestDto = SaveCommentRequest.builder().content("메롱이다.").build(); + String requestBody = om.writeValueAsString(requestDto); + System.out.println("테스트 : " + requestBody); + // when + ResultActions resultActions = + mvc.perform( + MockMvcRequestBuilders.post("/votes/1/comments") + .content(requestBody) + .contentType(MediaType.APPLICATION_JSON)); + + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + + System.out.println("테스트 : " + responseBody); + } +} diff --git a/src/test/java/com/kakao/golajuma/comment/api/DeleteCommentApiTest.java b/src/test/java/com/kakao/golajuma/comment/api/DeleteCommentApiTest.java new file mode 100644 index 0000000..1c8931c --- /dev/null +++ b/src/test/java/com/kakao/golajuma/comment/api/DeleteCommentApiTest.java @@ -0,0 +1,33 @@ +package com.kakao.golajuma.comment.api; + +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +public class DeleteCommentApiTest { + @Autowired private MockMvc mvc; + + @DisplayName("comment-delete-success-case") + @Test + public void deleteTest() throws Exception { + // given + + // when + ResultActions resultActions = + mvc.perform( + MockMvcRequestBuilders.post("/votes/1/comments/1") + .contentType(MediaType.APPLICATION_JSON)); + + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + + System.out.println("테스트 : " + responseBody); + } +} diff --git a/src/test/java/com/kakao/golajuma/comment/api/ReadCommentApiTest.java b/src/test/java/com/kakao/golajuma/comment/api/ReadCommentApiTest.java new file mode 100644 index 0000000..e7bd32b --- /dev/null +++ b/src/test/java/com/kakao/golajuma/comment/api/ReadCommentApiTest.java @@ -0,0 +1,34 @@ +package com.kakao.golajuma.comment.api; + +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +public class ReadCommentApiTest { + + @Autowired private MockMvc mvc; + + @DisplayName("comment-read-success-case") + @Test + public void readTest() throws Exception { + // given + + // when + ResultActions resultActions = + mvc.perform( + MockMvcRequestBuilders.post("/votes/1/comments") + .contentType(MediaType.APPLICATION_JSON)); + + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + + System.out.println("테스트 : " + responseBody); + } +} diff --git a/src/test/java/com/kakao/golajuma/comment/api/UpdateCommentApiTest.java b/src/test/java/com/kakao/golajuma/comment/api/UpdateCommentApiTest.java new file mode 100644 index 0000000..95479af --- /dev/null +++ b/src/test/java/com/kakao/golajuma/comment/api/UpdateCommentApiTest.java @@ -0,0 +1,44 @@ +package com.kakao.golajuma.comment.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kakao.golajuma.comment.web.dto.request.SaveCommentRequest; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +public class UpdateCommentApiTest { + @Autowired private ObjectMapper om; + + @Autowired private MockMvc mvc; + + @DisplayName("comment-update-success-test") + @Test + public void updateTest() throws Exception { + // given + SaveCommentRequest requestDto = SaveCommentRequest.builder().content("메롱이다.2222").build(); + String requestBody = om.writeValueAsString(requestDto); + + // when + ResultActions resultActions = + mvc.perform( + MockMvcRequestBuilders.post("/votes/1/comments/1") + .content(requestBody) + .contentType(MediaType.APPLICATION_JSON)); + + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + + System.out.println("테스트 : " + responseBody); + + // then + // resultActions.andExpect(MockMvcResultMatchers.jsonPath("$.success").value("true")); + // resultActions.andDo(MockMvcResultHandlers.print()).andDo(document); + } +} diff --git a/src/test/java/com/kakao/golajuma/vote/CreateVoteControllerTest.java b/src/test/java/com/kakao/golajuma/vote/CreateVoteControllerTest.java new file mode 100644 index 0000000..c3ecfdb --- /dev/null +++ b/src/test/java/com/kakao/golajuma/vote/CreateVoteControllerTest.java @@ -0,0 +1,161 @@ +package com.kakao.golajuma.vote; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kakao.golajuma.vote.web.dto.request.CreateVoteRequest; +import java.util.ArrayList; +import java.util.List; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +public class CreateVoteControllerTest { + + @Autowired private ObjectMapper om; + @Autowired private MockMvc mvc; + + @DisplayName("투표 생성 정상 작동") + @Test + public void createVoteTest() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO("가라", "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + + CreateVoteRequest request = new CreateVoteRequest("군대 가야할까요?", "total", "...", 60, options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().isOk()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @DisplayName("투표 생성 시 제목 입력 안했을 경우") + @Test + public void createVoteTest_error1() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO("가라", "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + + CreateVoteRequest request = new CreateVoteRequest(null, "total", "...", 60, options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().is4xxClientError()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @DisplayName("투표 생성 시 옵션명이 없는 경우") + @Test + public void createVoteTest_error2() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO(null, "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + + CreateVoteRequest request = new CreateVoteRequest("군대 가야할까요?", "total", "...", 60, options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().is4xxClientError()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @DisplayName("투표 생성 시 옵션이 6개 초과인 경우") + @Test + public void createVoteTest_error3() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO("가라", "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option3 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option4 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option5 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option6 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option7 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + options.add(option3); + options.add(option4); + options.add(option5); + options.add(option6); + options.add(option7); + + CreateVoteRequest request = new CreateVoteRequest("군대 가야할까요?", "total", "...", 60, options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().is4xxClientError()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @DisplayName("투표 생성 시 존재하지 않는 카테고리인 경우") + @Test + public void createVoteTest_error4() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO("가라", "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + + CreateVoteRequest request = + new CreateVoteRequest("군대 가야할까요?", "no category", "...", 60, options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().is4xxClientError()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } +} diff --git a/src/test/java/com/kakao/golajuma/vote/GetVoteListControllerTest.java b/src/test/java/com/kakao/golajuma/vote/GetVoteListControllerTest.java new file mode 100644 index 0000000..0e56dbd --- /dev/null +++ b/src/test/java/com/kakao/golajuma/vote/GetVoteListControllerTest.java @@ -0,0 +1,69 @@ +package com.kakao.golajuma.vote; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +public class GetVoteListControllerTest { + + @Autowired private ObjectMapper om; + @Autowired private MockMvc mvc; + + @DisplayName("메인페이지 투표 조회 정상 요청") + @Test + public void getVoteList_test() throws Exception { + + // when + ResultActions resultActions = + mvc.perform( + get("/votes") + .param("idx", "5") + .param("sort", "current") + .param("active", "continue") + .param("category", "total")); + resultActions.andExpect(status().isOk()); + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @DisplayName("완료된 페이지 조회 정상 요청") + @Test + public void getVoteList_finishPage_test() throws Exception { + + // when + ResultActions resultActions = + mvc.perform( + get("/votes") + .param("sort", "current") + .param("active", "finish") + .param("category", "total")); + resultActions.andExpect(status().isOk()); + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @Test + public void getVoteListInMyPageByAsk_test() throws Exception { + + // when + ResultActions resultActions = mvc.perform(get("/users/votes/ask")); + + // resultActions.andExpect(status().isOk()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } +} diff --git a/src/test/java/com/kakao/golajuma/vote/VoteCreateControllerTest.java b/src/test/java/com/kakao/golajuma/vote/VoteCreateControllerTest.java new file mode 100644 index 0000000..c944af5 --- /dev/null +++ b/src/test/java/com/kakao/golajuma/vote/VoteCreateControllerTest.java @@ -0,0 +1,135 @@ +package com.kakao.golajuma.vote; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kakao.golajuma.vote.web.dto.request.CreateVoteRequest; +import java.util.ArrayList; +import java.util.List; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +public class VoteCreateControllerTest { + + @Autowired private ObjectMapper om; + @Autowired private MockMvc mvc; + + @DisplayName("투표 생성 정상 작동") + @Test + public void createVoteTest() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO("가라", "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + + CreateVoteRequest request = new CreateVoteRequest("군대 가야할까요?", "total", "...", "60", options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().isOk()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @DisplayName("투표 생성 시 제목 입력 안했을 경우") + @Test + public void createVoteTest_error1() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO("가라", "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + + CreateVoteRequest request = new CreateVoteRequest(null, "total", "...", "60", options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().is4xxClientError()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @DisplayName("투표 생성 시 옵션명이 없는 경우") + @Test + public void createVoteTest_error2() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO(null, "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + + CreateVoteRequest request = new CreateVoteRequest("군대 가야할까요?", "total", "...", "60", options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().is4xxClientError()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } + + @DisplayName("투표 생성 시 옵션이 6개 초과인 경우") + @Test + public void createVoteTest_error3() throws Exception { + List options = new ArrayList<>(); + CreateVoteRequest.OptionDTO option1 = new CreateVoteRequest.OptionDTO("가라", "image1"); + CreateVoteRequest.OptionDTO option2 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option3 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option4 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option5 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option6 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + CreateVoteRequest.OptionDTO option7 = new CreateVoteRequest.OptionDTO("가지마라", "image2"); + options.add(option1); + options.add(option2); + options.add(option3); + options.add(option4); + options.add(option5); + options.add(option6); + options.add(option7); + + CreateVoteRequest request = new CreateVoteRequest("군대 가야할까요?", "total", "...", "60", options); + + String requestBody = om.writeValueAsString(request); + System.out.println("테스트 : " + requestBody); + + // when + ResultActions resultActions = + mvc.perform( + post("/votes").content(requestBody).contentType(MediaType.APPLICATION_JSON_VALUE)); + resultActions.andExpect(status().is4xxClientError()); + + // eye + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + } +} diff --git a/tasks/formatting-task.gradle b/tasks/formatting-task.gradle new file mode 100644 index 0000000..aba4c8b --- /dev/null +++ b/tasks/formatting-task.gradle @@ -0,0 +1,19 @@ +spotless { + java { + importOrder() + removeUnusedImports() + trimTrailingWhitespace() + googleJavaFormat('1.15.0') + indentWithTabs(2) + endWithNewline() + target 'src/*/java/**/*.java' + } + + format 'misc', { + target '**/*.gradle', '**/*.md', '**/.gitignore' + targetExclude '.release/*.*' + indentWithSpaces() + trimTrailingWhitespace() + endWithNewline() + } +} diff --git a/tasks/install-git-hooks.gradle b/tasks/install-git-hooks.gradle new file mode 100644 index 0000000..f959d30 --- /dev/null +++ b/tasks/install-git-hooks.gradle @@ -0,0 +1,12 @@ +tasks.create(name: 'gitExecutableHooks') { + Runtime.getRuntime().exec("chmod -R +x .git/hooks/"); +} + +task installGitHooks(type: Copy) { + String scriptDir = rootProject.rootDir.toString() + '/scripts' + from new File(scriptDir, 'pre-commit') + into { new File(rootProject.rootDir, '.git/hooks') } +} + +gitExecutableHooks.dependsOn installGitHooks +clean.dependsOn gitExecutableHooks