diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..8af972c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+/gradlew text eol=lf
+*.bat text eol=crlf
+*.jar binary
diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml
new file mode 100644
index 0000000..2f86c5b
--- /dev/null
+++ b/.github/workflows/CICD.yml
@@ -0,0 +1,121 @@
+# github repository actions 페이지에 나타날 이름
+name: CI/CD using github actions & docker
+
+# event trigger
+# main브랜치에 pull request가 발생 되었을 때 실행
+on:
+ pull_request:
+ branches: [ "master" ]
+ push:
+ branches: [ "master" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@v3
+
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Gradle Caching
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x ./gradlew
+
+ - name: Build Project with Gradle
+ run: |
+ echo ${{ secrets.APPLICATION_SECRET }} | base64 --decode > ./src/main/resources/application-secret.yml
+ ./gradlew bootJar
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build docker image
+ run: docker build --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/vote .
+
+ - name: Publish image to docker hub
+ run: docker push ${{ secrets.DOCKER_USERNAME }}/vote:latest
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set Target IP
+ run: |
+ STATUS=$(curl -o /dev/null -w "%{http_code}" "${{ secrets.SERVER_IP }}/env")
+ echo $STATUS
+ if [ $STATUS = 200 ]; then
+ CURRENT_UPSTREAM=$(curl -s "${{ secrets.SERVER_IP }}/env")
+ else
+ CURRENT_UPSTREAM=green
+ fi
+ echo CURRENT_UPSTREAM=$CURRENT_UPSTREAM >> $GITHUB_ENV
+ if [ $CURRENT_UPSTREAM = blue ]; then
+ echo "CURRENT_PORT=8080" >> $GITHUB_ENV
+ echo "STOPPED_PORT=8081" >> $GITHUB_ENV
+ echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
+ else
+ echo "CURRENT_PORT=8081" >> $GITHUB_ENV
+ echo "STOPPED_PORT=8080" >> $GITHUB_ENV
+ echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
+ fi
+
+ - name: Docker Compose
+ uses: appleboy/ssh-action@master
+ with:
+ username: ubuntu
+ host: ${{ secrets.SERVER_IP }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ script_stop: true
+ script: |
+ export JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}
+ sudo docker pull ${{ secrets.DOCKER_USERNAME }}/vote:latest
+ sudo docker-compose -f docker-compose-${{env.TARGET_UPSTREAM}}.yml up -d
+
+ - name: Check deploy server URL
+ uses: jtalk/url-health-check-action@v3
+ with:
+ url: http://${{ secrets.SERVER_IP }}:${{ env.STOPPED_PORT }}/env
+ max-attempts: 5
+ retry-delay: 10s
+
+ - name: Change nginx upstream
+ uses: appleboy/ssh-action@master
+ with:
+ username: ubuntu
+ host: ${{ secrets.SERVER_IP }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ script_stop: true
+ script: |
+ sudo docker exec -i nginxserver bash -c 'echo "set \$service_url ${{env.TARGET_UPSTREAM}};" > etc/nginx/conf.d/service-env.inc && nginx -s reload'
+
+ - name: Stop current server
+ uses: appleboy/ssh-action@master
+ with:
+ username: ubuntu
+ host: ${{ secrets.SERVER_IP }}
+ key: ${{ secrets.EC2_SSH_KEY }}
+ script_stop: true
+ script: |
+ sudo docker stop ${{ env.CURRENT_UPSTREAM }}
+ sudo docker rm ${{ env.CURRENT_UPSTREAM }}
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..549350d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,42 @@
+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/
+
+### 환경변수 ###
+.env
+
+application-secret.yml
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..4d25e0b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,8 @@
+FROM openjdk:17-jdk-slim
+
+ARG JAR_FILE=/build/libs/*.jar
+ARG PROFILES
+ARG ENV
+
+COPY ${JAR_FILE} app.jar
+ENTRYPOINT ["java", "-Dspring.profiles.active=${PROFILES}", "-Dserver.env=${ENV}", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 76efd88..38a2d6e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,180 @@
-# spring_vote_20th
\ No newline at end of file
+# spring_vote_20th
+
+## ERD
+전체 ERD
+
+
+
+- 유저, 파트장 투표, 팀 투표를 분리시켜 구성하였다.
+
+### User
+사용자에 관한 테이블
+
+
+
+- 사용자는 name과 password를 통해 로그인을 할 수 있다.
+
+### Leader Vote
+파트장 투표에 관한 테이블
+
+
+
+- Leader Candidate에는 후보자의 이름과 파트 그리고 후보자 id가 있다.
+- Leader Vote는 투표한 사용자의 id와 사용자가 투표한 후보자 id 그리고 투표 고유 id가 있다.
+- 사용자 id와 후보자 id는 외래키로 받아왔다.
+
+### Team Vote
+팀 투표에 관한 테이블
+
+
+
+- Team Candidate는 팀명과 팀 고유 id로 구성되어 있다.
+- Team Vote는 투표한 사용자 id, 사용자가 투표한 팀 id 그리고 투표 고유 id가 있다.
+- 사용자 id와 사용자 id는 외래키로 받아왔다.
+
+## 배포 - blue green 무중단 배포
+### blue green 배포란?
+https://github.com/user-attachments/assets/d603aaa1-c8fc-4ef2-829a-629b2a35b09e
+
+```yaml
+spring:
+ profiles:
+ active: local
+ group:
+ local: local, common, secret
+ blue: blue, common, secret
+ green: green, common, secret
+
+---
+
+spring:
+ config:
+ activate:
+ on-profile: blue
+
+server:
+ port: 8080
+ serverAddress: 13.124.171.94
+
+serverName: blue_server
+
+---
+
+spring:
+ config:
+ activate:
+ on-profile: common
+ import: optional:file:.env[.properties]
+```
+application.yml
+
+
+### nginx load balancing
+- blue server - 8080 port
+- green server - 8081 port
+
+http://52.79.122.106 → nginx로 8080 or 8081 port 중 실행 중인 port로 연결
+
+
+### github action
+```yaml
+
+
+# event trigger
+# main브랜치에 pull request가 발생 되었을 때 실행
+on:
+ pull_request:
+ branches: [ "master" ]
+ push:
+ branches: [ "master" ]
+
+jobs:
+ build:
+ steps:
+ - name: Checkout source
+
+ - name: Setup Java
+
+ - name: Gradle Caching
+
+ - name: Grant execute permission for gradlew
+
+ - name: Build Project with Gradle
+
+ - name: Login to DockerHub
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build docker image
+ run: docker build --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/vote .
+
+ - name: Publish image to docker hub
+ run: docker push ${{ secrets.DOCKER_USERNAME }}/vote:latest
+
+ deploy:
+ steps:
+ - name: Set Target IP
+ run: |
+ STATUS=$(curl -o /dev/null -w "%{http_code}" "${{ secrets.SERVER_IP }}/env")
+ echo $STATUS
+ if [ $STATUS = 200 ]; then
+ CURRENT_UPSTREAM=$(curl -s "${{ secrets.SERVER_IP }}/env")
+ else
+ CURRENT_UPSTREAM=green
+ fi
+ echo CURRENT_UPSTREAM=$CURRENT_UPSTREAM >> $GITHUB_ENV
+ if [ $CURRENT_UPSTREAM = blue ]; then
+ echo "CURRENT_PORT=8080" >> $GITHUB_ENV
+ echo "STOPPED_PORT=8081" >> $GITHUB_ENV
+ echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
+ else
+ echo "CURRENT_PORT=8081" >> $GITHUB_ENV
+ echo "STOPPED_PORT=8080" >> $GITHUB_ENV
+ echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
+ fi
+
+ - name: Docker Compose
+ with:
+ script: |
+ export JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}
+ sudo docker pull ${{ secrets.DOCKER_USERNAME }}/vote:latest
+ sudo docker-compose -f docker-compose-${{env.TARGET_UPSTREAM}}.yml up -d
+
+ - name: Check deploy server URL
+ with:
+ url: http://${{ secrets.SERVER_IP }}:${{ env.STOPPED_PORT }}/env
+
+ - name: Change nginx upstream
+ with:
+ script: |
+ sudo docker exec -i nginxserver bash -c 'echo "set \$service_url ${{env.TARGET_UPSTREAM}};" > etc/nginx/conf.d/service-env.inc && nginx -s reload'
+
+ - name: Stop current server
+ with:
+ script: |
+ sudo docker stop ${{ env.CURRENT_UPSTREAM }}
+ sudo docker rm ${{ env.CURRENT_UPSTREAM }}
+
+```
+
+### trouble shooting
+**JWT_SECRET_KEY를 읽지 못해서 발생하는 빌드 에러**
+
+1. git secrets에 JWT_SECRET_KEY 변수를 추가 → docker-compose 파일을 실행할 때 넣어주려고 했으나 인식하지 못하는 문제가 발생함.
+2. CICD.yml 파일 내에서 $GITHUB_ENV에 JWT_SECRET_KEY값을 저장 → 키가 외부에 노출되면 안되기 때문에 보안 문제 발생
+3. docker-compse.yml 파일에서 직접 추가 → 성공
+ ```yaml
+ environment:
+ - JWT_SECRET_KEY=
+ ```
+
+
+**보안 규칙**
+1. 8080 port, 8081 port를 번갈아가며 사용하기 때문에 두 port에 대한 inbound 규칙 추가
+
+**비밀번호를 인식하지 못하는 문제**
+1. 로그인 기능을 먼저 구현한 뒤, 테이블에 직접 데이터를 넣고 로그인 테스트를 해보았으나 비밀번호를 인식하지 못하여 실패
+2. 비밀번호가 암호화되어 입력되어야 하는데 직접 데이터를 넣었기 때문에 암호화되지 않은 채로 들어가 문제가 생겼음을 인식
+3. 회원가입 구현 후에 다시 로그인을 시도했으나 여전히 비밀번호를 인식하지 못함
+4. import문을 잘못 가지고 왔다는 것을 깨달음... → 해결..^_^
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..4a9c3b0
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,74 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '3.4.0'
+ id 'io.spring.dependency-management' version '1.1.6'
+}
+
+group = 'com.ceos.vote'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.projectlombok:lombok:1.18.24'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.jetbrains:annotations:24.0.0'
+ annotationProcessor('org.projectlombok:lombok')
+ runtimeOnly 'com.mysql:mysql-connector-j'
+
+ 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'
+
+ // security
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+
+ // env
+ implementation 'io.github.cdimascio:dotenv-java:3.0.0'
+
+
+ // spring security
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
+ testImplementation 'org.springframework.security:spring-security-test'
+
+ //JWT
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
+ implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
+
+ //mysql
+ implementation 'com.mysql:mysql-connector-j'
+
+}
+
+tasks.named('compileJava') {
+ doLast {
+ def generated = layout.buildDirectory.dir("generated/source/apt/main").get().asFile
+ sourceSets.main.java.srcDirs += generated
+ }
+}
+
+sourceSets {
+ main {
+ java {
+ srcDirs += layout.buildDirectory.dir("generated/source/apt/main").get().asFile
+ }
+ }
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..a4b76b9
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..e2847c8
--- /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.11.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..f5feea6
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,252 @@
+#!/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
+' "$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=$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=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, 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" \
+ 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..9d21a21
--- /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=%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/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e906f4a
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'ceos-vote-BE'
diff --git a/src/main/java/com/ceos/vote/CeosVoteBeApplication.java b/src/main/java/com/ceos/vote/CeosVoteBeApplication.java
new file mode 100644
index 0000000..37428c6
--- /dev/null
+++ b/src/main/java/com/ceos/vote/CeosVoteBeApplication.java
@@ -0,0 +1,17 @@
+package com.ceos.vote;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication
+@EnableJpaAuditing
+@EnableScheduling
+public class CeosVoteBeApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(CeosVoteBeApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/JwtAuthenticationFilter.java b/src/main/java/com/ceos/vote/domain/auth/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..c9d3321
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/JwtAuthenticationFilter.java
@@ -0,0 +1,55 @@
+package com.ceos.vote.domain.auth;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+@Slf4j
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends GenericFilterBean {
+ private final JwtTokenProvider jwtTokenProvider;
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+
+ // 1. Request Header에서 JWT 토큰 추출
+ String token = resolveToken((HttpServletRequest) servletRequest);
+
+ if (token != null && jwtTokenProvider.validateToken(token)) {
+ Authentication authentication = jwtTokenProvider.getAuthentication(token);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ log.info("User authenticated: {}", authentication.getName());
+ log.info("Authorities: {}", authentication.getAuthorities());
+ } else {
+ log.warn("Invalid or missing JWT token.");
+ }
+
+ filterChain.doFilter(servletRequest, servletResponse);
+ }
+
+ // Request Header에서 토큰 정보 추출
+ private String resolveToken(HttpServletRequest request) {
+ String bearerToken = request.getHeader("Authorization");
+ if (bearerToken == null) {
+ log.warn("Authorization header is completely missing in the request.");
+ } else if (!bearerToken.startsWith("Bearer ")) {
+ log.warn("Authorization header is malformed. Value: {}", bearerToken);
+ } else {
+ log.info("Authorization header found and valid.");
+ }
+ return StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")
+ ? bearerToken.substring(7)
+ : null;
+ }
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/JwtToken.java b/src/main/java/com/ceos/vote/domain/auth/JwtToken.java
new file mode 100644
index 0000000..911a2f8
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/JwtToken.java
@@ -0,0 +1,14 @@
+package com.ceos.vote.domain.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+
+@Builder
+@Data
+@AllArgsConstructor
+public class JwtToken {
+ private String grantType;
+ private String accessToken;
+ private String refreshToken;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/auth/JwtTokenProvider.java b/src/main/java/com/ceos/vote/domain/auth/JwtTokenProvider.java
new file mode 100644
index 0000000..4339994
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/JwtTokenProvider.java
@@ -0,0 +1,129 @@
+package com.ceos.vote.domain.auth;
+
+import com.ceos.vote.domain.users.entity.Users;
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.io.Decoders;
+import io.jsonwebtoken.security.Keys;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+import javax.swing.*;
+import java.security.Key;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Component
+public class JwtTokenProvider {
+
+ private final Key key;
+
+ //application-secret.yml에서 secret 값 가져와서 key에 저장
+ public JwtTokenProvider(@Value("${JWT_SECRET_KEY}") String secretKey){
+ log.info("Loaded secret key: {}", secretKey);
+ byte[] keyBytes = Decoders.BASE64.decode(secretKey);
+ this.key = Keys.hmacShaKeyFor(keyBytes);
+ }
+
+ // 인증(authentication) 객체를 기반으로 AccessToken, RefreshToken을 생성하는 메서드
+ public JwtToken generateToken(Authentication authentication){
+
+ //권한 가져오기
+ @NotNull String authorities = authentication.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.joining(","));
+
+ long now = (new Date()).getTime();
+
+ Date accessTokenExpiresIn = new Date(now + 1000L * 60 * 60);
+ String accessToken = Jwts.builder()
+ .setSubject(authentication.getName())
+ .claim("auth", authorities)
+ .setExpiration(accessTokenExpiresIn)
+ .signWith(key, SignatureAlgorithm.HS256)
+ .compact();
+
+ //Refresh Token 생성
+ String refreshToken = Jwts.builder()
+ .setExpiration(new Date(now + 1000L * 60 * 60 * 24))
+ .signWith(key, SignatureAlgorithm.HS256)
+ .compact();
+
+ return JwtToken.builder()
+ .grantType("Bearer")
+ .accessToken(accessToken)
+ .refreshToken(refreshToken)
+ .build();
+ }
+
+ // Jwt 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드
+ public Authentication getAuthentication(String accessToken) {
+
+ // Jwt 토큰 복호화
+ Claims claims = parseClaims(accessToken);
+
+ if (claims.get("auth") == null) {
+ log.error("No authority information found in the token.");
+ throw new RuntimeException("권한 정보가 없는 토큰입니다.");
+ }
+
+ // 클레임에서 권한 정보 가져오기
+ Collection extends GrantedAuthority> authorities = Arrays.stream(claims.get("auth").toString().split(","))
+ .map(SimpleGrantedAuthority::new)
+ .collect(Collectors.toList());
+
+ // UserDetails 객체를 만들어서 Authentication return
+ // UserDetails: interface, User: UserDetails를 구현한 class
+ UserDetails principal = new User(claims.getSubject(), "", authorities);
+
+ return new UsernamePasswordAuthenticationToken(principal, "", authorities);
+ }
+
+ public boolean validateToken(String token) {
+ try {
+ Jwts.parserBuilder()
+ .setSigningKey(key)
+ .build()
+ .parseClaimsJws(token);
+ return true;
+ } catch (SecurityException | MalformedJwtException e) {
+ log.error("Invalid JWT signature or malformed token: {}", e.getMessage());
+ } catch (ExpiredJwtException e) {
+ log.error("JWT token expired: {}", e.getMessage());
+ } catch (UnsupportedJwtException e) {
+ log.error("Unsupported JWT token: {}", e.getMessage());
+ } catch (IllegalArgumentException e) {
+ log.error("JWT claims string is empty or invalid: {}", e.getMessage());
+ }
+ return false;
+ }
+
+ //accessToken
+ private Claims parseClaims(String accessToken) {
+ try {
+ return Jwts.parserBuilder()
+ .setSigningKey(key)
+ .build()
+ .parseClaimsJws(accessToken)
+ .getBody();
+ } catch (ExpiredJwtException e) {
+ log.error("Token expired: {}", e.getMessage());
+ throw new RuntimeException("Token expired");
+ } catch (Exception e) {
+ log.error("Failed to parse token: {}", e.getMessage());
+ throw new RuntimeException("Failed to parse token");
+ }
+ }
+
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/controller/AuthController.java b/src/main/java/com/ceos/vote/domain/auth/controller/AuthController.java
new file mode 100644
index 0000000..203a341
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/controller/AuthController.java
@@ -0,0 +1,69 @@
+package com.ceos.vote.domain.auth.controller;
+
+import com.ceos.vote.domain.auth.JwtToken;
+import com.ceos.vote.domain.auth.dto.request.SignInRequestDto;
+import com.ceos.vote.domain.auth.dto.request.SignUpRequestDto;
+import com.ceos.vote.domain.auth.dto.response.SignInResponseDto;
+import com.ceos.vote.domain.auth.dto.response.UserInfoDto;
+import com.ceos.vote.domain.auth.service.UserDetailService;
+import com.ceos.vote.domain.auth.service.UserService;
+import com.ceos.vote.domain.leaderVote.service.LeaderVoteService;
+import com.ceos.vote.domain.teamVote.service.TeamVoteService;
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.domain.users.enumerate.Part;
+import com.ceos.vote.domain.utils.SecurityUtil;
+import com.ceos.vote.global.common.response.CommonResponse;
+import com.ceos.vote.global.exception.ApplicationException;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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;
+
+
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+ private final UserService userService;
+ private final LeaderVoteService leaderVoteService;
+ private final TeamVoteService teamVoteService;
+ private final UserDetailService userDetailService;
+
+ // 회원가입
+ @PostMapping("/sign-up")
+ public CommonResponse signUp(@Valid @RequestBody SignUpRequestDto signUpRequestDto) {
+ log.debug("Request body: ", signUpRequestDto);
+ UserInfoDto userInfoDto = userService.signUp(signUpRequestDto);
+ return new CommonResponse<>(userInfoDto, "회원가입에 성공했습니다.");
+ }
+
+ @PostMapping("/sign-in")
+ public CommonResponse signIn(@RequestBody SignInRequestDto signinRequestDto){
+
+ String username = signinRequestDto.getUsername();
+ String password = signinRequestDto.getPassword();
+ JwtToken jwtToken = userService.signIn(username, password);
+
+ Long userId = userService.findUserIdByUsername(username);
+ Boolean isVotingLeader = leaderVoteService.checkLeaderVoteByUserId(userId);
+ Boolean isVotingTeam = teamVoteService.checkTeamVoteByUserId(userId);
+
+ Users user = userDetailService.findUserByUsername(username);
+
+ SignInResponseDto responseDto = SignInResponseDto.from(jwtToken, user, isVotingLeader, isVotingTeam);
+ return new CommonResponse<>(responseDto, "로그인에 성공헀습니다.");
+ }
+
+ @PostMapping("/test")
+ public CommonResponse test(){
+
+ String currentUsername = SecurityUtil.getCurrentUsername();
+
+ return new CommonResponse<>(currentUsername, "현재 사용자 정보를 반환합니다.");
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/dto/request/SignInRequestDto.java b/src/main/java/com/ceos/vote/domain/auth/dto/request/SignInRequestDto.java
new file mode 100644
index 0000000..ed998dc
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/dto/request/SignInRequestDto.java
@@ -0,0 +1,15 @@
+package com.ceos.vote.domain.auth.dto.request;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+@NoArgsConstructor
+public class SignInRequestDto {
+ private String username;
+ private String password;
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/dto/request/SignUpRequestDto.java b/src/main/java/com/ceos/vote/domain/auth/dto/request/SignUpRequestDto.java
new file mode 100644
index 0000000..d7e2d6b
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/dto/request/SignUpRequestDto.java
@@ -0,0 +1,39 @@
+package com.ceos.vote.domain.auth.dto.request;
+
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.domain.users.enumerate.Part;
+import com.ceos.vote.domain.users.enumerate.Team;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SignUpRequestDto {
+
+ private String username;
+ private String email;
+ private String password;
+
+ private Team userTeam;
+ private Part userPart;
+
+ public Users toEntity(String encodedPassword){
+ return Users.builder()
+ .username(username)
+ .email(email)
+ .password(encodedPassword)
+ .userTeam(userTeam)
+ .userPart(userPart)
+ .build();
+ }
+
+ public String getPassword() {
+ System.out.println("Getter called for password: " + password);
+ return password;
+ }
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/dto/response/SignInResponseDto.java b/src/main/java/com/ceos/vote/domain/auth/dto/response/SignInResponseDto.java
new file mode 100644
index 0000000..8b2153a
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/dto/response/SignInResponseDto.java
@@ -0,0 +1,35 @@
+package com.ceos.vote.domain.auth.dto.response;
+
+
+import com.ceos.vote.domain.auth.JwtToken;
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.domain.users.enumerate.Part;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+
+@Builder
+@Data
+@AllArgsConstructor
+public class SignInResponseDto {
+
+ private String grantType;
+ private String accessToken;
+ private String refreshToken;
+ private Long userId;
+ private Part userPart;
+ private Boolean isVotingLeader;
+ private Boolean isVotingTeam;
+
+ public static SignInResponseDto from(JwtToken jwtToken, Users user, Boolean isVotingLeader, Boolean isVotingTeam) {
+ return SignInResponseDto.builder()
+ .grantType(jwtToken.getGrantType())
+ .accessToken(jwtToken.getAccessToken())
+ .refreshToken(jwtToken.getRefreshToken())
+ .userId(user.getId())
+ .userPart(user.getUserPart())
+ .isVotingLeader(isVotingLeader)
+ .isVotingTeam(isVotingTeam)
+ .build();
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/dto/response/UserInfoDto.java b/src/main/java/com/ceos/vote/domain/auth/dto/response/UserInfoDto.java
new file mode 100644
index 0000000..1b3519c
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/dto/response/UserInfoDto.java
@@ -0,0 +1,33 @@
+package com.ceos.vote.domain.auth.dto.response;
+
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.domain.users.enumerate.Part;
+import com.ceos.vote.domain.users.enumerate.Team;
+import lombok.*;
+
+@Getter
+@ToString
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class UserInfoDto {
+
+ private Long id;
+ private String username;
+ private String email;
+ private String password;
+ private Team userTeam;
+ private Part userPart;
+
+ static public UserInfoDto from(Users users) {
+ return UserInfoDto.builder()
+ .id(users.getId())
+ .password(users.getPassword())
+ .username(users.getUsername())
+ .email(users.getEmail())
+ .userTeam(users.getUserTeam())
+ .userPart(users.getUserPart())
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/service/UserDetailService.java b/src/main/java/com/ceos/vote/domain/auth/service/UserDetailService.java
new file mode 100644
index 0000000..1beda79
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/service/UserDetailService.java
@@ -0,0 +1,48 @@
+package com.ceos.vote.domain.auth.service;
+
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.domain.users.repository.UserRepository;
+import com.ceos.vote.global.exception.ApplicationException;
+import com.ceos.vote.global.exception.ExceptionCode;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+public class UserDetailService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ return userRepository.findByUsername(username)
+ .map(this::createUserDetails)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_USER));
+ }
+
+ public Users findUserById(Long id) {
+ return userRepository.findById(id)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_USER));
+ }
+
+ public Users findUserByUsername(String username) {
+ return userRepository.findByUsername(username)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_USER));
+ }
+
+ private UserDetails createUserDetails(@NotNull Users users) {
+ return Users.builder()
+ .username(users.getUsername())
+ .password(users.getPassword())
+ .build();
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/auth/service/UserService.java b/src/main/java/com/ceos/vote/domain/auth/service/UserService.java
new file mode 100644
index 0000000..2bd4ad5
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/auth/service/UserService.java
@@ -0,0 +1,110 @@
+package com.ceos.vote.domain.auth.service;
+
+import com.ceos.vote.domain.auth.JwtToken;
+import com.ceos.vote.domain.auth.JwtTokenProvider;
+import com.ceos.vote.domain.auth.dto.request.SignUpRequestDto;
+import com.ceos.vote.domain.auth.dto.response.UserInfoDto;
+import com.ceos.vote.domain.users.dto.response.UserResponseDto;
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.domain.users.enumerate.Part;
+import com.ceos.vote.domain.users.repository.UserRepository;
+import com.ceos.vote.global.exception.ApplicationException;
+import com.ceos.vote.global.exception.ExceptionCode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@RequiredArgsConstructor
+@Service
+@Transactional(readOnly = true)
+@Slf4j
+public class UserService {
+
+ private final UserRepository userRepository;
+ private final AuthenticationManagerBuilder authenticationManagerBuilder;
+ private final JwtTokenProvider jwtTokenProvider;
+ private final PasswordEncoder passwordEncoder;
+
+ public UserResponseDto getUserInfo(String username){
+ Users users = userRepository.findByUsername(username).orElse(null);
+
+ return new UserResponseDto(
+ users.getUserPart().name(),
+ users.getUserTeam().name(),
+ users.getUsername()
+ );
+ }
+
+ public Long findUserIdByUsername(String username) {
+ return userRepository.findByUsername(username)
+ .map(Users::getId)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_USER));
+ }
+
+ @Transactional
+ //@Override
+ public UserInfoDto signUp(SignUpRequestDto signUpRequestDto){
+
+ // 사용자 이름 중복 확인
+ if(userRepository.existsByUsername(signUpRequestDto.getUsername())){
+ throw new IllegalArgumentException("이미 사용 중인 사용자 이름입니다.");
+ }
+
+ // 사용자 이메일 중복 확인
+ if(userRepository.existsByEmail(signUpRequestDto.getEmail())){
+ throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
+ }
+
+ log.debug("PASSWORD{}", signUpRequestDto.getPassword());
+
+ //비밀번호 유효성 검사
+ if (signUpRequestDto.getPassword() == null || signUpRequestDto.getPassword().isEmpty()) {
+ throw new ApplicationException(ExceptionCode.INVALID_PASSWORD);
+ }
+
+ //password 암호화
+ String encodedPassword = passwordEncoder.encode(signUpRequestDto.getPassword());
+ log.debug("encodedPassword{}", encodedPassword);
+
+ //사용자 저장
+ Users newUser = userRepository.save(signUpRequestDto.toEntity(encodedPassword));
+ return UserInfoDto.from(newUser);
+ }
+
+ @Transactional
+ public JwtToken signIn(String username, String password){
+
+ // username 존재여부 확인
+ Users users = userRepository.findByUsername(username)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_USER));
+
+ // password 일치 여부 확인
+ if (!passwordEncoder.matches(password, users.getPassword())) {
+ throw new ApplicationException(ExceptionCode.INVALID_PASSWORD);
+ }
+
+ // username + password 를 기반으로 Authentication 객체 생성
+ // 이때 authentication 은 인증 여부를 확인하는 authenticated 값이 false
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
+
+ // 실제 검증. authenticate() 메서드를 통해 요청된 Member 에 대한 검증 진행
+ // authenticate 메서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드 실행
+ Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
+
+ // 인증 정보를 기반으로 JWT 토큰 생성
+ JwtToken jwtToken = jwtTokenProvider.generateToken(authentication);
+
+ return jwtToken;
+ }
+
+ public Part findUserPartByUsername(String username) {
+ return userRepository.findByUsername(username)
+ .map(Users::getUserPart) // User 엔티티에서 Part 필드 가져오기
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_USER));
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderCandidate/entity/LeaderCandidate.java b/src/main/java/com/ceos/vote/domain/leaderCandidate/entity/LeaderCandidate.java
new file mode 100644
index 0000000..0ee9e00
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderCandidate/entity/LeaderCandidate.java
@@ -0,0 +1,34 @@
+package com.ceos.vote.domain.leaderCandidate.entity;
+
+import com.ceos.vote.domain.users.enumerate.Part;
+import com.ceos.vote.global.BaseEntity;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class LeaderCandidate extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "leader_candidate_id")
+ private Long id;
+
+ @Enumerated(EnumType.STRING)
+ @Column(length = 32)
+ private Part part;
+
+ @Column
+ private String name;
+
+ @Builder
+ public LeaderCandidate(Part part, String name) {
+ this.part = part;
+ this.name = name;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/leaderCandidate/repository/LeaderCandidateRepository.java b/src/main/java/com/ceos/vote/domain/leaderCandidate/repository/LeaderCandidateRepository.java
new file mode 100644
index 0000000..de67b3a
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderCandidate/repository/LeaderCandidateRepository.java
@@ -0,0 +1,13 @@
+package com.ceos.vote.domain.leaderCandidate.repository;
+
+import com.ceos.vote.domain.leaderCandidate.entity.LeaderCandidate;
+import com.ceos.vote.domain.users.enumerate.Part;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface LeaderCandidateRepository extends JpaRepository {
+ List findByPart(Part part);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/leaderCandidate/service/LeaderCandidateService.java b/src/main/java/com/ceos/vote/domain/leaderCandidate/service/LeaderCandidateService.java
new file mode 100644
index 0000000..5cfe72d
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderCandidate/service/LeaderCandidateService.java
@@ -0,0 +1,23 @@
+package com.ceos.vote.domain.leaderCandidate.service;
+
+import com.ceos.vote.domain.leaderCandidate.entity.LeaderCandidate;
+import com.ceos.vote.domain.leaderCandidate.repository.LeaderCandidateRepository;
+import com.ceos.vote.global.exception.ApplicationException;
+import com.ceos.vote.global.exception.ExceptionCode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class LeaderCandidateService {
+
+ private final LeaderCandidateRepository leaderCandidateRepository;
+
+ public LeaderCandidate findLeaderCandidateById(Long id) {
+ return leaderCandidateRepository.findById(id)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_LEADER_CANDIDATE));
+ }
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/controller/LeaderVoteController.java b/src/main/java/com/ceos/vote/domain/leaderVote/controller/LeaderVoteController.java
new file mode 100644
index 0000000..13f7475
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/controller/LeaderVoteController.java
@@ -0,0 +1,59 @@
+package com.ceos.vote.domain.leaderVote.controller;
+
+import com.ceos.vote.domain.leaderVote.dto.request.LeaderVoteCreateRequestDto;
+import com.ceos.vote.domain.leaderVote.dto.request.LeaderVoteUpdateRequestDto;
+import com.ceos.vote.domain.leaderVote.dto.response.LeaderVoteByUserResponseDto;
+import com.ceos.vote.domain.leaderVote.dto.response.LeaderVoteFinalResultResponseDto;
+import com.ceos.vote.domain.leaderVote.dto.response.PartCandidateResponseDto;
+import com.ceos.vote.domain.leaderVote.service.LeaderVoteService;
+import com.ceos.vote.global.common.response.CommonResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "Leader Vote", description = "leader vote api")
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("api/v1/leader")
+public class LeaderVoteController {
+ private final LeaderVoteService leaderVoteService;
+
+ @Operation(summary = "leader candidate 전체 조회")
+ @GetMapping("/candidate")
+ public CommonResponse> getLeaderCandidates(){
+ final List leaderCandidates = leaderVoteService.findLeaderCandidates();
+ return new CommonResponse<>(leaderCandidates, "전체 파트장 후보 조회를 성공하였습니다.");
+ }
+
+ @Operation(summary = "leader vote 생성")
+ @PostMapping
+ public CommonResponse createLeaderVote(@RequestBody LeaderVoteCreateRequestDto requestDto){
+ leaderVoteService.createLeaderVote(requestDto);
+ return new CommonResponse<>("new leader vote를 생성하였습니다.");
+ }
+
+ @Operation(summary = "leader vote 전체 결과 조회")
+ @GetMapping
+ public CommonResponse> getLeaderVoteResult(){
+ final List results = leaderVoteService.getLeaderVoteFinalResult();
+ return new CommonResponse<>(results, "전체 투표 결과 조회를 성공하였습니다.");
+ }
+
+ @Operation(summary = "user의 leader vote 결과 조회")
+ @GetMapping("/{userId}")
+ public CommonResponse getLeaderVoteByUser(@PathVariable Long userId){
+ final LeaderVoteByUserResponseDto leaderVoteByUser = leaderVoteService.getLeaderVoteByUser(userId);
+ return new CommonResponse<>(leaderVoteByUser, "해당 user의 투표 결과 조회에 성공하였습니다.");
+ }
+
+ @Operation(summary = "user의 leader vote 수정")
+ @PutMapping("/{userId}")
+ public CommonResponse updateLeaderVote(@PathVariable Long userId, @RequestBody LeaderVoteUpdateRequestDto requestDto){
+ leaderVoteService.updateLeaderVoteByUser(userId, requestDto);
+ return new CommonResponse<>("leader vote 수정을 성공하였습니다.");
+ }
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/dto/request/LeaderVoteCreateRequestDto.java b/src/main/java/com/ceos/vote/domain/leaderVote/dto/request/LeaderVoteCreateRequestDto.java
new file mode 100644
index 0000000..956bdeb
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/dto/request/LeaderVoteCreateRequestDto.java
@@ -0,0 +1,19 @@
+package com.ceos.vote.domain.leaderVote.dto.request;
+
+import com.ceos.vote.domain.leaderCandidate.entity.LeaderCandidate;
+import com.ceos.vote.domain.leaderVote.entity.LeaderVote;
+import com.ceos.vote.domain.users.entity.Users;
+import jakarta.validation.constraints.NotNull;
+
+public record LeaderVoteCreateRequestDto (
+ Long user_id,
+ @NotNull
+ Long leader_candidate_id
+){
+ public LeaderVote toEntity(Users user, LeaderCandidate leaderCandidate) {
+ return LeaderVote.builder()
+ .user(user)
+ .leaderCandidate(leaderCandidate)
+ .build();
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/dto/request/LeaderVoteUpdateRequestDto.java b/src/main/java/com/ceos/vote/domain/leaderVote/dto/request/LeaderVoteUpdateRequestDto.java
new file mode 100644
index 0000000..5009f5e
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/dto/request/LeaderVoteUpdateRequestDto.java
@@ -0,0 +1,10 @@
+package com.ceos.vote.domain.leaderVote.dto.request;
+
+import com.ceos.vote.domain.leaderCandidate.entity.LeaderCandidate;
+import jakarta.validation.constraints.NotNull;
+
+public record LeaderVoteUpdateRequestDto(
+ @NotNull
+ Long leader_candidate_id
+){
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderCandidateResponseDto.java b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderCandidateResponseDto.java
new file mode 100644
index 0000000..b470d2e
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderCandidateResponseDto.java
@@ -0,0 +1,15 @@
+package com.ceos.vote.domain.leaderVote.dto.response;
+
+import com.ceos.vote.domain.leaderCandidate.entity.LeaderCandidate;
+
+public record LeaderCandidateResponseDto (
+ Long id,
+ String name
+){
+ public static LeaderCandidateResponseDto from(LeaderCandidate leaderCandidate) {
+ return new LeaderCandidateResponseDto(
+ leaderCandidate.getId(),
+ leaderCandidate.getName()
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderResultResponseDto.java b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderResultResponseDto.java
new file mode 100644
index 0000000..6ab11d7
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderResultResponseDto.java
@@ -0,0 +1,17 @@
+package com.ceos.vote.domain.leaderVote.dto.response;
+
+public record LeaderResultResponseDto (
+ String leader_name,
+ Long count
+){
+ public Long getVoteCount() {
+ return count;
+ }
+
+ public static LeaderResultResponseDto from(String leader_name, Long count){
+ return new LeaderResultResponseDto(
+ leader_name,
+ count
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderVoteByUserResponseDto.java b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderVoteByUserResponseDto.java
new file mode 100644
index 0000000..59a1db4
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderVoteByUserResponseDto.java
@@ -0,0 +1,17 @@
+package com.ceos.vote.domain.leaderVote.dto.response;
+
+import com.ceos.vote.domain.leaderVote.entity.LeaderVote;
+import jakarta.validation.constraints.NotNull;
+
+public record LeaderVoteByUserResponseDto (
+ @NotNull
+ Boolean is_voting,
+ Long leader_Candidate_id
+){
+ public static LeaderVoteByUserResponseDto from(LeaderVote leaderVote, Boolean is_voting){
+ return new LeaderVoteByUserResponseDto(
+ is_voting,
+ leaderVote != null ? leaderVote.getLeaderCandidate().getId() : null
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderVoteFinalResultResponseDto.java b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderVoteFinalResultResponseDto.java
new file mode 100644
index 0000000..ecc3977
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/LeaderVoteFinalResultResponseDto.java
@@ -0,0 +1,19 @@
+package com.ceos.vote.domain.leaderVote.dto.response;
+
+import com.ceos.vote.domain.users.enumerate.Part;
+
+import java.util.List;
+
+public record LeaderVoteFinalResultResponseDto (
+ String part,
+ Long total_count,
+ List results
+){
+ public static LeaderVoteFinalResultResponseDto from(Part part, Long total_count, List results){
+ return new LeaderVoteFinalResultResponseDto(
+ part.getDescription(),
+ total_count,
+ results
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/PartCandidateResponseDto.java b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/PartCandidateResponseDto.java
new file mode 100644
index 0000000..a148aae
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/dto/response/PartCandidateResponseDto.java
@@ -0,0 +1,17 @@
+package com.ceos.vote.domain.leaderVote.dto.response;
+
+import com.ceos.vote.domain.users.enumerate.Part;
+
+import java.util.List;
+
+public record PartCandidateResponseDto (
+ String part,
+ List leaderCandidates
+){
+ public static PartCandidateResponseDto from(Part part, List leaderCandidates) {
+ return new PartCandidateResponseDto(
+ part.getDescription(),
+ leaderCandidates
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/entity/LeaderVote.java b/src/main/java/com/ceos/vote/domain/leaderVote/entity/LeaderVote.java
new file mode 100644
index 0000000..fee5ff8
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/entity/LeaderVote.java
@@ -0,0 +1,42 @@
+package com.ceos.vote.domain.leaderVote.entity;
+
+import com.ceos.vote.domain.leaderCandidate.entity.LeaderCandidate;
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.global.BaseEntity;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@EntityListeners(AuditingEntityListener.class)
+public class LeaderVote extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "leader_vote_id")
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id")
+ private Users user;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "leader_candidate_id")
+ private LeaderCandidate leaderCandidate;
+
+ public void setLeaderCandidate(LeaderCandidate leaderCandidate) {
+ this.leaderCandidate = leaderCandidate;
+ }
+
+ @Builder
+ public LeaderVote(Users user, LeaderCandidate leaderCandidate) {
+ this.user = user;
+ this.leaderCandidate = leaderCandidate;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/repository/LeaderVoteRepository.java b/src/main/java/com/ceos/vote/domain/leaderVote/repository/LeaderVoteRepository.java
new file mode 100644
index 0000000..70beb6a
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/repository/LeaderVoteRepository.java
@@ -0,0 +1,17 @@
+package com.ceos.vote.domain.leaderVote.repository;
+
+import com.ceos.vote.domain.leaderCandidate.entity.LeaderCandidate;
+import com.ceos.vote.domain.leaderVote.entity.LeaderVote;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface LeaderVoteRepository extends JpaRepository {
+ Optional findByUserId(Long userId);
+
+ Long countByLeaderCandidate(LeaderCandidate leaderCandidate);
+
+ boolean existsByUserId(Long userId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/leaderVote/service/LeaderVoteService.java b/src/main/java/com/ceos/vote/domain/leaderVote/service/LeaderVoteService.java
new file mode 100644
index 0000000..f32ab6b
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/leaderVote/service/LeaderVoteService.java
@@ -0,0 +1,141 @@
+package com.ceos.vote.domain.leaderVote.service;
+
+import com.ceos.vote.domain.auth.service.UserDetailService;
+import com.ceos.vote.domain.leaderCandidate.entity.LeaderCandidate;
+import com.ceos.vote.domain.leaderCandidate.repository.LeaderCandidateRepository;
+import com.ceos.vote.domain.leaderCandidate.service.LeaderCandidateService;
+import com.ceos.vote.domain.leaderVote.dto.request.LeaderVoteCreateRequestDto;
+import com.ceos.vote.domain.leaderVote.dto.request.LeaderVoteUpdateRequestDto;
+import com.ceos.vote.domain.leaderVote.dto.response.*;
+import com.ceos.vote.domain.leaderVote.entity.LeaderVote;
+import com.ceos.vote.domain.leaderVote.repository.LeaderVoteRepository;
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.domain.users.enumerate.Part;
+import com.ceos.vote.domain.users.repository.UserRepository;
+import com.ceos.vote.global.exception.ApplicationException;
+import com.ceos.vote.global.exception.ExceptionCode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class LeaderVoteService {
+ private final UserRepository userRepository;
+ private final LeaderCandidateRepository leaderCandidateRepository;
+ private final LeaderCandidateService leaderCandidateService;
+ private final UserDetailService userDetailService;
+ private final LeaderVoteRepository leaderVoteRepository;
+
+ public boolean checkLeaderVoteByUserId(Long id) {
+ return leaderVoteRepository.existsByUserId(id);
+ }
+
+ public LeaderVote findLeaderVoteByUserId(Long id) {
+ return leaderVoteRepository.findByUserId(id)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_LEADER_VOTE));
+ }
+
+ /*
+ 파트별 leader candidate 조회
+ */
+ public List findLeaderCandidatesByPart(Part part) {
+ List leaderCandidates = leaderCandidateRepository.findByPart(part);
+
+ return leaderCandidates.stream()
+ .map(LeaderCandidateResponseDto::from)
+ .toList();
+ }
+
+ /*
+ 전체 leader candidate 조회
+ */
+ public List findLeaderCandidates() {
+ // part별 leader candidate을 조회하여 PartCandidateResponseDto로 반환
+ return Arrays.stream(Part.values())
+ .map(part -> PartCandidateResponseDto.from(part, findLeaderCandidatesByPart(part)))
+ .collect(Collectors.toList());
+ }
+
+ /*
+ leaderVote 생성
+ */
+ @Transactional
+ public void createLeaderVote(LeaderVoteCreateRequestDto requestDto) {
+ Users user = userDetailService.findUserById(requestDto.user_id());
+ LeaderCandidate leaderCandidate = leaderCandidateService.findLeaderCandidateById(requestDto.leader_candidate_id());
+
+ final LeaderVote leaderVote = requestDto.toEntity(user, leaderCandidate);
+ leaderVoteRepository.save(leaderVote);
+ }
+
+ /*
+ user의 leaderVote 결과 조회
+ */
+ public LeaderVoteByUserResponseDto getLeaderVoteByUser(final Long userId) {
+ try {
+ final LeaderVote leaderVote = findLeaderVoteByUserId(userId);
+ return LeaderVoteByUserResponseDto.from(leaderVote, true);
+ } catch (Exception e) {
+ return LeaderVoteByUserResponseDto.from(null, false);
+ }
+ }
+
+ /*
+ user의 leaderVote 수정
+ */
+ @Transactional
+ public void updateLeaderVoteByUser(final Long userId, final LeaderVoteUpdateRequestDto requestDto) {
+ LeaderVote leaderVote = findLeaderVoteByUserId(userId);
+
+ LeaderCandidate leaderCandidate = leaderCandidateService.findLeaderCandidateById(requestDto.leader_candidate_id());
+
+ leaderVote.setLeaderCandidate(leaderCandidate);
+ leaderVoteRepository.save(leaderVote);
+
+ }
+
+ /*
+ 파트별 leaderVote 결과 조회
+ */
+ public List getLeaderResult(Part part) {
+ // 파트에 속한 모든 리더 후보 조회
+ List leaderCandidates = leaderCandidateRepository.findByPart(part);
+
+ // 각 후보의 투표 결과 반환 및 득표수 내림차순 정렬
+ return leaderCandidates.stream()
+ .map(candidate -> LeaderResultResponseDto.from(candidate.getName(), leaderVoteRepository.countByLeaderCandidate(candidate)))
+ .sorted(Comparator.comparingLong(LeaderResultResponseDto::getVoteCount).reversed()) // 내림차순 정렬
+ .toList();
+ }
+
+ /*
+ 파트별 leaderVote 총 투표수 조회
+ */
+ public long getTotalVotesByPart(Part part) {
+ // 파트에 속한 모든 리더 후보 조회
+ List leaderCandidates = leaderCandidateRepository.findByPart(part);
+
+ // 각 후보의 투표수를 합산
+ return leaderCandidates.stream()
+ .mapToLong(candidate -> leaderVoteRepository.countByLeaderCandidate(candidate))
+ .sum();
+ }
+
+ /*
+ leaderVote 전체 결과 조회
+ */
+ public List getLeaderVoteFinalResult() {
+
+ // part별 투표 결과를 LeaderVoteFinalResultResponseDto로 반환
+ return Arrays.stream(Part.values())
+ .map(part -> LeaderVoteFinalResultResponseDto.from(part, getTotalVotesByPart(part), getLeaderResult(part)))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamCandidate/entity/TeamCandidate.java b/src/main/java/com/ceos/vote/domain/teamCandidate/entity/TeamCandidate.java
new file mode 100644
index 0000000..a6a48cb
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamCandidate/entity/TeamCandidate.java
@@ -0,0 +1,27 @@
+package com.ceos.vote.domain.teamCandidate.entity;
+
+import com.ceos.vote.domain.users.enumerate.Team;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class TeamCandidate {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "team_candidate_id")
+ private Long id;
+
+ @Enumerated(EnumType.STRING)
+ @Column
+ private Team teamName;
+
+ @Builder
+ public TeamCandidate(Team team) {
+ this.teamName = team;
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamCandidate/repository/TeamCandidateRepository.java b/src/main/java/com/ceos/vote/domain/teamCandidate/repository/TeamCandidateRepository.java
new file mode 100644
index 0000000..3c5ed94
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamCandidate/repository/TeamCandidateRepository.java
@@ -0,0 +1,9 @@
+package com.ceos.vote.domain.teamCandidate.repository;
+
+import com.ceos.vote.domain.teamCandidate.entity.TeamCandidate;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface TeamCandidateRepository extends JpaRepository {
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/teamCandidate/service/TeamCandidateService.java b/src/main/java/com/ceos/vote/domain/teamCandidate/service/TeamCandidateService.java
new file mode 100644
index 0000000..5eb1b28
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamCandidate/service/TeamCandidateService.java
@@ -0,0 +1,24 @@
+package com.ceos.vote.domain.teamCandidate.service;
+
+import com.ceos.vote.domain.teamCandidate.entity.TeamCandidate;
+import com.ceos.vote.domain.teamCandidate.repository.TeamCandidateRepository;
+import com.ceos.vote.global.exception.ApplicationException;
+import com.ceos.vote.global.exception.ExceptionCode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class TeamCandidateService {
+
+
+ private final TeamCandidateRepository teamCandidateRepository;
+
+ public TeamCandidate findTeamCandidateById(Long id) {
+ return teamCandidateRepository.findById(id)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_TEAM_CANDIDATE));
+ }
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/controller/TeamVoteController.java b/src/main/java/com/ceos/vote/domain/teamVote/controller/TeamVoteController.java
new file mode 100644
index 0000000..fe29821
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/controller/TeamVoteController.java
@@ -0,0 +1,58 @@
+package com.ceos.vote.domain.teamVote.controller;
+
+import com.ceos.vote.domain.teamVote.dto.request.TeamVoteUpdateRequestDto;
+import com.ceos.vote.domain.teamVote.dto.response.TeamVoteByUserResponseDto;
+import com.ceos.vote.domain.teamVote.dto.request.TeamVoteCreateRequestDto;
+import com.ceos.vote.domain.teamVote.dto.response.TeamVoteFinalResultResponseDto;
+import com.ceos.vote.domain.teamVote.service.TeamVoteService;
+import com.ceos.vote.domain.teamVote.dto.response.TeamCandidateResponseDto;
+import com.ceos.vote.global.common.response.CommonResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "Team Vote", description = "team vote api")
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("api/v1/team")
+public class TeamVoteController {
+ private final TeamVoteService teamVoteService;
+
+ @Operation(summary = "team candidate 전체 조회")
+ @GetMapping("/candidate")
+ public CommonResponse> getTeamCandidates(){
+ final List teamCandidates = teamVoteService.findTeamCandidates();
+ return new CommonResponse<>(teamCandidates, "전체 후보팀 조회를 성공하였습니다.");
+ }
+
+ @Operation(summary = "team vote 생성")
+ @PostMapping
+ public CommonResponse createTeamVote(@RequestBody TeamVoteCreateRequestDto requestDto){
+ teamVoteService.createTeamVote(requestDto);
+ return new CommonResponse<>("new team vote를 생성하였습니다.");
+ }
+
+ @Operation(summary = "team vote 전체 결과 조회")
+ @GetMapping
+ public CommonResponse getTeamVoteResult(){
+ final TeamVoteFinalResultResponseDto result = teamVoteService.getTeamVoteFinalResult();
+ return new CommonResponse<>(result, "전체 투표 결과 조회를 성공하였습니다.");
+ }
+
+ @Operation(summary = "user의 team vote 결과 조회")
+ @GetMapping("/{userId}")
+ public CommonResponse getTeamVoteByUser(@PathVariable Long userId){
+ final TeamVoteByUserResponseDto teamVoteByUser = teamVoteService.getTeamVoteByUser(userId);
+ return new CommonResponse<>(teamVoteByUser, "해당 user의 투표 결과 조회에 성공하였습니다.");
+ }
+
+ @Operation(summary = "user의 team vote 수정")
+ @PutMapping("/{userId}")
+ public CommonResponse updateTeamVote(@PathVariable Long userId, @RequestBody TeamVoteUpdateRequestDto requestDto){
+ teamVoteService.updateTeamVoteByUser(userId, requestDto);
+ return new CommonResponse<>("team vote 수정을 성공하였습니다.");
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/dto/request/TeamVoteCreateRequestDto.java b/src/main/java/com/ceos/vote/domain/teamVote/dto/request/TeamVoteCreateRequestDto.java
new file mode 100644
index 0000000..902e03b
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/dto/request/TeamVoteCreateRequestDto.java
@@ -0,0 +1,19 @@
+package com.ceos.vote.domain.teamVote.dto.request;
+
+import com.ceos.vote.domain.teamCandidate.entity.TeamCandidate;
+import com.ceos.vote.domain.teamVote.entity.TeamVote;
+import com.ceos.vote.domain.users.entity.Users;
+import jakarta.validation.constraints.NotNull;
+
+public record TeamVoteCreateRequestDto(
+ Long user_id,
+ @NotNull
+ Long team_candidate_id
+){
+ public TeamVote toEntity(Users user, TeamCandidate teamCandidate) {
+ return TeamVote.builder()
+ .user(user)
+ .teamCandidate(teamCandidate)
+ .build();
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/dto/request/TeamVoteUpdateRequestDto.java b/src/main/java/com/ceos/vote/domain/teamVote/dto/request/TeamVoteUpdateRequestDto.java
new file mode 100644
index 0000000..4623712
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/dto/request/TeamVoteUpdateRequestDto.java
@@ -0,0 +1,9 @@
+package com.ceos.vote.domain.teamVote.dto.request;
+
+import jakarta.validation.constraints.NotNull;
+
+public record TeamVoteUpdateRequestDto(
+ @NotNull
+ Long team_candidate_id
+){
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamCandidateResponseDto.java b/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamCandidateResponseDto.java
new file mode 100644
index 0000000..00ae42d
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamCandidateResponseDto.java
@@ -0,0 +1,15 @@
+package com.ceos.vote.domain.teamVote.dto.response;
+
+import com.ceos.vote.domain.teamCandidate.entity.TeamCandidate;
+
+public record TeamCandidateResponseDto (
+ Long id,
+ String name
+){
+ public static TeamCandidateResponseDto from(TeamCandidate teamCandidate) {
+ return new TeamCandidateResponseDto(
+ teamCandidate.getId(),
+ teamCandidate.getTeamName().getDescription()
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamResultResponseDto.java b/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamResultResponseDto.java
new file mode 100644
index 0000000..ab48556
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamResultResponseDto.java
@@ -0,0 +1,17 @@
+package com.ceos.vote.domain.teamVote.dto.response;
+
+public record TeamResultResponseDto(
+ String team_name,
+ Long count
+){
+ public Long getVoteCount() {
+ return count;
+ }
+
+ public static TeamResultResponseDto from(String team_name, Long count){
+ return new TeamResultResponseDto(
+ team_name,
+ count
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamVoteByUserResponseDto.java b/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamVoteByUserResponseDto.java
new file mode 100644
index 0000000..168f631
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamVoteByUserResponseDto.java
@@ -0,0 +1,17 @@
+package com.ceos.vote.domain.teamVote.dto.response;
+
+import com.ceos.vote.domain.teamVote.entity.TeamVote;
+import jakarta.validation.constraints.NotNull;
+
+public record TeamVoteByUserResponseDto(
+ @NotNull
+ Boolean is_voting,
+ Long team_Candidate_id
+){
+ public static TeamVoteByUserResponseDto from(TeamVote teamVote, Boolean is_voting){
+ return new TeamVoteByUserResponseDto(
+ is_voting,
+ teamVote != null ? teamVote.getTeamCandidate().getId() : null
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamVoteFinalResultResponseDto.java b/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamVoteFinalResultResponseDto.java
new file mode 100644
index 0000000..ae63581
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/dto/response/TeamVoteFinalResultResponseDto.java
@@ -0,0 +1,15 @@
+package com.ceos.vote.domain.teamVote.dto.response;
+
+import java.util.List;
+
+public record TeamVoteFinalResultResponseDto(
+ Long total_count,
+ List results
+){
+ public static TeamVoteFinalResultResponseDto from(Long total_count, List results){
+ return new TeamVoteFinalResultResponseDto(
+ total_count,
+ results
+ );
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/entity/TeamVote.java b/src/main/java/com/ceos/vote/domain/teamVote/entity/TeamVote.java
new file mode 100644
index 0000000..eedaa2e
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/entity/TeamVote.java
@@ -0,0 +1,39 @@
+package com.ceos.vote.domain.teamVote.entity;
+
+import com.ceos.vote.domain.teamCandidate.entity.TeamCandidate;
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.global.BaseEntity;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class TeamVote extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "team_vote_id")
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id")
+ private Users user;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "team_candidate_id")
+ private TeamCandidate teamCandidate;
+
+ public void setTeamCandidate(TeamCandidate teamCandidate) {
+ this.teamCandidate = teamCandidate;
+ }
+
+ @Builder
+ public TeamVote(Users user, TeamCandidate teamCandidate) {
+ this.user = user;
+ this.teamCandidate = teamCandidate;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/repository/TeamVoteRepository.java b/src/main/java/com/ceos/vote/domain/teamVote/repository/TeamVoteRepository.java
new file mode 100644
index 0000000..7ccd1ce
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/repository/TeamVoteRepository.java
@@ -0,0 +1,16 @@
+package com.ceos.vote.domain.teamVote.repository;
+
+import com.ceos.vote.domain.teamCandidate.entity.TeamCandidate;
+import com.ceos.vote.domain.teamVote.entity.TeamVote;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface TeamVoteRepository extends JpaRepository {
+ Optional findByUserId(Long userId);
+ Long countByTeamCandidate(TeamCandidate teamCandidate);
+
+ Boolean existsByUserId(Long userId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/teamVote/service/TeamVoteService.java b/src/main/java/com/ceos/vote/domain/teamVote/service/TeamVoteService.java
new file mode 100644
index 0000000..18a4761
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/teamVote/service/TeamVoteService.java
@@ -0,0 +1,116 @@
+package com.ceos.vote.domain.teamVote.service;
+
+import com.ceos.vote.domain.auth.service.UserDetailService;
+import com.ceos.vote.domain.leaderVote.service.LeaderVoteService;
+import com.ceos.vote.domain.teamCandidate.entity.TeamCandidate;
+import com.ceos.vote.domain.teamCandidate.service.TeamCandidateService;
+import com.ceos.vote.domain.teamVote.dto.request.TeamVoteCreateRequestDto;
+import com.ceos.vote.domain.teamVote.dto.request.TeamVoteUpdateRequestDto;
+import com.ceos.vote.domain.teamVote.dto.response.TeamResultResponseDto;
+import com.ceos.vote.domain.teamVote.dto.response.TeamVoteByUserResponseDto;
+import com.ceos.vote.domain.teamVote.dto.response.TeamVoteFinalResultResponseDto;
+import com.ceos.vote.domain.teamVote.entity.TeamVote;
+import com.ceos.vote.domain.teamCandidate.repository.TeamCandidateRepository;
+import com.ceos.vote.domain.teamVote.dto.response.TeamCandidateResponseDto;
+import com.ceos.vote.domain.teamVote.repository.TeamVoteRepository;
+import com.ceos.vote.domain.users.entity.Users;
+import com.ceos.vote.global.exception.ApplicationException;
+import com.ceos.vote.global.exception.ExceptionCode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Comparator;
+import java.util.List;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class TeamVoteService {
+ private final TeamVoteRepository teamVoteRepository;
+ private final TeamCandidateRepository teamCandidateRepository;
+ private final TeamCandidateService teamCandidateService;
+ private final UserDetailService userDetailService;
+
+ public TeamVote findTeamVoteByUserId(Long id) {
+ return teamVoteRepository.findByUserId(id)
+ .orElseThrow(() -> new ApplicationException(ExceptionCode.NOT_FOUND_TEAM_VOTE));
+ }
+
+ public Boolean checkTeamVoteByUserId(Long id) {
+ return teamVoteRepository.existsByUserId(id);
+ }
+
+ /*
+ 전체 team candidate 조회
+ */
+ public List findTeamCandidates() {
+ List teamCandidates = teamCandidateRepository.findAll();
+
+ return teamCandidates.stream()
+ .map(TeamCandidateResponseDto::from)
+ .toList();
+ }
+
+ /*
+ teamVote 생성
+ */
+ @Transactional
+ public void createTeamVote(TeamVoteCreateRequestDto requestDto) {
+ Users user = userDetailService.findUserById(requestDto.user_id());
+ TeamCandidate teamCandidate = teamCandidateService.findTeamCandidateById(requestDto.team_candidate_id());
+
+ final TeamVote teamVote = requestDto.toEntity(user, teamCandidate);
+ teamVoteRepository.save(teamVote);
+ }
+
+ /*
+ user의 teamVote 결과 조회
+ */
+ public TeamVoteByUserResponseDto getTeamVoteByUser(final Long userId) {
+ try {
+ final TeamVote teamVote = findTeamVoteByUserId(userId);
+ return TeamVoteByUserResponseDto.from(teamVote, true);
+ } catch (Exception e) {
+ return TeamVoteByUserResponseDto.from(null, false);
+ }
+ }
+
+ /*
+ user의 teamVote 수정
+ */
+ @Transactional
+ public void updateTeamVoteByUser(final Long userId, final TeamVoteUpdateRequestDto requestDto) {
+ TeamVote teamVote = findTeamVoteByUserId(userId);
+
+ TeamCandidate teamCandidate = teamCandidateService.findTeamCandidateById(requestDto.team_candidate_id());
+
+ teamVote.setTeamCandidate(teamCandidate);
+ teamVoteRepository.save(teamVote);
+
+ }
+
+ /*
+ 팀별 teamVote 결과 조회
+ */
+ public List getTeamResult() {
+ // 모든 후보팀 조회
+ List teamCandidates = teamCandidateRepository.findAll();
+
+ // 각 후보의 투표 결과 반환 및 득표수 내림차순 정렬
+ return teamCandidates.stream()
+ .map(candidate -> TeamResultResponseDto.from(candidate.getTeamName().getDescription(), teamVoteRepository.countByTeamCandidate(candidate)))
+ .sorted(Comparator.comparingLong(TeamResultResponseDto::getVoteCount).reversed()) // 내림차순 정렬
+ .toList();
+ }
+
+ /*
+ teamVote 전체 결과 조회
+ */
+ public TeamVoteFinalResultResponseDto getTeamVoteFinalResult(){
+ List results= getTeamResult();
+ Long total_count = teamVoteRepository.count();
+
+ return TeamVoteFinalResultResponseDto.from(total_count, results);
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/users/controller/UserController.java b/src/main/java/com/ceos/vote/domain/users/controller/UserController.java
new file mode 100644
index 0000000..b3b5247
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/users/controller/UserController.java
@@ -0,0 +1,29 @@
+package com.ceos.vote.domain.users.controller;
+
+import com.ceos.vote.domain.auth.service.UserService;
+import com.ceos.vote.domain.users.dto.response.UserResponseDto;
+import com.ceos.vote.global.common.response.CommonResponse;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("api/v1/users")
+public class UserController {
+
+ private final UserService userService;
+
+ public UserController(UserService userService) {
+ this.userService = userService;
+ }
+
+ //현재 가지고 있는(로그인된) 정보로 회원 정보 조회
+ @GetMapping
+ public CommonResponse getUserInfo(@AuthenticationPrincipal User user) {
+ UserResponseDto userResponseDto = userService.getUserInfo(user.getUsername());
+ return new CommonResponse<>(userResponseDto, "사용자 정보 조회에 성공했습니다.");
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/users/dto/response/UserResponseDto.java b/src/main/java/com/ceos/vote/domain/users/dto/response/UserResponseDto.java
new file mode 100644
index 0000000..5d6cb54
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/users/dto/response/UserResponseDto.java
@@ -0,0 +1,12 @@
+package com.ceos.vote.domain.users.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class UserResponseDto {
+ private String username;
+ private String userPart;
+ private String userTeam;
+}
diff --git a/src/main/java/com/ceos/vote/domain/users/entity/Users.java b/src/main/java/com/ceos/vote/domain/users/entity/Users.java
new file mode 100644
index 0000000..871ed41
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/users/entity/Users.java
@@ -0,0 +1,54 @@
+package com.ceos.vote.domain.users.entity;
+
+
+import com.ceos.vote.domain.users.enumerate.Part;
+import com.ceos.vote.domain.users.enumerate.Team;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Getter
+@Table(name = "USER")
+@Builder
+@EqualsAndHashCode(of = "id")
+public class Users implements UserDetails {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "user_id", nullable = false, unique = true)
+ private Long id;
+
+ @Column(name="name")
+ private String username;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+
+ @Column(name = "password", nullable = false)
+ private String password;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name="user_team")
+ private Team userTeam;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name="user_part")
+ private Part userPart;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of(new SimpleGrantedAuthority("ROLE_USER"));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/users/enumerate/Part.java b/src/main/java/com/ceos/vote/domain/users/enumerate/Part.java
new file mode 100644
index 0000000..b4d23c6
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/users/enumerate/Part.java
@@ -0,0 +1,20 @@
+package com.ceos.vote.domain.users.enumerate;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public enum Part {
+
+ BE("BE"),
+ FE("FE"),
+ DESIGN("DESIGN"),
+ PLANNING("PLANNING");
+
+ private final String description;
+
+ Part(String description) {
+ this.description = description;
+ }
+}
diff --git a/src/main/java/com/ceos/vote/domain/users/enumerate/Team.java b/src/main/java/com/ceos/vote/domain/users/enumerate/Team.java
new file mode 100644
index 0000000..449fc28
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/users/enumerate/Team.java
@@ -0,0 +1,22 @@
+package com.ceos.vote.domain.users.enumerate;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public enum Team {
+
+ CUPFEEDEAL("CUPFEEDEAL"),
+ MUSAI("MUSAI"),
+ CAKEWAY("CAKEWAY"),
+ PHOTOGROUND("PHOTOGROUND"),
+ ANGELBRIDGE("ANGELBRIDGE"); ;
+
+ private final String description;
+
+ Team(String description) {
+ this.description = description;
+ }
+
+}
diff --git a/src/main/java/com/ceos/vote/domain/users/repository/UserRepository.java b/src/main/java/com/ceos/vote/domain/users/repository/UserRepository.java
new file mode 100644
index 0000000..f7e6ac1
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/users/repository/UserRepository.java
@@ -0,0 +1,17 @@
+package com.ceos.vote.domain.users.repository;
+
+import com.ceos.vote.domain.users.entity.Users;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface UserRepository extends JpaRepository {
+
+ Optional findByUsername(String username);
+
+ String username(String username);
+ boolean existsByUsername(String username);
+ boolean existsByEmail(String email);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/domain/utils/SecurityUtil.java b/src/main/java/com/ceos/vote/domain/utils/SecurityUtil.java
new file mode 100644
index 0000000..6a2ed6a
--- /dev/null
+++ b/src/main/java/com/ceos/vote/domain/utils/SecurityUtil.java
@@ -0,0 +1,17 @@
+package com.ceos.vote.domain.utils;
+
+import com.ceos.vote.global.exception.ApplicationException;
+import com.ceos.vote.global.exception.ExceptionCode;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+public class SecurityUtil {
+
+ public static String getCurrentUsername() {
+ final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication == null || authentication.getName()==null) {
+ throw new ApplicationException(ExceptionCode.INVALID_USER_CREDENTIALS);
+ }
+ return authentication.getName();
+ }
+}
diff --git a/src/main/java/com/ceos/vote/global/BaseEntity.java b/src/main/java/com/ceos/vote/global/BaseEntity.java
new file mode 100644
index 0000000..8bd834d
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/BaseEntity.java
@@ -0,0 +1,30 @@
+package com.ceos.vote.global;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class BaseEntity {
+
+ @CreatedDate
+ @Column(name = "created_at", nullable = false, columnDefinition = "TIMESTAMP")
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ @Column(name = "updated_at", nullable = false, columnDefinition = "TIMESTAMP")
+ private LocalDateTime updatedAt;
+
+ @Column(name = "deleted_at", columnDefinition = "TIMESTAMP")
+ private LocalDateTime deletedAt;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/global/common/response/CommonResponse.java b/src/main/java/com/ceos/vote/global/common/response/CommonResponse.java
new file mode 100644
index 0000000..34ef387
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/common/response/CommonResponse.java
@@ -0,0 +1,33 @@
+package com.ceos.vote.global.common.response;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+import java.time.LocalDateTime;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+public class CommonResponse {
+
+ private LocalDateTime timestamp;
+ private int code;
+ private String message;
+ private T result;
+
+ // 반환값 있는 경우
+ public CommonResponse(T result, String message) {
+ this.timestamp = LocalDateTime.now();
+ this.code = HttpStatus.OK.value();
+ this.message = message;
+ this.result = result;
+ }
+
+ // 반환값 없는 경우
+ public CommonResponse(String message) {
+ this.timestamp = LocalDateTime.now();
+ this.code = HttpStatus.OK.value();
+ this.message = message;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/global/config/CorsMvcConfig.java b/src/main/java/com/ceos/vote/global/config/CorsMvcConfig.java
new file mode 100644
index 0000000..75eb7f9
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/config/CorsMvcConfig.java
@@ -0,0 +1,18 @@
+package com.ceos.vote.global.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class CorsMvcConfig implements WebMvcConfigurer {
+
+ @Override
+ public void addCorsMappings(CorsRegistry corsRegistry) {
+
+ corsRegistry.addMapping("/**")
+ .allowedOriginPatterns("*")
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
+ .allowedHeaders("*");
+ }
+}
diff --git a/src/main/java/com/ceos/vote/global/config/SecurityConfig.java b/src/main/java/com/ceos/vote/global/config/SecurityConfig.java
new file mode 100644
index 0000000..c378c1c
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/config/SecurityConfig.java
@@ -0,0 +1,102 @@
+package com.ceos.vote.global.config;
+
+
+import com.ceos.vote.domain.auth.JwtAuthenticationFilter;
+import com.ceos.vote.domain.auth.JwtTokenProvider;
+import jakarta.servlet.http.HttpServletRequest;
+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.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+import lombok.RequiredArgsConstructor;
+
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+
+import java.util.Collections;
+import java.util.List;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+ private final JwtTokenProvider jwtTokenProvider;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ .cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
+
+ @Override
+ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
+
+ CorsConfiguration configuration = new CorsConfiguration();
+ /*
+ configuration.setAllowedOrigins(List.of(
+ "http://localhost:3000", // 로컬 개발 환경
+ "http://localhost:8080",
+ "http://52.79.122.106", // EC2 서버
+ "http://52.79.122.106:8081",
+ "http://52.79.122.106:8080"
+ ));
+ */
+ configuration.addAllowedOriginPattern("*"); // 테스트용 전체 허용
+ configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH"));
+ configuration.setAllowCredentials(true);
+ configuration.setAllowedHeaders(Collections.singletonList("*"));
+ configuration.setMaxAge(3600L);
+
+ configuration.setExposedHeaders(Collections.singletonList("Authorization"));
+
+ return configuration;
+ }
+ })));
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .formLogin(AbstractHttpConfigurer::disable)
+ .httpBasic(AbstractHttpConfigurer::disable)
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers("/hc", "/env", "/").permitAll()
+ .requestMatchers("/swagger/**", "/swagger-ui.html/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
+ .requestMatchers("/static/**", "/css/**", "/js/**", "/images/**").permitAll()
+ .requestMatchers("/api/v1/auth/**", "/reissue").permitAll()
+ .requestMatchers("/api/v1/**").hasRole("USER")
+ .anyRequest().authenticated())
+ .logout((logout) -> logout
+ .logoutSuccessUrl("/login")
+ .invalidateHttpSession(true))
+ .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
+ return configuration.getAuthenticationManager();
+ }
+
+ @Bean
+ public BCryptPasswordEncoder bCryptPasswordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ public static PasswordEncoder passwordEncoder() {
+ return PasswordEncoderFactories.createDelegatingPasswordEncoder();
+// return NoOpPasswordEncoder.getInstance();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/global/config/SwaggerConfig.java b/src/main/java/com/ceos/vote/global/config/SwaggerConfig.java
new file mode 100644
index 0000000..589b4ce
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/config/SwaggerConfig.java
@@ -0,0 +1,40 @@
+package com.ceos.vote.global.config;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.info.Info;
+import io.swagger.v3.oas.annotations.servers.Server;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@OpenAPIDefinition(
+ info = @Info(
+ title = "API Test",
+ description = "CEOS vote service API 명세서",
+ version = "v1"
+ ),
+ servers = {@Server(url = "http://localhost:8080", description = "local server"),
+ @Server(url = "http://52.79.122.106", description = "ec2 server")}
+)
+
+public class SwaggerConfig {
+ @Bean
+ public OpenAPI openAPI() {
+ String jwt = "JWT";
+ SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt);
+ Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme()
+ .name(jwt)
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat(jwt)
+ );
+
+ return new OpenAPI()
+ .addSecurityItem(securityRequirement)
+ .components(components);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/global/exception/ApplicationException.java b/src/main/java/com/ceos/vote/global/exception/ApplicationException.java
new file mode 100644
index 0000000..0b0ab98
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/exception/ApplicationException.java
@@ -0,0 +1,11 @@
+package com.ceos.vote.global.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class ApplicationException extends RuntimeException {
+
+ public ExceptionCode exceptionCode;
+}
diff --git a/src/main/java/com/ceos/vote/global/exception/ExceptionCode.java b/src/main/java/com/ceos/vote/global/exception/ExceptionCode.java
new file mode 100644
index 0000000..202b234
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/exception/ExceptionCode.java
@@ -0,0 +1,48 @@
+package com.ceos.vote.global.exception;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@RequiredArgsConstructor
+public enum ExceptionCode {
+
+ // 1000: Success Case
+
+ // 2000: Common Error
+ INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 2000, "서버 에러가 발생하였습니다. 관리자에게 문의해 주세요."),
+ NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, 2001, "존재하지 않는 리소스입니다."),
+ INVALID_VALUE_EXCEPTION(HttpStatus.BAD_REQUEST, 2002, "올바르지 않은 요청 값입니다."),
+ UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED, 2003, "권한이 없는 요청입니다."),
+ ALREADY_DELETE_EXCEPTION(HttpStatus.BAD_REQUEST, 2004, "이미 삭제된 리소스입니다."),
+ FORBIDDEN_EXCEPTION(HttpStatus.FORBIDDEN, 2005, "인가되지 않는 요청입니다."),
+ ALREADY_EXIST_EXCEPTION(HttpStatus.BAD_REQUEST, 2006, "이미 존재하는 리소스입니다."),
+ INVALID_SORT_EXCEPTION(HttpStatus.BAD_REQUEST, 2007, "올바르지 않은 정렬 값입니다."),
+ BAD_REQUEST_ERROR(HttpStatus.BAD_REQUEST, 2008, "잘못된 요청입니다."),
+
+ // 3000: USER/Auth
+ NOT_FOUND_USER(HttpStatus.NOT_FOUND, 3001, "해당 회원을 찾을 수 없습니다."),
+ INVALID_USER_CREDENTIALS(HttpStatus.UNAUTHORIZED, 3002, "잘못된 사용자 인증 정보입니다."),
+ INVALID_PASSWORD(HttpStatus.BAD_REQUEST, 3003, "비밀번호를 찾을 수 없습니다."),
+ NOT_FOUND_USERNAME(HttpStatus.NOT_FOUND, 3004, "해당 아이디를 찾을 수 없습니다."),
+
+ // 4000: Leader Vote Error
+ NOT_FOUND_LEADER_CANDIDATE(HttpStatus.NOT_FOUND, 4001, "해당 leader candidate가 존재하지 않습니다."),
+ NOT_FOUND_LEADER_VOTE(HttpStatus.NOT_FOUND, 4002, "해당 leader vote가 존재하지 않습니다."),
+
+ // 5000: Team Vote Error
+ NOT_FOUND_TEAM_CANDIDATE(HttpStatus.NOT_FOUND, 4001, "해당 team candidate가 존재하지 않습니다."),
+ NOT_FOUND_TEAM_VOTE(HttpStatus.NOT_FOUND, 4002, "해당 team vote가 존재하지 않습니다.");
+
+
+ // 6000:
+
+ //7000: [임의] Error
+
+
+ private final HttpStatus httpStatus;
+ private final int code;
+ private final String message;
+
+}
diff --git a/src/main/java/com/ceos/vote/global/exception/ExceptionResponse.java b/src/main/java/com/ceos/vote/global/exception/ExceptionResponse.java
new file mode 100644
index 0000000..887cce2
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/exception/ExceptionResponse.java
@@ -0,0 +1,24 @@
+package com.ceos.vote.global.exception;
+
+import java.time.LocalDateTime;
+
+public record ExceptionResponse (
+ LocalDateTime timestamp,
+ int code,
+ String message
+) {
+ // 기본 생성자: exceptionCode만 사용하는 경우
+ public ExceptionResponse(ExceptionCode exceptionCode) {
+ this(LocalDateTime.now(), exceptionCode.getCode(), exceptionCode.getMessage());
+ }
+
+ // 메시지를 직접 지정하는 경우
+ public ExceptionResponse(String message) {
+ this(LocalDateTime.now(), ExceptionCode.INTERNAL_SERVER_ERROR.getCode(), message);
+ }
+
+ // exceptionCode와 메시지를 동시에 사용하는 경우
+ public ExceptionResponse(ExceptionCode exceptionCode, String message) {
+ this(LocalDateTime.now(), exceptionCode.getCode(), message);
+ }
+}
diff --git a/src/main/java/com/ceos/vote/global/exception/GlobalExceptionHandler.java b/src/main/java/com/ceos/vote/global/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..5dfb5b0
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/exception/GlobalExceptionHandler.java
@@ -0,0 +1,46 @@
+package com.ceos.vote.global.exception;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.Objects;
+
+
+@Slf4j
+@RestControllerAdvice
+@RequiredArgsConstructor
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(ApplicationException.class)
+ protected ResponseEntity handleBadRequestException(ApplicationException e){
+
+ log.error("BadRequestException 발생: {}", e.getMessage(), e);
+
+ return ResponseEntity.status(e.getExceptionCode().getHttpStatus())
+ .body(new ExceptionResponse(e.getExceptionCode()));
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e) {
+
+ log.error("MethodArgumentNotValidException 발생: {}", e.getMessage(), e);
+
+ return ResponseEntity
+ .status(HttpStatus.BAD_REQUEST)
+ .body(new ExceptionResponse(ExceptionCode.INVALID_VALUE_EXCEPTION, Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage()));
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleException(final Exception e){
+ log.error("UnhandledException 발생: {}", e.getMessage(), e);
+
+ return ResponseEntity.internalServerError()
+ .body(new ExceptionResponse(ExceptionCode.INTERNAL_SERVER_ERROR, e.getMessage()));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/global/healthCheck/HealthCheckController.java b/src/main/java/com/ceos/vote/global/healthCheck/HealthCheckController.java
new file mode 100644
index 0000000..895925c
--- /dev/null
+++ b/src/main/java/com/ceos/vote/global/healthCheck/HealthCheckController.java
@@ -0,0 +1,38 @@
+package com.ceos.vote.global.healthCheck;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+public class HealthCheckController {
+
+ @Value("${server.env}")
+ private String env;
+
+ @Value("${server.port}")
+ private String serverPort;
+
+ @Value("${server.serverAddress}")
+ private String serverAddress;
+
+ @GetMapping("/hc")
+ public ResponseEntity> healthCheck() {
+ Map response = new HashMap<>();
+ response.put("serverAddress", serverAddress);
+ response.put("serverPort", serverPort);
+ response.put("env", env);
+
+ return ResponseEntity.ok(response);
+ }
+
+ @GetMapping("/env")
+ public ResponseEntity> getEnv() {
+
+ return ResponseEntity.ok(env);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ceos/vote/utils/PasswordEncoderGenerator.java b/src/main/java/com/ceos/vote/utils/PasswordEncoderGenerator.java
new file mode 100644
index 0000000..4b4451c
--- /dev/null
+++ b/src/main/java/com/ceos/vote/utils/PasswordEncoderGenerator.java
@@ -0,0 +1,11 @@
+package com.ceos.vote.utils;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+public class PasswordEncoderGenerator {
+ public static void main(String[] args) {
+ BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
+ String encodedPassword = encoder.encode("5678");
+ System.out.println(encodedPassword);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..a5010a6
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,66 @@
+spring:
+ profiles:
+ active: local
+ group:
+ local: local, common, secret
+ blue: blue, common, secret
+ green: green, common, secret
+
+server:
+ env: blue
+
+---
+
+# local
+spring:
+ config:
+ activate:
+ on-profile: local
+
+server:
+ port: 8080
+ serverAddress: localhost
+
+serverName: local_server
+
+---
+
+# blue server
+spring:
+ config:
+ activate:
+ on-profile: blue
+
+server:
+ port: 8080
+ serverAddress: 52.79.122.106
+
+serverName: blue_server
+
+---
+
+# green server
+spring:
+ config:
+ activate:
+ on-profile: green
+
+server:
+ port: 8081
+ serverAddress: 52.79.122.106
+
+serverName: green_server
+
+---
+
+spring:
+ config:
+ activate:
+ on-profile: common
+ import: optional:file:.env[.properties]
+logging:
+ level:
+ org.hibernate.SQL: debug
+springdoc:
+ swagger-ui:
+ path: /swagger
diff --git a/src/test/java/com/ceos/vote/CeosVoteBeApplicationTests.java b/src/test/java/com/ceos/vote/CeosVoteBeApplicationTests.java
new file mode 100644
index 0000000..bd83ad5
--- /dev/null
+++ b/src/test/java/com/ceos/vote/CeosVoteBeApplicationTests.java
@@ -0,0 +1,13 @@
+package com.ceos.vote;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class CeosVoteBeApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}