diff --git a/.github/ISSUE_TEMPLATE/chore-request.md b/.github/ISSUE_TEMPLATE/chore-request.md
new file mode 100644
index 0000000..74306fd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/chore-request.md
@@ -0,0 +1,20 @@
+---
+name: Chore request
+about: 환경 세팅 이슈
+title: "[chore]"
+labels: "⚙️ chore"
+assignees: ''
+
+---
+
+## 🍒 IssueName
+> 이슈 명을 작성해주세요.
+
+## 📝 Description
+> 이슈에 대해 간결하게 설명해주세요.
+
+## 🌱 Todo
+> - [ ] 진행 예정
+> - [x] 진행 완료
+>
+> 위의 방식으로 Task를 정리해주세요.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
new file mode 100644
index 0000000..6e62682
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: 기능 개발 이슈
+title: "[feat]"
+labels: "✨ feature"
+assignees: ''
+
+---
+
+## 🍒 IssueName
+> 이슈 명을 작성해주세요.
+
+## 📝 Description
+> 이슈에 대해 간결하게 설명해주세요.
+
+## 🌱 Todo
+> - [ ] 진행 예정
+> - [x] 진행 완료
+>
+> 위의 방식으로 Task를 정리해주세요.
diff --git a/.github/ISSUE_TEMPLATE/fix-request.md b/.github/ISSUE_TEMPLATE/fix-request.md
new file mode 100644
index 0000000..2df438b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/fix-request.md
@@ -0,0 +1,20 @@
+---
+name: Fix request
+about: 에러 수정 이슈
+title: "[fix]"
+labels: "\U0001F41B fix"
+assignees: ''
+
+---
+
+## 🍒 IssueName
+> 이슈 명을 작성해주세요.
+
+## 📝 Description
+> 이슈에 대해 간결하게 설명해주세요.
+
+## 🌱 Todo
+> - [ ] 진행 예정
+> - [x] 진행 완료
+>
+> 위의 방식으로 Task를 정리해주세요.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..e59867a
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,18 @@
+## #️⃣연관된 이슈
+
+> close #이슈번호, #이슈번호
+
+## 📝작업 내용
+
+> 이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)
+
+- [ ] 작업 내용 1
+- [ ] 작업 내용 2
+
+### 스크린샷 (선택)
+
+## 💬리뷰 요구사항(선택)
+
+> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요
+>
+> ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..25149a3
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,65 @@
+name: CI/CD Pipeline
+
+on:
+ push:
+ branches: [ develop ] # develop 브랜치에 push가 일어날 때 실행
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ # 저장소 코드 체크아웃
+ - uses: actions/checkout@v3
+
+ # JDK 17 설정
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'adopt'
+ java-version: '17'
+
+ # application.yml 파일 생성
+ - name: Make application.yml
+ run: |
+ cd ./src/main/resources
+ touch ./application.yml
+ echo "${{ secrets.APPLICATION }}" > ./application.yml
+ shell: bash
+
+ # Gradle로 빌드
+ - name: Build with Gradle
+ run: |
+ chmod +x ./gradlew
+ ./gradlew clean build -x test
+
+ # Docker 이미지 빌드 및 푸시 (🚨 `--build-arg` 추가!)
+ - name: Docker build & push to Docker Hub
+ run: |
+ docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
+ docker build --no-cache -t ${{ secrets.DOCKER_REPO }}:${{ github.sha }} .
+ docker push ${{ secrets.DOCKER_REPO }}:${{ github.sha }}
+
+ # 서버 배포
+ - name: Deploy to server with Docker
+ uses: appleboy/ssh-action@master
+ env:
+ IMAGE_TAG: ${{ github.sha }}
+ with:
+ host: ${{ secrets.EC2_HOST }}
+ username: ${{ secrets.EC2_USERNAME }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ envs: IMAGE_TAG
+ script: |
+ sudo docker pull ${{ secrets.DOCKER_REPO }}:$IMAGE_TAG
+ cd /root
+ docker compose down --remove-orphans -v || true
+ IMAGE_TAG=$IMAGE_TAG docker compose up -d --force-recreate
+ sudo docker image prune -f
+ # jar 파일명 확인
+ - name: Show built files
+ run: |
+ ls -alh build/libs
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e8ce01e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+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/
+
+### yml ###
+application.yml
+application.yaml
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..569a675
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,4 @@
+FROM eclipse-temurin:17-jdk
+ARG JAR_FILE=/build/libs/*.jar
+COPY ${JAR_FILE} app.jar
+ENTRYPOINT ["java","-jar", "/app.jar"]
\ No newline at end of file
diff --git a/HELP.md b/HELP.md
new file mode 100644
index 0000000..0bb779a
--- /dev/null
+++ b/HELP.md
@@ -0,0 +1,22 @@
+# Getting Started
+
+### Reference Documentation
+For further reference, please consider the following sections:
+
+* [Official Gradle documentation](https://docs.gradle.org)
+* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.7/gradle-plugin)
+* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.7/gradle-plugin/packaging-oci-image.html)
+* [Spring Web](https://docs.spring.io/spring-boot/3.5.7/reference/web/servlet.html)
+
+### Guides
+The following guides illustrate how to use some features concretely:
+
+* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
+* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
+* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)
+
+### Additional Links
+These additional references should also help you:
+
+* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle)
+
diff --git a/README.md b/README.md
index 7f5d396..6c73410 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,16 @@
-# spring-vote-22nd
-ceos back-end 22nd voting service project
+# 프백 합동 과제 - Modelly 팀
+
+### 1. 서비스 소개 / 시연
+
+해당 과제는 CEOS의 차기 파트장이나 데모데이 우수팀을 투표하는 서비스입니다.
+
+1. 중복 투표는 불가능하다.
+2. 데모데이 투표 시 본인이 속한 팀에는 투표할 수 없다.
+3. 파트장 투표 시, 자신이 해당하는 파트의 파트장 투표만 가능하다.
+4. 로그인을 하지 않은 사람은 투표할 수 없다.
+
+서비스 링크: https://next-vote-22nd-eight.vercel.app/
+
+**ERD**
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..8db42f7
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,57 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '3.5.7'
+ id 'io.spring.dependency-management' version '1.1.7'
+}
+
+group = 'vote'
+version = '0.0.1-SNAPSHOT'
+description = 'FE, BE joint project'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+
+ compileOnly 'org.projectlombok:lombok'
+ runtimeOnly 'com.mysql:mysql-connector-j'
+ annotationProcessor 'org.projectlombok:lombok'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+ //Swagger
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
+
+ // Redis
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+ implementation 'org.springframework.session:spring-session-data-redis'
+
+ // JWT
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
+
+ //Security
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..1b33c55
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..d4081da
--- /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.14.3-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..23d15a9
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,251 @@
+#!/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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# 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/platforms/jvm/plugins-application/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##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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="\\\"\\\""
+
+
+# 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=SC2039,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=SC2039,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 optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# 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..5eed7ee
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@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
+@rem SPDX-License-Identifier: Apache-2.0
+@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. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+: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/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..648ae7f
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'vote_be'
diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000..d57debe
Binary files /dev/null and b/src/.DS_Store differ
diff --git a/src/main/java/vote/vote_be/HealthCheckController.java b/src/main/java/vote/vote_be/HealthCheckController.java
new file mode 100644
index 0000000..224112d
--- /dev/null
+++ b/src/main/java/vote/vote_be/HealthCheckController.java
@@ -0,0 +1,14 @@
+package vote.vote_be;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class HealthCheckController {
+
+
+ @GetMapping("/health-check")
+ public String healthCheck() {
+ return "Server is running!";
+ }
+}
diff --git a/src/main/java/vote/vote_be/VoteBeApplication.java b/src/main/java/vote/vote_be/VoteBeApplication.java
new file mode 100644
index 0000000..7fa3a54
--- /dev/null
+++ b/src/main/java/vote/vote_be/VoteBeApplication.java
@@ -0,0 +1,13 @@
+package vote.vote_be;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class VoteBeApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(VoteBeApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/controller/AuthController.java b/src/main/java/vote/vote_be/domain/auth/controller/AuthController.java
new file mode 100644
index 0000000..feecd97
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/controller/AuthController.java
@@ -0,0 +1,93 @@
+package vote.vote_be.domain.auth.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import vote.vote_be.domain.auth.dto.internal.LoginResult;
+import vote.vote_be.domain.auth.dto.request.EmailCheckRequest;
+import vote.vote_be.domain.auth.dto.request.LoginRequest;
+import vote.vote_be.domain.auth.dto.request.SignupRequest;
+import vote.vote_be.domain.auth.dto.response.*;
+import vote.vote_be.domain.auth.service.AuthService;
+import vote.vote_be.global.apiPayload.ApiResponse;
+import vote.vote_be.global.apiPayload.code.SimpleMessageDTO;
+import vote.vote_be.global.apiPayload.code.status.SuccessStatus;
+import jakarta.servlet.http.HttpServletResponse;
+import vote.vote_be.global.security.jwt.CookieUtil;
+
+@RestController
+@RequiredArgsConstructor
+public class AuthController {
+
+ private final AuthService authService;
+
+ @Operation(summary = "회원가입", description = "회원가입 완료 메시지, loginId, 이름을 반환합니다.")
+ @PostMapping("/auth/signup")
+ public ApiResponse signup(@RequestBody @Valid SignupRequest request) {
+ return ApiResponse.of(SuccessStatus.CREATED,authService.signup(request));
+ }
+
+ /* 로그인 */
+ @Operation(summary = "로그인", description = "Access/Refresh 토큰을 포함한 로그인 응답을 반환합니다.")
+ @PostMapping("/auth/login")
+ public ApiResponse login(@RequestBody @Valid LoginRequest request, HttpServletResponse response) {
+ LoginResult result = authService.login(request);
+
+ var cookie = CookieUtil.buildRefreshCookie(
+ result.getRefreshToken(),
+ result.getRefreshTtlSec(),
+ null,
+ true,
+ "None"
+ );
+ response.addHeader("Set-Cookie", cookie.toString());
+
+ return ApiResponse.onSuccess(result.getLoginResponse());
+ }
+
+ /* 로그아웃 */
+ @Operation(summary = "로그아웃", description = "사용자의 Refresh Token을 Redis에서 삭제합니다.")
+ @PostMapping("/auth/logout")
+ public ApiResponse logout(HttpServletRequest request, HttpServletResponse response) {
+
+ var del = CookieUtil.deleteRefreshCookie(
+ null,
+ true,
+ "None"
+ );
+ response.addHeader("Set-Cookie", del.toString());
+
+ return ApiResponse.onSuccess(authService.logout(request));
+ }
+
+ /* 토큰 재발급 */
+ @Operation(summary = "Access Token 재발급", description = "사용자의 Access Token만 재발급합니다.")
+ @PostMapping("/auth/refresh")
+ public ApiResponse newAccessToken(@CookieValue(name = CookieUtil.REFRESH_COOKIE, required = false) String refreshToken) {
+ return ApiResponse.onSuccess(authService.newAccessToken(refreshToken));
+ }
+
+ /* Access Token 만료 기간 확인 */
+ @Operation(summary = "Access Token 만료 확인", description = "사용자의 Access Token의 만료 여부를 확인합니다.")
+ @GetMapping("/auth/validate")
+ public ApiResponse validate(HttpServletRequest request) {
+ return ApiResponse.onSuccess(authService.isValidAccess(request));
+ }
+
+ /* 로그인 아이디 중복 체크 */
+ @Operation(summary = "아이디 중복 체크", description = "available = true면 중복 X")
+ @GetMapping("/check/login-id")
+ public ApiResponse checkLoginId(@RequestParam("value") String value) {
+ return ApiResponse.onSuccess(authService.checkLoginId(value));
+ }
+
+ /* 이메일 중복 체크 */
+ @Operation(summary = "이메일 중복 체크", description = "available = true면 중복 X")
+ @PostMapping("/check/email")
+ public ApiResponse checkEmail(@RequestBody @Valid EmailCheckRequest request) {
+ return ApiResponse.onSuccess(authService.checkEmail(request.getValue()));
+ }
+
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/internal/LoginResult.java b/src/main/java/vote/vote_be/domain/auth/dto/internal/LoginResult.java
new file mode 100644
index 0000000..aeac929
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/internal/LoginResult.java
@@ -0,0 +1,23 @@
+package vote.vote_be.domain.auth.dto.internal;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import vote.vote_be.domain.auth.dto.response.LoginResponse;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class LoginResult {
+ // login 컨트롤러에서 사용할 정보를 전달하는 DTO
+ private LoginResponse loginResponse;
+ private String refreshToken;
+ private long refreshTtlSec;
+
+ public static LoginResult of(LoginResponse loginResponse, String refreshToken, long refreshTtlSec) {
+ LoginResult result = new LoginResult();
+ result.loginResponse = loginResponse;
+ result.refreshToken = refreshToken;
+ result.refreshTtlSec = refreshTtlSec;
+ return result;
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/request/EmailCheckRequest.java b/src/main/java/vote/vote_be/domain/auth/dto/request/EmailCheckRequest.java
new file mode 100644
index 0000000..f1b0ee5
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/request/EmailCheckRequest.java
@@ -0,0 +1,14 @@
+package vote.vote_be.domain.auth.dto.request;
+
+import jakarta.persistence.Column;
+import jakarta.validation.constraints.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+public class EmailCheckRequest {
+ @NotBlank
+ @Email(message = "이메일 형식이 올바르지 않습니다.")
+ private String value;
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/request/LoginRequest.java b/src/main/java/vote/vote_be/domain/auth/dto/request/LoginRequest.java
new file mode 100644
index 0000000..23398b2
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/request/LoginRequest.java
@@ -0,0 +1,15 @@
+package vote.vote_be.domain.auth.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+public class LoginRequest {
+ @NotBlank
+ private String loginId;
+
+ @NotBlank
+ private String password;
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/request/SignupRequest.java b/src/main/java/vote/vote_be/domain/auth/dto/request/SignupRequest.java
new file mode 100644
index 0000000..edfd0e0
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/request/SignupRequest.java
@@ -0,0 +1,29 @@
+package vote.vote_be.domain.auth.dto.request;
+
+import jakarta.validation.constraints.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import vote.vote_be.domain.user.entity.Part;
+import vote.vote_be.domain.user.entity.Team;
+
+@Getter
+@NoArgsConstructor
+public class SignupRequest {
+ @NotBlank
+ private String loginId;
+
+ @NotBlank
+ private String password;
+
+ @Email @NotBlank
+ private String email;
+
+ @NotNull
+ private Part part; // FRONTEND / BACKEND
+
+ @NotBlank
+ private String name;
+
+ @NotNull
+ private Team team; // MODELLY / DIGGINDIE / CATCHUP / MENUAL / STORIX
+}
\ No newline at end of file
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/response/AccessTokenResponse.java b/src/main/java/vote/vote_be/domain/auth/dto/response/AccessTokenResponse.java
new file mode 100644
index 0000000..2c2d824
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/response/AccessTokenResponse.java
@@ -0,0 +1,15 @@
+package vote.vote_be.domain.auth.dto.response;
+
+import lombok.*;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class AccessTokenResponse {
+ private String accessToken;
+
+ public static AccessTokenResponse of(String accessToken) {
+ AccessTokenResponse response = new AccessTokenResponse();
+ response.accessToken = accessToken;
+ return response;
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/response/DuplicateCheckResponse.java b/src/main/java/vote/vote_be/domain/auth/dto/response/DuplicateCheckResponse.java
new file mode 100644
index 0000000..748151f
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/response/DuplicateCheckResponse.java
@@ -0,0 +1,20 @@
+package vote.vote_be.domain.auth.dto.response;
+
+import lombok.AccessLevel;
+import lombok.*;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class DuplicateCheckResponse {
+ private String field; // loginId or email
+ private String value;
+ private boolean available; // true(중복 X)
+
+ public static DuplicateCheckResponse of(String field, String value, boolean available) {
+ DuplicateCheckResponse response = new DuplicateCheckResponse();
+ response.field = field;
+ response.value = value;
+ response.available = available;
+ return response;
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/response/LoginResponse.java b/src/main/java/vote/vote_be/domain/auth/dto/response/LoginResponse.java
new file mode 100644
index 0000000..85c9712
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/response/LoginResponse.java
@@ -0,0 +1,28 @@
+package vote.vote_be.domain.auth.dto.response;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import vote.vote_be.domain.user.entity.Part;
+import vote.vote_be.domain.user.entity.Team;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class LoginResponse {
+ private Long userId;
+ private String name;
+ private Part part;
+ private Team team;
+ private String accessToken;
+
+ public static LoginResponse of (Long userId, String name, Part part, Team team, String accessToken) {
+ LoginResponse loginResponse = new LoginResponse();
+ loginResponse.userId = userId;
+ loginResponse.name = name;
+ loginResponse.part = part;
+ loginResponse.team = team;
+ loginResponse.accessToken = accessToken;
+
+ return loginResponse;
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/response/SignupResponse.java b/src/main/java/vote/vote_be/domain/auth/dto/response/SignupResponse.java
new file mode 100644
index 0000000..ef6b5c0
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/response/SignupResponse.java
@@ -0,0 +1,21 @@
+package vote.vote_be.domain.auth.dto.response;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class SignupResponse {
+ private String message;
+ private String loginId;
+ private String name;
+
+ public static SignupResponse of(String message, String loginId, String name) {
+ SignupResponse signupResponse = new SignupResponse();
+ signupResponse.message = message;
+ signupResponse.loginId = loginId;
+ signupResponse.name = name;
+ return signupResponse;
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/dto/response/TokenValidationResponse.java b/src/main/java/vote/vote_be/domain/auth/dto/response/TokenValidationResponse.java
new file mode 100644
index 0000000..1f6766f
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/dto/response/TokenValidationResponse.java
@@ -0,0 +1,21 @@
+package vote.vote_be.domain.auth.dto.response;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import vote.vote_be.global.security.entity.TokenStatus;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class TokenValidationResponse {
+ private String message;
+ private TokenStatus isValid; // 유효 여부
+
+ public static TokenValidationResponse of(String message, TokenStatus isValid) {
+ TokenValidationResponse response = new TokenValidationResponse();
+ response.message = message;
+ response.isValid = isValid;
+ return response;
+ }
+
+}
diff --git a/src/main/java/vote/vote_be/domain/auth/service/AuthService.java b/src/main/java/vote/vote_be/domain/auth/service/AuthService.java
new file mode 100644
index 0000000..92d95e5
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/auth/service/AuthService.java
@@ -0,0 +1,168 @@
+package vote.vote_be.domain.auth.service;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import vote.vote_be.domain.auth.dto.internal.LoginResult;
+import vote.vote_be.domain.auth.dto.request.LoginRequest;
+import vote.vote_be.domain.auth.dto.request.SignupRequest;
+import vote.vote_be.domain.auth.dto.response.*;
+import vote.vote_be.domain.user.entity.*;
+import vote.vote_be.domain.user.repository.UserRepository;
+import vote.vote_be.global.apiPayload.code.SimpleMessageDTO;
+import vote.vote_be.global.apiPayload.code.status.ErrorStatus;
+import vote.vote_be.global.apiPayload.exception.GeneralException;
+import vote.vote_be.global.redis.RedisService;
+import vote.vote_be.global.security.dto.TokenResponse;
+import vote.vote_be.global.security.entity.TokenStatus;
+import vote.vote_be.global.security.jwt.TokenProvider;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+ private static final String RT_KEY_PREFIX = "refresh-token:";
+
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final TokenProvider tokenProvider;
+ private final RedisService redisService;
+
+ /* 회원가입 */
+ @Transactional
+ public SignupResponse signup(SignupRequest req) {
+
+ // 중복 체크(login_id, email)
+ if (userRepository.existsByLoginId(req.getLoginId()))
+ throw new GeneralException(ErrorStatus.DUPLICATE_LOGIN_ID);
+
+ if (req.getEmail() != null && userRepository.existsByEmail(req.getEmail()))
+ throw new GeneralException(ErrorStatus.DUPLICATE_EMAIL);
+
+ User user = User.builder()
+ .loginId(req.getLoginId())
+ .password(passwordEncoder.encode(req.getPassword()))
+ .email(req.getEmail())
+ .part(req.getPart())
+ .name(req.getName())
+ .team(req.getTeam())
+ .userRole(UserRole.USER)
+ .build();
+
+ userRepository.save(user);
+
+ return SignupResponse.of("회원가입이 완료되었습니다.", user.getLoginId(), user.getName());
+ }
+
+ /* 로그인 */
+ @Transactional(readOnly = true)
+ public LoginResult login(LoginRequest req) {
+ // LoginId 존재 여부 확인
+ User user = userRepository.findByLoginId(req.getLoginId())
+ .orElseThrow(() -> new GeneralException(ErrorStatus.NOT_FOUND_LOGIN_ID));
+
+ if (!passwordEncoder.matches(req.getPassword(), user.getPassword()))
+ throw new GeneralException(ErrorStatus.LOGIN_FAIL);
+
+ TokenResponse tokens = tokenProvider.createToken(user);
+
+ // refresh token
+ String refreshToken = tokens.getRefreshToken();
+
+ // userId 기준으로 레디스에 refresh token 저장(TTL=refresh 남은 시간)
+ long ttlSec = tokenProvider.getRemainingSeconds(refreshToken);
+ redisService.setRefreshToken(RT_KEY_PREFIX + user.getId(), refreshToken, ttlSec);
+
+ // 로그인 응답 생성
+ LoginResponse loginResponse = LoginResponse.of(
+ user.getId(),
+ user.getName(),
+ user.getPart(),
+ user.getTeam(),
+ tokens.getAccessToken()
+ );
+
+ return LoginResult.of(loginResponse, refreshToken, ttlSec);
+ }
+
+ @Transactional
+ public SimpleMessageDTO logout(HttpServletRequest request) {
+ String accessToken = tokenProvider.resolveToken(request);
+
+ if (accessToken == null || accessToken.isBlank())
+ throw new GeneralException(ErrorStatus.TOKEN_INVALID);
+
+ String userId = tokenProvider.getUserIdFromToken(accessToken);
+ redisService.deleteValue(RT_KEY_PREFIX + userId);
+
+ return new SimpleMessageDTO("로그아웃이 완료되었습니다.");
+ }
+
+
+ @Transactional(readOnly = true)
+ public AccessTokenResponse newAccessToken(String refreshToken) { // 프론트에서 Authorization 헤더를 빼고 전송해줘야함.
+
+ // refresh 토큰 검증
+ TokenStatus tokenStatus = tokenProvider.validateToken(refreshToken);
+ if(tokenStatus == TokenStatus.EXPIRED){
+ throw new GeneralException(ErrorStatus.REFRESH_TOKEN_EXPIRED);
+ }
+ if(tokenStatus == TokenStatus.INVALID || tokenStatus == TokenStatus.MISSING){
+ throw new GeneralException(ErrorStatus.TOKEN_INVALID);
+ }
+
+ // user 확인
+ String userId = tokenProvider.getUserIdFromToken(refreshToken);
+
+ // Redis의 refresh 토큰과 비교
+ String key = RT_KEY_PREFIX + userId;
+ String stored = redisService.getValue(key);
+ if (stored == null || stored.isEmpty()) {
+ // 만료/로그아웃 등으로 없는 상태
+ throw new GeneralException(ErrorStatus.REFRESH_TOKEN_EXPIRED);
+ }
+ if (!stored.equals(refreshToken)) {
+ // 탈취 등으로 일치하지 않는 상태
+ throw new GeneralException(ErrorStatus.TOKEN_INVALID);
+ }
+
+ // user 조회
+ User user = userRepository.findById(Long.valueOf(userId))
+ .orElseThrow(() -> new GeneralException(ErrorStatus.NOT_FOUND_USER));
+
+ // AccessToken만 새로 발급
+ String newAccess = tokenProvider.createAccessToken(user);
+
+ return AccessTokenResponse.of(newAccess);
+ }
+
+ /* 로그인 아이디 중복 체크 */
+ @Transactional(readOnly = true)
+ public DuplicateCheckResponse checkLoginId(String value) {
+ boolean available = !userRepository.existsByLoginId(value);
+ return DuplicateCheckResponse.of("loginId", value, available);
+ }
+
+ /* 이메일 중복 체크 */
+ @Transactional(readOnly = true)
+ public DuplicateCheckResponse checkEmail(String value) {
+ boolean available = !userRepository.existsByEmail(value);
+ return DuplicateCheckResponse.of("email", value, available);
+ }
+
+ // Access Token의 유효성 확인
+ @Transactional(readOnly = true)
+ public TokenValidationResponse isValidAccess(HttpServletRequest request) {
+ // 유효성 검사는 JwtAuthenticationFilter에서 처리
+ String accessToken = tokenProvider.resolveToken(request);
+ TokenStatus status = tokenProvider.validateToken(accessToken);
+
+ if (status == TokenStatus.MISSING) {
+ throw new GeneralException(ErrorStatus.TOKEN_INVALID);
+ }
+
+ return TokenValidationResponse.of("Access Token이 유효합니다.", status);
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/user/entity/Part.java b/src/main/java/vote/vote_be/domain/user/entity/Part.java
new file mode 100644
index 0000000..4090cd7
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/user/entity/Part.java
@@ -0,0 +1,6 @@
+package vote.vote_be.domain.user.entity;
+
+public enum Part {
+ FRONTEND,
+ BACKEND
+}
diff --git a/src/main/java/vote/vote_be/domain/user/entity/Team.java b/src/main/java/vote/vote_be/domain/user/entity/Team.java
new file mode 100644
index 0000000..bcb7588
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/user/entity/Team.java
@@ -0,0 +1,9 @@
+package vote.vote_be.domain.user.entity;
+
+public enum Team {
+ MODELLY, // 최고의 팀
+ DIGGINDIE,
+ CATCHUP,
+ MENUAL,
+ STORIX
+}
\ No newline at end of file
diff --git a/src/main/java/vote/vote_be/domain/user/entity/User.java b/src/main/java/vote/vote_be/domain/user/entity/User.java
new file mode 100644
index 0000000..6614b38
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/user/entity/User.java
@@ -0,0 +1,43 @@
+package vote.vote_be.domain.user.entity;
+
+import jakarta.persistence.*;
+import lombok.*;
+import vote.vote_be.global.entity.BaseEntity;
+
+@Entity
+@Table(name = "users")
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+public class User extends BaseEntity {
+ @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "user_id")
+ private Long id;
+
+ @Column(name = "login_id", length = 20, nullable = false, unique = true) // VARCHAR(20)
+ private String loginId;
+
+ @Column(name = "password", length = 255, nullable = false) // VARCHAR(255)
+ private String password;
+
+ @Column(name = "email", length = 50, nullable = false, unique = true) // VARCHAR(50)
+ private String email;
+
+ @Column(name = "name", length = 20, nullable = false) // VARCHAR(20)
+ private String name;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "part", nullable = false)
+ private Part part;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "team", nullable = false)
+ private Team team;
+
+ // 일단 UserRole 도입.
+ @Enumerated(EnumType.STRING)
+ @Builder.Default
+ @Column(name = "user_role", nullable = false)
+ private UserRole userRole = UserRole.USER;
+}
diff --git a/src/main/java/vote/vote_be/domain/user/entity/UserRole.java b/src/main/java/vote/vote_be/domain/user/entity/UserRole.java
new file mode 100644
index 0000000..7d356a8
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/user/entity/UserRole.java
@@ -0,0 +1,6 @@
+package vote.vote_be.domain.user.entity;
+
+public enum UserRole {
+ USER,
+ ADMIN
+}
diff --git a/src/main/java/vote/vote_be/domain/user/repository/UserRepository.java b/src/main/java/vote/vote_be/domain/user/repository/UserRepository.java
new file mode 100644
index 0000000..1c48273
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/user/repository/UserRepository.java
@@ -0,0 +1,12 @@
+package vote.vote_be.domain.user.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import vote.vote_be.domain.user.entity.User;
+
+import java.util.Optional;
+
+public interface UserRepository extends JpaRepository {
+ Optional findByLoginId(String loginId);
+ boolean existsByLoginId(String loginId);
+ boolean existsByEmail(String email);
+}
\ No newline at end of file
diff --git a/src/main/java/vote/vote_be/domain/vote/controller/DemodayVoteController.java b/src/main/java/vote/vote_be/domain/vote/controller/DemodayVoteController.java
new file mode 100644
index 0000000..6ec4ab4
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/controller/DemodayVoteController.java
@@ -0,0 +1,51 @@
+package vote.vote_be.domain.vote.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+import vote.vote_be.domain.vote.dto.request.VoteRequestDto;
+import vote.vote_be.domain.vote.dto.response.CandidateListResponseDto;
+import vote.vote_be.domain.vote.dto.response.VoteResultListResponseDto;
+import vote.vote_be.domain.vote.service.DemodayVoteService;
+import vote.vote_be.global.apiPayload.ApiResponse;
+import vote.vote_be.global.security.AuthDetails;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@Tag(name = "데모데이 투표관련 API")
+public class DemodayVoteController {
+
+ private final DemodayVoteService demodayVoteService;
+
+ @GetMapping("/votes/demoday")
+ @Operation(summary = "데모데이 후보(팀) 목록 조회", description = "데모데이 팀 조회 시에 사용하는 API입니다.")
+ public ApiResponse> getDemodayCandidates(
+ @AuthenticationPrincipal AuthDetails authDetails
+ ) {
+ authDetails.user(); //jwt 인증 용도
+ return ApiResponse.onSuccess(demodayVoteService.getDemodayCandidateList());
+ }
+
+ @PostMapping("/votes/demoday")
+ @Operation(summary = "데모데이 투표", description = "데모데이 투표 시에 사용하는 API입니다.")
+ public ApiResponse voteDemoday(
+ @AuthenticationPrincipal AuthDetails authDetails,
+ @RequestBody VoteRequestDto requestDto
+ ) {
+ demodayVoteService.voteDemoday(authDetails.user(), requestDto);
+ return ApiResponse.onSuccess("데모데이 투표가 완료되었습니다.");
+ }
+
+ @GetMapping("/votes/demoday-result")
+ @Operation(summary = "데모데이 투표 결과 조회", description = "데모데이 투표 결과 조회 시에 사용하는 API입니다.")
+ public ApiResponse getDemodayResult(
+ @AuthenticationPrincipal AuthDetails authDetails
+ ) {
+ authDetails.user(); //jwt 인증 용도
+ return ApiResponse.onSuccess(demodayVoteService.getDemodayVoteResult());
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/controller/LeaderVoteController.java b/src/main/java/vote/vote_be/domain/vote/controller/LeaderVoteController.java
new file mode 100644
index 0000000..4efcde4
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/controller/LeaderVoteController.java
@@ -0,0 +1,49 @@
+package vote.vote_be.domain.vote.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+import vote.vote_be.domain.vote.dto.request.VoteRequestDto;
+import vote.vote_be.domain.vote.dto.response.CandidateListResponseDto;
+import vote.vote_be.domain.vote.dto.response.VoteResultListResponseDto;
+import vote.vote_be.domain.vote.service.LeaderVoteService;
+import vote.vote_be.global.apiPayload.ApiResponse;
+import vote.vote_be.global.security.AuthDetails;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@Tag(name = "파트장 투표관련 API")
+public class LeaderVoteController {
+
+ private final LeaderVoteService leaderVoteService;
+
+ @GetMapping("/votes")
+ @Operation(summary = "파트장 후보자 명단 조회 API", description = "파트장 후보자 명단 조회 시에 사용하는 API입니다.")
+ public ApiResponse> getCandidateList(@AuthenticationPrincipal AuthDetails authDetails) {
+ List responseDtos = leaderVoteService.getCandidateList(authDetails.user());
+
+ return ApiResponse.onSuccess(responseDtos);
+ }
+
+ @PostMapping("/votes/leader")
+ @Operation(summary = "파트장 투표 API", description = "파트장 투표 시에 사용하는 API입니다.")
+ public ApiResponse votePartLeader(@AuthenticationPrincipal AuthDetails authDetails, @RequestBody VoteRequestDto requestDto) {
+
+ leaderVoteService.votePartLeader(authDetails.user(), requestDto);
+
+ return ApiResponse.onSuccess("파트장 투표가 완료되었습니다.");
+ }
+
+ @GetMapping("/votes/leader-result")
+ @Operation(summary = "파트장 투표 결과 조회 API", description = "파트장 투표 결과 조회 시에 사용하는 API입니다.")
+ public ApiResponse getPartLeaderVoteResult(@AuthenticationPrincipal AuthDetails authDetails) {
+ VoteResultListResponseDto result = leaderVoteService.getPartLeaderVoteResult(authDetails.user());
+
+ return ApiResponse.onSuccess(result);
+ }
+
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/dto/CandidateComponent.java b/src/main/java/vote/vote_be/domain/vote/dto/CandidateComponent.java
new file mode 100644
index 0000000..b1ef6ab
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/dto/CandidateComponent.java
@@ -0,0 +1,7 @@
+package vote.vote_be.domain.vote.dto;
+
+public record CandidateComponent(
+ String candidateName,
+ long voteCount
+) {
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/dto/request/VoteRequestDto.java b/src/main/java/vote/vote_be/domain/vote/dto/request/VoteRequestDto.java
new file mode 100644
index 0000000..96b5fc8
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/dto/request/VoteRequestDto.java
@@ -0,0 +1,6 @@
+package vote.vote_be.domain.vote.dto.request;
+
+public record VoteRequestDto(
+ Long candidateId
+) {
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/dto/response/CandidateListResponseDto.java b/src/main/java/vote/vote_be/domain/vote/dto/response/CandidateListResponseDto.java
new file mode 100644
index 0000000..2336878
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/dto/response/CandidateListResponseDto.java
@@ -0,0 +1,7 @@
+package vote.vote_be.domain.vote.dto.response;
+
+public record CandidateListResponseDto(
+ String candidateName,
+ Long candidateId
+) {
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/dto/response/VoteResultListResponseDto.java b/src/main/java/vote/vote_be/domain/vote/dto/response/VoteResultListResponseDto.java
new file mode 100644
index 0000000..8a066b7
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/dto/response/VoteResultListResponseDto.java
@@ -0,0 +1,15 @@
+package vote.vote_be.domain.vote.dto.response;
+
+import vote.vote_be.domain.vote.dto.CandidateComponent;
+
+import java.util.List;
+
+public record VoteResultListResponseDto(
+ List candidateList,
+ int totalVoteCount
+) {
+
+ public static VoteResultListResponseDto from(List candidateList, int totalVoteCount) {
+ return new VoteResultListResponseDto(candidateList, totalVoteCount);
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/entity/Candidate.java b/src/main/java/vote/vote_be/domain/vote/entity/Candidate.java
new file mode 100644
index 0000000..6e69781
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/entity/Candidate.java
@@ -0,0 +1,27 @@
+package vote.vote_be.domain.vote.entity;
+
+import jakarta.persistence.*;
+import lombok.*;
+import vote.vote_be.domain.user.entity.Part;
+import vote.vote_be.domain.user.entity.Team;
+import vote.vote_be.global.entity.BaseEntity;
+
+@Entity
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Candidate extends BaseEntity {
+ @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private VoteCategory voteCategory;
+
+ private String name;
+
+ @Enumerated(EnumType.STRING)
+ private Team team;
+
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/entity/Vote.java b/src/main/java/vote/vote_be/domain/vote/entity/Vote.java
new file mode 100644
index 0000000..851dcec
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/entity/Vote.java
@@ -0,0 +1,31 @@
+package vote.vote_be.domain.vote.entity;
+
+import jakarta.persistence.*;
+import lombok.*;
+import vote.vote_be.domain.user.entity.User;
+import vote.vote_be.global.entity.BaseEntity;
+
+@Entity
+@Getter
+@Builder
+@AllArgsConstructor
+@Table(name = "vote",
+ uniqueConstraints =
+ @UniqueConstraint(columnNames = {"user_id", "vote_category"}))
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Vote extends BaseEntity {
+ @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "vote_category", nullable = false)
+ private VoteCategory voteCategory;
+
+ @ManyToOne
+ @JoinColumn(name = "candidate_id", nullable = false)
+ private Candidate candidate;
+
+ @ManyToOne
+ @JoinColumn(name = "user_id", nullable = false)
+ private User user;
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/entity/VoteCategory.java b/src/main/java/vote/vote_be/domain/vote/entity/VoteCategory.java
new file mode 100644
index 0000000..5c47daf
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/entity/VoteCategory.java
@@ -0,0 +1,11 @@
+package vote.vote_be.domain.vote.entity;
+
+public enum VoteCategory {
+ FRONTEND_PART_LEADER("프론트엔드 파트장 투표"),
+ BACKEND_PART_LEADER("백엔드 파트장 투표"),
+ DEMODAY("데모데이 투표");
+
+ public final String description;
+
+ VoteCategory(String description) {this.description = description;}
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/repository/CandidateRepository.java b/src/main/java/vote/vote_be/domain/vote/repository/CandidateRepository.java
new file mode 100644
index 0000000..f498c6d
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/repository/CandidateRepository.java
@@ -0,0 +1,26 @@
+package vote.vote_be.domain.vote.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import vote.vote_be.domain.vote.dto.CandidateComponent;
+import vote.vote_be.domain.vote.dto.response.CandidateListResponseDto;
+import vote.vote_be.domain.vote.entity.Candidate;
+import vote.vote_be.domain.vote.entity.VoteCategory;
+
+import java.util.List;
+
+public interface CandidateRepository extends JpaRepository {
+ @Query("select c.name, count(v.id) " +
+ "from Candidate c " +
+ "left join Vote v on v.candidate = c " +
+ "where c.voteCategory = :voteCategory " +
+ "group by c.id, c.name " +
+ "order by count (v.id) desc, c.id desc " +
+ "limit 3 ")
+ List findVoteResultsByVoteCategory(VoteCategory voteCategory);
+
+ @Query("select c.name, c.id " +
+ "from Candidate c " +
+ "where c.voteCategory = :voteCategory ")
+ List findAllByVoteCategory(VoteCategory voteCategory);
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/repository/VoteRepository.java b/src/main/java/vote/vote_be/domain/vote/repository/VoteRepository.java
new file mode 100644
index 0000000..c22848f
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/repository/VoteRepository.java
@@ -0,0 +1,20 @@
+package vote.vote_be.domain.vote.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import vote.vote_be.domain.user.entity.User;
+import vote.vote_be.domain.vote.dto.CandidateComponent;
+import vote.vote_be.domain.vote.entity.Vote;
+import vote.vote_be.domain.vote.entity.VoteCategory;
+
+import java.util.List;
+
+public interface VoteRepository extends JpaRepository {
+ boolean existsByUserAndVoteCategory(User user, VoteCategory voteCategory);
+
+
+ @Query("select count(v.id) " +
+ "from Vote v " +
+ "where v.voteCategory = :voteCategory")
+ int countsByVoteCategory(VoteCategory voteCategory);
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/service/CandidateService.java b/src/main/java/vote/vote_be/domain/vote/service/CandidateService.java
new file mode 100644
index 0000000..5b32730
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/service/CandidateService.java
@@ -0,0 +1,33 @@
+package vote.vote_be.domain.vote.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import vote.vote_be.domain.vote.dto.CandidateComponent;
+import vote.vote_be.domain.vote.dto.response.CandidateListResponseDto;
+import vote.vote_be.domain.vote.entity.Candidate;
+import vote.vote_be.domain.vote.entity.VoteCategory;
+import vote.vote_be.domain.vote.repository.CandidateRepository;
+import vote.vote_be.global.apiPayload.code.status.ErrorStatus;
+import vote.vote_be.global.apiPayload.exception.GeneralException;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class CandidateService {
+
+ private final CandidateRepository candidateRepository;
+
+ public Candidate getById(Long candidateId) {
+ return candidateRepository.findById(candidateId)
+ .orElseThrow(()-> new GeneralException(ErrorStatus.NOT_FOUND_CANDIDATE));
+ }
+
+ public List getAllByVoteCategory(VoteCategory voteCategory) {
+ return candidateRepository.findAllByVoteCategory(voteCategory);
+ }
+
+ public List getVoteResultByVoteCategory(VoteCategory voteCategory) {
+ return candidateRepository.findVoteResultsByVoteCategory(voteCategory);
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/service/DemodayVoteService.java b/src/main/java/vote/vote_be/domain/vote/service/DemodayVoteService.java
new file mode 100644
index 0000000..bd4dfb9
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/service/DemodayVoteService.java
@@ -0,0 +1,61 @@
+package vote.vote_be.domain.vote.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import vote.vote_be.domain.user.entity.User;
+import vote.vote_be.domain.vote.dto.CandidateComponent;
+import vote.vote_be.domain.vote.dto.request.VoteRequestDto;
+import vote.vote_be.domain.vote.dto.response.CandidateListResponseDto;
+import vote.vote_be.domain.vote.dto.response.VoteResultListResponseDto;
+import vote.vote_be.domain.vote.entity.Candidate;
+import vote.vote_be.domain.vote.entity.Vote;
+import vote.vote_be.domain.vote.entity.VoteCategory;
+import vote.vote_be.global.apiPayload.code.status.ErrorStatus;
+import vote.vote_be.global.apiPayload.exception.GeneralException;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class DemodayVoteService {
+
+ private final CandidateService candidateService;
+ private final VoteService voteService;
+
+ @Transactional(readOnly = true)
+ public List getDemodayCandidateList() {
+ return candidateService.getAllByVoteCategory(VoteCategory.DEMODAY);
+ }
+
+ @Transactional
+ public void voteDemoday(User user, VoteRequestDto requestDto) {
+ Candidate candidate = candidateService.getById(requestDto.candidateId());
+
+ if (candidate.getVoteCategory() != VoteCategory.DEMODAY) {
+ throw new GeneralException(ErrorStatus.NOT_FOUND_DEMODAY);
+ }
+
+ if (user.getTeam() == candidate.getTeam()) {
+ throw new GeneralException(ErrorStatus.CANNOT_VOTE_OWN_TEAM);
+ }
+
+ voteService.existAlreadyVote(user, VoteCategory.DEMODAY);
+
+ Vote vote = Vote.builder()
+ .voteCategory(VoteCategory.DEMODAY)
+ .user(user)
+ .candidate(candidate)
+ .build();
+
+ voteService.save(vote);
+ }
+
+ @Transactional(readOnly = true)
+ public VoteResultListResponseDto getDemodayVoteResult() {
+ List results = candidateService.getVoteResultByVoteCategory(VoteCategory.DEMODAY);
+ int totalVotes = voteService.getTotalVotes(VoteCategory.DEMODAY);
+
+ return VoteResultListResponseDto.from(results, totalVotes);
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/service/LeaderVoteService.java b/src/main/java/vote/vote_be/domain/vote/service/LeaderVoteService.java
new file mode 100644
index 0000000..72ffdc1
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/service/LeaderVoteService.java
@@ -0,0 +1,76 @@
+package vote.vote_be.domain.vote.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import vote.vote_be.domain.user.entity.Part;
+import vote.vote_be.domain.user.entity.User;
+import vote.vote_be.domain.vote.dto.CandidateComponent;
+import vote.vote_be.domain.vote.dto.request.VoteRequestDto;
+import vote.vote_be.domain.vote.dto.response.CandidateListResponseDto;
+import vote.vote_be.domain.vote.dto.response.VoteResultListResponseDto;
+import vote.vote_be.domain.vote.entity.Candidate;
+import vote.vote_be.domain.vote.entity.Vote;
+import vote.vote_be.domain.vote.entity.VoteCategory;
+import vote.vote_be.global.apiPayload.code.status.ErrorStatus;
+import vote.vote_be.global.apiPayload.exception.GeneralException;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class LeaderVoteService {
+
+ private final CandidateService candidateService;
+ private final VoteService voteService;
+
+ @Transactional
+ public void votePartLeader(User user, VoteRequestDto requestDto) {
+ Candidate candidate = candidateService.getById(requestDto.candidateId());
+
+ //파트 확인
+ if ( !((user.getPart() == Part.FRONTEND && candidate.getVoteCategory() == VoteCategory.FRONTEND_PART_LEADER) ||
+ (user.getPart() == Part.BACKEND && candidate.getVoteCategory() == VoteCategory.BACKEND_PART_LEADER))){
+ throw new GeneralException(ErrorStatus.MISMATCH_PART);
+ }
+
+ //중복투표인지 확인
+ voteService.existAlreadyVote(user, candidate.getVoteCategory());
+
+ Vote vote = Vote.builder()
+ .voteCategory(candidate.getVoteCategory())
+ .user(user)
+ .candidate(candidate)
+ .build();
+
+ voteService.save(vote);
+ }
+
+ @Transactional(readOnly = true)
+ public VoteResultListResponseDto getPartLeaderVoteResult(User user) {
+ List candidateComponentList;
+ int totalVotes;
+
+ if (user.getPart() == Part.FRONTEND) {
+ candidateComponentList = candidateService.getVoteResultByVoteCategory(VoteCategory.FRONTEND_PART_LEADER);
+ totalVotes = voteService.getTotalVotes(VoteCategory.FRONTEND_PART_LEADER);
+ } else {
+ candidateComponentList = candidateService.getVoteResultByVoteCategory(VoteCategory.BACKEND_PART_LEADER);
+ totalVotes = voteService.getTotalVotes(VoteCategory.BACKEND_PART_LEADER);
+ }
+
+ return VoteResultListResponseDto.from(candidateComponentList, totalVotes);
+ }
+
+ @Transactional(readOnly = true)
+ public List getCandidateList(User user) {
+ List candidateListResponseDtos;
+ if (user.getPart() == Part.FRONTEND) {
+ candidateListResponseDtos = candidateService.getAllByVoteCategory(VoteCategory.FRONTEND_PART_LEADER);
+ } else {
+ candidateListResponseDtos = candidateService.getAllByVoteCategory(VoteCategory.BACKEND_PART_LEADER);
+ }
+
+ return candidateListResponseDtos;
+ }
+}
diff --git a/src/main/java/vote/vote_be/domain/vote/service/VoteService.java b/src/main/java/vote/vote_be/domain/vote/service/VoteService.java
new file mode 100644
index 0000000..94c6187
--- /dev/null
+++ b/src/main/java/vote/vote_be/domain/vote/service/VoteService.java
@@ -0,0 +1,34 @@
+package vote.vote_be.domain.vote.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import vote.vote_be.domain.user.entity.User;
+import vote.vote_be.domain.vote.dto.CandidateComponent;
+import vote.vote_be.domain.vote.entity.Vote;
+import vote.vote_be.domain.vote.entity.VoteCategory;
+import vote.vote_be.domain.vote.repository.VoteRepository;
+import vote.vote_be.global.apiPayload.code.status.ErrorStatus;
+import vote.vote_be.global.apiPayload.exception.GeneralException;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class VoteService {
+
+ private final VoteRepository voteRepository;
+
+ public void existAlreadyVote(User user, VoteCategory voteCategory) {
+ if (voteRepository.existsByUserAndVoteCategory(user, voteCategory)){
+ throw new GeneralException(ErrorStatus.DUPLICATE_VOTE);
+ }
+ }
+
+ public void save(Vote vote) {
+ voteRepository.save(vote);
+ }
+
+ public int getTotalVotes(VoteCategory voteCategory) {
+ return voteRepository.countsByVoteCategory(voteCategory);
+ }
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/ApiResponse.java b/src/main/java/vote/vote_be/global/apiPayload/ApiResponse.java
new file mode 100644
index 0000000..0362704
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/ApiResponse.java
@@ -0,0 +1,36 @@
+package vote.vote_be.global.apiPayload;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import vote.vote_be.global.apiPayload.code.BaseCode;
+import vote.vote_be.global.apiPayload.code.status.SuccessStatus;
+
+@Getter
+@AllArgsConstructor
+@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
+public class ApiResponse {
+
+ @JsonProperty("isSuccess")
+ private final Boolean isSuccess;
+ private final String code;
+ private final String message;
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private T result;
+
+ // 성공한 경우 응답 생성
+ public static ApiResponse onSuccess(T result){
+ return new ApiResponse<>(true, SuccessStatus.OK.getCode(), SuccessStatus.OK.getMessage(), result);
+ }
+
+ public static ApiResponse of(BaseCode code, T result){
+ return new ApiResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result);
+ }
+
+ // 실패한 경우 응답 생성
+ public static ApiResponse onFailure(String code, String message, T data){
+ return new ApiResponse<>(false, code, message, data);
+ }
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/code/BaseCode.java b/src/main/java/vote/vote_be/global/apiPayload/code/BaseCode.java
new file mode 100644
index 0000000..6d4c485
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/code/BaseCode.java
@@ -0,0 +1,6 @@
+package vote.vote_be.global.apiPayload.code;
+
+public interface BaseCode {
+ public ReasonDTO getReason();
+ public ReasonDTO getReasonHttpStatus();
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/code/BaseErrorCode.java b/src/main/java/vote/vote_be/global/apiPayload/code/BaseErrorCode.java
new file mode 100644
index 0000000..4e8ad52
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/code/BaseErrorCode.java
@@ -0,0 +1,6 @@
+package vote.vote_be.global.apiPayload.code;
+
+public interface BaseErrorCode {
+ public ErrorReasonDTO getReason();
+ public ErrorReasonDTO getReasonHttpStatus();
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/code/ErrorReasonDTO.java b/src/main/java/vote/vote_be/global/apiPayload/code/ErrorReasonDTO.java
new file mode 100644
index 0000000..5961133
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/code/ErrorReasonDTO.java
@@ -0,0 +1,23 @@
+package vote.vote_be.global.apiPayload.code;
+
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public class ErrorReasonDTO {
+
+ private final Boolean isSuccess;
+ private final String code;
+ private final String message;
+
+ private final HttpStatus httpStatus;
+
+ @Builder
+ public ErrorReasonDTO(Boolean isSuccess, String code, String message, HttpStatus httpStatus) {
+ this.isSuccess = isSuccess;
+ this.code = code;
+ this.message = message;
+ this.httpStatus = httpStatus;
+ }
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/code/ReasonDTO.java b/src/main/java/vote/vote_be/global/apiPayload/code/ReasonDTO.java
new file mode 100644
index 0000000..b0c1e18
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/code/ReasonDTO.java
@@ -0,0 +1,23 @@
+package vote.vote_be.global.apiPayload.code;
+
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public class ReasonDTO {
+
+ private final Boolean isSuccess;
+ private final String code;
+ private final String message;
+
+ private final HttpStatus httpStatus;
+
+ @Builder
+ public ReasonDTO(Boolean isSuccess, String code, String message, HttpStatus httpStatus) {
+ this.isSuccess = isSuccess;
+ this.code = code;
+ this.message = message;
+ this.httpStatus = httpStatus;
+ }
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/code/SimpleMessageDTO.java b/src/main/java/vote/vote_be/global/apiPayload/code/SimpleMessageDTO.java
new file mode 100644
index 0000000..ed266da
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/code/SimpleMessageDTO.java
@@ -0,0 +1,10 @@
+package vote.vote_be.global.apiPayload.code;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class SimpleMessageDTO {
+ private String message;
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/vote/vote_be/global/apiPayload/code/status/ErrorStatus.java
new file mode 100644
index 0000000..11d9fdd
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/code/status/ErrorStatus.java
@@ -0,0 +1,68 @@
+package vote.vote_be.global.apiPayload.code.status;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+import vote.vote_be.global.apiPayload.code.BaseErrorCode;
+import vote.vote_be.global.apiPayload.code.ErrorReasonDTO;
+
+@Getter
+@AllArgsConstructor
+public enum ErrorStatus implements BaseErrorCode {
+
+ // 일반적인 응답
+ _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
+ _BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
+ _UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
+ _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
+
+ // 레디스 설정 오류
+ REDIS_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "REDIS_ERROR", "Redis 설정에 오류가 발생했습니다."),
+
+ // User
+ NOT_FOUND_USER(HttpStatus.NOT_FOUND, "USER404", "해당 유저를 찾을 수 없습니다."),
+
+ // Token/JWT
+ NOT_FOUND_TOKEN(HttpStatus.NOT_FOUND,"TOKEN404","토큰을 찾을 수 없습니다."),
+ TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "TOKEN401", "토큰이 유효하지 않습니다."),
+ ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "TOKEN401", "Access Token이 만료되었습니다."),
+ REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "TOKEN401", "Refresh Token이 만료되었습니다."),
+
+ // Auth(로그인, 회원가입 관련)
+ NOT_FOUND_LOGIN_ID(HttpStatus.NOT_FOUND, "AUTH404", "존재하지 않는 로그인 아이디입니다."),
+ DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "AUTH409", "이미 사용 중인 아이디입니다."),
+ DUPLICATE_EMAIL(HttpStatus.CONFLICT, "AUTH409", "이미 사용 중인 이메일입니다."),
+ DESIGNER_FIELDS_REQUIRED(HttpStatus.BAD_REQUEST, "AUTH400", "디자이너 회원가입에 필요한 정보가 누락되었습니다."),
+ LOGIN_FAIL(HttpStatus.UNAUTHORIZED, "AUTH401", "아이디 또는 비밀번호가 잘못 되었습니다."),
+
+ //Candidate
+ NOT_FOUND_CANDIDATE(HttpStatus.NOT_FOUND, "CANDIDATE404", "후보자를 찾을 수 없습니다."),
+ MISMATCH_PART(HttpStatus.BAD_REQUEST, "VOTE400", "자신이 속한 파트의 파트장 투표만 가능합니다."),
+ DUPLICATE_VOTE(HttpStatus.CONFLICT, "VOTE409", "중복 투표는 불가능합니다."),
+ NOT_FOUND_DEMODAY(HttpStatus.NOT_FOUND, "CANDIDATE404", "데모데이 팀을 찾을 수 없습니다."),
+ CANNOT_VOTE_OWN_TEAM(HttpStatus.BAD_REQUEST, "VOTE400", "자신이 속한 팀은 투표할 수 없습니다.")
+ ;
+
+ private final HttpStatus httpStatus;
+ private final String code;
+ private final String message;
+
+ @Override
+ public ErrorReasonDTO getReason() {
+ return ErrorReasonDTO.builder()
+ .message(message)
+ .code(code)
+ .isSuccess(false)
+ .build();
+ }
+
+ @Override
+ public ErrorReasonDTO getReasonHttpStatus() {
+ return ErrorReasonDTO.builder()
+ .message(message)
+ .code(code)
+ .isSuccess(false)
+ .httpStatus(httpStatus)
+ .build();
+ }
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/code/status/SuccessStatus.java b/src/main/java/vote/vote_be/global/apiPayload/code/status/SuccessStatus.java
new file mode 100644
index 0000000..a4a520f
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/code/status/SuccessStatus.java
@@ -0,0 +1,39 @@
+package vote.vote_be.global.apiPayload.code.status;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+import vote.vote_be.global.apiPayload.code.BaseCode;
+import vote.vote_be.global.apiPayload.code.ReasonDTO;
+
+@Getter
+@AllArgsConstructor
+public enum SuccessStatus implements BaseCode {
+
+ // 가장 일반적인 응답
+ OK(HttpStatus.OK, "200", "요청이 성공했습니다."),
+ CREATED(HttpStatus.CREATED, "201", "생성이 완료되었습니다.");
+
+ private final HttpStatus httpStatus;
+ private final String code;
+ private final String message;
+
+ @Override
+ public ReasonDTO getReason() {
+ return ReasonDTO.builder()
+ .message(message)
+ .code(code)
+ .isSuccess(true)
+ .build();
+ }
+
+ @Override
+ public ReasonDTO getReasonHttpStatus() {
+ return ReasonDTO.builder()
+ .message(message)
+ .code(code)
+ .isSuccess(true)
+ .httpStatus(httpStatus)
+ .build();
+ }
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/exception/GeneralException.java b/src/main/java/vote/vote_be/global/apiPayload/exception/GeneralException.java
new file mode 100644
index 0000000..a78812f
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/exception/GeneralException.java
@@ -0,0 +1,23 @@
+package vote.vote_be.global.apiPayload.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import vote.vote_be.global.apiPayload.code.BaseErrorCode;
+import vote.vote_be.global.apiPayload.code.ErrorReasonDTO;
+
+
+@Getter
+@AllArgsConstructor
+public class GeneralException extends RuntimeException {
+
+ private BaseErrorCode code;
+
+ public ErrorReasonDTO getErrorReason() {
+ return this.code.getReason();
+ }
+
+ public ErrorReasonDTO getErrorReasonHttpStatus(){
+ return this.code.getReasonHttpStatus();
+ }
+
+}
diff --git a/src/main/java/vote/vote_be/global/apiPayload/exception/handler/GlobalExceptionHandler.java b/src/main/java/vote/vote_be/global/apiPayload/exception/handler/GlobalExceptionHandler.java
new file mode 100644
index 0000000..bff93ec
--- /dev/null
+++ b/src/main/java/vote/vote_be/global/apiPayload/exception/handler/GlobalExceptionHandler.java
@@ -0,0 +1,121 @@
+package vote.vote_be.global.apiPayload.exception.handler;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+import vote.vote_be.global.apiPayload.ApiResponse;
+import vote.vote_be.global.apiPayload.code.ErrorReasonDTO;
+import vote.vote_be.global.apiPayload.code.status.ErrorStatus;
+import vote.vote_be.global.apiPayload.exception.GeneralException;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+@Slf4j
+@RestControllerAdvice(annotations = {RestController.class})
+public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
+
+
+ @ExceptionHandler
+ public ResponseEntity