From 069c1d33a61b2dfdccc56872bf40c0dd7b44036b Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 23 Dec 2024 18:23:15 +0900 Subject: [PATCH 01/56] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 45 ++++ HELP.md | 28 ++ build.gradle | 39 +++ gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 252 ++++++++++++++++++ gradlew.bat | 94 +++++++ settings.gradle | 1 + src/main/java/ceos/vote/VoteApplication.java | 13 + .../vote/domain/member/entity/Member.java | 37 +++ .../vote/domain/member/entity/PartType.java | 6 + .../vote/domain/member/entity/TeamType.java | 9 + src/main/java/ceos/vote/domain/vote/Vote.java | 24 ++ src/main/resources/application.properties | 1 + .../java/ceos/vote/VoteApplicationTests.java | 13 + 14 files changed, 569 insertions(+) create mode 100644 .gitignore create mode 100644 HELP.md create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/ceos/vote/VoteApplication.java create mode 100644 src/main/java/ceos/vote/domain/member/entity/Member.java create mode 100644 src/main/java/ceos/vote/domain/member/entity/PartType.java create mode 100644 src/main/java/ceos/vote/domain/member/entity/TeamType.java create mode 100644 src/main/java/ceos/vote/domain/vote/Vote.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/ceos/vote/VoteApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4059871 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +*# +*.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 \ 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/build.gradle b/build.gradle new file mode 100644 index 0000000..3c5f974 --- /dev/null +++ b/build.gradle @@ -0,0 +1,39 @@ +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 { + 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' +} + +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..058ae58 --- /dev/null +++ b/src/main/java/ceos/vote/VoteApplication.java @@ -0,0 +1,13 @@ +package ceos.vote; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class VoteApplication { + + public static void main(String[] args) { + SpringApplication.run(VoteApplication.class, args); + } + +} 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..fcc4a2e --- /dev/null +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -0,0 +1,37 @@ +package ceos.vote.domain.member.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member { + + @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; + + @Column(length = 320, nullable = false) + private String email; + + @Enumerated(EnumType.STRING) + @Column(length = 50, nullable = false) + private PartType part; + + @Column(length = 50, nullable = false) + private String name; + + @Enumerated(EnumType.STRING) + @Column(length = 50, nullable = false) + private TeamType 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..ccdf23c --- /dev/null +++ b/src/main/java/ceos/vote/domain/member/entity/PartType.java @@ -0,0 +1,6 @@ +package ceos.vote.domain.member.entity; + +public enum PartType { + BACKEND, + FRONTEND +} 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..09230cf --- /dev/null +++ b/src/main/java/ceos/vote/domain/member/entity/TeamType.java @@ -0,0 +1,9 @@ +package ceos.vote.domain.member.entity; + +public enum TeamType { + PHOTOGROUND, + ANGELBRIDGE, + PEDALGENIE, + CAKEWAY, + COFFEEDEAL +} \ No newline at end of file diff --git a/src/main/java/ceos/vote/domain/vote/Vote.java b/src/main/java/ceos/vote/domain/vote/Vote.java new file mode 100644 index 0000000..80e1229 --- /dev/null +++ b/src/main/java/ceos/vote/domain/vote/Vote.java @@ -0,0 +1,24 @@ +package ceos.vote.domain.vote; + +import ceos.vote.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Vote { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "vote_id", nullable = false) + private Long id; + + private Long count; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..ac5d379 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=vote 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() { + } + +} From 6060bea68ee72226c7bff7aed39235f6dbd6fb13 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 23 Dec 2024 18:25:48 +0900 Subject: [PATCH 02/56] =?UTF-8?q?feat:=20vote=20count=200=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/domain/vote/Vote.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ceos/vote/domain/vote/Vote.java b/src/main/java/ceos/vote/domain/vote/Vote.java index 80e1229..20a1f48 100644 --- a/src/main/java/ceos/vote/domain/vote/Vote.java +++ b/src/main/java/ceos/vote/domain/vote/Vote.java @@ -16,7 +16,7 @@ public class Vote { @Column(name = "vote_id", nullable = false) private Long id; - private Long count; + private int count = 0; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") From 596c9c30333b8b49f05a817a8a2b6369aeb79228 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 23 Dec 2024 18:28:58 +0900 Subject: [PATCH 03/56] =?UTF-8?q?feat:=20vote=20=ED=96=88=EB=8A=94?= =?UTF-8?q?=EC=A7=80=20=EC=95=88=20=ED=96=88=EB=8A=94=EC=A7=80=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/domain/member/entity/Member.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/ceos/vote/domain/member/entity/Member.java b/src/main/java/ceos/vote/domain/member/entity/Member.java index fcc4a2e..afe67f8 100644 --- a/src/main/java/ceos/vote/domain/member/entity/Member.java +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -34,4 +34,10 @@ public class Member { @Enumerated(EnumType.STRING) @Column(length = 50, nullable = false) private TeamType team; + + private Boolean voteBack = false; + + private Boolean voteFront = false; + + private Boolean voteTeam = false; } From c0006cea7a411b1af5c89ac69d9d870d9ac69eb4 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 26 Dec 2024 23:08:06 +0900 Subject: [PATCH 04/56] =?UTF-8?q?chore:=20build.gradle=20mysql=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 3c5f974..27f6e1c 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ repositories { } 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' From 30009a196a7af909b5747dce0306f026da324094 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 26 Dec 2024 23:09:01 +0900 Subject: [PATCH 05/56] =?UTF-8?q?chore:=20gitignore=EC=97=90=20yml=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4059871..5b1cc00 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ secrets.yml .idea .env -src/test/resources/application.yml \ No newline at end of file +src/test/resources/application.yml +src/main/resources/application.yml \ No newline at end of file From 14a3611942f362bc619b64c06c448b2f86f82ad4 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 26 Dec 2024 23:15:31 +0900 Subject: [PATCH 06/56] =?UTF-8?q?chore:=20BaseEntity=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/VoteApplication.java | 2 ++ .../vote/domain/member/entity/Member.java | 3 +- .../vote/domain/vote/{ => entity}/Vote.java | 5 ++-- .../java/ceos/vote/global/BaseEntity.java | 28 +++++++++++++++++++ src/main/resources/application.properties | 1 - 5 files changed, 35 insertions(+), 4 deletions(-) rename src/main/java/ceos/vote/domain/vote/{ => entity}/Vote.java (81%) create mode 100644 src/main/java/ceos/vote/global/BaseEntity.java delete mode 100644 src/main/resources/application.properties diff --git a/src/main/java/ceos/vote/VoteApplication.java b/src/main/java/ceos/vote/VoteApplication.java index 058ae58..1d66a88 100644 --- a/src/main/java/ceos/vote/VoteApplication.java +++ b/src/main/java/ceos/vote/VoteApplication.java @@ -2,8 +2,10 @@ 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) { diff --git a/src/main/java/ceos/vote/domain/member/entity/Member.java b/src/main/java/ceos/vote/domain/member/entity/Member.java index afe67f8..bfc5771 100644 --- a/src/main/java/ceos/vote/domain/member/entity/Member.java +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -1,5 +1,6 @@ package ceos.vote.domain.member.entity; +import ceos.vote.global.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -8,7 +9,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/ceos/vote/domain/vote/Vote.java b/src/main/java/ceos/vote/domain/vote/entity/Vote.java similarity index 81% rename from src/main/java/ceos/vote/domain/vote/Vote.java rename to src/main/java/ceos/vote/domain/vote/entity/Vote.java index 20a1f48..d44822c 100644 --- a/src/main/java/ceos/vote/domain/vote/Vote.java +++ b/src/main/java/ceos/vote/domain/vote/entity/Vote.java @@ -1,6 +1,7 @@ -package ceos.vote.domain.vote; +package ceos.vote.domain.vote.entity; import ceos.vote.domain.member.entity.Member; +import ceos.vote.global.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -9,7 +10,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Vote { +public class Vote extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/ceos/vote/global/BaseEntity.java b/src/main/java/ceos/vote/global/BaseEntity.java new file mode 100644 index 0000000..d5e05b0 --- /dev/null +++ b/src/main/java/ceos/vote/global/BaseEntity.java @@ -0,0 +1,28 @@ +package ceos.vote.global; + +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/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index ac5d379..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=vote From 38e253aa28ff1363f1532ae5912c1ecefad7a2c2 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 26 Dec 2024 23:24:52 +0900 Subject: [PATCH 07/56] =?UTF-8?q?chore:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/domain/member/entity/Member.java | 3 +++ src/main/java/ceos/vote/domain/vote/entity/Vote.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/ceos/vote/domain/member/entity/Member.java b/src/main/java/ceos/vote/domain/member/entity/Member.java index bfc5771..5a68906 100644 --- a/src/main/java/ceos/vote/domain/member/entity/Member.java +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -36,9 +36,12 @@ public class Member extends BaseEntity { @Column(length = 50, nullable = false) private TeamType team; + @Column(name = "vote_back") private Boolean voteBack = false; + @Column(name = "vote_front") private Boolean voteFront = false; + @Column(name = "vote_team") private Boolean voteTeam = false; } diff --git a/src/main/java/ceos/vote/domain/vote/entity/Vote.java b/src/main/java/ceos/vote/domain/vote/entity/Vote.java index d44822c..ebb63a7 100644 --- a/src/main/java/ceos/vote/domain/vote/entity/Vote.java +++ b/src/main/java/ceos/vote/domain/vote/entity/Vote.java @@ -17,7 +17,7 @@ public class Vote extends BaseEntity { @Column(name = "vote_id", nullable = false) private Long id; - private int count = 0; + private Integer count = 0; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") From 2d5da8bc4efe14b68a1c4b3517d50ef27bde19e0 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 26 Dec 2024 23:37:15 +0900 Subject: [PATCH 08/56] =?UTF-8?q?feat:=20=EC=84=B1=EA=B3=B5=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/entity/Member.java | 7 ++-- .../ceos/vote/domain/vote/entity/Vote.java | 2 +- .../vote/global/{ => common}/BaseEntity.java | 2 +- .../common/response/CommonResponse.java | 34 +++++++++++++++++++ .../global/common/response/SuccessCode.java | 20 +++++++++++ 5 files changed, 61 insertions(+), 4 deletions(-) rename src/main/java/ceos/vote/global/{ => common}/BaseEntity.java (96%) create mode 100644 src/main/java/ceos/vote/global/common/response/CommonResponse.java create mode 100644 src/main/java/ceos/vote/global/common/response/SuccessCode.java diff --git a/src/main/java/ceos/vote/domain/member/entity/Member.java b/src/main/java/ceos/vote/domain/member/entity/Member.java index 5a68906..2917002 100644 --- a/src/main/java/ceos/vote/domain/member/entity/Member.java +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -1,6 +1,6 @@ package ceos.vote.domain.member.entity; -import ceos.vote.global.BaseEntity; +import ceos.vote.global.common.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -16,6 +16,7 @@ public class Member extends BaseEntity { @Column(name = "member_id", nullable = false) private Long id; + // 로그인 아이디 @Column(name = "user_id", length = 50, nullable = false) private String userId; @@ -25,11 +26,13 @@ public class Member extends BaseEntity { @Column(length = 320, nullable = false) private String email; + private String role; + @Enumerated(EnumType.STRING) @Column(length = 50, nullable = false) private PartType part; - @Column(length = 50, nullable = false) + @Column(length = 20, nullable = false) private String name; @Enumerated(EnumType.STRING) diff --git a/src/main/java/ceos/vote/domain/vote/entity/Vote.java b/src/main/java/ceos/vote/domain/vote/entity/Vote.java index ebb63a7..fc22c3c 100644 --- a/src/main/java/ceos/vote/domain/vote/entity/Vote.java +++ b/src/main/java/ceos/vote/domain/vote/entity/Vote.java @@ -1,7 +1,7 @@ package ceos.vote.domain.vote.entity; import ceos.vote.domain.member.entity.Member; -import ceos.vote.global.BaseEntity; +import ceos.vote.global.common.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; diff --git a/src/main/java/ceos/vote/global/BaseEntity.java b/src/main/java/ceos/vote/global/common/BaseEntity.java similarity index 96% rename from src/main/java/ceos/vote/global/BaseEntity.java rename to src/main/java/ceos/vote/global/common/BaseEntity.java index d5e05b0..eff8df6 100644 --- a/src/main/java/ceos/vote/global/BaseEntity.java +++ b/src/main/java/ceos/vote/global/common/BaseEntity.java @@ -1,4 +1,4 @@ -package ceos.vote.global; +package ceos.vote.global.common; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; 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; + } +} From 04ab8bf4dbf10638b78748c821446dec178f55ff Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 26 Dec 2024 23:45:04 +0900 Subject: [PATCH 09/56] =?UTF-8?q?feat:=20=EC=98=A4=EB=A5=98=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ApplicationException.java | 11 +++++ .../vote/global/exception/ExceptionCode.java | 41 ++++++++++++++++ .../global/exception/ExceptionResponse.java | 28 +++++++++++ .../exception/GlobalExceptionHandler.java | 47 +++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 src/main/java/ceos/vote/global/exception/ApplicationException.java create mode 100644 src/main/java/ceos/vote/global/exception/ExceptionCode.java create mode 100644 src/main/java/ceos/vote/global/exception/ExceptionResponse.java create mode 100644 src/main/java/ceos/vote/global/exception/GlobalExceptionHandler.java 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..04ce205 --- /dev/null +++ b/src/main/java/ceos/vote/global/exception/ExceptionCode.java @@ -0,0 +1,41 @@ +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, "잘못된 요청입니다."), + + // 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_ADMIN_USERNAME(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,"존재하지 않는 사용자입니다."); + + 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)); + } +} From c5f568fcf65f8288c4ce3d131b03057f22c0b6e8 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 26 Dec 2024 23:49:30 +0900 Subject: [PATCH 10/56] =?UTF-8?q?refactor:=20enum=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=97=90=20description=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/entity/PartType.java | 16 ++++++++++++-- .../vote/domain/member/entity/TeamType.java | 22 ++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/java/ceos/vote/domain/member/entity/PartType.java b/src/main/java/ceos/vote/domain/member/entity/PartType.java index ccdf23c..a5db4e6 100644 --- a/src/main/java/ceos/vote/domain/member/entity/PartType.java +++ b/src/main/java/ceos/vote/domain/member/entity/PartType.java @@ -1,6 +1,18 @@ package ceos.vote.domain.member.entity; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString public enum PartType { - BACKEND, - FRONTEND + + BACKEND("백엔드"), + FRONTEND("프론트엔드"); + + private final String description; + + PartType(String description) { + this.description = description; + } } diff --git a/src/main/java/ceos/vote/domain/member/entity/TeamType.java b/src/main/java/ceos/vote/domain/member/entity/TeamType.java index 09230cf..e035a78 100644 --- a/src/main/java/ceos/vote/domain/member/entity/TeamType.java +++ b/src/main/java/ceos/vote/domain/member/entity/TeamType.java @@ -1,9 +1,21 @@ package ceos.vote.domain.member.entity; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString public enum TeamType { - PHOTOGROUND, - ANGELBRIDGE, - PEDALGENIE, - CAKEWAY, - COFFEEDEAL + + PHOTOGROUND("포토그라운드"), + ANGELBRIDGE("엔젤브릿지"), + PEDALGENIE("페달지니"), + CAKEWAY("케이크웨이"), + COFFEEDEAL("커피딜"); + + private final String description; + + TeamType(String description) { + this.description = description; + } } \ No newline at end of file From f006d1a536d69cc942a3a02e6577bcbf16ee09a4 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:10:49 +0900 Subject: [PATCH 11/56] =?UTF-8?q?chore:=20swagger=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +++++ .../vote/global/config/SwaggerConfig.java | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/main/java/ceos/vote/global/config/SwaggerConfig.java diff --git a/build.gradle b/build.gradle index 27f6e1c..5a8ba2e 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,15 @@ dependencies { 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') { 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 From 4b79dd01c59c24b1cb8149cde2262dfbe61c87f9 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:12:30 +0900 Subject: [PATCH 12/56] =?UTF-8?q?chore:=20cors=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/global/config/CorsMvcConfig.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/ceos/vote/global/config/CorsMvcConfig.java 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("*"); + } +} From 09b5b8548ce24f60e39d648c97daddd6bd799050 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:15:51 +0900 Subject: [PATCH 13/56] =?UTF-8?q?feat:=20JWTUtil=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ceos/vote/global/jwt/JWTUtil.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/main/java/ceos/vote/global/jwt/JWTUtil.java 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(); + } +} From 0042ab2c893e3ef8f173a58e8a1faa6a14269b63 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:20:10 +0900 Subject: [PATCH 14/56] =?UTF-8?q?feat:=20Refresh=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/entity/Refresh.java | 34 +++++++++++++++++++ .../global/repository/RefreshRepository.java | 13 +++++++ 2 files changed, 47 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/member/entity/Refresh.java create mode 100644 src/main/java/ceos/vote/global/repository/RefreshRepository.java 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/global/repository/RefreshRepository.java b/src/main/java/ceos/vote/global/repository/RefreshRepository.java new file mode 100644 index 0000000..6af4540 --- /dev/null +++ b/src/main/java/ceos/vote/global/repository/RefreshRepository.java @@ -0,0 +1,13 @@ +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); +} From 33284f8dc851e9496ed2724ab98a5c309dfc71de Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:22:11 +0900 Subject: [PATCH 15/56] =?UTF-8?q?feat:=20LoginRequestDto=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/dto/request/LoginRequestDto.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/member/dto/request/LoginRequestDto.java 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..16fb2f0 --- /dev/null +++ b/src/main/java/ceos/vote/domain/member/dto/request/LoginRequestDto.java @@ -0,0 +1,12 @@ +package ceos.vote.domain.member.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record LoginRequestDto( + + @NotNull + String userId, + @NotNull + String password +) { +} From 0fcd5a3fca11a081c4f4988f6f058ccd2da092fb Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:26:57 +0900 Subject: [PATCH 16/56] =?UTF-8?q?feat:=20LoginFilter=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ceos/vote/global/jwt/LoginFilter.java | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/main/java/ceos/vote/global/jwt/LoginFilter.java 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 authorities = authentication.getAuthorities(); + Iterator 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; + } +} From 7e855d332d1388b7c038e3fcee51e45df8646229 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:32:48 +0900 Subject: [PATCH 17/56] =?UTF-8?q?feat:=20CustomUserDetails=20dto=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/dto/CustomUserDetails.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/member/dto/CustomUserDetails.java 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 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; + } +} From a3377309db68548bd662374bb82fec13555f4dd2 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:33:22 +0900 Subject: [PATCH 18/56] =?UTF-8?q?feat:=20JWTFilter=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ceos/vote/global/jwt/JWTFilter.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/main/java/ceos/vote/global/jwt/JWTFilter.java 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); + } +} From 56d9c172cfe5f32821519e027a12e459b56dc0f6 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 00:36:03 +0900 Subject: [PATCH 19/56] =?UTF-8?q?feat:=20CustomUserDetailsService=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomUserDetailsService.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/member/service/CustomUserDetailsService.java 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); + } +} From 1f8af7d3c74c48434f0673047e32c1b8e6323a54 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 02:17:25 +0900 Subject: [PATCH 20/56] =?UTF-8?q?feat:=20SignupRequestDto=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/dto/request/LoginRequestDto.java | 4 ++ .../member/dto/request/SignupRequestDto.java | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java 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 index 16fb2f0..16c7f1a 100644 --- a/src/main/java/ceos/vote/domain/member/dto/request/LoginRequestDto.java +++ b/src/main/java/ceos/vote/domain/member/dto/request/LoginRequestDto.java @@ -1,11 +1,15 @@ 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..e84c126 --- /dev/null +++ b/src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java @@ -0,0 +1,53 @@ +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 io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +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 = "ceos@gmail.com") + @Email(message = "유효한 이메일 주소를 입력해주세요.") + String email, + + @Schema(description = "소속 팀명", example = "포토그라운드 | 엔젤브릿지 | 페달지니 | 케이크웨이 | 커피딜") + @NotNull(message = "소속 팀명은 필수 입력 사항입니다.") + String team, + + @Schema(description = "소속 파트", example = "프론트엔드 | 백엔드") + @NotNull(message = "소속 파트는 필수 입력 사항입니다.") + String part +) { + public Member toEntity(String encodedPassword, PartType part, TeamType team) { + return Member.builder() + .userId(userId) + .password(encodedPassword) + .email(email) + .role("ROLE_USER") + .part(part) + .name(name) + .team(team) + .voteBack(false) + .voteFront(false) + .voteTeam(false) + .build(); + } +} + From 12774e8695deb43ce3c24f2e0052c696560362b3 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 02:17:43 +0900 Subject: [PATCH 21/56] =?UTF-8?q?feat:=20MemberResponseDto=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/MemberResponseDto.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java 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..abb0dd1 --- /dev/null +++ b/src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java @@ -0,0 +1,30 @@ +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 email, + 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()) + .email(member.getEmail()) + .team(member.getTeam().getDescription()) + .part(member.getPart().getDescription()) + .voteBack(member.getVoteBack()) + .voteFront(member.getVoteFront()) + .voteTeam(member.getVoteTeam()) + .build(); + } +} From 12b2b396187fd31ab1f5bdc7e0f33a2166f5c996 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 02:18:29 +0900 Subject: [PATCH 22/56] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/entity/Member.java | 22 +- .../domain/member/service/AuthService.java | 193 ++++++++++++++++++ .../vote/global/exception/ExceptionCode.java | 7 +- .../global/repository/MemberRepository.java | 13 ++ 4 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ceos/vote/domain/member/service/AuthService.java create mode 100644 src/main/java/ceos/vote/global/repository/MemberRepository.java diff --git a/src/main/java/ceos/vote/domain/member/entity/Member.java b/src/main/java/ceos/vote/domain/member/entity/Member.java index 2917002..fd97d2d 100644 --- a/src/main/java/ceos/vote/domain/member/entity/Member.java +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -3,6 +3,7 @@ import ceos.vote.global.common.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -40,11 +41,26 @@ public class Member extends BaseEntity { private TeamType team; @Column(name = "vote_back") - private Boolean voteBack = false; + private Boolean voteBack; @Column(name = "vote_front") - private Boolean voteFront = false; + private Boolean voteFront; @Column(name = "vote_team") - private Boolean voteTeam = false; + private Boolean voteTeam; + + @Builder + public Member(String userId, String password, String email, String role, PartType part, String name, TeamType team, Boolean voteBack, Boolean voteFront, Boolean voteTeam) { + + this.userId = userId; + this.password = password; + this.email = email; + this.role = role; + this.part = part; + this.name = name; + this.team = team; + this.voteBack = voteBack; + this.voteFront = voteFront; + this.voteTeam = voteTeam; + } } 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..f461a8b --- /dev/null +++ b/src/main/java/ceos/vote/domain/member/service/AuthService.java @@ -0,0 +1,193 @@ +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.global.exception.ApplicationException; +import ceos.vote.global.jwt.JWTUtil; +import ceos.vote.global.repository.MemberRepository; +import ceos.vote.global.repository.RefreshRepository; +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; + + // [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); + } + + Member newMember = request.toEntity(bCryptPasswordEncoder.encode(password), part, team); + + 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.deleteByRefresh(newRefresh); + + 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/global/exception/ExceptionCode.java b/src/main/java/ceos/vote/global/exception/ExceptionCode.java index 04ce205..a047cf1 100644 --- a/src/main/java/ceos/vote/global/exception/ExceptionCode.java +++ b/src/main/java/ceos/vote/global/exception/ExceptionCode.java @@ -27,13 +27,16 @@ public enum ExceptionCode { WRONG_TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, 3002, "유효하지 않은 토큰입니다."), INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, 3003,"올바르지 않은 형식의 RefreshToken 입니다."), INVALID_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, 3004,"올바르지 않은 형식의 AccessToken 입니다."), - DUPLICATED_ADMIN_USERNAME(HttpStatus.BAD_REQUEST, 3005,"중복된 사용자 이름입니다."), + 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,"존재하지 않는 사용자입니다."); + 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; 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); +} From 47a55407218b8eb3dff494c99f645db1bf00057b Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 02:18:48 +0900 Subject: [PATCH 23/56] =?UTF-8?q?feat:=20AuthController=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/AuthController.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/member/controller/AuthController.java 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<>("토큰 재발급을 성공하였습니다."); + } +} From 11eb4f92e8d4b5880cf2c6d9381d7bc6f2c941fc Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 02:38:52 +0900 Subject: [PATCH 24/56] =?UTF-8?q?fix:=20deleteByUserId=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/AuthService.java | 2 +- .../vote/global/jwt/CustomLogoutFilter.java | 99 +++++++++++++++++++ .../global/repository/RefreshRepository.java | 3 + 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ceos/vote/global/jwt/CustomLogoutFilter.java diff --git a/src/main/java/ceos/vote/domain/member/service/AuthService.java b/src/main/java/ceos/vote/domain/member/service/AuthService.java index f461a8b..fd9de95 100644 --- a/src/main/java/ceos/vote/domain/member/service/AuthService.java +++ b/src/main/java/ceos/vote/domain/member/service/AuthService.java @@ -159,7 +159,7 @@ private Cookie createCookie(String key, String value) { @Transactional public void deleteAndSaveNewRefreshToken(String userId, String newRefresh, Long expiredMs) { - refreshRepository.deleteByRefresh(newRefresh); + refreshRepository.deleteByUserId(userId); addRefreshEntity(userId, newRefresh, expiredMs); } 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/repository/RefreshRepository.java b/src/main/java/ceos/vote/global/repository/RefreshRepository.java index 6af4540..cbd89b4 100644 --- a/src/main/java/ceos/vote/global/repository/RefreshRepository.java +++ b/src/main/java/ceos/vote/global/repository/RefreshRepository.java @@ -10,4 +10,7 @@ public interface RefreshRepository extends JpaRepository { @Transactional void deleteByRefresh(String refresh); + + @Transactional + void deleteByUserId(String userId); } From 12b4ba37b4c28e6f8cbbcee92f3c4dc5c9e709e4 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 02:39:14 +0900 Subject: [PATCH 25/56] =?UTF-8?q?feat:=20SecurityConfig=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/global/config/SecurityConfig.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/main/java/ceos/vote/global/config/SecurityConfig.java 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..e17b5ca --- /dev/null +++ b/src/main/java/ceos/vote/global/config/SecurityConfig.java @@ -0,0 +1,111 @@ +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://localhost:8081" +// "http://배포주소:8080", +// "http://배포주소:8081" + )); + 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() + .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(); + } +} From 17ef40a044a514da193fe9c2abe7c6c4e117768e Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Fri, 27 Dec 2024 02:54:37 +0900 Subject: [PATCH 26/56] =?UTF-8?q?feat:=20[GET]=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 31 +++++++++++++++++++ .../domain/member/service/MemberService.java | 31 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/member/controller/MemberController.java create mode 100644 src/main/java/ceos/vote/domain/member/service/MemberService.java 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..30e7db0 --- /dev/null +++ b/src/main/java/ceos/vote/domain/member/controller/MemberController.java @@ -0,0 +1,31 @@ +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.service.MemberService; +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/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); + } +} From 01b2138c9506d01b925c0dcefcc098c3348d3bff Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:07:19 +0900 Subject: [PATCH 27/56] =?UTF-8?q?delete:=20vote=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ceos/vote/domain/vote/entity/Vote.java | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/main/java/ceos/vote/domain/vote/entity/Vote.java diff --git a/src/main/java/ceos/vote/domain/vote/entity/Vote.java b/src/main/java/ceos/vote/domain/vote/entity/Vote.java deleted file mode 100644 index fc22c3c..0000000 --- a/src/main/java/ceos/vote/domain/vote/entity/Vote.java +++ /dev/null @@ -1,25 +0,0 @@ -package ceos.vote.domain.vote.entity; - -import ceos.vote.domain.member.entity.Member; -import ceos.vote.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Vote extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "vote_id", nullable = false) - private Long id; - - private Integer count = 0; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; -} From 973337afd8f0ee7a1a549a10737114c3c8bb916d Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:07:44 +0900 Subject: [PATCH 28/56] =?UTF-8?q?feat:=20member=EC=97=90=20team=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=EB=84=A3=EC=96=B4=EC=A3=BC?= =?UTF-8?q?=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ceos/vote/domain/member/service/AuthService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/ceos/vote/domain/member/service/AuthService.java b/src/main/java/ceos/vote/domain/member/service/AuthService.java index fd9de95..7f2f2af 100644 --- a/src/main/java/ceos/vote/domain/member/service/AuthService.java +++ b/src/main/java/ceos/vote/domain/member/service/AuthService.java @@ -6,10 +6,12 @@ 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; @@ -32,6 +34,7 @@ public class AuthService { private final BCryptPasswordEncoder bCryptPasswordEncoder; private final JWTUtil jwtUtil; private final RefreshRepository refreshRepository; + private final TeamRepository teamRepository; // [POST] 회원가입 @Transactional @@ -69,7 +72,8 @@ public MemberResponseDto signup(SignupRequestDto request) { throw new ApplicationException(INVALID_TEAM_TYPE); } - Member newMember = request.toEntity(bCryptPasswordEncoder.encode(password), part, team); + Team myTeam = teamRepository.findByType(team); + Member newMember = request.toEntity(bCryptPasswordEncoder.encode(password), part, myTeam); memberRepository.save(newMember); From e775f71459187a47f57ebffba74aaea6ec2a1f65 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:07:56 +0900 Subject: [PATCH 29/56] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/global/exception/ExceptionCode.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/ceos/vote/global/exception/ExceptionCode.java b/src/main/java/ceos/vote/global/exception/ExceptionCode.java index a047cf1..bb98ed4 100644 --- a/src/main/java/ceos/vote/global/exception/ExceptionCode.java +++ b/src/main/java/ceos/vote/global/exception/ExceptionCode.java @@ -20,6 +20,11 @@ public enum ExceptionCode { 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, "토큰 발급에서 오류가 발생했습니다."), From a48da715637a127e8ee272937325a13712eb55e4 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:08:14 +0900 Subject: [PATCH 30/56] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EA=B4=80=EB=A0=A8=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/MemberResponseDto.java | 6 +-- .../vote/domain/member/entity/Member.java | 42 +++++++++++++------ .../global/repository/MemberRepository.java | 6 +++ 3 files changed, 38 insertions(+), 16 deletions(-) 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 index abb0dd1..19df169 100644 --- a/src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java +++ b/src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java @@ -22,9 +22,9 @@ public static MemberResponseDto from(Member member) { .email(member.getEmail()) .team(member.getTeam().getDescription()) .part(member.getPart().getDescription()) - .voteBack(member.getVoteBack()) - .voteFront(member.getVoteFront()) - .voteTeam(member.getVoteTeam()) + .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 index fd97d2d..c00b9f2 100644 --- a/src/main/java/ceos/vote/domain/member/entity/Member.java +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -1,5 +1,6 @@ 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; @@ -36,21 +37,39 @@ public class Member extends BaseEntity { @Column(length = 20, nullable = false) private String name; - @Enumerated(EnumType.STRING) - @Column(length = 50, nullable = false) - private TeamType team; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id") + private Team team; + + @Column(name = "vote_back", nullable = false) + private boolean voteBack = false; - @Column(name = "vote_back") - private Boolean voteBack; + @Column(name = "vote_front", nullable = false) + private boolean voteFront = false; - @Column(name = "vote_front") - private Boolean voteFront; + @Column(name = "vote_team", nullable = false) + private boolean voteTeam = false; + + private int count = 0; + + public void voteToMe() { + this.count++; + } - @Column(name = "vote_team") - private Boolean voteTeam; + 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 email, String role, PartType part, String name, TeamType team, Boolean voteBack, Boolean voteFront, Boolean voteTeam) { + public Member(String userId, String password, String email, String role, PartType part, String name, Team team) { this.userId = userId; this.password = password; @@ -59,8 +78,5 @@ public Member(String userId, String password, String email, String role, PartTyp this.part = part; this.name = name; this.team = team; - this.voteBack = voteBack; - this.voteFront = voteFront; - this.voteTeam = voteTeam; } } diff --git a/src/main/java/ceos/vote/global/repository/MemberRepository.java b/src/main/java/ceos/vote/global/repository/MemberRepository.java index 86b8c26..d41f39c 100644 --- a/src/main/java/ceos/vote/global/repository/MemberRepository.java +++ b/src/main/java/ceos/vote/global/repository/MemberRepository.java @@ -1,8 +1,10 @@ package ceos.vote.global.repository; import ceos.vote.domain.member.entity.Member; +import ceos.vote.domain.member.entity.PartType; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface MemberRepository extends JpaRepository { @@ -10,4 +12,8 @@ public interface MemberRepository extends JpaRepository { Optional findByUserId(String userId); Boolean existsMemberByUserId(String userId); + + List findByPart(PartType part); + + List findByPartOrderByCountDesc(PartType part); } From 178bf7b20f893f85ad097967240c2741a3404f77 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:08:24 +0900 Subject: [PATCH 31/56] =?UTF-8?q?feat:=20=ED=97=88=EC=9A=A9=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/global/config/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ceos/vote/global/config/SecurityConfig.java b/src/main/java/ceos/vote/global/config/SecurityConfig.java index e17b5ca..90145a3 100644 --- a/src/main/java/ceos/vote/global/config/SecurityConfig.java +++ b/src/main/java/ceos/vote/global/config/SecurityConfig.java @@ -91,6 +91,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { http .authorizeHttpRequests((auth) -> auth .requestMatchers("/login", "/", "/api/auth/**", "/swagger-ui.html", "/swagger-ui/**","/v3/api-docs/**").permitAll() + .requestMatchers("/api/v1/vote/developer", "/api/v1/vote/team").permitAll() // 개발자, 팀 조회하는건 허용 .anyRequest().authenticated() ); From 66166c345fc42c0181eeaa66cc2f6ab8b4dcbde7 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:08:40 +0900 Subject: [PATCH 32/56] =?UTF-8?q?feat:=20=20team=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=EC=A3=BC=EC=9E=85=ED=95=98=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/dto/request/SignupRequestDto.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 index e84c126..4d7776b 100644 --- a/src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java +++ b/src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java @@ -3,6 +3,7 @@ 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.Email; import jakarta.validation.constraints.NotNull; @@ -35,7 +36,7 @@ public record SignupRequestDto( @NotNull(message = "소속 파트는 필수 입력 사항입니다.") String part ) { - public Member toEntity(String encodedPassword, PartType part, TeamType team) { + public Member toEntity(String encodedPassword, PartType part, Team team) { return Member.builder() .userId(userId) .password(encodedPassword) @@ -44,9 +45,6 @@ public Member toEntity(String encodedPassword, PartType part, TeamType team) { .part(part) .name(name) .team(team) - .voteBack(false) - .voteFront(false) - .voteTeam(false) .build(); } } From be24d4035e89e7912e3315182845193bc9982348 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:08:57 +0900 Subject: [PATCH 33/56] =?UTF-8?q?feat:=20=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/team/dto/TeamRequestDto.java | 23 +++++++++++++++++++ .../response/DeveloperListResponseDto.java | 20 ++++++++++++++++ .../DeveloperVoteResultResponseDto.java | 17 ++++++++++++++ .../dto/response/TeamListResponseDto.java | 21 +++++++++++++++++ .../response/TeamVoteResultResponseDto.java | 19 +++++++++++++++ 5 files changed, 100 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/team/dto/TeamRequestDto.java create mode 100644 src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java create mode 100644 src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java create mode 100644 src/main/java/ceos/vote/domain/vote/dto/response/TeamListResponseDto.java create mode 100644 src/main/java/ceos/vote/domain/vote/dto/response/TeamVoteResultResponseDto.java 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/vote/dto/response/DeveloperListResponseDto.java b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java new file mode 100644 index 0000000..d94aa0b --- /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.member.entity.Member; +import lombok.Builder; + +@Builder +public record DeveloperListResponseDto( + Long memberId, + String memberName, + String teamName +) { + + public static DeveloperListResponseDto from (Member member) { + return DeveloperListResponseDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .teamName(member.getTeam().getDescription()) + .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..9d25826 --- /dev/null +++ b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java @@ -0,0 +1,17 @@ +package ceos.vote.domain.vote.dto.response; + +import ceos.vote.domain.member.entity.Member; +import lombok.Builder; + +@Builder +public record DeveloperVoteResultResponseDto( + String memberName, + int count +) { + public static DeveloperVoteResultResponseDto from(Member m) { + return DeveloperVoteResultResponseDto.builder() + .memberName(m.getName()) + .count(m.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(); + } +} From 936c465bff9a81345e7f4a17d5f168183f5e627f Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:09:08 +0900 Subject: [PATCH 34/56] =?UTF-8?q?feat:=20=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ceos/vote/global/annotation/Login.java | 11 +++++ .../LoginArgumentResolver.java | 45 +++++++++++++++++++ .../ceos/vote/global/config/WebConfig.java | 25 +++++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/main/java/ceos/vote/global/annotation/Login.java create mode 100644 src/main/java/ceos/vote/global/annotation/argumentresolver/LoginArgumentResolver.java create mode 100644 src/main/java/ceos/vote/global/config/WebConfig.java 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/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); + } +} From ba3ccafe1eb0e211ad04106f329b47d1e0002216 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:09:26 +0900 Subject: [PATCH 35/56] =?UTF-8?q?feat:=20=20Team=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../team/controller/TeamController.java | 26 +++++++++ .../ceos/vote/domain/team/entity/Team.java | 55 +++++++++++++++++++ .../global/repository/TeamRepository.java | 14 +++++ 3 files changed, 95 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/team/controller/TeamController.java create mode 100644 src/main/java/ceos/vote/domain/team/entity/Team.java create mode 100644 src/main/java/ceos/vote/global/repository/TeamRepository.java 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..7222079 --- /dev/null +++ b/src/main/java/ceos/vote/domain/team/controller/TeamController.java @@ -0,0 +1,26 @@ +package ceos.vote.domain.team.controller; + +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.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 TeamRepository teamRepository; + + @PostMapping + public void createTeam(@RequestBody TeamRequestDto teamRequestDto) { + + Team team = teamRequestDto.toEntity(); + teamRepository.save(team); + + } +} 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/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); +} From c8bbc4e8de0a979b891df702ab14e57a7628fbf8 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 01:09:34 +0900 Subject: [PATCH 36/56] =?UTF-8?q?feat:=20=20vote=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/controller/VoteController.java | 70 +++++++++++ .../vote/domain/vote/service/VoteService.java | 117 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/vote/controller/VoteController.java create mode 100644 src/main/java/ceos/vote/domain/vote/service/VoteService.java 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..d605664 --- /dev/null +++ b/src/main/java/ceos/vote/domain/vote/controller/VoteController.java @@ -0,0 +1,70 @@ +package ceos.vote.domain.vote.controller; + +import ceos.vote.domain.member.entity.Member; +import ceos.vote.domain.vote.dto.response.DeveloperListResponseDto; +import ceos.vote.domain.vote.dto.response.DeveloperVoteResultResponseDto; +import ceos.vote.domain.vote.dto.response.TeamListResponseDto; +import ceos.vote.domain.vote.dto.response.TeamVoteResultResponseDto; +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/v1/vote") +@RequiredArgsConstructor +@Tag(name = "Vote", description = "투표 관련 API") +public class VoteController { + + private final VoteService voteService; + + + @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/service/VoteService.java b/src/main/java/ceos/vote/domain/vote/service/VoteService.java new file mode 100644 index 0000000..5f95757 --- /dev/null +++ b/src/main/java/ceos/vote/domain/vote/service/VoteService.java @@ -0,0 +1,117 @@ +package ceos.vote.domain.vote.service; + +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.DeveloperListResponseDto; +import ceos.vote.domain.vote.dto.response.TeamListResponseDto; +import ceos.vote.domain.vote.dto.response.DeveloperVoteResultResponseDto; +import ceos.vote.domain.vote.dto.response.TeamVoteResultResponseDto; +import ceos.vote.global.exception.ApplicationException; +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.stream.Collectors; + +import static ceos.vote.global.exception.ExceptionCode.*; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class VoteService { + + private final MemberRepository memberRepository; + private final TeamRepository teamRepository; + + // [GET] 개발자 리스트 조회 + public List getDeveloperList(String type) { + + PartType partType = PartType.valueOf(type.toUpperCase()); + List members = memberRepository.findByPartOrderByCountDesc(partType); + + return members.stream() + .map(DeveloperListResponseDto::from) + .collect(Collectors.toList()); + } + + // [GET] 팀 리스트 조회 + public List getTeamList() { + + List teams = teamRepository.findAll(); + return teams.stream() + .map(TeamListResponseDto::from) + .collect(Collectors.toList()); + + } + + // [PATCH] 개발자 파트장 투표 + @Transactional + public void voteDeveloper(Long memberId, Member loginMember) { + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new ApplicationException(NOT_FOUND_USER)); + // 나 자신에게 투표할 수 없음 + if (member == loginMember) + throw new ApplicationException(BAD_REQUEST_SELF); + // 같은 파트에게만 투표할 수 있음 + if (member.getPart() != loginMember.getPart()) + throw new ApplicationException(BAD_REQUEST_DEVELOPER); + // 이미 투표했다면 다시 투표할 수 없음 + PartType type = member.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(); + } + member.voteToMe(); + memberRepository.save(member); + memberRepository.save(loginMember); + } + + // [PATCH] 팀 투표 + @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 members = memberRepository.findByPartOrderByCountDesc(partType); + + return members.stream() + .map(DeveloperVoteResultResponseDto::from) + .collect(Collectors.toList()); + } + + // [GET] 팀 투표 결과 조회 + public List getTeamVoteResult() { + + List teams = teamRepository.findAllByOrderByCountDesc(); + return teams.stream() + .map(TeamVoteResultResponseDto::from) + .collect(Collectors.toList()); + } +} From 19b33f493671f3df310827e6adbc1b2fa7701c45 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 23:32:29 +0900 Subject: [PATCH 37/56] =?UTF-8?q?feat:=20=20developer=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=B6=94=EA=B0=80=EB=A1=9C=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=88=98=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DeveloperController.java | 24 ++++++++++ .../developer/dto/DeveloperRequestDto.java | 21 ++++++++ .../domain/developer/entity/Developer.java | 45 +++++++++++++++++ .../developer/service/DeveloperService.java | 48 +++++++++++++++++++ .../repository/DeveloperRepository.java | 12 +++++ 5 files changed, 150 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/developer/controller/DeveloperController.java create mode 100644 src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java create mode 100644 src/main/java/ceos/vote/domain/developer/entity/Developer.java create mode 100644 src/main/java/ceos/vote/domain/developer/service/DeveloperService.java create mode 100644 src/main/java/ceos/vote/global/repository/DeveloperRepository.java 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..eb17315 --- /dev/null +++ b/src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java @@ -0,0 +1,21 @@ +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 = "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..7fe830c --- /dev/null +++ b/src/main/java/ceos/vote/domain/developer/entity/Developer.java @@ -0,0 +1,45 @@ +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 int count = 0; + + public void voteToMe() { + this.count++; + } + + @Builder + public Developer(String developerName, PartType part, Team team) { + + this.developerName = developerName; + this.part = part; + this.team = team; + } + +} 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..1c56e11 --- /dev/null +++ b/src/main/java/ceos/vote/domain/developer/service/DeveloperService.java @@ -0,0 +1,48 @@ +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) + .build(); + developerRepository.save(developer); + } +} 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); +} From 3c1ee466405039c5f9f2c44935f5486a147f7b0a Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 23:32:45 +0900 Subject: [PATCH 38/56] =?UTF-8?q?feat:=20=20team=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B6=84=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/team/service/TeamService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/team/service/TeamService.java 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); + } +} From e72cf7429fbceeda593191a1d6a92e459c189cc5 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 23:33:07 +0900 Subject: [PATCH 39/56] =?UTF-8?q?feat:=20=20teamController=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/team/controller/TeamController.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/ceos/vote/domain/team/controller/TeamController.java b/src/main/java/ceos/vote/domain/team/controller/TeamController.java index 7222079..08f3f5f 100644 --- a/src/main/java/ceos/vote/domain/team/controller/TeamController.java +++ b/src/main/java/ceos/vote/domain/team/controller/TeamController.java @@ -1,8 +1,8 @@ package ceos.vote.domain.team.controller; import ceos.vote.domain.team.dto.TeamRequestDto; -import ceos.vote.domain.team.entity.Team; -import ceos.vote.global.repository.TeamRepository; +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; @@ -14,13 +14,12 @@ @RequiredArgsConstructor public class TeamController { - private final TeamRepository teamRepository; + private final TeamService teamService; @PostMapping - public void createTeam(@RequestBody TeamRequestDto teamRequestDto) { - - Team team = teamRequestDto.toEntity(); - teamRepository.save(team); + public CommonResponse createTeam(@RequestBody TeamRequestDto teamRequestDto) { + teamService.createTeam(teamRequestDto); + return new CommonResponse<>("팀 생성 성공"); } } From e73e74b9d18f50fbb5b9a4bdf20f83fbccc7c6ec Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sun, 29 Dec 2024 23:33:49 +0900 Subject: [PATCH 40/56] =?UTF-8?q?feat:=20=20=ED=88=AC=ED=91=9C=EB=B0=9B?= =?UTF-8?q?=EC=9D=84=20=EC=97=94=ED=8B=B0=ED=8B=B0=EB=A5=BC=20developer?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/entity/Member.java | 6 ---- .../response/DeveloperListResponseDto.java | 14 ++++---- .../DeveloperVoteResultResponseDto.java | 10 +++--- .../vote/domain/vote/service/VoteService.java | 32 +++++++++++-------- .../global/repository/MemberRepository.java | 6 ---- 5 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/main/java/ceos/vote/domain/member/entity/Member.java b/src/main/java/ceos/vote/domain/member/entity/Member.java index c00b9f2..feae75f 100644 --- a/src/main/java/ceos/vote/domain/member/entity/Member.java +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -50,12 +50,6 @@ public class Member extends BaseEntity { @Column(name = "vote_team", nullable = false) private boolean voteTeam = false; - private int count = 0; - - public void voteToMe() { - this.count++; - } - public void voteToBack() { this.voteBack = true; } 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 index d94aa0b..a49fd74 100644 --- a/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java +++ b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java @@ -1,20 +1,20 @@ package ceos.vote.domain.vote.dto.response; -import ceos.vote.domain.member.entity.Member; +import ceos.vote.domain.developer.entity.Developer; import lombok.Builder; @Builder public record DeveloperListResponseDto( - Long memberId, - String memberName, + Long developerId, + String developerName, String teamName ) { - public static DeveloperListResponseDto from (Member member) { + public static DeveloperListResponseDto from (Developer developer) { return DeveloperListResponseDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .teamName(member.getTeam().getDescription()) + .developerId(developer.getId()) + .developerName(developer.getDeveloperName()) + .teamName(developer.getTeam().getDescription()) .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 index 9d25826..72e419b 100644 --- a/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java +++ b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java @@ -1,17 +1,17 @@ package ceos.vote.domain.vote.dto.response; -import ceos.vote.domain.member.entity.Member; +import ceos.vote.domain.developer.entity.Developer; import lombok.Builder; @Builder public record DeveloperVoteResultResponseDto( - String memberName, + String developerName, int count ) { - public static DeveloperVoteResultResponseDto from(Member m) { + public static DeveloperVoteResultResponseDto from(Developer developer) { return DeveloperVoteResultResponseDto.builder() - .memberName(m.getName()) - .count(m.getCount()) + .developerName(developer.getDeveloperName()) + .count(developer.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 index 5f95757..3bc2031 100644 --- a/src/main/java/ceos/vote/domain/vote/service/VoteService.java +++ b/src/main/java/ceos/vote/domain/vote/service/VoteService.java @@ -1,5 +1,6 @@ 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; @@ -8,6 +9,7 @@ import ceos.vote.domain.vote.dto.response.DeveloperVoteResultResponseDto; import ceos.vote.domain.vote.dto.response.TeamVoteResultResponseDto; 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; @@ -15,6 +17,7 @@ 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.*; @@ -24,16 +27,17 @@ @RequiredArgsConstructor public class VoteService { - private final MemberRepository memberRepository; private final TeamRepository teamRepository; + private final MemberRepository memberRepository; + private final DeveloperRepository developerRepository; // [GET] 개발자 리스트 조회 public List getDeveloperList(String type) { PartType partType = PartType.valueOf(type.toUpperCase()); - List members = memberRepository.findByPartOrderByCountDesc(partType); + List developers = developerRepository.findByPartOrderByCountDesc(partType); - return members.stream() + return developers.stream() .map(DeveloperListResponseDto::from) .collect(Collectors.toList()); } @@ -48,20 +52,20 @@ public List getTeamList() { } - // [PATCH] 개발자 파트장 투표 + // [POST] 개발자 파트장 투표 @Transactional - public void voteDeveloper(Long memberId, Member loginMember) { + public void voteDeveloper(Long developerId, Member loginMember) { - Member member = memberRepository.findById(memberId) + Developer developer = developerRepository.findById(developerId) .orElseThrow(() -> new ApplicationException(NOT_FOUND_USER)); // 나 자신에게 투표할 수 없음 - if (member == loginMember) + if (Objects.equals(developer.getDeveloperName(), loginMember.getName())) throw new ApplicationException(BAD_REQUEST_SELF); // 같은 파트에게만 투표할 수 있음 - if (member.getPart() != loginMember.getPart()) + if (developer.getPart() != loginMember.getPart()) throw new ApplicationException(BAD_REQUEST_DEVELOPER); // 이미 투표했다면 다시 투표할 수 없음 - PartType type = member.getPart(); + PartType type = developer.getPart(); if (type.equals(PartType.BACKEND)) { if (loginMember.isVoteBack()) throw new ApplicationException(ALREADY_VOTE_DEVELOPER); @@ -72,12 +76,12 @@ else if (type.equals(PartType.FRONTEND)) { throw new ApplicationException(ALREADY_VOTE_DEVELOPER); loginMember.voteToFront(); } - member.voteToMe(); - memberRepository.save(member); + developer.voteToMe(); + developerRepository.save(developer); memberRepository.save(loginMember); } - // [PATCH] 팀 투표 + // [POST] 팀 투표 @Transactional public void voteTeam(Long teamId, Member loginMember) { @@ -99,9 +103,9 @@ public void voteTeam(Long teamId, Member loginMember) { // [GET] 개발자 파트장 투표 결과 조회 public List getDeveloperVoteResult(String type) { PartType partType = PartType.valueOf(type.toUpperCase()); - List members = memberRepository.findByPartOrderByCountDesc(partType); + List developers = developerRepository.findByPartOrderByCountDesc(partType); - return members.stream() + return developers.stream() .map(DeveloperVoteResultResponseDto::from) .collect(Collectors.toList()); } diff --git a/src/main/java/ceos/vote/global/repository/MemberRepository.java b/src/main/java/ceos/vote/global/repository/MemberRepository.java index d41f39c..86b8c26 100644 --- a/src/main/java/ceos/vote/global/repository/MemberRepository.java +++ b/src/main/java/ceos/vote/global/repository/MemberRepository.java @@ -1,10 +1,8 @@ package ceos.vote.global.repository; import ceos.vote.domain.member.entity.Member; -import ceos.vote.domain.member.entity.PartType; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; import java.util.Optional; public interface MemberRepository extends JpaRepository { @@ -12,8 +10,4 @@ public interface MemberRepository extends JpaRepository { Optional findByUserId(String userId); Boolean existsMemberByUserId(String userId); - - List findByPart(PartType part); - - List findByPartOrderByCountDesc(PartType part); } From b88a8a52f803998b7d15c3ecda866079b1029472 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 30 Dec 2024 17:40:47 +0900 Subject: [PATCH 41/56] =?UTF-8?q?feat:=20=20api=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20+=20team,=20developer=20=EC=83=9D=EC=84=B1=20permit?= =?UTF-8?q?=20all?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ceos/vote/domain/vote/controller/VoteController.java | 2 +- src/main/java/ceos/vote/global/config/SecurityConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ceos/vote/domain/vote/controller/VoteController.java b/src/main/java/ceos/vote/domain/vote/controller/VoteController.java index d605664..e911849 100644 --- a/src/main/java/ceos/vote/domain/vote/controller/VoteController.java +++ b/src/main/java/ceos/vote/domain/vote/controller/VoteController.java @@ -16,7 +16,7 @@ import java.util.List; @RestController -@RequestMapping("/api/v1/vote") +@RequestMapping("/api/vote") @RequiredArgsConstructor @Tag(name = "Vote", description = "투표 관련 API") public class VoteController { diff --git a/src/main/java/ceos/vote/global/config/SecurityConfig.java b/src/main/java/ceos/vote/global/config/SecurityConfig.java index 90145a3..4b0406a 100644 --- a/src/main/java/ceos/vote/global/config/SecurityConfig.java +++ b/src/main/java/ceos/vote/global/config/SecurityConfig.java @@ -91,7 +91,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { http .authorizeHttpRequests((auth) -> auth .requestMatchers("/login", "/", "/api/auth/**", "/swagger-ui.html", "/swagger-ui/**","/v3/api-docs/**").permitAll() - .requestMatchers("/api/v1/vote/developer", "/api/v1/vote/team").permitAll() // 개발자, 팀 조회하는건 허용 + .requestMatchers("/api/vote/developer", "/api/vote/team", "/api/team", "/api/developer").permitAll() // 개발자, 팀 조회하는건 허용 .anyRequest().authenticated() ); From 62da5939fb9fb667aa498e7971430000d15fe126 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 30 Dec 2024 22:48:43 +0900 Subject: [PATCH 42/56] =?UTF-8?q?fix=20:=20=ED=8C=8C=ED=8A=B8=EC=9E=A5=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20=EB=B6=80=EB=B6=84=20tea?= =?UTF-8?q?mName=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/vote/dto/response/DeveloperListResponseDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a49fd74..9d91117 100644 --- a/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java +++ b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperListResponseDto.java @@ -14,7 +14,7 @@ public static DeveloperListResponseDto from (Developer developer) { return DeveloperListResponseDto.builder() .developerId(developer.getId()) .developerName(developer.getDeveloperName()) - .teamName(developer.getTeam().getDescription()) + .teamName(developer.getTeam().getTeamName()) .build(); } } From 03546b2fc300db56b39264a9ebb1d27e7d2c42f6 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 2 Jan 2025 22:17:47 +0900 Subject: [PATCH 43/56] =?UTF-8?q?fix:=20@Login=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20MemberController=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/controller/MemberController.java | 8 ++++---- .../ceos/vote/domain/member/service/MemberService.java | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/ceos/vote/domain/member/controller/MemberController.java b/src/main/java/ceos/vote/domain/member/controller/MemberController.java index 30e7db0..9e38e44 100644 --- a/src/main/java/ceos/vote/domain/member/controller/MemberController.java +++ b/src/main/java/ceos/vote/domain/member/controller/MemberController.java @@ -2,7 +2,9 @@ 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; @@ -22,10 +24,8 @@ public class MemberController { @Operation(summary = "회원 기본 정보 조회", description = "회원의 기본 정보를 조회하는 API") @GetMapping - public CommonResponse getMemberInfo(@AuthenticationPrincipal CustomUserDetails userDetails) { + public CommonResponse getMemberInfo(@Login Member loginMember) { - Long memberId = userDetails.getMemberId(); - - return new CommonResponse<>(memberService.getMemberInfo(memberId), "회원 기본 정보 조회를 성공하였습니다."); + return new CommonResponse<>(memberService.getMemberInfo(loginMember), "회원 기본 정보 조회를 성공하였습니다."); } } diff --git a/src/main/java/ceos/vote/domain/member/service/MemberService.java b/src/main/java/ceos/vote/domain/member/service/MemberService.java index 45c983c..66f2e16 100644 --- a/src/main/java/ceos/vote/domain/member/service/MemberService.java +++ b/src/main/java/ceos/vote/domain/member/service/MemberService.java @@ -22,10 +22,8 @@ public Member findMemberById(Long memberId) { } // [GET] 회원 기본 정보 조회 - public MemberResponseDto getMemberInfo(Long memberId) { + public MemberResponseDto getMemberInfo(Member loginMember) { - Member member = findMemberById(memberId); - - return MemberResponseDto.from(member); + return MemberResponseDto.from(loginMember); } } From 2b055b82a8614d2ef945ea5328fd02490ab5ce66 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 2 Jan 2025 23:00:44 +0900 Subject: [PATCH 44/56] =?UTF-8?q?fix:=20email=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/dto/request/SignupRequestDto.java | 8 -------- .../domain/member/dto/response/MemberResponseDto.java | 2 -- src/main/java/ceos/vote/domain/member/entity/Member.java | 6 +----- 3 files changed, 1 insertion(+), 15 deletions(-) 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 index 4d7776b..10bdd41 100644 --- a/src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java +++ b/src/main/java/ceos/vote/domain/member/dto/request/SignupRequestDto.java @@ -5,10 +5,7 @@ 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.Email; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; public record SignupRequestDto( @@ -24,10 +21,6 @@ public record SignupRequestDto( @NotNull(message = "비밀번호는 필수 입력 사항입니다.") String password, - @Schema(description = "이메일", example = "ceos@gmail.com") - @Email(message = "유효한 이메일 주소를 입력해주세요.") - String email, - @Schema(description = "소속 팀명", example = "포토그라운드 | 엔젤브릿지 | 페달지니 | 케이크웨이 | 커피딜") @NotNull(message = "소속 팀명은 필수 입력 사항입니다.") String team, @@ -40,7 +33,6 @@ public Member toEntity(String encodedPassword, PartType part, Team team) { return Member.builder() .userId(userId) .password(encodedPassword) - .email(email) .role("ROLE_USER") .part(part) .name(name) 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 index 19df169..8f0c19e 100644 --- a/src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java +++ b/src/main/java/ceos/vote/domain/member/dto/response/MemberResponseDto.java @@ -8,7 +8,6 @@ public record MemberResponseDto ( String userId, String name, - String email, String team, String part, Boolean voteBack, @@ -19,7 +18,6 @@ public static MemberResponseDto from(Member member) { return MemberResponseDto.builder() .userId(member.getUserId()) .name(member.getName()) - .email(member.getEmail()) .team(member.getTeam().getDescription()) .part(member.getPart().getDescription()) .voteBack(member.isVoteBack()) diff --git a/src/main/java/ceos/vote/domain/member/entity/Member.java b/src/main/java/ceos/vote/domain/member/entity/Member.java index feae75f..a394b1b 100644 --- a/src/main/java/ceos/vote/domain/member/entity/Member.java +++ b/src/main/java/ceos/vote/domain/member/entity/Member.java @@ -25,9 +25,6 @@ public class Member extends BaseEntity { @Column(nullable = false) private String password; - @Column(length = 320, nullable = false) - private String email; - private String role; @Enumerated(EnumType.STRING) @@ -63,11 +60,10 @@ public void voteToTeam() { } @Builder - public Member(String userId, String password, String email, String role, PartType part, String name, Team team) { + public Member(String userId, String password, String role, PartType part, String name, Team team) { this.userId = userId; this.password = password; - this.email = email; this.role = role; this.part = part; this.name = name; From 4e0fb8d2fbb280c38598ddaa3a943eab75e59d00 Mon Sep 17 00:00:00 2001 From: Hanseul Lee Date: Thu, 2 Jan 2025 23:27:10 +0900 Subject: [PATCH 45/56] =?UTF-8?q?fix:=20AuthenticationPrincipal=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/domain/member/controller/MemberController.java | 6 ++++-- .../java/ceos/vote/domain/member/service/MemberService.java | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/ceos/vote/domain/member/controller/MemberController.java b/src/main/java/ceos/vote/domain/member/controller/MemberController.java index 9e38e44..71e7da9 100644 --- a/src/main/java/ceos/vote/domain/member/controller/MemberController.java +++ b/src/main/java/ceos/vote/domain/member/controller/MemberController.java @@ -24,8 +24,10 @@ public class MemberController { @Operation(summary = "회원 기본 정보 조회", description = "회원의 기본 정보를 조회하는 API") @GetMapping - public CommonResponse getMemberInfo(@Login Member loginMember) { + public CommonResponse getMemberInfo(@AuthenticationPrincipal CustomUserDetails userDetails) { - return new CommonResponse<>(memberService.getMemberInfo(loginMember), "회원 기본 정보 조회를 성공하였습니다."); + Long memberId = userDetails.getMemberId(); + + return new CommonResponse<>(memberService.getMemberInfo(memberId), "회원 기본 정보 조회를 성공하였습니다."); } } diff --git a/src/main/java/ceos/vote/domain/member/service/MemberService.java b/src/main/java/ceos/vote/domain/member/service/MemberService.java index 66f2e16..45c983c 100644 --- a/src/main/java/ceos/vote/domain/member/service/MemberService.java +++ b/src/main/java/ceos/vote/domain/member/service/MemberService.java @@ -22,8 +22,10 @@ public Member findMemberById(Long memberId) { } // [GET] 회원 기본 정보 조회 - public MemberResponseDto getMemberInfo(Member loginMember) { + public MemberResponseDto getMemberInfo(Long memberId) { - return MemberResponseDto.from(loginMember); + Member member = findMemberById(memberId); + + return MemberResponseDto.from(member); } } From 2a984048d056d18914dba7a99020c19153ae781d Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 14:08:15 +0900 Subject: [PATCH 46/56] =?UTF-8?q?feat:=20=ED=8C=8C=ED=8A=B8=EC=9E=A5=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EA=B2=B0=EA=B3=BC=EC=97=90=20teamName=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/dto/response/DeveloperVoteResultResponseDto.java | 2 ++ 1 file changed, 2 insertions(+) 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 index 72e419b..f96462a 100644 --- a/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java +++ b/src/main/java/ceos/vote/domain/vote/dto/response/DeveloperVoteResultResponseDto.java @@ -6,11 +6,13 @@ @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(); } From 43871e05ce61246f85701d6e1bc55cc08301dc85 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 14:30:49 +0900 Subject: [PATCH 47/56] =?UTF-8?q?feat:=20=EA=B0=9C=EB=B0=9C=EC=9E=90=20?= =?UTF-8?q?=EC=9E=90=EA=B8=B0=EC=86=8C=EA=B0=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20dto=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/DeveloperIntroductionResponseDto.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/ceos/vote/domain/vote/dto/response/DeveloperIntroductionResponseDto.java 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 +) { +} From 7248eef8a4675f0cf557ba3107adbd218d25540c Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 14:31:01 +0900 Subject: [PATCH 48/56] =?UTF-8?q?feat:=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ceos/vote/domain/developer/entity/Developer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/ceos/vote/domain/developer/entity/Developer.java b/src/main/java/ceos/vote/domain/developer/entity/Developer.java index 7fe830c..5b1cd37 100644 --- a/src/main/java/ceos/vote/domain/developer/entity/Developer.java +++ b/src/main/java/ceos/vote/domain/developer/entity/Developer.java @@ -28,6 +28,8 @@ public class Developer { @JoinColumn(name = "team_id") private Team team; + private String introduction; + private int count = 0; public void voteToMe() { @@ -35,11 +37,12 @@ public void voteToMe() { } @Builder - public Developer(String developerName, PartType part, Team team) { + public Developer(String developerName, PartType part, Team team, String introduction) { this.developerName = developerName; this.part = part; this.team = team; + this.introduction = introduction; } } From b760a71b2b75c9553dafa35367465ef18717f875 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 14:31:06 +0900 Subject: [PATCH 49/56] =?UTF-8?q?feat:=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ceos/vote/domain/developer/dto/DeveloperRequestDto.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java b/src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java index eb17315..0b78e30 100644 --- a/src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java +++ b/src/main/java/ceos/vote/domain/developer/dto/DeveloperRequestDto.java @@ -14,6 +14,10 @@ public record DeveloperRequestDto( @NotNull(message = "소속 팀명은 필수 입력 사항입니다.") String team, + @Schema(description = "자기소개", example = "안녕하세요~ 이건 자기 소개입니다.") + @NotNull(message = "자기소개는 필수 입력 사항입니다.") + String introduction, + @Schema(description = "소속 파트", example = "backend | frontend") @NotNull(message = "소속 파트는 필수 입력 사항입니다.") String part From 2ffe0050f9ca820f6f9ce360cee941e0de6b8da8 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 14:31:21 +0900 Subject: [PATCH 50/56] =?UTF-8?q?feat:=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20api=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/vote/controller/VoteController.java | 11 +++++++---- .../vote/domain/vote/service/VoteService.java | 17 +++++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/ceos/vote/domain/vote/controller/VoteController.java b/src/main/java/ceos/vote/domain/vote/controller/VoteController.java index e911849..6a51d02 100644 --- a/src/main/java/ceos/vote/domain/vote/controller/VoteController.java +++ b/src/main/java/ceos/vote/domain/vote/controller/VoteController.java @@ -1,10 +1,7 @@ package ceos.vote.domain.vote.controller; import ceos.vote.domain.member.entity.Member; -import ceos.vote.domain.vote.dto.response.DeveloperListResponseDto; -import ceos.vote.domain.vote.dto.response.DeveloperVoteResultResponseDto; -import ceos.vote.domain.vote.dto.response.TeamListResponseDto; -import ceos.vote.domain.vote.dto.response.TeamVoteResultResponseDto; +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; @@ -23,6 +20,12 @@ 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 를 넘겨주세요") diff --git a/src/main/java/ceos/vote/domain/vote/service/VoteService.java b/src/main/java/ceos/vote/domain/vote/service/VoteService.java index 3bc2031..af0da8a 100644 --- a/src/main/java/ceos/vote/domain/vote/service/VoteService.java +++ b/src/main/java/ceos/vote/domain/vote/service/VoteService.java @@ -4,10 +4,7 @@ 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.DeveloperListResponseDto; -import ceos.vote.domain.vote.dto.response.TeamListResponseDto; -import ceos.vote.domain.vote.dto.response.DeveloperVoteResultResponseDto; -import ceos.vote.domain.vote.dto.response.TeamVoteResultResponseDto; +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; @@ -31,6 +28,18 @@ public class VoteService { 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) { From 80a0a079a6c8836692abe231053bfbe6d18df2d8 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 14:36:46 +0900 Subject: [PATCH 51/56] =?UTF-8?q?feat:=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20api=EB=8F=84?= =?UTF-8?q?=20permit=20all=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ceos/vote/global/config/SecurityConfig.java b/src/main/java/ceos/vote/global/config/SecurityConfig.java index 4b0406a..3b0c984 100644 --- a/src/main/java/ceos/vote/global/config/SecurityConfig.java +++ b/src/main/java/ceos/vote/global/config/SecurityConfig.java @@ -91,7 +91,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { 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").permitAll() // 개발자, 팀 조회하는건 허용 + .requestMatchers("/api/vote/developer", "/api/vote/team", "/api/team", "/api/developer", "/api/developer/detail").permitAll() // 개발자, 팀 조회하는건 허용 .anyRequest().authenticated() ); From 9741fa796ebabf7d17130a40ca55e73a66badd8a Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 14:45:27 +0900 Subject: [PATCH 52/56] =?UTF-8?q?feat:=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20api=EB=8F=84?= =?UTF-8?q?=20permit=20all=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ceos/vote/global/config/SecurityConfig.java b/src/main/java/ceos/vote/global/config/SecurityConfig.java index 3b0c984..3ffdde7 100644 --- a/src/main/java/ceos/vote/global/config/SecurityConfig.java +++ b/src/main/java/ceos/vote/global/config/SecurityConfig.java @@ -91,7 +91,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { 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/developer/detail").permitAll() // 개발자, 팀 조회하는건 허용 + .requestMatchers("/api/vote/developer", "/api/vote/team", "/api/team", "/api/developer", "/api/developer/detail/**").permitAll() // 개발자, 팀 조회하는건 허용 .anyRequest().authenticated() ); From dd4b6ac0a242b7ba3f5d8d03677a93c448b7907e Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 15:07:23 +0900 Subject: [PATCH 53/56] =?UTF-8?q?feat:=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20api=EB=8F=84?= =?UTF-8?q?=20permit=20all=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ceos/vote/global/config/SecurityConfig.java b/src/main/java/ceos/vote/global/config/SecurityConfig.java index 3ffdde7..cc0bd95 100644 --- a/src/main/java/ceos/vote/global/config/SecurityConfig.java +++ b/src/main/java/ceos/vote/global/config/SecurityConfig.java @@ -91,7 +91,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { 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/developer/detail/**").permitAll() // 개발자, 팀 조회하는건 허용 + .requestMatchers("/api/vote/developer", "/api/vote/team", "/api/team", "/api/developer", "/api/vote/developer/detail/**").permitAll() // 개발자, 팀 조회하는건 허용 .anyRequest().authenticated() ); From 848fcec6fdff3d6f8b6b83c0ebeafa6ae9ce4c9b Mon Sep 17 00:00:00 2001 From: choiseoji Date: Sat, 4 Jan 2025 20:46:14 +0900 Subject: [PATCH 54/56] =?UTF-8?q?feat:=20developer=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=A0=EB=95=8C=20introduction=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ceos/vote/domain/developer/service/DeveloperService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ceos/vote/domain/developer/service/DeveloperService.java b/src/main/java/ceos/vote/domain/developer/service/DeveloperService.java index 1c56e11..54bb8a3 100644 --- a/src/main/java/ceos/vote/domain/developer/service/DeveloperService.java +++ b/src/main/java/ceos/vote/domain/developer/service/DeveloperService.java @@ -42,6 +42,7 @@ else if (developerRequestDto.team().equals("커피딜")) .developerName(developerRequestDto.name()) .part(partType) .team(team) + .introduction(developerRequestDto.introduction()) .build(); developerRepository.save(developer); } From da94105417d85c181e6494cfd6e33720d4204c14 Mon Sep 17 00:00:00 2001 From: choiseoji Date: Mon, 6 Jan 2025 00:07:48 +0900 Subject: [PATCH 55/56] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=A0=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EC=A3=BC=EC=86=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ceos/vote/global/config/SecurityConfig.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ceos/vote/global/config/SecurityConfig.java b/src/main/java/ceos/vote/global/config/SecurityConfig.java index cc0bd95..112f515 100644 --- a/src/main/java/ceos/vote/global/config/SecurityConfig.java +++ b/src/main/java/ceos/vote/global/config/SecurityConfig.java @@ -61,9 +61,9 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { configuration.setAllowedOrigins(Arrays.asList( "http://localhost:3000", "http://localhost:8080", - "http://localhost:8081" -// "http://배포주소:8080", -// "http://배포주소:8081" + "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")); From 4e86872c1b0c3d4511237ee53dcdcfb2688f1b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=84=9C=EC=A7=80?= <103797531+choiseoji@users.noreply.github.com> Date: Mon, 6 Jan 2025 00:42:26 +0900 Subject: [PATCH 56/56] Update README.md --- README.md | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) 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** + +![vote (1) (1)](https://github.com/user-attachments/assets/97c8d1d2-0247-48b3-845d-d1a0ec284b9a) + +1. **Member** + - 로그인한 사용자(멤버)를 의미한다. + - 투표 권한이 있으며, 아래의 투표에 각각 최대 **1회 참여**할 수 있다. + - **vote_back, vote_front**: 개발자 투표 + - **vote_team**: 팀 투표 +2. **Team**과 **Developer** + - 투표 가능한 **후보**를 의미한다. + - 각 객체는 자신의 투표수를 나타내는 **count** 필드를 가진다. + +스크린샷 2025-01-06 오전 12 18 48 + +**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 요청을 했는데 다음과 같은 에러가 발생했다. + +KakaoTalk_Photo_2025-01-05-20-33-15 (1) + +### 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가 응답을 받아 클라이언트에게 전달.