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