diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5b1cc00
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,46 @@
+*#
+*.iml
+*.ipr
+*.iws
+*.jar
+*.sw?
+*~
+.#*
+.*.md.html
+.DS_Store
+.attach_pid*
+.classpath
+.factorypath
+.gradle
+.metadata
+.project
+.recommenders
+.settings
+.springBeans
+.vscode
+/code
+MANIFEST.MF
+_site/
+activemq-data
+bin
+build
+!/**/src/**/bin
+!/**/src/**/build
+build.log
+dependency-reduced-pom.xml
+dump.rdb
+interpolated*.xml
+lib/
+manifest.yml
+out
+overridedb.*
+target
+.flattened-pom.xml
+secrets.yml
+.gradletasknamecache
+.sts4-cache
+
+.idea
+.env
+src/test/resources/application.yml
+src/main/resources/application.yml
\ No newline at end of file
diff --git a/HELP.md b/HELP.md
new file mode 100644
index 0000000..9bc5cae
--- /dev/null
+++ b/HELP.md
@@ -0,0 +1,28 @@
+# Getting Started
+
+### Reference Documentation
+For further reference, please consider the following sections:
+
+* [Official Gradle documentation](https://docs.gradle.org)
+* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/3.3.7/gradle-plugin)
+* [Create an OCI image](https://docs.spring.io/spring-boot/3.3.7/gradle-plugin/packaging-oci-image.html)
+* [Spring Web](https://docs.spring.io/spring-boot/3.3.7/reference/web/servlet.html)
+* [Spring Data JPA](https://docs.spring.io/spring-boot/3.3.7/reference/data/sql.html#data.sql.jpa-and-spring-data)
+* [Spring Security](https://docs.spring.io/spring-boot/3.3.7/reference/web/spring-security.html)
+
+### Guides
+The following guides illustrate how to use some features concretely:
+
+* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
+* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
+* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)
+* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/)
+* [Securing a Web Application](https://spring.io/guides/gs/securing-web/)
+* [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/)
+* [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/)
+
+### Additional Links
+These additional references should also help you:
+
+* [Gradle Build Scans โ insights for your project's build](https://scans.gradle.com#gradle)
+
diff --git a/README.md b/README.md
index 76efd88..ec3eb11 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,113 @@
-# spring_vote_20th
\ No newline at end of file
+# spring_vote_20th
+
+## ๐กย ๊ตฌํ ๊ธฐ๋ฅ
+
+**ERD**
+
+
+
+1. **Member**
+ - ๋ก๊ทธ์ธํ ์ฌ์ฉ์(๋ฉค๋ฒ)๋ฅผ ์๋ฏธํ๋ค.
+ - ํฌํ ๊ถํ์ด ์์ผ๋ฉฐ, ์๋์ ํฌํ์ ๊ฐ๊ฐ ์ต๋ **1ํ ์ฐธ์ฌ**ํ ์ ์๋ค.
+ - **vote_back, vote_front**: ๊ฐ๋ฐ์ ํฌํ
+ - **vote_team**: ํ ํฌํ
+2. **Team**๊ณผ **Developer**
+ - ํฌํ ๊ฐ๋ฅํ **ํ๋ณด**๋ฅผ ์๋ฏธํ๋ค.
+ - ๊ฐ ๊ฐ์ฒด๋ ์์ ์ ํฌํ์๋ฅผ ๋ํ๋ด๋ **count** ํ๋๋ฅผ ๊ฐ์ง๋ค.
+
+
+
+**Auth**
+
+- JWT ๋ฐฉ์ ๋ก๊ทธ์ธ์ ์ฌ์ฉํฉ๋๋ค.
+ - **Access Token**์ ํค๋์ ๋ฐ๊ธํ๊ณ , **Refresh Token**์ ์ฟ ํค๋ก ๋ฐ๊ธํฉ๋๋ค.
+ - **Refresh Token**์ ๋ณ๋์ `Refresh` ์ํฐํฐ์ ์ ์ฅํ์ฌ ๊ฒ์ฆํฉ๋๋ค.
+ - ๋ก๊ทธ์์ ์ **Refresh Token**์ ์ญ์ ํ์ฌ ์ฌ์ฌ์ฉ์ ๋ฐฉ์งํฉ๋๋ค.
+
+**Vote**
+
+- ์๋ฌ ์ฒ๋ฆฌ
+ 1. ๊ฐ์ ํ์ ํฌํ: `BAD_REQUEST_TEAM`
+ 2. ๋ค๋ฅธ ํํธ์ ํฌํ: `BAD_REQUEST_DEVELOPER`
+ 3. ์ค๋ณต ํฌํ
+ - ๊ฐ๋ฐ์ ํฌํ: `ALREADY_VOTE_DEVELOPER`
+ - ํ ํฌํ: `ALREADY_VOTE_TEAM`
+
+## ๐งฉย ๋ฐฐํฌ
+
+### ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฐํฌ ๋ฐฉ๋ฒ
+
+1. ์คํ๋ง๋ถํธ์์ BootJar์ ์คํํด์ฃผ๋ฉด build/libs ํด๋ ์์ jar ํ์ผ์ด ์์ฑ๋จ
+2. ์๋ ๋ช
๋ น์ด๋ฅผ ํตํด jar ํ์ผ์ ๋ด ์ธ์คํด์ค์ ์ฎ๊ฒจ์ค
+
+ ```java
+ scp -i "{my-key}.pem" ./build/libs/{jar-file-name}.jar ubuntu@{ํผ๋ธ๋ฆญ IP}:/home/ubuntu
+ ```
+
+ - **scp (Secure Copy Protocol)**
+ - SSH๋ฅผ ์ด์ฉํด ํ์ผ์ ์์ ํ๊ฒ ๋ณต์ฌํ๋ ๋ช
๋ น์ด์ด๋ค. ๋ก์ปฌ ์์คํ
๊ณผ ์๊ฒฉ ์์คํ
๊ฐ, ๋๋ ์๊ฒฉ ์์คํ
๋ค ๊ฐ์ ํ์ผ์ ์ ์กํ ๋ ์ฌ์ฉํ๋ค.
+3. ์ธ์คํด์ค ํฐ๋ฏธ๋์ ์ ์ํด ์๋ ๋ช
๋ น์ด๋ฅผ ํตํด jar ํ์ผ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํ์์ผ์ฃผ๋ฉด ๋!
+
+ ```java
+ nohup java -jar backend-0.0.1-SNAPSHOT.jar &
+ ```
+
+ - **nohup (no hang up)**
+ - ํฐ๋ฏธ๋์ด ์ข
๋ฃ๋์ด๋ ๋ช
๋ น์ด ์คํ์ด ์ค๋จ๋์ง ์๋๋ก ๋ณด์ฅํ๋ ๋ช
๋ น์ด์ด๋ค.
+ - **&**
+ - ๋ช
๋ น์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํํ๋ ์ ์ฐ์ฐ์์ด๋ค.
+
+## ๐จย ํธ๋ฌ๋ธ ์ํ
+
+ํ๋ก ํธ๊น์ง ๋ฐฐํฌ๋ฅผ ํ๊ณ ๋ฐฑ์๋์ api ์์ฒญ์ ํ๋๋ฐ ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
+
+
+
+### Mixed Content ๋?
+
+๋ธ๋ผ์ฐ์ ์์ HTTPS๋ก ์ ๊ณต๋๋ ์น ํ์ด์ง๊ฐ ๋ณด์๋์ง ์์ HTTP ๋ฆฌ์์ค๋ฅผ ๋ก๋ํ๊ฑฐ๋ ์์ฒญํ ๋ ๋ฐ์ํ๋ ์ํฉ์ ๋งํ๋ค.
+
+HTTPS๋ ๋ฐ์ดํฐ๊ฐ ์ํธํ ๋์ด ์์ ํ๊ฒ ์ ์ก๋จ์ ๋ณด์ฅํ๋๋ฐ HTTP๋ ์ํธํ๋์ง ์์ ์ฐ๊ฒฐ์ ์ฌ์ฉํ์ฌ HTTPS ํ์ด์ง์์ HTTP ๋ฆฌ์์ค๋ฅผ ๋ก๋ํ๋ฉด ๋ณด์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๊ณ ํ๋ค.
+
+โ ์ฐ๋ฆฌ ํ๋ก ํธ๊ฐ Https๋ก ๋ฐฐํฌ๋ฅผ ํ๋๋ฐ, ๋ฐฑ์๋์์ http๋ก ๋ฐฐํฌ๋ฅผ ํด์ ๋ฐ์ํ ๋ฌธ์ ์๋ค. ์ฐ๋ฆฌ ๋ฐฑ์๋ ์๋ฒ์ Https ๋ฅผ ์ ์ฉํด์ฃผ๊ธฐ๋ก ํ๋ค.
+
+### https ์ ์ฉํ๊ธฐ
+
+์ผ๋จ ๋๋ฉ์ธ์ด ์๋ ์ํฉ์ด๋ผ, ๋๋ฉ์ธ ์์ด https๋ฅผ ์ ์ฉํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ฐพ๋ ์ค caddy๋ฅผ ์๊ฒ๋์์ต๋๋ค.
+
+**caddy์ ์ฃผ์ ์ญํ **
+
+1. ์๋์ผ๋ก tls ์ธ์ฆ์๋ฅผ ๋ฐ๊ธํด์ค๋ค
+2. nginx.conf์ ๊ฐ์ Caddyfile์ด ์กด์ฌํด, ๋ฆฌ๋ฒ์ค ํ๋ก์ ์ค์ ์ด ๊ฐ๋ฅํ๋ค.
+
+**CaddyFile**
+
+```java
+{
+ admin 0.0.0.0:2020
+}
+
+[ec2 PUBLIC IP์ฃผ์].nip.io {
+
+ tls [์ด๋ฉ์ผ ์ฃผ์]
+ reverse_proxy localhost:8080
+
+}
+```
+
+- `[ec2 PUBLIC IP์ฃผ์]`: EC2 ์ธ์คํด์ค์ ํผ๋ธ๋ฆญ IP ์ฃผ์๋ฅผ ํฌํจํฉ๋๋ค.
+- **`.nip.io`**: **๋์ DNS ์๋น์ค**๋ก, ํน์ IP ์ฃผ์๋ฅผ ํฌํจํ๋ ์์ ๋๋ฉ์ธ ์ด๋ฆ์ ์์ฑํด ์ค๋๋ค.
+ - ์: `123.45.67.89.nip.io`๋ก ์ ์ํ๋ฉด `123.45.67.89`๋ก ์ฐ๊ฒฐ๋ฉ๋๋ค.
+ - โ ์ด๋ฅผ ํตํด ๋๋ฉ์ธ์ด ์์ด๋ HTTPS๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
+- `tls [์ด๋ฉ์ผ ์ฃผ์]` : Let's Encrypt๋ฅผ ์ฌ์ฉํด ์ธ์ฆ์๋ฅผ ์๋์ผ๋ก ๋ฐ๊ธ๋ฐ๋๋ก ์ด๋ฉ์ผ ์ค์ ์ ํด์ค๋๋ค.
+ - ๋ง์ฝ **tls internal** ์ ์ ๋๋ค๋ฉด ์ธ๋ถ ์ธ์ฆ์ ๋ฐ๊ธ ์์ด caddy ์์ฒด ์ธ์ฆ์๋ฅผ ์์ฑํ์ฌ ๋ฐ๊ธํฉ๋๋ค. โ ์ฃผ์ํ ์ ์ ๋ก์ปฌ์์๋ง ์ฌ์ฉ ๊ฐ๋ฅํ๋ค๋ ์ ..ใ
ใ
+
+**๋์ ํ๋ฆ**
+
+`https://123.45.67.89.nip.io/api/data`
+
+1. ํด๋ผ์ด์ธํธ๊ฐ ๋๋ฉ์ธ(`123.45.67.89.nip.io`)๋ก HTTPS ์์ฒญ.
+2. Caddy๊ฐ ์์ฒญ ์์ โ ์ธ์ฆ์ ํ์ธ ๋ฐ ์ํธํ๋ ์ฐ๊ฒฐ ์ค์ .
+3. Caddy๋ ์์ฒญ์ ๋ถ์ ํ, `/api/data`๋ฅผ `localhost:8080`์ผ๋ก ์ ๋ฌ.
+4. ๋ด๋ถ ์ ํ๋ฆฌ์ผ์ด์
(Spring Boot ์๋ฒ)์ด ์์ฒญ์ ์ฒ๋ฆฌํ๊ณ ์๋ต ๋ฐํ.
+5. Caddy๊ฐ ์๋ต์ ๋ฐ์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌ.
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..5a8ba2e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,49 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '3.3.7'
+ id 'io.spring.dependency-management' version '1.1.7'
+}
+
+group = 'ceos'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ runtimeOnly 'com.mysql:mysql-connector-j'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+
+ // Swagger
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
+
+ // jwt
+ implementation "org.springframework.boot:spring-boot-starter-security"
+ implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
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..9b42019
--- /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..bfce6e0
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'vote'
diff --git a/src/main/java/ceos/vote/VoteApplication.java b/src/main/java/ceos/vote/VoteApplication.java
new file mode 100644
index 0000000..1d66a88
--- /dev/null
+++ b/src/main/java/ceos/vote/VoteApplication.java
@@ -0,0 +1,15 @@
+package ceos.vote;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@SpringBootApplication
+@EnableJpaAuditing
+public class VoteApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(VoteApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/ceos/vote/domain/developer/controller/DeveloperController.java b/src/main/java/ceos/vote/domain/developer/controller/DeveloperController.java
new file mode 100644
index 0000000..46c6e34
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/developer/controller/DeveloperController.java
@@ -0,0 +1,24 @@
+package ceos.vote.domain.developer.controller;
+import ceos.vote.domain.developer.dto.DeveloperRequestDto;
+import ceos.vote.domain.developer.service.DeveloperService;
+import ceos.vote.global.common.response.CommonResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/developer")
+@RequiredArgsConstructor
+public class DeveloperController {
+
+ private final DeveloperService developerService;
+
+ @PostMapping
+ public CommonResponse createDeveloper(@RequestBody DeveloperRequestDto developerRequestDto) {
+
+ developerService.createDeveloper(developerRequestDto);
+ return new CommonResponse<>("๊ฐ๋ฐ์ ์์ฑ ์๋ฃ");
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java b/src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java
new file mode 100644
index 0000000..0b78e30
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java
@@ -0,0 +1,25 @@
+package ceos.vote.domain.developer.dto;
+
+import ceos.vote.domain.developer.entity.Developer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+
+public record DeveloperRequestDto(
+
+ @Schema(description = "์ด๋ฆ", example = "ํ๊ธธ๋")
+ @NotNull(message = "์ด๋ฆ์ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String name,
+
+ @Schema(description = "์์ ํ๋ช
", example = "ํฌํ ๊ทธ๋ผ์ด๋ | ์์ ค๋ธ๋ฆฟ์ง | ํ๋ฌ์ง๋ | ์ผ์ดํฌ์จ์ด | ์ปคํผ๋")
+ @NotNull(message = "์์ ํ๋ช
์ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String team,
+
+ @Schema(description = "์๊ธฐ์๊ฐ", example = "์๋
ํ์ธ์~ ์ด๊ฑด ์๊ธฐ ์๊ฐ์
๋๋ค.")
+ @NotNull(message = "์๊ธฐ์๊ฐ๋ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String introduction,
+
+ @Schema(description = "์์ ํํธ", example = "backend | frontend")
+ @NotNull(message = "์์ ํํธ๋ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String part
+) {
+}
diff --git a/src/main/java/ceos/vote/domain/developer/entity/Developer.java b/src/main/java/ceos/vote/domain/developer/entity/Developer.java
new file mode 100644
index 0000000..5b1cd37
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/developer/entity/Developer.java
@@ -0,0 +1,48 @@
+package ceos.vote.domain.developer.entity;
+
+import ceos.vote.domain.member.entity.PartType;
+import ceos.vote.domain.team.entity.Team;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Developer {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "developer_id", nullable = false)
+ private Long id;
+
+ private String developerName;
+
+ @Enumerated(EnumType.STRING)
+ @Column(length = 50, nullable = false)
+ private PartType part;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "team_id")
+ private Team team;
+
+ private String introduction;
+
+ private int count = 0;
+
+ public void voteToMe() {
+ this.count++;
+ }
+
+ @Builder
+ public Developer(String developerName, PartType part, Team team, String introduction) {
+
+ this.developerName = developerName;
+ this.part = part;
+ this.team = team;
+ this.introduction = introduction;
+ }
+
+}
diff --git a/src/main/java/ceos/vote/domain/developer/service/DeveloperService.java b/src/main/java/ceos/vote/domain/developer/service/DeveloperService.java
new file mode 100644
index 0000000..54bb8a3
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/developer/service/DeveloperService.java
@@ -0,0 +1,49 @@
+package ceos.vote.domain.developer.service;
+
+import ceos.vote.domain.developer.dto.DeveloperRequestDto;
+import ceos.vote.domain.developer.entity.Developer;
+import ceos.vote.domain.member.entity.PartType;
+import ceos.vote.domain.member.entity.TeamType;
+import ceos.vote.domain.team.entity.Team;
+import ceos.vote.global.repository.DeveloperRepository;
+import ceos.vote.global.repository.TeamRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class DeveloperService {
+
+ private final TeamRepository teamRepository;
+ private final DeveloperRepository developerRepository;
+
+ @Transactional
+ public void createDeveloper(DeveloperRequestDto developerRequestDto) {
+
+ // team
+ TeamType type = null;
+ if (developerRequestDto.team().equals("ํฌํ ๊ทธ๋ผ์ด๋"))
+ type = TeamType.PHOTOGROUND;
+ else if (developerRequestDto.team().equals("์์ ค๋ธ๋ฆฟ์ง"))
+ type = TeamType.ANGELBRIDGE;
+ else if (developerRequestDto.team().equals("ํ๋ฌ์ง๋"))
+ type = TeamType.PEDALGENIE;
+ else if (developerRequestDto.team().equals("์ผ์ดํฌ์จ์ด"))
+ type = TeamType.CAKEWAY;
+ else if (developerRequestDto.team().equals("์ปคํผ๋"))
+ type = TeamType.COFFEEDEAL;
+ Team team = teamRepository.findByType(type);
+
+ // ํํธ (๋ฐฑ์๋ or ํ๋ก ํธ)
+ PartType partType = PartType.valueOf(developerRequestDto.part().toUpperCase());
+
+ Developer developer = Developer.builder()
+ .developerName(developerRequestDto.name())
+ .part(partType)
+ .team(team)
+ .introduction(developerRequestDto.introduction())
+ .build();
+ developerRepository.save(developer);
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/controller/AuthController.java b/src/main/java/ceos/vote/domain/member/controller/AuthController.java
new file mode 100644
index 0000000..c6e298a
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/controller/AuthController.java
@@ -0,0 +1,50 @@
+package ceos.vote.domain.member.controller;
+
+import ceos.vote.domain.member.dto.request.SignupRequestDto;
+import ceos.vote.domain.member.dto.response.MemberResponseDto;
+import ceos.vote.domain.member.service.AuthService;
+import ceos.vote.global.common.response.CommonResponse;
+import ceos.vote.global.jwt.JWTUtil;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+@Tag(name = "Auth", description = "์ธ์ฆ ๊ด๋ จ API")
+public class AuthController {
+
+ private final AuthService authService;
+ private final JWTUtil jwtUtil;
+
+ @PostMapping("/signup")
+ @Operation(summary = "ํ์๊ฐ์
", description = "ํ์๊ฐ์
์์ฒญ API")
+ public CommonResponse signup(@Valid @RequestBody SignupRequestDto request) {
+
+ return new CommonResponse<>(authService.signup(request), "ํ์๊ฐ์
์ ์ฑ๊ณตํ์์ต๋๋ค.");
+ }
+
+ @PostMapping("/reissue")
+ @Operation(summary = "ํ ํฐ ์ฌ๋ฐ๊ธ", description = "ํ ํฐ ์ฌ๋ฐ๊ธ ์์ฒญ API")
+ public CommonResponse reissue(HttpServletRequest request, HttpServletResponse response) {
+
+ String refreshToken = authService.extractRefreshToken(request);
+ authService.validateRefreshToken(refreshToken);
+
+ String newAccessToken = authService.reissueAccessToken(refreshToken);
+ Cookie RefreshTokenCookie = authService.createRefreshTokenCookie(refreshToken);
+
+ authService.setNewTokens(response, newAccessToken, RefreshTokenCookie);
+
+ return new CommonResponse<>("ํ ํฐ ์ฌ๋ฐ๊ธ์ ์ฑ๊ณตํ์์ต๋๋ค.");
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/controller/MemberController.java b/src/main/java/ceos/vote/domain/member/controller/MemberController.java
new file mode 100644
index 0000000..71e7da9
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/controller/MemberController.java
@@ -0,0 +1,33 @@
+package ceos.vote.domain.member.controller;
+
+import ceos.vote.domain.member.dto.CustomUserDetails;
+import ceos.vote.domain.member.dto.response.MemberResponseDto;
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.domain.member.service.MemberService;
+import ceos.vote.global.annotation.Login;
+import 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.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/member")
+@RequiredArgsConstructor
+@Tag(name = "Member", description = "ํ์ ๊ด๋ จ API")
+public class MemberController {
+
+ private final MemberService memberService;
+
+ @Operation(summary = "ํ์ ๊ธฐ๋ณธ ์ ๋ณด ์กฐํ", description = "ํ์์ ๊ธฐ๋ณธ ์ ๋ณด๋ฅผ ์กฐํํ๋ API")
+ @GetMapping
+ public CommonResponse getMemberInfo(@AuthenticationPrincipal CustomUserDetails userDetails) {
+
+ Long memberId = userDetails.getMemberId();
+
+ return new CommonResponse<>(memberService.getMemberInfo(memberId), "ํ์ ๊ธฐ๋ณธ ์ ๋ณด ์กฐํ๋ฅผ ์ฑ๊ณตํ์์ต๋๋ค.");
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/dto/CustomUserDetails.java b/src/main/java/ceos/vote/domain/member/dto/CustomUserDetails.java
new file mode 100644
index 0000000..b3244cc
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/dto/CustomUserDetails.java
@@ -0,0 +1,72 @@
+package ceos.vote.domain.member.dto;
+
+import ceos.vote.domain.member.entity.Member;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+@RequiredArgsConstructor
+public class CustomUserDetails implements UserDetails {
+
+ private final Member member;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+
+ Collection collection = new ArrayList<>();
+
+ collection.add(new GrantedAuthority() {
+
+ @Override
+ public String getAuthority() {
+ return member.getRole();
+ }
+ });
+
+ return collection;
+ }
+
+ @Override
+ public String getPassword() {
+
+ return member.getPassword();
+ }
+
+ @Override
+ public String getUsername() {
+
+ return member.getUserId();
+ }
+
+ public Long getMemberId() {
+
+ return member.getId();
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+
+ return true;
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/dto/request/LoginRequestDto.java b/src/main/java/ceos/vote/domain/member/dto/request/LoginRequestDto.java
new file mode 100644
index 0000000..16c7f1a
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/dto/request/LoginRequestDto.java
@@ -0,0 +1,16 @@
+package ceos.vote.domain.member.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+
+public record LoginRequestDto(
+
+ @Schema(description = "์์ด๋", example = "ceos2024")
+ @NotNull
+ String userId,
+
+ @Schema(description = "๋น๋ฐ๋ฒํธ", example = "12345678")
+ @NotNull
+ String password
+) {
+}
diff --git a/src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java b/src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java
new file mode 100644
index 0000000..10bdd41
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java
@@ -0,0 +1,43 @@
+package ceos.vote.domain.member.dto.request;
+
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.domain.member.entity.PartType;
+import ceos.vote.domain.member.entity.TeamType;
+import ceos.vote.domain.team.entity.Team;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+
+public record SignupRequestDto(
+
+ @Schema(description = "์ด๋ฆ", example = "ํ๊ธธ๋")
+ @NotNull(message = "์ด๋ฆ์ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String name,
+
+ @Schema(description = "์์ด๋", example = "ceos2024")
+ @NotNull(message = "์์ด๋๋ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String userId,
+
+ @Schema(description = "๋น๋ฐ๋ฒํธ", example = "12345678")
+ @NotNull(message = "๋น๋ฐ๋ฒํธ๋ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String password,
+
+ @Schema(description = "์์ ํ๋ช
", example = "ํฌํ ๊ทธ๋ผ์ด๋ | ์์ ค๋ธ๋ฆฟ์ง | ํ๋ฌ์ง๋ | ์ผ์ดํฌ์จ์ด | ์ปคํผ๋")
+ @NotNull(message = "์์ ํ๋ช
์ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String team,
+
+ @Schema(description = "์์ ํํธ", example = "ํ๋ก ํธ์๋ | ๋ฐฑ์๋")
+ @NotNull(message = "์์ ํํธ๋ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String part
+) {
+ public Member toEntity(String encodedPassword, PartType part, Team team) {
+ return Member.builder()
+ .userId(userId)
+ .password(encodedPassword)
+ .role("ROLE_USER")
+ .part(part)
+ .name(name)
+ .team(team)
+ .build();
+ }
+}
+
diff --git a/src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java b/src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java
new file mode 100644
index 0000000..8f0c19e
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java
@@ -0,0 +1,28 @@
+package ceos.vote.domain.member.dto.response;
+
+import ceos.vote.domain.member.entity.Member;
+import lombok.Builder;
+
+@Builder
+public record MemberResponseDto (
+
+ String userId,
+ String name,
+ String team,
+ String part,
+ Boolean voteBack,
+ Boolean voteFront,
+ Boolean voteTeam
+) {
+ public static MemberResponseDto from(Member member) {
+ return MemberResponseDto.builder()
+ .userId(member.getUserId())
+ .name(member.getName())
+ .team(member.getTeam().getDescription())
+ .part(member.getPart().getDescription())
+ .voteBack(member.isVoteBack())
+ .voteFront(member.isVoteFront())
+ .voteTeam(member.isVoteTeam())
+ .build();
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/entity/Member.java b/src/main/java/ceos/vote/domain/member/entity/Member.java
new file mode 100644
index 0000000..a394b1b
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/entity/Member.java
@@ -0,0 +1,72 @@
+package ceos.vote.domain.member.entity;
+
+import ceos.vote.domain.team.entity.Team;
+import ceos.vote.global.common.BaseEntity;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Member extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "member_id", nullable = false)
+ private Long id;
+
+ // ๋ก๊ทธ์ธ ์์ด๋
+ @Column(name = "user_id", length = 50, nullable = false)
+ private String userId;
+
+ @Column(nullable = false)
+ private String password;
+
+ private String role;
+
+ @Enumerated(EnumType.STRING)
+ @Column(length = 50, nullable = false)
+ private PartType part;
+
+ @Column(length = 20, nullable = false)
+ private String name;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "team_id")
+ private Team team;
+
+ @Column(name = "vote_back", nullable = false)
+ private boolean voteBack = false;
+
+ @Column(name = "vote_front", nullable = false)
+ private boolean voteFront = false;
+
+ @Column(name = "vote_team", nullable = false)
+ private boolean voteTeam = false;
+
+ public void voteToBack() {
+ this.voteBack = true;
+ }
+
+ public void voteToFront() {
+ this.voteFront = true;
+ }
+
+ public void voteToTeam() {
+ this.voteTeam = true;
+ }
+
+ @Builder
+ public Member(String userId, String password, String role, PartType part, String name, Team team) {
+
+ this.userId = userId;
+ this.password = password;
+ this.role = role;
+ this.part = part;
+ this.name = name;
+ this.team = team;
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/entity/PartType.java b/src/main/java/ceos/vote/domain/member/entity/PartType.java
new file mode 100644
index 0000000..a5db4e6
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/entity/PartType.java
@@ -0,0 +1,18 @@
+package ceos.vote.domain.member.entity;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public enum PartType {
+
+ BACKEND("๋ฐฑ์๋"),
+ FRONTEND("ํ๋ก ํธ์๋");
+
+ private final String description;
+
+ PartType(String description) {
+ this.description = description;
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/entity/Refresh.java b/src/main/java/ceos/vote/domain/member/entity/Refresh.java
new file mode 100644
index 0000000..70546cf
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/entity/Refresh.java
@@ -0,0 +1,34 @@
+package ceos.vote.domain.member.entity;
+
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Refresh {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "refresh_id", nullable = false)
+ private Long id;
+
+ @Column(length = 50, nullable = false)
+ private String userId;
+
+ @Column(nullable = false)
+ private String refresh;
+
+ @Column(nullable = false)
+ private String expiration;
+
+ @Builder
+ public Refresh(String userId, String refresh, String expiration) {
+ this.userId = userId;
+ this.refresh = refresh;
+ this.expiration = expiration;
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/entity/TeamType.java b/src/main/java/ceos/vote/domain/member/entity/TeamType.java
new file mode 100644
index 0000000..e035a78
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/entity/TeamType.java
@@ -0,0 +1,21 @@
+package ceos.vote.domain.member.entity;
+
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+public enum TeamType {
+
+ PHOTOGROUND("ํฌํ ๊ทธ๋ผ์ด๋"),
+ ANGELBRIDGE("์์ ค๋ธ๋ฆฟ์ง"),
+ PEDALGENIE("ํ๋ฌ์ง๋"),
+ CAKEWAY("์ผ์ดํฌ์จ์ด"),
+ COFFEEDEAL("์ปคํผ๋");
+
+ private final String description;
+
+ TeamType(String description) {
+ this.description = description;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ceos/vote/domain/member/service/AuthService.java b/src/main/java/ceos/vote/domain/member/service/AuthService.java
new file mode 100644
index 0000000..7f2f2af
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/service/AuthService.java
@@ -0,0 +1,197 @@
+package ceos.vote.domain.member.service;
+
+import ceos.vote.domain.member.dto.request.SignupRequestDto;
+import ceos.vote.domain.member.dto.response.MemberResponseDto;
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.domain.member.entity.PartType;
+import ceos.vote.domain.member.entity.Refresh;
+import ceos.vote.domain.member.entity.TeamType;
+import ceos.vote.domain.team.entity.Team;
+import ceos.vote.global.exception.ApplicationException;
+import ceos.vote.global.jwt.JWTUtil;
+import ceos.vote.global.repository.MemberRepository;
+import ceos.vote.global.repository.RefreshRepository;
+import ceos.vote.global.repository.TeamRepository;
+import io.jsonwebtoken.ExpiredJwtException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+
+import static ceos.vote.global.exception.ExceptionCode.*;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class AuthService {
+
+ private final MemberRepository memberRepository;
+ private final BCryptPasswordEncoder bCryptPasswordEncoder;
+ private final JWTUtil jwtUtil;
+ private final RefreshRepository refreshRepository;
+ private final TeamRepository teamRepository;
+
+ // [POST] ํ์๊ฐ์
+ @Transactional
+ public MemberResponseDto signup(SignupRequestDto request) {
+
+ String userId = request.userId();
+ String password = request.password();
+
+ if(memberRepository.existsMemberByUserId(userId)){
+ throw new ApplicationException(DUPLICATED_USER_ID);
+ }
+
+ PartType part;
+ TeamType team;
+
+ if (request.part().equals("ํ๋ก ํธ์๋")) {
+ part = PartType.FRONTEND;
+ } else if (request.part().equals("๋ฐฑ์๋")) {
+ part = PartType.BACKEND;
+ } else {
+ throw new ApplicationException(INVALID_PART_TYPE);
+ }
+
+ if (request.team().equals("ํฌํ ๊ทธ๋ผ์ด๋")) {
+ team = TeamType.PHOTOGROUND;
+ } else if (request.team().equals("์์ ค๋ธ๋ฆฟ์ง")) {
+ team = TeamType.ANGELBRIDGE;
+ } else if (request.team().equals("ํ๋ฌ์ง๋")) {
+ team = TeamType.PEDALGENIE;
+ } else if (request.team().equals("์ผ์ดํฌ์จ์ด")) {
+ team = TeamType.CAKEWAY;
+ } else if (request.team().equals("์ปคํผ๋")) {
+ team = TeamType.COFFEEDEAL;
+ } else {
+ throw new ApplicationException(INVALID_TEAM_TYPE);
+ }
+
+ Team myTeam = teamRepository.findByType(team);
+ Member newMember = request.toEntity(bCryptPasswordEncoder.encode(password), part, myTeam);
+
+ memberRepository.save(newMember);
+
+ return MemberResponseDto.from(newMember);
+ }
+
+ /**
+ * Refresh Token ์ถ์ถ
+ * **/
+ public String extractRefreshToken(HttpServletRequest request) {
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals("refreshToken")) {
+ return cookie.getValue();
+ }
+ }
+ }
+ throw new ApplicationException(NOT_FOUND_REFRESH_TOKEN);
+ }
+
+ /**
+ * Refresh Token ๊ฒ์ฆ
+ * **/
+ public void validateRefreshToken(String refreshToken) {
+
+ try {
+ jwtUtil.isExpired(refreshToken);
+ } catch (ExpiredJwtException e) {
+ throw new ApplicationException(EXPIRED_PERIOD_REFRESH_TOKEN);
+ }
+
+ String category = jwtUtil.getCategory(refreshToken);
+ if (!category.equals("refresh")) {
+ throw new ApplicationException(INVALID_REFRESH_TOKEN);
+ }
+
+ Boolean isExist = refreshRepository.existsByRefresh(refreshToken);
+ if (!isExist) {
+ throw new ApplicationException(INVALID_REFRESH_TOKEN);
+ }
+ }
+
+ /**
+ * Access Token ์ฌ๋ฐ๊ธ
+ * **/
+ public String reissueAccessToken(String refreshToken) {
+
+ String userId = jwtUtil.getUsername(refreshToken);
+ String role = jwtUtil.getRole(refreshToken);
+ return jwtUtil.createJwt("access", userId, role, 1000L * 60 * 60 * 24 * 14); // 2์ฃผ (์์)
+ }
+
+ /**
+ * ์๋ก์ด Refresh Token ์์ฑ
+ * **/
+ @Transactional
+ public Cookie createRefreshTokenCookie(String refreshToken) {
+
+ String userId = jwtUtil.getUsername(refreshToken);
+ String role = jwtUtil.getRole(refreshToken);
+ String newRefresh = jwtUtil.createJwt("refresh", userId, role, 1000L * 60 * 60 * 24 * 14);
+
+ if (userId == null) {
+ throw new ApplicationException(FAIL_TO_VALIDATE_TOKEN);
+ }
+
+ deleteAndSaveNewRefreshToken(userId, newRefresh, 1000L * 60 * 60 * 24 * 14);
+
+ return createCookie("refreshToken", newRefresh);
+ }
+
+ private Cookie createCookie(String key, String value) {
+
+ Cookie cookie = new Cookie(key, value);
+ cookie.setMaxAge(60 * 60 * 24 * 14);
+ cookie.setHttpOnly(true);
+ cookie.setPath("/");
+ // cookie.setSecure(true);
+
+ return cookie;
+ }
+
+ /**
+ * ๊ธฐ์กด์ Refresh Token ์ญ์ ํ ์ Refresh Token ์ ์ฅ
+ **/
+ @Transactional
+ public void deleteAndSaveNewRefreshToken(String userId, String newRefresh, Long expiredMs) {
+
+ refreshRepository.deleteByUserId(userId);
+
+ addRefreshEntity(userId, newRefresh, expiredMs);
+ }
+
+ /**
+ * ์๋ก์ด Refresh Token ์ ์ฅํ๋ ๋ฉ์๋
+ **/
+ @Transactional
+ public void addRefreshEntity(String userId, String refresh, Long expiredMs) {
+
+ Date expirationDate = new Date(System.currentTimeMillis() + expiredMs);
+
+ Refresh refreshEntity = Refresh.builder()
+ .userId(userId)
+ .refresh(refresh)
+ .expiration(expirationDate.toString())
+ .build();
+
+ refreshRepository.save(refreshEntity);
+ }
+
+ /**
+ * ์๋ต ํค๋ ๋ฐ ์ฟ ํค ์ค์
+ * **/
+ public void setNewTokens(HttpServletResponse response, String newAccessToken, Cookie refreshCookie) {
+
+ response.setHeader("Authorization", "Bearer " + newAccessToken);
+
+ response.addCookie(refreshCookie);
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/service/CustomUserDetailsService.java b/src/main/java/ceos/vote/domain/member/service/CustomUserDetailsService.java
new file mode 100644
index 0000000..0862e56
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/service/CustomUserDetailsService.java
@@ -0,0 +1,32 @@
+package ceos.vote.domain.member.service;
+
+import ceos.vote.domain.member.dto.CustomUserDetails;
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.global.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private final MemberRepository memberRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
+
+ // DB์์ ์กฐํ
+ Optional userDataOptional = memberRepository.findByUserId(userId);
+
+ Member userData = userDataOptional.orElseThrow(() ->
+ new UsernameNotFoundException("ํด๋น ์ ์ ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค : " + userId));
+
+ // UserDetails์ ๋ด์์ returnํ๋ฉด AuthenticationManager๊ฐ ๊ฒ์ฆํจ
+ return new CustomUserDetails(userData);
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/member/service/MemberService.java b/src/main/java/ceos/vote/domain/member/service/MemberService.java
new file mode 100644
index 0000000..45c983c
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/member/service/MemberService.java
@@ -0,0 +1,31 @@
+package ceos.vote.domain.member.service;
+
+import ceos.vote.domain.member.dto.response.MemberResponseDto;
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.global.exception.ApplicationException;
+import ceos.vote.global.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import static ceos.vote.global.exception.ExceptionCode.NOT_FOUND_USER;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class MemberService {
+
+ private final MemberRepository memberRepository;
+
+ public Member findMemberById(Long memberId) {
+ return memberRepository.findById(memberId).orElseThrow(() -> new ApplicationException(NOT_FOUND_USER));
+ }
+
+ // [GET] ํ์ ๊ธฐ๋ณธ ์ ๋ณด ์กฐํ
+ public MemberResponseDto getMemberInfo(Long memberId) {
+
+ Member member = findMemberById(memberId);
+
+ return MemberResponseDto.from(member);
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/team/controller/TeamController.java b/src/main/java/ceos/vote/domain/team/controller/TeamController.java
new file mode 100644
index 0000000..08f3f5f
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/team/controller/TeamController.java
@@ -0,0 +1,25 @@
+package ceos.vote.domain.team.controller;
+
+import ceos.vote.domain.team.dto.TeamRequestDto;
+import ceos.vote.domain.team.service.TeamService;
+import ceos.vote.global.common.response.CommonResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/team")
+@RequiredArgsConstructor
+public class TeamController {
+
+ private final TeamService teamService;
+
+ @PostMapping
+ public CommonResponse createTeam(@RequestBody TeamRequestDto teamRequestDto) {
+
+ teamService.createTeam(teamRequestDto);
+ return new CommonResponse<>("ํ ์์ฑ ์ฑ๊ณต");
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/team/dto/TeamRequestDto.java b/src/main/java/ceos/vote/domain/team/dto/TeamRequestDto.java
new file mode 100644
index 0000000..158aac8
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/team/dto/TeamRequestDto.java
@@ -0,0 +1,23 @@
+package ceos.vote.domain.team.dto;
+
+import ceos.vote.domain.team.entity.Team;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+
+public record TeamRequestDto(
+ String name,
+ @Schema(description = "์์ ํ๋ช
", example = "ํฌํ ๊ทธ๋ผ์ด๋ | ์์ ค๋ธ๋ฆฟ์ง | ํ๋ฌ์ง๋ | ์ผ์ดํฌ์จ์ด | ์ปคํผ๋")
+ @NotNull(message = "์์ ํ๋ช
์ ํ์ ์
๋ ฅ ์ฌํญ์
๋๋ค.")
+ String team,
+ String description
+) {
+
+ public Team toEntity() {
+
+ return Team.builder()
+ .name(name)
+ .type(team)
+ .description(description)
+ .build();
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/team/entity/Team.java b/src/main/java/ceos/vote/domain/team/entity/Team.java
new file mode 100644
index 0000000..088f602
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/team/entity/Team.java
@@ -0,0 +1,55 @@
+package ceos.vote.domain.team.entity;
+
+import ceos.vote.domain.member.entity.TeamType;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Team {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "team_id", nullable = false)
+ private Long id;
+
+ @Column(name = "team_name", nullable = false)
+ private String teamName;
+
+ @Enumerated(EnumType.STRING)
+ @Column(length = 50, nullable = false)
+ private TeamType type;
+
+ private String description;
+
+ private int count = 0;
+
+ public void voteToMe() {
+ this.count++;
+ }
+
+ @Builder
+ public Team(String name, String type, String description) {
+
+ TeamType team = null;
+
+ if (type.equals("ํฌํ ๊ทธ๋ผ์ด๋"))
+ team = TeamType.PHOTOGROUND;
+ else if (type.equals("์์ ค๋ธ๋ฆฟ์ง"))
+ team = TeamType.ANGELBRIDGE;
+ else if (type.equals("ํ๋ฌ์ง๋"))
+ team = TeamType.PEDALGENIE;
+ else if (type.equals("์ผ์ดํฌ์จ์ด"))
+ team = TeamType.CAKEWAY;
+ else if (type.equals("์ปคํผ๋"))
+ team = TeamType.COFFEEDEAL;
+
+ this.teamName = name;
+ this.type = team;
+ this.description = description;
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/team/service/TeamService.java b/src/main/java/ceos/vote/domain/team/service/TeamService.java
new file mode 100644
index 0000000..5b4713d
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/team/service/TeamService.java
@@ -0,0 +1,22 @@
+package ceos.vote.domain.team.service;
+
+import ceos.vote.domain.team.dto.TeamRequestDto;
+import ceos.vote.domain.team.entity.Team;
+import ceos.vote.global.repository.TeamRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class TeamService {
+
+ private final TeamRepository teamRepository;
+
+ @Transactional
+ public void createTeam(TeamRequestDto teamRequestDto) {
+
+ Team team = teamRequestDto.toEntity();
+ teamRepository.save(team);
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/vote/controller/VoteController.java b/src/main/java/ceos/vote/domain/vote/controller/VoteController.java
new file mode 100644
index 0000000..6a51d02
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/vote/controller/VoteController.java
@@ -0,0 +1,73 @@
+package ceos.vote.domain.vote.controller;
+
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.domain.vote.dto.response.*;
+import ceos.vote.domain.vote.service.VoteService;
+import ceos.vote.global.annotation.Login;
+import 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;
+
+@RestController
+@RequestMapping("/api/vote")
+@RequiredArgsConstructor
+@Tag(name = "Vote", description = "ํฌํ ๊ด๋ จ API")
+public class VoteController {
+
+ private final VoteService voteService;
+
+ @GetMapping("/developer/detail/{developerId}")
+ @Operation(summary = "๊ฐ๋ฐ์ ์๊ธฐ์๊ฐ ์กฐํํ๋ API", description = "-")
+ public CommonResponse getIntroduction(@PathVariable Long developerId) {
+
+ return new CommonResponse<>(voteService.getIntroduce(developerId), "๊ฐ๋ฐ์ ์๊ธฐ์๊ฐ ์กฐํ ์๋ฃ");
+ }
+
+ @GetMapping("/developer")
+ @Operation(summary = "๊ฐ๋ฐ์ ๋ฆฌ์คํธ ์กฐํํ๋ API", description = "request param์ผ๋ก type=backend ๋๋ type=frontend ๋ฅผ ๋๊ฒจ์ฃผ์ธ์")
+ public CommonResponse> getDeveloperList(@RequestParam String type) {
+
+ return new CommonResponse<>(voteService.getDeveloperList(type), "๊ฐ๋ฐ์ ๋ฆฌ์คํธ ์กฐํ ์๋ฃ");
+ }
+
+ @GetMapping("/team")
+ @Operation(summary = "ํ ๋ฆฌ์คํธ ์กฐํํ๋ API", description = "-")
+ public CommonResponse> getDeveloperList() {
+
+ return new CommonResponse<>(voteService.getTeamList(), "ํ ๋ฆฌ์คํธ ์กฐํ ์๋ฃ");
+ }
+
+ @PostMapping("/developer/{memberId}")
+ @Operation(summary = "๊ฐ๋ฐ์ ํํธ์ฅ ํฌํํ๋ API", description = "pathvariable๋ก ํฌํํ๊ณ ์ ํ๋ ๋ฉค๋ฒ์ id๋ฅผ ๋๊ฒจ์ฃผ์ธ์")
+ public CommonResponse voteDeveloper(@PathVariable Long memberId, @Login Member loginMember) {
+
+ voteService.voteDeveloper(memberId, loginMember);
+ return new CommonResponse<>("ํํธ์ฅ ํฌํ ์๋ฃ");
+ }
+
+ @PostMapping("/team/{teamId}")
+ @Operation(summary = "ํ ํฌํํ๋ API", description = "pathvariable๋ก ํฌํํ๊ณ ์ ํ๋ ํ์ id๋ฅผ ๋๊ฒจ์ฃผ์ธ์")
+ public CommonResponse voteTeam(@PathVariable Long teamId, @Login Member loginMember) {
+
+ voteService.voteTeam(teamId, loginMember);
+ return new CommonResponse<>("ํ ํฌํ ์๋ฃ");
+ }
+
+ @GetMapping("/developer/result")
+ @Operation(summary = "ํํธ์ฅ ํฌํ ๊ฒฐ๊ณผ ์กฐํํ๋ API", description = "request param์ผ๋ก type=backend ๋๋ type=frontend ๋ฅผ ๋๊ฒจ์ฃผ์ธ์")
+ public CommonResponse> getDeveloperVoteResult(@RequestParam String type) {
+
+ return new CommonResponse<>(voteService.getDeveloperVoteResult(type), "ํํธ์ฅ ํฌํ ๊ฒฐ๊ณผ ์กฐํ ์๋ฃ");
+ }
+
+ @GetMapping("/team/result")
+ @Operation(summary = "ํ ํฌํ ๊ฒฐ๊ณผ ์กฐํํ๋ API", description = "-")
+ public CommonResponse> getTeamVoteResult() {
+
+ return new CommonResponse<>(voteService.getTeamVoteResult(), "ํ ํฌํ ๊ฒฐ๊ณผ ์กฐํ ์๋ฃ");
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperIntroductionResponseDto.java b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperIntroductionResponseDto.java
new file mode 100644
index 0000000..39637c8
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperIntroductionResponseDto.java
@@ -0,0 +1,10 @@
+package ceos.vote.domain.vote.dto.response;
+
+import lombok.Builder;
+
+@Builder
+public record DeveloperIntroductionResponseDto(
+ String name,
+ String introduction
+) {
+}
diff --git a/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java
new file mode 100644
index 0000000..9d91117
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java
@@ -0,0 +1,20 @@
+package ceos.vote.domain.vote.dto.response;
+
+import ceos.vote.domain.developer.entity.Developer;
+import lombok.Builder;
+
+@Builder
+public record DeveloperListResponseDto(
+ Long developerId,
+ String developerName,
+ String teamName
+) {
+
+ public static DeveloperListResponseDto from (Developer developer) {
+ return DeveloperListResponseDto.builder()
+ .developerId(developer.getId())
+ .developerName(developer.getDeveloperName())
+ .teamName(developer.getTeam().getTeamName())
+ .build();
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java
new file mode 100644
index 0000000..f96462a
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java
@@ -0,0 +1,19 @@
+package ceos.vote.domain.vote.dto.response;
+
+import ceos.vote.domain.developer.entity.Developer;
+import lombok.Builder;
+
+@Builder
+public record DeveloperVoteResultResponseDto(
+ String developerName,
+ String teamName,
+ int count
+) {
+ public static DeveloperVoteResultResponseDto from(Developer developer) {
+ return DeveloperVoteResultResponseDto.builder()
+ .developerName(developer.getDeveloperName())
+ .teamName(developer.getTeam().getTeamName())
+ .count(developer.getCount())
+ .build();
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/vote/dto/response/TeamListResponseDto.java b/src/main/java/ceos/vote/domain/vote/dto/response/TeamListResponseDto.java
new file mode 100644
index 0000000..e79fbf6
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/vote/dto/response/TeamListResponseDto.java
@@ -0,0 +1,21 @@
+package ceos.vote.domain.vote.dto.response;
+
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.domain.team.entity.Team;
+import lombok.Builder;
+
+@Builder
+public record TeamListResponseDto(
+ Long teamId,
+ String teamName,
+ String description
+) {
+
+ public static TeamListResponseDto from (Team team) {
+ return TeamListResponseDto.builder()
+ .teamId(team.getId())
+ .teamName(team.getTeamName())
+ .description(team.getDescription())
+ .build();
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/vote/dto/response/TeamVoteResultResponseDto.java b/src/main/java/ceos/vote/domain/vote/dto/response/TeamVoteResultResponseDto.java
new file mode 100644
index 0000000..0b2d719
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/vote/dto/response/TeamVoteResultResponseDto.java
@@ -0,0 +1,19 @@
+package ceos.vote.domain.vote.dto.response;
+
+import ceos.vote.domain.team.entity.Team;
+import lombok.Builder;
+
+@Builder
+public record TeamVoteResultResponseDto(
+ String teamName,
+ String description,
+ int count
+) {
+ public static TeamVoteResultResponseDto from (Team team) {
+ return TeamVoteResultResponseDto.builder()
+ .teamName(team.getTeamName())
+ .description(team.getDescription())
+ .count(team.getCount())
+ .build();
+ }
+}
diff --git a/src/main/java/ceos/vote/domain/vote/service/VoteService.java b/src/main/java/ceos/vote/domain/vote/service/VoteService.java
new file mode 100644
index 0000000..af0da8a
--- /dev/null
+++ b/src/main/java/ceos/vote/domain/vote/service/VoteService.java
@@ -0,0 +1,130 @@
+package ceos.vote.domain.vote.service;
+
+import ceos.vote.domain.developer.entity.Developer;
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.domain.member.entity.PartType;
+import ceos.vote.domain.team.entity.Team;
+import ceos.vote.domain.vote.dto.response.*;
+import ceos.vote.global.exception.ApplicationException;
+import ceos.vote.global.repository.DeveloperRepository;
+import ceos.vote.global.repository.MemberRepository;
+import ceos.vote.global.repository.TeamRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static ceos.vote.global.exception.ExceptionCode.*;
+
+@Service
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class VoteService {
+
+ private final TeamRepository teamRepository;
+ private final MemberRepository memberRepository;
+ private final DeveloperRepository developerRepository;
+
+ // [GET] ๊ฐ๋ฐ์ ์๊ธฐ์๊ฐ ์กฐํ
+ public DeveloperIntroductionResponseDto getIntroduce(Long developerId) {
+
+ Developer developer = developerRepository.findById(developerId)
+ .orElseThrow(() -> new ApplicationException(NOT_FOUND_USER));
+
+ return DeveloperIntroductionResponseDto.builder()
+ .name(developer.getDeveloperName())
+ .introduction(developer.getIntroduction())
+ .build();
+ }
+
+ // [GET] ๊ฐ๋ฐ์ ๋ฆฌ์คํธ ์กฐํ
+ public List getDeveloperList(String type) {
+
+ PartType partType = PartType.valueOf(type.toUpperCase());
+ List developers = developerRepository.findByPartOrderByCountDesc(partType);
+
+ return developers.stream()
+ .map(DeveloperListResponseDto::from)
+ .collect(Collectors.toList());
+ }
+
+ // [GET] ํ ๋ฆฌ์คํธ ์กฐํ
+ public List getTeamList() {
+
+ List teams = teamRepository.findAll();
+ return teams.stream()
+ .map(TeamListResponseDto::from)
+ .collect(Collectors.toList());
+
+ }
+
+ // [POST] ๊ฐ๋ฐ์ ํํธ์ฅ ํฌํ
+ @Transactional
+ public void voteDeveloper(Long developerId, Member loginMember) {
+
+ Developer developer = developerRepository.findById(developerId)
+ .orElseThrow(() -> new ApplicationException(NOT_FOUND_USER));
+ // ๋ ์์ ์๊ฒ ํฌํํ ์ ์์
+ if (Objects.equals(developer.getDeveloperName(), loginMember.getName()))
+ throw new ApplicationException(BAD_REQUEST_SELF);
+ // ๊ฐ์ ํํธ์๊ฒ๋ง ํฌํํ ์ ์์
+ if (developer.getPart() != loginMember.getPart())
+ throw new ApplicationException(BAD_REQUEST_DEVELOPER);
+ // ์ด๋ฏธ ํฌํํ๋ค๋ฉด ๋ค์ ํฌํํ ์ ์์
+ PartType type = developer.getPart();
+ if (type.equals(PartType.BACKEND)) {
+ if (loginMember.isVoteBack())
+ throw new ApplicationException(ALREADY_VOTE_DEVELOPER);
+ loginMember.voteToBack();
+ }
+ else if (type.equals(PartType.FRONTEND)) {
+ if (loginMember.isVoteFront())
+ throw new ApplicationException(ALREADY_VOTE_DEVELOPER);
+ loginMember.voteToFront();
+ }
+ developer.voteToMe();
+ developerRepository.save(developer);
+ memberRepository.save(loginMember);
+ }
+
+ // [POST] ํ ํฌํ
+ @Transactional
+ public void voteTeam(Long teamId, Member loginMember) {
+
+ Team team = teamRepository.findById(teamId)
+ .orElseThrow(() -> new ApplicationException(INVALID_TEAM_TYPE));
+ // ๊ฐ์ ํ์๊ฒ ํฌํํ ์ ์์
+ if (loginMember.getTeam() == team)
+ throw new ApplicationException(BAD_REQUEST_TEAM);
+ // ์ด๋ฏธ ํฌํํ์ผ๋ฉด ๋ค์ ํฌํํ ์ ์์
+ if (loginMember.isVoteTeam())
+ throw new ApplicationException(ALREADY_VOTE_TEAM);
+
+ team.voteToMe();
+ teamRepository.save(team);
+ loginMember.voteToTeam();
+ memberRepository.save(loginMember);
+ }
+
+ // [GET] ๊ฐ๋ฐ์ ํํธ์ฅ ํฌํ ๊ฒฐ๊ณผ ์กฐํ
+ public List getDeveloperVoteResult(String type) {
+ PartType partType = PartType.valueOf(type.toUpperCase());
+ List developers = developerRepository.findByPartOrderByCountDesc(partType);
+
+ return developers.stream()
+ .map(DeveloperVoteResultResponseDto::from)
+ .collect(Collectors.toList());
+ }
+
+ // [GET] ํ ํฌํ ๊ฒฐ๊ณผ ์กฐํ
+ public List getTeamVoteResult() {
+
+ List teams = teamRepository.findAllByOrderByCountDesc();
+ return teams.stream()
+ .map(TeamVoteResultResponseDto::from)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/ceos/vote/global/annotation/Login.java b/src/main/java/ceos/vote/global/annotation/Login.java
new file mode 100644
index 0000000..9c1a700
--- /dev/null
+++ b/src/main/java/ceos/vote/global/annotation/Login.java
@@ -0,0 +1,11 @@
+package ceos.vote.global.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Login {
+}
diff --git a/src/main/java/ceos/vote/global/annotation/argumentresolver/LoginArgumentResolver.java b/src/main/java/ceos/vote/global/annotation/argumentresolver/LoginArgumentResolver.java
new file mode 100644
index 0000000..769c770
--- /dev/null
+++ b/src/main/java/ceos/vote/global/annotation/argumentresolver/LoginArgumentResolver.java
@@ -0,0 +1,45 @@
+package ceos.vote.global.annotation.argumentresolver;
+
+import ceos.vote.domain.member.dto.CustomUserDetails;
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.global.annotation.Login;
+import ceos.vote.global.repository.MemberRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.MethodParameter;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+@Component
+public class LoginArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private final MemberRepository memberRepository;
+
+ @Autowired
+ public LoginArgumentResolver(MemberRepository memberRepository) {
+ this.memberRepository = memberRepository;
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+
+ boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
+ boolean isMemberType = parameter.getParameterType().equals(Member.class);
+ return hasLoginAnnotation && isMemberType;
+ }
+
+ @Override
+ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
+ NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
+
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
+
+ Long memberId = userDetails.getMemberId();
+ return memberRepository.findById(memberId).orElse(null);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ceos/vote/global/common/BaseEntity.java b/src/main/java/ceos/vote/global/common/BaseEntity.java
new file mode 100644
index 0000000..eff8df6
--- /dev/null
+++ b/src/main/java/ceos/vote/global/common/BaseEntity.java
@@ -0,0 +1,28 @@
+package ceos.vote.global.common;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import lombok.Getter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@Getter
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class BaseEntity {
+
+ @CreatedDate
+ @Column(name = "created_at", columnDefinition = "TIMESTAMP")
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ @Column(name = "updated_at", columnDefinition = "TIMESTAMP")
+ private LocalDateTime updatedAt;
+
+ @Column(name = "deleted_at", columnDefinition = "TIMESTAMP")
+ private LocalDateTime deletedAt;
+}
diff --git a/src/main/java/ceos/vote/global/common/response/CommonResponse.java b/src/main/java/ceos/vote/global/common/response/CommonResponse.java
new file mode 100644
index 0000000..5db6775
--- /dev/null
+++ b/src/main/java/ceos/vote/global/common/response/CommonResponse.java
@@ -0,0 +1,34 @@
+package ceos.vote.global.common.response;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+public class CommonResponse {
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime timestamp;
+ private int code;
+ private String message;
+ private T result;
+
+ // ๋ฐํ๊ฐ ์๋ ๊ฒฝ์ฐ
+ public CommonResponse(T result, String message) {
+ this.timestamp = LocalDateTime.now();
+ this.code = SuccessCode.SUCCESS.getCode();
+ this.message = message;
+ this.result = result;
+ }
+
+ // ๋ฐํ๊ฐ ์๋ ๊ฒฝ์ฐ
+ public CommonResponse(String message) {
+ this.timestamp = LocalDateTime.now();
+ this.code = SuccessCode.SUCCESS.getCode();
+ this.message = message;
+ }
+}
diff --git a/src/main/java/ceos/vote/global/common/response/SuccessCode.java b/src/main/java/ceos/vote/global/common/response/SuccessCode.java
new file mode 100644
index 0000000..b33482e
--- /dev/null
+++ b/src/main/java/ceos/vote/global/common/response/SuccessCode.java
@@ -0,0 +1,20 @@
+package ceos.vote.global.common.response;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public enum SuccessCode {
+
+ SUCCESS(HttpStatus.OK,1000, "์์ฒญ์ ์ฑ๊ณตํ์์ต๋๋ค.");
+
+ private HttpStatus httpStatus;
+ private int code;
+ private String message;
+
+ SuccessCode(HttpStatus httpStatus, int code, String message) {
+ this.httpStatus = httpStatus;
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/src/main/java/ceos/vote/global/config/CorsMvcConfig.java b/src/main/java/ceos/vote/global/config/CorsMvcConfig.java
new file mode 100644
index 0000000..9ce01d1
--- /dev/null
+++ b/src/main/java/ceos/vote/global/config/CorsMvcConfig.java
@@ -0,0 +1,19 @@
+package 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 registry) {
+
+ registry.addMapping("/**")
+ .allowedOriginPatterns("*")
+ .allowCredentials(true)
+ .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
+ .allowedHeaders("*");
+ }
+}
diff --git a/src/main/java/ceos/vote/global/config/SecurityConfig.java b/src/main/java/ceos/vote/global/config/SecurityConfig.java
new file mode 100644
index 0000000..112f515
--- /dev/null
+++ b/src/main/java/ceos/vote/global/config/SecurityConfig.java
@@ -0,0 +1,112 @@
+package ceos.vote.global.config;
+
+import ceos.vote.global.jwt.CustomLogoutFilter;
+import ceos.vote.global.jwt.JWTFilter;
+import ceos.vote.global.jwt.JWTUtil;
+import ceos.vote.global.jwt.LoginFilter;
+import ceos.vote.global.repository.MemberRepository;
+import ceos.vote.global.repository.RefreshRepository;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final AuthenticationConfiguration authenticationConfiguration;
+ private final JWTUtil jwtUtil;
+ private final RefreshRepository refreshRepository;
+ private final MemberRepository memberRepository;
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
+
+ return configuration.getAuthenticationManager();
+ }
+
+ @Bean
+ public BCryptPasswordEncoder bCryptPasswordEncoder() {
+
+ return new BCryptPasswordEncoder();
+ }
+
+ @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(Arrays.asList(
+ "http://localhost:3000",
+ "http://localhost:8080",
+ "http://3.35.91.98:8080",
+ "https://3.35.91.98",
+ "https://angelbridge-vote-rho.vercel.app"
+ ));
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
+
+
+ configuration.setAllowedMethods(Collections.singletonList("*"));
+ configuration.setAllowCredentials(true);
+ configuration.setAllowedHeaders(Collections.singletonList("*"));
+ configuration.setMaxAge(3600L);
+
+ configuration.setExposedHeaders(Arrays.asList("Set-Cookie", "Authorization"));
+
+ return configuration;
+ }
+ })));
+
+ http
+ .csrf((auth) -> auth.disable());
+
+ http
+ .formLogin((auth) -> auth.disable());
+
+ http
+ .httpBasic((auth) -> auth.disable());
+
+ http
+ .authorizeHttpRequests((auth) -> auth
+ .requestMatchers("/login", "/", "/api/auth/**", "/swagger-ui.html", "/swagger-ui/**","/v3/api-docs/**").permitAll()
+ .requestMatchers("/api/vote/developer", "/api/vote/team", "/api/team", "/api/developer", "/api/vote/developer/detail/**").permitAll() // ๊ฐ๋ฐ์, ํ ์กฐํํ๋๊ฑด ํ์ฉ
+ .anyRequest().authenticated()
+ );
+
+ http
+ .addFilterBefore(new JWTFilter(jwtUtil, memberRepository), LoginFilter.class);
+ http
+ .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository), UsernamePasswordAuthenticationFilter.class);
+
+ http
+ .addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class);
+
+ http
+ .sessionManagement((session) -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+
+ return http.build();
+ }
+}
diff --git a/src/main/java/ceos/vote/global/config/SwaggerConfig.java b/src/main/java/ceos/vote/global/config/SwaggerConfig.java
new file mode 100644
index 0000000..9e6a56d
--- /dev/null
+++ b/src/main/java/ceos/vote/global/config/SwaggerConfig.java
@@ -0,0 +1,38 @@
+package ceos.vote.global.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+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
+public class SwaggerConfig {
+ @Bean
+ public OpenAPI openAPI() {
+ String jwt = "JWT";
+
+ // Security Requirement์ Security Scheme ์ค์
+ SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt);
+ Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme()
+ .name(jwt)
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")
+ );
+
+ // OpenAPI ๊ฐ์ฒด ๋ฐํ
+ return new OpenAPI()
+ .components(components)
+ .info(apiInfo())
+ .addSecurityItem(securityRequirement);
+ }
+
+ private Info apiInfo() {
+ return new Info()
+ .title("CEOS ํฌํ ์ฌ์ดํธ API") // Swagger ๋ฉ์ธ ํ์ดํ
+ .description("Ceos ํฌํ ์ฌ์ดํธ ํ-๋ฐฑ ๊ณผ์ API"); // Swagger ์ค๋ช
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ceos/vote/global/config/WebConfig.java b/src/main/java/ceos/vote/global/config/WebConfig.java
new file mode 100644
index 0000000..2f31ceb
--- /dev/null
+++ b/src/main/java/ceos/vote/global/config/WebConfig.java
@@ -0,0 +1,25 @@
+package ceos.vote.global.config;
+
+import ceos.vote.global.annotation.argumentresolver.LoginArgumentResolver;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+ private final LoginArgumentResolver loginArgumentResolver;
+
+ @Autowired
+ public WebConfig(LoginArgumentResolver loginArgumentResolver) {
+ this.loginArgumentResolver = loginArgumentResolver;
+ }
+
+ @Override
+ public void addArgumentResolvers(List resolvers) {
+ resolvers.add(loginArgumentResolver);
+ }
+}
diff --git a/src/main/java/ceos/vote/global/exception/ApplicationException.java b/src/main/java/ceos/vote/global/exception/ApplicationException.java
new file mode 100644
index 0000000..4134e87
--- /dev/null
+++ b/src/main/java/ceos/vote/global/exception/ApplicationException.java
@@ -0,0 +1,11 @@
+package 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/ceos/vote/global/exception/ExceptionCode.java b/src/main/java/ceos/vote/global/exception/ExceptionCode.java
new file mode 100644
index 0000000..bb98ed4
--- /dev/null
+++ b/src/main/java/ceos/vote/global/exception/ExceptionCode.java
@@ -0,0 +1,49 @@
+package 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, "์๋ชป๋ ์์ฒญ์
๋๋ค."),
+ BAD_REQUEST_TEAM(HttpStatus.BAD_REQUEST, 2009, "๊ฐ์ ํ์๊ฒ ํฌํํ ์ ์์ต๋๋ค."),
+ ALREADY_VOTE_TEAM(HttpStatus.BAD_REQUEST, 2010, "์ด๋ฏธ ๋ค๋ฅธ ํ์๊ฒ ํฌํํ์ต๋๋ค."),
+ BAD_REQUEST_DEVELOPER(HttpStatus.BAD_REQUEST, 2011, "๊ฐ์ ํํธ์๊ฒ๋ง ํฌํํ ์ ์์ต๋๋ค."),
+ ALREADY_VOTE_DEVELOPER(HttpStatus.BAD_REQUEST, 2012, "์ด๋ฏธ ๋ค๋ฅธ ๊ฐ๋ฐ์์๊ฒ ํฌํํ์ต๋๋ค."),
+ BAD_REQUEST_SELF(HttpStatus.BAD_REQUEST, 2013, "๋ ์์ ์๊ฒ ํฌํํ ์ ์์ต๋๋ค."),
+
+ // 3000: Auth Error
+ KAKAO_TOKEN_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, 3000, "ํ ํฐ ๋ฐ๊ธ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."),
+ KAKAO_USER_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, 3001, "Kakao ํ๋กํ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ๊ณผ์ ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."),
+ WRONG_TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, 3002, "์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค."),
+ INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, 3003,"์ฌ๋ฐ๋ฅด์ง ์์ ํ์์ RefreshToken ์
๋๋ค."),
+ INVALID_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, 3004,"์ฌ๋ฐ๋ฅด์ง ์์ ํ์์ AccessToken ์
๋๋ค."),
+ DUPLICATED_USER_ID(HttpStatus.BAD_REQUEST, 3005,"์ค๋ณต๋ ์ฌ์ฉ์ ์์ด๋์
๋๋ค."),
+ DUPLICATED_ADMIN_EMAIL(HttpStatus.BAD_REQUEST, 3006,"์ค๋ณต๋ ์ฌ์ฉ์ ์ด๋ฉ์ผ์
๋๋ค."),
+ NOT_FOUND_REFRESH_TOKEN(HttpStatus.NOT_FOUND, 3007,"์กด์ฌํ์ง ์๋ RefreshToken ์
๋๋ค."),
+ EXPIRED_PERIOD_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, 3008,"๊ธฐํ์ด ๋ง๋ฃ๋ RefreshToken ์
๋๋ค."),
+ EXPIRED_PERIOD_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, 3009,"๊ธฐํ์ด ๋ง๋ฃ๋ AccessToken ์
๋๋ค."),
+ NOT_FOUND_REFRESH_TOKEN_IN_DB(HttpStatus.NOT_FOUND, 3010,"ํ์ฌ DB์ ์กด์ฌํ์ง ์๋ RefreshToken ์
๋๋ค."),
+ NOT_FOUND_USER(HttpStatus.NOT_FOUND, 3011,"์กด์ฌํ์ง ์๋ ์ฌ์ฉ์์
๋๋ค."),
+ INVALID_PART_TYPE(HttpStatus.BAD_REQUEST, 3012,"์ฌ๋ฐ๋ฅด์ง ์์ ํ์์ ์์ ํํธ์
๋๋ค."),
+ INVALID_TEAM_TYPE(HttpStatus.BAD_REQUEST, 3013,"์ฌ๋ฐ๋ฅด์ง ์์ ํ์์ ์์ ํ๋ช
์
๋๋ค."),
+ FAIL_TO_VALIDATE_TOKEN(HttpStatus.INTERNAL_SERVER_ERROR, 3014, "ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.");
+
+ private final HttpStatus httpStatus;
+ private final int code;
+ private final String message;
+}
diff --git a/src/main/java/ceos/vote/global/exception/ExceptionResponse.java b/src/main/java/ceos/vote/global/exception/ExceptionResponse.java
new file mode 100644
index 0000000..e92f7e2
--- /dev/null
+++ b/src/main/java/ceos/vote/global/exception/ExceptionResponse.java
@@ -0,0 +1,28 @@
+package ceos.vote.global.exception;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+import java.time.LocalDateTime;
+
+public record ExceptionResponse (
+
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
+ 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/ceos/vote/global/exception/GlobalExceptionHandler.java b/src/main/java/ceos/vote/global/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..06ff1d0
--- /dev/null
+++ b/src/main/java/ceos/vote/global/exception/GlobalExceptionHandler.java
@@ -0,0 +1,47 @@
+package 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;
+
+import static ceos.vote.global.exception.ExceptionCode.INTERNAL_SERVER_ERROR;
+
+@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(INTERNAL_SERVER_ERROR));
+ }
+}
diff --git a/src/main/java/ceos/vote/global/jwt/CustomLogoutFilter.java b/src/main/java/ceos/vote/global/jwt/CustomLogoutFilter.java
new file mode 100644
index 0000000..c34fb27
--- /dev/null
+++ b/src/main/java/ceos/vote/global/jwt/CustomLogoutFilter.java
@@ -0,0 +1,99 @@
+package ceos.vote.global.jwt;
+
+import ceos.vote.global.exception.ApplicationException;
+import ceos.vote.global.repository.RefreshRepository;
+import io.jsonwebtoken.ExpiredJwtException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+import static ceos.vote.global.exception.ExceptionCode.*;
+
+@RequiredArgsConstructor
+public class CustomLogoutFilter extends GenericFilterBean {
+
+ private final JWTUtil jwtUtil;
+ private final RefreshRepository refreshRepository;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+
+ doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
+ }
+
+ private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+
+ String requestUri = request.getRequestURI();
+ if (!requestUri.matches("^\\/logout$")) {
+
+ filterChain.doFilter(request, response);
+ return;
+ }
+ String requestMethod = request.getMethod();
+ if (!requestMethod.equals("POST")) {
+
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String refresh = null;
+ Cookie[] cookies = request.getCookies();
+ for (Cookie cookie : cookies) {
+
+ if (cookie.getName().equals("refreshToken")) {
+
+ refresh = cookie.getValue();
+ }
+ }
+
+ if (refresh == null) {
+
+ throw new ApplicationException(NOT_FOUND_REFRESH_TOKEN);
+ }
+
+ try {
+ jwtUtil.isExpired(refresh);
+ } catch (ExpiredJwtException e) {
+
+ throw new ApplicationException(EXPIRED_PERIOD_REFRESH_TOKEN);
+ }
+
+ String category = jwtUtil.getCategory(refresh);
+ if (!category.equals("refresh")) {
+
+ throw new ApplicationException(INVALID_REFRESH_TOKEN);
+ }
+
+ Boolean isExist = refreshRepository.existsByRefresh(refresh);
+ if (!isExist) {
+
+ throw new ApplicationException(NOT_FOUND_REFRESH_TOKEN_IN_DB);
+ }
+
+ /**
+ * [ ๋ก๊ทธ์์ ์งํ ]
+ * **/
+
+ // Refresh ํ ํฐ DB์์ ์ ๊ฑฐ
+ refreshRepository.deleteByRefresh(refresh);
+
+ //Refresh ํ ํฐ Cookie ๊ฐ 0
+ Cookie cookie = new Cookie("refreshToken", null);
+ cookie.setMaxAge(0);
+ cookie.setPath("/");
+
+ response.addCookie(cookie);
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.setContentType("application/json");
+ response.setCharacterEncoding("UTF-8");
+ response.getWriter().write("{\"result\": \"๋ก๊ทธ์์์ด ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋์์ต๋๋ค.\"}");
+ }
+}
diff --git a/src/main/java/ceos/vote/global/jwt/JWTFilter.java b/src/main/java/ceos/vote/global/jwt/JWTFilter.java
new file mode 100644
index 0000000..098dff6
--- /dev/null
+++ b/src/main/java/ceos/vote/global/jwt/JWTFilter.java
@@ -0,0 +1,74 @@
+package ceos.vote.global.jwt;
+
+import ceos.vote.domain.member.dto.CustomUserDetails;
+import ceos.vote.domain.member.entity.Member;
+import ceos.vote.global.repository.MemberRepository;
+import io.jsonwebtoken.ExpiredJwtException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@RequiredArgsConstructor
+public class JWTFilter extends OncePerRequestFilter {
+
+ private final JWTUtil jwtUtil;
+ private final MemberRepository memberRepository;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+
+ String header = request.getHeader("Authorization");
+ if (header == null || !header.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String accessToken = header.substring(7); // "Bearer " ์ ๊ฑฐ ํ ํ ํฐ๋ง ์ถ์ถ
+
+ // ํ ํฐ ๋ง๋ฃ ์ฌ๋ถ ํ์ธ, ๋ง๋ฃ ์ ๋ค์ ํํฐ๋ก ๋๊ธฐ์ง ์์
+ try {
+ jwtUtil.isExpired(accessToken);
+ } catch (ExpiredJwtException e) {
+
+ PrintWriter writer = response.getWriter();
+ writer.print("AccessToken์ด ๋ง๋ฃ๋์์ต๋๋ค.");
+
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+
+ // ํ ํฐ์ด access ์ธ์ง ํ์ธ (๋ฐ๊ธ์ ํ์ด๋ก๋์ ๋ช
์)
+ String category = jwtUtil.getCategory(accessToken);
+
+ if (!category.equals("access")) {
+
+ PrintWriter writer = response.getWriter();
+ writer.print("์ ํจํ์ง ์์ AccessToken ์
๋๋ค.");
+
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ }
+
+ String userId = jwtUtil.getUsername(accessToken);
+
+ Member member = memberRepository.findByUserId(userId)
+ .orElseThrow(() -> new UsernameNotFoundException("ํด๋น ์ ์ ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."));
+
+ CustomUserDetails customUserDetails = new CustomUserDetails(member);
+
+ Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/ceos/vote/global/jwt/JWTUtil.java b/src/main/java/ceos/vote/global/jwt/JWTUtil.java
new file mode 100644
index 0000000..fee6ccf
--- /dev/null
+++ b/src/main/java/ceos/vote/global/jwt/JWTUtil.java
@@ -0,0 +1,53 @@
+package ceos.vote.global.jwt;
+
+import io.jsonwebtoken.Jwts;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+@Component
+public class JWTUtil {
+
+ private SecretKey secretKey;
+
+ public JWTUtil(@Value("${spring.jwt.secret}") String secret) {
+
+ secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
+ }
+
+ public String getUsername(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("userId", String.class);
+ }
+
+ public String getRole(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
+ }
+
+ public String getCategory(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("category", String.class);
+ }
+
+ public Boolean isExpired(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
+ }
+
+ public String createJwt(String category, String userId, String role, Long expiredMs) {
+
+ return Jwts.builder()
+ .claim("category", category)
+ .claim("userId", userId)
+ .claim("role", role)
+ .issuedAt(new Date(System.currentTimeMillis())) // ๋ฐ๊ธ ์๊ฐ
+ .expiration(new Date(System.currentTimeMillis() + expiredMs)) // ๋ง๋ฃ ์๊ฐ
+ .signWith(secretKey)
+ .compact();
+ }
+}
diff --git a/src/main/java/ceos/vote/global/jwt/LoginFilter.java b/src/main/java/ceos/vote/global/jwt/LoginFilter.java
new file mode 100644
index 0000000..75a71e5
--- /dev/null
+++ b/src/main/java/ceos/vote/global/jwt/LoginFilter.java
@@ -0,0 +1,120 @@
+package ceos.vote.global.jwt;
+
+import ceos.vote.domain.member.dto.request.LoginRequestDto;
+import ceos.vote.domain.member.entity.Refresh;
+import ceos.vote.global.repository.RefreshRepository;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+
+@RequiredArgsConstructor
+public class LoginFilter extends UsernamePasswordAuthenticationFilter {
+
+ private final AuthenticationManager authenticationManager;
+ private final JWTUtil jwtUtil;
+ private final RefreshRepository refreshRepository;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+
+ LoginRequestDto loginRequestDto;
+
+ try {
+ // JSON ์์ฒญ ๋ณธ๋ฌธ์ ์ฝ์ด LoginRequestDto ๊ฐ์ฒด๋ก ๋ณํ
+ ServletInputStream inputStream = request.getInputStream();
+ String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
+ loginRequestDto = objectMapper.readValue(messageBody, LoginRequestDto.class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ String userId = loginRequestDto.userId();
+ String password = loginRequestDto.password();
+
+ // ์คํ๋ง ์ํ๋ฆฌํฐ์์ userId์ password๋ฅผ ๊ฒ์ฆํ๊ธฐ ์ํด์๋ token(dto)์ ๋ด์์ผ ํจ
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userId, password, null);
+
+ // token์ ๋ด์ ๊ฐ๋ค์ ๊ฒ์ฆ์ ์ํด AuthenticationManager๋ก ์ ๋ฌ -> ๊ฒ์ฆ ์งํ
+ return authenticationManager.authenticate(authToken);
+ }
+
+ // ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์คํํ๋ ๋ฉ์๋ (JWT ๋ฐ๊ธ)
+ @Override
+ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException {
+
+ String nickname = authentication.getName();
+
+ Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
+ Iterator extends GrantedAuthority> iterator = authorities.iterator();
+ GrantedAuthority auth = iterator.next();
+ String role = auth.getAuthority();
+
+ //ํ ํฐ ์์ฑ
+ String access = jwtUtil.createJwt("access", nickname, role, 1000L * 60 * 60 * 24 * 14); // 2์ฃผ (์์)
+ String refresh = jwtUtil.createJwt("refresh", nickname, role, 1000L * 60 * 60 * 24 * 14); // 2์ฃผ
+
+ //Refresh ํ ํฐ ์ ์ฅ
+ addRefreshEntity(nickname, refresh, 1000L * 60 * 60 * 24 * 14);
+
+ //์๋ต ์ค์
+ response.setHeader("Authorization", "Bearer " + access);
+ response.addCookie(createCookie("refreshToken", refresh));
+ response.setStatus(HttpStatus.OK.value());
+ response.setContentType("application/json");
+ response.setCharacterEncoding("UTF-8");
+ response.getWriter().write("{\"result\": \"๋ก๊ทธ์ธ์ด ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋์์ต๋๋ค.\"}");
+ }
+
+ //๋ก๊ทธ์ธ ์คํจ์ ์คํํ๋ ๋ฉ์๋
+ @Override
+ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
+
+ response.setStatus(401);
+ response.setContentType("application/json");
+ response.setCharacterEncoding("UTF-8");
+ response.getWriter().write("{\"result\": \"๋ก๊ทธ์ธ์ ์คํจํ์์ต๋๋ค.\"}");
+ }
+
+ private void addRefreshEntity(String userId, String refresh, Long expiredMs) {
+
+ Date date = new Date(System.currentTimeMillis() + expiredMs);
+
+ Refresh refreshEntity = Refresh.builder()
+ .userId(userId)
+ .refresh(refresh)
+ .expiration(date.toString())
+ .build();
+
+ refreshRepository.save(refreshEntity);
+ }
+
+ private Cookie createCookie(String key, String value) {
+
+ Cookie cookie = new Cookie(key, value);
+ cookie.setMaxAge(60 * 60 * 24 * 14);
+ //cookie.setSecure(true);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+
+ return cookie;
+ }
+}
diff --git a/src/main/java/ceos/vote/global/repository/DeveloperRepository.java b/src/main/java/ceos/vote/global/repository/DeveloperRepository.java
new file mode 100644
index 0000000..12cc447
--- /dev/null
+++ b/src/main/java/ceos/vote/global/repository/DeveloperRepository.java
@@ -0,0 +1,12 @@
+package ceos.vote.global.repository;
+
+import ceos.vote.domain.developer.entity.Developer;
+import ceos.vote.domain.member.entity.PartType;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface DeveloperRepository extends JpaRepository {
+
+ List findByPartOrderByCountDesc(PartType part);
+}
diff --git a/src/main/java/ceos/vote/global/repository/MemberRepository.java b/src/main/java/ceos/vote/global/repository/MemberRepository.java
new file mode 100644
index 0000000..86b8c26
--- /dev/null
+++ b/src/main/java/ceos/vote/global/repository/MemberRepository.java
@@ -0,0 +1,13 @@
+package ceos.vote.global.repository;
+
+import ceos.vote.domain.member.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface MemberRepository extends JpaRepository {
+
+ Optional findByUserId(String userId);
+
+ Boolean existsMemberByUserId(String userId);
+}
diff --git a/src/main/java/ceos/vote/global/repository/RefreshRepository.java b/src/main/java/ceos/vote/global/repository/RefreshRepository.java
new file mode 100644
index 0000000..cbd89b4
--- /dev/null
+++ b/src/main/java/ceos/vote/global/repository/RefreshRepository.java
@@ -0,0 +1,16 @@
+package ceos.vote.global.repository;
+
+import ceos.vote.domain.member.entity.Refresh;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.transaction.annotation.Transactional;
+
+public interface RefreshRepository extends JpaRepository {
+
+ Boolean existsByRefresh(String refresh);
+
+ @Transactional
+ void deleteByRefresh(String refresh);
+
+ @Transactional
+ void deleteByUserId(String userId);
+}
diff --git a/src/main/java/ceos/vote/global/repository/TeamRepository.java b/src/main/java/ceos/vote/global/repository/TeamRepository.java
new file mode 100644
index 0000000..f0f37de
--- /dev/null
+++ b/src/main/java/ceos/vote/global/repository/TeamRepository.java
@@ -0,0 +1,14 @@
+package ceos.vote.global.repository;
+
+import ceos.vote.domain.member.entity.TeamType;
+import ceos.vote.domain.team.entity.Team;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface TeamRepository extends JpaRepository {
+
+ List findAllByOrderByCountDesc();
+
+ Team findByType(TeamType type);
+}
diff --git a/src/test/java/ceos/vote/VoteApplicationTests.java b/src/test/java/ceos/vote/VoteApplicationTests.java
new file mode 100644
index 0000000..804bb2f
--- /dev/null
+++ b/src/test/java/ceos/vote/VoteApplicationTests.java
@@ -0,0 +1,13 @@
+package ceos.vote;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class VoteApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}