From af718a09826eab361861d9e4614114cffa66c68f Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 16 Jan 2024 21:28:14 +0900 Subject: [PATCH 001/158] =?UTF-8?q?feat:=20=EC=8A=A4=ED=94=84=EB=A7=81=20?= =?UTF-8?q?=EB=B6=80=ED=8A=B8=20=EC=B4=88=EA=B8=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 37 +++ build.gradle | 35 +++ gradlew | 249 ++++++++++++++++++ gradlew.bat | 92 +++++++ settings.gradle | 1 + .../SolidConnectionApplication.java | 13 + src/main/resources/application.properties | 1 + .../SolidConnectionApplicationTests.java | 13 + 8 files changed, 441 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/example/solidconnection/SolidConnectionApplication.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..c2065bc26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..ad4d28323 --- /dev/null +++ b/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..1aa94a426 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=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 000000000..93e3f59f1 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..ab5896b5d --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'solid-connection' diff --git a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java new file mode 100644 index 000000000..1e378189c --- /dev/null +++ b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java @@ -0,0 +1,13 @@ +package com.example.solidconnection; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SolidConnectionApplication { + + public static void main(String[] args) { + SpringApplication.run(SolidConnectionApplication.class, args); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java b/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java new file mode 100644 index 000000000..76051385e --- /dev/null +++ b/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.solidconnection; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SolidConnectionApplicationTests { + + @Test + void contextLoads() { + } + +} From 9d4502a4225afe0efc785d454081e3c392cf153e Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 17 Jan 2024 18:54:28 +0900 Subject: [PATCH 002/158] =?UTF-8?q?feat:=20=EB=8F=84=EC=BB=A4=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ Dockerfile | 14 ++++++++++++++ build.gradle | 2 -- src/main/resources/application.properties | 1 - 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 Dockerfile delete mode 100644 src/main/resources/application.properties diff --git a/.gitignore b/.gitignore index c2065bc26..488d587d8 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### YML ### +*.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..847788e44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# JDK 버전 설정 +FROM openjdk:17-jdk + +# JAR_FILE 변수 정의 +ARG JAR_FILE=./build/libs/solid-connection-0.0.1-SNAPSHOT.jar + +# JAR 파일 메인 디렉토리에 복사 +COPY ${JAR_FILE} app.jar + +# 시스템 진입점 정의 +ENTRYPOINT ["java","-jar","/app.jar"] + +# 볼륨 설정 +VOLUME /tmp \ No newline at end of file diff --git a/build.gradle b/build.gradle index ad4d28323..802f27812 100644 --- a/build.gradle +++ b/build.gradle @@ -22,10 +22,8 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b1378917..000000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - From 30fe148082b65e0a6918185573cf2309c4a45b17 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 17 Jan 2024 19:04:54 +0900 Subject: [PATCH 003/158] =?UTF-8?q?fix:=20GradleWrapper=20=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 zcma&NWl&^owk(X(xVyW%ySuwf;qI=D6|RlDJ2cR^yEKh!@I- zp9QeisK*rlxC>+~7Dk4IxIRsKBHqdR9b3+fyL=ynHmIDe&|>O*VlvO+%z5;9Z$|DJ zb4dO}-R=MKr^6EKJiOrJdLnCJn>np?~vU-1sSFgPu;pthGwf}bG z(1db%xwr#x)r+`4AGu$j7~u2MpVs3VpLp|mx&;>`0p0vH6kF+D2CY0fVdQOZ@h;A` z{infNyvmFUiu*XG}RNMNwXrbec_*a3N=2zJ|Wh5z* z5rAX$JJR{#zP>KY**>xHTuw?|-Rg|o24V)74HcfVT;WtQHXlE+_4iPE8QE#DUm%x0 zEKr75ur~W%w#-My3Tj`hH6EuEW+8K-^5P62$7Sc5OK+22qj&Pd1;)1#4tKihi=~8C zHiQSst0cpri6%OeaR`PY>HH_;CPaRNty%WTm4{wDK8V6gCZlG@U3$~JQZ;HPvDJcT1V{ z?>H@13MJcCNe#5z+MecYNi@VT5|&UiN1D4ATT+%M+h4c$t;C#UAs3O_q=GxK0}8%8 z8J(_M9bayxN}69ex4dzM_P3oh@ZGREjVvn%%r7=xjkqxJP4kj}5tlf;QosR=%4L5y zWhgejO=vao5oX%mOHbhJ8V+SG&K5dABn6!WiKl{|oPkq(9z8l&Mm%(=qGcFzI=eLu zWc_oCLyf;hVlB@dnwY98?75B20=n$>u3b|NB28H0u-6Rpl((%KWEBOfElVWJx+5yg z#SGqwza7f}$z;n~g%4HDU{;V{gXIhft*q2=4zSezGK~nBgu9-Q*rZ#2f=Q}i2|qOp z!!y4p)4o=LVUNhlkp#JL{tfkhXNbB=Ox>M=n6soptJw-IDI|_$is2w}(XY>a=H52d z3zE$tjPUhWWS+5h=KVH&uqQS=$v3nRs&p$%11b%5qtF}S2#Pc`IiyBIF4%A!;AVoI zXU8-Rpv!DQNcF~(qQnyyMy=-AN~U>#&X1j5BLDP{?K!%h!;hfJI>$mdLSvktEr*89 zdJHvby^$xEX0^l9g$xW-d?J;L0#(`UT~zpL&*cEh$L|HPAu=P8`OQZV!-}l`noSp_ zQ-1$q$R-gDL)?6YaM!=8H=QGW$NT2SeZlb8PKJdc=F-cT@j7Xags+Pr*jPtlHFnf- zh?q<6;)27IdPc^Wdy-mX%2s84C1xZq9Xms+==F4);O`VUASmu3(RlgE#0+#giLh-& zcxm3_e}n4{%|X zJp{G_j+%`j_q5}k{eW&TlP}J2wtZ2^<^E(O)4OQX8FDp6RJq!F{(6eHWSD3=f~(h} zJXCf7=r<16X{pHkm%yzYI_=VDP&9bmI1*)YXZeB}F? z(%QsB5fo*FUZxK$oX~X^69;x~j7ms8xlzpt-T15e9}$4T-pC z6PFg@;B-j|Ywajpe4~bk#S6(fO^|mm1hKOPfA%8-_iGCfICE|=P_~e;Wz6my&)h_~ zkv&_xSAw7AZ%ThYF(4jADW4vg=oEdJGVOs>FqamoL3Np8>?!W#!R-0%2Bg4h?kz5I zKV-rKN2n(vUL%D<4oj@|`eJ>0i#TmYBtYmfla;c!ATW%;xGQ0*TW@PTlGG><@dxUI zg>+3SiGdZ%?5N=8uoLA|$4isK$aJ%i{hECP$bK{J#0W2gQ3YEa zZQ50Stn6hqdfxJ*9#NuSLwKFCUGk@c=(igyVL;;2^wi4o30YXSIb2g_ud$ zgpCr@H0qWtk2hK8Q|&wx)}4+hTYlf;$a4#oUM=V@Cw#!$(nOFFpZ;0lc!qd=c$S}Z zGGI-0jg~S~cgVT=4Vo)b)|4phjStD49*EqC)IPwyeKBLcN;Wu@Aeph;emROAwJ-0< z_#>wVm$)ygH|qyxZaet&(Vf%pVdnvKWJn9`%DAxj3ot;v>S$I}jJ$FLBF*~iZ!ZXE zkvui&p}fI0Y=IDX)mm0@tAd|fEHl~J&K}ZX(Mm3cm1UAuwJ42+AO5@HwYfDH7ipIc zmI;1J;J@+aCNG1M`Btf>YT>~c&3j~Qi@Py5JT6;zjx$cvOQW@3oQ>|}GH?TW-E z1R;q^QFjm5W~7f}c3Ww|awg1BAJ^slEV~Pk`Kd`PS$7;SqJZNj->it4DW2l15}xP6 zoCl$kyEF%yJni0(L!Z&14m!1urXh6Btj_5JYt1{#+H8w?5QI%% zo-$KYWNMJVH?Hh@1n7OSu~QhSswL8x0=$<8QG_zepi_`y_79=nK=_ZP_`Em2UI*tyQoB+r{1QYZCpb?2OrgUw#oRH$?^Tj!Req>XiE#~B|~ z+%HB;=ic+R@px4Ld8mwpY;W^A%8%l8$@B@1m5n`TlKI6bz2mp*^^^1mK$COW$HOfp zUGTz-cN9?BGEp}5A!mDFjaiWa2_J2Iq8qj0mXzk; z66JBKRP{p%wN7XobR0YjhAuW9T1Gw3FDvR5dWJ8ElNYF94eF3ebu+QwKjtvVu4L zI9ip#mQ@4uqVdkl-TUQMb^XBJVLW(-$s;Nq;@5gr4`UfLgF$adIhd?rHOa%D);whv z=;krPp~@I+-Z|r#s3yCH+c1US?dnm+C*)r{m+86sTJusLdNu^sqLrfWed^ndHXH`m zd3#cOe3>w-ga(Dus_^ppG9AC>Iq{y%%CK+Cro_sqLCs{VLuK=dev>OL1dis4(PQ5R zcz)>DjEkfV+MO;~>VUlYF00SgfUo~@(&9$Iy2|G0T9BSP?&T22>K46D zL*~j#yJ?)^*%J3!16f)@Y2Z^kS*BzwfAQ7K96rFRIh>#$*$_Io;z>ux@}G98!fWR@ zGTFxv4r~v)Gsd|pF91*-eaZ3Qw1MH$K^7JhWIdX%o$2kCbvGDXy)a?@8T&1dY4`;L z4Kn+f%SSFWE_rpEpL9bnlmYq`D!6F%di<&Hh=+!VI~j)2mfil03T#jJ_s?}VV0_hp z7T9bWxc>Jm2Z0WMU?`Z$xE74Gu~%s{mW!d4uvKCx@WD+gPUQ zV0vQS(Ig++z=EHN)BR44*EDSWIyT~R4$FcF*VEY*8@l=218Q05D2$|fXKFhRgBIEE zdDFB}1dKkoO^7}{5crKX!p?dZWNz$m>1icsXG2N+((x0OIST9Zo^DW_tytvlwXGpn zs8?pJXjEG;T@qrZi%#h93?FP$!&P4JA(&H61tqQi=opRzNpm zkrG}$^t9&XduK*Qa1?355wd8G2CI6QEh@Ua>AsD;7oRUNLPb76m4HG3K?)wF~IyS3`fXuNM>${?wmB zpVz;?6_(Fiadfd{vUCBM*_kt$+F3J+IojI;9L(gc9n3{sEZyzR9o!_mOwFC#tQ{Q~ zP3-`#uK#tP3Q7~Q;4H|wjZHO8h7e4IuBxl&vz2w~D8)w=Wtg31zpZhz%+kzSzL*dV zwp@{WU4i;hJ7c2f1O;7Mz6qRKeASoIv0_bV=i@NMG*l<#+;INk-^`5w@}Dj~;k=|}qM1vq_P z|GpBGe_IKq|LNy9SJhKOQ$c=5L{Dv|Q_lZl=-ky*BFBJLW9&y_C|!vyM~rQx=!vun z?rZJQB5t}Dctmui5i31C_;_}CEn}_W%>oSXtt>@kE1=JW*4*v4tPp;O6 zmAk{)m!)}34pTWg8{i>($%NQ(Tl;QC@J@FfBoc%Gr&m560^kgSfodAFrIjF}aIw)X zoXZ`@IsMkc8_=w%-7`D6Y4e*CG8k%Ud=GXhsTR50jUnm+R*0A(O3UKFg0`K;qp1bl z7``HN=?39ic_kR|^R^~w-*pa?Vj#7|e9F1iRx{GN2?wK!xR1GW!qa=~pjJb-#u1K8 zeR?Y2i-pt}yJq;SCiVHODIvQJX|ZJaT8nO+(?HXbLefulKKgM^B(UIO1r+S=7;kLJ zcH}1J=Px2jsh3Tec&v8Jcbng8;V-`#*UHt?hB(pmOipKwf3Lz8rG$heEB30Sg*2rx zV<|KN86$soN(I!BwO`1n^^uF2*x&vJ$2d$>+`(romzHP|)K_KkO6Hc>_dwMW-M(#S zK(~SiXT1@fvc#U+?|?PniDRm01)f^#55;nhM|wi?oG>yBsa?~?^xTU|fX-R(sTA+5 zaq}-8Tx7zrOy#3*JLIIVsBmHYLdD}!0NP!+ITW+Thn0)8SS!$@)HXwB3tY!fMxc#1 zMp3H?q3eD?u&Njx4;KQ5G>32+GRp1Ee5qMO0lZjaRRu&{W<&~DoJNGkcYF<5(Ab+J zgO>VhBl{okDPn78<%&e2mR{jwVCz5Og;*Z;;3%VvoGo_;HaGLWYF7q#jDX=Z#Ml`H z858YVV$%J|e<1n`%6Vsvq7GmnAV0wW4$5qQ3uR@1i>tW{xrl|ExywIc?fNgYlA?C5 zh$ezAFb5{rQu6i7BSS5*J-|9DQ{6^BVQ{b*lq`xS@RyrsJN?-t=MTMPY;WYeKBCNg z^2|pN!Q^WPJuuO4!|P@jzt&tY1Y8d%FNK5xK(!@`jO2aEA*4 zkO6b|UVBipci?){-Ke=+1;mGlND8)6+P;8sq}UXw2hn;fc7nM>g}GSMWu&v&fqh

iViYT=fZ(|3Ox^$aWPp4a8h24tD<|8-!aK0lHgL$N7Efw}J zVIB!7=T$U`ao1?upi5V4Et*-lTG0XvExbf!ya{cua==$WJyVG(CmA6Of*8E@DSE%L z`V^$qz&RU$7G5mg;8;=#`@rRG`-uS18$0WPN@!v2d{H2sOqP|!(cQ@ zUHo!d>>yFArLPf1q`uBvY32miqShLT1B@gDL4XoVTK&@owOoD)OIHXrYK-a1d$B{v zF^}8D3Y^g%^cnvScOSJR5QNH+BI%d|;J;wWM3~l>${fb8DNPg)wrf|GBP8p%LNGN# z3EaIiItgwtGgT&iYCFy9-LG}bMI|4LdmmJt@V@% zb6B)1kc=T)(|L@0;wr<>=?r04N;E&ef+7C^`wPWtyQe(*pD1pI_&XHy|0gIGHMekd zF_*M4yi6J&Z4LQj65)S zXwdM{SwUo%3SbPwFsHgqF@V|6afT|R6?&S;lw=8% z3}@9B=#JI3@B*#4s!O))~z zc>2_4Q_#&+5V`GFd?88^;c1i7;Vv_I*qt!_Yx*n=;rj!82rrR2rQ8u5(Ejlo{15P% zs~!{%XJ>FmJ})H^I9bn^Re&38H{xA!0l3^89k(oU;bZWXM@kn$#aoS&Y4l^-WEn-fH39Jb9lA%s*WsKJQl?n9B7_~P z-XM&WL7Z!PcoF6_D>V@$CvUIEy=+Z&0kt{szMk=f1|M+r*a43^$$B^MidrT0J;RI` z(?f!O<8UZkm$_Ny$Hth1J#^4ni+im8M9mr&k|3cIgwvjAgjH z8`N&h25xV#v*d$qBX5jkI|xOhQn!>IYZK7l5#^P4M&twe9&Ey@@GxYMxBZq2e7?`q z$~Szs0!g{2fGcp9PZEt|rdQ6bhAgpcLHPz?f-vB?$dc*!9OL?Q8mn7->bFD2Si60* z!O%y)fCdMSV|lkF9w%x~J*A&srMyYY3{=&$}H zGQ4VG_?$2X(0|vT0{=;W$~icCI{b6W{B!Q8xdGhF|D{25G_5_+%s(46lhvNLkik~R z>nr(&C#5wwOzJZQo9m|U<;&Wk!_#q|V>fsmj1g<6%hB{jGoNUPjgJslld>xmODzGjYc?7JSuA?A_QzjDw5AsRgi@Y|Z0{F{!1=!NES-#*f^s4l0Hu zz468))2IY5dmD9pa*(yT5{EyP^G>@ZWumealS-*WeRcZ}B%gxq{MiJ|RyX-^C1V=0 z@iKdrGi1jTe8Ya^x7yyH$kBNvM4R~`fbPq$BzHum-3Zo8C6=KW@||>zsA8-Y9uV5V z#oq-f5L5}V<&wF4@X@<3^C%ptp6+Ce)~hGl`kwj)bsAjmo_GU^r940Z-|`<)oGnh7 zFF0Tde3>ui?8Yj{sF-Z@)yQd~CGZ*w-6p2U<8}JO-sRsVI5dBji`01W8A&3$?}lxBaC&vn0E$c5tW* zX>5(zzZ=qn&!J~KdsPl;P@bmA-Pr8T*)eh_+Dv5=Ma|XSle6t(k8qcgNyar{*ReQ8 zTXwi=8vr>!3Ywr+BhggHDw8ke==NTQVMCK`$69fhzEFB*4+H9LIvdt-#IbhZvpS}} zO3lz;P?zr0*0$%-Rq_y^k(?I{Mk}h@w}cZpMUp|ucs55bcloL2)($u%mXQw({Wzc~ z;6nu5MkjP)0C(@%6Q_I_vsWrfhl7Zpoxw#WoE~r&GOSCz;_ro6i(^hM>I$8y>`!wW z*U^@?B!MMmb89I}2(hcE4zN2G^kwyWCZp5JG>$Ez7zP~D=J^LMjSM)27_0B_X^C(M z`fFT+%DcKlu?^)FCK>QzSnV%IsXVcUFhFdBP!6~se&xxrIxsvySAWu++IrH;FbcY$ z2DWTvSBRfLwdhr0nMx+URA$j3i7_*6BWv#DXfym?ZRDcX9C?cY9sD3q)uBDR3uWg= z(lUIzB)G$Hr!){>E{s4Dew+tb9kvToZp-1&c?y2wn@Z~(VBhqz`cB;{E4(P3N2*nJ z_>~g@;UF2iG{Kt(<1PyePTKahF8<)pozZ*xH~U-kfoAayCwJViIrnqwqO}7{0pHw$ zs2Kx?s#vQr7XZ264>5RNKSL8|Ty^=PsIx^}QqOOcfpGUU4tRkUc|kc7-!Ae6!+B{o~7nFpm3|G5^=0#Bnm6`V}oSQlrX(u%OWnC zoLPy&Q;1Jui&7ST0~#+}I^&?vcE*t47~Xq#YwvA^6^} z`WkC)$AkNub|t@S!$8CBlwbV~?yp&@9h{D|3z-vJXgzRC5^nYm+PyPcgRzAnEi6Q^gslXYRv4nycsy-SJu?lMps-? zV`U*#WnFsdPLL)Q$AmD|0`UaC4ND07+&UmOu!eHruzV|OUox<+Jl|Mr@6~C`T@P%s zW7sgXLF2SSe9Fl^O(I*{9wsFSYb2l%-;&Pi^dpv!{)C3d0AlNY6!4fgmSgj_wQ*7Am7&$z;Jg&wgR-Ih;lUvWS|KTSg!&s_E9_bXBkZvGiC6bFKDWZxsD$*NZ#_8bl zG1P-#@?OQzED7@jlMJTH@V!6k;W>auvft)}g zhoV{7$q=*;=l{O>Q4a@ ziMjf_u*o^PsO)#BjC%0^h>Xp@;5$p{JSYDt)zbb}s{Kbt!T*I@Pk@X0zds6wsefuU zW$XY%yyRGC94=6mf?x+bbA5CDQ2AgW1T-jVAJbm7K(gp+;v6E0WI#kuACgV$r}6L? zd|Tj?^%^*N&b>Dd{Wr$FS2qI#Ucs1yd4N+RBUQiSZGujH`#I)mG&VKoDh=KKFl4=G z&MagXl6*<)$6P}*Tiebpz5L=oMaPrN+caUXRJ`D?=K9!e0f{@D&cZLKN?iNP@X0aF zE(^pl+;*T5qt?1jRC=5PMgV!XNITRLS_=9{CJExaQj;lt!&pdzpK?8p>%Mb+D z?yO*uSung=-`QQ@yX@Hyd4@CI^r{2oiu`%^bNkz+Nkk!IunjwNC|WcqvX~k=><-I3 zDQdbdb|!v+Iz01$w@aMl!R)koD77Xp;eZwzSl-AT zr@Vu{=xvgfq9akRrrM)}=!=xcs+U1JO}{t(avgz`6RqiiX<|hGG1pmop8k6Q+G_mv zJv|RfDheUp2L3=^C=4aCBMBn0aRCU(DQwX-W(RkRwmLeuJYF<0urcaf(=7)JPg<3P zQs!~G)9CT18o!J4{zX{_e}4eS)U-E)0FAt}wEI(c0%HkxgggW;(1E=>J17_hsH^sP z%lT0LGgbUXHx-K*CI-MCrP66UP0PvGqM$MkeLyqHdbgP|_Cm!7te~b8p+e6sQ_3k| zVcwTh6d83ltdnR>D^)BYQpDKlLk3g0Hdcgz2}%qUs9~~Rie)A-BV1mS&naYai#xcZ z(d{8=-LVpTp}2*y)|gR~;qc7fp26}lPcLZ#=JpYcn3AT9(UIdOyg+d(P5T7D&*P}# zQCYplZO5|7+r19%9e`v^vfSS1sbX1c%=w1;oyruXB%Kl$ACgKQ6=qNWLsc=28xJjg zwvsI5-%SGU|3p>&zXVl^vVtQT3o-#$UT9LI@Npz~6=4!>mc431VRNN8od&Ul^+G_kHC`G=6WVWM z%9eWNyy(FTO|A+@x}Ou3CH)oi;t#7rAxdIXfNFwOj_@Y&TGz6P_sqiB`Q6Lxy|Q{`|fgmRG(k+!#b*M+Z9zFce)f-7;?Km5O=LHV9f9_87; zF7%R2B+$?@sH&&-$@tzaPYkw0;=i|;vWdI|Wl3q_Zu>l;XdIw2FjV=;Mq5t1Q0|f< zs08j54Bp`3RzqE=2enlkZxmX6OF+@|2<)A^RNQpBd6o@OXl+i)zO%D4iGiQNuXd+zIR{_lb96{lc~bxsBveIw6umhShTX+3@ZJ=YHh@ zWY3(d0azg;7oHn>H<>?4@*RQbi>SmM=JrHvIG(~BrvI)#W(EAeO6fS+}mxxcc+X~W6&YVl86W9WFSS}Vz-f9vS?XUDBk)3TcF z8V?$4Q)`uKFq>xT=)Y9mMFVTUk*NIA!0$?RP6Ig0TBmUFrq*Q-Agq~DzxjStQyJ({ zBeZ;o5qUUKg=4Hypm|}>>L=XKsZ!F$yNTDO)jt4H0gdQ5$f|d&bnVCMMXhNh)~mN z@_UV6D7MVlsWz+zM+inZZp&P4fj=tm6fX)SG5H>OsQf_I8c~uGCig$GzuwViK54bcgL;VN|FnyQl>Ed7(@>=8$a_UKIz|V6CeVSd2(P z0Uu>A8A+muM%HLFJQ9UZ5c)BSAv_zH#1f02x?h9C}@pN@6{>UiAp>({Fn(T9Q8B z^`zB;kJ5b`>%dLm+Ol}ty!3;8f1XDSVX0AUe5P#@I+FQ-`$(a;zNgz)4x5hz$Hfbg z!Q(z26wHLXko(1`;(BAOg_wShpX0ixfWq3ponndY+u%1gyX)_h=v1zR#V}#q{au6; z!3K=7fQwnRfg6FXtNQmP>`<;!N137paFS%y?;lb1@BEdbvQHYC{976l`cLqn;b8lp zIDY>~m{gDj(wfnK!lpW6pli)HyLEiUrNc%eXTil|F2s(AY+LW5hkKb>TQ3|Q4S9rr zpDs4uK_co6XPsn_z$LeS{K4jFF`2>U`tbgKdyDne`xmR<@6AA+_hPNKCOR-Zqv;xk zu5!HsBUb^!4uJ7v0RuH-7?l?}b=w5lzzXJ~gZcxRKOovSk@|#V+MuX%Y+=;14i*%{)_gSW9(#4%)AV#3__kac1|qUy!uyP{>?U#5wYNq}y$S9pCc zFc~4mgSC*G~j0u#qqp9 z${>3HV~@->GqEhr_Xwoxq?Hjn#=s2;i~g^&Hn|aDKpA>Oc%HlW(KA1?BXqpxB;Ydx)w;2z^MpjJ(Qi(X!$5RC z*P{~%JGDQqojV>2JbEeCE*OEu!$XJ>bWA9Oa_Hd;y)F%MhBRi*LPcdqR8X`NQ&1L# z5#9L*@qxrx8n}LfeB^J{%-?SU{FCwiWyHp682F+|pa+CQa3ZLzBqN1{)h4d6+vBbV zC#NEbQLC;}me3eeYnOG*nXOJZEU$xLZ1<1Y=7r0(-U0P6-AqwMAM`a(Ed#7vJkn6plb4eI4?2y3yOTGmmDQ!z9`wzbf z_OY#0@5=bnep;MV0X_;;SJJWEf^E6Bd^tVJ9znWx&Ks8t*B>AM@?;D4oWUGc z!H*`6d7Cxo6VuyS4Eye&L1ZRhrRmN6Lr`{NL(wDbif|y&z)JN>Fl5#Wi&mMIr5i;x zBx}3YfF>>8EC(fYnmpu~)CYHuHCyr5*`ECap%t@y=jD>!_%3iiE|LN$mK9>- zHdtpy8fGZtkZF?%TW~29JIAfi2jZT8>OA7=h;8T{{k?c2`nCEx9$r zS+*&vt~2o^^J+}RDG@+9&M^K*z4p{5#IEVbz`1%`m5c2};aGt=V?~vIM}ZdPECDI)47|CWBCfDWUbxBCnmYivQ*0Nu_xb*C>~C9(VjHM zxe<*D<#dQ8TlpMX2c@M<9$w!RP$hpG4cs%AI){jp*Sj|*`m)5(Bw*A0$*i-(CA5#%>a)$+jI2C9r6|(>J8InryENI z$NohnxDUB;wAYDwrb*!N3noBTKPpPN}~09SEL18tkG zxgz(RYU_;DPT{l?Q$+eaZaxnsWCA^ds^0PVRkIM%bOd|G2IEBBiz{&^JtNsODs;5z zICt_Zj8wo^KT$7Bg4H+y!Df#3mbl%%?|EXe!&(Vmac1DJ*y~3+kRKAD=Ovde4^^%~ zw<9av18HLyrf*_>Slp;^i`Uy~`mvBjZ|?Ad63yQa#YK`4+c6;pW4?XIY9G1(Xh9WO8{F-Aju+nS9Vmv=$Ac0ienZ+p9*O%NG zMZKy5?%Z6TAJTE?o5vEr0r>f>hb#2w2U3DL64*au_@P!J!TL`oH2r*{>ffu6|A7tv zL4juf$DZ1MW5ZPsG!5)`k8d8c$J$o;%EIL0va9&GzWvkS%ZsGb#S(?{!UFOZ9<$a| zY|a+5kmD5N&{vRqkgY>aHsBT&`rg|&kezoD)gP0fsNYHsO#TRc_$n6Lf1Z{?+DLziXlHrq4sf(!>O{?Tj;Eh@%)+nRE_2VxbN&&%%caU#JDU%vL3}Cb zsb4AazPI{>8H&d=jUaZDS$-0^AxE@utGs;-Ez_F(qC9T=UZX=>ok2k2 ziTn{K?y~a5reD2A)P${NoI^>JXn>`IeArow(41c-Wm~)wiryEP(OS{YXWi7;%dG9v zI?mwu1MxD{yp_rrk!j^cKM)dc4@p4Ezyo%lRN|XyD}}>v=Xoib0gOcdXrQ^*61HNj z=NP|pd>@yfvr-=m{8$3A8TQGMTE7g=z!%yt`8`Bk-0MMwW~h^++;qyUP!J~ykh1GO z(FZ59xuFR$(WE;F@UUyE@Sp>`aVNjyj=Ty>_Vo}xf`e7`F;j-IgL5`1~-#70$9_=uBMq!2&1l zomRgpD58@)YYfvLtPW}{C5B35R;ZVvB<<#)x%srmc_S=A7F@DW8>QOEGwD6suhwCg z>Pa+YyULhmw%BA*4yjDp|2{!T98~<6Yfd(wo1mQ!KWwq0eg+6)o1>W~f~kL<-S+P@$wx*zeI|1t7z#Sxr5 zt6w+;YblPQNplq4Z#T$GLX#j6yldXAqj>4gAnnWtBICUnA&-dtnlh=t0Ho_vEKwV` z)DlJi#!@nkYV#$!)@>udAU*hF?V`2$Hf=V&6PP_|r#Iv*J$9)pF@X3`k;5})9^o4y z&)~?EjX5yX12O(BsFy-l6}nYeuKkiq`u9145&3Ssg^y{5G3Pse z9w(YVa0)N-fLaBq1`P!_#>SS(8fh_5!f{UrgZ~uEdeMJIz7DzI5!NHHqQtm~#CPij z?=N|J>nPR6_sL7!f4hD_|KH`vf8(Wpnj-(gPWH+ZvID}%?~68SwhPTC3u1_cB`otq z)U?6qo!ZLi5b>*KnYHWW=3F!p%h1;h{L&(Q&{qY6)_qxNfbP6E3yYpW!EO+IW3?@J z);4>g4gnl^8klu7uA>eGF6rIGSynacogr)KUwE_R4E5Xzi*Qir@b-jy55-JPC8c~( zo!W8y9OGZ&`xmc8;=4-U9=h{vCqfCNzYirONmGbRQlR`WWlgnY+1wCXbMz&NT~9*| z6@FrzP!LX&{no2!Ln_3|I==_4`@}V?4a;YZKTdw;vT<+K+z=uWbW(&bXEaWJ^W8Td z-3&1bY^Z*oM<=M}LVt>_j+p=2Iu7pZmbXrhQ_k)ysE9yXKygFNw$5hwDn(M>H+e1&9BM5!|81vd%r%vEm zqxY3?F@fb6O#5UunwgAHR9jp_W2zZ}NGp2%mTW@(hz7$^+a`A?mb8|_G*GNMJ) zjqegXQio=i@AINre&%ofexAr95aop5C+0MZ0m-l=MeO8m3epm7U%vZB8+I+C*iNFM z#T3l`gknX;D$-`2XT^Cg*vrv=RH+P;_dfF++cP?B_msQI4j+lt&rX2)3GaJx%W*Nn zkML%D{z5tpHH=dksQ*gzc|}gzW;lwAbxoR07VNgS*-c3d&8J|;@3t^ zVUz*J*&r7DFRuFVDCJDK8V9NN5hvpgGjwx+5n)qa;YCKe8TKtdnh{I7NU9BCN!0dq zczrBk8pE{{@vJa9ywR@mq*J=v+PG;?fwqlJVhijG!3VmIKs>9T6r7MJpC)m!Tc#>g zMtVsU>wbwFJEfwZ{vB|ZlttNe83)$iz`~#8UJ^r)lJ@HA&G#}W&ZH*;k{=TavpjWE z7hdyLZPf*X%Gm}i`Y{OGeeu^~nB8=`{r#TUrM-`;1cBvEd#d!kPqIgYySYhN-*1;L z^byj%Yi}Gx)Wnkosi337BKs}+5H5dth1JA{Ir-JKN$7zC)*}hqeoD(WfaUDPT>0`- z(6sa0AoIqASwF`>hP}^|)a_j2s^PQn*qVC{Q}htR z5-)duBFXT_V56-+UohKXlq~^6uf!6sA#ttk1o~*QEy_Y-S$gAvq47J9Vtk$5oA$Ct zYhYJ@8{hsC^98${!#Ho?4y5MCa7iGnfz}b9jE~h%EAAv~Qxu)_rAV;^cygV~5r_~?l=B`zObj7S=H=~$W zPtI_m%g$`kL_fVUk9J@>EiBH zOO&jtn~&`hIFMS5S`g8w94R4H40mdNUH4W@@XQk1sr17b{@y|JB*G9z1|CrQjd+GX z6+KyURG3;!*BQrentw{B2R&@2&`2}n(z-2&X7#r!{yg@Soy}cRD~j zj9@UBW+N|4HW4AWapy4wfUI- zZ`gSL6DUlgj*f1hSOGXG0IVH8HxK?o2|3HZ;KW{K+yPAlxtb)NV_2AwJm|E)FRs&& z=c^e7bvUsztY|+f^k7NXs$o1EUq>cR7C0$UKi6IooHWlK_#?IWDkvywnzg&ThWo^? z2O_N{5X39#?eV9l)xI(>@!vSB{DLt*oY!K1R8}_?%+0^C{d9a%N4 zoxHVT1&Lm|uDX%$QrBun5e-F`HJ^T$ zmzv)p@4ZHd_w9!%Hf9UYNvGCw2TTTbrj9pl+T9%-_-}L(tES>Or-}Z4F*{##n3~L~TuxjirGuIY#H7{%$E${?p{Q01 zi6T`n;rbK1yIB9jmQNycD~yZq&mbIsFWHo|ZAChSFPQa<(%d8mGw*V3fh|yFoxOOiWJd(qvVb!Z$b88cg->N=qO*4k~6;R==|9ihg&riu#P~s4Oap9O7f%crSr^rljeIfXDEg>wi)&v*a%7zpz<9w z*r!3q9J|390x`Zk;g$&OeN&ctp)VKRpDSV@kU2Q>jtok($Y-*x8_$2piTxun81@vt z!Vj?COa0fg2RPXMSIo26T=~0d`{oGP*eV+$!0I<(4azk&Vj3SiG=Q!6mX0p$z7I}; z9BJUFgT-K9MQQ-0@Z=^7R<{bn2Fm48endsSs`V7_@%8?Bxkqv>BDoVcj?K#dV#uUP zL1ND~?D-|VGKe3Rw_7-Idpht>H6XRLh*U7epS6byiGvJpr%d}XwfusjH9g;Z98H`x zyde%%5mhGOiL4wljCaWCk-&uE4_OOccb9c!ZaWt4B(wYl!?vyzl%7n~QepN&eFUrw zFIOl9c({``6~QD+43*_tzP{f2x41h(?b43^y6=iwyB)2os5hBE!@YUS5?N_tXd=h( z)WE286Fbd>R4M^P{!G)f;h<3Q>Fipuy+d2q-)!RyTgt;wr$(?9ox3;q+{E*ZQHhOn;lM`cjnu9 zXa48ks-v(~b*;MAI<>YZH(^NV8vjb34beE<_cwKlJoR;k6lJNSP6v}uiyRD?|0w+X@o1ONrH8a$fCxXpf? z?$DL0)7|X}Oc%h^zrMKWc-NS9I0Utu@>*j}b@tJ=ixQSJ={4@854wzW@E>VSL+Y{i z#0b=WpbCZS>kUCO_iQz)LoE>P5LIG-hv9E+oG}DtlIDF>$tJ1aw9^LuhLEHt?BCj& z(O4I8v1s#HUi5A>nIS-JK{v!7dJx)^Yg%XjNmlkWAq2*cv#tHgz`Y(bETc6CuO1VkN^L-L3j_x<4NqYb5rzrLC-7uOv z!5e`GZt%B782C5-fGnn*GhDF$%(qP<74Z}3xx+{$4cYKy2ikxI7B2N+2r07DN;|-T->nU&!=Cm#rZt%O_5c&1Z%nlWq3TKAW0w zQqemZw_ue--2uKQsx+niCUou?HjD`xhEjjQd3%rrBi82crq*~#uA4+>vR<_S{~5ce z-2EIl?~s z1=GVL{NxP1N3%=AOaC}j_Fv=ur&THz zyO!d9kHq|c73kpq`$+t+8Bw7MgeR5~`d7ChYyGCBWSteTB>8WAU(NPYt2Dk`@#+}= zI4SvLlyk#pBgVigEe`?NG*vl7V6m+<}%FwPV=~PvvA)=#ths==DRTDEYh4V5}Cf$z@#;< zyWfLY_5sP$gc3LLl2x+Ii)#b2nhNXJ{R~vk`s5U7Nyu^3yFg&D%Txwj6QezMX`V(x z=C`{76*mNb!qHHs)#GgGZ_7|vkt9izl_&PBrsu@}L`X{95-2jf99K)0=*N)VxBX2q z((vkpP2RneSIiIUEnGb?VqbMb=Zia+rF~+iqslydE34cSLJ&BJW^3knX@M;t*b=EA zNvGzv41Ld_T+WT#XjDB840vovUU^FtN_)G}7v)1lPetgpEK9YS^OWFkPoE{ovj^=@ zO9N$S=G$1ecndT_=5ehth2Lmd1II-PuT~C9`XVePw$y8J#dpZ?Tss<6wtVglm(Ok7 z3?^oi@pPio6l&!z8JY(pJvG=*pI?GIOu}e^EB6QYk$#FJQ%^AIK$I4epJ+9t?KjqA+bkj&PQ*|vLttme+`9G=L% ziadyMw_7-M)hS(3E$QGNCu|o23|%O+VN7;Qggp?PB3K-iSeBa2b}V4_wY`G1Jsfz4 z9|SdB^;|I8E8gWqHKx!vj_@SMY^hLEIbSMCuE?WKq=c2mJK z8LoG-pnY!uhqFv&L?yEuxo{dpMTsmCn)95xanqBrNPTgXP((H$9N${Ow~Is-FBg%h z53;|Y5$MUN)9W2HBe2TD`ct^LHI<(xWrw}$qSoei?}s)&w$;&!14w6B6>Yr6Y8b)S z0r71`WmAvJJ`1h&poLftLUS6Ir zC$bG9!Im_4Zjse)#K=oJM9mHW1{%l8sz$1o?ltdKlLTxWWPB>Vk22czVt|1%^wnN@*!l)}?EgtvhC>vlHm^t+ogpgHI1_$1ox9e;>0!+b(tBrmXRB`PY1vp-R**8N7 zGP|QqI$m(Rdu#=(?!(N}G9QhQ%o!aXE=aN{&wtGP8|_qh+7a_j_sU5|J^)vxq;# zjvzLn%_QPHZZIWu1&mRAj;Sa_97p_lLq_{~j!M9N^1yp3U_SxRqK&JnR%6VI#^E12 z>CdOVI^_9aPK2eZ4h&^{pQs}xsijXgFYRIxJ~N7&BB9jUR1fm!(xl)mvy|3e6-B3j zJn#ajL;bFTYJ2+Q)tDjx=3IklO@Q+FFM}6UJr6km7hj7th9n_&JR7fnqC!hTZoM~T zBeaVFp%)0cbPhejX<8pf5HyRUj2>aXnXBqDJe73~J%P(2C?-RT{c3NjE`)om! zl$uewSgWkE66$Kb34+QZZvRn`fob~Cl9=cRk@Es}KQm=?E~CE%spXaMO6YmrMl%9Q zlA3Q$3|L1QJ4?->UjT&CBd!~ru{Ih^in&JXO=|<6J!&qp zRe*OZ*cj5bHYlz!!~iEKcuE|;U4vN1rk$xq6>bUWD*u(V@8sG^7>kVuo(QL@Ki;yL zWC!FT(q{E8#on>%1iAS0HMZDJg{Z{^!De(vSIq&;1$+b)oRMwA3nc3mdTSG#3uYO_ z>+x;7p4I;uHz?ZB>dA-BKl+t-3IB!jBRgdvAbW!aJ(Q{aT>+iz?91`C-xbe)IBoND z9_Xth{6?(y3rddwY$GD65IT#f3<(0o#`di{sh2gm{dw*#-Vnc3r=4==&PU^hCv$qd zjw;>i&?L*Wq#TxG$mFIUf>eK+170KG;~+o&1;Tom9}}mKo23KwdEM6UonXgc z!6N(@k8q@HPw{O8O!lAyi{rZv|DpgfU{py+j(X_cwpKqcalcqKIr0kM^%Br3SdeD> zHSKV94Yxw;pjzDHo!Q?8^0bb%L|wC;4U^9I#pd5O&eexX+Im{ z?jKnCcsE|H?{uGMqVie_C~w7GX)kYGWAg%-?8|N_1#W-|4F)3YTDC+QSq1s!DnOML3@d`mG%o2YbYd#jww|jD$gotpa)kntakp#K;+yo-_ZF9qrNZw<%#C zuPE@#3RocLgPyiBZ+R_-FJ_$xP!RzWm|aN)S+{$LY9vvN+IW~Kf3TsEIvP+B9Mtm! zpfNNxObWQpLoaO&cJh5>%slZnHl_Q~(-Tfh!DMz(dTWld@LG1VRF`9`DYKhyNv z2pU|UZ$#_yUx_B_|MxUq^glT}O5Xt(Vm4Mr02><%C)@v;vPb@pT$*yzJ4aPc_FZ3z z3}PLoMBIM>q_9U2rl^sGhk1VUJ89=*?7|v`{!Z{6bqFMq(mYiA?%KbsI~JwuqVA9$H5vDE+VocjX+G^%bieqx->s;XWlKcuv(s%y%D5Xbc9+ zc(_2nYS1&^yL*ey664&4`IoOeDIig}y-E~_GS?m;D!xv5-xwz+G`5l6V+}CpeJDi^ z%4ed$qowm88=iYG+(`ld5Uh&>Dgs4uPHSJ^TngXP_V6fPyl~>2bhi20QB%lSd#yYn zO05?KT1z@?^-bqO8Cg`;ft>ilejsw@2%RR7;`$Vs;FmO(Yr3Fp`pHGr@P2hC%QcA|X&N2Dn zYf`MqXdHi%cGR@%y7Rg7?d3?an){s$zA{!H;Ie5exE#c~@NhQUFG8V=SQh%UxUeiV zd7#UcYqD=lk-}sEwlpu&H^T_V0{#G?lZMxL7ih_&{(g)MWBnCZxtXg znr#}>U^6!jA%e}@Gj49LWG@*&t0V>Cxc3?oO7LSG%~)Y5}f7vqUUnQ;STjdDU}P9IF9d9<$;=QaXc zL1^X7>fa^jHBu_}9}J~#-oz3Oq^JmGR#?GO7b9a(=R@fw@}Q{{@`Wy1vIQ#Bw?>@X z-_RGG@wt|%u`XUc%W{J z>iSeiz8C3H7@St3mOr_mU+&bL#Uif;+Xw-aZdNYUpdf>Rvu0i0t6k*}vwU`XNO2he z%miH|1tQ8~ZK!zmL&wa3E;l?!!XzgV#%PMVU!0xrDsNNZUWKlbiOjzH-1Uoxm8E#r`#2Sz;-o&qcqB zC-O_R{QGuynW14@)7&@yw1U}uP(1cov)twxeLus0s|7ayrtT8c#`&2~Fiu2=R;1_4bCaD=*E@cYI>7YSnt)nQc zohw5CsK%m?8Ack)qNx`W0_v$5S}nO|(V|RZKBD+btO?JXe|~^Qqur%@eO~<8-L^9d z=GA3-V14ng9L29~XJ>a5k~xT2152zLhM*@zlp2P5Eu}bywkcqR;ISbas&#T#;HZSf z2m69qTV(V@EkY(1Dk3`}j)JMo%ZVJ*5eB zYOjIisi+igK0#yW*gBGj?@I{~mUOvRFQR^pJbEbzFxTubnrw(Muk%}jI+vXmJ;{Q6 zrSobKD>T%}jV4Ub?L1+MGOD~0Ir%-`iTnWZN^~YPrcP5y3VMAzQ+&en^VzKEb$K!Q z<7Dbg&DNXuow*eD5yMr+#08nF!;%4vGrJI++5HdCFcGLfMW!KS*Oi@=7hFwDG!h2< zPunUEAF+HncQkbfFj&pbzp|MU*~60Z(|Ik%Tn{BXMN!hZOosNIseT?R;A`W?=d?5X zK(FB=9mZusYahp|K-wyb={rOpdn=@;4YI2W0EcbMKyo~-#^?h`BA9~o285%oY zfifCh5Lk$SY@|2A@a!T2V+{^!psQkx4?x0HSV`(w9{l75QxMk!)U52Lbhn{8ol?S) zCKo*7R(z!uk<6*qO=wh!Pul{(qq6g6xW;X68GI_CXp`XwO zxuSgPRAtM8K7}5E#-GM!*ydOOG_{A{)hkCII<|2=ma*71ci_-}VPARm3crFQjLYV! z9zbz82$|l01mv`$WahE2$=fAGWkd^X2kY(J7iz}WGS z@%MyBEO=A?HB9=^?nX`@nh;7;laAjs+fbo!|K^mE!tOB>$2a_O0y-*uaIn8k^6Y zSbuv;5~##*4Y~+y7Z5O*3w4qgI5V^17u*ZeupVGH^nM&$qmAk|anf*>r zWc5CV;-JY-Z@Uq1Irpb^O`L_7AGiqd*YpGUShb==os$uN3yYvb`wm6d=?T*it&pDk zo`vhw)RZX|91^^Wa_ti2zBFyWy4cJu#g)_S6~jT}CC{DJ_kKpT`$oAL%b^!2M;JgT zM3ZNbUB?}kP(*YYvXDIH8^7LUxz5oE%kMhF!rnPqv!GiY0o}NR$OD=ITDo9r%4E>E0Y^R(rS^~XjWyVI6 zMOR5rPXhTp*G*M&X#NTL`Hu*R+u*QNoiOKg4CtNPrjgH>c?Hi4MUG#I917fx**+pJfOo!zFM&*da&G_x)L(`k&TPI*t3e^{crd zX<4I$5nBQ8Ax_lmNRa~E*zS-R0sxkz`|>7q_?*e%7bxqNm3_eRG#1ae3gtV9!fQpY z+!^a38o4ZGy9!J5sylDxZTx$JmG!wg7;>&5H1)>f4dXj;B+@6tMlL=)cLl={jLMxY zbbf1ax3S4>bwB9-$;SN2?+GULu;UA-35;VY*^9Blx)Jwyb$=U!D>HhB&=jSsd^6yw zL)?a|>GxU!W}ocTC(?-%z3!IUhw^uzc`Vz_g>-tv)(XA#JK^)ZnC|l1`@CdX1@|!| z_9gQ)7uOf?cR@KDp97*>6X|;t@Y`k_N@)aH7gY27)COv^P3ya9I{4z~vUjLR9~z1Z z5=G{mVtKH*&$*t0@}-i_v|3B$AHHYale7>E+jP`ClqG%L{u;*ff_h@)al?RuL7tOO z->;I}>%WI{;vbLP3VIQ^iA$4wl6@0sDj|~112Y4OFjMs`13!$JGkp%b&E8QzJw_L5 zOnw9joc0^;O%OpF$Qp)W1HI!$4BaXX84`%@#^dk^hFp^pQ@rx4g(8Xjy#!X%+X5Jd@fs3amGT`}mhq#L97R>OwT5-m|h#yT_-v@(k$q7P*9X~T*3)LTdzP!*B} z+SldbVWrrwQo9wX*%FyK+sRXTa@O?WM^FGWOE?S`R(0P{<6p#f?0NJvnBia?k^fX2 zNQs7K-?EijgHJY}&zsr;qJ<*PCZUd*x|dD=IQPUK_nn)@X4KWtqoJNHkT?ZWL_hF? zS8lp2(q>;RXR|F;1O}EE#}gCrY~#n^O`_I&?&z5~7N;zL0)3Tup`%)oHMK-^r$NT% zbFg|o?b9w(q@)6w5V%si<$!U<#}s#x@0aX-hP>zwS#9*75VXA4K*%gUc>+yzupTDBOKH8WR4V0pM(HrfbQ&eJ79>HdCvE=F z|J>s;;iDLB^3(9}?biKbxf1$lI!*Z%*0&8UUq}wMyPs_hclyQQi4;NUY+x2qy|0J; zhn8;5)4ED1oHwg+VZF|80<4MrL97tGGXc5Sw$wAI#|2*cvQ=jB5+{AjMiDHmhUC*a zlmiZ`LAuAn_}hftXh;`Kq0zblDk8?O-`tnilIh|;3lZp@F_osJUV9`*R29M?7H{Fy z`nfVEIDIWXmU&YW;NjU8)EJpXhxe5t+scf|VXM!^bBlwNh)~7|3?fWwo_~ZFk(22% zTMesYw+LNx3J-_|DM~`v93yXe=jPD{q;li;5PD?Dyk+b? zo21|XpT@)$BM$%F=P9J19Vi&1#{jM3!^Y&fr&_`toi`XB1!n>sbL%U9I5<7!@?t)~ z;&H%z>bAaQ4f$wIzkjH70;<8tpUoxzKrPhn#IQfS%9l5=Iu))^XC<58D!-O z{B+o5R^Z21H0T9JQ5gNJnqh#qH^na|z92=hONIM~@_iuOi|F>jBh-?aA20}Qx~EpDGElELNn~|7WRXRFnw+Wdo`|# zBpU=Cz3z%cUJ0mx_1($X<40XEIYz(`noWeO+x#yb_pwj6)R(__%@_Cf>txOQ74wSJ z0#F3(zWWaR-jMEY$7C*3HJrohc79>MCUu26mfYN)f4M~4gD`}EX4e}A!U}QV8!S47 z6y-U-%+h`1n`*pQuKE%Av0@)+wBZr9mH}@vH@i{v(m-6QK7Ncf17x_D=)32`FOjjo zg|^VPf5c6-!FxN{25dvVh#fog=NNpXz zfB$o+0jbRkHH{!TKhE709f+jI^$3#v1Nmf80w`@7-5$1Iv_`)W^px8P-({xwb;D0y z7LKDAHgX<84?l!I*Dvi2#D@oAE^J|g$3!)x1Ua;_;<@#l1fD}lqU2_tS^6Ht$1Wl} zBESo7o^)9-Tjuz$8YQSGhfs{BQV6zW7dA?0b(Dbt=UnQs&4zHfe_sj{RJ4uS-vQpC zX;Bbsuju4%!o8?&m4UZU@~ZZjeFF6ex2ss5_60_JS_|iNc+R0GIjH1@Z z=rLT9%B|WWgOrR7IiIwr2=T;Ne?30M!@{%Qf8o`!>=s<2CBpCK_TWc(DX51>e^xh8 z&@$^b6CgOd7KXQV&Y4%}_#uN*mbanXq(2=Nj`L7H7*k(6F8s6{FOw@(DzU`4-*77{ zF+dxpv}%mFpYK?>N_2*#Y?oB*qEKB}VoQ@bzm>ptmVS_EC(#}Lxxx730trt0G)#$b zE=wVvtqOct1%*9}U{q<)2?{+0TzZzP0jgf9*)arV)*e!f`|jgT{7_9iS@e)recI#z zbzolURQ+TOzE!ymqvBY7+5NnAbWxvMLsLTwEbFqW=CPyCsmJ}P1^V30|D5E|p3BC5 z)3|qgw@ra7aXb-wsa|l^in~1_fm{7bS9jhVRkYVO#U{qMp z)Wce+|DJ}4<2gp8r0_xfZpMo#{Hl2MfjLcZdRB9(B(A(f;+4s*FxV{1F|4d`*sRNd zp4#@sEY|?^FIJ;tmH{@keZ$P(sLh5IdOk@k^0uB^BWr@pk6mHy$qf&~rI>P*a;h0C{%oA*i!VjWn&D~O#MxN&f@1Po# zKN+ zrGrkSjcr?^R#nGl<#Q722^wbYcgW@{+6CBS<1@%dPA8HC!~a`jTz<`g_l5N1M@9wn9GOAZ>nqNgq!yOCbZ@1z`U_N`Z>}+1HIZxk*5RDc&rd5{3qjRh8QmT$VyS;jK z;AF+r6XnnCp=wQYoG|rT2@8&IvKq*IB_WvS%nt%e{MCFm`&W*#LXc|HrD?nVBo=(8*=Aq?u$sDA_sC_RPDUiQ+wnIJET8vx$&fxkW~kP9qXKt zozR)@xGC!P)CTkjeWvXW5&@2?)qt)jiYWWBU?AUtzAN}{JE1I)dfz~7$;}~BmQF`k zpn11qmObXwRB8&rnEG*#4Xax3XBkKlw(;tb?Np^i+H8m(Wyz9k{~ogba@laiEk;2! zV*QV^6g6(QG%vX5Um#^sT&_e`B1pBW5yVth~xUs#0}nv?~C#l?W+9Lsb_5)!71rirGvY zTIJ$OPOY516Y|_014sNv+Z8cc5t_V=i>lWV=vNu#!58y9Zl&GsMEW#pPYPYGHQ|;vFvd*9eM==$_=vc7xnyz0~ zY}r??$<`wAO?JQk@?RGvkWVJlq2dk9vB(yV^vm{=NVI8dhsX<)O(#nr9YD?I?(VmQ z^r7VfUBn<~p3()8yOBjm$#KWx!5hRW)5Jl7wY@ky9lNM^jaT##8QGVsYeaVywmpv>X|Xj7gWE1Ezai&wVLt3p)k4w~yrskT-!PR!kiyQlaxl(( zXhF%Q9x}1TMt3~u@|#wWm-Vq?ZerK={8@~&@9r5JW}r#45#rWii};t`{5#&3$W)|@ zbAf2yDNe0q}NEUvq_Quq3cTjcw z@H_;$hu&xllCI9CFDLuScEMg|x{S7GdV8<&Mq=ezDnRZAyX-8gv97YTm0bg=d)(>N z+B2FcqvI9>jGtnK%eO%y zoBPkJTk%y`8TLf4)IXPBn`U|9>O~WL2C~C$z~9|0m*YH<-vg2CD^SX#&)B4ngOSG$ zV^wmy_iQk>dfN@Pv(ckfy&#ak@MLC7&Q6Ro#!ezM*VEh`+b3Jt%m(^T&p&WJ2Oqvj zs-4nq0TW6cv~(YI$n0UkfwN}kg3_fp?(ijSV#tR9L0}l2qjc7W?i*q01=St0eZ=4h zyGQbEw`9OEH>NMuIe)hVwYHsGERWOD;JxEiO7cQv%pFCeR+IyhwQ|y@&^24k+|8fD zLiOWFNJ2&vu2&`Jv96_z-Cd5RLgmeY3*4rDOQo?Jm`;I_(+ejsPM03!ly!*Cu}Cco zrQSrEDHNyzT(D5s1rZq!8#?f6@v6dB7a-aWs(Qk>N?UGAo{gytlh$%_IhyL7h?DLXDGx zgxGEBQoCAWo-$LRvM=F5MTle`M})t3vVv;2j0HZY&G z22^iGhV@uaJh(XyyY%} zd4iH_UfdV#T=3n}(Lj^|n;O4|$;xhu*8T3hR1mc_A}fK}jfZ7LX~*n5+`8N2q#rI$ z@<_2VANlYF$vIH$ zl<)+*tIWW78IIINA7Rr7i{<;#^yzxoLNkXL)eSs=%|P>$YQIh+ea_3k z_s7r4%j7%&*NHSl?R4k%1>Z=M9o#zxY!n8sL5>BO-ZP;T3Gut>iLS@U%IBrX6BA3k z)&@q}V8a{X<5B}K5s(c(LQ=%v1ocr`t$EqqY0EqVjr65usa=0bkf|O#ky{j3)WBR(((L^wmyHRzoWuL2~WTC=`yZ zn%VX`L=|Ok0v7?s>IHg?yArBcync5rG#^+u)>a%qjES%dRZoIyA8gQ;StH z1Ao7{<&}6U=5}4v<)1T7t!J_CL%U}CKNs-0xWoTTeqj{5{?Be$L0_tk>M9o8 zo371}S#30rKZFM{`H_(L`EM9DGp+Mifk&IP|C2Zu_)Ghr4Qtpmkm1osCf@%Z$%t+7 zYH$Cr)Ro@3-QDeQJ8m+x6%;?YYT;k6Z0E-?kr>x33`H%*ueBD7Zx~3&HtWn0?2Wt} zTG}*|v?{$ajzt}xPzV%lL1t-URi8*Zn)YljXNGDb>;!905Td|mpa@mHjIH%VIiGx- zd@MqhpYFu4_?y5N4xiHn3vX&|e6r~Xt> zZG`aGq|yTNjv;9E+Txuoa@A(9V7g?1_T5FzRI;!=NP1Kqou1z5?%X~Wwb{trRfd>i z8&y^H)8YnKyA_Fyx>}RNmQIczT?w2J4SNvI{5J&}Wto|8FR(W;Qw#b1G<1%#tmYzQ zQ2mZA-PAdi%RQOhkHy9Ea#TPSw?WxwL@H@cbkZwIq0B!@ns}niALidmn&W?!Vd4Gj zO7FiuV4*6Mr^2xlFSvM;Cp_#r8UaqIzHJQg_z^rEJw&OMm_8NGAY2)rKvki|o1bH~ z$2IbfVeY2L(^*rMRU1lM5Y_sgrDS`Z??nR2lX;zyR=c%UyGb*%TC-Dil?SihkjrQy~TMv6;BMs7P8il`H7DmpVm@rJ;b)hW)BL)GjS154b*xq-NXq2cwE z^;VP7ua2pxvCmxrnqUYQMH%a%nHmwmI33nJM(>4LznvY*k&C0{8f*%?zggpDgkuz&JBx{9mfb@wegEl2v!=}Sq2Gaty0<)UrOT0{MZtZ~j5y&w zXlYa_jY)I_+VA-^#mEox#+G>UgvM!Ac8zI<%JRXM_73Q!#i3O|)lOP*qBeJG#BST0 zqohi)O!|$|2SeJQo(w6w7%*92S})XfnhrH_Z8qe!G5>CglP=nI7JAOW?(Z29;pXJ9 zR9`KzQ=WEhy*)WH>$;7Cdz|>*i>=##0bB)oU0OR>>N<21e4rMCHDemNi2LD>Nc$;& zQRFthpWniC1J6@Zh~iJCoLOxN`oCKD5Q4r%ynwgUKPlIEd#?QViIqovY|czyK8>6B zSP%{2-<;%;1`#0mG^B(8KbtXF;Nf>K#Di72UWE4gQ%(_26Koiad)q$xRL~?pN71ZZ zujaaCx~jXjygw;rI!WB=xrOJO6HJ!!w}7eiivtCg5K|F6$EXa)=xUC za^JXSX98W`7g-tm@uo|BKj39Dl;sg5ta;4qjo^pCh~{-HdLl6qI9Ix6f$+qiZ$}s= zNguKrU;u+T@ko(Vr1>)Q%h$?UKXCY>3se%&;h2osl2D zE4A9bd7_|^njDd)6cI*FupHpE3){4NQ*$k*cOWZ_?CZ>Z4_fl@n(mMnYK62Q1d@+I zr&O))G4hMihgBqRIAJkLdk(p(D~X{-oBUA+If@B}j& zsHbeJ3RzTq96lB7d($h$xTeZ^gP0c{t!Y0c)aQE;$FY2!mACg!GDEMKXFOPI^)nHZ z`aSPJpvV0|bbrzhWWkuPURlDeN%VT8tndV8?d)eN*i4I@u zVKl^6{?}A?P)Fsy?3oi#clf}L18t;TjNI2>eI&(ezDK7RyqFxcv%>?oxUlonv(px) z$vnPzRH`y5A(x!yOIfL0bmgeMQB$H5wenx~!ujQK*nUBW;@Em&6Xv2%s(~H5WcU2R z;%Nw<$tI)a`Ve!>x+qegJnQsN2N7HaKzrFqM>`6R*gvh%O*-%THt zrB$Nk;lE;z{s{r^PPm5qz(&lM{sO*g+W{sK+m3M_z=4=&CC>T`{X}1Vg2PEfSj2x_ zmT*(x;ov%3F?qoEeeM>dUn$a*?SIGyO8m806J1W1o+4HRhc2`9$s6hM#qAm zChQ87b~GEw{ADfs+5}FJ8+|bIlIv(jT$Ap#hSHoXdd9#w<#cA<1Rkq^*EEkknUd4& zoIWIY)sAswy6fSERVm&!SO~#iN$OgOX*{9@_BWFyJTvC%S++ilSfCrO(?u=Dc?CXZ zzCG&0yVR{Z`|ZF0eEApWEo#s9osV>F{uK{QA@BES#&;#KsScf>y zvs?vIbI>VrT<*!;XmQS=bhq%46-aambZ(8KU-wOO2=en~D}MCToB_u;Yz{)1ySrPZ z@=$}EvjTdzTWU7c0ZI6L8=yP+YRD_eMMos}b5vY^S*~VZysrkq<`cK3>>v%uy7jgq z0ilW9KjVDHLv0b<1K_`1IkbTOINs0=m-22c%M~l=^S}%hbli-3?BnNq?b`hx^HX2J zIe6ECljRL0uBWb`%{EA=%!i^4sMcj+U_TaTZRb+~GOk z^ZW!nky0n*Wb*r+Q|9H@ml@Z5gU&W`(z4-j!OzC1wOke`TRAYGZVl$PmQ16{3196( zO*?`--I}Qf(2HIwb2&1FB^!faPA2=sLg(@6P4mN)>Dc3i(B0;@O-y2;lM4akD>@^v z=u>*|!s&9zem70g7zfw9FXl1bpJW(C#5w#uy5!V?Q(U35A~$dR%LDVnq@}kQm13{} zd53q3N(s$Eu{R}k2esbftfjfOITCL;jWa$}(mmm}d(&7JZ6d3%IABCapFFYjdEjdK z&4Edqf$G^MNAtL=uCDRs&Fu@FXRgX{*0<(@c3|PNHa>L%zvxWS={L8%qw`STm+=Rd zA}FLspESSIpE_^41~#5yI2bJ=9`oc;GIL!JuW&7YetZ?0H}$$%8rW@*J37L-~Rsx!)8($nI4 zZhcZ2^=Y+p4YPl%j!nFJA|*M^gc(0o$i3nlphe+~-_m}jVkRN{spFs(o0ajW@f3K{ zDV!#BwL322CET$}Y}^0ixYj2w>&Xh12|R8&yEw|wLDvF!lZ#dOTHM9pK6@Nm-@9Lnng4ZHBgBSrr7KI8YCC9DX5Kg|`HsiwJHg2(7#nS;A{b3tVO?Z% za{m5b3rFV6EpX;=;n#wltDv1LE*|g5pQ+OY&*6qCJZc5oDS6Z6JD#6F)bWxZSF@q% z+1WV;m!lRB!n^PC>RgQCI#D1br_o^#iPk>;K2hB~0^<~)?p}LG%kigm@moD#q3PE+ zA^Qca)(xnqw6x>XFhV6ku9r$E>bWNrVH9fum0?4s?Rn2LG{Vm_+QJHse6xa%nzQ?k zKug4PW~#Gtb;#5+9!QBgyB@q=sk9=$S{4T>wjFICStOM?__fr+Kei1 z3j~xPqW;W@YkiUM;HngG!;>@AITg}vAE`M2Pj9Irl4w1fo4w<|Bu!%rh%a(Ai^Zhi zs92>v5;@Y(Zi#RI*ua*h`d_7;byQSa*v9E{2x$<-_=5Z<7{%)}4XExANcz@rK69T0x3%H<@frW>RA8^swA+^a(FxK| zFl3LD*ImHN=XDUkrRhp6RY5$rQ{bRgSO*(vEHYV)3Mo6Jy3puiLmU&g82p{qr0F?ohmbz)f2r{X2|T2 z$4fdQ=>0BeKbiVM!e-lIIs8wVTuC_m7}y4A_%ikI;Wm5$9j(^Y z(cD%U%k)X>_>9~t8;pGzL6L-fmQO@K; zo&vQzMlgY95;1BSkngY)e{`n0!NfVgf}2mB3t}D9@*N;FQ{HZ3Pb%BK6;5#-O|WI( zb6h@qTLU~AbVW#_6?c!?Dj65Now7*pU{h!1+eCV^KCuPAGs28~3k@ueL5+u|Z-7}t z9|lskE`4B7W8wMs@xJa{#bsCGDFoRSNSnmNYB&U7 zVGKWe%+kFB6kb)e;TyHfqtU6~fRg)f|>=5(N36)0+C z`hv65J<$B}WUc!wFAb^QtY31yNleq4dzmG`1wHTj=c*=hay9iD071Hc?oYoUk|M*_ zU1GihAMBsM@5rUJ(qS?9ZYJ6@{bNqJ`2Mr+5#hKf?doa?F|+^IR!8lq9)wS3tF_9n zW_?hm)G(M+MYb?V9YoX^_mu5h-LP^TL^!Q9Z7|@sO(rg_4+@=PdI)WL(B7`!K^ND- z-uIuVDCVEdH_C@c71YGYT^_Scf_dhB8Z2Xy6vGtBSlYud9vggOqv^L~F{BraSE_t} zIkP+Hp2&nH^-MNEs}^`oMLy11`PQW$T|K(`Bu*(f@)mv1-qY(_YG&J2M2<7k;;RK~ zL{Fqj9yCz8(S{}@c)S!65aF<=&eLI{hAMErCx&>i7OeDN>okvegO87OaG{Jmi<|}D zaT@b|0X{d@OIJ7zvT>r+eTzgLq~|Dpu)Z&db-P4z*`M$UL51lf>FLlq6rfG)%doyp z)3kk_YIM!03eQ8Vu_2fg{+osaEJPtJ-s36R+5_AEG12`NG)IQ#TF9c@$99%0iye+ zUzZ57=m2)$D(5Nx!n)=5Au&O0BBgwxIBaeI(mro$#&UGCr<;C{UjJVAbVi%|+WP(a zL$U@TYCxJ=1{Z~}rnW;7UVb7+ZnzgmrogDxhjLGo>c~MiJAWs&&;AGg@%U?Y^0JhL ze(x6Z74JG6FlOFK(T}SXQfhr}RIFl@QXKnIcXYF)5|V~e-}suHILKT-k|<*~Ij|VF zC;t@=uj=hot~*!C68G8hTA%8SzOfETOXQ|3FSaIEjvBJp(A)7SWUi5!Eu#yWgY+;n zlm<$+UDou*V+246_o#V4kMdto8hF%%Lki#zPh}KYXmMf?hrN0;>Mv%`@{0Qn`Ujp) z=lZe+13>^Q!9zT);H<(#bIeRWz%#*}sgUX9P|9($kexOyKIOc`dLux}c$7It4u|Rl z6SSkY*V~g_B-hMPo_ak>>z@AVQ(_N)VY2kB3IZ0G(iDUYw+2d7W^~(Jq}KY=JnWS( z#rzEa&0uNhJ>QE8iiyz;n2H|SV#Og+wEZv=f2%1ELX!SX-(d3tEj$5$1}70Mp<&eI zCkfbByL7af=qQE@5vDVxx1}FSGt_a1DoE3SDI+G)mBAna)KBG4p8Epxl9QZ4BfdAN zFnF|Y(umr;gRgG6NLQ$?ZWgllEeeq~z^ZS7L?<(~O&$5|y)Al^iMKy}&W+eMm1W z7EMU)u^ke(A1#XCV>CZ71}P}0x)4wtHO8#JRG3MA-6g=`ZM!FcICCZ{IEw8Dm2&LQ z1|r)BUG^0GzI6f946RrBlfB1Vs)~8toZf~7)+G;pv&XiUO(%5bm)pl=p>nV^o*;&T z;}@oZSibzto$arQgfkp|z4Z($P>dTXE{4O=vY0!)kDO* zGF8a4wq#VaFpLfK!iELy@?-SeRrdz%F*}hjKcA*y@mj~VD3!it9lhRhX}5YOaR9$} z3mS%$2Be7{l(+MVx3 z(4?h;P!jnRmX9J9sYN#7i=iyj_5q7n#X(!cdqI2lnr8T$IfOW<_v`eB!d9xY1P=2q&WtOXY=D9QYteP)De?S4}FK6#6Ma z=E*V+#s8>L;8aVroK^6iKo=MH{4yEZ_>N-N z`(|;aOATba1^asjxlILk<4}f~`39dBFlxj>Dw(hMYKPO3EEt1@S`1lxFNM+J@uB7T zZ8WKjz7HF1-5&2=l=fqF-*@>n5J}jIxdDwpT?oKM3s8Nr`x8JnN-kCE?~aM1H!hAE z%%w(3kHfGwMnMmNj(SU(w42OrC-euI>Dsjk&jz3ts}WHqmMpzQ3vZrsXrZ|}+MHA7 z068obeXZTsO*6RS@o3x80E4ok``rV^Y3hr&C1;|ZZ0|*EKO`$lECUYG2gVFtUTw)R z4Um<0ZzlON`zTdvVdL#KFoMFQX*a5wM0Czp%wTtfK4Sjs)P**RW&?lP$(<}q%r68Z zS53Y!d@&~ne9O)A^tNrXHhXBkj~$8j%pT1%%mypa9AW5E&s9)rjF4@O3ytH{0z6riz|@< zB~UPh*wRFg2^7EbQrHf0y?E~dHlkOxof_a?M{LqQ^C!i2dawHTPYUE=X@2(3<=OOxs8qn_(y>pU>u^}3y&df{JarR0@VJn0f+U%UiF=$Wyq zQvnVHESil@d|8&R<%}uidGh7@u^(%?$#|&J$pvFC-n8&A>utA=n3#)yMkz+qnG3wd zP7xCnF|$9Dif@N~L)Vde3hW8W!UY0BgT2v(wzp;tlLmyk2%N|0jfG$%<;A&IVrOI< z!L)o>j>;dFaqA3pL}b-Je(bB@VJ4%!JeX@3x!i{yIeIso^=n?fDX`3bU=eG7sTc%g%ye8$v8P@yKE^XD=NYxTb zbf!Mk=h|otpqjFaA-vs5YOF-*GwWPc7VbaOW&stlANnCN8iftFMMrUdYNJ_Bnn5Vt zxfz@Ah|+4&P;reZxp;MmEI7C|FOv8NKUm8njF7Wb6Gi7DeODLl&G~}G4be&*Hi0Qw z5}77vL0P+7-B%UL@3n1&JPxW^d@vVwp?u#gVcJqY9#@-3X{ok#UfW3<1fb%FT`|)V~ggq z(3AUoUS-;7)^hCjdT0Kf{i}h)mBg4qhtHHBti=~h^n^OTH5U*XMgDLIR@sre`AaB$ zg)IGBET_4??m@cx&c~bA80O7B8CHR7(LX7%HThkeC*@vi{-pL%e)yXp!B2InafbDF zjPXf1mko3h59{lT6EEbxKO1Z5GF71)WwowO6kY|6tjSVSWdQ}NsK2x{>i|MKZK8%Q zfu&_0D;CO-Jg0#YmyfctyJ!mRJp)e#@O0mYdp|8x;G1%OZQ3Q847YWTyy|%^cpA;m zze0(5p{tMu^lDkpe?HynyO?a1$_LJl2L&mpeKu%8YvgRNr=%2z${%WThHG=vrWY@4 zsA`OP#O&)TetZ>s%h!=+CE15lOOls&nvC~$Qz0Ph7tHiP;O$i|eDwpT{cp>+)0-|; zY$|bB+Gbel>5aRN3>c0x)4U=|X+z+{ zn*_p*EQoquRL+=+p;=lm`d71&1NqBz&_ph)MXu(Nv6&XE7(RsS)^MGj5Q?Fwude-(sq zjJ>aOq!7!EN>@(fK7EE#;i_BGvli`5U;r!YA{JRodLBc6-`n8K+Fjgwb%sX;j=qHQ z7&Tr!)!{HXoO<2BQrV9Sw?JRaLXV8HrsNevvnf>Y-6|{T!pYLl7jp$-nEE z#X!4G4L#K0qG_4Z;Cj6=;b|Be$hi4JvMH!-voxqx^@8cXp`B??eFBz2lLD8RRaRGh zn7kUfy!YV~p(R|p7iC1Rdgt$_24i0cd-S8HpG|`@my70g^y`gu%#Tf_L21-k?sRRZHK&at(*ED0P8iw{7?R$9~OF$Ko;Iu5)ur5<->x!m93Eb zFYpIx60s=Wxxw=`$aS-O&dCO_9?b1yKiPCQmSQb>T)963`*U+Ydj5kI(B(B?HNP8r z*bfSBpSu)w(Z3j7HQoRjUG(+d=IaE~tv}y14zHHs|0UcN52fT8V_<@2ep_ee{QgZG zmgp8iv4V{k;~8@I%M3<#B;2R>Ef(Gg_cQM7%}0s*^)SK6!Ym+~P^58*wnwV1BW@eG z4sZLqsUvBbFsr#8u7S1r4teQ;t)Y@jnn_m5jS$CsW1um!p&PqAcc8!zyiXHVta9QC zY~wCwCF0U%xiQPD_INKtTb;A|Zf29(mu9NI;E zc-e>*1%(LSXB`g}kd`#}O;veb<(sk~RWL|f3ljxCnEZDdNSTDV6#Td({6l&y4IjKF z^}lIUq*ZUqgTPumD)RrCN{M^jhY>E~1pn|KOZ5((%F)G|*ZQ|r4zIbrEiV%42hJV8 z3xS)=!X1+=olbdGJ=yZil?oXLct8FM{(6ikLL3E%=q#O6(H$p~gQu6T8N!plf!96| z&Q3=`L~>U0zZh;z(pGR2^S^{#PrPxTRHD1RQOON&f)Siaf`GLj#UOk&(|@0?zm;Sx ztsGt8=29-MZs5CSf1l1jNFtNt5rFNZxJPvkNu~2}7*9468TWm>nN9TP&^!;J{-h)_ z7WsHH9|F%I`Pb!>KAS3jQWKfGivTVkMJLO-HUGM_a4UQ_%RgL6WZvrW+Z4ujZn;y@ zz9$=oO!7qVTaQAA^BhX&ZxS*|5dj803M=k&2%QrXda`-Q#IoZL6E(g+tN!6CA!CP* zCpWtCujIea)ENl0liwVfj)Nc<9mV%+e@=d`haoZ*`B7+PNjEbXBkv=B+Pi^~L#EO$D$ZqTiD8f<5$eyb54-(=3 zh)6i8i|jp(@OnRrY5B8t|LFXFQVQ895n*P16cEKTrT*~yLH6Z4e*bZ5otpRDri&+A zfNbK1D5@O=sm`fN=WzWyse!za5n%^+6dHPGX#8DyIK>?9qyX}2XvBWVqbP%%D)7$= z=#$WulZlZR<{m#gU7lwqK4WS1Ne$#_P{b17qe$~UOXCl>5b|6WVh;5vVnR<%d+Lnp z$uEmML38}U4vaW8>shm6CzB(Wei3s#NAWE3)a2)z@i{4jTn;;aQS)O@l{rUM`J@K& l00vQ5JBs~;vo!vr%%-k{2_Fq1Mn4QF81S)AQ99zk{{c4yR+0b! literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..1af9e0930 --- /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.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists From 217c52de48e8608b49046907f84071a2a07f14d3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 17 Jan 2024 23:25:02 +0900 Subject: [PATCH 004/158] =?UTF-8?q?feat:=20=EC=97=B0=EA=B2=B0=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=EC=9A=A9=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/index.html | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/main/resources/static/index.html diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 000000000..0c0efabe1 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1 @@ +

Solid Connection Backend Page

\ No newline at end of file From c55c39ce94a00b43bfa590eb8126062ce3058257 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 21 Jan 2024 18:27:19 +0900 Subject: [PATCH 005/158] =?UTF-8?q?feat:=20entity=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/entity/Application.java | 43 +++++++++ .../solidconnection/entity/Country.java | 28 ++++++ .../entity/GpaRequirement.java | 21 +++++ .../entity/InterestedCountry.java | 23 +++++ .../entity/InterestedRegion.java | 19 ++++ .../entity/LanguageRequirement.java | 23 +++++ .../solidconnection/entity/Region.java | 27 ++++++ .../solidconnection/entity/SiteUser.java | 49 +++++++++++ .../solidconnection/entity/University.java | 88 +++++++++++++++++++ .../entity/WishUniversity.java | 19 ++++ 10 files changed, 340 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/entity/Application.java create mode 100644 src/main/java/com/example/solidconnection/entity/Country.java create mode 100644 src/main/java/com/example/solidconnection/entity/GpaRequirement.java create mode 100644 src/main/java/com/example/solidconnection/entity/InterestedCountry.java create mode 100644 src/main/java/com/example/solidconnection/entity/InterestedRegion.java create mode 100644 src/main/java/com/example/solidconnection/entity/LanguageRequirement.java create mode 100644 src/main/java/com/example/solidconnection/entity/Region.java create mode 100644 src/main/java/com/example/solidconnection/entity/SiteUser.java create mode 100644 src/main/java/com/example/solidconnection/entity/University.java create mode 100644 src/main/java/com/example/solidconnection/entity/WishUniversity.java diff --git a/src/main/java/com/example/solidconnection/entity/Application.java b/src/main/java/com/example/solidconnection/entity/Application.java new file mode 100644 index 000000000..04bbdc9ce --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/Application.java @@ -0,0 +1,43 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.type.LanguageTestType; +import jakarta.persistence.*; + +@Entity +public class Application { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 10) + @Enumerated(EnumType.STRING) + private LanguageTestType languageTestType; + + @Column(nullable = false) + private String languageTestScore; + + @Column(nullable = false, length = 500) + private String languageTestReportUrl; + + @Column(nullable = false) + private Float gpa; + + @Column(nullable = false, length = 500) + private String gpaReportUrl; + + @Column(nullable = false, length = 50) + private String verifyStatus; + + // 연관 관계 + @ManyToOne + @JoinColumn(name = "first_choice_univ_id") + private University firstChoiceUniversity; + + @ManyToOne + @JoinColumn(name = "second_choice_univ_id") + private University secondChoiceUniversity; + + @ManyToOne + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; +} diff --git a/src/main/java/com/example/solidconnection/entity/Country.java b/src/main/java/com/example/solidconnection/entity/Country.java new file mode 100644 index 000000000..0a7d106c4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/Country.java @@ -0,0 +1,28 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.type.CountryCode; +import jakarta.persistence.*; + +import java.util.Set; + +@Entity +public class Country { + @Id + @Column(length = 2) + @Enumerated(EnumType.STRING) + private CountryCode countryCode; + + @Column(nullable = false, length = 100) + private String name; + + // 연관 관계 + @ManyToOne + @JoinColumn(name = "region_id") + private Region region; + + @OneToMany(mappedBy = "country") + private Set universities; + + @OneToMany(mappedBy = "country") + private Set interestedCountries; +} diff --git a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java new file mode 100644 index 000000000..911be4a55 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java @@ -0,0 +1,21 @@ +package com.example.solidconnection.entity; + +import jakarta.persistence.*; + +@Entity +public class GpaRequirement { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 5) + private String scale; + + @Column(nullable = false) + private Float minGpa; + + // 연관 관계 + @ManyToOne + @JoinColumn(name = "university_id") + private University university; +} diff --git a/src/main/java/com/example/solidconnection/entity/InterestedCountry.java b/src/main/java/com/example/solidconnection/entity/InterestedCountry.java new file mode 100644 index 000000000..3e2c16329 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/InterestedCountry.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.entity; + +import jakarta.persistence.*; + +@Entity +public class InterestedCountry { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 연관 관계 + @ManyToOne + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + @ManyToOne + @JoinColumn(name = "country_id") + private Country country; + + @ManyToOne + @JoinColumn(name = "region_id") + private Region region; +} diff --git a/src/main/java/com/example/solidconnection/entity/InterestedRegion.java b/src/main/java/com/example/solidconnection/entity/InterestedRegion.java new file mode 100644 index 000000000..cdf6758b9 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/InterestedRegion.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.entity; + +import jakarta.persistence.*; + +@Entity +public class InterestedRegion { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 연관 관계 + @ManyToOne + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + @ManyToOne + @JoinColumn(name = "region_id") + private Region region; +} diff --git a/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java b/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java new file mode 100644 index 000000000..b0010ac98 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.type.LanguageTestType; +import jakarta.persistence.*; + +@Entity +public class LanguageRequirement { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 10) + @Enumerated(EnumType.STRING) + private LanguageTestType languageTestType; + + @Column(nullable = false) + private String minScore; + + // 연관 관계 + @ManyToOne + @JoinColumn(name = "university_id") + private University university; +} diff --git a/src/main/java/com/example/solidconnection/entity/Region.java b/src/main/java/com/example/solidconnection/entity/Region.java new file mode 100644 index 000000000..7be6bd3c9 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/Region.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.type.RegionCode; +import jakarta.persistence.*; + +import java.util.Set; + +@Entity +public class Region { + @Id + @Column(length = 10) + @Enumerated(EnumType.STRING) + private RegionCode id; + + // 연관 관계 + @OneToMany(mappedBy = "region") + private Set countries; + + @OneToMany(mappedBy = "region") + private Set interestedRegions; + + @OneToMany(mappedBy = "region") + private Set interestedCountries; + + @OneToMany(mappedBy = "region") + private Set universities; +} diff --git a/src/main/java/com/example/solidconnection/entity/SiteUser.java b/src/main/java/com/example/solidconnection/entity/SiteUser.java new file mode 100644 index 000000000..eb7cdaa10 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/SiteUser.java @@ -0,0 +1,49 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.Set; + +@Entity +public class SiteUser { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String email; + + @Column(nullable = false, length = 100) + private String nickname; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private PreparationStatus preparationStage; + + @Column(length = 500) + private String profileImageUrl; + + private LocalDateTime nicknameModifiedAt; + + private LocalDateTime quitedAt; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private Role role; + + // 연관관계 + @OneToMany(mappedBy = "siteUser") + private Set interestedRegions; + + @OneToMany(mappedBy = "siteUser") + private Set interestedCountries; + + @OneToMany(mappedBy = "siteUser") + private Set applications; + + @OneToMany(mappedBy = "siteUser") + private Set wishUniversities; +} diff --git a/src/main/java/com/example/solidconnection/entity/University.java b/src/main/java/com/example/solidconnection/entity/University.java new file mode 100644 index 000000000..2998087b7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/University.java @@ -0,0 +1,88 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.type.ExchangeSemester; +import com.example.solidconnection.type.TuitionFeePaymentType; +import jakarta.persistence.*; + +import java.util.Set; + +@Entity +public class University { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String koreanName; + + @Column(nullable = false, length = 100) + private String englishName; + + @Column(nullable = false, length = 100) + private String internalName; + + @Column(nullable = false) + private Integer recruitNumber; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private TuitionFeePaymentType tuitionFeePaymentType; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private ExchangeSemester exchangeSemester; + + @Column(length = 1000) + private String detailsForLanguage; + + @Column(length = 1000) + private String detailsForApply; + + @Column(length = 1000) + private String detailsForMajor; + + @Column(length = 1000) + private String detailsForAccommodation; + + @Column(length = 500) + private String homepageUrl; + + @Column(length = 500) + private String englishCourseUrl; + + @Column(length = 500) + private String accommodationUrl; + + @Column(length = 500) + private String details; + + @Column(nullable = false, length = 500) + private String logoImageUrl; + + @Column(nullable = false, length = 500) + private String backgroundImageUrl; + + // 연관 관계 + @ManyToOne + @JoinColumn(name = "country_id") + private Country country; + + @ManyToOne + @JoinColumn(name = "region_id") + private Region region; + + @OneToMany(mappedBy = "university") + private Set languageRequirements; + + @OneToMany(mappedBy = "university") + private Set gpaRequirements; + + @OneToMany(mappedBy = "firstChoiceUniversity") + private Set firstChoiceApplications; + + @OneToMany(mappedBy = "secondChoiceUniversity") + private Set secondChoiceApplications; + + @OneToMany(mappedBy = "university") + private Set wishUniversities; +} diff --git a/src/main/java/com/example/solidconnection/entity/WishUniversity.java b/src/main/java/com/example/solidconnection/entity/WishUniversity.java new file mode 100644 index 000000000..46f2e1086 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/WishUniversity.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.entity; + +import jakarta.persistence.*; + +@Entity +public class WishUniversity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 연관 관계 + @ManyToOne + @JoinColumn(name = "university_id") + private University university; + + @ManyToOne + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; +} \ No newline at end of file From 975b084ebca9df2267325228acc36b0d510ace34 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 21 Jan 2024 18:27:36 +0900 Subject: [PATCH 006/158] =?UTF-8?q?feat:=20type=20enum=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/type/CountryCode.java | 50 +++++++++++++++++++ .../type/ExchangeSemester.java | 16 ++++++ .../type/LanguageTestType.java | 5 ++ .../type/PreparationStatus.java | 7 +++ .../solidconnection/type/RegionCode.java | 7 +++ .../example/solidconnection/type/Role.java | 6 +++ .../type/TuitionFeePaymentType.java | 17 +++++++ 7 files changed, 108 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/type/CountryCode.java create mode 100644 src/main/java/com/example/solidconnection/type/ExchangeSemester.java create mode 100644 src/main/java/com/example/solidconnection/type/LanguageTestType.java create mode 100644 src/main/java/com/example/solidconnection/type/PreparationStatus.java create mode 100644 src/main/java/com/example/solidconnection/type/RegionCode.java create mode 100644 src/main/java/com/example/solidconnection/type/Role.java create mode 100644 src/main/java/com/example/solidconnection/type/TuitionFeePaymentType.java diff --git a/src/main/java/com/example/solidconnection/type/CountryCode.java b/src/main/java/com/example/solidconnection/type/CountryCode.java new file mode 100644 index 000000000..d0988bf18 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/CountryCode.java @@ -0,0 +1,50 @@ +package com.example.solidconnection.type; + +public enum CountryCode { + BRUNEI("브루나이"), + SINGAPORE("싱가포르"), + AZERBAIJAN("아제르바이잔"), + INDONESIA("인도네시아"), + JAPAN("일본"), + TURKEY("튀르키예"), + HONG_KONG("홍콩"), + UNITED_STATES("미국"), + CANADA("캐나다"), + AUSTRALIA("호주"), + BRAZIL("브라질"), + NETHERLANDS("네덜란드"), + NORWAY("노르웨이"), + DENMARK("덴마크"), + GERMANY("독일"), + SWEDEN("스웨덴"), + SWITZERLAND("스위스"), + SPAIN("스페인"), + UNITED_KINGDOM("영국"), + AUSTRIA("오스트리아"), + ITALY("이탈리아"), + CZECH_REPUBLIC("체코"), + PORTUGAL("포르투갈"), + FRANCE("프랑스"), + FINLAND("핀란드"), + CHINA("중국"), + TAIWAN("대만"); + + private final String koreanName; + + CountryCode(String koreanName) { + this.koreanName = koreanName; + } + + public static CountryCode getCountryCodeByKoreanName(String koreanName) { + for (CountryCode countryCode : CountryCode.values()) { + if (countryCode.getKoreanName().equals(koreanName)) { + return countryCode; + } + } + throw new IllegalArgumentException("No country found with Korean name: " + koreanName); //TODO: 에러 타입 정리 필요 + } + + public String getKoreanName() { + return koreanName; + } +} diff --git a/src/main/java/com/example/solidconnection/type/ExchangeSemester.java b/src/main/java/com/example/solidconnection/type/ExchangeSemester.java new file mode 100644 index 000000000..67129d1c8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/ExchangeSemester.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.type; + +public enum ExchangeSemester { + ONE_SEMESTER("1개학기"), + NO_PREFERENCE("무관"); + + private final String koreanName; + + ExchangeSemester(String koreanName) { + this.koreanName = koreanName; + } + + public String getKoreanName() { + return koreanName; + } +} diff --git a/src/main/java/com/example/solidconnection/type/LanguageTestType.java b/src/main/java/com/example/solidconnection/type/LanguageTestType.java new file mode 100644 index 000000000..0d80fc019 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/LanguageTestType.java @@ -0,0 +1,5 @@ +package com.example.solidconnection.type; + +public enum LanguageTestType { + TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DOULINGO, CEFR +} diff --git a/src/main/java/com/example/solidconnection/type/PreparationStatus.java b/src/main/java/com/example/solidconnection/type/PreparationStatus.java new file mode 100644 index 000000000..d21530cad --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/PreparationStatus.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.type; + +public enum PreparationStatus { + CONSIDERING, // 교환학생 지원 고민 상태 + PREPARING_FOR_DEPARTURE, // 교환학생 합격 후 파견 준비 상태 + STUDYING_ABROAD // 해외 학교에서 공부중인 상태 +} diff --git a/src/main/java/com/example/solidconnection/type/RegionCode.java b/src/main/java/com/example/solidconnection/type/RegionCode.java new file mode 100644 index 000000000..db3e533fa --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/RegionCode.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.type; + +public enum RegionCode { + EUROPE, + AMERICAS, + ASIA +} diff --git a/src/main/java/com/example/solidconnection/type/Role.java b/src/main/java/com/example/solidconnection/type/Role.java new file mode 100644 index 000000000..aaf464bf8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/Role.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.type; + +public enum Role { + MENTOR, + MENTEE +} diff --git a/src/main/java/com/example/solidconnection/type/TuitionFeePaymentType.java b/src/main/java/com/example/solidconnection/type/TuitionFeePaymentType.java new file mode 100644 index 000000000..dfbe189ea --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/TuitionFeePaymentType.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.type; + +public enum TuitionFeePaymentType { + HOME_UNIVERSITY_PAYMENT("본교등록금납부형"), + OVERSEAS_UNIVERSITY_PAYMENT("해외대학등록금납부형"), + MIXED_PAYMENT("혼합형"); + + private final String koreanName; + + TuitionFeePaymentType(String koreanName) { + this.koreanName = koreanName; + } + + public String getKoreanName() { + return koreanName; + } +} From e957c41d64d5374ff377abccc266d33993621d79 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 21 Jan 2024 18:27:49 +0900 Subject: [PATCH 007/158] =?UTF-8?q?feat:=20gradle=20=EC=84=A4=EC=A0=95=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 --- build.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 802f27812..dd76ce5f6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.2.1' + id 'org.springframework.boot' version '3.1.5' id 'io.spring.dependency-management' version '1.1.4' } @@ -23,9 +23,13 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.mysql:mysql-connector-j:8.2.0' + implementation 'org.hibernate:hibernate-core:6.3.0.CR1' + compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + } tasks.named('test') { From c395fb5468756d38f09c083e1c03c0e82c368297 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 21 Jan 2024 18:28:20 +0900 Subject: [PATCH 008/158] =?UTF-8?q?feat:=20schema.sql=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 113 ++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/main/resources/schema.sql diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..703ae55b9 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,113 @@ +-- Country +CREATE TABLE country ( + country_code VARCHAR(255) PRIMARY KEY, + name VARCHAR(255) NOT NULL, + region_code VARCHAR(255), + FOREIGN KEY (region_code) REFERENCES region(region_code) +); + +-- GpaRequirement +CREATE TABLE gpa_requirement ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + scale VARCHAR(255) NOT NULL, + min_gpa FLOAT NOT NULL, + university_id BIGINT, + FOREIGN KEY (university_id) REFERENCES university(id) +); + +-- InterestedCountry +CREATE TABLE interested_country ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + site_user_id BIGINT, + country_code VARCHAR(255), + region_code VARCHAR(255), + FOREIGN KEY (site_user_id) REFERENCES site_user(id), + FOREIGN KEY (country_code) REFERENCES country(country_code), + FOREIGN KEY (region_code) REFERENCES region(region_code) +); + +-- Region +CREATE TABLE region ( + region_code VARCHAR(255) PRIMARY KEY +); + +-- University +CREATE TABLE university ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + korean_name VARCHAR(255) NOT NULL, + english_name VARCHAR(255) NOT NULL, + internal_name VARCHAR(255) NOT NULL, + recruit_number INT, + tuition_fee_payment_type VARCHAR(255), -- Enum, adjust as needed + exchange_semester VARCHAR(255), -- Enum, adjust as needed + details_for_language TEXT, + details_for_apply TEXT, + details_for_major TEXT, + details_for_accommodation TEXT, + homepage_url VARCHAR(255), + english_course_url VARCHAR(255), + accommodation_url VARCHAR(255), + details TEXT, + logo_image_url VARCHAR(255), + background_image_url VARCHAR(255), + country_code VARCHAR(255), + region_code VARCHAR(255), + FOREIGN KEY (country_code) REFERENCES country(country_code), + FOREIGN KEY (region_code) REFERENCES region(region_code) +); + +-- WishUniversity +CREATE TABLE wish_university ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + university_id BIGINT, + site_user_id BIGINT, + FOREIGN KEY (university_id) REFERENCES university(id), + FOREIGN KEY (site_user_id) REFERENCES site_user(id) +); + +-- InterestedRegion +CREATE TABLE interested_region ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + site_user_id BIGINT, + region_code VARCHAR(255), + FOREIGN KEY (site_user_id) REFERENCES site_user(id), + FOREIGN KEY (region_code) REFERENCES region(region_code) +); + +-- Application +CREATE TABLE application ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + language_test_type VARCHAR(255), -- Enum, adjust as needed + language_test_score VARCHAR(255), + language_test_report_url VARCHAR(255), + gpa FLOAT NOT NULL, + gpa_report_url VARCHAR(255), + verify_status VARCHAR(255), + first_choice_university_id BIGINT, + second_choice_university_id BIGINT, + site_user_id BIGINT, + FOREIGN KEY (first_choice_university_id) REFERENCES university(id), + FOREIGN KEY (second_choice_university_id) REFERENCES university(id), + FOREIGN KEY (site_user_id) REFERENCES site_user(id) +); + +-- LanguageRequirement +CREATE TABLE language_requirement ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + language_test_type VARCHAR(255), -- Enum, adjust as needed + min_score VARCHAR(255), + university_id BIGINT, + FOREIGN KEY (university_id) REFERENCES university(id) +); + +-- SiteUser +CREATE TABLE site_user ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + nickname VARCHAR(255) NOT NULL UNIQUE, + preparation_stage VARCHAR(255), -- Enum, adjust as needed + profile_image_url VARCHAR(255), + nickname_modified_at TIMESTAMP, + quited_at TIMESTAMP, + role VARCHAR(255) -- Enum, adjust as needed +); From 4d259c1806eb49fca50f1602aac9f888aa0c7b89 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 26 Jan 2024 09:09:14 +0900 Subject: [PATCH 009/158] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5,=20=EC=97=90=EB=9F=AC=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../custom/exception/CustomException.java | 16 +++++++++++++++ .../exception/CustomExceptionHandler.java | 16 +++++++++++++++ .../custom/exception/ErrorCode.java | 20 +++++++++++++++++++ .../custom/response/CustomResponse.java | 4 ++++ .../custom/response/DataResponse.java | 13 ++++++++++++ .../custom/response/ErrorResponse.java | 13 ++++++++++++ 6 files changed, 82 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/custom/exception/CustomException.java create mode 100644 src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java create mode 100644 src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java create mode 100644 src/main/java/com/example/solidconnection/custom/response/CustomResponse.java create mode 100644 src/main/java/com/example/solidconnection/custom/response/DataResponse.java create mode 100644 src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java new file mode 100644 index 000000000..0805b5444 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.custom.exception; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CustomException extends RuntimeException { + private int code; + private String message; + + public CustomException(ErrorCode errorCode){ + code = errorCode.getCode(); + message = errorCode.getMessage(); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java new file mode 100644 index 000000000..3a7c4b6ac --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.custom.exception; + +import com.example.solidconnection.custom.response.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Slf4j +@ControllerAdvice +public class CustomExceptionHandler { + + @ExceptionHandler(CustomException.class) + protected ErrorResponse handleCustomException(CustomException e) { + return new ErrorResponse<>(e); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java new file mode 100644 index 000000000..d6644712b --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.custom.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), + KAKAO_ACCESS_TOKEN_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 엑세스 토큰 발급에 실패했습니다."), + KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 사용자 정보 조회에 실패했습니다."), + ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), + REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), + KAKAO_AUTH_CODE_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"카카오 인증 코드가 만료되었습니다. 카카오 로그인 후 다시 시도해주세요.") + ; + + private final int code; + private final String message; +} diff --git a/src/main/java/com/example/solidconnection/custom/response/CustomResponse.java b/src/main/java/com/example/solidconnection/custom/response/CustomResponse.java new file mode 100644 index 000000000..18c5d35b2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/response/CustomResponse.java @@ -0,0 +1,4 @@ +package com.example.solidconnection.custom.response; + +public class CustomResponse { +} diff --git a/src/main/java/com/example/solidconnection/custom/response/DataResponse.java b/src/main/java/com/example/solidconnection/custom/response/DataResponse.java new file mode 100644 index 000000000..c92e6ec43 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/response/DataResponse.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.custom.response; + +import lombok.Getter; + +@Getter +public class DataResponse extends CustomResponse { + private final Boolean success = true; + private final T data; + + public DataResponse(T data){ + this.data = data; + } +} diff --git a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java new file mode 100644 index 000000000..a13000544 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.custom.response; + +import lombok.Getter; + +@Getter +public class ErrorResponse extends CustomResponse { + private final Boolean success = false; + private final CustomException exception; + + public ErrorResponse(CustomException exception) { + this.exception = exception; + } +} From 9eb115e93593f2d067df58089bdcf7e878bc1ace Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 26 Jan 2024 09:09:53 +0900 Subject: [PATCH 010/158] =?UTF-8?q?feat:=20=EC=8A=A4=ED=94=84=EB=A7=81=20?= =?UTF-8?q?=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0,=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0,=20Redis=20=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/redis/RedisConfig.java | 39 ++++++++ .../config/rest/RestTemplateConfig.java | 20 ++++ .../security/JwtAuthenticationEntryPoint.java | 26 +++++ .../security/JwtAuthenticationFilter.java | 79 +++++++++++++++ .../security/SecurityConfiguration.java | 59 +++++++++++ .../config/security/TokenProvider.java | 97 +++++++++++++++++++ .../config/security/TokenType.java | 18 ++++ 7 files changed, 338 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/config/redis/RedisConfig.java create mode 100644 src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java create mode 100644 src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java create mode 100644 src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java create mode 100644 src/main/java/com/example/solidconnection/config/security/TokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/config/security/TokenType.java diff --git a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java new file mode 100644 index 000000000..1c02f7196 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java @@ -0,0 +1,39 @@ +package com.example.solidconnection.config.redis; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + private final String redisHost; + + private final int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + + public RedisConfig(@Value("${spring.data.redis.host}") final String redisHost, + @Value("${spring.data.redis.port}") final int redisPort) { + this.redisHost = redisHost; + this.redisPort = redisPort; + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java b/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java new file mode 100644 index 000000000..4bd0354d2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.config.rest; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(5)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java new file mode 100644 index 000000000..06b6ec785 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.config.security; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Authentication Failed: " + authException.getMessage()); + } + + public void customCommence(HttpServletResponse response) throws IOException, ServletException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Authentication Failed: You are logged out." ); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java new file mode 100644 index 000000000..9bc40afe1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -0,0 +1,79 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.exception.CustomException; +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.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.HashSet; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + public static final String TOKEN_HEADER = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; + private final TokenProvider tokenProvider; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + AntPathMatcher pathMatcher = new AntPathMatcher(); + + for (String endpoint : getPermitAllEndpoints()) { + if (pathMatcher.match(endpoint, request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + } + + try { + String token = this.resolveTokenFromRequest(request); // 웹 요청에서 토큰 추출 + if (StringUtils.hasText(token) && this.tokenProvider.validateAccessToken(token)) { // 토큰이 실제 텍스트를 가지고 있으며, 유효한지 검증 + Authentication auth = this.tokenProvider.getAuthentication(token); // 토큰에서 인증 정보 가져옴. + SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 + } else { + throw new AuthenticationException("Invalid token") {}; // 토큰이 없거나 유효하지 않다면 에러 발생 + } + filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 + } catch (AuthenticationException e) { + jwtAuthenticationEntryPoint.commence(request, response, e); + } catch (CustomException e) { + jwtAuthenticationEntryPoint.customCommence(response); + } + } + + private String resolveTokenFromRequest(HttpServletRequest request) { + String token = request.getHeader(TOKEN_HEADER); + + if (!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) { // 토큰이 비어 있지 않고, Bearer로 시작한다면 + return token.substring(TOKEN_PREFIX.length()); // Bearer 제외한 실제 토큰 부분 반환 + } + return null; + } + + private HashSet getPermitAllEndpoints() { + var permitAllEndpoints = new HashSet(); + + permitAllEndpoints.add("/img-upload/profile"); + permitAllEndpoints.add("/img-upload/gpa"); + permitAllEndpoints.add("/img-upload/language"); + + permitAllEndpoints.add("/auth/kakao"); + permitAllEndpoints.add("/auth/sign-up"); + + return permitAllEndpoints; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java new file mode 100644 index 000000000..9a07ffc01 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -0,0 +1,59 @@ +package com.example.solidconnection.config.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@RequiredArgsConstructor +public class SecurityConfiguration { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("https://www.solid-connect.net", "http://localhost:8080", "https://www.api.solid-connect.net")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("Authorization", "content-type")); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) + .httpBasic(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement((session) -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authorizeRequest + -> authorizeRequest + .requestMatchers( + "/img-upload/profile", "/img-upload/gpa", "/img-upload/language", + "/auth/kakao", "/auth/sign-up") + .permitAll() + .anyRequest().authenticated()) + .addFilterBefore(this.jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .formLogin(AbstractHttpConfigurer::disable); + + return http.build(); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/TokenProvider.java b/src/main/java/com/example/solidconnection/config/security/TokenProvider.java new file mode 100644 index 000000000..beee3b546 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/TokenProvider.java @@ -0,0 +1,97 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static com.example.solidconnection.custom.exception.ErrorCode.EMAIL_NOT_FOUND; +import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; + +@Component +@RequiredArgsConstructor +public class TokenProvider { + private final RedisTemplate redisTemplate; + private final SiteUserRepository siteUserRepository; + + @Value("${jwt.secret}") + private String secretKey; + + public String generateToken(String email, TokenType tokenType) { + Claims claims = Jwts.claims().setSubject(email); + + + var now = new Date(); + var expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, this.secretKey) + .compact(); + } + + public String saveToken(String email, TokenType tokenType) { + String token = generateToken(email, tokenType); + + redisTemplate.opsForValue().set( + tokenType.getPrefix() + email, + token, + tokenType.getExpireTime(), + TimeUnit.MILLISECONDS + ); + return token; + } + + public Authentication getAuthentication(String token) { + UserDetails userDetails = (UserDetails) siteUserRepository.findByEmail(this.getUserEmail(token)) + .orElseThrow(() -> new CustomException(EMAIL_NOT_FOUND)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String getUserEmail(String token) { + return this.parseClaims(token).getSubject(); + } + + public boolean validateAccessToken(String token) { + if (!StringUtils.hasText(token)) { + return false; + } + validateToken(token, TokenType.REFRESH); + return isExpired(token); + } + + private void validateToken(String token, TokenType tokenType) { + if (token.equals(redisTemplate.opsForValue().get(tokenType.getPrefix() + getUserEmail(token)))) { + throw new CustomException(REFRESH_TOKEN_EXPIRED); + } + } + + private Claims parseClaims(String token) { + try { + return Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + public boolean isExpired(String token) { + Date expiration = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody().getExpiration(); + long now = new Date().getTime(); + return (expiration.getTime() - now) < 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/config/security/TokenType.java b/src/main/java/com/example/solidconnection/config/security/TokenType.java new file mode 100644 index 000000000..5bef0b7b2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/TokenType.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.config.security; + +import lombok.Getter; + +@Getter +public enum TokenType { + ACCESS("", 1000 * 60 * 60), + REFRESH("refresh:", 1000 * 60 * 60 * 24 * 7), + KAKAO_OAUTH("kakao:", 1000 * 60 * 60); + + private final String prefix; + private final int expireTime; + + TokenType(String prefix, int expireTime){ + this.prefix = prefix; + this.expireTime = expireTime; + } +} From 053b75acdf7a3474f5350cd6507a2466bcc34dc2 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 26 Jan 2024 09:10:12 +0900 Subject: [PATCH 011/158] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +- .../auth/controller/AuthController.java | 25 ++++ .../auth/dto/FirstAccessResponseDto.java | 26 +++++ .../auth/dto/KakaoAccount.java | 19 ++++ .../auth/dto/KakaoCodeDto.java | 12 ++ .../auth/dto/KakaoOauthResponseDto.java | 4 + .../auth/dto/KakaoProfile.java | 19 ++++ .../auth/dto/KakaoTokenDto.java | 20 ++++ .../auth/dto/KakaoUserInfoDto.java | 18 +++ .../auth/dto/SignInResponseDto.java | 14 +++ .../auth/service/KakaoOAuthService.java | 107 ++++++++++++++++++ .../custom/response/StatusResponse.java | 12 ++ .../repository/SiteUserRepository.java | 14 +++ .../type/PreparationStatus.java | 3 +- 14 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/controller/AuthController.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/FirstAccessResponseDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/KakaoAccount.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/KakaoCodeDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/KakaoOauthResponseDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/KakaoProfile.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/KakaoTokenDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/KakaoUserInfoDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java create mode 100644 src/main/java/com/example/solidconnection/custom/response/StatusResponse.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java diff --git a/build.gradle b/build.gradle index dd76ce5f6..b6e8719eb 100644 --- a/build.gradle +++ b/build.gradle @@ -26,10 +26,16 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'com.mysql:mysql-connector-j:8.2.0' implementation 'org.hibernate:hibernate-core:6.3.0.CR1' + implementation 'org.springframework.data:spring-data-redis:3.1.2' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'org.springframework.security:spring-security-core:6.1.2' + implementation 'org.springframework.security:spring-security-config:6.1.2' + implementation 'org.springframework.security:spring-security-web:6.1.2' + implementation 'io.lettuce:lettuce-core:6.2.5.RELEASE' + implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - } tasks.named('test') { diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java new file mode 100644 index 000000000..e564aa7b7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.auth.controller; + +import com.example.solidconnection.auth.dto.KakaoCodeDto; +import com.example.solidconnection.auth.dto.KakaoOauthResponseDto; +import com.example.solidconnection.auth.service.KakaoOAuthService; +import com.example.solidconnection.custom.response.CustomResponse; +import com.example.solidconnection.custom.response.DataResponse; +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("auth") +@RequiredArgsConstructor +public class AuthController { + private final KakaoOAuthService kakaoOAuthService; + + @PostMapping("/kakao") + public CustomResponse signUp(@RequestBody KakaoCodeDto kakaoCodeDto) { + KakaoOauthResponseDto kakaoOauthResponseDto = kakaoOAuthService.processOauth(kakaoCodeDto.getCode()); + return new DataResponse<>(kakaoOauthResponseDto); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/FirstAccessResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/FirstAccessResponseDto.java new file mode 100644 index 000000000..a28522320 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/FirstAccessResponseDto.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.auth.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FirstAccessResponseDto extends KakaoOauthResponseDto { + private boolean registered; + private String nickname; + private String email; + private String profileImageUrl; + private String kakaoOauthToken; + + public static FirstAccessResponseDto fromKakaoUserInfo(KakaoUserInfoDto kakaoUserInfoDto, String kakaoOauthToken){ + return FirstAccessResponseDto.builder() + .registered(false) + .email(kakaoUserInfoDto.getKakaoAccount().getEmail()) + .profileImageUrl(kakaoUserInfoDto.getKakaoAccount().getProfile().getProfileImageUrl()) + .nickname(kakaoUserInfoDto.getKakaoAccount().getProfile().getNickname()) + .kakaoOauthToken(kakaoOauthToken) + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoAccount.java b/src/main/java/com/example/solidconnection/auth/dto/KakaoAccount.java new file mode 100644 index 000000000..47e07d23d --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/KakaoAccount.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.auth.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class KakaoAccount { + @JsonProperty("profile") + private KakaoProfile profile; + private String email; +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoCodeDto.java b/src/main/java/com/example/solidconnection/auth/dto/KakaoCodeDto.java new file mode 100644 index 000000000..625c99969 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/KakaoCodeDto.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.auth.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class KakaoCodeDto { + private String code; +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoOauthResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/KakaoOauthResponseDto.java new file mode 100644 index 000000000..f24255e85 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/KakaoOauthResponseDto.java @@ -0,0 +1,4 @@ +package com.example.solidconnection.auth.dto; + +public class KakaoOauthResponseDto { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoProfile.java b/src/main/java/com/example/solidconnection/auth/dto/KakaoProfile.java new file mode 100644 index 000000000..323dbb798 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/KakaoProfile.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.auth.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class KakaoProfile { + private String nickname; + @JsonProperty("profile_image_url") + private String profileImageUrl; +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/KakaoTokenDto.java new file mode 100644 index 000000000..f51d9decb --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/KakaoTokenDto.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.auth.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class KakaoTokenDto { + @JsonProperty("access_token") + private String accessToken; + @JsonProperty("refresh_token") + private String refreshToken; +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/KakaoUserInfoDto.java new file mode 100644 index 000000000..c2cf1d982 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/KakaoUserInfoDto.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.auth.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@NoArgsConstructor +public class KakaoUserInfoDto { + @JsonProperty("kakao_account") + private KakaoAccount kakaoAccount; +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java new file mode 100644 index 000000000..55c02509e --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.auth.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SignInResponseDto extends KakaoOauthResponseDto { + private boolean registered; + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java new file mode 100644 index 000000000..37f3b56d4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -0,0 +1,107 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.*; +import com.example.solidconnection.config.security.TokenProvider; +import com.example.solidconnection.config.security.TokenType; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; + +@Service +@RequiredArgsConstructor +public class KakaoOAuthService { + + private final RestTemplate restTemplate; + private final TokenProvider tokenProvider; + private final SiteUserRepository siteUserRepository; + + @Value("${kakao.client_id}") + private String clientId; + @Value("${kakao.redirect_uri}") + private String redirectUri; + @Value("${kakao.token_url}") + private String tokenUrl; + @Value("${kakao.user_info_url}") + private String userInfoUrl; + + public KakaoOauthResponseDto processOauth(String code) { + String kakaoAccessToken = getKakaoAccessToken(code); + KakaoUserInfoDto kakaoUserInfoDto = getKakaoUserInfo(kakaoAccessToken); + String email = kakaoUserInfoDto.getKakaoAccount().getEmail(); + boolean isAlreadyRegistered = siteUserRepository.existsByEmail(email); + if (isAlreadyRegistered) { + return kakaoSignIn(email); + } + String kakaoOauthToken = tokenProvider.generateToken(email, TokenType.KAKAO_OAUTH); + return FirstAccessResponseDto.fromKakaoUserInfo(kakaoUserInfoDto, kakaoOauthToken); + } + + private String getKakaoAccessToken(String code) { + // 카카오 엑세스 토큰 요청 + ResponseEntity response = restTemplate.exchange( + buildTokenUri(code), + HttpMethod.POST, + null, + KakaoTokenDto.class + ); + + // 응답 예외처리 + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + return response.getBody().getAccessToken(); + } else { + throw new CustomException(KAKAO_ACCESS_TOKEN_FAIL); + } + } + + // 카카오에게 엑세스 토큰 발급 요청하는 URI 생성 + private String buildTokenUri(String code) { + return UriComponentsBuilder.fromHttpUrl(tokenUrl) + .queryParam("grant_type", "authorization_code") + .queryParam("client_id", clientId) + .queryParam("redirect_uri", redirectUri) + .queryParam("code", code) + .toUriString(); + } + + private KakaoUserInfoDto getKakaoUserInfo(String accessToken) { + // 카카오 엑세스 토큰을 헤더에 담은 HttpEntity + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity entity = new HttpEntity<>(headers); + + // 사용자의 정보 요청 + ResponseEntity response = restTemplate.exchange( + userInfoUrl, + HttpMethod.GET, + entity, + KakaoUserInfoDto.class + ); + + // 응답 예외처리 + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + return response.getBody(); + } else { + throw new CustomException(KAKAO_USER_INFO_FAIL); + } + } + + private SignInResponseDto kakaoSignIn(String email) { + siteUserRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(EMAIL_NOT_FOUND)); + + var accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + var refreshToken = tokenProvider.saveToken(email, TokenType.REFRESH); + return SignInResponseDto.builder() + .registered(true) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java b/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java new file mode 100644 index 000000000..b9cd7684f --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.custom.response; + +import lombok.Getter; + +@Getter +public class StatusResponse extends CustomResponse { + private final Boolean status; + + StatusResponse(Boolean status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java new file mode 100644 index 000000000..df58d5ac2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.siteuser.repository; + +import com.example.solidconnection.entity.SiteUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SiteUserRepository extends JpaRepository { + Optional findByEmail(String email); + boolean existsByEmail(String email); + boolean existsByNickname(String nickname); +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/type/PreparationStatus.java b/src/main/java/com/example/solidconnection/type/PreparationStatus.java index d21530cad..c4f1650e9 100644 --- a/src/main/java/com/example/solidconnection/type/PreparationStatus.java +++ b/src/main/java/com/example/solidconnection/type/PreparationStatus.java @@ -3,5 +3,6 @@ public enum PreparationStatus { CONSIDERING, // 교환학생 지원 고민 상태 PREPARING_FOR_DEPARTURE, // 교환학생 합격 후 파견 준비 상태 - STUDYING_ABROAD // 해외 학교에서 공부중인 상태 + STUDYING_ABROAD, // 해외 학교에서 공부중인 상태 + AFTER_EXCHANGE } From a30b1b0f828c2c20492b8822918c15d05c8339c3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 27 Jan 2024 18:39:15 +0900 Subject: [PATCH 012/158] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/controller/AuthController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index e564aa7b7..a4ce65835 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -18,8 +18,9 @@ public class AuthController { private final KakaoOAuthService kakaoOAuthService; @PostMapping("/kakao") - public CustomResponse signUp(@RequestBody KakaoCodeDto kakaoCodeDto) { + public CustomResponse kakaoOauth(@RequestBody KakaoCodeDto kakaoCodeDto) { KakaoOauthResponseDto kakaoOauthResponseDto = kakaoOAuthService.processOauth(kakaoCodeDto.getCode()); return new DataResponse<>(kakaoOauthResponseDto); } + } From ab1d40ce40e71d2a857cdb6a36deb60dda8707e0 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 27 Jan 2024 18:46:16 +0900 Subject: [PATCH 013/158] =?UTF-8?q?refactor:=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=97=B0=EA=B2=B0=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 0c0efabe1..81d218964 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -1 +1,3 @@ -

Solid Connection Backend Page

\ No newline at end of file +

Solid Connection Backend Page

+ +이 페이지가 보이면 백엔드 배포 서버가 잘 작동하고 있다는 것입니다. \ No newline at end of file From 02a5dd2e1a176b5473ae071d96fcac8f125c1f54 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 27 Jan 2024 18:55:54 +0900 Subject: [PATCH 014/158] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/SolidConnectionApplicationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java b/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java index 76051385e..3a68f4d45 100644 --- a/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java +++ b/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +@SpringBootTest(classes = SolidConnectionApplicationTests.class) class SolidConnectionApplicationTests { @Test From f336a6257eec1c2b12c9ee1251721f61011990ba Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 27 Jan 2024 19:18:18 +0900 Subject: [PATCH 015/158] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/SolidConnectionApplicationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java b/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java index 3a68f4d45..76051385e 100644 --- a/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java +++ b/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest(classes = SolidConnectionApplicationTests.class) +@SpringBootTest class SolidConnectionApplicationTests { @Test From 1ce7fe136c5174dae7726a4c5d8064b865adca9b Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 27 Jan 2024 20:52:35 +0900 Subject: [PATCH 016/158] =?UTF-8?q?refactor:=20security=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/KakaoOAuthService.java | 12 ++-- .../security/JwtAuthenticationEntryPoint.java | 22 ++++--- .../security/JwtAuthenticationFilter.java | 33 ++++++----- .../security/SecurityConfiguration.java | 1 + .../TokenService.java} | 46 +++------------ .../config/{security => token}/TokenType.java | 2 +- .../config/token/TokenValidator.java | 58 +++++++++++++++++++ .../custom/exception/CustomException.java | 5 ++ .../custom/exception/ErrorCode.java | 2 + 9 files changed, 113 insertions(+), 68 deletions(-) rename src/main/java/com/example/solidconnection/config/{security/TokenProvider.java => token/TokenService.java} (61%) rename src/main/java/com/example/solidconnection/config/{security => token}/TokenType.java (87%) create mode 100644 src/main/java/com/example/solidconnection/config/token/TokenValidator.java diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java index 37f3b56d4..49fc09ab0 100644 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -1,8 +1,8 @@ package com.example.solidconnection.auth.service; import com.example.solidconnection.auth.dto.*; -import com.example.solidconnection.config.security.TokenProvider; -import com.example.solidconnection.config.security.TokenType; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; @@ -19,7 +19,7 @@ public class KakaoOAuthService { private final RestTemplate restTemplate; - private final TokenProvider tokenProvider; + private final TokenService tokenService; private final SiteUserRepository siteUserRepository; @Value("${kakao.client_id}") @@ -39,7 +39,7 @@ public KakaoOauthResponseDto processOauth(String code) { if (isAlreadyRegistered) { return kakaoSignIn(email); } - String kakaoOauthToken = tokenProvider.generateToken(email, TokenType.KAKAO_OAUTH); + String kakaoOauthToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); return FirstAccessResponseDto.fromKakaoUserInfo(kakaoUserInfoDto, kakaoOauthToken); } @@ -96,8 +96,8 @@ private SignInResponseDto kakaoSignIn(String email) { siteUserRepository.findByEmail(email) .orElseThrow(() -> new CustomException(EMAIL_NOT_FOUND)); - var accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - var refreshToken = tokenProvider.saveToken(email, TokenType.REFRESH); + var accessToken = tokenService.generateToken(email, TokenType.ACCESS); + var refreshToken = tokenService.saveToken(email, TokenType.REFRESH); return SignInResponseDto.builder() .registered(true) .accessToken(accessToken) diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 06b6ec785..8166d5b6a 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -1,26 +1,32 @@ package com.example.solidconnection.config.security; -import jakarta.servlet.ServletException; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.response.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; + @Component +@RequiredArgsConstructor public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper; + @Override public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException, ServletException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write("Authentication Failed: " + authException.getMessage()); - } - - public void customCommence(HttpServletResponse response) throws IOException, ServletException { + AuthenticationException authException) throws IOException { + ErrorResponse errorResponse = new ErrorResponse<>(new CustomException(AUTHENTICATION_FAILED, authException.getMessage())); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write("Authentication Failed: You are logged out." ); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 9bc40afe1..497a6f27a 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -1,6 +1,7 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenValidator; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -12,7 +13,6 @@ import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -24,14 +24,15 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; - private final TokenProvider tokenProvider; + private final TokenService tokenService; + private final TokenValidator tokenValidator; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - AntPathMatcher pathMatcher = new AntPathMatcher(); + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + AntPathMatcher pathMatcher = new AntPathMatcher(); for (String endpoint : getPermitAllEndpoints()) { if (pathMatcher.match(endpoint, request.getRequestURI())) { filterChain.doFilter(request, response); @@ -40,22 +41,17 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } try { - String token = this.resolveTokenFromRequest(request); // 웹 요청에서 토큰 추출 - if (StringUtils.hasText(token) && this.tokenProvider.validateAccessToken(token)) { // 토큰이 실제 텍스트를 가지고 있으며, 유효한지 검증 - Authentication auth = this.tokenProvider.getAuthentication(token); // 토큰에서 인증 정보 가져옴. - SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 - } else { - throw new AuthenticationException("Invalid token") {}; // 토큰이 없거나 유효하지 않다면 에러 발생 - } + String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 + tokenValidator.validateAccessToken(token); // 유효한 액세스 토큰인지 검증 + Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 + SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 } catch (AuthenticationException e) { jwtAuthenticationEntryPoint.commence(request, response, e); - } catch (CustomException e) { - jwtAuthenticationEntryPoint.customCommence(response); } } - private String resolveTokenFromRequest(HttpServletRequest request) { + private String resolveAccessTokenFromRequest(HttpServletRequest request) { String token = request.getHeader(TOKEN_HEADER); if (!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) { // 토큰이 비어 있지 않고, Bearer로 시작한다면 @@ -67,10 +63,15 @@ private String resolveTokenFromRequest(HttpServletRequest request) { private HashSet getPermitAllEndpoints() { var permitAllEndpoints = new HashSet(); + // 서버 정상 작동 확인 + permitAllEndpoints.add(""); + + // 이미지 업로드 permitAllEndpoints.add("/img-upload/profile"); permitAllEndpoints.add("/img-upload/gpa"); permitAllEndpoints.add("/img-upload/language"); + // 토큰이 필요하지 않은 인증 permitAllEndpoints.add("/auth/kakao"); permitAllEndpoints.add("/auth/sign-up"); diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 9a07ffc01..b74b5dba8 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -47,6 +47,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(authorizeRequest -> authorizeRequest .requestMatchers( + "", "/img-upload/profile", "/img-upload/gpa", "/img-upload/language", "/auth/kakao", "/auth/sign-up") .permitAll() diff --git a/src/main/java/com/example/solidconnection/config/security/TokenProvider.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java similarity index 61% rename from src/main/java/com/example/solidconnection/config/security/TokenProvider.java rename to src/main/java/com/example/solidconnection/config/token/TokenService.java index beee3b546..6de984a76 100644 --- a/src/main/java/com/example/solidconnection/config/security/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenService.java @@ -1,9 +1,8 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.config.token; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.RequiredArgsConstructor; @@ -13,17 +12,15 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import java.util.Date; import java.util.concurrent.TimeUnit; import static com.example.solidconnection.custom.exception.ErrorCode.EMAIL_NOT_FOUND; -import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @Component @RequiredArgsConstructor -public class TokenProvider { +public class TokenService { private final RedisTemplate redisTemplate; private final SiteUserRepository siteUserRepository; @@ -33,7 +30,6 @@ public class TokenProvider { public String generateToken(String email, TokenType tokenType) { Claims claims = Jwts.claims().setSubject(email); - var now = new Date(); var expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); @@ -58,40 +54,16 @@ public String saveToken(String email, TokenType tokenType) { } public Authentication getAuthentication(String token) { - UserDetails userDetails = (UserDetails) siteUserRepository.findByEmail(this.getUserEmail(token)) + String email = getClaim(token).getSubject(); + UserDetails userDetails = (UserDetails) siteUserRepository.findByEmail(email) .orElseThrow(() -> new CustomException(EMAIL_NOT_FOUND)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } - public String getUserEmail(String token) { - return this.parseClaims(token).getSubject(); - } - - public boolean validateAccessToken(String token) { - if (!StringUtils.hasText(token)) { - return false; - } - validateToken(token, TokenType.REFRESH); - return isExpired(token); - } - - private void validateToken(String token, TokenType tokenType) { - if (token.equals(redisTemplate.opsForValue().get(tokenType.getPrefix() + getUserEmail(token)))) { - throw new CustomException(REFRESH_TOKEN_EXPIRED); - } - } - - private Claims parseClaims(String token) { - try { - return Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody(); - } catch (ExpiredJwtException e) { - return e.getClaims(); - } - } - - public boolean isExpired(String token) { - Date expiration = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody().getExpiration(); - long now = new Date().getTime(); - return (expiration.getTime() - now) < 0; + private Claims getClaim(String token) { + return Jwts.parser() + .setSigningKey(this.secretKey) + .parseClaimsJws(token) + .getBody(); } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/config/security/TokenType.java b/src/main/java/com/example/solidconnection/config/token/TokenType.java similarity index 87% rename from src/main/java/com/example/solidconnection/config/security/TokenType.java rename to src/main/java/com/example/solidconnection/config/token/TokenType.java index 5bef0b7b2..fb6e7c5a9 100644 --- a/src/main/java/com/example/solidconnection/config/security/TokenType.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenType.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.config.token; import lombok.Getter; diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java new file mode 100644 index 000000000..534661af8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java @@ -0,0 +1,58 @@ +package com.example.solidconnection.config.token; + +import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Date; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; + +@Component +@RequiredArgsConstructor +public class TokenValidator { + private final RedisTemplate redisTemplate; + + @Value("${jwt.secret}") + private String secretKey; + + public void validateAccessToken(String token) { + validateTokenNotEmpty(token); + validateAccessTokenNotExpired(token); + validateRefreshToken(token); + // TODO : validateNotLogOut 함수 생성 및 추가 + } + + private void validateRefreshToken(String token) { + String email = getClaim(token).getSubject(); + if (redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email) != null) { + throw new CustomException(REFRESH_TOKEN_EXPIRED); + } + } + + private void validateAccessTokenNotExpired(String token) { + Date expiration = getClaim(token).getExpiration(); + long now = new Date().getTime(); + if((expiration.getTime() - now) < 0){ + throw new CustomException(ACCESS_TOKEN_EXPIRED); + } + } + + private void validateTokenNotEmpty(String token) { + if (!StringUtils.hasText(token)) { + throw new CustomException(INVALID_TOKEN); + } + } + + private Claims getClaim(String token) { + return Jwts.parser() + .setSigningKey(this.secretKey) + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java index 0805b5444..79f2f8594 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java @@ -13,4 +13,9 @@ public CustomException(ErrorCode errorCode){ code = errorCode.getCode(); message = errorCode.getMessage(); } + + public CustomException(ErrorCode errorCode, String detail){ + code = errorCode.getCode(); + message = errorCode.getMessage() + detail; + } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index d6644712b..097f21596 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,6 +7,8 @@ @Getter @AllArgsConstructor public enum ErrorCode { + INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), KAKAO_ACCESS_TOKEN_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 엑세스 토큰 발급에 실패했습니다."), KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 사용자 정보 조회에 실패했습니다."), From 400fbd66fe63ee8b44676e48e1ab5f65b4abd76f Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 27 Jan 2024 21:41:52 +0900 Subject: [PATCH 017/158] =?UTF-8?q?feat:=20=ED=8F=AC=ED=8A=B8=20=EC=97=B4?= =?UTF-8?q?=EB=A0=B8=EB=8A=94=EC=A7=80=20=ED=99=95=EC=9D=B8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/config/cors/WebConfig.java | 18 ++++++++++++++++++ .../security/JwtAuthenticationFilter.java | 4 +++- .../config/security/SecurityConfiguration.java | 2 +- .../custom/exception/ErrorCode.java | 2 +- src/main/resources/static/index.html | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/config/cors/WebConfig.java diff --git a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java new file mode 100644 index 000000000..030b68250 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.config.cors; + +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 WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:8080") + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(true); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 497a6f27a..2ea95cffe 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -64,7 +64,9 @@ private HashSet getPermitAllEndpoints() { var permitAllEndpoints = new HashSet(); // 서버 정상 작동 확인 - permitAllEndpoints.add(""); + permitAllEndpoints.add("/"); + permitAllEndpoints.add("/index.html"); + permitAllEndpoints.add("/favicon.ico"); // 이미지 업로드 permitAllEndpoints.add("/img-upload/profile"); diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index b74b5dba8..bc7256b28 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -47,7 +47,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(authorizeRequest -> authorizeRequest .requestMatchers( - "", + "/", "/index.html", "/favicon.ico", "/img-upload/profile", "/img-upload/gpa", "/img-upload/language", "/auth/kakao", "/auth/sign-up") .permitAll() diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 097f21596..f131c25bc 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,7 +7,7 @@ @Getter @AllArgsConstructor public enum ErrorCode { - INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), KAKAO_ACCESS_TOKEN_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 엑세스 토큰 발급에 실패했습니다."), diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 81d218964..c3dca01d5 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -1,3 +1,3 @@ +

Solid Connection Backend Page

- 이 페이지가 보이면 백엔드 배포 서버가 잘 작동하고 있다는 것입니다. \ No newline at end of file From aeb4d4aed5b23e81b958dfba6c3de71cdcc4cac1 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 27 Jan 2024 23:54:24 +0900 Subject: [PATCH 018/158] =?UTF-8?q?fix:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=BD=94=EB=93=9C=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 3 +- .../auth/service/KakaoOAuthService.java | 29 +++++++++---------- .../security/JwtAuthenticationEntryPoint.java | 2 +- .../custom/exception/CustomException.java | 7 ++--- .../exception/CustomExceptionHandler.java | 7 +++-- .../custom/exception/ErrorCode.java | 3 +- .../custom/response/ErrorResponse.java | 19 +++++++++--- .../SolidConnectionApplicationTests.java | 2 +- 8 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index a4ce65835..dfd043c94 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -3,6 +3,7 @@ import com.example.solidconnection.auth.dto.KakaoCodeDto; import com.example.solidconnection.auth.dto.KakaoOauthResponseDto; import com.example.solidconnection.auth.service.KakaoOAuthService; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.CustomResponse; import com.example.solidconnection.custom.response.DataResponse; import lombok.RequiredArgsConstructor; @@ -18,7 +19,7 @@ public class AuthController { private final KakaoOAuthService kakaoOAuthService; @PostMapping("/kakao") - public CustomResponse kakaoOauth(@RequestBody KakaoCodeDto kakaoCodeDto) { + public CustomResponse kakaoOauth(@RequestBody KakaoCodeDto kakaoCodeDto) throws CustomException { KakaoOauthResponseDto kakaoOauthResponseDto = kakaoOAuthService.processOauth(kakaoCodeDto.getCode()); return new DataResponse<>(kakaoOauthResponseDto); } diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java index 49fc09ab0..26565d28e 100644 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -12,6 +12,8 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.util.Objects; + import static com.example.solidconnection.custom.exception.ErrorCode.*; @Service @@ -31,7 +33,7 @@ public class KakaoOAuthService { @Value("${kakao.user_info_url}") private String userInfoUrl; - public KakaoOauthResponseDto processOauth(String code) { + public KakaoOauthResponseDto processOauth(String code) throws CustomException { String kakaoAccessToken = getKakaoAccessToken(code); KakaoUserInfoDto kakaoUserInfoDto = getKakaoUserInfo(kakaoAccessToken); String email = kakaoUserInfoDto.getKakaoAccount().getEmail(); @@ -45,18 +47,16 @@ public KakaoOauthResponseDto processOauth(String code) { private String getKakaoAccessToken(String code) { // 카카오 엑세스 토큰 요청 - ResponseEntity response = restTemplate.exchange( - buildTokenUri(code), - HttpMethod.POST, - null, - KakaoTokenDto.class - ); - - // 응답 예외처리 - if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { - return response.getBody().getAccessToken(); - } else { - throw new CustomException(KAKAO_ACCESS_TOKEN_FAIL); + try { + ResponseEntity response = restTemplate.exchange( + buildTokenUri(code), + HttpMethod.POST, + null, + KakaoTokenDto.class + ); + return Objects.requireNonNull(response.getBody()).getAccessToken(); + } catch (Exception e){ + throw new CustomException(KAKAO_AUTH_CODE_FAIL); } } @@ -93,9 +93,6 @@ private KakaoUserInfoDto getKakaoUserInfo(String accessToken) { } private SignInResponseDto kakaoSignIn(String email) { - siteUserRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(EMAIL_NOT_FOUND)); - var accessToken = tokenService.generateToken(email, TokenType.ACCESS); var refreshToken = tokenService.saveToken(email, TokenType.REFRESH); return SignInResponseDto.builder() diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 8166d5b6a..a7c0d94d4 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -23,7 +23,7 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse<>(new CustomException(AUTHENTICATION_FAILED, authException.getMessage())); + ErrorResponse errorResponse = new ErrorResponse(new CustomException(AUTHENTICATION_FAILED, authException.getMessage())); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java index 79f2f8594..08122faf5 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java @@ -1,15 +1,14 @@ package com.example.solidconnection.custom.exception; import lombok.Getter; -import lombok.Setter; @Getter -@Setter public class CustomException extends RuntimeException { - private int code; - private String message; + private final int code; + private final String message; public CustomException(ErrorCode errorCode){ + super(errorCode.getMessage()); code = errorCode.getCode(); message = errorCode.getMessage(); } diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java index 3a7c4b6ac..e30d79134 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java @@ -2,6 +2,8 @@ import com.example.solidconnection.custom.response.ErrorResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -10,7 +12,8 @@ public class CustomExceptionHandler { @ExceptionHandler(CustomException.class) - protected ErrorResponse handleCustomException(CustomException e) { - return new ErrorResponse<>(e); + protected ResponseEntity handleCustomException(CustomException e) { + ErrorResponse errorResponse = new ErrorResponse(e); + return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(e.getCode())); } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index f131c25bc..8640313ee 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -10,11 +10,10 @@ public enum ErrorCode { INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), - KAKAO_ACCESS_TOKEN_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 엑세스 토큰 발급에 실패했습니다."), + KAKAO_AUTH_CODE_FAIL(HttpStatus.BAD_REQUEST.value(),"잘못된 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 사용자 정보 조회에 실패했습니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), - KAKAO_AUTH_CODE_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"카카오 인증 코드가 만료되었습니다. 카카오 로그인 후 다시 시도해주세요.") ; private final int code; diff --git a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java index a13000544..7a2d6aa19 100644 --- a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java +++ b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java @@ -1,13 +1,24 @@ package com.example.solidconnection.custom.response; +import com.example.solidconnection.custom.exception.CustomException; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.Setter; @Getter -public class ErrorResponse extends CustomResponse { +@Setter +public class ErrorResponse extends CustomResponse { private final Boolean success = false; - private final CustomException exception; + private ErrorDetail error; - public ErrorResponse(CustomException exception) { - this.exception = exception; + @Getter + @AllArgsConstructor + private static class ErrorDetail { + int code; + String message; + } + + public ErrorResponse(CustomException e) { + this.error = new ErrorDetail(e.getCode(), e.getMessage()); } } diff --git a/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java b/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java index 76051385e..3a68f4d45 100644 --- a/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java +++ b/src/test/java/com/example/solidconnection/SolidConnectionApplicationTests.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +@SpringBootTest(classes = SolidConnectionApplicationTests.class) class SolidConnectionApplicationTests { @Test From 1c6139296fc90609a5d7833379420feade35c5f7 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 28 Jan 2024 04:28:13 +0900 Subject: [PATCH 019/158] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 16 +- .../auth/dto/KakaoOauthResponseDto.java | 4 - .../auth/dto/SignInResponseDto.java | 1 + .../auth/dto/SignUpRequestDto.java | 23 +++ .../{ => kakao}/FirstAccessResponseDto.java | 2 +- .../auth/dto/{ => kakao}/KakaoAccount.java | 2 +- .../auth/dto/{ => kakao}/KakaoCodeDto.java | 2 +- .../auth/dto/kakao/KakaoOauthResponseDto.java | 4 + .../auth/dto/{ => kakao}/KakaoProfile.java | 2 +- .../auth/dto/{ => kakao}/KakaoTokenDto.java | 2 +- .../dto/{ => kakao}/KakaoUserInfoDto.java | 2 +- .../auth/service/AuthService.java | 117 +++++++++++ .../auth/service/KakaoOAuthService.java | 10 +- .../security/JwtAuthenticationEntryPoint.java | 3 +- .../config/token/TokenService.java | 17 +- .../config/token/TokenValidator.java | 27 ++- .../country/CountryRepository.java | 13 ++ .../country/InterestedCountyRepository.java | 12 ++ .../custom/exception/CustomException.java | 3 +- .../exception/CustomExceptionHandler.java | 12 +- .../custom/exception/ErrorCode.java | 9 +- .../custom/response/ErrorResponse.java | 6 + .../custom/response/StatusResponse.java | 4 +- .../solidconnection/entity/Country.java | 5 +- .../entity/InterestedCountry.java | 14 +- .../entity/InterestedRegion.java | 10 +- .../solidconnection/entity/Region.java | 5 +- .../solidconnection/entity/SiteUser.java | 29 ++- .../solidconnection/entity/University.java | 4 +- .../region/InterestedRegionRepository.java | 12 ++ .../region/RegionRepository.java | 13 ++ .../solidconnection/type/CountryCode.java | 71 ++++--- .../example/solidconnection/type/Gender.java | 5 + .../solidconnection/type/RegionCode.java | 31 ++- src/main/resources/data.sql | 34 +++ src/main/resources/schema.sql | 193 +++++++++--------- 36 files changed, 528 insertions(+), 191 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/KakaoOauthResponseDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java rename src/main/java/com/example/solidconnection/auth/dto/{ => kakao}/FirstAccessResponseDto.java (94%) rename src/main/java/com/example/solidconnection/auth/dto/{ => kakao}/KakaoAccount.java (89%) rename src/main/java/com/example/solidconnection/auth/dto/{ => kakao}/KakaoCodeDto.java (78%) create mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponseDto.java rename src/main/java/com/example/solidconnection/auth/dto/{ => kakao}/KakaoProfile.java (89%) rename src/main/java/com/example/solidconnection/auth/dto/{ => kakao}/KakaoTokenDto.java (90%) rename src/main/java/com/example/solidconnection/auth/dto/{ => kakao}/KakaoUserInfoDto.java (89%) create mode 100644 src/main/java/com/example/solidconnection/auth/service/AuthService.java create mode 100644 src/main/java/com/example/solidconnection/country/CountryRepository.java create mode 100644 src/main/java/com/example/solidconnection/country/InterestedCountyRepository.java create mode 100644 src/main/java/com/example/solidconnection/region/InterestedRegionRepository.java create mode 100644 src/main/java/com/example/solidconnection/region/RegionRepository.java create mode 100644 src/main/java/com/example/solidconnection/type/Gender.java create mode 100644 src/main/resources/data.sql diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index dfd043c94..15bc21377 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -1,11 +1,13 @@ package com.example.solidconnection.auth.controller; -import com.example.solidconnection.auth.dto.KakaoCodeDto; -import com.example.solidconnection.auth.dto.KakaoOauthResponseDto; +import com.example.solidconnection.auth.dto.SignUpRequestDto; +import com.example.solidconnection.auth.dto.kakao.KakaoCodeDto; +import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponseDto; +import com.example.solidconnection.auth.service.AuthService; import com.example.solidconnection.auth.service.KakaoOAuthService; -import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.CustomResponse; import com.example.solidconnection.custom.response.DataResponse; +import com.example.solidconnection.custom.response.StatusResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -17,11 +19,17 @@ @RequiredArgsConstructor public class AuthController { private final KakaoOAuthService kakaoOAuthService; + private final AuthService authService; @PostMapping("/kakao") - public CustomResponse kakaoOauth(@RequestBody KakaoCodeDto kakaoCodeDto) throws CustomException { + public CustomResponse kakaoOauth(@RequestBody KakaoCodeDto kakaoCodeDto) { KakaoOauthResponseDto kakaoOauthResponseDto = kakaoOAuthService.processOauth(kakaoCodeDto.getCode()); return new DataResponse<>(kakaoOauthResponseDto); } + @PostMapping("/sign-up") + public CustomResponse signUp(@RequestBody SignUpRequestDto signUpRequestDto) { + boolean status = authService.signUp(signUpRequestDto); + return new StatusResponse(status); + } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoOauthResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/KakaoOauthResponseDto.java deleted file mode 100644 index f24255e85..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/KakaoOauthResponseDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.solidconnection.auth.dto; - -public class KakaoOauthResponseDto { -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java index 55c02509e..16d0cf162 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java @@ -1,5 +1,6 @@ package com.example.solidconnection.auth.dto; +import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponseDto; import lombok.*; @Getter diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java new file mode 100644 index 000000000..3bf9f72be --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.auth.dto; + +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SignUpRequestDto { + private String kakaoOauthToken; + private List interestedRegions; + private List interestedCountries; + private PreparationStatus preparationStatus; + private String nickname; + private String profileImageUrl; + private Gender gender; + private String birth; +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/FirstAccessResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponseDto.java similarity index 94% rename from src/main/java/com/example/solidconnection/auth/dto/FirstAccessResponseDto.java rename to src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponseDto.java index a28522320..531d3e77f 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/FirstAccessResponseDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponseDto.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.dto; +package com.example.solidconnection.auth.dto.kakao; import lombok.*; diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoAccount.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoAccount.java similarity index 89% rename from src/main/java/com/example/solidconnection/auth/dto/KakaoAccount.java rename to src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoAccount.java index 47e07d23d..3c1791516 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/KakaoAccount.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoAccount.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.dto; +package com.example.solidconnection.auth.dto.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoCodeDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeDto.java similarity index 78% rename from src/main/java/com/example/solidconnection/auth/dto/KakaoCodeDto.java rename to src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeDto.java index 625c99969..3c55e728d 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/KakaoCodeDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeDto.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.dto; +package com.example.solidconnection.auth.dto.kakao; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponseDto.java new file mode 100644 index 000000000..57f4dd3da --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponseDto.java @@ -0,0 +1,4 @@ +package com.example.solidconnection.auth.dto.kakao; + +public class KakaoOauthResponseDto { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoProfile.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoProfile.java similarity index 89% rename from src/main/java/com/example/solidconnection/auth/dto/KakaoProfile.java rename to src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoProfile.java index 323dbb798..4f8dcf2c5 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/KakaoProfile.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoProfile.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.dto; +package com.example.solidconnection.auth.dto.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java similarity index 90% rename from src/main/java/com/example/solidconnection/auth/dto/KakaoTokenDto.java rename to src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java index f51d9decb..5a4b65704 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/KakaoTokenDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.dto; +package com.example.solidconnection.auth.dto.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/example/solidconnection/auth/dto/KakaoUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java similarity index 89% rename from src/main/java/com/example/solidconnection/auth/dto/KakaoUserInfoDto.java rename to src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java index c2cf1d982..88f25eaf7 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/KakaoUserInfoDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.dto; +package com.example.solidconnection.auth.dto.kakao; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java new file mode 100644 index 000000000..fa76f7602 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -0,0 +1,117 @@ +package com.example.solidconnection.auth.service; + + +import com.example.solidconnection.auth.dto.SignUpRequestDto; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenValidator; +import com.example.solidconnection.country.CountryRepository; +import com.example.solidconnection.country.InterestedCountyRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.*; +import com.example.solidconnection.region.InterestedRegionRepository; +import com.example.solidconnection.region.RegionRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.CountryCode; +import com.example.solidconnection.type.RegionCode; +import com.example.solidconnection.type.Role; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.stream.Collectors; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final TokenValidator tokenValidator; + private final TokenService tokenService; + private final SiteUserRepository siteUserRepository; + private final RegionRepository regionRepository; + private final InterestedRegionRepository interestedRegionRepository; + private final CountryRepository countryRepository; + private final InterestedCountyRepository interestedCountyRepository; + + public boolean signUp(SignUpRequestDto signUpRequestDto) { + tokenValidator.validateKakaoToken(signUpRequestDto.getKakaoOauthToken()); + validateUserNotDuplicated(signUpRequestDto); + validateNicknameDuplicated(signUpRequestDto.getNickname()); + validateBirthFormat(signUpRequestDto.getBirth()); + + SiteUser siteUser = makeSiteUserEntity(signUpRequestDto); + SiteUser savedSiteUser = siteUserRepository.save(siteUser); + + saveInterestedRegion(signUpRequestDto, savedSiteUser); + saveInterestedCountry(signUpRequestDto, savedSiteUser); + return true; + } + + private void validateUserNotDuplicated(SignUpRequestDto signUpRequestDto){ + String email = tokenService.getEmail(signUpRequestDto.getKakaoOauthToken()); + if(siteUserRepository.existsByEmail(email)){ + throw new CustomException(USER_ALREADY_EXISTED); + } + } + + private void validateNicknameDuplicated(String nickname){ + if(siteUserRepository.existsByNickname(nickname)){ + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + + private void validateBirthFormat(String birthInput) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + try { + LocalDate.parse(birthInput, formatter); + } catch (DateTimeParseException e) { + throw new CustomException(INVALID_BIRTH_FORMAT); + } + } + + private SiteUser makeSiteUserEntity(SignUpRequestDto signUpRequestDto) { + return SiteUser.builder() + .email(tokenService.getEmail(signUpRequestDto.getKakaoOauthToken())) + .nickname(signUpRequestDto.getNickname()) + .preparationStage(signUpRequestDto.getPreparationStatus()) + .profileImageUrl(signUpRequestDto.getProfileImageUrl()) + .gender(signUpRequestDto.getGender()) + .birth(signUpRequestDto.getBirth()) + .role(Role.MENTEE) + .build(); + } + + private void saveInterestedCountry(SignUpRequestDto signUpRequestDto, SiteUser savedSiteUser) { + List interestedCountries = signUpRequestDto.getInterestedCountries().stream() + .map(CountryCode::getCountryCodeByKoreanName) + .map(countryCode -> { + Country country = countryRepository.findByCountryCode(countryCode) + .orElseThrow(() -> new RuntimeException("Country Code enum이랑 table이랑 다름 : " + countryCode.name())); + return InterestedCountry.builder() + .siteUser(savedSiteUser) + .country(country) + .build(); + }) + .collect(Collectors.toList()); + interestedCountyRepository.saveAll(interestedCountries); + } + + private void saveInterestedRegion(SignUpRequestDto signUpRequestDto, SiteUser savedSiteUser) { + List interestedRegions = signUpRequestDto.getInterestedRegions().stream() + .map(RegionCode::getRegionCodeByKoreanName) + .map(regionCode -> { + Region region = regionRepository.findByRegionCode(regionCode) + .orElseThrow(() -> new RuntimeException("Region Code enum이랑 table이랑 다름 : " + regionCode.name())); + return InterestedRegion.builder() + .siteUser(savedSiteUser) + .region(region) + .build(); + }) + .collect(Collectors.toList()); + interestedRegionRepository.saveAll(interestedRegions); + } + +} diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java index 26565d28e..08d681a6c 100644 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -1,6 +1,7 @@ package com.example.solidconnection.auth.service; -import com.example.solidconnection.auth.dto.*; +import com.example.solidconnection.auth.dto.SignInResponseDto; +import com.example.solidconnection.auth.dto.kakao.*; import com.example.solidconnection.config.token.TokenService; import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.custom.exception.CustomException; @@ -56,7 +57,7 @@ private String getKakaoAccessToken(String code) { ); return Objects.requireNonNull(response.getBody()).getAccessToken(); } catch (Exception e){ - throw new CustomException(KAKAO_AUTH_CODE_FAIL); + throw new CustomException(INVALID_KAKAO_AUTH_CODE); } } @@ -93,8 +94,9 @@ private KakaoUserInfoDto getKakaoUserInfo(String accessToken) { } private SignInResponseDto kakaoSignIn(String email) { - var accessToken = tokenService.generateToken(email, TokenType.ACCESS); - var refreshToken = tokenService.saveToken(email, TokenType.REFRESH); + String accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); return SignInResponseDto.builder() .registered(true) .accessToken(accessToken) diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index a7c0d94d4..2bad197e7 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -1,6 +1,5 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -23,7 +22,7 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(new CustomException(AUTHENTICATION_FAILED, authException.getMessage())); + ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); diff --git a/src/main/java/com/example/solidconnection/config/token/TokenService.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java index 6de984a76..58548f94d 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenService.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenService.java @@ -29,10 +29,8 @@ public class TokenService { public String generateToken(String email, TokenType tokenType) { Claims claims = Jwts.claims().setSubject(email); - - var now = new Date(); - var expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); - + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) @@ -41,16 +39,13 @@ public String generateToken(String email, TokenType tokenType) { .compact(); } - public String saveToken(String email, TokenType tokenType) { - String token = generateToken(email, tokenType); - + public void saveToken(String token, TokenType tokenType) { redisTemplate.opsForValue().set( - tokenType.getPrefix() + email, + tokenType.getPrefix() + getClaim(token).getSubject(), token, tokenType.getExpireTime(), TimeUnit.MILLISECONDS ); - return token; } public Authentication getAuthentication(String token) { @@ -60,6 +55,10 @@ public Authentication getAuthentication(String token) { return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } + public String getEmail(String token) { + return getClaim(token).getSubject(); + } + private Claims getClaim(String token) { return Jwts.parser() .setSigningKey(this.secretKey) diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java index 534661af8..99d4bf3fd 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java @@ -10,6 +10,7 @@ import org.springframework.util.StringUtils; import java.util.Date; +import java.util.Objects; import static com.example.solidconnection.custom.exception.ErrorCode.*; @@ -23,7 +24,7 @@ public class TokenValidator { public void validateAccessToken(String token) { validateTokenNotEmpty(token); - validateAccessTokenNotExpired(token); + validateTokenNotExpired(token, TokenType.ACCESS); validateRefreshToken(token); // TODO : validateNotLogOut 함수 생성 및 추가 } @@ -35,11 +36,29 @@ private void validateRefreshToken(String token) { } } - private void validateAccessTokenNotExpired(String token) { + public void validateKakaoToken(String token) { + validateTokenNotEmpty(token); + validateTokenNotExpired(token, TokenType.KAKAO_OAUTH); + validateKakaoTokenNotUsed(token); + } + + private void validateKakaoTokenNotUsed(String token) { + String email = getClaim(token).getSubject(); + if (Objects.equals(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.getPrefix() + email), token)) { + throw new CustomException(INVALID_KAKAO_TOKEN); + } + } + + private void validateTokenNotExpired(String token, TokenType tokenType) { Date expiration = getClaim(token).getExpiration(); long now = new Date().getTime(); - if((expiration.getTime() - now) < 0){ - throw new CustomException(ACCESS_TOKEN_EXPIRED); + if ((expiration.getTime() - now) < 0) { + if (tokenType.equals(TokenType.ACCESS)) { + throw new CustomException(ACCESS_TOKEN_EXPIRED); + } + if (token.equals(TokenType.KAKAO_OAUTH)) { + throw new CustomException(INVALID_KAKAO_TOKEN); + } } } diff --git a/src/main/java/com/example/solidconnection/country/CountryRepository.java b/src/main/java/com/example/solidconnection/country/CountryRepository.java new file mode 100644 index 000000000..53657de3b --- /dev/null +++ b/src/main/java/com/example/solidconnection/country/CountryRepository.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.country; + +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.type.CountryCode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CountryRepository extends JpaRepository { + Optional findByCountryCode(CountryCode countryCode); +} diff --git a/src/main/java/com/example/solidconnection/country/InterestedCountyRepository.java b/src/main/java/com/example/solidconnection/country/InterestedCountyRepository.java new file mode 100644 index 000000000..907b304a1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/country/InterestedCountyRepository.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.country; + +import com.example.solidconnection.entity.InterestedCountry; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface InterestedCountyRepository extends JpaRepository { + List findAllBySiteUser_Email(String email); +} diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java index 08122faf5..209d3aa7e 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java @@ -8,13 +8,12 @@ public class CustomException extends RuntimeException { private final String message; public CustomException(ErrorCode errorCode){ - super(errorCode.getMessage()); code = errorCode.getCode(); message = errorCode.getMessage(); } public CustomException(ErrorCode errorCode, String detail){ code = errorCode.getCode(); - message = errorCode.getMessage() + detail; + message = errorCode.getMessage() + " : " + detail; } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java index e30d79134..29a5e0a3a 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java @@ -1,12 +1,15 @@ package com.example.solidconnection.custom.exception; import com.example.solidconnection.custom.response.ErrorResponse; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import static com.example.solidconnection.custom.exception.ErrorCode.JSON_PARSING_FAILED; + @Slf4j @ControllerAdvice public class CustomExceptionHandler { @@ -16,4 +19,11 @@ protected ResponseEntity handleCustomException(CustomException e) ErrorResponse errorResponse = new ErrorResponse(e); return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(e.getCode())); } -} + + @ExceptionHandler(InvalidFormatException.class) + public ResponseEntity handleInvalidFormatException(InvalidFormatException ex) { + String errorMessage = ex.getValue() + " 은(는) 유효하지 않은 값입니다."; + ErrorResponse errorResponse = new ErrorResponse(JSON_PARSING_FAILED, errorMessage); + return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(JSON_PARSING_FAILED.getCode())); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 8640313ee..6b14c997e 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,12 +7,19 @@ @Getter @AllArgsConstructor public enum ErrorCode { + USER_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 회원입니다."), + JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱 에러"), + INVALID_REGION_NAME(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 지역명입니다."), + INVALID_COUNTRY_NAME(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 국가명입니다."), + INVALID_BIRTH_FORMAT(HttpStatus.BAD_REQUEST.value(), "잘못된 생년월일 형식입니다."), + NICKNAME_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 닉네임입니다."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), - KAKAO_AUTH_CODE_FAIL(HttpStatus.BAD_REQUEST.value(),"잘못된 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), + INVALID_KAKAO_AUTH_CODE(HttpStatus.BAD_REQUEST.value(),"사용할 수 없는 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 사용자 정보 조회에 실패했습니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), + INVALID_KAKAO_TOKEN(HttpStatus.UNAUTHORIZED.value(),"사용할 수 없는 카카오 로그인 토큰입니다."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), ; diff --git a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java index 7a2d6aa19..5ad96a938 100644 --- a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java +++ b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java @@ -1,6 +1,7 @@ package com.example.solidconnection.custom.response; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -21,4 +22,9 @@ private static class ErrorDetail { public ErrorResponse(CustomException e) { this.error = new ErrorDetail(e.getCode(), e.getMessage()); } + + public ErrorResponse(ErrorCode e, String detail){ + String detailedMessage = e.getMessage() + " : " + detail; + this.error = new ErrorDetail(e.getCode(), detailedMessage); + } } diff --git a/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java b/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java index b9cd7684f..6a1e5d453 100644 --- a/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java +++ b/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java @@ -4,9 +4,9 @@ @Getter public class StatusResponse extends CustomResponse { - private final Boolean status; + private final boolean status; - StatusResponse(Boolean status) { + public StatusResponse(boolean status) { this.status = status; } } diff --git a/src/main/java/com/example/solidconnection/entity/Country.java b/src/main/java/com/example/solidconnection/entity/Country.java index 0a7d106c4..b6f5cb0ae 100644 --- a/src/main/java/com/example/solidconnection/entity/Country.java +++ b/src/main/java/com/example/solidconnection/entity/Country.java @@ -12,12 +12,9 @@ public class Country { @Enumerated(EnumType.STRING) private CountryCode countryCode; - @Column(nullable = false, length = 100) - private String name; - // 연관 관계 @ManyToOne - @JoinColumn(name = "region_id") + @JoinColumn(name = "region_code") private Region region; @OneToMany(mappedBy = "country") diff --git a/src/main/java/com/example/solidconnection/entity/InterestedCountry.java b/src/main/java/com/example/solidconnection/entity/InterestedCountry.java index 3e2c16329..baadab398 100644 --- a/src/main/java/com/example/solidconnection/entity/InterestedCountry.java +++ b/src/main/java/com/example/solidconnection/entity/InterestedCountry.java @@ -1,8 +1,16 @@ package com.example.solidconnection.entity; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter public class InterestedCountry { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -14,10 +22,6 @@ public class InterestedCountry { private SiteUser siteUser; @ManyToOne - @JoinColumn(name = "country_id") + @JoinColumn(name = "country_code") private Country country; - - @ManyToOne - @JoinColumn(name = "region_id") - private Region region; } diff --git a/src/main/java/com/example/solidconnection/entity/InterestedRegion.java b/src/main/java/com/example/solidconnection/entity/InterestedRegion.java index cdf6758b9..8bc83ee15 100644 --- a/src/main/java/com/example/solidconnection/entity/InterestedRegion.java +++ b/src/main/java/com/example/solidconnection/entity/InterestedRegion.java @@ -1,8 +1,16 @@ package com.example.solidconnection.entity; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter public class InterestedRegion { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -14,6 +22,6 @@ public class InterestedRegion { private SiteUser siteUser; @ManyToOne - @JoinColumn(name = "region_id") + @JoinColumn(name = "region_code") private Region region; } diff --git a/src/main/java/com/example/solidconnection/entity/Region.java b/src/main/java/com/example/solidconnection/entity/Region.java index 7be6bd3c9..6c9f0557f 100644 --- a/src/main/java/com/example/solidconnection/entity/Region.java +++ b/src/main/java/com/example/solidconnection/entity/Region.java @@ -10,7 +10,7 @@ public class Region { @Id @Column(length = 10) @Enumerated(EnumType.STRING) - private RegionCode id; + private RegionCode regionCode; // 연관 관계 @OneToMany(mappedBy = "region") @@ -19,9 +19,6 @@ public class Region { @OneToMany(mappedBy = "region") private Set interestedRegions; - @OneToMany(mappedBy = "region") - private Set interestedCountries; - @OneToMany(mappedBy = "region") private Set universities; } diff --git a/src/main/java/com/example/solidconnection/entity/SiteUser.java b/src/main/java/com/example/solidconnection/entity/SiteUser.java index eb7cdaa10..485e97ffa 100644 --- a/src/main/java/com/example/solidconnection/entity/SiteUser.java +++ b/src/main/java/com/example/solidconnection/entity/SiteUser.java @@ -1,13 +1,22 @@ package com.example.solidconnection.entity; +import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.Set; @Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter public class SiteUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -19,21 +28,28 @@ public class SiteUser { @Column(nullable = false, length = 100) private String nickname; - @Column(nullable = false, length = 50) - @Enumerated(EnumType.STRING) - private PreparationStatus preparationStage; - @Column(length = 500) private String profileImageUrl; - private LocalDateTime nicknameModifiedAt; + @Column(nullable = false, length = 20) + private String birth; - private LocalDateTime quitedAt; + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private PreparationStatus preparationStage; @Column(nullable = false, length = 50) @Enumerated(EnumType.STRING) private Role role; + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) + private Gender gender; + + private LocalDateTime nicknameModifiedAt; + + private LocalDateTime quitedAt; + // 연관관계 @OneToMany(mappedBy = "siteUser") private Set interestedRegions; @@ -46,4 +62,5 @@ public class SiteUser { @OneToMany(mappedBy = "siteUser") private Set wishUniversities; + } diff --git a/src/main/java/com/example/solidconnection/entity/University.java b/src/main/java/com/example/solidconnection/entity/University.java index 2998087b7..90f940cb6 100644 --- a/src/main/java/com/example/solidconnection/entity/University.java +++ b/src/main/java/com/example/solidconnection/entity/University.java @@ -64,11 +64,11 @@ public class University { // 연관 관계 @ManyToOne - @JoinColumn(name = "country_id") + @JoinColumn(name = "country_code") private Country country; @ManyToOne - @JoinColumn(name = "region_id") + @JoinColumn(name = "region_code") private Region region; @OneToMany(mappedBy = "university") diff --git a/src/main/java/com/example/solidconnection/region/InterestedRegionRepository.java b/src/main/java/com/example/solidconnection/region/InterestedRegionRepository.java new file mode 100644 index 000000000..b7e3bb4ec --- /dev/null +++ b/src/main/java/com/example/solidconnection/region/InterestedRegionRepository.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.region; + +import com.example.solidconnection.entity.InterestedRegion; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface InterestedRegionRepository extends JpaRepository { + List findAllBySiteUser_Email(String email); +} diff --git a/src/main/java/com/example/solidconnection/region/RegionRepository.java b/src/main/java/com/example/solidconnection/region/RegionRepository.java new file mode 100644 index 000000000..2c99b0e08 --- /dev/null +++ b/src/main/java/com/example/solidconnection/region/RegionRepository.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.region; + +import com.example.solidconnection.entity.Region; +import com.example.solidconnection.type.RegionCode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RegionRepository extends JpaRepository { + Optional findByRegionCode(RegionCode regionCode); +} diff --git a/src/main/java/com/example/solidconnection/type/CountryCode.java b/src/main/java/com/example/solidconnection/type/CountryCode.java index d0988bf18..abcd67407 100644 --- a/src/main/java/com/example/solidconnection/type/CountryCode.java +++ b/src/main/java/com/example/solidconnection/type/CountryCode.java @@ -1,33 +1,40 @@ package com.example.solidconnection.type; +import com.example.solidconnection.custom.exception.CustomException; + +import java.util.Arrays; +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COUNTRY_NAME; + public enum CountryCode { - BRUNEI("브루나이"), - SINGAPORE("싱가포르"), - AZERBAIJAN("아제르바이잔"), - INDONESIA("인도네시아"), - JAPAN("일본"), - TURKEY("튀르키예"), - HONG_KONG("홍콩"), - UNITED_STATES("미국"), - CANADA("캐나다"), - AUSTRALIA("호주"), - BRAZIL("브라질"), - NETHERLANDS("네덜란드"), - NORWAY("노르웨이"), - DENMARK("덴마크"), - GERMANY("독일"), - SWEDEN("스웨덴"), - SWITZERLAND("스위스"), - SPAIN("스페인"), - UNITED_KINGDOM("영국"), - AUSTRIA("오스트리아"), - ITALY("이탈리아"), - CZECH_REPUBLIC("체코"), - PORTUGAL("포르투갈"), - FRANCE("프랑스"), - FINLAND("핀란드"), - CHINA("중국"), - TAIWAN("대만"); + BN("브루나이"), + SG("싱가포르"), + AZ("아제르바이잔"), + ID("인도네시아"), + JP("일본"), + TR("튀르키예"), + HK("홍콩"), + US("미국"), + CA("캐나다"), + AU("호주"), + BR("브라질"), + NL("네덜란드"), + NO("노르웨이"), + DK("덴마크"), + DE("독일"), + SE("스웨덴"), + CH("스위스"), + ES("스페인"), + GB("영국"), + AT("오스트리아"), + IT("이탈리아"), + CZ("체코"), + PT("포르투갈"), + FR("프랑스"), + FI("핀란드"), + CN("중국"), + TW("대만"); private final String koreanName; @@ -36,12 +43,10 @@ public enum CountryCode { } public static CountryCode getCountryCodeByKoreanName(String koreanName) { - for (CountryCode countryCode : CountryCode.values()) { - if (countryCode.getKoreanName().equals(koreanName)) { - return countryCode; - } - } - throw new IllegalArgumentException("No country found with Korean name: " + koreanName); //TODO: 에러 타입 정리 필요 + Optional matchingCountryCode = Arrays.stream(CountryCode.values()) + .filter(countryCode -> countryCode.getKoreanName().equals(koreanName)) + .findFirst(); + return matchingCountryCode.orElseThrow(() -> new CustomException(INVALID_COUNTRY_NAME, koreanName)); } public String getKoreanName() { diff --git a/src/main/java/com/example/solidconnection/type/Gender.java b/src/main/java/com/example/solidconnection/type/Gender.java new file mode 100644 index 000000000..92a78814b --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/Gender.java @@ -0,0 +1,5 @@ +package com.example.solidconnection.type; + +public enum Gender { + MALE, FEMALE, PREFER_NOT_TO_SAY +} diff --git a/src/main/java/com/example/solidconnection/type/RegionCode.java b/src/main/java/com/example/solidconnection/type/RegionCode.java index db3e533fa..6a37ec6c0 100644 --- a/src/main/java/com/example/solidconnection/type/RegionCode.java +++ b/src/main/java/com/example/solidconnection/type/RegionCode.java @@ -1,7 +1,32 @@ package com.example.solidconnection.type; +import com.example.solidconnection.custom.exception.CustomException; + +import java.util.Arrays; +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_REGION_NAME; + public enum RegionCode { - EUROPE, - AMERICAS, - ASIA + ASIA("아시아권"), + AMERICAS("미주권"), + CHINA("중국권"), + EUROPE("유럽권"); + + private final String koreanName; + + RegionCode(String koreanName) { + this.koreanName = koreanName; + } + + public static RegionCode getRegionCodeByKoreanName(String koreanName) { + Optional matchingRegionCode = Arrays.stream(RegionCode.values()) + .filter(regionCode -> regionCode.getKoreanName().equals(koreanName)) + .findFirst(); + return matchingRegionCode.orElseThrow(() -> new CustomException(INVALID_REGION_NAME, koreanName)); + } + + public String getKoreanName() { + return koreanName; + } } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..c7a93c878 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,34 @@ +INSERT INTO region (region_code) VALUES + ('ASIA'), + ('AMERICAS'), + ('CHINA'), + ('EUROPE'); + +INSERT INTO country (country_code, region_code) VALUES + ('BN', 'ASIA'), + ('SG', 'ASIA'), + ('AZ', 'ASIA'), + ('ID', 'ASIA'), + ('JP', 'ASIA'), + ('TR', 'EUROPE'), + ('HK', 'ASIA'), + ('US', 'AMERICAS'), + ('CA', 'AMERICAS'), + ('AU', 'AMERICAS'), + ('BR', 'AMERICAS'), + ('NL', 'EUROPE'), + ('NO', 'EUROPE'), + ('DK', 'EUROPE'), + ('DE', 'EUROPE'), + ('SE', 'EUROPE'), + ('CH', 'EUROPE'), + ('ES', 'EUROPE'), + ('GB', 'EUROPE'), + ('AT', 'EUROPE'), + ('IT', 'EUROPE'), + ('CZ', 'EUROPE'), + ('PT', 'EUROPE'), + ('FR', 'EUROPE'), + ('FI', 'EUROPE'), + ('CN', 'CHINA'), + ('TW', 'CHINA'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 703ae55b9..da64823a7 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,113 +1,118 @@ --- Country -CREATE TABLE country ( - country_code VARCHAR(255) PRIMARY KEY, - name VARCHAR(255) NOT NULL, - region_code VARCHAR(255), - FOREIGN KEY (region_code) REFERENCES region(region_code) +DROP TABLE IF EXISTS `application`, `country`, `gpa_requirement`, `interested_country`, `interested_region`, `language_requirement`, `region`, `site_user`, `university`, `wish_university`; + +CREATE TABLE `application` ( + `gpa` float NOT NULL, + `first_choice_univ_id` bigint DEFAULT NULL, + `id` bigint NOT NULL AUTO_INCREMENT, + `second_choice_univ_id` bigint DEFAULT NULL, + `site_user_id` bigint DEFAULT NULL, + `language_test_type` varchar(255) NOT NULL, + `verify_status` varchar(50) NOT NULL, + `gpa_report_url` varchar(500) NOT NULL, + `language_test_report_url` varchar(500) NOT NULL, + `language_test_score` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `FK4xffa66ucb9651me7uc8ek71c` (`first_choice_univ_id`), + KEY `FK401wya8j2e7jfrx7hu6gcc4fx` (`second_choice_univ_id`), + KEY `FKs4s3hebtn7vwd0b4xt8msxsis` (`site_user_id`), + CONSTRAINT `FK401wya8j2e7jfrx7hu6gcc4fx` FOREIGN KEY (`second_choice_univ_id`) REFERENCES `university` (`id`), + CONSTRAINT `FK4xffa66ucb9651me7uc8ek71c` FOREIGN KEY (`first_choice_univ_id`) REFERENCES `university` (`id`), + CONSTRAINT `FKs4s3hebtn7vwd0b4xt8msxsis` FOREIGN KEY (`site_user_id`) REFERENCES `site_user` (`id`) ); --- GpaRequirement -CREATE TABLE gpa_requirement ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - scale VARCHAR(255) NOT NULL, - min_gpa FLOAT NOT NULL, - university_id BIGINT, - FOREIGN KEY (university_id) REFERENCES university(id) +CREATE TABLE `country` ( + `country_code` varchar(255) NOT NULL, + `region_code` varchar(10) DEFAULT NULL, + PRIMARY KEY (`country_code`) ); --- InterestedCountry -CREATE TABLE interested_country ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - site_user_id BIGINT, - country_code VARCHAR(255), - region_code VARCHAR(255), - FOREIGN KEY (site_user_id) REFERENCES site_user(id), - FOREIGN KEY (country_code) REFERENCES country(country_code), - FOREIGN KEY (region_code) REFERENCES region(region_code) +CREATE TABLE `gpa_requirement` ( + `min_gpa` float NOT NULL, + `scale` varchar(5) NOT NULL, + `id` bigint NOT NULL AUTO_INCREMENT, + `university_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FK74nj7od0mj9e63ervpl0wmy4q` (`university_id`), + CONSTRAINT `FK74nj7od0mj9e63ervpl0wmy4q` FOREIGN KEY (`university_id`) REFERENCES `university` (`id`) ); --- Region -CREATE TABLE region ( - region_code VARCHAR(255) PRIMARY KEY +CREATE TABLE `interested_country` ( + `country_code` varchar(2) DEFAULT NULL, + `id` bigint NOT NULL AUTO_INCREMENT, + `site_user_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FK26u5am55jefclcd7r5smk8ai7` (`site_user_id`), + CONSTRAINT `FK26u5am55jefclcd7r5smk8ai7` FOREIGN KEY (`site_user_id`) REFERENCES `site_user` (`id`) ); --- University -CREATE TABLE university ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - korean_name VARCHAR(255) NOT NULL, - english_name VARCHAR(255) NOT NULL, - internal_name VARCHAR(255) NOT NULL, - recruit_number INT, - tuition_fee_payment_type VARCHAR(255), -- Enum, adjust as needed - exchange_semester VARCHAR(255), -- Enum, adjust as needed - details_for_language TEXT, - details_for_apply TEXT, - details_for_major TEXT, - details_for_accommodation TEXT, - homepage_url VARCHAR(255), - english_course_url VARCHAR(255), - accommodation_url VARCHAR(255), - details TEXT, - logo_image_url VARCHAR(255), - background_image_url VARCHAR(255), - country_code VARCHAR(255), - region_code VARCHAR(255), - FOREIGN KEY (country_code) REFERENCES country(country_code), - FOREIGN KEY (region_code) REFERENCES region(region_code) +CREATE TABLE `interested_region` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `site_user_id` bigint DEFAULT NULL, + `region_code` varchar(10) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FKia6h0pbisqhgm3lkeya6vqo4w` (`site_user_id`), + CONSTRAINT `FKia6h0pbisqhgm3lkeya6vqo4w` FOREIGN KEY (`site_user_id`) REFERENCES `site_user` (`id`) ); --- WishUniversity -CREATE TABLE wish_university ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - university_id BIGINT, - site_user_id BIGINT, - FOREIGN KEY (university_id) REFERENCES university(id), - FOREIGN KEY (site_user_id) REFERENCES site_user(id) +CREATE TABLE `language_requirement` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `university_id` bigint DEFAULT NULL, + `language_test_type` varchar(255) NOT NULL, + `min_score` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `FKp723kfidkuu8kus5svxnqq5hw` (`university_id`), + CONSTRAINT `FKp723kfidkuu8kus5svxnqq5hw` FOREIGN KEY (`university_id`) REFERENCES `university` (`id`) ); --- InterestedRegion -CREATE TABLE interested_region ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - site_user_id BIGINT, - region_code VARCHAR(255), - FOREIGN KEY (site_user_id) REFERENCES site_user(id), - FOREIGN KEY (region_code) REFERENCES region(region_code) +CREATE TABLE `region` ( + `region_code` varchar(255) NOT NULL, + PRIMARY KEY (`region_code`) ); --- Application -CREATE TABLE application ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - language_test_type VARCHAR(255), -- Enum, adjust as needed - language_test_score VARCHAR(255), - language_test_report_url VARCHAR(255), - gpa FLOAT NOT NULL, - gpa_report_url VARCHAR(255), - verify_status VARCHAR(255), - first_choice_university_id BIGINT, - second_choice_university_id BIGINT, - site_user_id BIGINT, - FOREIGN KEY (first_choice_university_id) REFERENCES university(id), - FOREIGN KEY (second_choice_university_id) REFERENCES university(id), - FOREIGN KEY (site_user_id) REFERENCES site_user(id) +CREATE TABLE `site_user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `nickname_modified_at` datetime(6) DEFAULT NULL, + `quited_at` datetime(6) DEFAULT NULL, + `birth` varchar(20) NOT NULL, + `gender` varchar(255) NOT NULL, + `preparation_stage` varchar(255) NOT NULL, + `role` varchar(255) NOT NULL, + `email` varchar(100) NOT NULL, + `nickname` varchar(100) NOT NULL, + `profile_image_url` varchar(500) DEFAULT NULL, + PRIMARY KEY (`id`) ); --- LanguageRequirement -CREATE TABLE language_requirement ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - language_test_type VARCHAR(255), -- Enum, adjust as needed - min_score VARCHAR(255), - university_id BIGINT, - FOREIGN KEY (university_id) REFERENCES university(id) +CREATE TABLE `university` ( + `country_code` varchar(2) DEFAULT NULL, + `recruit_number` int NOT NULL, + `id` bigint NOT NULL AUTO_INCREMENT, + `region_code` varchar(10) DEFAULT NULL, + `exchange_semester` varchar(255) NOT NULL, + `tuition_fee_payment_type` varchar(255) NOT NULL, + `english_name` varchar(100) NOT NULL, + `internal_name` varchar(100) NOT NULL, + `korean_name` varchar(100) NOT NULL, + `accommodation_url` varchar(500) DEFAULT NULL, + `background_image_url` varchar(500) NOT NULL, + `details` varchar(500) DEFAULT NULL, + `english_course_url` varchar(500) DEFAULT NULL, + `homepage_url` varchar(500) DEFAULT NULL, + `logo_image_url` varchar(500) NOT NULL, + `details_for_accommodation` varchar(1000) DEFAULT NULL, + `details_for_apply` varchar(1000) DEFAULT NULL, + `details_for_language` varchar(1000) DEFAULT NULL, + `details_for_major` varchar(1000) DEFAULT NULL, + PRIMARY KEY (`id`) ); --- SiteUser -CREATE TABLE site_user ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - email VARCHAR(255) NOT NULL UNIQUE, - nickname VARCHAR(255) NOT NULL UNIQUE, - preparation_stage VARCHAR(255), -- Enum, adjust as needed - profile_image_url VARCHAR(255), - nickname_modified_at TIMESTAMP, - quited_at TIMESTAMP, - role VARCHAR(255) -- Enum, adjust as needed +CREATE TABLE `wish_university` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `site_user_id` bigint DEFAULT NULL, + `university_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FKrrhud921brslcukx6fyuh0th3` (`site_user_id`), + KEY `FKhj3gn3mqmfeiiw9jt83g7t3rk` (`university_id`), + CONSTRAINT `FKhj3gn3mqmfeiiw9jt83g7t3rk` FOREIGN KEY (`university_id`) REFERENCES `university` (`id`), + CONSTRAINT `FKrrhud921brslcukx6fyuh0th3` FOREIGN KEY (`site_user_id`) REFERENCES `site_user` (`id`) ); From 75fbd5feddf5506f668e67024df23ed8dd25d1c9 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 28 Jan 2024 06:23:37 +0900 Subject: [PATCH 020/158] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 8 +++ .../auth/service/AuthService.java | 16 +++++- .../security/JwtAuthenticationEntryPoint.java | 12 ++++- .../security/JwtAuthenticationFilter.java | 3 ++ .../config/token/TokenService.java | 8 ++- .../config/token/TokenValidator.java | 11 +++- .../custom/exception/ErrorCode.java | 3 +- .../custom/userdetails/CustomUserDetails.java | 54 +++++++++++++++++++ .../userdetails/CustomUserDetailsService.java | 24 +++++++++ 9 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java create mode 100644 src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 15bc21377..ce79d85f1 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.security.Principal; + @RestController @RequestMapping("auth") @RequiredArgsConstructor @@ -32,4 +34,10 @@ public CustomResponse signUp(@RequestBody SignUpRequestDto signUpRequestDto) { boolean status = authService.signUp(signUpRequestDto); return new StatusResponse(status); } + + @PostMapping("/sign-out") + public CustomResponse signOut(Principal principal) { + boolean status = authService.signOut(principal.getName()); + return new StatusResponse(status); + } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index fa76f7602..6c50de055 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -3,6 +3,7 @@ import com.example.solidconnection.auth.dto.SignUpRequestDto; import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.config.token.TokenValidator; import com.example.solidconnection.country.CountryRepository; import com.example.solidconnection.country.InterestedCountyRepository; @@ -15,12 +16,14 @@ import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.type.Role; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.example.solidconnection.custom.exception.ErrorCode.*; @@ -28,6 +31,8 @@ @Service @RequiredArgsConstructor public class AuthService { + + private final RedisTemplate redisTemplate; private final TokenValidator tokenValidator; private final TokenService tokenService; private final SiteUserRepository siteUserRepository; @@ -50,6 +55,16 @@ public boolean signUp(SignUpRequestDto signUpRequestDto) { return true; } + public boolean signOut(String email){ + redisTemplate.opsForValue().set( + TokenType.REFRESH.getPrefix() + email, + "signOut", + TokenType.REFRESH.getExpireTime(), + TimeUnit.MILLISECONDS + ); + return true; + } + private void validateUserNotDuplicated(SignUpRequestDto signUpRequestDto){ String email = tokenService.getEmail(signUpRequestDto.getKakaoOauthToken()); if(siteUserRepository.existsByEmail(email)){ @@ -113,5 +128,4 @@ private void saveInterestedRegion(SignUpRequestDto signUpRequestDto, SiteUser sa .collect(Collectors.toList()); interestedRegionRepository.saveAll(interestedRegions); } - } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 2bad197e7..7ca357158 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -21,11 +22,20 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { + AuthenticationException authException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } + + public void customCommence(HttpServletRequest request, HttpServletResponse response, + CustomException customException) throws IOException { + ErrorResponse errorResponse = new ErrorResponse(customException); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 2ea95cffe..4b37e3077 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -2,6 +2,7 @@ import com.example.solidconnection.config.token.TokenService; import com.example.solidconnection.config.token.TokenValidator; +import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -48,6 +49,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 } catch (AuthenticationException e) { jwtAuthenticationEntryPoint.commence(request, response, e); + } catch (CustomException e){ + jwtAuthenticationEntryPoint.customCommence(request, response, e); } } diff --git a/src/main/java/com/example/solidconnection/config/token/TokenService.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java index 58548f94d..31617f607 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenService.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenService.java @@ -1,6 +1,6 @@ package com.example.solidconnection.config.token; -import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.userdetails.CustomUserDetailsService; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -16,13 +16,12 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.custom.exception.ErrorCode.EMAIL_NOT_FOUND; - @Component @RequiredArgsConstructor public class TokenService { private final RedisTemplate redisTemplate; private final SiteUserRepository siteUserRepository; + private final CustomUserDetailsService customUserDetailsService; @Value("${jwt.secret}") private String secretKey; @@ -50,8 +49,7 @@ public void saveToken(String token, TokenType tokenType) { public Authentication getAuthentication(String token) { String email = getClaim(token).getSubject(); - UserDetails userDetails = (UserDetails) siteUserRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(EMAIL_NOT_FOUND)); + UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java index 99d4bf3fd..e66846196 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java @@ -25,17 +25,24 @@ public class TokenValidator { public void validateAccessToken(String token) { validateTokenNotEmpty(token); validateTokenNotExpired(token, TokenType.ACCESS); + validateNotSignOut(token); validateRefreshToken(token); - // TODO : validateNotLogOut 함수 생성 및 추가 } private void validateRefreshToken(String token) { String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email) != null) { + if (redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email) == null) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } } + private void validateNotSignOut(String token) { + String email = getClaim(token).getSubject(); + if ("signOut".equals(redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email))) { + throw new CustomException(USER_ALREADY_SIGN_OUT); + } + } + public void validateKakaoToken(String token) { validateTokenNotEmpty(token); validateTokenNotExpired(token, TokenType.KAKAO_OAUTH); diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 6b14c997e..10bd81800 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,6 +7,7 @@ @Getter @AllArgsConstructor public enum ErrorCode { + USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), USER_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 회원입니다."), JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱 에러"), INVALID_REGION_NAME(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 지역명입니다."), @@ -15,7 +16,7 @@ public enum ErrorCode { NICKNAME_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 닉네임입니다."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), - EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), INVALID_KAKAO_AUTH_CODE(HttpStatus.BAD_REQUEST.value(),"사용할 수 없는 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 사용자 정보 조회에 실패했습니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java new file mode 100644 index 000000000..53af94c94 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.custom.userdetails; + +import com.example.solidconnection.entity.SiteUser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + private final SiteUser siteUser; + + public CustomUserDetails(SiteUser siteUser) { + this.siteUser = siteUser; + } + + public String getEmail(){ + return siteUser.getEmail(); + } + + @Override + public String getUsername() { + return siteUser.getEmail(); + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } +} diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java new file mode 100644 index 000000000..51cfb78b9 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.custom.userdetails; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final SiteUserRepository siteUserRepository; + + @Override + public UserDetails loadUserByUsername(String username) { + SiteUser siteUser = siteUserRepository.findByEmail(username) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND, username)); + return new CustomUserDetails(siteUser); + } +} From 7b1fed3e569a6a7c4a4f0d80bae3643669d737d0 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 28 Jan 2024 07:27:26 +0900 Subject: [PATCH 021/158] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SolidConnectionApplication.java | 2 ++ .../auth/controller/AuthController.java | 16 +++++---- .../auth/dto/SignUpResponseDto.java | 13 +++++++ .../auth/service/AuthService.java | 34 +++++++++++++++---- .../auth/service/KakaoOAuthService.java | 9 +++++ .../solidconnection/entity/SiteUser.java | 15 ++++---- .../CountryRepository.java | 2 +- .../InterestedCountyRepository.java | 2 +- .../InterestedRegionRepository.java | 2 +- .../RegionRepository.java | 2 +- .../scheduler/UserRemovalScheduler.java | 17 ++++++++++ .../repository/SiteUserRepository.java | 7 ++++ .../siteuser/service/SiteUserService.java | 21 ++++++++++++ 13 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignUpResponseDto.java rename src/main/java/com/example/solidconnection/{country => repositories}/CountryRepository.java (88%) rename src/main/java/com/example/solidconnection/{country => repositories}/InterestedCountyRepository.java (88%) rename src/main/java/com/example/solidconnection/{region => repositories}/InterestedRegionRepository.java (88%) rename src/main/java/com/example/solidconnection/{region => repositories}/RegionRepository.java (88%) create mode 100644 src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java diff --git a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java index 1e378189c..81e27f96b 100644 --- a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java +++ b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication public class SolidConnectionApplication { diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index ce79d85f1..348f7bf15 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -1,6 +1,7 @@ package com.example.solidconnection.auth.controller; import com.example.solidconnection.auth.dto.SignUpRequestDto; +import com.example.solidconnection.auth.dto.SignUpResponseDto; import com.example.solidconnection.auth.dto.kakao.KakaoCodeDto; import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponseDto; import com.example.solidconnection.auth.service.AuthService; @@ -9,10 +10,7 @@ import com.example.solidconnection.custom.response.DataResponse; import com.example.solidconnection.custom.response.StatusResponse; 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; +import org.springframework.web.bind.annotation.*; import java.security.Principal; @@ -31,8 +29,8 @@ public CustomResponse kakaoOauth(@RequestBody KakaoCodeDto kakaoCodeDto) { @PostMapping("/sign-up") public CustomResponse signUp(@RequestBody SignUpRequestDto signUpRequestDto) { - boolean status = authService.signUp(signUpRequestDto); - return new StatusResponse(status); + SignUpResponseDto signUpResponseDto = authService.signUp(signUpRequestDto); + return new DataResponse<>(signUpResponseDto); } @PostMapping("/sign-out") @@ -40,4 +38,10 @@ public CustomResponse signOut(Principal principal) { boolean status = authService.signOut(principal.getName()); return new StatusResponse(status); } + + @PatchMapping("/quit") + public CustomResponse quit(Principal principal) { + boolean status = authService.quit(principal.getName()); + return new StatusResponse(status); + } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponseDto.java new file mode 100644 index 000000000..3db1c11f4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponseDto.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.auth.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SignUpResponseDto { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 6c50de055..d98e80d04 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -2,15 +2,16 @@ import com.example.solidconnection.auth.dto.SignUpRequestDto; +import com.example.solidconnection.auth.dto.SignUpResponseDto; import com.example.solidconnection.config.token.TokenService; import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.config.token.TokenValidator; -import com.example.solidconnection.country.CountryRepository; -import com.example.solidconnection.country.InterestedCountyRepository; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.*; -import com.example.solidconnection.region.InterestedRegionRepository; -import com.example.solidconnection.region.RegionRepository; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.CountryCode; import com.example.solidconnection.type.RegionCode; @@ -18,6 +19,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -29,6 +31,7 @@ import static com.example.solidconnection.custom.exception.ErrorCode.*; @Service +@Transactional @RequiredArgsConstructor public class AuthService { @@ -41,7 +44,7 @@ public class AuthService { private final CountryRepository countryRepository; private final InterestedCountyRepository interestedCountyRepository; - public boolean signUp(SignUpRequestDto signUpRequestDto) { + public SignUpResponseDto signUp(SignUpRequestDto signUpRequestDto) { tokenValidator.validateKakaoToken(signUpRequestDto.getKakaoOauthToken()); validateUserNotDuplicated(signUpRequestDto); validateNicknameDuplicated(signUpRequestDto.getNickname()); @@ -52,7 +55,16 @@ public boolean signUp(SignUpRequestDto signUpRequestDto) { saveInterestedRegion(signUpRequestDto, savedSiteUser); saveInterestedCountry(signUpRequestDto, savedSiteUser); - return true; + + String email = savedSiteUser.getEmail(); + String accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + + return SignUpResponseDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); } public boolean signOut(String email){ @@ -65,6 +77,16 @@ public boolean signOut(String email){ return true; } + public boolean quit(String email){ + SiteUser siteUser = getValidatedUser(email); + siteUser.setQuitedAt(LocalDate.now().plusDays(1)); + return true; + } + + private SiteUser getValidatedUser(String email){ + return siteUserRepository.findByEmail(email).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + } + private void validateUserNotDuplicated(SignUpRequestDto signUpRequestDto){ String email = tokenService.getEmail(signUpRequestDto.getKakaoOauthToken()); if(siteUserRepository.existsByEmail(email)){ diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java index 08d681a6c..4583802ba 100644 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -5,11 +5,13 @@ import com.example.solidconnection.config.token.TokenService; import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -18,6 +20,7 @@ import static com.example.solidconnection.custom.exception.ErrorCode.*; @Service +@Transactional @RequiredArgsConstructor public class KakaoOAuthService { @@ -40,6 +43,7 @@ public KakaoOauthResponseDto processOauth(String code) throws CustomException { String email = kakaoUserInfoDto.getKakaoAccount().getEmail(); boolean isAlreadyRegistered = siteUserRepository.existsByEmail(email); if (isAlreadyRegistered) { + resetQuitedAt(email); return kakaoSignIn(email); } String kakaoOauthToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); @@ -103,4 +107,9 @@ private SignInResponseDto kakaoSignIn(String email) { .refreshToken(refreshToken) .build(); } + + public void resetQuitedAt(String email){ + SiteUser siteUser = siteUserRepository.findByEmail(email).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + siteUser.setQuitedAt(null); + } } diff --git a/src/main/java/com/example/solidconnection/entity/SiteUser.java b/src/main/java/com/example/solidconnection/entity/SiteUser.java index 485e97ffa..f42d2c1b6 100644 --- a/src/main/java/com/example/solidconnection/entity/SiteUser.java +++ b/src/main/java/com/example/solidconnection/entity/SiteUser.java @@ -4,12 +4,9 @@ import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.Set; @Entity @@ -26,9 +23,11 @@ public class SiteUser { private String email; @Column(nullable = false, length = 100) + @Setter private String nickname; @Column(length = 500) + @Setter private String profileImageUrl; @Column(nullable = false, length = 20) @@ -46,9 +45,11 @@ public class SiteUser { @Enumerated(EnumType.STRING) private Gender gender; - private LocalDateTime nicknameModifiedAt; + @Setter + private LocalDate nicknameModifiedAt; - private LocalDateTime quitedAt; + @Setter + private LocalDate quitedAt; // 연관관계 @OneToMany(mappedBy = "siteUser") diff --git a/src/main/java/com/example/solidconnection/country/CountryRepository.java b/src/main/java/com/example/solidconnection/repositories/CountryRepository.java similarity index 88% rename from src/main/java/com/example/solidconnection/country/CountryRepository.java rename to src/main/java/com/example/solidconnection/repositories/CountryRepository.java index 53657de3b..a61fc08d4 100644 --- a/src/main/java/com/example/solidconnection/country/CountryRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/CountryRepository.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.country; +package com.example.solidconnection.repositories; import com.example.solidconnection.entity.Country; import com.example.solidconnection.type.CountryCode; diff --git a/src/main/java/com/example/solidconnection/country/InterestedCountyRepository.java b/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java similarity index 88% rename from src/main/java/com/example/solidconnection/country/InterestedCountyRepository.java rename to src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java index 907b304a1..fff21cf38 100644 --- a/src/main/java/com/example/solidconnection/country/InterestedCountyRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.country; +package com.example.solidconnection.repositories; import com.example.solidconnection.entity.InterestedCountry; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/example/solidconnection/region/InterestedRegionRepository.java b/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java similarity index 88% rename from src/main/java/com/example/solidconnection/region/InterestedRegionRepository.java rename to src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java index b7e3bb4ec..9b429b63c 100644 --- a/src/main/java/com/example/solidconnection/region/InterestedRegionRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.region; +package com.example.solidconnection.repositories; import com.example.solidconnection.entity.InterestedRegion; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/example/solidconnection/region/RegionRepository.java b/src/main/java/com/example/solidconnection/repositories/RegionRepository.java similarity index 88% rename from src/main/java/com/example/solidconnection/region/RegionRepository.java rename to src/main/java/com/example/solidconnection/repositories/RegionRepository.java index 2c99b0e08..907295a13 100644 --- a/src/main/java/com/example/solidconnection/region/RegionRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/RegionRepository.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.region; +package com.example.solidconnection.repositories; import com.example.solidconnection.entity.Region; import com.example.solidconnection.type.RegionCode; diff --git a/src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java b/src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java new file mode 100644 index 000000000..07c67ca2c --- /dev/null +++ b/src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.scheduler; + +import com.example.solidconnection.siteuser.service.SiteUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserRemovalScheduler { + private final SiteUserService siteUserService; + + @Scheduled(cron = "0 0 0 * * ?") // 매일 자정에 실행 + public void scheduledUserRemoval() { + siteUserService.deleteUsersNeverVisitedAfterQuited(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java index df58d5ac2..f557becb9 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java @@ -2,8 +2,12 @@ import com.example.solidconnection.entity.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; @Repository @@ -11,4 +15,7 @@ public interface SiteUserRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); boolean existsByNickname(String nickname); + + @Query("SELECT u FROM SiteUser u WHERE u.quitedAt <= :cutoffDate") + List findUsersToBeRemoved(@Param("cutoffDate") LocalDate cutoffDate); } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java new file mode 100644 index 000000000..6d700d3ff --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -0,0 +1,21 @@ +package com.example.solidconnection.siteuser.service; + +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SiteUserService { + private final SiteUserRepository siteUserRepository; + + public void deleteUsersNeverVisitedAfterQuited() { + LocalDate cutoffDate = LocalDate.now().minusDays(30); + List usersToRemove = siteUserRepository.findUsersToBeRemoved(cutoffDate); + siteUserRepository.deleteAll(usersToRemove); + } +} \ No newline at end of file From b2d81ef75ca7185ff271f4aff4ff89a6f3e7a18c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 31 Jan 2024 02:38:58 +0900 Subject: [PATCH 022/158] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=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 --- build.gradle | 1 + .../auth/service/AuthService.java | 8 +- .../auth/service/KakaoOAuthService.java | 4 +- .../security/JwtAuthenticationFilter.java | 6 +- .../security/SecurityConfiguration.java | 2 +- .../custom/exception/ErrorCode.java | 5 + .../solidconnection/s3/AmazonS3Config.java | 31 ++++++ .../solidconnection/s3/ImageUrlDto.java | 11 ++ .../solidconnection/s3/S3Controller.java | 45 ++++++++ .../example/solidconnection/s3/S3Service.java | 105 ++++++++++++++++++ .../siteuser/service/SiteUserValidator.java | 20 ++++ .../example/solidconnection/type/ImgType.java | 14 +++ 12 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/s3/AmazonS3Config.java create mode 100644 src/main/java/com/example/solidconnection/s3/ImageUrlDto.java create mode 100644 src/main/java/com/example/solidconnection/s3/S3Controller.java create mode 100644 src/main/java/com/example/solidconnection/s3/S3Service.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java create mode 100644 src/main/java/com/example/solidconnection/type/ImgType.java diff --git a/build.gradle b/build.gradle index b6e8719eb..c577ad641 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ dependencies { implementation 'org.springframework.security:spring-security-web:6.1.2' implementation 'io.lettuce:lettuce-core:6.2.5.RELEASE' implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.470' compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index d98e80d04..4558dfd73 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -13,6 +13,7 @@ import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.siteuser.service.SiteUserValidator; import com.example.solidconnection.type.CountryCode; import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.type.Role; @@ -38,6 +39,7 @@ public class AuthService { private final RedisTemplate redisTemplate; private final TokenValidator tokenValidator; private final TokenService tokenService; + private final SiteUserValidator siteUserValidator; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -78,15 +80,11 @@ public boolean signOut(String email){ } public boolean quit(String email){ - SiteUser siteUser = getValidatedUser(email); + SiteUser siteUser = siteUserValidator.validateExistByEmail(email); siteUser.setQuitedAt(LocalDate.now().plusDays(1)); return true; } - private SiteUser getValidatedUser(String email){ - return siteUserRepository.findByEmail(email).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - } - private void validateUserNotDuplicated(SignUpRequestDto signUpRequestDto){ String email = tokenService.getEmail(signUpRequestDto.getKakaoOauthToken()); if(siteUserRepository.existsByEmail(email)){ diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java index 4583802ba..f4b40c596 100644 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -7,6 +7,7 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.siteuser.service.SiteUserValidator; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; @@ -26,6 +27,7 @@ public class KakaoOAuthService { private final RestTemplate restTemplate; private final TokenService tokenService; + private final SiteUserValidator siteUserValidator; private final SiteUserRepository siteUserRepository; @Value("${kakao.client_id}") @@ -109,7 +111,7 @@ private SignInResponseDto kakaoSignIn(String email) { } public void resetQuitedAt(String email){ - SiteUser siteUser = siteUserRepository.findByEmail(email).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + SiteUser siteUser = siteUserValidator.validateExistByEmail(email); siteUser.setQuitedAt(null); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 4b37e3077..954d7999f 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -43,7 +43,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 - tokenValidator.validateAccessToken(token); // 유효한 액세스 토큰인지 검증 + tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 @@ -72,9 +72,7 @@ private HashSet getPermitAllEndpoints() { permitAllEndpoints.add("/favicon.ico"); // 이미지 업로드 - permitAllEndpoints.add("/img-upload/profile"); - permitAllEndpoints.add("/img-upload/gpa"); - permitAllEndpoints.add("/img-upload/language"); + permitAllEndpoints.add("/img/profile/pre"); // 토큰이 필요하지 않은 인증 permitAllEndpoints.add("/auth/kakao"); diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index bc7256b28..1012c4256 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -48,7 +48,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { -> authorizeRequest .requestMatchers( "/", "/index.html", "/favicon.ico", - "/img-upload/profile", "/img-upload/gpa", "/img-upload/language", + "/img/profile/pre", "/auth/kakao", "/auth/sign-up") .permitAll() .anyRequest().authenticated()) diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 10bd81800..98b076da7 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,6 +7,11 @@ @Getter @AllArgsConstructor public enum ErrorCode { + S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), + S3_CLIENT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 클라이언트 에러 발생"), + FILE_NOT_EXIST(HttpStatus.UNAUTHORIZED.value(), "파일이 없습니다."), + INVALID_FILE_EXTENSIONS(HttpStatus.UNAUTHORIZED.value(), "파일 형식이 유효하지 않습니다."), + NOT_IMG_FILE_EXTENSIONS(HttpStatus.UNAUTHORIZED.value(), "이미지만 업로드 할 수 있습니다."), USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), USER_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 회원입니다."), JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱 에러"), diff --git a/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java b/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java new file mode 100644 index 000000000..8f027505a --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java @@ -0,0 +1,31 @@ +package com.example.solidconnection.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AmazonS3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/s3/ImageUrlDto.java b/src/main/java/com/example/solidconnection/s3/ImageUrlDto.java new file mode 100644 index 000000000..1690f56dd --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/ImageUrlDto.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.s3; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ImageUrlDto { + private String imageUrl; +} diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java new file mode 100644 index 000000000..4b1dc7c28 --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -0,0 +1,45 @@ +package com.example.solidconnection.s3; + +import com.example.solidconnection.custom.response.CustomResponse; +import com.example.solidconnection.custom.response.DataResponse; +import com.example.solidconnection.type.ImgType; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.security.Principal; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/img") +public class S3Controller { + private final S3Service s3Service; + + @PostMapping("/profile/pre") + public CustomResponse uploadPreProfileImage(@RequestParam("imageFile") MultipartFile imageFile) { + ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.PROFILE); + return new DataResponse<>(profileImageUrl); + } + + @PostMapping("/profile/post") + public CustomResponse uploadPostProfileImage(@RequestParam("imageFile") MultipartFile imageFile, Principal principal) { + ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.PROFILE); + s3Service.deleteExProfile(principal.getName()); + return new DataResponse<>(profileImageUrl); + } + + @PostMapping("/gpa") + public CustomResponse uploadGpaImage(@RequestParam("imageFile") MultipartFile imageFile) { + ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.GPA); + return new DataResponse<>(profileImageUrl); + } + + @PostMapping("/language-test") + public CustomResponse uploadLanguageImage(@RequestParam("imageFile") MultipartFile imageFile) { + ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.LANGUAGE_TEST); + return new DataResponse<>(profileImageUrl); + } +} diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java new file mode 100644 index 000000000..185400a3a --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -0,0 +1,105 @@ +package com.example.solidconnection.s3; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.SdkClientException; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.service.SiteUserValidator; +import com.example.solidconnection.type.ImgType; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; + +@Service +@RequiredArgsConstructor +public class S3Service { + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3Client amazonS3; + private final SiteUserValidator siteUserValidator; + + public ImageUrlDto uploadImgFile(MultipartFile multipartFile, ImgType imageFile) { + validateImgFile(multipartFile); + String contentType = multipartFile.getContentType(); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(contentType); + metadata.setContentLength(multipartFile.getSize()); + + UUID randomUUID = UUID.randomUUID(); + String fileName = imageFile.getType() + "/"+ randomUUID; + + try { + amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (AmazonServiceException e) { + e.printStackTrace(); + throw new CustomException(S3_SERVICE_EXCEPTION); + } catch (SdkClientException | IOException e) { + e.printStackTrace(); + throw new CustomException(S3_CLIENT_EXCEPTION); + } + + return new ImageUrlDto(amazonS3.getUrl(bucket, fileName).toString()); + } + + private void validateImgFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new CustomException(FILE_NOT_EXIST); + } + + String fileName = Objects.requireNonNull(file.getOriginalFilename()); + String fileExtension = getFileExtension(fileName).toLowerCase(); + + List allowedExtensions = Arrays.asList("jpg", "jpeg", "png"); + if (!allowedExtensions.contains(fileExtension)) { + throw new CustomException(NOT_IMG_FILE_EXTENSIONS, "허용된 형식: " + allowedExtensions); + } + } + + private String getFileExtension(String fileName) { + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex < 0 || dotIndex == fileName.length() - 1) { + throw new CustomException(INVALID_FILE_EXTENSIONS); + } + return fileName.substring(dotIndex + 1); + } + + public void deleteExProfile(String email){ + String key = getExProfileImageUrl(email); + deleteFile(key); + } + + private void deleteFile(String fileName) { + try { + amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); + } catch (AmazonServiceException e) { + e.printStackTrace(); + throw new CustomException(S3_SERVICE_EXCEPTION); + } catch (SdkClientException e) { + e.printStackTrace(); + throw new CustomException(S3_CLIENT_EXCEPTION); + } + } + + private String getExProfileImageUrl(String email){ + SiteUser siteUser = siteUserValidator.validateExistByEmail(email); + String fileName = siteUser.getProfileImageUrl(); + int domainStartIndex = fileName.indexOf(".com"); + return fileName.substring(domainStartIndex + 5); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java new file mode 100644 index 000000000..47143370a --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.siteuser.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class SiteUserValidator { + private final SiteUserRepository siteUserRepository; + + public SiteUser validateExistByEmail(String email){ + return siteUserRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + } +} diff --git a/src/main/java/com/example/solidconnection/type/ImgType.java b/src/main/java/com/example/solidconnection/type/ImgType.java new file mode 100644 index 000000000..6f34ec267 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/ImgType.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.type; + +import lombok.Getter; + +@Getter +public enum ImgType { + PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"); + + private final String type; + + ImgType(String type){ + this.type = type; + } +} From a82cc7df8699b033478382c30d9f0a1c2f8f0628 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 3 Feb 2024 11:16:13 +0900 Subject: [PATCH 023/158] =?UTF-8?q?feat:=20=EC=A0=80=EC=9E=A5=EC=9A=A9=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/KakaoOAuthService.java | 3 + .../exception/CustomExceptionHandler.java | 2 + .../solidconnection/entity/Country.java | 3 - .../entity/GpaRequirement.java | 2 +- .../solidconnection/entity/Region.java | 3 - .../solidconnection/entity/SiteUser.java | 4 - .../solidconnection/entity/University.java | 20 +-- .../type/ExchangeSemester.java | 4 +- ...eePaymentType.java => TuitionFeeType.java} | 4 +- src/main/resources/data.sql | 157 ++++++++++++++++++ src/main/resources/schema.sql | 118 ------------- 11 files changed, 175 insertions(+), 145 deletions(-) rename src/main/java/com/example/solidconnection/type/{TuitionFeePaymentType.java => TuitionFeeType.java} (81%) delete mode 100644 src/main/resources/schema.sql diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java index f4b40c596..93055acfe 100644 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -16,6 +16,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.util.Arrays; import java.util.Objects; import static com.example.solidconnection.custom.exception.ErrorCode.*; @@ -63,6 +64,8 @@ private String getKakaoAccessToken(String code) { ); return Objects.requireNonNull(response.getBody()).getAccessToken(); } catch (Exception e){ + System.out.println(e.getMessage()); + System.out.println(Arrays.toString(e.getStackTrace())); throw new CustomException(INVALID_KAKAO_AUTH_CODE); } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java index 29a5e0a3a..eed88a015 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java @@ -16,6 +16,8 @@ public class CustomExceptionHandler { @ExceptionHandler(CustomException.class) protected ResponseEntity handleCustomException(CustomException e) { + log.error(e.getMessage()); + e.printStackTrace(); ErrorResponse errorResponse = new ErrorResponse(e); return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(e.getCode())); } diff --git a/src/main/java/com/example/solidconnection/entity/Country.java b/src/main/java/com/example/solidconnection/entity/Country.java index b6f5cb0ae..5dbbcd463 100644 --- a/src/main/java/com/example/solidconnection/entity/Country.java +++ b/src/main/java/com/example/solidconnection/entity/Country.java @@ -19,7 +19,4 @@ public class Country { @OneToMany(mappedBy = "country") private Set universities; - - @OneToMany(mappedBy = "country") - private Set interestedCountries; } diff --git a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java index 911be4a55..4d54b86de 100644 --- a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java +++ b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java @@ -12,7 +12,7 @@ public class GpaRequirement { private String scale; @Column(nullable = false) - private Float minGpa; + private String minGpa; // 연관 관계 @ManyToOne diff --git a/src/main/java/com/example/solidconnection/entity/Region.java b/src/main/java/com/example/solidconnection/entity/Region.java index 6c9f0557f..ca0ed1078 100644 --- a/src/main/java/com/example/solidconnection/entity/Region.java +++ b/src/main/java/com/example/solidconnection/entity/Region.java @@ -16,9 +16,6 @@ public class Region { @OneToMany(mappedBy = "region") private Set countries; - @OneToMany(mappedBy = "region") - private Set interestedRegions; - @OneToMany(mappedBy = "region") private Set universities; } diff --git a/src/main/java/com/example/solidconnection/entity/SiteUser.java b/src/main/java/com/example/solidconnection/entity/SiteUser.java index f42d2c1b6..0ebea57fa 100644 --- a/src/main/java/com/example/solidconnection/entity/SiteUser.java +++ b/src/main/java/com/example/solidconnection/entity/SiteUser.java @@ -60,8 +60,4 @@ public class SiteUser { @OneToMany(mappedBy = "siteUser") private Set applications; - - @OneToMany(mappedBy = "siteUser") - private Set wishUniversities; - } diff --git a/src/main/java/com/example/solidconnection/entity/University.java b/src/main/java/com/example/solidconnection/entity/University.java index 90f940cb6..65b956f94 100644 --- a/src/main/java/com/example/solidconnection/entity/University.java +++ b/src/main/java/com/example/solidconnection/entity/University.java @@ -1,7 +1,7 @@ package com.example.solidconnection.entity; import com.example.solidconnection.type.ExchangeSemester; -import com.example.solidconnection.type.TuitionFeePaymentType; +import com.example.solidconnection.type.TuitionFeeType; import jakarta.persistence.*; import java.util.Set; @@ -19,19 +19,22 @@ public class University { private String englishName; @Column(nullable = false, length = 100) - private String internalName; + private String formatName; @Column(nullable = false) - private Integer recruitNumber; + private Integer studentCapacity; @Column(nullable = false, length = 50) @Enumerated(EnumType.STRING) - private TuitionFeePaymentType tuitionFeePaymentType; + private TuitionFeeType tuitionFeeType; @Column(nullable = false, length = 50) @Enumerated(EnumType.STRING) private ExchangeSemester exchangeSemester; + @Column(length = 10) + private Integer semesterRequirement; + @Column(length = 1000) private String detailsForLanguage; @@ -76,13 +79,4 @@ public class University { @OneToMany(mappedBy = "university") private Set gpaRequirements; - - @OneToMany(mappedBy = "firstChoiceUniversity") - private Set firstChoiceApplications; - - @OneToMany(mappedBy = "secondChoiceUniversity") - private Set secondChoiceApplications; - - @OneToMany(mappedBy = "university") - private Set wishUniversities; } diff --git a/src/main/java/com/example/solidconnection/type/ExchangeSemester.java b/src/main/java/com/example/solidconnection/type/ExchangeSemester.java index 67129d1c8..2d5eda6de 100644 --- a/src/main/java/com/example/solidconnection/type/ExchangeSemester.java +++ b/src/main/java/com/example/solidconnection/type/ExchangeSemester.java @@ -2,7 +2,9 @@ public enum ExchangeSemester { ONE_SEMESTER("1개학기"), - NO_PREFERENCE("무관"); + ONE_YEAR("1년만 가능"), + IRRELEVANT("무관"); + private final String koreanName; diff --git a/src/main/java/com/example/solidconnection/type/TuitionFeePaymentType.java b/src/main/java/com/example/solidconnection/type/TuitionFeeType.java similarity index 81% rename from src/main/java/com/example/solidconnection/type/TuitionFeePaymentType.java rename to src/main/java/com/example/solidconnection/type/TuitionFeeType.java index dfbe189ea..21ab6700e 100644 --- a/src/main/java/com/example/solidconnection/type/TuitionFeePaymentType.java +++ b/src/main/java/com/example/solidconnection/type/TuitionFeeType.java @@ -1,13 +1,13 @@ package com.example.solidconnection.type; -public enum TuitionFeePaymentType { +public enum TuitionFeeType { HOME_UNIVERSITY_PAYMENT("본교등록금납부형"), OVERSEAS_UNIVERSITY_PAYMENT("해외대학등록금납부형"), MIXED_PAYMENT("혼합형"); private final String koreanName; - TuitionFeePaymentType(String koreanName) { + TuitionFeeType(String koreanName) { this.koreanName = koreanName; } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index c7a93c878..0c3d45e7a 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -32,3 +32,160 @@ INSERT INTO country (country_code, region_code) VALUES ('FI', 'EUROPE'), ('CN', 'CHINA'), ('TW', 'CHINA'); + +INSERT INTO university +(region_code, country_code, korean_name, format_name, english_name, + student_capacity, semester_requirement,tuition_fee_type, exchange_semester, + homepage_url, english_course_url, accommodation_url, + details_for_language, details_for_apply, details_for_major, details_for_accommodation, details, + background_image_url, logo_image_url) +VALUES + ('AMERICAS', 'US', '괌대학(A형)', 'University of Guam', '1', 'HOME_UNIVERSITY_PAYMENT', '무관', '61', '500', NULL, '5.5', NULL, NULL, NULL, NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.uog.edu/admissions/international-students', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/life-at-uog/residence-halls/', NULL), + ('AMERICAS', 'US', '괌대학(B형)', 'University of Guam', '2', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', '500', NULL, '5.5', NULL, NULL, NULL, NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.uog.edu/admissions/international-students', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/life-at-uog/residence-halls/', '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend'), + ('AMERICAS', 'US', '네바다주립대학 라스베이거스(B형)', 'University of Nevada, Las Vegas', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', '500', '650', '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS : 모든 영역에서 5.5 이상', '2.5', '4', '2', NULL, 'https://www.unlv.edu/engineering/eip', '- 지원가능전공: 공학계열 관련 전공자\n- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', 'Academic Programs | Howard R. Hughes College of Engineering | University of Nevada, Las Vegas (unlv.edu)', 'https://www.unlv.edu/housing', '- The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨 \n※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)\n- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000\n- 한 학기 등록금: 약 $7,500\n- International Program and Service Fees $2,500'), + ('AMERICAS', 'US', '네바다주립대학 라스베이거스(어학연수)', 'University of Nevada, Las Vegas', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', '550', '800', '6.5', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS : 모든 영역에서 6.5 이상', '3', '4', '2', NULL, 'https://www.unlv.edu/engineering/eip', '- 지원가능전공: 공학계열 관련 전공자\n- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', 'Academic Programs | Howard R. Hughes College of Engineering | University of Nevada, Las Vegas (unlv.edu)', 'https://www.unlv.edu/housing', '- The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨 \n※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)\n- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000\n- International Program and Service Fees $2,500'), + ('AMERICAS', 'US', '네브라스카 주립대학(A형)', 'University of Nebraska at Kearney', '5', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '61', '530', NULL, '5.5', NULL, NULL, 'DUOLINGO', '100', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', '2.5', '4', '2', NULL, 'University of Nebraska at Kearney (unk.edu)', '타전공 지원 및 수강 가능\n- 학과에 지원 전제조건이 있을 경우 충족해야 함', 'https://catalog.unk.edu/undergraduate/', 'https://www.unk.edu/offices/reslife/index.php', '※ On Campus 기숙사 신청 필수! (기숙사 미신청 시 해외대학등록금납부형(B형)으로 전환)\n- 기숙사 관련 정보 : https://www.unk.edu/offices/reslife/housing-options.php\n- 보험료 약 $2,273/학기 (가격변동가능), 보험 가입 필수!\nhttps://www.unk.edu/international/international-student-services/medical-insurance.php'), + ('AMERICAS', 'US', '네브라스카 주립대학(B형)', 'University of Nebraska at Kearney', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', '530', NULL, '5.5', NULL, NULL, 'DUOLINGO', '100', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', '2.5', '4', '2', 'ELI(어학연수) 과정으로 지원시 영어 성적 무관 (https://www.unk.edu/international/english-language-institute/index.php)', 'University of Nebraska at Kearney (unk.edu)', '타전공 지원 및 수강 가능\n- 학과에 지원 전제조건이 있을 경우 충족해야 함', 'https://catalog.unk.edu/undergraduate/', 'https://www.unk.edu/offices/reslife/index.php', '- ELI 어학연수 과정으로 지원시, 전공/ ESL 크레딧은 자체배치고사 점수에 따라 상이\n- 기숙사 관련 정보 : https://www.unk.edu/offices/reslife/housing-options.php\n- 등록금 관련 정보 : https://www.unk.edu/costs.php\n- 보험료 약 $2,273/학기 (가격변동가능), 보험 가입 필수!\nhttps://www.unk.edu/international/international-student-services/medical-insurance.php'), + ('AMERICAS', 'US', '노스파크대학(A형)', 'North Park University', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '100', '-영어 점수는 다음의 세부영역 점수를 각각 \n 만족해야 함\n - TOEFL iBT : 모든 영역에서 15점 이상\n - IELTS : 모든 영역에서 5.5 이상\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.75', '4.0', '2', NULL, 'https://www.northpark.edu/', '- 타전공 지원 및 수강 가능\n지원불가능전공 : Nursing, Athletic training, Education', 'https://paygate.northpark.edu:8173/Student/Courses', 'www.northpark.edu/housing', '한 학기 기숙사 비용: 약 $5,442'), + ('AMERICAS', 'US', '노스파크대학(B형)', 'North Park University', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '79', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '100', '-영어 점수는 다음의 세부영역 점수를 각각 \n 만족해야 함\n - TOEFL iBT : 모든 영역에서 15점 이상\n - IELTS : 모든 영역에서 5.5 이상\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.75', '4.0', '2', NULL, 'https://www.northpark.edu/', '- 타전공 지원 및 수강 가능\n지원불가능전공 : Nursing, Athletic training, Education', 'https://paygate.northpark.edu:8173/Student/Courses', 'www.northpark.edu/housing', '한 학기 등록금: 약 $6,938 (in-state 적용, 2023-24기준)\n한 학기 기숙사 비용: 약 $5,442'), + ('AMERICAS', 'US', '뉴욕주립대 환경과학임학대학(A형)', 'SUNY ESF', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6', NULL, NULL, 'DUOLINGO', '100', '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 쓰기 영역에서 5.0 이상', '3', '4', '2', NULL, 'https://www.esf.edu/international/', '타전공 지원 및 수강 가능', 'https://www.esf.edu/catalog/courses/', 'https://www.esf.edu/housing/', '교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음\n- College Fee : 약 $1,070'), + ('AMERICAS', 'US', '뉴욕주립대 환경과학임학대학(B형)', 'SUNY ESF', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '79', NULL, NULL, '6', NULL, NULL, 'DUOLINGO', '100', '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 쓰기 영역에서 5.0 이상', '3', '4', '2', NULL, 'https://www.esf.edu/international/', '타전공 지원 및 수강 가능', 'https://www.esf.edu/catalog/courses/', 'https://www.esf.edu/housing/', '교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음\n- 한 학기 등록금: 약 $9,450 (out of state)\n- College Fee : 약 $1,070'), + ('AMERICAS', 'US', '뉴욕주립대학 스토니브룩(B형)', 'Stony Brook University', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '80', NULL, '850', '6.5', NULL, NULL, 'DUOLINGO', '105', NULL, '2.75', '4.0', '2', NULL, 'https://www.stonybrook.edu/', '- 지원 불가 전공 : Writing (WRT), Health Sciences, Nursing, Pharmacology, Education\n- Languages 강좌(독일어, 프랑스어 등), Dance, Theatre Arts and Cinema & Cultural Studies 수강 제한\n- Business/Accounting 전공 학생은 최대 3학점까지만 전공 수업 수강 가능하며 나머지 학점은 다른 전공에서 수강 가능', 'https://www.stonybrook.edu/sb/bulletin/current/courses/browse/byabbreviation/', 'https://www.stonybrook.edu/commcms/studentaffairs/res/', '- 한 학기 기숙사 비용: 약 $4,500~6,200\n- 한 학기 등록금: 약 $12,495 (out of state 적용)\n- 등록금 및 기타 Fee Rates 관련 정보: https://www.stonybrook.edu/commcms/sfs/tuition/index.php'), + ('AMERICAS', 'US', '뉴저지시티대학(A형)', 'New Jersey City Univeristy', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '69', NULL, NULL, '5.5', NULL, NULL, 'DUOLINGO', '95', '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.njcu.edu/admissions-aid/international-students', '- 파견대학에 지원 및 수강하는 전공과 본교 전공이 일치해야함\n- 지원 불가 전공 : Nursing', 'https://www.njcu.edu/admissions-aid/international-students/information-accepted-students/choosing-classes', 'https://www.njcu.edu/admissions-aid/international-students/international-student-housing', NULL), + ('AMERICAS', 'US', '뉴저지시티대학(B형)', 'New Jersey City Univeristy', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '69', NULL, NULL, '5.5', NULL, NULL, 'DUOLINGO', '95', '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.njcu.edu/admissions-aid/international-students', '- 파견대학에 지원 및 수강하는 전공과 본교 전공이 일치해야함\n- 지원 불가 전공 : Nursing', 'https://www.njcu.edu/admissions-aid/international-students/information-accepted-students/choosing-classes', 'https://www.njcu.edu/admissions-aid/international-students/international-student-housing', '한 학기 등록금: 약 $6,892 (In-state 적용)'), + ('AMERICAS', 'US', '아칸소주립대학(A형)', 'Arkansas State University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '61', NULL, NULL, '5.5', NULL, NULL, NULL, NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2', '4', '2', NULL, 'https://www.astate.edu/', '- 타전공 지원 및 수강 가능\n지원 불가 전공: Nursing, Counseling, Music, Teaching, Occupational and Speech therapy', 'https://ssb-prod.ec.astate.edu/PROD/bwckschd.p_disp_dyn_sched', 'https://www.astate.edu/a/university-housing/housing-options/', NULL), + ('AMERICAS', 'US', '아칸소주립대학(B형)', 'Arkansas State University', '1', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', NULL, NULL, '5.5', NULL, NULL, NULL, NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2', '4', '2', NULL, 'https://www.astate.edu/', '- 타전공 지원 및 수강 가능\n지원 불가 전공: Nursing, Counseling, Music, Teaching, Occupational and Speech therapy', 'https://ssb-prod.ec.astate.edu/PROD/bwckschd.p_disp_dyn_sched', 'https://www.astate.edu/a/university-housing/housing-options/', '한 학기 등록금: 약 $6,000 (in state+10%)'), + ('AMERICAS', 'US', '안젤로주립대학(A형)', 'Angelo State University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '69', NULL, NULL, '6', NULL, NULL, 'DUOLINGO', '100', NULL, '2.5', '4.0', '2', NULL, 'https://www.angelo.edu/', '- 지원불가능전공: \nAthletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, and Nursing', 'https://ssb.angelo.edu/prod/bwwkschd.p_disp_dyn_sched', 'https://www.angelo.edu/dept/residential_programs', '- 모든 국제학생들은 안전과 영어향상을 위해 무조건 기숙사를 사용을 강제하는 International Studies Policy를 공지합니다'), + ('AMERICAS', 'US', '안젤로주립대학(B형)', 'Angelo State University', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '69', '550', '800', '6', NULL, NULL, 'DUOLINGO', '100', NULL, '2.5', '4.0', '2', NULL, 'https://www.angelo.edu/', '- 지원불가능전공: \nAthletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, and Nursing', 'https://ssb.angelo.edu/prod/bwwkschd.p_disp_dyn_sched', 'https://www.angelo.edu/dept/residential_programs', '- 모든 국제학생들은 안전과 영어향상을 위해 무조건 기숙사를 사용을 강제하는 International Studies Policy를 공지합니다\n- 등록금은 in-state rate 적용됨\n- 1년 과정으로 봄학기 수학 후 가을학기 수학을 하는 경우, 여름학기 수강 가능 (Optional)'), + ('AMERICAS', 'US', '앨러배마헌츠빌대학 (A형)', 'The University of Alabama in Huntsville', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '75', NULL, NULL, '6', NULL, NULL, 'DuOLINGO', '100', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 모든 영역에서 18점 이상\n - IELTS : 모든 영역에서 6.0 이상 \n - Duolingo : 모든 영역에서 95점 이상 (Duolingo의 경우, 토플과 아이엘츠 성적을 보유하지 않은 경우 예외적으로 적용되므로 합격 이후 필요시 파견교에서 영어능력에 대해 재확인할 수 있음/ https://www.uah.edu/admissions/undergraduate/apply-for-admission/international)\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '3', '4', '2', NULL, 'https://www.uah.edu/', '타전공 지원 및 수강 가능', 'https://catalog.uah.edu/undergrad/course-descriptions/', 'https://www.uah.edu/housing', NULL), + ('AMERICAS', 'US', '앨러배마헌츠빌대학 (B형)', 'The University of Alabama in Huntsville', '1', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '75', NULL, NULL, '6', NULL, NULL, 'DuOLINGO', '100', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 모든 영역에서 18점 이상\n - IELTS : 모든 영역에서 6.0 이상 \n - Duolingo : 모든 영역에서 95점 이상 (Duolingo의 경우, 토플과 아이엘츠 성적을 보유하지 않은 경우 예외적으로 적용되므로 합격 이후 필요시 파견교에서 영어능력에 대해 재확인할 수 있음/ https://www.uah.edu/admissions/undergraduate/apply-for-admission/international)\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '3', '4', '2', NULL, 'https://www.uah.edu/', '타전공 지원 및 수강 가능', 'https://catalog.uah.edu/undergrad/course-descriptions/', 'https://www.uah.edu/housing', '등록금 관련 정보 : https://www.uah.edu/bursar/tuition'), + ('AMERICAS', 'US', '일리노이공과대학(교환학생 과정)', 'Illinois Institute of Technology', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '80', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '110', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 모든 영역에서 20점 이상\n - IELTS : 모든 영역에서 6.0 이상 \n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '3.25', '4.0', '2', 'SAT시험 면제 조건으로 교양 및 전공 포함하여 최소 30학점 이수하여야 하며 입학사정시 전공과목 및 영어과목 위주로 검토 됨.', 'https://www.iit.edu/', '- 선이수과목이 비숫해야 후에 IIT에서 전공과목을 수강할 수 있음\n- 타전공 지원 및 수강 가능 (단, 각 학과의 사전허가 필요)', '학과소개 : \nhttp://bulletin.iit.edu/undergraduate/courses/\n\n지원안내:\nhttps://www.iit.edu/admissions-aid/undergraduate-admission/international-undergraduate-students/how-apply-international-undergraduate-students/international-visiting-and-exchange-students', '- 기숙사 정보 https://www.iit.edu/housing/housing-options/housing-rates \n- 식비(Meal Plan) 정보 \nhttps://www.iit.edu/housing/dining-and-meal-plan/options-and-rates \n- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid\n\n - 학비관련 site \nhttps://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate \n- 한 학기 등록금: 약 $14.820 (방문학생 학비장학금 $10,000/학기 차감한 금액 기준, 징학금은 12크레딧 이상 full time 등록 시에만 지급 가능)\n - 보험료 site \nhttps://www.iit.edu/shwc/insurance/plan-info-and-requirements\n- 세부사항 변동 가능'), + ('AMERICAS', 'US', '일리노이공과대학(복수학위 과정)', 'Illinois Institute of Technology', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '4개학기', '90', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '110', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 모든 영역에서 20점 이상\n - IELTS : 모든 영역에서 6.0 이상 \n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '3.25', '4.0', '4', 'SAT시험 면제 조건으로 교양 및 전공 포함하여 최소 30학점 이수하여야 하며 입학사정시 전공과목 및 영어과목 위주로 검토 됨. \n- 2023-2학기에 4차 학기 이수 예정인 학생도 조건부 지원 가능\n(반드시 정규학기 이수하여야 2024-1 파견 가능)', 'https://www.iit.edu/admissions-aid/undergraduate-admission/international-undergraduate-students/how-apply-international-undergraduate-students/international-transfer-students', '파견대학에 지원하는 전공과 본교 전공이 정확하게 일치해야함', '학과소개 : \nhttp://bulletin.iit.edu/undergraduate/courses/\n지원안내:\nhttps://www.iit.edu/admissions-aid/undergraduate-admission/international-undergraduate-students/how-apply-international-undergraduate-students/international-visiting-and-exchange-students', '- 기숙사 정보 https://www.iit.edu/housing/housing-options/housing-rates \n- 식비(Meal Plan) 정보 \nhttps://www.iit.edu/housing/dining-and-meal-plan/options-and-rates \n- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid\n \n - 학비관련 site \nhttps://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate\n- 연간 등록금: 약 $49,643, 복수학위 학비장학금 연간 1만~3만 달러 지급 (징학금은 12크레딧 이상 full time 등록 시에만 지급 가능)\n - 보험료 site \nhttps://www.iit.edu/shwc/insurance/plan-info-and-requirements\n- 세부사항 변동 가능'), + ('AMERICAS', 'US', '테일러대학', 'Taylor university', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, NULL, '3.0', '4.0', '2', NULL, 'https://www.taylor.edu/', NULL, 'https://www.taylor.edu/offices/registrar/class-schedules', 'https://www.taylor.edu/life-at-taylor/facilities/residence-halls/index', '※ 테일러대학은 기독교 정신을 기반으로 설립된 학교이므로 대다수의 학생들이 기독교를 믿고 있음. 기독교에 거부감이 없고 성실한 학교생활을 하며, 정기적인 교회 생활을 하고있는 학생들만 지원 권장\n- 원칙은 1학기 지원이나, 2학기도 학생이 원하면 지원 가능 \n다만, 파견대학에서 학생의 교환학생 성과를 평가해 이에 미치지 못할 경우 2학기를 이어 진행하지 못하고 한 학기만 진행할 수 있으니 해당 사항 유의하기 바람. 이에 2학기를 지원하는 학생의 경우, 파견 지원 전 반드시 지역담당자에게 사전에 연락해 관련 내용에 대해 논의하고 지원하길 바람.'), + ('AMERICAS', 'US', '템플대학(혼합형)', 'Temple University', '5', 'MIXED_PAYMENT', '무관', '79', '550', NULL, '6', NULL, NULL, 'DUOLINGO', '110', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함 \n - IELTS : 모든 영역에서 6 이상\n- 영어시험 성적 파견대학 지원시까지 유효하여야 함\n- 최저 기준 어학 점수가 넘더라도, 선발대학에서 특정 섹션의 어학실력이 부족하다고 판단될 경우 파견 시 별도로 대학부설 어학코스(ielp) 수강이 필요할 수 있음 [수업료 외 별도,비용발생]', '3', '4.0', '2', NULL, 'http://globalprograms.temple.edu/', '- 타전공 지원 및 수강 가능\n- 다음 전공들은 지원 가능하나 수강신청이 제한적일 수 있음 Architecture, Computer & Information Sciences, Business, Education, Performing Arts (Dance, Music, Theater), Professional Schools (Dentistry, Law, Medicine, Pharmacy, Podiatry), Visual Arts (Film/Media Arts, Graphic Design, Fine Arts, etc), Sport, Tourism and Hospitality Management\n- Business 전공의 경우, 본교 경영학과 학생만 지원가능', 'https://prd-xereg.temple.edu/StudentRegistrationSsb/ssb/term/termSelection?mode=search', 'https://globalprograms.temple.edu/housing', '※ 1개 학기로도 지원 가능\n※ 혼합형은 첫 번째 학기는 템플대학교에 등록금 지불, 두 번째 학기는 인하대에 등록금 지불하는 유형 (2개 학기를 모두 마치고 올 경우에만 두 번쨰 학기에 템플대학교 등록금 면제 및 인하대에 등록금 지불 적용 가능)\n- 한 학기 등록금: 약 $15,432 (out of rate, 관련정보: https://globalprograms.temple.edu/programs/inbound-study-abroad-exchange/costs-dates)\n- 한 학기 기숙사: 약 $5,000 (기숙사 유형에 따라 상이)'), + ('AMERICAS', 'US', '트로이주립대학(A형)', 'Troy University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '61', NULL, NULL, '5.5', NULL, NULL, 'DUOLINGO', '85', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.troy.edu/international/index.html', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)\n- 지원제힌전공: Nursing', 'https://www.troy.edu/academics/catalogs/#Undergraduate', 'https://www.troy.edu/student-life-resources/housing/index.html', NULL), + ('AMERICAS', 'US', '트로이주립대학(B형)', 'Troy University', '4', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', NULL, NULL, '5.5', NULL, NULL, 'DUOLINGO', '85', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.troy.edu/international/index.html', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)\n- 지원제힌전공: Nursing', 'https://www.troy.edu/academics/catalogs/#Undergraduate', 'https://www.troy.edu/student-life-resources/housing/index.html', '한 학기 등록금: 약 $9,312 (50% tuition scholarship 지급)'), + ('AMERICAS', 'US', '하와이대학(B형)', 'University of Hawaii at Manoa', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '68', '520', NULL, '6', NULL, NULL, 'DUOLINGO', '96', '- 토플 IBT(100), 토플 ITP(600), IELTS(7.0) DUOLINGO(125) 미만시 파견후 별도 영어시험 필수이며 결과에 따라 1-3개 어학 수업 수강하게 될 수 있음\n- 어학성적은 파견대학 지원시까지 유효하여야 함', '2.5', '4', '2', NULL, 'Mānoa International Exchange – Come to Mānoa, See the World! (hawaii.edu)', '타전공 지원 및 수강 가능', 'https://www.sis.hawaii.edu/uhdad/avail.classes?i=MAN', 'https://manoa.hawaii.edu/mix/inbound/housing-meals/', '등록금: Hoakipa Visiting Student 유형으로 in-state 150% 적용'), + ('AMERICAS', 'BR', '카톨릭 대학 미나스제라이스', 'Pontificia Universidade Catolica de Minas Gerais', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', NULL, '5.5', NULL, NULL, 'DUOLINGO', '95', NULL, NULL, NULL, '2', NULL, 'https://www.pucminas.br/', '- 타전공 지원 및 수강 가능', '※ 영어강의 제공하지 않음, 모든 강의 포르투갈어로 진행\nhttp://www1.pucminas.br/ari/index_padrao.php?pagina=5829', NULL, '교내 기숙사 미제공, International Affairs와 버디프로그램을 통해 교외숙소 계약을 도와줄 예정'), + ('AMERICAS', 'BR', '포르탈레자 대학', 'UNIVERSITY OF FORTALEZA', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', NULL, '5.5', NULL, NULL, 'DUOLINGO', '95', NULL, NULL, NULL, '2', NULL, 'https://www.unifor.br/', '- 타전공 지원 및 수강 가능\n- 지원불가전공 : Medicine', '※ 대부분의 강의 포르투갈어로 진행, 주로 Business Field에 영어수업\nhttps://unifor.br/web/guest/international/exchange-students#tabs', 'https://unifor.br/web/guest/international/exchange-students#tabs', '포르투갈어 어학 수업 수강 가능'), + ('AMERICAS', 'CA', '리자이나대학(A형)', 'University of Regina', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '83', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - TOEFL iBT : 모든 영역에서 20점 이상\n - IELTS : 모든 영역에서 6.0 이상', '2.5', '4.0', '2', NULL, 'https://www.uregina.ca/international/', '- 아래 8개 Faculties 내에서만 수강 가능 : \nArts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science\n- 지원 불가 전공: Nursing, Social work', 'https://banner.uregina.ca/prod/sct/bwckschd.p_disp_dyn_sched', 'https://www.uregina.ca/housing/housing-options/index.html', NULL), + ('AMERICAS', 'CA', '리자이나대학(B형)', 'University of Regina', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '83', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - TOEFL iBT : 모든 영역에서 20점 이상\n - IELTS : 모든 영역에서 6.0 이상', '2.5', '4.0', '2', NULL, 'https://www.uregina.ca/international/', '- 아래 8개 Faculties 내에서만 수강 가능 : \nArts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science\n- 지원 불가 전공: Nursing, Social work', 'https://banner.uregina.ca/prod/sct/bwckschd.p_disp_dyn_sched', 'https://www.uregina.ca/housing/housing-options/index.html', '국제학생 등록금 적용(지원 전공 및 학점에 따라 금액 상이)\n- 관련 링크: https://www.uregina.ca/fs/students/fee-schedule.html'), + ('AMERICAS', 'CA', '메모리얼 대학 세인트존스(A형)', 'Memorial University of Newfoundland St. John\'s', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '115', '영어 점수는 다음의 세부영역 점수를 각각 만족해야함 - TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상 - IELTS : 모든 영역에서 6.0 이상 - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.5', '4.0', '2', NULL, 'https://mun.ca/goabroad/visiting-students-inbound/', '타전공 지원 및 수강 가능 - 지원불가능전공: Medicine, Pharmacy, Social work, Nursing- Computer Science, Music 지원 제한적', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'www.mun.ca/residences', NULL), + ('AMERICAS', 'CA', '메모리얼 대학 세인트존스(B형)', 'Memorial University of Newfoundland St. John\'s', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '79', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '115', '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상\n - IELTS : 모든 영역에서 6.0 이상\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.5', '4.0', '2', NULL, 'https://mun.ca/goabroad/visiting-students-inbound/', '타전공 지원 및 수강 가능 \n- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing\n- Computer Science, Music 지원 제한적', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'www.mun.ca/residences', '국제학생 등록금 적용 (학점당 $2,080)'), + ('AMERICAS', 'CA', '메모리얼대학 그랜펠(어학연수)', 'Memorial University of Newfoundland, Grenfell Campus', '4', 'OVERSEAS_UNIVERSITY_PAYMENT', '1개학기', '40', '440', '500', '4.5', NULL, NULL, NULL, NULL, '※ 가장 기초 과정(IEP-G)의 최소 지원 요건이며 레벨과 지원과정에 따라 지원자격 상이하므로 fact sheet 참조 바람\n- 학부과정 수강 가능한 IEBP-G 지원 시 다음의 세부영역 점수를 만족해야함\n - IELTS : 쓰기 5.5 이상, 모든 영역에서 5.0 이상\n - TOEFL : 쓰기 16점 이상\n- 외국어 성적 유효기간이 파견학기 시작시까지 유효해야함', '2.5', '4.0', '2', NULL, 'www.grenfell.mun.ca/esl', NULL, 'www.grenfell.mun.ca/esl', 'www.grenfell.mun.ca/housing', '선발 학생의 어학성적에 따라 레벨이 정해지며 비용 또한 상이. \n- IEBP-G 레벨에 배정될 경우 학부 수업 1-2개 수강 가능하며 (선택 제한적) 학부수업에 대한 등록금은 면제 (국제처 홈페이지 내 대학 Fact Sheet 참조 바람)'), + ('AMERICAS', 'AU', 'RMIT멜버른공과대학(A형)', 'RMIT University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '60', NULL, NULL, '6', NULL, NULL, NULL, NULL, '어학성적은 파견 학기 지원 마감일까지 유효 하여야함', '2', '4', '2', NULL, 'https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange', '파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)\n- 지원 유의 전공 : Fashion, Media and Communication, Design, Interior Design, Architecture Design\n(참고 : https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange/how-to-search-for-your-courses)', 'https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/study-abroad/study-abroad-exchange-course-search', 'https://www.rmit.edu.au/students/student-life/accommodation', NULL), + ('AMERICAS', 'AU', '서던퀸스랜드대학(A형)', 'University of Southern Queensland', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS: 각 영역 최소 5.5 이상\n- 외국어 성적 유효기간이 파견대학의 지원 시점 기준까지 유효해야함', '4', '7', '2', NULL, 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', '- 타전공 지원 및 수강 가능 \n- 미술 계열, 간호학, 약학, 교육학 등 제한 있음\n- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', '2023 UniSQ Courses', 'https://www.unisq.edu.au/current-students/support/accommodation', '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람'), + ('AMERICAS', 'AU', '서던퀸스랜드대학(B형)', 'University of Southern Queensland', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS: 각 영역 최소 5.5 이상\n- 외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '4', '7', '2', NULL, 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', '- 타전공 지원 및 수강 가능 \n- 미술 계열, 간호학, 약학, 교육학 등 제한 있음\n- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', '2023 UniSQ Courses', 'https://www.unisq.edu.au/current-students/support/accommodation', '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람\n- In-state 등록금 납부 \n(등록금 관련 정보 : https://www.unisq.edu.au/international/partnerships/study-abroad-exchange/fees-scholarships)'), + ('AMERICAS', 'AU', '시드니대학', 'University of Sydney', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '85', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 모든 영역에서 6.0 이상\n - TOEFL IBT: 읽기/듣기/말하기 17점, 쓰기 19점 이상\n- 어학성적은 파견학기 시작시까지 유효하여야함', '3', '4', '2', NULL, 'https://www.sydney.edu.au/', '타전공 지원 및 수강 가능\n- MECO, CAEL, LAWS unit 수강 여석 제한 있음', 'www.sydney.edu.au/sydney-abroad-units', 'https://www.sydney.edu.au/study/accommodation.html', 'OSHC(Overseas Student Health Cover) 국제학생 보험가입 의무 (2023년 기준 AUD 348/학기, 학기마다 비용 상이)'), + ('AMERICAS', 'AU', '커틴대학(A형)', 'Curtin University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '68', NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 모든 영역에서 6.0 이상\n - TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상\n- 어학성적은 파견학기 시작시까지 유효하여야함', '2.75', '4', '2', NULL, 'Curtin University | Make tomorrow better', '타전공 지원 및 수강 가능\n지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', '※ 24-1학기에 한하여 \'Destination Australia Cheung Kong Exchange Program Scholarship\' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), + ('AMERICAS', 'AU', '커틴대학(B형)', 'Curtin University', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '68', NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 모든 영역에서 6.0 이상\n - TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 19점 이상\n- 어학성적은 파견학기 시작시까지 유효하여야함', '2.75', '4', '2', NULL, 'Curtin University | Make tomorrow better', '타전공 지원 및 수강 가능\n지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', '한 학기 등록금: 약 10,400 AUD (in state 적용)\n※ 24-1학기에 한하여 \'Destination Australia Cheung Kong Exchange Program Scholarship\' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), + ('EUROPE', 'NL', '그로닝겐 대학교', 'University of Groningen', '4', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, NULL, '6', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상\n - IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, NULL, '2', NULL, 'https://www.rug.nl/feb/education/exchange', 'Faculty of Economics and Business로만 지원가능', 'https://www.rug.nl/feb/education/exchange/incoming/before/courses-exams', 'https://www.rug.nl/feb/education/exchange/incoming/practical-information/accommodation', NULL), + ('EUROPE', 'NL', '삭시온대학교', 'Saxion University of Applied Sciences', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '870', '6', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상\n - IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, NULL, '3', NULL, 'https://www.saxion.edu/programmes/exchange-programme/international-business/course-content', NULL, 'https://www.saxion.edu/programmes#facet_Education%20type=exchange&viewmode=0&page=1', 'https://www.saxion.edu/studying-in-the-netherlands/practical-matters/accommodation/housing-via-saxion', '기숙사 여석 많지않음'), + ('EUROPE', 'NL', '폰티스대학', 'Fontys University of Applied Sciences', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, '6', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://fontys.edu/Short-term-programmes/Exchange-programmes.htm', 'https://fontys.edu/Short-term-programmes/Exchange-programmes/Exchange-programmes-per-faculty.htm', 'https://fontys.edu/Study-at-Fontys/Exchange-programmes.htm#query={%22fields%22:[{%22key%22:%22wilIetsMet%22,%22value%22:[%22Engine%22,%22ICT%22]}],%22keywords%22:%22%22}', 'https://fontys.edu/Study-at-Fontys/Practical-information-1/Arriving-in-The-Netherlands-1/Accommodation.htm', NULL), + ('EUROPE', 'NO', '노르웨이 경영대학', 'BI Norwegian Business School', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, NULL, NULL, NULL, '2', '4.5', '2', NULL, 'www.bi.edu/exchange', '- 경영학 수업만 개설됨\n- 경영학에 대한 사전의 기초적인 과목을 이수하여야 함\n- 한 학기 최대 30ECTS까지 수강 가능함', 'www.bi.edu/exchange', '- 외부숙소 제공\n- https://www.bi.edu/study-at-bi/housing/', NULL), + ('EUROPE', 'DK', '서던덴마크대학교', 'University of Southern Denmark', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '88', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '4', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 1일)', 'https://www.sdu.dk/da', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함\n- 교환학생에게 제공되는 수업만 수강 가능\n- Faculty of Engineering 내에서 2/3이상의 수업을 수강하여야 함\n- 30 ECTS 수강', 'https://www.sdu.dk/en/uddannelse/exchange_programmes', '- 교외 숙소\n-https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing', NULL), + ('EUROPE', 'DK', '코펜하겐 IT대학', 'IT University of Copenhagen', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '88', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', 'https://en.itu.dk/programmes/exchange-students/become-an-exchange-student-at-itu', '- 본교 기초과목 이수사항에 따라 지원이 제한될 수 있으나 소속전공과 정확하게 일치 하지 않아도 지원은 가능(연관 전공이어야 함)\n- 최소 7.5 ECTS, 최대 30ECTS 수강 가능\n- 교차 수강 가능(선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)', 'https://en.itu.dk/Programmes/Exchange-students/Become-an-exchange-student-at-ITU', '- 제공(학교 운영 기숙사 아님) \n- 선착순 배정\n- https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students', NULL), + ('EUROPE', 'DE', '노이울름 대학', 'Neu-Ulm University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', NULL, '785\n(S/W 제출)', '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점\n - TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점\n외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, '2', NULL, 'International - Hochschule Neu-Ulm (hnu.de)', '타전공 지원 및 수강 가능', 'https://www.hnu.de/en/international/international-exchange-students/courses-taught-in-english', 'https://www.hnu.de/fileadmin/user_upload/5_Internationales/International_Incomings/Bewerbung/Housing_Broschure.pdf', NULL), + ('EUROPE', 'DE', '데겐도르프대학', 'Deggendorf University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, '2', NULL, 'www.th-deg.de/exchange', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)\n- 일반적으로 Business, Computer Sciences, Engineering, Tourism Field에서 영어강의를 교환학생에게 제공함', 'https://th-deg.de/exchange-students#course-choices', 'https://www.th-deg.de/en/study-with-us/accommodation', '교환 학생 프로그램에 독일 어학 수업 포함'), + ('EUROPE', 'DE', '라벤스부르크 바인가르텐 응용과학대학교', 'RWU Ravensburg Weingarten\n University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '70', NULL, '785\n(S/W제출)', '6', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- TOEIC: 말하기 160점 이상, 쓰기 150점 이상\n- IELTS: 모든 영역에서 5.5 이상\n- 어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, '2', NULL, 'https://www.rwu.de/en', '타전공 지원 및 수강 가능\n- Language 관련 강의 수강 제한적, Social Faculty의 경우 영어강의 제공하지 않음', 'https://www.rwu.de/en/international/exchange-students/study-and-course-offer', 'https://www.rwu.de/en/international/exchange-students/application', '※ BW Scholarship RWU : Fall term 2023/24 (Sept – Feb) 기간동안 한 달에 €850 장학금 수령 (제출서류 및 기한 등 자세한 정보는 국제처 홈페이지 해외대학정보 공지글 참고)'), + ('EUROPE', 'DE', '로이틀링겐 대학', 'Reutlingen University', '9', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요', NULL, NULL, '2', NULL, 'Hochschule Reutlingen – Reutlingen University: international, praxisnah, unternehmerisch (reutlingen-university.de)', '타전공 지원 및 수강 가능', '각 단과대학별 상이하므로 국제처 홈페이지 해외대학정보 Fact sheet 및 홈페이지 참조 바람', 'https://www.reutlingen-university.de/en/studies/student-life/student-accommodation', NULL), + ('EUROPE', 'DE', '루트비히스하펜 경영사회대학교', 'Ludwigshafen University of Business and Society', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요\n- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '2', NULL, 'https://www.hwg-lu.de/en/', '타전공 지원 및 수강 가능', 'https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/business-courses-in-english-bachelor', 'https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/housing', '보험 관련 정보: https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/health-insurance'), + ('EUROPE', 'DE', '바덴뷔르템베르크 산학협력대학', 'Baden-Wuerttemberg cooperative state univ.(DHBW)', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', '550', '765', '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상\n- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.5', '4.0', '2', NULL, 'https://www.dhbw-stuttgart.de/', '- 타전공 지원 및 수강 가능\n- International Study Programme(ISP) 내 수업만 수강 가능', 'https://www.dhbw-stuttgart.de/studium/internationales/international-students/exchange-students/academic-information/', 'https://www.studierendenwerk-stuttgart.de/en/accommodation/', '기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음'), + ('EUROPE', 'DE', '베를린자유대학교', 'Freie Universitat Berlin', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '- Department of Humanities, Social Science, Business Administration and Economics 수강요건: 독일어 공인성적 B2 레벨 이상의 증빙 필수, Department of Natural Science 수강요건: 독일어 공인성적 B1 레벨 이상 증빙 필수\n- John F Kennedy Institute for North American Studies 수강요건: 영어 공인성적 C1 레벨 이상의 증빙 필수', NULL, NULL, '2', NULL, 'https://www.fu-berlin.de/en/', '타전공 지원 및 수강 가능\n- 지원 불가능 전공: Pharmacy, Human Medicine and Veterinary Medicine\n- Biochemistry, Bioinformatics and Biology, Law 지원 제한적 (학과 사전승인 필요)\n- Business Administration, Economics 학과 수업 대부분이 독일어로 진행, 독일어 가능자 지원 권장', '※ 주로 Departments of English and North American Studies에서 영어강의 제공, 이 외의 학과 영어수업 제한적\nhttps://www.fu-berlin.de/vv/en/fb', 'http://www.fu-berlin.de/en/sites/unterbringung', '- 기숙사 신청 제한적, 여석 부족으로 기숙사 배정을 못 받을 가능성 있음\n- 보험 관련 정보: http://www.fu-berlin.de/en/studium/international/studium_fu/einreise_aufenthalt/krankenversicherung'), + ('EUROPE', 'DE', '뷔르츠부르크-슈바인푸르트 대학', 'Technical University of Applied Sciences Wurzburg-Schweinfurt', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 어학성적 파견 대학 지원시까지 유효하여야함', '2.5', '4', '4', NULL, 'Incoming Exchange students :: University of Applied Sciences Würzburg-Schweinfurt (fhws.de)', '※ Faculty of Economics, Business Administration에 한하여 지원 가능\n- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', 'https://fwiwi.thws.de/en/international/incoming-exchange-students/studying-at-fhws/schedule/', 'https://www.studentenwerk-wuerzburg.de/en/wohnen/move-in-guide.html', '보험 관련 정보: https://international.fhws.de/en/fhws-international/ways-to-fhws/applicants-and-student-support/before-your-arrival-at-fhws/'), + ('EUROPE', 'DE', '슈말칼덴 응용과학대학', 'Hochschule Schmalkalden University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.hs-schmalkalden.de/en.html', '타전공 지원 및 수강 가능', 'https://www.hs-schmalkalden.de/en/international/incoming-students/courses-for-incomings/exchange-students', 'https://www.stw-thueringen.de/en/housing/', '- 기숙사 신청 제한적\n- 보험 관련정보: https://signupbarmer.de/?utm_source=barmer_schmalkalden'), + ('EUROPE', 'DE', '슈투트가르트 공과대학', 'Stuttgart University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS: 모든 영역에서 6.0 이상', NULL, NULL, '2', NULL, 'www.hft-stuttgart.de', '- 타전공 지원 및 수강 가능\n- 지원 불가 전공: International Project Management, Smart City Solution\n- Civil Engineering, Surveying, Mathematic : 독일어 가능자 지원 권장, Architecture, Interior Architecture and General Management : 독일어 수업 수강 필수', 'https://www.hft-stuttgart.com/international/incoming-students/information-application', 'https://www.hft-stuttgart.com/international/incoming-students/services#c15978', '기숙사 신청 제한적'), + ('EUROPE', 'DE', '아샤펜부르크 대학', 'University of Applied Sciences Aschaffenburg', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '70', '500', '780', '5', NULL, NULL, NULL, NULL, '어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '2', NULL, 'www.th-ab.de/eng', '- 타전공 지원 및 수강 가능\n- 지원 불가 전공 : Health, Midwifery, Extra-occupational courses', 'www.th-ab.de/course-offer', 'https://www.studentenwerk-wuerzburg.de/en/aschaffenburg/student-residences.html', NULL), + ('EUROPE', 'DE', '아우크스부르크대학', 'Augsburg University of Applied Sciences', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.hs-augsburg.de/', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함\n- Department of Architecture and Civil Engineering의 경우 영어강의 제공하지 않음', 'https://www.hs-augsburg.de/en/International/Course-Catalogue.html', 'https://www.hs-augsburg.de/Binaries/Binary49994/EN-Guideline-for-finding-accomondation.pdf', NULL), + ('EUROPE', 'DE', '알브슈타트 지그마링엔 대학', 'Albstadt-Sigmaringen University of Applied Science', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, '650', '5.5', NULL, NULL, NULL, NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 영어스피킹 중급 이상 학생 지원 권장\n- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '3', NULL, 'https://www.hs-albsig.de/studieninfos/im-studium/international-office', '타전공 지원 및 수강 가능', 'https://www.hs-albsig.de/fileadmin/user_upload/hsas/International_Office/Courses_in_English_HS_AlbSig.pdf', 'https://www.my-stuwe.de/en/housing/halls-of-residence-albstadt/', '- 독일 어학 수업 수강 필수\n- 독일어 어학성적으로 지원하는 경우에 한하여 2학기로도 지원 가능'), + ('EUROPE', 'DE', '에어랑엔 뉘른베르크 대학', 'University of Erlangen Nuremberg', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '2', NULL, 'Exchange programme students › Friedrich-Alexander-Universität Erlangen-Nürnberg (fau.eu)', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함\n- 지원 불가능 전공: Human medicine, dentistry, pharmacy, law and psychology. Practical sports', 'https://www.campo.fau.de/qisserver/pages/cm/exa/coursecatalog/showCourseCatalog.xhtml?_flowId=showCourseCatalog-flow&_flowExecutionKey=e1s1&noDBAction=y&init=y', 'https://www.fau.eu/education/student-life/accommodation-2/', NULL), + ('EUROPE', 'DE', '오토폰귀릭케마그데부르그 대학', 'Otto von Guericke University of Magdeburg', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', '500', '785', '5', NULL, NULL, 'DUOLINGO', '110', '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 어학성적 파견 대학 지원시까지 유효하여야함', '3', '4', '4', NULL, 'https://www.ovgu.de/en/International/Incoming+_+Ways+to+the+University/International+Students/Exchange+Programmes.html', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함\n- 지원 불가 전공: Medicine', 'https://www.ovgu.de/unimagdeburg/en/International/Incoming+_+Ways+to+the+University/International+Students/Exchange+Programmes/Studying+as+a+WORLDWIDE+Exchange+Student-p-48750.html', 'https://tl1host.eu/SWMD/#admission', NULL), + ('EUROPE', 'DE', '올덴부르크 대학', 'Carl von Ossietzky University of Oldenburg', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '42', NULL, '550\n(S/W 제출)', '4', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEIC S/W : 말하기 120점 이상, 쓰기 120점 이상\n - 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, '2', NULL, 'uol.de/exchange-studies', '- 타전공 지원 및 수강 가능\n- 지원 불가 전공: Medicine, Programmes offered by the Centre for Lifelong Learning', 'https://elearning.uni-oldenburg.de/plugins.php/veranstaltungsverzeichnis_lvsg/englishmodules?vvz_sem_select=e770f053fbe29f2d1bd56a01d0dde1d0', 'https://uol.de/en/exchange-studies/living-in-oldenburg', '보헙관련 정보: https://uol.de/en/exchange-studies/health-insurance'), + ('EUROPE', 'DE', '제플린대학', 'Zepplin University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '90', '543', NULL, '6.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '2', NULL, 'https://www.zu.de/', '타전공 지원 및 수강 가능', 'https://zuhause.zeppelin-university.net/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=ACTION&ARGUMENTS=-AXFLjV7~Wgj~xhfmwa1nYvK3AVqeNsVYlJE1s9BJUvPyN3hZATz-SN~fW4BQGvcqQrbg69LM7Vb2PmCS13njtn6vY7wlZ3PR0VVc--5~HKXDORfzpyZYMWO-LB2OopwYkzVvJJ~JUF3g150btYFH8iNVzj12-lBywRT6Aplt7cIeSaUbvmD5Cny-23I6rfUTkzn1OdViRhkbSGv0_', 'https://www.zeppelin-university.com/info-wAssets/universitaet/dokumente/international-office/housing.pdf', NULL), + ('EUROPE', 'DE', '쳄니츠 공과대학', 'Chemnitz University of Technology', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.tu-chemnitz.de/', '타전공 지원 및 수강 가능\n- 지원 제한 전공 : Psychology', 'https://www.tu-chemnitz.de/international/incoming/erasmus/vlvz.php.en', 'https://www.swcz.de/en/student-housing/our-halls-of-residence/', NULL), + ('EUROPE', 'DE', '칼스루에 대학', 'Karlsruhe University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '2', NULL, 'Die HKA - Die Hochschule Karlsruhe:: Homepage (h-ka.de)', '타전공 지원 및 수강 가능', 'https://www.h-ka.de/en/internationalprogram/summer-semester#c33228', 'https://www.h-ka.de/en/accommodation', NULL), + ('EUROPE', 'DE', '하일브론 과학대학교', 'Heilbronn University of Applied Sciences', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '3', NULL, 'https://www.hs-heilbronn.de/en/incoming-exchange-students', '※ Faculty of Business Administration, Global Finance and Banking, Economics에 한하여 지원 가능\n- Campus Schwäbisch Hall, Campus Künzelsau 내 강의 수강 가능', 'https://cdn.hs-heilbronn.de/4641dc9db5209412/389f4f6cac1d/English-Course-Offer-and-Course-descriptions-Campus-Schw-bisch-Hall_WS-2023-24.pdf', 'https://www.hs-heilbronn.de/accommodation', NULL), + ('EUROPE', 'SE', '말뫼대학', 'Malmo University', '1', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', NULL, 'https://student.mau.se/en/go-international/exchange-studies/', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함\n- 학기당 최소 30ECTS 수강필수\n- 지원불가전공: Nursing/Dentitstry/Odontology 등\n- 교차 수강 가능', 'The previous year\'s courses open for Exchange students : https://mau.se/en/study-education/?r.PagingNumber=1&r.Languages=en&r.Query=&r.TypesSelected=67498&r.Sort=alphabetically', '- 재공하나 보장되는 것은 아님- 1인실 기준으로 4830~5387SEK/1달- https://mau.se/en/education/housing/', NULL), + ('EUROPE', 'SE', '보라스 대학교', 'University College of Boras', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '90', NULL, '775', '6.5', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함- TOEFL iBT: 쓰기 20점 이상- IELTS: 모든 영역에서 5.5이상', NULL, NULL, '4', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', 'https://www.hb.se/en/international-student/exchange-students/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없으나 일치하는 것을 권고함. 본교에서 기초과목을 이수하여야 함- 학기당 최소 30ECTS 수강필수- 교차 수강 가능(단, 선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)- 지원불가 전공 : Fashion Designer, Textile Design', 'https://www.hb.se/exchangecourses', '- 미제공, 외부숙소 지원- 2500~6000 SEK/1달- www.hb.se/en/accommodation', '2024년 봄학기 : 15 January - 2 June 2024- Orientation Days : 11-12 January 2024.'), + ('EUROPE', 'CH', '취리히응용과학기술대학', 'Zurich University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '83', NULL, '785(S/W제출)', '5.5', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함. TOEIC 말하기 160점 이상, 쓰기 150점 이상', '2.5', '4.5', '3', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.zhaw.ch/en/engineering/study/international-office/studying-in-switzerland/#c96360', '-주전공 혹은 제2전공(혹은 연계전공과) 지원 권장', 'https://www.zhaw.ch/storage/engineering/studium/internationales-studium/vom-ausland-in-die-schweiz/spring_semester_zhaw_school_of_engineering.pdf', 'https://www.zhaw.ch/en/study/before-your-studies/student-accommodation/#c72617', NULL), + ('EUROPE', 'ES', '나바라대학교', 'Universidad de Navarra', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, NULL, '5.5', NULL, NULL, NULL, NULL, NULL, '2.5', '4', '3', '지원 전 권역 담당자와 사전상담 요망', 'https://www.unav.edu/web/facultad-de-derecho/estudiantes/programas-de-intercambio/incoming-students', '- School of Law로만지원가능(지원가능전공:international relations, law)- 반드시 24ECTS 이상을 수강하여야 함', NULL, NULL, NULL), + ('EUROPE', 'ES', '마드리드카를로스3세 대학교', 'Universidad Carlos III de Madrid', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '560', '800', '5', NULL, NULL, NULL, NULL, '영어과목이 다양하지 않으므로 신중한 지원 요망. 스페인어 공인어학성적(DELE 중급이상 성적)이 있을 시 추후 합격 후 담당자에게 제출하시길 권장드림(필수아님)', '2.7', '4.0', '3', NULL, 'https://www.uc3m.es/studies/international-exchange-students-in-UC3M-/bachelor-degrees', '- 영어과목이 제한적임.', 'https://www.uc3m.es/studies/international-exchage-students-in-UC3M/bachelor-degrees/course-offer', 'https://www.uc3m.es/studies/international-exchage-students-in-uc3m/bachelor-degrees/Accommodation', '기숙사 여석 많지않음'), + ('EUROPE', 'ES', '예이다대학교', 'University of Lleida', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '75', NULL, '800', '5.5', NULL, NULL, NULL, NULL, 'TOEIC의 경우 S/W 합산 320점 이상 추가로 필수 제출', '2.5', '4', '2', NULL, 'www.udl.cat/ca/serveis/ori/', NULL, 'https://www.udl.cat/ca/serveis/ori/estudiantat_estranger/eng/infoeng/subjects/', 'http://www.udl.cat/ca/serveis/ori/estudiantat_estranger/eng/infoeng/accommodation/', NULL), + ('EUROPE', 'GB', '헐대학', 'University of Hull', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '82', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함 - TOEFL iBT : 듣기 및 쓰기 18점, 읽기 18점, 말하기 20점, 쓰기 18점 이상 - IELTS : 모든 영역에서 6.0이상', '3.5', '4', '4', '지원 전 권역 담당자와 사전상담 요망', 'https://www.hull.ac.uk/choose-hull/study-at-hull/need-to-know/key-dates', '제한학과 많음. (Factsheet참조및Factsheet언급된 제한학과 외에도 학기마다 제한학과 발생가능성있음). 지원 전 권역 담당자랑 사전상담 요망. 학기당 30ECTS수강해야 LA승인남. 성적처리 늦은 편이라 8차 학기 수학자는 성적처리 늦은 거 감안하고 추가 이에 따른 불편함이 있음을 인지후 지원요망.', 'https://universityofhull.app.box.com/s/mpvulz3yz0uijdt68rybce19nek0d8eh', 'https://www.hull.ac.uk/Choose-Hull/Student-life/Accommodation/accommodation.aspx', '영국 생활비 및 숙소비용 유럽권 지역 중 상대적으로 매우 높은편. 지원전 반드시 사전고려 요망'), + ('EUROPE', 'AT', '그라츠 대학', 'University of Graz', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '530', '770', '5.5', NULL, NULL, NULL, NULL, NULL, '2.5', '4', '3', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://international.uni-graz.at/en/incoming-exchange/exchange-students/ > go to "Selecting and Changing Courses"', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', 'https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf', 'https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/', '학교인근 외부 숙소는 있지만, 외부업체운영숙소라 대학관할아님'), + ('EUROPE', 'AT', '그라츠공과대학', 'Graz University of Technology', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', NULL, '785(S/W제출필수)', '6', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함 - TOEFL IBT: 읽기 18점 이상, 쓰기 17점 이상, 말하기 20점 이상, 듣기 17점 이상 - IELTS: 쓰기 5.5점 이상, 말하기 6점 이상 - TOEIC의 경우 S/W 점수 합산 310점 이상', '2.5', '4', '2', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.tugraz.at/fileadmin/user_upload/tugrazInternal/Studium/International_studieren_und_lehren/Mobilitaetsprogramme/OverSEAs_Factsheet.pdf', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', 'https://tugraz.at/go/search-courses', 'https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033', '자체기숙사는 없음. 교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있음(선착순 경우많음). 더블룸 기준약 한달에 € 340 per month (기숙사 종류게 따라 가격 차이 유) 예산잡으면됨.'), + ('EUROPE', 'AT', '린츠 카톨릭 대학교', 'Catholic Private University Linz', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '800', '6', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '3', '봄학기에는 영어과목이 극히 제한적으로 열린다고 함. 지원 전 권역 담당자와 사전상담 요망', 'https://ku-linz.at/en', '- 지원가능전공: History, Philosophy, Art History, theology\n(영어과목 수가 그리 많지는 않으므로, 사전 확인필요)\n - 학기당 최소 15ECTS 수강신청해야 함', 'KULIS - ku-linz.at', '학교에서 몇가지 기숙사 옵션 합격시 연결예정.', NULL), + ('EUROPE', 'AT', '빈 공과대학교', 'University of Applied Sciences Technikum Wien', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '560', '800', '6', NULL, NULL, NULL, NULL, NULL, '2.5', '4', '3', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.technikum-wien.at/international/studierendenmobilitaet-2/', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음 -학기당 최소 15.ECTS 수강신청해야함', 'https://www.technikum-wien.at/en/international/student-mobility/', '기숙사없음', NULL), + ('EUROPE', 'IT', '밀라노공과대학', 'Polytechnic University of Milan', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '84', NULL, NULL, '5', NULL, NULL, NULL, NULL, NULL, '2.5', '4.5', '5', '지원 전 권역 담당자와 사전상담요망. 주로 학부보다 석사과정에 영어교과목이 개설된 편', 'https://www.polimi.it/en/international-prospective-students/laurea-magistrale-programmes-equivalent-to-master-of-science/programme-catalogue/', NULL, NULL, NULL, '양교 교류협약에 따라, Bovisa 캠퍼스로만 지원가능'), + ('EUROPE', 'IT', '베르가모 대학', 'University of Bergamo', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '77', NULL, '820', '5.5', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상\n - IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', '2.5', '4.5', '3', '지원 전 권역 담당자와 사전상담 요망. 기존 파견자 없어서 후기 자료없음.', 'https://en.unibg.it/international/students-exchange', '지원 전 권역 담당자와 사전상담 요망', NULL, NULL, NULL), + ('EUROPE', 'IT', '카포스카리대학교', 'University of Ca\'Poscari', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '73', NULL, '790', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '3', NULL, 'https://www.unive.it/data/9639/?aa=2023&titolo=&periodo=&anno_corso=&ssd=&livello=&cds=&sede=&pagina=', NULL, 'https://www.unive.it/data/9639/?aa=2023&titolo=&periodo=&anno_corso=&ssd=&livello=&cds=&sede=&pagina= you will also find an updated Ecxel file with all the courses listed', NULL, NULL), + ('EUROPE', 'CZ', '오스트라바 대학', 'University of Ostrava', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '67', '515', '650', '5.5', NULL, NULL, NULL, NULL, NULL, '2', '4', '3', '지원 전 권역 담당자와 사전상담 요망', 'https://www.osu.eu', '지원전공과 본인 전공이 일치하지 않아도 지원가능하나 수업따라가기가 어려울 수 있으므로 배경지식이 없다면 지원에 신중할것.. Faculty ofFine Arts/Music/Medicine은 교환학생 지원불가학부임.', 'https://www.osu.eu/22821/courses/', 'https://koleje.osu.eu/', NULL), + ('EUROPE', 'CZ', '체코 생명과학대학', 'Czech University of Life Sciences Prague', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '84', '565', '800', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.czu.cz/en/', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음 -학기당 최소 15.ECTS 수강신청해야함', 'https://www.czu.cz/en/r-9190-international-relations/r-17025-course-offer-academic-year-2023-2024', NULL, '기숙사 입사경쟁률 매우높음'), + ('EUROPE', 'CZ', '프라하공과대학', 'Czech Technical University in Prague', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '78', '550', '800', '5.5', NULL, NULL, NULL, NULL, '- TOEIC R/C 최소 385점 이상 TOIEC L/C 최소 400점 이상', '2.5', '4.5', '2', NULL, 'https://international.cvut.cz/students/incoming-students/erasmus-and-exchange/', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음\n-학기당 최소 15.ECTS 수강신청해야함', 'https://legacy.mobility.cvut.cz/prospectus/2023/index.php', 'https://www.suz.cvut.cz/cz', '기숙사 월260유로 정도임'), + ('EUROPE', 'PT', '리스본대학 공과대학', 'INSTITUTO SUPERIOR TECNICO-Universidade de Lisboa', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '75', '540', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '3', NULL, NULL, '지원 전 권역 담당자와 사전상담 요망', 'http://www.catolicabs.porto.ucp.pt/en/international-mobility', '-기숙사 없음.', NULL), + ('EUROPE', 'PT', '포르투갈 가톨릭대학', 'Catholic University of Portugal', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', '550', '800', '5.5', NULL, NULL, NULL, NULL, NULL, '2.5', '4', '2', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.catolicabs.porto.ucp.pt/en/catolicabs-porto', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', 'https://catolicabs.porto.ucp.pt/international-programmes-0', NULL, NULL), + ('EUROPE', 'FR', 'EBS Paris', 'EBS Paris', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '530', '600', '5.5', NULL, NULL, NULL, NULL, NULL, '2', '4', '4', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', 'https://www.ebs-paris.fr/', '- 지원전공이 본교 소속 전공 또는 제2전공과 일치하여야 함 : 경영학 과목만 개설됨- 교차 수강 가능- 최소 9, 최대 36ECTS 수강신청 가능', NULL, '- 미제공', '- 2024년 봄학기 : 2024년 1월 ~ 4월'), + ('EUROPE', 'FR', 'EPITA', 'EPITA', '7', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, '750', '6', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '4', NULL, 'https://sway.office.com/u5VHBu9DbBH6Mr8F?ref=Link', '- 지원 가능 전공 : IT계열(컴퓨터공학)- 교차 수강 불가- 학기당 최소 15ECTS, 최대 30ECTS 수강', 'https://sway.office.com/u5VHBu9DbBH6Mr8F', '- 미제공하나 교환학생들이 외부 숙소를 찾을 수 있도록 지원- http://housing.epitamasters.com', NULL), + ('EUROPE', 'FR', 'EPITECH', 'EPITECH(L\'echole de L\'expertise Informatique)', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '65', '543', '600', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', '- 어학성적표가 2024년 11월 1일까지 유효하여야 함', 'https://international.epitech.eu/', '- 지원가능전공: Computer Science / Information technology(소속전공과 지원전공이 일치하여야 함)- 교차수강 불가 - 학기당 최소 10ECTS, 최대 30ECTS 수강', 'https://international.epitech.eu/', '- 미제공하나 교환학생들이 외부 숙소를 찾을 수 있도록 지원- https://international.epitech.eu/student-life/', NULL), + ('EUROPE', 'FR', 'ESCE 국제경영대학', 'ESCE International Business School', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', NULL, NULL, NULL, 'CEFR', 'B2', NULL, '2.5', '4', '6', NULL, 'http://www.esce.fr/international/', '- 지원가능전공: 경영학 계열(소속전공과 지원전공 일치) : - 교차 수강 가능- 학기당 최소 15ECTS, 최대 34ECTS 수강', NULL, '- 미제공- 숙소관련 문의 : gdesforges@omneseducation- 관련 링크 : https://www.studapart.com/en/studapart/student-accommodation?gad=1&gclid=Cj0KCQjw7uSkBhDGARIsAMCZNJvmPVROiqUSiAO69ks7UWWXcIKO1rGO_hXPmjlx72qsV1SF1VrEzRkaAjTQEALw_wcB', NULL), + ('EUROPE', 'FR', 'ESSCA경영대학', 'Ecole Superieure des Sciences Commerciales d\'Anger', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', '550', '800', '5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 30일)', 'https://www.essca.fr/en/international/exchange-student', '- 소속전공과 지원전공이 일치할 것을 권고함 : 경영학\n- 15ECTS 수강', 'https://pcee.azurewebsites.net', '- 미제공', NULL), + ('EUROPE', 'FR', 'IPSA', 'IPSA', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '65', '500', '700', '4.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '4', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', 'https://www.ipsa.fr/en/engineering-school/aeronautical-space', '- 소속전공과 지원전공이 일치 또는 유사하여야 함 : 전공이 제한적이므로 반드시 홈페이지에서 지원 가능 전공을 확인할 것\n- 최대 30ECTS 수강', NULL, '- 미제공\n- https://www.ipsa.fr/en/student-life/pratical-information/', NULL), + ('EUROPE', 'FR', 'ISEP', 'ISEP', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '79', '543', '785', '6.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '4', '- 어학성적표가 해당 대학 개강일까지 유효해야 함', 'http://en.isep.fr/studying-at-isep/exchange-students', '- 소속전공과 지원전공이 일치하여야 함\n- 교차수강 가능\n- 최소 15ECT, 최대 33ECTS까지 수강', 'https://en.isep.fr/studying-at-isep/course-catalog/', '- 미제공\n- 외부숙소 배정에 대해 지원', NULL), + ('EUROPE', 'FR', 'KEDGE경영대학', 'KEDGE Business School', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', '550', '785', '6', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 15일)', 'https://student.kedge.edu', '- 지원가능전공: 경영학계열\n- 학기당 최소 15ECTS, 최소 30~35ETS 수강해야 함\n(Program에 따라 상이함)', 'https://student.kedge.edu/exchange-programmes/academic-information', '- 미제공\n- https://student.kedge.edu/student-services/prepare-my-studies-abroad/student-accommodation-in-france', NULL), + ('EUROPE', 'FR', 'NEOMA경영대학', 'NEOMA Business School', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', NULL, 'https://neoma-bs.com/', '- 지원가능전공 : 경영학계열(소속대학에서 경영학 관련 기초 과목을 이수하여야 함)\n- 학기당 최소 20ECTS, 최대 30ECTS 수강', 'https://neoma-bs.com/welcome-to-neoma/steps/step-4-courses/', '- 교외 숙소 제공\n- 한달에 250~600€\n- https://neoma-bs.com/welcome-to-neoma/steps/step-3-housing/', NULL), + ('EUROPE', 'FR', '국제정치대학', 'HEIP', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', NULL, NULL, NULL, 'CEFR', 'B2', NULL, '2.5', '4', '6', NULL, 'www.heip.fr/', '- 소속 전공과 지원전공이 일치해야 함\n- 교차수강 가능\n- 최소 15ECTS, 최대 30ECTS 수강신청 가능', NULL, '- 미제공\n- 숙소관련 문의 : gdesforges@omneseducation\n- 관련 링크 : https://www.studapart.com/en/studapart/student-accommodation?gad=1&gclid=Cj0KCQjw7uSkBhDGARIsAMCZNJvmPVROiqUSiAO69ks7UWWXcIKO1rGO_hXPmjlx72qsV1SF1VrEzRkaAjTQEALw_wcB', NULL), + ('EUROPE', 'FR', '노르망디경영대학', 'EM NORMANDIE BUSINESS SCHOOL', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', NULL, '750', '5.5', NULL, NULL, NULL, NULL, '* 수강 과정에 따라 영어 성적이 상의하니 유의할 것\n- UNDERGRADUATE: IBT 72/ TOEIC 750/ IELTS 5.5 \n- GRADUATE: IBT 83/ TOEIC 790/ IELTS 6.0', NULL, NULL, '60ECTS 이상 이수해야 함', '- 어학성적표가 해당 대학 신청서 제출 시 유효해야 함(~10월 31일)\n- 최저 성적요건은 프로그램에 따라 상이함(별도 문의할 것)\n- 최저이수학기 : 2개 학기 이상 이수하고, 60ECTS 이상 취득하여야 함(세부 문의는 국제교류팀으로 연락)', 'https://www.em-normandie.com/fr', '- 개설전공 : 경영경상계열, 물류(소속전공과 지원전공이 일치할 것을 권고)\n- 전공간 교차수강 불가\n- 최소 수강학점 : 15ECTS', 'https://en.em-normandie.com/em-normandie-experience/open-world-studying-abroad/exchange-programmes', '- 미제공\n- https://en.em-normandie.com/em-normandie-experience/open-world-studying-abroad/exchange-programmes', NULL), + ('EUROPE', 'FR', '라로쉘경영대학', 'La Rochelle Business School', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '70', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.excelia-group.com/student-services', '- 소속전공과 지원전공이 반드시 일치할 필요는 없으나 기초과목을 이수해야 함\n- 최대 30ECTS 수강', 'https://www.excelia-group.com/student-services/international-students/exchange-students/programmes-details', '- 미제공\n- https://excelia-group.studapart.com/en/', '2024 봄학기 : January - April/May \n(depending on the program of study)'), + ('EUROPE', 'FR', '렌 경영대학', 'ESC Rennes School of Business', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '120ECTS 이상 이수해야 함\n(비고란 참조)', '-어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 20일)\n- 최저이수학기 : 120ECTS 이상 취득하여야 함(세부 문의는 국제교류팀으로 연락)', 'https://www.rennes-sb.com/programmes/exchange-programme/', '- 지원가능전공: 경영학계열(소속전공과 지원전공이 일치 또는 유사하여야 함, 기초과목을 이수해야 함)- 교차수강 불가- 최소 15에서 최대 34ECTS(프로그램에 따라 학기당 수강 가능 ECTS가 상이함)', 'https://www.rennes-sb.com/programmes/exchange-programme/incoming-exchange-students/', '- 미제공- 숙소 예약에 대한 가이드 제공- https://www.rennes-sb.com/student-life/accommodation-health/', NULL), + ('EUROPE', 'FR', '르아브르대학', 'University of Le Havre', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', NULL, 'https://www.univ-lehavre.fr', '- 교차수강 가능- 학기당 최소 15 ECTS 수강', '추후 이메일로 안내 예정', '- 기숙사제공(CROUS)- https://www.univ-lehavre.fr/spip.php?article70', NULL), + ('EUROPE', 'FR', '릴 가톨릭 대학', 'Lille Catholic University', '5', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, '2.75', '4', '2', NULL, 'http://www.univ-catholille.fr/', '- 주전공과 지원전공이 일치하여야 함- 최대 두 개의 establishment에서 수강 가능- 지원불가능전공: Medicine, Midwifery, Nursing, Physiotherapy, Chiropody, Law, Digital animations and Video gamrs / 2nd year of Master- 최소 20ECTS 수강', 'https://www.univ-catholille.fr/sites/default/files/fichiers/VF%20Catalogue%20cours%20en%20anglais%202022-2023_0.pdf', '- 기숙사 제공(선착순)- https://www.all-lacatho.fr/en/', NULL), + ('EUROPE', 'FR', '릴 가톨릭 대학(ESTICE)', 'Lille Catholic University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '70', '540', '780', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', '어학성적이 2024년 1월까지 유효해야 함', 'http://www.univ-catholille.fr/', '- 경영대학과 릴 카톨릭대학 ESTICE와의 별도 협약에 따라 경영대학 소속 학생에 한하여 선발함- 주전공과 지원전공이 일치할 것을 권고함(필수는 아님)- ESTIC에서 개설된 교과목만 수강 가능- 최소 12~최대 32ECTS 수강', 'https://estice.fr/programs/', '- 미제공- https://www.all-lacatho.fr/en/list-accommodation', NULL), + ('EUROPE', 'FR', '몽펠리에 대학교', 'Universite Montpellier 1', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '3', NULL, 'https://www.umontpellier.fr/university-of-montpell', '- 소속대전공과 지원전공이 일치할 필요 없음- 교차수강 불가하며 교확학생에게 오픈된 교과목만 수강 가능', 'https://iae.umontpellier.fr/en/institut/exchange-students', '- 제공(선착순)- 1200EUR/1학기-https://iae.umontpellier.fr/en/institut/accommodation', NULL), + ('EUROPE', 'FR', '발드센느대학', 'Ecole d\'Architecture, Paris Val de Seine', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'French language test', 'B1', '- 프랑스어 성적 제출이 필수는 아니나 대부분의 과목이 프랑스어로 진행되므로 프랑스어를 사전에 공부할 것을 권고함', NULL, NULL, '2', NULL, 'https://www.paris-valdeseine.archi.fr/', '- 지원가능전공: 건축학부 재학생에 함함', 'https://www.paris-valdeseine.archi.fr/fileadmin/mediatheque/document/International/etudiants_entrants/English_speaking_classes.pdf', '- 미제공', NULL), + ('EUROPE', 'FR', '부르군디 경영대학', 'Burgundy School of Business', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '750', '6', NULL, NULL, 'CEFR\n\n\nDuolingo', 'B1\n\n\n80', NULL, NULL, NULL, '4', '- 어학성적표가 11월 30일까지 유효하여야 함\n- 최저이수학기 관련 : "How to choose my courses" : https://international.bsb-education.com/course-catalogues/?lang=en 참조', 'https://www.bsb-education.com/', '- 서로 다른 프로그램, 언어, 학과 교차 수강 불가능\n- 제한 전공 : https://international.bsb-education.com/course-catalogues/?lang=en', 'https://international.bsb-education.com/course-catalogues/?lang=en', '- 학교 직영 기숙사는 없으나 CROUS, STUDAPART과 제휴한 숙소 제공(선착순)\n- https://www.studapart.com/fr', NULL), + ('EUROPE', 'FR', '오덴시아 낭트 경영대학', 'Audencia Business School', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '3', NULL, 'https://apply.exchangestudents.audencia.com/', '- 경영학계열, 소속 전공과 지원전공이 반드시 일치할 필요는 없으나 관련 기초과목을 이수하여야 함\n- 학기당 15~30ECTS 수강', 'https://apply.exchangestudents.audencia.com/index.cfm?FuseAction=Abroad.ViewLink&Parent_ID=FE40C425-5056-BA1F-74632B3DF27C287E&Link_ID=0365089E-9F2E-9844-AB9F3ABC4462019B', '- 미제공\n- https://www.expatistan.com/cost-of-living/nantes', '- https://international.audencia.com/student-life/before-you-arrive'), + ('EUROPE', 'FR', '장물랭리옹3세대학교', 'Universite Jean Moulin Lyon 3', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, '1020', '6.5', NULL, NULL, NULL, NULL, '- 영어점수는 아래 각 세부점수를 만족해야 함\n - TOEFL IBT: 모든 영역 20점 이상 \n - IELTS: 모든 영역에서 6 이상 \n - TOEIC: 말하기, 쓰기, 듣기, 읽기 영역 합산 1020점 이상', NULL, NULL, '3', NULL, 'https://www.univ-lyon3.fr/', '- 소속대 전공과 지원전공이 일치할 필요는 없으나 관련 기초과목을 이수하여야 함\n- 1년간 최소 8학점(프랑스어 5학점, 프랑스문화 3학점) 이상 이수\n- 교차수강 가능(2nd year of master law, LL.M제외)', 'https://www.univ-lyon3.fr/self-study-in-english-in-lyon-france', '- 외부숙소제공\n- https://associnterlyon3.fr/en/', NULL), + ('EUROPE', 'FR', '툴루즈 경영대학교', 'Toulouse Business school', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '79', NULL, NULL, '6', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 26일)\n* 최저이수학기\n- Bachelor Year 2과정 : 최소 2개 학기 이상 이수\n- Bachelor Year 3과정 : 최소 4개 학기 이상 이수', 'www.tbs-education.fr/en', '- 지원가능전공: 경영경상계열(소속대학 전공이 관련 학과여야 함)\n- 하나의 프로그램 내에서 수강 가능\n- 최소 24ECTS 수강', 'https://www.tbs-education.com/about-tbs/international/incoming-exchange-students/', 'https://www.tbs-education.com/about-tbs/student-services/', NULL), + ('EUROPE', 'FR', '툴루즈정치대학', 'Sciences Po Toulouse', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', 'http://www.sciencespo-toulouse.fr/home-english-version-589930.kjsp', '- 소속대 전공과 지원전공이 반드시 일치할 필요는 없으나 관련 기초과목을 이수하여야 함', 'https://www.sciencespo-toulouse.fr/en/courses/university-diploma-in-international-comparative-studies', '- 미제공', NULL), + ('EUROPE', 'FR', '파리8대학교', 'University of Paris 8', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, 'DELF', 'B2', NULL, '3', '4', '2', NULL, 'www.univ-paris8.fr/en/', '- 소속 전공과 지원 전공이 일치하여야 함\n- 교차 수강 가능\n- 최대 학기당 30ECTS 수강 가능', 'https://www.univ-paris8.fr/-Etudes-diplomes-', '- 제공(단, 제한적이며 선착순 배정)\n- 1600EUR/학기당\n- https://www.univ-paris8.fr/-Informations-pratiques-', NULL), + ('EUROPE', 'FI', 'LAB대학', 'Lahti University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', NULL, NULL, '6', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', NULL, 'https://www.lab.fi/en/exchange-student-guide/studies', '- 소속 전공과 지원전공이 일치해야 함 : 경영학, 경영정보공학으로 지원 가능 \n- 기초지식이 있는 경우에 한하여 교차수강 가능함(Design 및 Fine arts는 전공 학생만 수강 가능)\n- 학기당 30 ECTS 수강 권장', 'https://opinto-opas.lab.fi/70064/en/70060/70065?lang=en', '- 미제공\n- https://lab.fi/en/exchange-student-guide', '- 기존 "라티대학"이 "LAB대학"으로 교명 변경'), + ('EUROPE', 'FI', '라펜란타기술대학교', 'Lappeenranta University of Technology', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', '460', '785', '6', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '4', '- 어학성적표가 해당 대학 개강일까지 유효해야 함', 'https://www.lut.fi/en/studies/exchange-studies', '- 소속전공과 지원전공이 일치해야 함\n- 최소 수강 학점 20ECTS, 30ECTS 수강 권장', 'https://www.lut.fi/en/studies/exchange-studies/courses-exchange-students', '- 미제공\n- 교환학생의 경우 LOAS(www.loas.fi)를 통해 숙소를 신청함\n- 290~420EUR/1달\n- Lappeenranta Student Housing Foundation (LOAS) www.loas.fi/en', ''), + ('EUROPE', 'FI', '투르크 응용과학대학', 'Turku University of Applied Sciences', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, '', 'CEFR', 'B2', NULL, NULL, NULL, '3', NULL, 'https://www.tuas.fi/en/study-tuas/exchange-students/about/', '- 지원가능전공: Faculty of Engineering and Business 내 전공 지원 가능(우리대학의 경우 공과대학, 경영대학, 소프트융합대학 소속 학생만 신청 가능)\n- 소속대 전공과 지원전공이 일치해야 함\n- 학기당 최대 30ECTS 수강신청 가능', 'https://www.tuas.fi/en/study-tuas/exchange-students/courses/', '- 미제공\n- 한달에 280~370€\n- https://www.tuas.fi/en/study-tuas/exchange-students/accommodation/', NULL), + ('EUROPE', 'FR', '트루아 공과대학', 'Universite de Technologie de Troyes', '2', 'HOME_UNIVERSITY_PAYMENT', '1년만 가능', '42', '460', '550', '4', NULL, NULL, 'CEFR', 'B1', NULL, NULL, NULL, '2', NULL, 'https://www.utt.fr/study-at-utt/academic-programs', '- 소속전공과 지원전공이 일치 또는 유사하여야 함\n- 교차 수강 가능\n- 한 학기 30ECTS를 수강 권고', 'https://www.utt.fr/study-at-utt/courses-in-english', '- 미제공\n- https://www.utt.fr/study-at-utt/accommodation', NULL), + ('ASIA', 'BN', '브루나이 국립대학', 'Universiti Brunei Darussalam', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', '550', '750', '6', NULL, NULL, NULL, NULL, '-TOEFL IBT minimum Scores :\n Reading 18, Writing 23, Listing 18, Speaking 19\n- 영어성적은 교환학생을 하는 내내 유효한 성적표여야 함', '3', '5', '2', NULL, 'www.ubd.edu.bn/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음\n- 교차 수강 가능', NULL, '- 기숙사 보유(한학기에 250BND)', '- COVID 관련 안내 사이트 https://www.pmo.gov.bn/TAPressRelease/[FINAL]%20PR%20JKPC%2012023%20-%20Pengemaskinian%20Sukat-Sukat%20Pengawalan%20COVID-19.pdf\n-수업방식: 온라인, 오프라인 혼합'), + ('ASIA', 'SG', '싱가폴경영대학', 'Singapore Management University', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '93', NULL, NULL, '7', NULL, NULL, NULL, NULL, '- 유효한 영어공인인증시험점수는 2021년 9월 26일 이후 응시한 시험점수에 한함\n- 어학점수 미보유 시 어학요건 기준에 준하는 어학실력을 보유하고 있다는 내용을 담은 지도교수 추천서를 제출하여 어학점수 갈음 가능', NULL, NULL, '2', NULL, 'https://publiceservices.smu.edu.sg/psc/ps/EMPLOYEE/HRMS/c/SIS_CR.SIS_CLASS_SEARCH.GBL?&', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음\n- 교차 수강 가능', 'https://publiceservices.smu.edu.sg/psc/ps/EMPLOYEE/HRMS/c/SIS_CR.SIS_CLASS_SEARCH.GBL?&', '기숙사 없음', NULL), + ('ASIA', 'AZ', '아다대학교', 'ADA University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '605', '5.5', NULL, NULL, NULL, NULL, '- 유효한 영어공인인증시험점수는 2021년 11월 1일 이후 응시한 시험점수에 한함', '2', '4', '2', NULL, 'https://www.ada.edu.az/frq-content/plugins/policies_x1/entry/20220704132757_85764100.pdf', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음\n- 교차 수강 가능\n-지원불가 전공: Master of Science in Computer Science & Data Analytics and Master of Science in Electrical & Power Engineering', 'https://www.ada.edu.az/frq-content/plugins/policies_x1/entry/20221226165811_72744800.pdf', '- 기숙사 보유 (한학기700-950 USD)', '-COVID 관련 안내 사이트 \nhttps://koronavirusinfo.az/az'), + ('ASIA', 'ID', '비누스대학', 'BINUS University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', '550', NULL, '6', NULL, NULL, NULL, NULL, 'IELTS overall band score of 6\n- 유효한 영어공인인증시험점수는 2022년 3월 1일 이후 응시한 시험점수에 한함', '2.75', '4', '2', NULL, 'https://bit.ly/binusexchange', '- 캠퍼스별 수강 가능 전공 다 다름 (매학기 변동 가능)\n- 아래 캠퍼스 별 수강 가능 전공 참조\n-BINUS International Senayan Campus: Fashion, Business, Computer Science, Accounting, Graphic Design and New Media, Communication, Business Information System, \n\n-BINUS Kemanggisan: Hotel Management, Tourism, Computer Science, Accounting, Civil & Industrial Engineering, Management, Marketing communication, Business Law, English Literature, Architecture\n\n-BINUS Alam Sutera: International Business Management, Computer Science, International Relations, Food Technology\n\n-BINUS Bekasi: Bar & Hotel Management', 'https://linktr.ee/binusexchange', '- 기숙사 보유 (교환학생들은 도착 후 Limited Stary Permit이 나오는 한달동안 반드시 기숙사에서 거주해야함)\n- 2인실 한달에 USD240, 1인실 한달에 USD280\n-https://binus.ac.id/binussquare/', NULL), + ('ASIA', 'JP', '가나자와대학', 'Kanazawa University', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', NULL, '700', '5.5', NULL, 'N2', NULL, NULL, NULL, NULL, NULL, '2', 'College of Science and Engineering or College of Medical, Pharmaceutial and Health Sciences의 경우 3학년 이상만 신청 가능', 'https://kuglobal.w3.kanazawa-u.ac.jp/eg/sie/program/short-term-exchange-programs-at-kanazawa-university-2023-2024/', 'https://kuglobal.w3.kanazawa-u.ac.jp/wp/wp-content/uploads/KUEP2022-20231.pdf', 'https://eduweb.sta.kanazawa-.ac.jp/portal/Public/Syllabus/SearchMain.aspx', 'https://sgu.adm.kanazawa-u.ac.jp/international/wp-content/uploads/2022/04/03_GetStarted2022_upload.pdf', NULL), + ('ASIA', 'JP', '가쿠슈인대학', 'Gakushuin University', '1', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N4', NULL, NULL, 'Faculty of International social Sciences에 한해 영어점수 필요(TOEF L IBT 80, IELTS 6.0)\n-2년 이상의 일본어 공부 이력도 신청 가능', NULL, NULL, '2', NULL, 'https://www.univ.gakushuin.ac.jp/iss/en/program/registration.html', '지원불가 : Professional School of Law', NULL, 'WAKEIJUKU https://www.wakei.org/english/ KITAZONO WOMEN\'S STUDENT https://www.kitazono-j.co.jp/ CAMPUS VILLAGE KOTAKEMUKAIHARA https://749.jp/cd/2455/ CAMPUS VILLAGE AKATSUKASHINMACHI https://749.jp/cd/2442/', NULL), + ('ASIA', 'JP', '긴다이대학', 'Kindai University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '61', '500', NULL, '6', NULL, 'N2', NULL, NULL, NULL, '2.5', '4', '2', NULL, 'https://www.kindai.ac.jp/english/files/study-at-kindai/prospective.pdf', '- 학부생만 지원 가능\n- Higashiosaka campus만 지원 가능(약학과 지원불가)\n- 영어과정은 International Studies 또는 Business 계열만 가능', NULL, NULL, '기숙사 없음, 학교밖 숙소-약 270,000엔/학기'), + ('ASIA', 'JP', '니가타대학', 'Niigata University', '9', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, '교환학생 입학 기준은 별도로 없으나, 영어 일본어 교과목 수강을 위해서는 기본 언어능력 필요(일본어의 경우 N2)', NULL, NULL, '2', '2.30 points *NU 환산표 참조 : https://www.niigata-u.ac.jp/en/wp-content/uploads/2020/12/method.pdf', 'https://www.niigata-u.ac.jp/en/study/exchange/', NULL, 'https://www.niigata-u.ac.jp/en/study/exchange/', 'https://www.niigata-u.ac.jp/en/study/life/housing/', NULL), + ('ASIA', 'JP', '도요대학', 'Toyo University', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', NULL, '1530', '5.5', NULL, 'N3', NULL, NULL, 'TOEIC:TOEIC L&R+(TOEIC S&W*2.5)\n정규교과목 수강은 N2이상', '2.5', '4', '2', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/', '-Hakusan Campus에서만 수강 가능, \n-교환학생을 위한 교과목만 수강 가능 https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/#acl', NULL, 'https://www.toyo.ac.jp/contents/international-exchange/residence/index.php', NULL), + ('ASIA', 'JP', '도요대학', 'Toyo University', '3', 'HOME_UNIVERSITY_PAYMENT', '1년', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, NULL, '2.5', '4', '4', '*해당 학교 일정 상 10월초까지 서류제출 필요3+1 프로그램 신청자 대상으로 하는 별도 장학금 수혜 가능', 'https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/', 'Hakusan Campus에서만 수강 가능', NULL, 'https://www.toyo.ac.jp/contents/international-exchange/residence/index.php', '- [3+1 프로그램]만 지원 가능 (현지 취업을 위한 맞춤형 프로그램, 파견차수 학기 유의할 것)- 3+1 프로그램은 6차, 7차 학기를 일본대학에서 수학하면서 현지에서 직장을 구한 후 인하대에서 마지막 8차 학기 이수하고 일본으로 돌아가 직장생활을 하는 프로그램임'), + ('ASIA', 'JP', '돗쿄대학', 'Dokkyo University', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N4', NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.dokkyo.ac.jp/english/exchange/calendar/syllabus.html', NULL, NULL, 'https://www.dokkyo.ac.jp/english/exchange/student/accommodation.html', '기숙사 미보유, 교환학생을 위한 외부 숙소 보장- 280,000엔~330,000엔/학기 (2023년 기준)'), + ('ASIA', 'JP', '메이지대학', 'Meiji University', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, '2', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', NULL, 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL), + ('ASIA', 'JP', '바이카여자대학', 'BAIKA Women\'s University', '1', NULL, '무관', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, NULL, '2.3', '4', '2', '여학생만 신청가능', NULL, '교환학생 지원가능 : Department of Global English, Department of Japanese culture, Department of Media and Information, Department of Psychology.', NULL, 'https://dormy-ac.com/page/baika/', '기숙사 없음, 계약된 외부 기숙사 사용-“Maison de Claire Ibaraki” 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)'), + ('ASIA', 'JP', '분쿄가쿠인대학', 'Bunkyo Gakuin University', '3', 'HOME_UNIVERSITY_PAYMENT', '1년만 가능', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.bgu.ac.jp/', NULL, NULL, NULL, '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월'), + ('ASIA', 'JP', '야마구치대학', 'Yamaguchi University', '10', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Graduate School of Ecomnomics 토플 79 이상 토익 730이상 필요', NULL, NULL, '2', '- 전공별 TO에 따라 선발 (최대인원을 넘지 않도록)\n- Faculty of Global and Science Studies : 최대 5명 \n- Faculty of Economics: 최대 8명\n- Faculty of Engineering: 최대 3명\n- Faculty of Humanities: 최대 2명\n- Others: 최대 2명', 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', NULL, '-', '교환학생은 off-campus dormitory 거주의무'), + ('ASIA', 'JP', '오사카가쿠인대학', 'Osaka Gakuin University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/schedule.html', NULL, 'https://www.ogu.ac.jp/english/int_exchange/ie_program/syllabi.html', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/housing.html', '-봄학기는 한 개 학기만 교환학생 파견 가능\n-교환학생 용 "International Exchange Program" 제공 (일본어 능력이 높은 학생에게만 오후에 제공되는 일반 과정 수강 가능)'), + ('ASIA', 'JP', '오츠마여자대학', 'Otsuma Women\'s University', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, NULL, NULL, NULL, '2', '여학생만 신청가능', NULL, NULL, NULL, 'https://www.otsuma.ac.jp/english/international/dormitory.html', '36,500엔/월, 식비별도'), + ('ASIA', 'JP', '와세다대학', 'Waseda University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '730', '6.5', NULL, 'N1', NULL, NULL, '학부별로 기준 상이, 관련페이지 참조', '3', '4', '2', NULL, 'https://www.waseda.jp/inst/cie/en/exchange/application', 'Please refer to "Requirements and Course Lists" for further information. https://www.waseda.jp/inst/cie/en/exchange/application', NULL, 'https://www.waseda.jp/inst/rlc/en/student_dormitory/exchange/', 'https://www.waseda.jp/inst/rlc/en/student_dormitory/exchange/'), + ('ASIA', 'JP', '추오대학', 'Chuo University', '1', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, 'Commerce / Science & Engineering/ Global Informatics', '2.5', '4', '2', NULL, 'https://www.chuo-u.ac.jp/english/admissions/exchange/semester-or-full-year/', '지원불가 : Professional Graduate Program (Law School, Business School)', NULL, 'https://www.chuo-u.ac.jp/english/admissions/residences/', 'Seiseki-Sakuragaoka dormitory (off-campus)는 꽤 경쟁이 치열함. On-campus와 Off-campus 모두 약 1,800-1,900 EUR/학기'), + ('ASIA', 'JP', '치바대학', 'Chiba University', '2', 'HOME_UNIVERSITY_PAYMENT', '1년만 가능', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'https://www.le.chiba-u.jp/(Japanese only)', '경제학, 국제통상학과만 지원 가능', NULL, 'https://www.chiba-u.ac.jp/international/isd/en/index.html', NULL), + ('ASIA', 'TR', '앙카라대학', 'ANKARA UNIVERSITY', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 유효한 영어공인인증시험점수는 2021년 10월 16일 이후 응시한 시험점수에 한함', '2.75', '4', '2', NULL, 'https://en.ankara.edu.tr/', '- 소속전공과 지원전공이 일치할 것을 권장함', 'iso.ankara.edu.tr', '- 기숙사 보유 (한달에 550 TL ~ 2000 TL)', '- 오프라인 수업으로 예상하나 온라인 수업 등으로 변동 가능성 있음- COVID 관련 안내 사이트 https://covid19.saglik.gov.tr/'), + ('ASIA', 'TR', '외즈예인대학교', 'Ozyegin University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '*TOEFL IBT minimum Scores : Reading 20, Writing 20, Listing 20, Speaking 20유효한 영어공인인증시험점수는 2021년 12월 1일 이후 응시한 시험점수에 한함https://www.ozyegin.edu.tr/en/student-services/application-admission/language-proficiency-requirement', NULL, NULL, '2', NULL, 'https://www.ozyegin.edu.tr/en', '-소속전공과 지원전공이 일치할것-학부 학생이 석사용 수업 수강할 수 없음', 'https://www.ozyegin.edu.tr/en/ects-course-catalog-courses-offered/courses-offered', '-기숙사 보유하나 입사 보장은 불가 (https://www.ozyegin.edu.tr/en/dormitories/housing-fees)', '-COVID 관련 안내 사이트 https://www.ozyegin.edu.tr/en/covid-19'), + ('ASIA', 'HK', '수인대학교', 'Shue Yan University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, '6', NULL, NULL, NULL, NULL, 'IELTS overall band score of 6 with no band lower than 5.5', '2.5', '4', '2', NULL, 'https://iu.hksyu.edu/inbound-exchange/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음- 교차 수강 가능', 'https://iu.hksyu.edu/wp-content/uploads/2022/08/CourseListForExchangeStudents2022-23-20220816.pdf', '- 기숙사 2인실(매달 263-302 USD)', '-COVID 관련 안내 사이트 https://www.coronavirus.gov.hk/eng/index.html'), + ('ASIA', 'HK', '홍콩시립대학', 'City Univ Hong Kong', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, 'ielts overall band 6.5-유효한 영어공인인증시험점수는 2021년 10월 2일 이후 응시한 시험점수에 한함', NULL, NULL, '2', NULL, 'https://www.admo.cityu.edu.hk/exchange_visiting/exchange/info/', '-소속전공과 지원전공이 일치할것- College of Science(Chemistry/ Mathematics / Physics) 이 학과로 홍콩시립대에서 수강 시 위 학과에서 9학점 이상 이수 필수 수강을 해야함-지원제한 전공: Biomedical science-몇몇 수강인원이 제한된 학과는 교환학생을 받지 않음 주의-선행 과목 수강이 필수인 학과의 경우 수강 이력이 없을 시 수강 불가', 'https://www.cityu.edu.hk/admo/exchange/exchange_course_list_202402.pdf', '- 기숙사가 있으나 기숙사 배정이 상당히 어려움- 대략적 비용: 학기당 HKD$ 10,850 ~ HKD$ 21,700https://www.cityu.edu.hk/sro/StudentHousing/UGHalls/InboundExchangeStudents.htm', '- COVID 관련 안내 사이트 https://www.cityu.edu.hk/fmo/default.aspx?PageID=covid19infoctr'), + ('CHINA', 'TW', '국립정치대학교', 'National Chengchi University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '700', '6', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', '2.75', '4', '2', NULL, 'https://oic.nccu.edu.tw/Post/833', '- 소속전공과 지원전공이 일치하여야 함- 수강 제한 전공: IMBA, EMBA, IMAS', 'https://qrysub.nccu.edu.tw/', '- 기숙사 보유 - 대략적 비용: 한학기에 NTD$11,000~ 33,500', NULL), + ('CHINA', 'TW', '국립중산대학', 'National Sun Yat-sen University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함', NULL, NULL, '2', NULL, 'https://oia.nsysu.edu.tw/p/412-1308-20581.php?Lang=en', '-소속전공과 지원전공이 일치할것-중국어 실력이 유창하지 않으면 영어로 강의되는 수업만 수강 가능함- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', 'https://oia.nsysu.edu.tw/p/412-1308-20770.php?Lang=en', '- 기숙사 보유 (한학기235 USD-535 USD)https://oia.nsysu.edu.tw/p/412-1308-20581.php?Lang=en', '-COVID 관련 안내 사이트 https://www.cdc.gov.tw/En'), + ('CHINA', 'TW', '국립중앙대학교', 'National Central University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', '550', '750', '6', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함- 영어성적은 지원일로부터 2년간 유효한 성적표여야 함', '70', '100', '4', NULL, 'http://oia.ncu.edu.tw/index.php/en/international-students/incoming-exchange-programs.html', '- 소속전공과 지원전공이 일치하여야 함- 지원가능 전공 확인 : https://www.ncu.edu.tw/en/pages/index.php?num=2- 교환학생용으로 제공되는 영어강의는 본래 대학원생용이라 학부 3학년 또는 4학년 학생이 국립중앙대로의 교환학생에 지원할 수 있음', 'https://cis.ncu.edu.tw/Course/main/news/announce', '기숙사 없음', '-COVID 관련 안내 사이트 https://www.cdc.gov.tw/En'), + ('CHINA', 'TW', '타이베이시립대학', 'University of Taipei', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함', NULL, NULL, '2', NULL, 'https://international.utaipei.edu.tw/index.php?Lang=en', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음- 교차 수강 가능- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', NULL, '- 기숙사 보유- 한 학기에 약 500-800$', '-COVID 관련 안내: 병원과 관련된 시설에서는 반드시 마스크 착용 해야함'), + ('CHINA', 'CN', '강남대학교', 'Jiangnan University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '59', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함- 중국어 연수 프로그램 : 영어,중국어 성적이 없어도 지원 가능- 학위과정수업(영어) : IBT 59 이상', '2.8', '4', '2', NULL, 'http://www.jiangnan.edu.cn/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음- 교차 수강 가능- 주로 인하대 학생들은 중국어 어학과정을 수강함- 최소 수강학점은 없으며, 주로 3~6과목 (2~4학점/1과목)을 수강함', 'English taught courses are generally determined by the schools at the end of the previous semester. We are not sure about it until then.', '- 학교에 기숙사가 있으나 입사 보장은 불가International Students Building (only for foreign students): Single room with bedding packs, air-conditioner and bathroom,500RMB Local Student Dormitory: 4-6 person room without bedding packs, 600-1000RMB', NULL), + ('CHINA', 'CN', '길림대학교', 'Jilin University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함* 중국어 과정1) 중국어학연수과정 : 어학성적 필요 없음2) Diplomacy (School of International and Public Affairs) : 유효한 HSK 5급 180점 이상, TOEFL 550(PBT), 213(CBT), 79(IBT) 또는 IELTS 6.0.이상을 보유하여야 함(HSK및영어 동시만족)3) 중국어 과정(석사):HSK 5급 180점 이상4) 기타 중국어 과정 : HSK 4급 180점 이상 * 영어 과정 : 길림대 내 영어테스트가 있을 예정임- 영어과정을 들을 경우, 영어성적표을 제출하여야 하며 지원일로부터 2년간 유효한 성적표여야 함', NULL, NULL, '2', NULL, 'http://cie.jlu.edu.cn/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음- 지원제한 전공 : Medical Science', NULL, '- 기숙사 보유 (6개월 이상 거주 시 신청 가능)- 대략적 비용: 한달에 1500RMB', NULL), + ('CHINA', 'CN', '남경사범대학교', 'Nanjing Normal University', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함- 중국어로 된 전공 강의를 수강하기 희망할 경우, 최소 HSK 5급 180점 이상의 점수를 보유하여야 함-중국어학연수과정 : 어학성적 필요 없음', '75', '100', '2', NULL, 'http://gjc.njnu.edu.cn/index.htm', '- 소속전공과 지원전공이 일치하여야함(교차수강 불가능)- 대부분의 교환학생들은 중국어 어학 수업을 수강함', '- 영어수업을 제공할지 결정되지 않았음', '- 기숙사 보유 (한학기 5000RMB-6000RMB: 겨울방학 및 여름방학 제외)', NULL), + ('CHINA', 'CN', '동화대학교', 'Donghua University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, '6', NULL, NULL, NULL, NULL, '- 유효한 영어공인인증시험점수는 2021년 11월 16일 이후 응시한 시험점수에 한함- 본교 중국어 어학시험에 응시하여야 함', NULL, NULL, '2', NULL, 'http://english.dhu.edu.cn', '소속전공과 지원전공이 반드시 일치할 필요는 없으나 학과에 상관없이 교차 수강은 불가능-교환학생 대상으로 별도 제공된 수강 가능 교과목안에서만 수강 가능', 'https://english.dhu.edu.cn/incoming/list.htm', '- 기숙사 보유(한 학기 약 7800yuan)- https://korean.dhu.edu.cn/accommodation/list.htm', NULL), + ('CHINA', 'CN', '산동대학교(위해)', 'Shandong University, Weihai', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함 - 중국어 수업을 들을 학생은 HSK4급 보유를 권장함- 영어 수업을 들을 학생은 TOEFL IBT 80점, IELTS 6등급 보유를 권장함- 학점 4.0 만점에 2.3, 100점 만점에 70이상인 학생만 지원 가능', '2.3', '4', '2', NULL, 'https://en.wh.sdu.edu.cn/', '소속전공과 지원전공이 일치해야 함. 단, 중국어 어학과정 학생의 경우 전공 무관- 최소 학점은 없으며, 최대 24학점 수강 가능- 체육교육과 전공은 제공하지 않음- 지원 가능 전공 https://ipo.wh.sdu.edu.cn/kristudy/info/1021/1752.htm', NULL, '- 기숙사 보유 (한학기 4,000~5,000 RMB, 금액은 변동가능성있음)', NULL), + ('CHINA', 'CN', '연태대학교', 'Yantai University', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함\n- 중국어 어학강좌만 수강할 경우 HSK 또는 영어성적 제시 불필요- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK level 4 이상이어야 함)', '2.5', '4', '2', NULL, 'https://en.ytu.edu.cn/', '- 소속전공과 지원전공이 일치하여야 함 (교차 수강 불가능)', NULL, '- 기숙사 보유(한 학기 3100-3600RMB)', NULL), + ('CHINA', 'CN', '절강사범대학교', 'Zhejiang Normal University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, '5.5', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함\n- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK4급이상 보유자만 가능)\n-유효한 영어공인인증시험점수는 2021년 12월 21일 이후 응시한 시험점수에 한함', '2.5', '4', '2', NULL, 'http://iso.zjnu.edu.cn/ywb/main.htm', '- 소속전공과 지원전공이 일치할 것', 'http://iso.zjnu.edu.cn/wistwofwwrogramsw2018/list.htm', '-기숙사 보유 (2인실/ 매달 450-600RMB)', NULL), + ('CHINA', 'CN', '중앙민족대학교', 'Minzu University of China', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함\n- 중국어 연수 프로그램 : 어학성적 불필요\n- 영어강의 없고 중국어로 하는 전공수업만 제공함. HSK5급 보유자만 지원가능(확인중)', '2.5', '4', '2', NULL, 'https://oir.muc.edu.cn/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음', NULL, '- 기숙사 보유 (한학기 1100-1500 USD)', NULL), + (NULL, NULL, 'SAF 프로그램', 'SAF Program', '10', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 어학요건이 대학별로 상이하므로 반드시 Program Guide 참고할 것', NULL, NULL, NULL, NULL, 'http://korea.studyabroadfoundation.org/', NULL, NULL, NULL, '- 지원 전 반드시 국제교류팀 담당자와 상담할 것'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index da64823a7..000000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,118 +0,0 @@ -DROP TABLE IF EXISTS `application`, `country`, `gpa_requirement`, `interested_country`, `interested_region`, `language_requirement`, `region`, `site_user`, `university`, `wish_university`; - -CREATE TABLE `application` ( - `gpa` float NOT NULL, - `first_choice_univ_id` bigint DEFAULT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `second_choice_univ_id` bigint DEFAULT NULL, - `site_user_id` bigint DEFAULT NULL, - `language_test_type` varchar(255) NOT NULL, - `verify_status` varchar(50) NOT NULL, - `gpa_report_url` varchar(500) NOT NULL, - `language_test_report_url` varchar(500) NOT NULL, - `language_test_score` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `FK4xffa66ucb9651me7uc8ek71c` (`first_choice_univ_id`), - KEY `FK401wya8j2e7jfrx7hu6gcc4fx` (`second_choice_univ_id`), - KEY `FKs4s3hebtn7vwd0b4xt8msxsis` (`site_user_id`), - CONSTRAINT `FK401wya8j2e7jfrx7hu6gcc4fx` FOREIGN KEY (`second_choice_univ_id`) REFERENCES `university` (`id`), - CONSTRAINT `FK4xffa66ucb9651me7uc8ek71c` FOREIGN KEY (`first_choice_univ_id`) REFERENCES `university` (`id`), - CONSTRAINT `FKs4s3hebtn7vwd0b4xt8msxsis` FOREIGN KEY (`site_user_id`) REFERENCES `site_user` (`id`) -); - -CREATE TABLE `country` ( - `country_code` varchar(255) NOT NULL, - `region_code` varchar(10) DEFAULT NULL, - PRIMARY KEY (`country_code`) -); - -CREATE TABLE `gpa_requirement` ( - `min_gpa` float NOT NULL, - `scale` varchar(5) NOT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `university_id` bigint DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `FK74nj7od0mj9e63ervpl0wmy4q` (`university_id`), - CONSTRAINT `FK74nj7od0mj9e63ervpl0wmy4q` FOREIGN KEY (`university_id`) REFERENCES `university` (`id`) -); - -CREATE TABLE `interested_country` ( - `country_code` varchar(2) DEFAULT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `site_user_id` bigint DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `FK26u5am55jefclcd7r5smk8ai7` (`site_user_id`), - CONSTRAINT `FK26u5am55jefclcd7r5smk8ai7` FOREIGN KEY (`site_user_id`) REFERENCES `site_user` (`id`) -); - -CREATE TABLE `interested_region` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `site_user_id` bigint DEFAULT NULL, - `region_code` varchar(10) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `FKia6h0pbisqhgm3lkeya6vqo4w` (`site_user_id`), - CONSTRAINT `FKia6h0pbisqhgm3lkeya6vqo4w` FOREIGN KEY (`site_user_id`) REFERENCES `site_user` (`id`) -); - -CREATE TABLE `language_requirement` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `university_id` bigint DEFAULT NULL, - `language_test_type` varchar(255) NOT NULL, - `min_score` varchar(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `FKp723kfidkuu8kus5svxnqq5hw` (`university_id`), - CONSTRAINT `FKp723kfidkuu8kus5svxnqq5hw` FOREIGN KEY (`university_id`) REFERENCES `university` (`id`) -); - -CREATE TABLE `region` ( - `region_code` varchar(255) NOT NULL, - PRIMARY KEY (`region_code`) -); - -CREATE TABLE `site_user` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `nickname_modified_at` datetime(6) DEFAULT NULL, - `quited_at` datetime(6) DEFAULT NULL, - `birth` varchar(20) NOT NULL, - `gender` varchar(255) NOT NULL, - `preparation_stage` varchar(255) NOT NULL, - `role` varchar(255) NOT NULL, - `email` varchar(100) NOT NULL, - `nickname` varchar(100) NOT NULL, - `profile_image_url` varchar(500) DEFAULT NULL, - PRIMARY KEY (`id`) -); - -CREATE TABLE `university` ( - `country_code` varchar(2) DEFAULT NULL, - `recruit_number` int NOT NULL, - `id` bigint NOT NULL AUTO_INCREMENT, - `region_code` varchar(10) DEFAULT NULL, - `exchange_semester` varchar(255) NOT NULL, - `tuition_fee_payment_type` varchar(255) NOT NULL, - `english_name` varchar(100) NOT NULL, - `internal_name` varchar(100) NOT NULL, - `korean_name` varchar(100) NOT NULL, - `accommodation_url` varchar(500) DEFAULT NULL, - `background_image_url` varchar(500) NOT NULL, - `details` varchar(500) DEFAULT NULL, - `english_course_url` varchar(500) DEFAULT NULL, - `homepage_url` varchar(500) DEFAULT NULL, - `logo_image_url` varchar(500) NOT NULL, - `details_for_accommodation` varchar(1000) DEFAULT NULL, - `details_for_apply` varchar(1000) DEFAULT NULL, - `details_for_language` varchar(1000) DEFAULT NULL, - `details_for_major` varchar(1000) DEFAULT NULL, - PRIMARY KEY (`id`) -); - -CREATE TABLE `wish_university` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `site_user_id` bigint DEFAULT NULL, - `university_id` bigint DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `FKrrhud921brslcukx6fyuh0th3` (`site_user_id`), - KEY `FKhj3gn3mqmfeiiw9jt83g7t3rk` (`university_id`), - CONSTRAINT `FKhj3gn3mqmfeiiw9jt83g7t3rk` FOREIGN KEY (`university_id`) REFERENCES `university` (`id`), - CONSTRAINT `FKrrhud921brslcukx6fyuh0th3` FOREIGN KEY (`site_user_id`) REFERENCES `site_user` (`id`) -); From 7d7f44cd0352215cf6c584bcc827b1eb667ebed0 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 3 Feb 2024 11:48:08 +0900 Subject: [PATCH 024/158] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=9B=90=EC=9D=B8=20=EC=9E=90=EC=84=B8=ED=9E=88=20=EC=B0=8D?= =?UTF-8?q?=ED=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/service/KakaoOAuthService.java | 7 +++++-- .../custom/exception/CustomExceptionHandler.java | 8 ++++++++ .../solidconnection/custom/exception/ErrorCode.java | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java index 4583802ba..f48a3d7b5 100644 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java @@ -60,7 +60,10 @@ private String getKakaoAccessToken(String code) { KakaoTokenDto.class ); return Objects.requireNonNull(response.getBody()).getAccessToken(); - } catch (Exception e){ + } catch (Exception e) { + if (e.getMessage().contains("KOE303")) { + throw new CustomException(REDIRECT_URI_MISMATCH); + } throw new CustomException(INVALID_KAKAO_AUTH_CODE); } } @@ -108,7 +111,7 @@ private SignInResponseDto kakaoSignIn(String email) { .build(); } - public void resetQuitedAt(String email){ + public void resetQuitedAt(String email) { SiteUser siteUser = siteUserRepository.findByEmail(email).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); siteUser.setQuitedAt(null); } diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java index 29a5e0a3a..610a2fdc4 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import static com.example.solidconnection.custom.exception.ErrorCode.JSON_PARSING_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.NOT_DEFINED_ERROR; @Slf4j @ControllerAdvice @@ -26,4 +27,11 @@ public ResponseEntity handleInvalidFormatException(InvalidFormatExceptio ErrorResponse errorResponse = new ErrorResponse(JSON_PARSING_FAILED, errorMessage); return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(JSON_PARSING_FAILED.getCode())); } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleOtherException(Exception ex) { + String errorMessage = ex.getMessage(); + ErrorResponse errorResponse = new ErrorResponse(NOT_DEFINED_ERROR, errorMessage); + return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(NOT_DEFINED_ERROR.getCode())); + } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 10bd81800..e2211e01d 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,6 +7,8 @@ @Getter @AllArgsConstructor public enum ErrorCode { + REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST.value(), "리다이렉트 uri가 잘못되었습니다."), + NOT_DEFINED_ERROR(HttpStatus.BAD_REQUEST.value(), "에러가 발생했습니다."), USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), USER_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 회원입니다."), JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱 에러"), From 763c99a9b577f9f68e4b85cdd1867dcd4f2e0017 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 3 Feb 2024 19:34:09 +0900 Subject: [PATCH 025/158] =?UTF-8?q?refactor:=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthService.java | 4 +- .../solidconnection/entity/Country.java | 11 +- .../entity/GpaRequirement.java | 4 +- .../entity/LanguageRequirement.java | 6 +- .../solidconnection/entity/Region.java | 13 +- .../solidconnection/entity/University.java | 42 +---- .../entity/UniversityInfoForApply.java | 60 +++++++ .../repositories/CountryRepository.java | 2 +- .../repositories/RegionRepository.java | 2 +- ...java => SemesterAvailableForDispatch.java} | 4 +- src/main/resources/data.sql | 159 +----------------- 11 files changed, 80 insertions(+), 227 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java rename src/main/java/com/example/solidconnection/type/{ExchangeSemester.java => SemesterAvailableForDispatch.java} (74%) diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 4558dfd73..b14e54f1e 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -123,7 +123,7 @@ private void saveInterestedCountry(SignUpRequestDto signUpRequestDto, SiteUser s List interestedCountries = signUpRequestDto.getInterestedCountries().stream() .map(CountryCode::getCountryCodeByKoreanName) .map(countryCode -> { - Country country = countryRepository.findByCountryCode(countryCode) + Country country = countryRepository.findByCode(countryCode) .orElseThrow(() -> new RuntimeException("Country Code enum이랑 table이랑 다름 : " + countryCode.name())); return InterestedCountry.builder() .siteUser(savedSiteUser) @@ -138,7 +138,7 @@ private void saveInterestedRegion(SignUpRequestDto signUpRequestDto, SiteUser sa List interestedRegions = signUpRequestDto.getInterestedRegions().stream() .map(RegionCode::getRegionCodeByKoreanName) .map(regionCode -> { - Region region = regionRepository.findByRegionCode(regionCode) + Region region = regionRepository.findByCode(regionCode) .orElseThrow(() -> new RuntimeException("Region Code enum이랑 table이랑 다름 : " + regionCode.name())); return InterestedRegion.builder() .siteUser(savedSiteUser) diff --git a/src/main/java/com/example/solidconnection/entity/Country.java b/src/main/java/com/example/solidconnection/entity/Country.java index 5dbbcd463..dd17decb7 100644 --- a/src/main/java/com/example/solidconnection/entity/Country.java +++ b/src/main/java/com/example/solidconnection/entity/Country.java @@ -3,20 +3,15 @@ import com.example.solidconnection.type.CountryCode; import jakarta.persistence.*; -import java.util.Set; - @Entity public class Country { @Id - @Column(length = 2) + @Column(length = 2, name = "country_code", columnDefinition = "VARCHAR(2)") @Enumerated(EnumType.STRING) - private CountryCode countryCode; + private CountryCode code; // 연관 관계 @ManyToOne - @JoinColumn(name = "region_code") + @JoinColumn(name = "region_code", referencedColumnName="region_code") private Region region; - - @OneToMany(mappedBy = "country") - private Set universities; } diff --git a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java index 4d54b86de..71415fb21 100644 --- a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java +++ b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java @@ -16,6 +16,6 @@ public class GpaRequirement { // 연관 관계 @ManyToOne - @JoinColumn(name = "university_id") - private University university; + @JoinColumn(name = "university_info_for_apply_id") + private UniversityInfoForApply universityInfoForApply; } diff --git a/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java b/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java index b0010ac98..a9ed6c103 100644 --- a/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java +++ b/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java @@ -17,7 +17,7 @@ public class LanguageRequirement { private String minScore; // 연관 관계 - @ManyToOne - @JoinColumn(name = "university_id") - private University university; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "university_info_for_apply_id") + private UniversityInfoForApply universityInfoForApply; } diff --git a/src/main/java/com/example/solidconnection/entity/Region.java b/src/main/java/com/example/solidconnection/entity/Region.java index ca0ed1078..db4ded28f 100644 --- a/src/main/java/com/example/solidconnection/entity/Region.java +++ b/src/main/java/com/example/solidconnection/entity/Region.java @@ -3,19 +3,10 @@ import com.example.solidconnection.type.RegionCode; import jakarta.persistence.*; -import java.util.Set; - @Entity public class Region { @Id - @Column(length = 10) + @Column(length = 10, name = "region_code", columnDefinition = "VARCHAR(10)") @Enumerated(EnumType.STRING) - private RegionCode regionCode; - - // 연관 관계 - @OneToMany(mappedBy = "region") - private Set countries; - - @OneToMany(mappedBy = "region") - private Set universities; + private RegionCode code; } diff --git a/src/main/java/com/example/solidconnection/entity/University.java b/src/main/java/com/example/solidconnection/entity/University.java index 65b956f94..e4e385491 100644 --- a/src/main/java/com/example/solidconnection/entity/University.java +++ b/src/main/java/com/example/solidconnection/entity/University.java @@ -1,11 +1,7 @@ package com.example.solidconnection.entity; -import com.example.solidconnection.type.ExchangeSemester; -import com.example.solidconnection.type.TuitionFeeType; import jakarta.persistence.*; -import java.util.Set; - @Entity public class University { @Id @@ -21,32 +17,6 @@ public class University { @Column(nullable = false, length = 100) private String formatName; - @Column(nullable = false) - private Integer studentCapacity; - - @Column(nullable = false, length = 50) - @Enumerated(EnumType.STRING) - private TuitionFeeType tuitionFeeType; - - @Column(nullable = false, length = 50) - @Enumerated(EnumType.STRING) - private ExchangeSemester exchangeSemester; - - @Column(length = 10) - private Integer semesterRequirement; - - @Column(length = 1000) - private String detailsForLanguage; - - @Column(length = 1000) - private String detailsForApply; - - @Column(length = 1000) - private String detailsForMajor; - - @Column(length = 1000) - private String detailsForAccommodation; - @Column(length = 500) private String homepageUrl; @@ -56,15 +26,15 @@ public class University { @Column(length = 500) private String accommodationUrl; - @Column(length = 500) - private String details; - @Column(nullable = false, length = 500) private String logoImageUrl; @Column(nullable = false, length = 500) private String backgroundImageUrl; + @Column(length = 1000) + private String detailsForLocal; + // 연관 관계 @ManyToOne @JoinColumn(name = "country_code") @@ -73,10 +43,4 @@ public class University { @ManyToOne @JoinColumn(name = "region_code") private Region region; - - @OneToMany(mappedBy = "university") - private Set languageRequirements; - - @OneToMany(mappedBy = "university") - private Set gpaRequirements; } diff --git a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java b/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java new file mode 100644 index 000000000..5582ebd3c --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java @@ -0,0 +1,60 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.type.SemesterAvailableForDispatch; +import com.example.solidconnection.type.TuitionFeeType; +import jakarta.persistence.*; + +import java.util.Set; + +@Entity +public class UniversityInfoForApply { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 10) + private String semester; + + @Column(nullable = false) + private Integer studentCapacity; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private TuitionFeeType tuitionFeeType; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private SemesterAvailableForDispatch semesterAvailableForDispatch; + + @Column(length = 10) + private Integer semesterRequirement; + + @Column(length = 1000) + private String detailsForLanguage; + + @Column(length = 1000) + private String detailsForApply; + + @Column(length = 1000) + private String detailsForMajor; + + @Column(length = 1000) + private String detailsForAccommodation; + + @Column(length = 1000) + private String detailsForEnglishCourse; + + @Column(length = 500) + private String details; + + // 연관 관계 + @OneToMany(mappedBy = "universityInfoForApply", fetch = FetchType.LAZY) + private Set languageRequirements; + + @OneToMany(mappedBy = "universityInfoForApply", fetch = FetchType.LAZY) + private Set gpaRequirements; + + @OneToOne + @JoinColumn(name = "university_id") + private University university; +} diff --git a/src/main/java/com/example/solidconnection/repositories/CountryRepository.java b/src/main/java/com/example/solidconnection/repositories/CountryRepository.java index a61fc08d4..6ef237e2e 100644 --- a/src/main/java/com/example/solidconnection/repositories/CountryRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/CountryRepository.java @@ -9,5 +9,5 @@ @Repository public interface CountryRepository extends JpaRepository { - Optional findByCountryCode(CountryCode countryCode); + Optional findByCode(CountryCode countryCode); } diff --git a/src/main/java/com/example/solidconnection/repositories/RegionRepository.java b/src/main/java/com/example/solidconnection/repositories/RegionRepository.java index 907295a13..8d8746480 100644 --- a/src/main/java/com/example/solidconnection/repositories/RegionRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/RegionRepository.java @@ -9,5 +9,5 @@ @Repository public interface RegionRepository extends JpaRepository { - Optional findByRegionCode(RegionCode regionCode); + Optional findByCode(RegionCode regionCode); } diff --git a/src/main/java/com/example/solidconnection/type/ExchangeSemester.java b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java similarity index 74% rename from src/main/java/com/example/solidconnection/type/ExchangeSemester.java rename to src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java index 2d5eda6de..973fe28ec 100644 --- a/src/main/java/com/example/solidconnection/type/ExchangeSemester.java +++ b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java @@ -1,6 +1,6 @@ package com.example.solidconnection.type; -public enum ExchangeSemester { +public enum SemesterAvailableForDispatch { ONE_SEMESTER("1개학기"), ONE_YEAR("1년만 가능"), IRRELEVANT("무관"); @@ -8,7 +8,7 @@ public enum ExchangeSemester { private final String koreanName; - ExchangeSemester(String koreanName) { + SemesterAvailableForDispatch(String koreanName) { this.koreanName = koreanName; } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 0c3d45e7a..0fecd6bd8 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -31,161 +31,4 @@ INSERT INTO country (country_code, region_code) VALUES ('FR', 'EUROPE'), ('FI', 'EUROPE'), ('CN', 'CHINA'), - ('TW', 'CHINA'); - -INSERT INTO university -(region_code, country_code, korean_name, format_name, english_name, - student_capacity, semester_requirement,tuition_fee_type, exchange_semester, - homepage_url, english_course_url, accommodation_url, - details_for_language, details_for_apply, details_for_major, details_for_accommodation, details, - background_image_url, logo_image_url) -VALUES - ('AMERICAS', 'US', '괌대학(A형)', 'University of Guam', '1', 'HOME_UNIVERSITY_PAYMENT', '무관', '61', '500', NULL, '5.5', NULL, NULL, NULL, NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.uog.edu/admissions/international-students', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/life-at-uog/residence-halls/', NULL), - ('AMERICAS', 'US', '괌대학(B형)', 'University of Guam', '2', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', '500', NULL, '5.5', NULL, NULL, NULL, NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.uog.edu/admissions/international-students', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/life-at-uog/residence-halls/', '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend'), - ('AMERICAS', 'US', '네바다주립대학 라스베이거스(B형)', 'University of Nevada, Las Vegas', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', '500', '650', '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS : 모든 영역에서 5.5 이상', '2.5', '4', '2', NULL, 'https://www.unlv.edu/engineering/eip', '- 지원가능전공: 공학계열 관련 전공자\n- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', 'Academic Programs | Howard R. Hughes College of Engineering | University of Nevada, Las Vegas (unlv.edu)', 'https://www.unlv.edu/housing', '- The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨 \n※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)\n- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000\n- 한 학기 등록금: 약 $7,500\n- International Program and Service Fees $2,500'), - ('AMERICAS', 'US', '네바다주립대학 라스베이거스(어학연수)', 'University of Nevada, Las Vegas', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', '550', '800', '6.5', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS : 모든 영역에서 6.5 이상', '3', '4', '2', NULL, 'https://www.unlv.edu/engineering/eip', '- 지원가능전공: 공학계열 관련 전공자\n- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', 'Academic Programs | Howard R. Hughes College of Engineering | University of Nevada, Las Vegas (unlv.edu)', 'https://www.unlv.edu/housing', '- The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨 \n※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)\n- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000\n- International Program and Service Fees $2,500'), - ('AMERICAS', 'US', '네브라스카 주립대학(A형)', 'University of Nebraska at Kearney', '5', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '61', '530', NULL, '5.5', NULL, NULL, 'DUOLINGO', '100', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', '2.5', '4', '2', NULL, 'University of Nebraska at Kearney (unk.edu)', '타전공 지원 및 수강 가능\n- 학과에 지원 전제조건이 있을 경우 충족해야 함', 'https://catalog.unk.edu/undergraduate/', 'https://www.unk.edu/offices/reslife/index.php', '※ On Campus 기숙사 신청 필수! (기숙사 미신청 시 해외대학등록금납부형(B형)으로 전환)\n- 기숙사 관련 정보 : https://www.unk.edu/offices/reslife/housing-options.php\n- 보험료 약 $2,273/학기 (가격변동가능), 보험 가입 필수!\nhttps://www.unk.edu/international/international-student-services/medical-insurance.php'), - ('AMERICAS', 'US', '네브라스카 주립대학(B형)', 'University of Nebraska at Kearney', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', '530', NULL, '5.5', NULL, NULL, 'DUOLINGO', '100', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', '2.5', '4', '2', 'ELI(어학연수) 과정으로 지원시 영어 성적 무관 (https://www.unk.edu/international/english-language-institute/index.php)', 'University of Nebraska at Kearney (unk.edu)', '타전공 지원 및 수강 가능\n- 학과에 지원 전제조건이 있을 경우 충족해야 함', 'https://catalog.unk.edu/undergraduate/', 'https://www.unk.edu/offices/reslife/index.php', '- ELI 어학연수 과정으로 지원시, 전공/ ESL 크레딧은 자체배치고사 점수에 따라 상이\n- 기숙사 관련 정보 : https://www.unk.edu/offices/reslife/housing-options.php\n- 등록금 관련 정보 : https://www.unk.edu/costs.php\n- 보험료 약 $2,273/학기 (가격변동가능), 보험 가입 필수!\nhttps://www.unk.edu/international/international-student-services/medical-insurance.php'), - ('AMERICAS', 'US', '노스파크대학(A형)', 'North Park University', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '100', '-영어 점수는 다음의 세부영역 점수를 각각 \n 만족해야 함\n - TOEFL iBT : 모든 영역에서 15점 이상\n - IELTS : 모든 영역에서 5.5 이상\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.75', '4.0', '2', NULL, 'https://www.northpark.edu/', '- 타전공 지원 및 수강 가능\n지원불가능전공 : Nursing, Athletic training, Education', 'https://paygate.northpark.edu:8173/Student/Courses', 'www.northpark.edu/housing', '한 학기 기숙사 비용: 약 $5,442'), - ('AMERICAS', 'US', '노스파크대학(B형)', 'North Park University', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '79', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '100', '-영어 점수는 다음의 세부영역 점수를 각각 \n 만족해야 함\n - TOEFL iBT : 모든 영역에서 15점 이상\n - IELTS : 모든 영역에서 5.5 이상\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.75', '4.0', '2', NULL, 'https://www.northpark.edu/', '- 타전공 지원 및 수강 가능\n지원불가능전공 : Nursing, Athletic training, Education', 'https://paygate.northpark.edu:8173/Student/Courses', 'www.northpark.edu/housing', '한 학기 등록금: 약 $6,938 (in-state 적용, 2023-24기준)\n한 학기 기숙사 비용: 약 $5,442'), - ('AMERICAS', 'US', '뉴욕주립대 환경과학임학대학(A형)', 'SUNY ESF', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6', NULL, NULL, 'DUOLINGO', '100', '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 쓰기 영역에서 5.0 이상', '3', '4', '2', NULL, 'https://www.esf.edu/international/', '타전공 지원 및 수강 가능', 'https://www.esf.edu/catalog/courses/', 'https://www.esf.edu/housing/', '교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음\n- College Fee : 약 $1,070'), - ('AMERICAS', 'US', '뉴욕주립대 환경과학임학대학(B형)', 'SUNY ESF', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '79', NULL, NULL, '6', NULL, NULL, 'DUOLINGO', '100', '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 쓰기 영역에서 5.0 이상', '3', '4', '2', NULL, 'https://www.esf.edu/international/', '타전공 지원 및 수강 가능', 'https://www.esf.edu/catalog/courses/', 'https://www.esf.edu/housing/', '교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음\n- 한 학기 등록금: 약 $9,450 (out of state)\n- College Fee : 약 $1,070'), - ('AMERICAS', 'US', '뉴욕주립대학 스토니브룩(B형)', 'Stony Brook University', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '80', NULL, '850', '6.5', NULL, NULL, 'DUOLINGO', '105', NULL, '2.75', '4.0', '2', NULL, 'https://www.stonybrook.edu/', '- 지원 불가 전공 : Writing (WRT), Health Sciences, Nursing, Pharmacology, Education\n- Languages 강좌(독일어, 프랑스어 등), Dance, Theatre Arts and Cinema & Cultural Studies 수강 제한\n- Business/Accounting 전공 학생은 최대 3학점까지만 전공 수업 수강 가능하며 나머지 학점은 다른 전공에서 수강 가능', 'https://www.stonybrook.edu/sb/bulletin/current/courses/browse/byabbreviation/', 'https://www.stonybrook.edu/commcms/studentaffairs/res/', '- 한 학기 기숙사 비용: 약 $4,500~6,200\n- 한 학기 등록금: 약 $12,495 (out of state 적용)\n- 등록금 및 기타 Fee Rates 관련 정보: https://www.stonybrook.edu/commcms/sfs/tuition/index.php'), - ('AMERICAS', 'US', '뉴저지시티대학(A형)', 'New Jersey City Univeristy', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '69', NULL, NULL, '5.5', NULL, NULL, 'DUOLINGO', '95', '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.njcu.edu/admissions-aid/international-students', '- 파견대학에 지원 및 수강하는 전공과 본교 전공이 일치해야함\n- 지원 불가 전공 : Nursing', 'https://www.njcu.edu/admissions-aid/international-students/information-accepted-students/choosing-classes', 'https://www.njcu.edu/admissions-aid/international-students/international-student-housing', NULL), - ('AMERICAS', 'US', '뉴저지시티대학(B형)', 'New Jersey City Univeristy', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '69', NULL, NULL, '5.5', NULL, NULL, 'DUOLINGO', '95', '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.njcu.edu/admissions-aid/international-students', '- 파견대학에 지원 및 수강하는 전공과 본교 전공이 일치해야함\n- 지원 불가 전공 : Nursing', 'https://www.njcu.edu/admissions-aid/international-students/information-accepted-students/choosing-classes', 'https://www.njcu.edu/admissions-aid/international-students/international-student-housing', '한 학기 등록금: 약 $6,892 (In-state 적용)'), - ('AMERICAS', 'US', '아칸소주립대학(A형)', 'Arkansas State University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '61', NULL, NULL, '5.5', NULL, NULL, NULL, NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2', '4', '2', NULL, 'https://www.astate.edu/', '- 타전공 지원 및 수강 가능\n지원 불가 전공: Nursing, Counseling, Music, Teaching, Occupational and Speech therapy', 'https://ssb-prod.ec.astate.edu/PROD/bwckschd.p_disp_dyn_sched', 'https://www.astate.edu/a/university-housing/housing-options/', NULL), - ('AMERICAS', 'US', '아칸소주립대학(B형)', 'Arkansas State University', '1', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', NULL, NULL, '5.5', NULL, NULL, NULL, NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '2', '4', '2', NULL, 'https://www.astate.edu/', '- 타전공 지원 및 수강 가능\n지원 불가 전공: Nursing, Counseling, Music, Teaching, Occupational and Speech therapy', 'https://ssb-prod.ec.astate.edu/PROD/bwckschd.p_disp_dyn_sched', 'https://www.astate.edu/a/university-housing/housing-options/', '한 학기 등록금: 약 $6,000 (in state+10%)'), - ('AMERICAS', 'US', '안젤로주립대학(A형)', 'Angelo State University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '69', NULL, NULL, '6', NULL, NULL, 'DUOLINGO', '100', NULL, '2.5', '4.0', '2', NULL, 'https://www.angelo.edu/', '- 지원불가능전공: \nAthletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, and Nursing', 'https://ssb.angelo.edu/prod/bwwkschd.p_disp_dyn_sched', 'https://www.angelo.edu/dept/residential_programs', '- 모든 국제학생들은 안전과 영어향상을 위해 무조건 기숙사를 사용을 강제하는 International Studies Policy를 공지합니다'), - ('AMERICAS', 'US', '안젤로주립대학(B형)', 'Angelo State University', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '69', '550', '800', '6', NULL, NULL, 'DUOLINGO', '100', NULL, '2.5', '4.0', '2', NULL, 'https://www.angelo.edu/', '- 지원불가능전공: \nAthletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, and Nursing', 'https://ssb.angelo.edu/prod/bwwkschd.p_disp_dyn_sched', 'https://www.angelo.edu/dept/residential_programs', '- 모든 국제학생들은 안전과 영어향상을 위해 무조건 기숙사를 사용을 강제하는 International Studies Policy를 공지합니다\n- 등록금은 in-state rate 적용됨\n- 1년 과정으로 봄학기 수학 후 가을학기 수학을 하는 경우, 여름학기 수강 가능 (Optional)'), - ('AMERICAS', 'US', '앨러배마헌츠빌대학 (A형)', 'The University of Alabama in Huntsville', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '75', NULL, NULL, '6', NULL, NULL, 'DuOLINGO', '100', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 모든 영역에서 18점 이상\n - IELTS : 모든 영역에서 6.0 이상 \n - Duolingo : 모든 영역에서 95점 이상 (Duolingo의 경우, 토플과 아이엘츠 성적을 보유하지 않은 경우 예외적으로 적용되므로 합격 이후 필요시 파견교에서 영어능력에 대해 재확인할 수 있음/ https://www.uah.edu/admissions/undergraduate/apply-for-admission/international)\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '3', '4', '2', NULL, 'https://www.uah.edu/', '타전공 지원 및 수강 가능', 'https://catalog.uah.edu/undergrad/course-descriptions/', 'https://www.uah.edu/housing', NULL), - ('AMERICAS', 'US', '앨러배마헌츠빌대학 (B형)', 'The University of Alabama in Huntsville', '1', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '75', NULL, NULL, '6', NULL, NULL, 'DuOLINGO', '100', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 모든 영역에서 18점 이상\n - IELTS : 모든 영역에서 6.0 이상 \n - Duolingo : 모든 영역에서 95점 이상 (Duolingo의 경우, 토플과 아이엘츠 성적을 보유하지 않은 경우 예외적으로 적용되므로 합격 이후 필요시 파견교에서 영어능력에 대해 재확인할 수 있음/ https://www.uah.edu/admissions/undergraduate/apply-for-admission/international)\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '3', '4', '2', NULL, 'https://www.uah.edu/', '타전공 지원 및 수강 가능', 'https://catalog.uah.edu/undergrad/course-descriptions/', 'https://www.uah.edu/housing', '등록금 관련 정보 : https://www.uah.edu/bursar/tuition'), - ('AMERICAS', 'US', '일리노이공과대학(교환학생 과정)', 'Illinois Institute of Technology', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '80', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '110', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 모든 영역에서 20점 이상\n - IELTS : 모든 영역에서 6.0 이상 \n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '3.25', '4.0', '2', 'SAT시험 면제 조건으로 교양 및 전공 포함하여 최소 30학점 이수하여야 하며 입학사정시 전공과목 및 영어과목 위주로 검토 됨.', 'https://www.iit.edu/', '- 선이수과목이 비숫해야 후에 IIT에서 전공과목을 수강할 수 있음\n- 타전공 지원 및 수강 가능 (단, 각 학과의 사전허가 필요)', '학과소개 : \nhttp://bulletin.iit.edu/undergraduate/courses/\n\n지원안내:\nhttps://www.iit.edu/admissions-aid/undergraduate-admission/international-undergraduate-students/how-apply-international-undergraduate-students/international-visiting-and-exchange-students', '- 기숙사 정보 https://www.iit.edu/housing/housing-options/housing-rates \n- 식비(Meal Plan) 정보 \nhttps://www.iit.edu/housing/dining-and-meal-plan/options-and-rates \n- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid\n\n - 학비관련 site \nhttps://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate \n- 한 학기 등록금: 약 $14.820 (방문학생 학비장학금 $10,000/학기 차감한 금액 기준, 징학금은 12크레딧 이상 full time 등록 시에만 지급 가능)\n - 보험료 site \nhttps://www.iit.edu/shwc/insurance/plan-info-and-requirements\n- 세부사항 변동 가능'), - ('AMERICAS', 'US', '일리노이공과대학(복수학위 과정)', 'Illinois Institute of Technology', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '4개학기', '90', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '110', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 모든 영역에서 20점 이상\n - IELTS : 모든 영역에서 6.0 이상 \n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '3.25', '4.0', '4', 'SAT시험 면제 조건으로 교양 및 전공 포함하여 최소 30학점 이수하여야 하며 입학사정시 전공과목 및 영어과목 위주로 검토 됨. \n- 2023-2학기에 4차 학기 이수 예정인 학생도 조건부 지원 가능\n(반드시 정규학기 이수하여야 2024-1 파견 가능)', 'https://www.iit.edu/admissions-aid/undergraduate-admission/international-undergraduate-students/how-apply-international-undergraduate-students/international-transfer-students', '파견대학에 지원하는 전공과 본교 전공이 정확하게 일치해야함', '학과소개 : \nhttp://bulletin.iit.edu/undergraduate/courses/\n지원안내:\nhttps://www.iit.edu/admissions-aid/undergraduate-admission/international-undergraduate-students/how-apply-international-undergraduate-students/international-visiting-and-exchange-students', '- 기숙사 정보 https://www.iit.edu/housing/housing-options/housing-rates \n- 식비(Meal Plan) 정보 \nhttps://www.iit.edu/housing/dining-and-meal-plan/options-and-rates \n- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid\n \n - 학비관련 site \nhttps://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate\n- 연간 등록금: 약 $49,643, 복수학위 학비장학금 연간 1만~3만 달러 지급 (징학금은 12크레딧 이상 full time 등록 시에만 지급 가능)\n - 보험료 site \nhttps://www.iit.edu/shwc/insurance/plan-info-and-requirements\n- 세부사항 변동 가능'), - ('AMERICAS', 'US', '테일러대학', 'Taylor university', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, NULL, '3.0', '4.0', '2', NULL, 'https://www.taylor.edu/', NULL, 'https://www.taylor.edu/offices/registrar/class-schedules', 'https://www.taylor.edu/life-at-taylor/facilities/residence-halls/index', '※ 테일러대학은 기독교 정신을 기반으로 설립된 학교이므로 대다수의 학생들이 기독교를 믿고 있음. 기독교에 거부감이 없고 성실한 학교생활을 하며, 정기적인 교회 생활을 하고있는 학생들만 지원 권장\n- 원칙은 1학기 지원이나, 2학기도 학생이 원하면 지원 가능 \n다만, 파견대학에서 학생의 교환학생 성과를 평가해 이에 미치지 못할 경우 2학기를 이어 진행하지 못하고 한 학기만 진행할 수 있으니 해당 사항 유의하기 바람. 이에 2학기를 지원하는 학생의 경우, 파견 지원 전 반드시 지역담당자에게 사전에 연락해 관련 내용에 대해 논의하고 지원하길 바람.'), - ('AMERICAS', 'US', '템플대학(혼합형)', 'Temple University', '5', 'MIXED_PAYMENT', '무관', '79', '550', NULL, '6', NULL, NULL, 'DUOLINGO', '110', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함 \n - IELTS : 모든 영역에서 6 이상\n- 영어시험 성적 파견대학 지원시까지 유효하여야 함\n- 최저 기준 어학 점수가 넘더라도, 선발대학에서 특정 섹션의 어학실력이 부족하다고 판단될 경우 파견 시 별도로 대학부설 어학코스(ielp) 수강이 필요할 수 있음 [수업료 외 별도,비용발생]', '3', '4.0', '2', NULL, 'http://globalprograms.temple.edu/', '- 타전공 지원 및 수강 가능\n- 다음 전공들은 지원 가능하나 수강신청이 제한적일 수 있음 Architecture, Computer & Information Sciences, Business, Education, Performing Arts (Dance, Music, Theater), Professional Schools (Dentistry, Law, Medicine, Pharmacy, Podiatry), Visual Arts (Film/Media Arts, Graphic Design, Fine Arts, etc), Sport, Tourism and Hospitality Management\n- Business 전공의 경우, 본교 경영학과 학생만 지원가능', 'https://prd-xereg.temple.edu/StudentRegistrationSsb/ssb/term/termSelection?mode=search', 'https://globalprograms.temple.edu/housing', '※ 1개 학기로도 지원 가능\n※ 혼합형은 첫 번째 학기는 템플대학교에 등록금 지불, 두 번째 학기는 인하대에 등록금 지불하는 유형 (2개 학기를 모두 마치고 올 경우에만 두 번쨰 학기에 템플대학교 등록금 면제 및 인하대에 등록금 지불 적용 가능)\n- 한 학기 등록금: 약 $15,432 (out of rate, 관련정보: https://globalprograms.temple.edu/programs/inbound-study-abroad-exchange/costs-dates)\n- 한 학기 기숙사: 약 $5,000 (기숙사 유형에 따라 상이)'), - ('AMERICAS', 'US', '트로이주립대학(A형)', 'Troy University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '61', NULL, NULL, '5.5', NULL, NULL, 'DUOLINGO', '85', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.troy.edu/international/index.html', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)\n- 지원제힌전공: Nursing', 'https://www.troy.edu/academics/catalogs/#Undergraduate', 'https://www.troy.edu/student-life-resources/housing/index.html', NULL), - ('AMERICAS', 'US', '트로이주립대학(B형)', 'Troy University', '4', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '61', NULL, NULL, '5.5', NULL, NULL, 'DUOLINGO', '85', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', '2.5', '4', '2', NULL, 'https://www.troy.edu/international/index.html', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)\n- 지원제힌전공: Nursing', 'https://www.troy.edu/academics/catalogs/#Undergraduate', 'https://www.troy.edu/student-life-resources/housing/index.html', '한 학기 등록금: 약 $9,312 (50% tuition scholarship 지급)'), - ('AMERICAS', 'US', '하와이대학(B형)', 'University of Hawaii at Manoa', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '68', '520', NULL, '6', NULL, NULL, 'DUOLINGO', '96', '- 토플 IBT(100), 토플 ITP(600), IELTS(7.0) DUOLINGO(125) 미만시 파견후 별도 영어시험 필수이며 결과에 따라 1-3개 어학 수업 수강하게 될 수 있음\n- 어학성적은 파견대학 지원시까지 유효하여야 함', '2.5', '4', '2', NULL, 'Mānoa International Exchange – Come to Mānoa, See the World! (hawaii.edu)', '타전공 지원 및 수강 가능', 'https://www.sis.hawaii.edu/uhdad/avail.classes?i=MAN', 'https://manoa.hawaii.edu/mix/inbound/housing-meals/', '등록금: Hoakipa Visiting Student 유형으로 in-state 150% 적용'), - ('AMERICAS', 'BR', '카톨릭 대학 미나스제라이스', 'Pontificia Universidade Catolica de Minas Gerais', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', NULL, '5.5', NULL, NULL, 'DUOLINGO', '95', NULL, NULL, NULL, '2', NULL, 'https://www.pucminas.br/', '- 타전공 지원 및 수강 가능', '※ 영어강의 제공하지 않음, 모든 강의 포르투갈어로 진행\nhttp://www1.pucminas.br/ari/index_padrao.php?pagina=5829', NULL, '교내 기숙사 미제공, International Affairs와 버디프로그램을 통해 교외숙소 계약을 도와줄 예정'), - ('AMERICAS', 'BR', '포르탈레자 대학', 'UNIVERSITY OF FORTALEZA', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', NULL, '5.5', NULL, NULL, 'DUOLINGO', '95', NULL, NULL, NULL, '2', NULL, 'https://www.unifor.br/', '- 타전공 지원 및 수강 가능\n- 지원불가전공 : Medicine', '※ 대부분의 강의 포르투갈어로 진행, 주로 Business Field에 영어수업\nhttps://unifor.br/web/guest/international/exchange-students#tabs', 'https://unifor.br/web/guest/international/exchange-students#tabs', '포르투갈어 어학 수업 수강 가능'), - ('AMERICAS', 'CA', '리자이나대학(A형)', 'University of Regina', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '83', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - TOEFL iBT : 모든 영역에서 20점 이상\n - IELTS : 모든 영역에서 6.0 이상', '2.5', '4.0', '2', NULL, 'https://www.uregina.ca/international/', '- 아래 8개 Faculties 내에서만 수강 가능 : \nArts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science\n- 지원 불가 전공: Nursing, Social work', 'https://banner.uregina.ca/prod/sct/bwckschd.p_disp_dyn_sched', 'https://www.uregina.ca/housing/housing-options/index.html', NULL), - ('AMERICAS', 'CA', '리자이나대학(B형)', 'University of Regina', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '83', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - TOEFL iBT : 모든 영역에서 20점 이상\n - IELTS : 모든 영역에서 6.0 이상', '2.5', '4.0', '2', NULL, 'https://www.uregina.ca/international/', '- 아래 8개 Faculties 내에서만 수강 가능 : \nArts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science\n- 지원 불가 전공: Nursing, Social work', 'https://banner.uregina.ca/prod/sct/bwckschd.p_disp_dyn_sched', 'https://www.uregina.ca/housing/housing-options/index.html', '국제학생 등록금 적용(지원 전공 및 학점에 따라 금액 상이)\n- 관련 링크: https://www.uregina.ca/fs/students/fee-schedule.html'), - ('AMERICAS', 'CA', '메모리얼 대학 세인트존스(A형)', 'Memorial University of Newfoundland St. John\'s', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '115', '영어 점수는 다음의 세부영역 점수를 각각 만족해야함 - TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상 - IELTS : 모든 영역에서 6.0 이상 - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.5', '4.0', '2', NULL, 'https://mun.ca/goabroad/visiting-students-inbound/', '타전공 지원 및 수강 가능 - 지원불가능전공: Medicine, Pharmacy, Social work, Nursing- Computer Science, Music 지원 제한적', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'www.mun.ca/residences', NULL), - ('AMERICAS', 'CA', '메모리얼 대학 세인트존스(B형)', 'Memorial University of Newfoundland St. John\'s', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '79', NULL, NULL, '6.5', NULL, NULL, 'DUOLINGO', '115', '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상\n - IELTS : 모든 영역에서 6.0 이상\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.5', '4.0', '2', NULL, 'https://mun.ca/goabroad/visiting-students-inbound/', '타전공 지원 및 수강 가능 \n- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing\n- Computer Science, Music 지원 제한적', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'www.mun.ca/residences', '국제학생 등록금 적용 (학점당 $2,080)'), - ('AMERICAS', 'CA', '메모리얼대학 그랜펠(어학연수)', 'Memorial University of Newfoundland, Grenfell Campus', '4', 'OVERSEAS_UNIVERSITY_PAYMENT', '1개학기', '40', '440', '500', '4.5', NULL, NULL, NULL, NULL, '※ 가장 기초 과정(IEP-G)의 최소 지원 요건이며 레벨과 지원과정에 따라 지원자격 상이하므로 fact sheet 참조 바람\n- 학부과정 수강 가능한 IEBP-G 지원 시 다음의 세부영역 점수를 만족해야함\n - IELTS : 쓰기 5.5 이상, 모든 영역에서 5.0 이상\n - TOEFL : 쓰기 16점 이상\n- 외국어 성적 유효기간이 파견학기 시작시까지 유효해야함', '2.5', '4.0', '2', NULL, 'www.grenfell.mun.ca/esl', NULL, 'www.grenfell.mun.ca/esl', 'www.grenfell.mun.ca/housing', '선발 학생의 어학성적에 따라 레벨이 정해지며 비용 또한 상이. \n- IEBP-G 레벨에 배정될 경우 학부 수업 1-2개 수강 가능하며 (선택 제한적) 학부수업에 대한 등록금은 면제 (국제처 홈페이지 내 대학 Fact Sheet 참조 바람)'), - ('AMERICAS', 'AU', 'RMIT멜버른공과대학(A형)', 'RMIT University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '60', NULL, NULL, '6', NULL, NULL, NULL, NULL, '어학성적은 파견 학기 지원 마감일까지 유효 하여야함', '2', '4', '2', NULL, 'https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange', '파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)\n- 지원 유의 전공 : Fashion, Media and Communication, Design, Interior Design, Architecture Design\n(참고 : https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange/how-to-search-for-your-courses)', 'https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/study-abroad/study-abroad-exchange-course-search', 'https://www.rmit.edu.au/students/student-life/accommodation', NULL), - ('AMERICAS', 'AU', '서던퀸스랜드대학(A형)', 'University of Southern Queensland', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS: 각 영역 최소 5.5 이상\n- 외국어 성적 유효기간이 파견대학의 지원 시점 기준까지 유효해야함', '4', '7', '2', NULL, 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', '- 타전공 지원 및 수강 가능 \n- 미술 계열, 간호학, 약학, 교육학 등 제한 있음\n- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', '2023 UniSQ Courses', 'https://www.unisq.edu.au/current-students/support/accommodation', '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람'), - ('AMERICAS', 'AU', '서던퀸스랜드대학(B형)', 'University of Southern Queensland', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS: 각 영역 최소 5.5 이상\n- 외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', '4', '7', '2', NULL, 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', '- 타전공 지원 및 수강 가능 \n- 미술 계열, 간호학, 약학, 교육학 등 제한 있음\n- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', '2023 UniSQ Courses', 'https://www.unisq.edu.au/current-students/support/accommodation', '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람\n- In-state 등록금 납부 \n(등록금 관련 정보 : https://www.unisq.edu.au/international/partnerships/study-abroad-exchange/fees-scholarships)'), - ('AMERICAS', 'AU', '시드니대학', 'University of Sydney', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '85', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 모든 영역에서 6.0 이상\n - TOEFL IBT: 읽기/듣기/말하기 17점, 쓰기 19점 이상\n- 어학성적은 파견학기 시작시까지 유효하여야함', '3', '4', '2', NULL, 'https://www.sydney.edu.au/', '타전공 지원 및 수강 가능\n- MECO, CAEL, LAWS unit 수강 여석 제한 있음', 'www.sydney.edu.au/sydney-abroad-units', 'https://www.sydney.edu.au/study/accommodation.html', 'OSHC(Overseas Student Health Cover) 국제학생 보험가입 의무 (2023년 기준 AUD 348/학기, 학기마다 비용 상이)'), - ('AMERICAS', 'AU', '커틴대학(A형)', 'Curtin University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '68', NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 모든 영역에서 6.0 이상\n - TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상\n- 어학성적은 파견학기 시작시까지 유효하여야함', '2.75', '4', '2', NULL, 'Curtin University | Make tomorrow better', '타전공 지원 및 수강 가능\n지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', '※ 24-1학기에 한하여 \'Destination Australia Cheung Kong Exchange Program Scholarship\' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), - ('AMERICAS', 'AU', '커틴대학(B형)', 'Curtin University', '5', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', '68', NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함\n - IELTS: 모든 영역에서 6.0 이상\n - TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 19점 이상\n- 어학성적은 파견학기 시작시까지 유효하여야함', '2.75', '4', '2', NULL, 'Curtin University | Make tomorrow better', '타전공 지원 및 수강 가능\n지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', '한 학기 등록금: 약 10,400 AUD (in state 적용)\n※ 24-1학기에 한하여 \'Destination Australia Cheung Kong Exchange Program Scholarship\' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), - ('EUROPE', 'NL', '그로닝겐 대학교', 'University of Groningen', '4', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, NULL, '6', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상\n - IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, NULL, '2', NULL, 'https://www.rug.nl/feb/education/exchange', 'Faculty of Economics and Business로만 지원가능', 'https://www.rug.nl/feb/education/exchange/incoming/before/courses-exams', 'https://www.rug.nl/feb/education/exchange/incoming/practical-information/accommodation', NULL), - ('EUROPE', 'NL', '삭시온대학교', 'Saxion University of Applied Sciences', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '870', '6', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상\n - IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, NULL, '3', NULL, 'https://www.saxion.edu/programmes/exchange-programme/international-business/course-content', NULL, 'https://www.saxion.edu/programmes#facet_Education%20type=exchange&viewmode=0&page=1', 'https://www.saxion.edu/studying-in-the-netherlands/practical-matters/accommodation/housing-via-saxion', '기숙사 여석 많지않음'), - ('EUROPE', 'NL', '폰티스대학', 'Fontys University of Applied Sciences', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, '6', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://fontys.edu/Short-term-programmes/Exchange-programmes.htm', 'https://fontys.edu/Short-term-programmes/Exchange-programmes/Exchange-programmes-per-faculty.htm', 'https://fontys.edu/Study-at-Fontys/Exchange-programmes.htm#query={%22fields%22:[{%22key%22:%22wilIetsMet%22,%22value%22:[%22Engine%22,%22ICT%22]}],%22keywords%22:%22%22}', 'https://fontys.edu/Study-at-Fontys/Practical-information-1/Arriving-in-The-Netherlands-1/Accommodation.htm', NULL), - ('EUROPE', 'NO', '노르웨이 경영대학', 'BI Norwegian Business School', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, NULL, NULL, NULL, '2', '4.5', '2', NULL, 'www.bi.edu/exchange', '- 경영학 수업만 개설됨\n- 경영학에 대한 사전의 기초적인 과목을 이수하여야 함\n- 한 학기 최대 30ECTS까지 수강 가능함', 'www.bi.edu/exchange', '- 외부숙소 제공\n- https://www.bi.edu/study-at-bi/housing/', NULL), - ('EUROPE', 'DK', '서던덴마크대학교', 'University of Southern Denmark', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '88', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '4', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 1일)', 'https://www.sdu.dk/da', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함\n- 교환학생에게 제공되는 수업만 수강 가능\n- Faculty of Engineering 내에서 2/3이상의 수업을 수강하여야 함\n- 30 ECTS 수강', 'https://www.sdu.dk/en/uddannelse/exchange_programmes', '- 교외 숙소\n-https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing', NULL), - ('EUROPE', 'DK', '코펜하겐 IT대학', 'IT University of Copenhagen', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '88', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', 'https://en.itu.dk/programmes/exchange-students/become-an-exchange-student-at-itu', '- 본교 기초과목 이수사항에 따라 지원이 제한될 수 있으나 소속전공과 정확하게 일치 하지 않아도 지원은 가능(연관 전공이어야 함)\n- 최소 7.5 ECTS, 최대 30ECTS 수강 가능\n- 교차 수강 가능(선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)', 'https://en.itu.dk/Programmes/Exchange-students/Become-an-exchange-student-at-ITU', '- 제공(학교 운영 기숙사 아님) \n- 선착순 배정\n- https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students', NULL), - ('EUROPE', 'DE', '노이울름 대학', 'Neu-Ulm University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', NULL, '785\n(S/W 제출)', '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점\n - TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점\n외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, '2', NULL, 'International - Hochschule Neu-Ulm (hnu.de)', '타전공 지원 및 수강 가능', 'https://www.hnu.de/en/international/international-exchange-students/courses-taught-in-english', 'https://www.hnu.de/fileadmin/user_upload/5_Internationales/International_Incomings/Bewerbung/Housing_Broschure.pdf', NULL), - ('EUROPE', 'DE', '데겐도르프대학', 'Deggendorf University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, '2', NULL, 'www.th-deg.de/exchange', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)\n- 일반적으로 Business, Computer Sciences, Engineering, Tourism Field에서 영어강의를 교환학생에게 제공함', 'https://th-deg.de/exchange-students#course-choices', 'https://www.th-deg.de/en/study-with-us/accommodation', '교환 학생 프로그램에 독일 어학 수업 포함'), - ('EUROPE', 'DE', '라벤스부르크 바인가르텐 응용과학대학교', 'RWU Ravensburg Weingarten\n University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '70', NULL, '785\n(S/W제출)', '6', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- TOEIC: 말하기 160점 이상, 쓰기 150점 이상\n- IELTS: 모든 영역에서 5.5 이상\n- 어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, '2', NULL, 'https://www.rwu.de/en', '타전공 지원 및 수강 가능\n- Language 관련 강의 수강 제한적, Social Faculty의 경우 영어강의 제공하지 않음', 'https://www.rwu.de/en/international/exchange-students/study-and-course-offer', 'https://www.rwu.de/en/international/exchange-students/application', '※ BW Scholarship RWU : Fall term 2023/24 (Sept – Feb) 기간동안 한 달에 €850 장학금 수령 (제출서류 및 기한 등 자세한 정보는 국제처 홈페이지 해외대학정보 공지글 참고)'), - ('EUROPE', 'DE', '로이틀링겐 대학', 'Reutlingen University', '9', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요', NULL, NULL, '2', NULL, 'Hochschule Reutlingen – Reutlingen University: international, praxisnah, unternehmerisch (reutlingen-university.de)', '타전공 지원 및 수강 가능', '각 단과대학별 상이하므로 국제처 홈페이지 해외대학정보 Fact sheet 및 홈페이지 참조 바람', 'https://www.reutlingen-university.de/en/studies/student-life/student-accommodation', NULL), - ('EUROPE', 'DE', '루트비히스하펜 경영사회대학교', 'Ludwigshafen University of Business and Society', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요\n- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '2', NULL, 'https://www.hwg-lu.de/en/', '타전공 지원 및 수강 가능', 'https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/business-courses-in-english-bachelor', 'https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/housing', '보험 관련 정보: https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/health-insurance'), - ('EUROPE', 'DE', '바덴뷔르템베르크 산학협력대학', 'Baden-Wuerttemberg cooperative state univ.(DHBW)', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', '550', '765', '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상\n- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', '2.5', '4.0', '2', NULL, 'https://www.dhbw-stuttgart.de/', '- 타전공 지원 및 수강 가능\n- International Study Programme(ISP) 내 수업만 수강 가능', 'https://www.dhbw-stuttgart.de/studium/internationales/international-students/exchange-students/academic-information/', 'https://www.studierendenwerk-stuttgart.de/en/accommodation/', '기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음'), - ('EUROPE', 'DE', '베를린자유대학교', 'Freie Universitat Berlin', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '- Department of Humanities, Social Science, Business Administration and Economics 수강요건: 독일어 공인성적 B2 레벨 이상의 증빙 필수, Department of Natural Science 수강요건: 독일어 공인성적 B1 레벨 이상 증빙 필수\n- John F Kennedy Institute for North American Studies 수강요건: 영어 공인성적 C1 레벨 이상의 증빙 필수', NULL, NULL, '2', NULL, 'https://www.fu-berlin.de/en/', '타전공 지원 및 수강 가능\n- 지원 불가능 전공: Pharmacy, Human Medicine and Veterinary Medicine\n- Biochemistry, Bioinformatics and Biology, Law 지원 제한적 (학과 사전승인 필요)\n- Business Administration, Economics 학과 수업 대부분이 독일어로 진행, 독일어 가능자 지원 권장', '※ 주로 Departments of English and North American Studies에서 영어강의 제공, 이 외의 학과 영어수업 제한적\nhttps://www.fu-berlin.de/vv/en/fb', 'http://www.fu-berlin.de/en/sites/unterbringung', '- 기숙사 신청 제한적, 여석 부족으로 기숙사 배정을 못 받을 가능성 있음\n- 보험 관련 정보: http://www.fu-berlin.de/en/studium/international/studium_fu/einreise_aufenthalt/krankenversicherung'), - ('EUROPE', 'DE', '뷔르츠부르크-슈바인푸르트 대학', 'Technical University of Applied Sciences Wurzburg-Schweinfurt', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 어학성적 파견 대학 지원시까지 유효하여야함', '2.5', '4', '4', NULL, 'Incoming Exchange students :: University of Applied Sciences Würzburg-Schweinfurt (fhws.de)', '※ Faculty of Economics, Business Administration에 한하여 지원 가능\n- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', 'https://fwiwi.thws.de/en/international/incoming-exchange-students/studying-at-fhws/schedule/', 'https://www.studentenwerk-wuerzburg.de/en/wohnen/move-in-guide.html', '보험 관련 정보: https://international.fhws.de/en/fhws-international/ways-to-fhws/applicants-and-student-support/before-your-arrival-at-fhws/'), - ('EUROPE', 'DE', '슈말칼덴 응용과학대학', 'Hochschule Schmalkalden University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.hs-schmalkalden.de/en.html', '타전공 지원 및 수강 가능', 'https://www.hs-schmalkalden.de/en/international/incoming-students/courses-for-incomings/exchange-students', 'https://www.stw-thueringen.de/en/housing/', '- 기숙사 신청 제한적\n- 보험 관련정보: https://signupbarmer.de/?utm_source=barmer_schmalkalden'), - ('EUROPE', 'DE', '슈투트가르트 공과대학', 'Stuttgart University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, '6', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - IELTS: 모든 영역에서 6.0 이상', NULL, NULL, '2', NULL, 'www.hft-stuttgart.de', '- 타전공 지원 및 수강 가능\n- 지원 불가 전공: International Project Management, Smart City Solution\n- Civil Engineering, Surveying, Mathematic : 독일어 가능자 지원 권장, Architecture, Interior Architecture and General Management : 독일어 수업 수강 필수', 'https://www.hft-stuttgart.com/international/incoming-students/information-application', 'https://www.hft-stuttgart.com/international/incoming-students/services#c15978', '기숙사 신청 제한적'), - ('EUROPE', 'DE', '아샤펜부르크 대학', 'University of Applied Sciences Aschaffenburg', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '70', '500', '780', '5', NULL, NULL, NULL, NULL, '어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '2', NULL, 'www.th-ab.de/eng', '- 타전공 지원 및 수강 가능\n- 지원 불가 전공 : Health, Midwifery, Extra-occupational courses', 'www.th-ab.de/course-offer', 'https://www.studentenwerk-wuerzburg.de/en/aschaffenburg/student-residences.html', NULL), - ('EUROPE', 'DE', '아우크스부르크대학', 'Augsburg University of Applied Sciences', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.hs-augsburg.de/', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함\n- Department of Architecture and Civil Engineering의 경우 영어강의 제공하지 않음', 'https://www.hs-augsburg.de/en/International/Course-Catalogue.html', 'https://www.hs-augsburg.de/Binaries/Binary49994/EN-Guideline-for-finding-accomondation.pdf', NULL), - ('EUROPE', 'DE', '알브슈타트 지그마링엔 대학', 'Albstadt-Sigmaringen University of Applied Science', '4', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, '650', '5.5', NULL, NULL, NULL, NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 영어스피킹 중급 이상 학생 지원 권장\n- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '3', NULL, 'https://www.hs-albsig.de/studieninfos/im-studium/international-office', '타전공 지원 및 수강 가능', 'https://www.hs-albsig.de/fileadmin/user_upload/hsas/International_Office/Courses_in_English_HS_AlbSig.pdf', 'https://www.my-stuwe.de/en/housing/halls-of-residence-albstadt/', '- 독일 어학 수업 수강 필수\n- 독일어 어학성적으로 지원하는 경우에 한하여 2학기로도 지원 가능'), - ('EUROPE', 'DE', '에어랑엔 뉘른베르크 대학', 'University of Erlangen Nuremberg', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '2', NULL, 'Exchange programme students › Friedrich-Alexander-Universität Erlangen-Nürnberg (fau.eu)', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함\n- 지원 불가능 전공: Human medicine, dentistry, pharmacy, law and psychology. Practical sports', 'https://www.campo.fau.de/qisserver/pages/cm/exa/coursecatalog/showCourseCatalog.xhtml?_flowId=showCourseCatalog-flow&_flowExecutionKey=e1s1&noDBAction=y&init=y', 'https://www.fau.eu/education/student-life/accommodation-2/', NULL), - ('EUROPE', 'DE', '오토폰귀릭케마그데부르그 대학', 'Otto von Guericke University of Magdeburg', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', '500', '785', '5', NULL, NULL, 'DUOLINGO', '110', '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 어학성적 파견 대학 지원시까지 유효하여야함', '3', '4', '4', NULL, 'https://www.ovgu.de/en/International/Incoming+_+Ways+to+the+University/International+Students/Exchange+Programmes.html', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함\n- 지원 불가 전공: Medicine', 'https://www.ovgu.de/unimagdeburg/en/International/Incoming+_+Ways+to+the+University/International+Students/Exchange+Programmes/Studying+as+a+WORLDWIDE+Exchange+Student-p-48750.html', 'https://tl1host.eu/SWMD/#admission', NULL), - ('EUROPE', 'DE', '올덴부르크 대학', 'Carl von Ossietzky University of Oldenburg', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '42', NULL, '550\n(S/W 제출)', '4', NULL, NULL, NULL, NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEIC S/W : 말하기 120점 이상, 쓰기 120점 이상\n - 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n - 외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, '2', NULL, 'uol.de/exchange-studies', '- 타전공 지원 및 수강 가능\n- 지원 불가 전공: Medicine, Programmes offered by the Centre for Lifelong Learning', 'https://elearning.uni-oldenburg.de/plugins.php/veranstaltungsverzeichnis_lvsg/englishmodules?vvz_sem_select=e770f053fbe29f2d1bd56a01d0dde1d0', 'https://uol.de/en/exchange-studies/living-in-oldenburg', '보헙관련 정보: https://uol.de/en/exchange-studies/health-insurance'), - ('EUROPE', 'DE', '제플린대학', 'Zepplin University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '90', '543', NULL, '6.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함\n- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '2', NULL, 'https://www.zu.de/', '타전공 지원 및 수강 가능', 'https://zuhause.zeppelin-university.net/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=ACTION&ARGUMENTS=-AXFLjV7~Wgj~xhfmwa1nYvK3AVqeNsVYlJE1s9BJUvPyN3hZATz-SN~fW4BQGvcqQrbg69LM7Vb2PmCS13njtn6vY7wlZ3PR0VVc--5~HKXDORfzpyZYMWO-LB2OopwYkzVvJJ~JUF3g150btYFH8iNVzj12-lBywRT6Aplt7cIeSaUbvmD5Cny-23I6rfUTkzn1OdViRhkbSGv0_', 'https://www.zeppelin-university.com/info-wAssets/universitaet/dokumente/international-office/housing.pdf', NULL), - ('EUROPE', 'DE', '쳄니츠 공과대학', 'Chemnitz University of Technology', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.tu-chemnitz.de/', '타전공 지원 및 수강 가능\n- 지원 제한 전공 : Psychology', 'https://www.tu-chemnitz.de/international/incoming/erasmus/vlvz.php.en', 'https://www.swcz.de/en/student-housing/our-halls-of-residence/', NULL), - ('EUROPE', 'DE', '칼스루에 대학', 'Karlsruhe University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '2', NULL, 'Die HKA - Die Hochschule Karlsruhe:: Homepage (h-ka.de)', '타전공 지원 및 수강 가능', 'https://www.h-ka.de/en/internationalprogram/summer-semester#c33228', 'https://www.h-ka.de/en/accommodation', NULL), - ('EUROPE', 'DE', '하일브론 과학대학교', 'Heilbronn University of Applied Sciences', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '550', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '3', NULL, 'https://www.hs-heilbronn.de/en/incoming-exchange-students', '※ Faculty of Business Administration, Global Finance and Banking, Economics에 한하여 지원 가능\n- Campus Schwäbisch Hall, Campus Künzelsau 내 강의 수강 가능', 'https://cdn.hs-heilbronn.de/4641dc9db5209412/389f4f6cac1d/English-Course-Offer-and-Course-descriptions-Campus-Schw-bisch-Hall_WS-2023-24.pdf', 'https://www.hs-heilbronn.de/accommodation', NULL), - ('EUROPE', 'SE', '말뫼대학', 'Malmo University', '1', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', NULL, 'https://student.mau.se/en/go-international/exchange-studies/', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함\n- 학기당 최소 30ECTS 수강필수\n- 지원불가전공: Nursing/Dentitstry/Odontology 등\n- 교차 수강 가능', 'The previous year\'s courses open for Exchange students : https://mau.se/en/study-education/?r.PagingNumber=1&r.Languages=en&r.Query=&r.TypesSelected=67498&r.Sort=alphabetically', '- 재공하나 보장되는 것은 아님- 1인실 기준으로 4830~5387SEK/1달- https://mau.se/en/education/housing/', NULL), - ('EUROPE', 'SE', '보라스 대학교', 'University College of Boras', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '90', NULL, '775', '6.5', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함- TOEFL iBT: 쓰기 20점 이상- IELTS: 모든 영역에서 5.5이상', NULL, NULL, '4', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', 'https://www.hb.se/en/international-student/exchange-students/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없으나 일치하는 것을 권고함. 본교에서 기초과목을 이수하여야 함- 학기당 최소 30ECTS 수강필수- 교차 수강 가능(단, 선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)- 지원불가 전공 : Fashion Designer, Textile Design', 'https://www.hb.se/exchangecourses', '- 미제공, 외부숙소 지원- 2500~6000 SEK/1달- www.hb.se/en/accommodation', '2024년 봄학기 : 15 January - 2 June 2024- Orientation Days : 11-12 January 2024.'), - ('EUROPE', 'CH', '취리히응용과학기술대학', 'Zurich University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '83', NULL, '785(S/W제출)', '5.5', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함. TOEIC 말하기 160점 이상, 쓰기 150점 이상', '2.5', '4.5', '3', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.zhaw.ch/en/engineering/study/international-office/studying-in-switzerland/#c96360', '-주전공 혹은 제2전공(혹은 연계전공과) 지원 권장', 'https://www.zhaw.ch/storage/engineering/studium/internationales-studium/vom-ausland-in-die-schweiz/spring_semester_zhaw_school_of_engineering.pdf', 'https://www.zhaw.ch/en/study/before-your-studies/student-accommodation/#c72617', NULL), - ('EUROPE', 'ES', '나바라대학교', 'Universidad de Navarra', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, NULL, '5.5', NULL, NULL, NULL, NULL, NULL, '2.5', '4', '3', '지원 전 권역 담당자와 사전상담 요망', 'https://www.unav.edu/web/facultad-de-derecho/estudiantes/programas-de-intercambio/incoming-students', '- School of Law로만지원가능(지원가능전공:international relations, law)- 반드시 24ECTS 이상을 수강하여야 함', NULL, NULL, NULL), - ('EUROPE', 'ES', '마드리드카를로스3세 대학교', 'Universidad Carlos III de Madrid', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '560', '800', '5', NULL, NULL, NULL, NULL, '영어과목이 다양하지 않으므로 신중한 지원 요망. 스페인어 공인어학성적(DELE 중급이상 성적)이 있을 시 추후 합격 후 담당자에게 제출하시길 권장드림(필수아님)', '2.7', '4.0', '3', NULL, 'https://www.uc3m.es/studies/international-exchange-students-in-UC3M-/bachelor-degrees', '- 영어과목이 제한적임.', 'https://www.uc3m.es/studies/international-exchage-students-in-UC3M/bachelor-degrees/course-offer', 'https://www.uc3m.es/studies/international-exchage-students-in-uc3m/bachelor-degrees/Accommodation', '기숙사 여석 많지않음'), - ('EUROPE', 'ES', '예이다대학교', 'University of Lleida', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '75', NULL, '800', '5.5', NULL, NULL, NULL, NULL, 'TOEIC의 경우 S/W 합산 320점 이상 추가로 필수 제출', '2.5', '4', '2', NULL, 'www.udl.cat/ca/serveis/ori/', NULL, 'https://www.udl.cat/ca/serveis/ori/estudiantat_estranger/eng/infoeng/subjects/', 'http://www.udl.cat/ca/serveis/ori/estudiantat_estranger/eng/infoeng/accommodation/', NULL), - ('EUROPE', 'GB', '헐대학', 'University of Hull', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '82', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함 - TOEFL iBT : 듣기 및 쓰기 18점, 읽기 18점, 말하기 20점, 쓰기 18점 이상 - IELTS : 모든 영역에서 6.0이상', '3.5', '4', '4', '지원 전 권역 담당자와 사전상담 요망', 'https://www.hull.ac.uk/choose-hull/study-at-hull/need-to-know/key-dates', '제한학과 많음. (Factsheet참조및Factsheet언급된 제한학과 외에도 학기마다 제한학과 발생가능성있음). 지원 전 권역 담당자랑 사전상담 요망. 학기당 30ECTS수강해야 LA승인남. 성적처리 늦은 편이라 8차 학기 수학자는 성적처리 늦은 거 감안하고 추가 이에 따른 불편함이 있음을 인지후 지원요망.', 'https://universityofhull.app.box.com/s/mpvulz3yz0uijdt68rybce19nek0d8eh', 'https://www.hull.ac.uk/Choose-Hull/Student-life/Accommodation/accommodation.aspx', '영국 생활비 및 숙소비용 유럽권 지역 중 상대적으로 매우 높은편. 지원전 반드시 사전고려 요망'), - ('EUROPE', 'AT', '그라츠 대학', 'University of Graz', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '530', '770', '5.5', NULL, NULL, NULL, NULL, NULL, '2.5', '4', '3', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://international.uni-graz.at/en/incoming-exchange/exchange-students/ > go to "Selecting and Changing Courses"', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', 'https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf', 'https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/', '학교인근 외부 숙소는 있지만, 외부업체운영숙소라 대학관할아님'), - ('EUROPE', 'AT', '그라츠공과대학', 'Graz University of Technology', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', NULL, '785(S/W제출필수)', '6', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함 - TOEFL IBT: 읽기 18점 이상, 쓰기 17점 이상, 말하기 20점 이상, 듣기 17점 이상 - IELTS: 쓰기 5.5점 이상, 말하기 6점 이상 - TOEIC의 경우 S/W 점수 합산 310점 이상', '2.5', '4', '2', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.tugraz.at/fileadmin/user_upload/tugrazInternal/Studium/International_studieren_und_lehren/Mobilitaetsprogramme/OverSEAs_Factsheet.pdf', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', 'https://tugraz.at/go/search-courses', 'https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033', '자체기숙사는 없음. 교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있음(선착순 경우많음). 더블룸 기준약 한달에 € 340 per month (기숙사 종류게 따라 가격 차이 유) 예산잡으면됨.'), - ('EUROPE', 'AT', '린츠 카톨릭 대학교', 'Catholic Private University Linz', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '800', '6', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '3', '봄학기에는 영어과목이 극히 제한적으로 열린다고 함. 지원 전 권역 담당자와 사전상담 요망', 'https://ku-linz.at/en', '- 지원가능전공: History, Philosophy, Art History, theology\n(영어과목 수가 그리 많지는 않으므로, 사전 확인필요)\n - 학기당 최소 15ECTS 수강신청해야 함', 'KULIS - ku-linz.at', '학교에서 몇가지 기숙사 옵션 합격시 연결예정.', NULL), - ('EUROPE', 'AT', '빈 공과대학교', 'University of Applied Sciences Technikum Wien', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '560', '800', '6', NULL, NULL, NULL, NULL, NULL, '2.5', '4', '3', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.technikum-wien.at/international/studierendenmobilitaet-2/', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음 -학기당 최소 15.ECTS 수강신청해야함', 'https://www.technikum-wien.at/en/international/student-mobility/', '기숙사없음', NULL), - ('EUROPE', 'IT', '밀라노공과대학', 'Polytechnic University of Milan', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '84', NULL, NULL, '5', NULL, NULL, NULL, NULL, NULL, '2.5', '4.5', '5', '지원 전 권역 담당자와 사전상담요망. 주로 학부보다 석사과정에 영어교과목이 개설된 편', 'https://www.polimi.it/en/international-prospective-students/laurea-magistrale-programmes-equivalent-to-master-of-science/programme-catalogue/', NULL, NULL, NULL, '양교 교류협약에 따라, Bovisa 캠퍼스로만 지원가능'), - ('EUROPE', 'IT', '베르가모 대학', 'University of Bergamo', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '77', NULL, '820', '5.5', NULL, NULL, NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함\n - TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상\n - IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', '2.5', '4.5', '3', '지원 전 권역 담당자와 사전상담 요망. 기존 파견자 없어서 후기 자료없음.', 'https://en.unibg.it/international/students-exchange', '지원 전 권역 담당자와 사전상담 요망', NULL, NULL, NULL), - ('EUROPE', 'IT', '카포스카리대학교', 'University of Ca\'Poscari', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '73', NULL, '790', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '3', NULL, 'https://www.unive.it/data/9639/?aa=2023&titolo=&periodo=&anno_corso=&ssd=&livello=&cds=&sede=&pagina=', NULL, 'https://www.unive.it/data/9639/?aa=2023&titolo=&periodo=&anno_corso=&ssd=&livello=&cds=&sede=&pagina= you will also find an updated Ecxel file with all the courses listed', NULL, NULL), - ('EUROPE', 'CZ', '오스트라바 대학', 'University of Ostrava', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '67', '515', '650', '5.5', NULL, NULL, NULL, NULL, NULL, '2', '4', '3', '지원 전 권역 담당자와 사전상담 요망', 'https://www.osu.eu', '지원전공과 본인 전공이 일치하지 않아도 지원가능하나 수업따라가기가 어려울 수 있으므로 배경지식이 없다면 지원에 신중할것.. Faculty ofFine Arts/Music/Medicine은 교환학생 지원불가학부임.', 'https://www.osu.eu/22821/courses/', 'https://koleje.osu.eu/', NULL), - ('EUROPE', 'CZ', '체코 생명과학대학', 'Czech University of Life Sciences Prague', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '84', '565', '800', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.czu.cz/en/', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음 -학기당 최소 15.ECTS 수강신청해야함', 'https://www.czu.cz/en/r-9190-international-relations/r-17025-course-offer-academic-year-2023-2024', NULL, '기숙사 입사경쟁률 매우높음'), - ('EUROPE', 'CZ', '프라하공과대학', 'Czech Technical University in Prague', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '78', '550', '800', '5.5', NULL, NULL, NULL, NULL, '- TOEIC R/C 최소 385점 이상 TOIEC L/C 최소 400점 이상', '2.5', '4.5', '2', NULL, 'https://international.cvut.cz/students/incoming-students/erasmus-and-exchange/', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음\n-학기당 최소 15.ECTS 수강신청해야함', 'https://legacy.mobility.cvut.cz/prospectus/2023/index.php', 'https://www.suz.cvut.cz/cz', '기숙사 월260유로 정도임'), - ('EUROPE', 'PT', '리스본대학 공과대학', 'INSTITUTO SUPERIOR TECNICO-Universidade de Lisboa', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '75', '540', '785', '5.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '3', NULL, NULL, '지원 전 권역 담당자와 사전상담 요망', 'http://www.catolicabs.porto.ucp.pt/en/international-mobility', '-기숙사 없음.', NULL), - ('EUROPE', 'PT', '포르투갈 가톨릭대학', 'Catholic University of Portugal', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', '550', '800', '5.5', NULL, NULL, NULL, NULL, NULL, '2.5', '4', '2', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', 'https://www.catolicabs.porto.ucp.pt/en/catolicabs-porto', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', 'https://catolicabs.porto.ucp.pt/international-programmes-0', NULL, NULL), - ('EUROPE', 'FR', 'EBS Paris', 'EBS Paris', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '530', '600', '5.5', NULL, NULL, NULL, NULL, NULL, '2', '4', '4', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', 'https://www.ebs-paris.fr/', '- 지원전공이 본교 소속 전공 또는 제2전공과 일치하여야 함 : 경영학 과목만 개설됨- 교차 수강 가능- 최소 9, 최대 36ECTS 수강신청 가능', NULL, '- 미제공', '- 2024년 봄학기 : 2024년 1월 ~ 4월'), - ('EUROPE', 'FR', 'EPITA', 'EPITA', '7', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, '750', '6', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '4', NULL, 'https://sway.office.com/u5VHBu9DbBH6Mr8F?ref=Link', '- 지원 가능 전공 : IT계열(컴퓨터공학)- 교차 수강 불가- 학기당 최소 15ECTS, 최대 30ECTS 수강', 'https://sway.office.com/u5VHBu9DbBH6Mr8F', '- 미제공하나 교환학생들이 외부 숙소를 찾을 수 있도록 지원- http://housing.epitamasters.com', NULL), - ('EUROPE', 'FR', 'EPITECH', 'EPITECH(L\'echole de L\'expertise Informatique)', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '65', '543', '600', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', '- 어학성적표가 2024년 11월 1일까지 유효하여야 함', 'https://international.epitech.eu/', '- 지원가능전공: Computer Science / Information technology(소속전공과 지원전공이 일치하여야 함)- 교차수강 불가 - 학기당 최소 10ECTS, 최대 30ECTS 수강', 'https://international.epitech.eu/', '- 미제공하나 교환학생들이 외부 숙소를 찾을 수 있도록 지원- https://international.epitech.eu/student-life/', NULL), - ('EUROPE', 'FR', 'ESCE 국제경영대학', 'ESCE International Business School', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', NULL, NULL, NULL, 'CEFR', 'B2', NULL, '2.5', '4', '6', NULL, 'http://www.esce.fr/international/', '- 지원가능전공: 경영학 계열(소속전공과 지원전공 일치) : - 교차 수강 가능- 학기당 최소 15ECTS, 최대 34ECTS 수강', NULL, '- 미제공- 숙소관련 문의 : gdesforges@omneseducation- 관련 링크 : https://www.studapart.com/en/studapart/student-accommodation?gad=1&gclid=Cj0KCQjw7uSkBhDGARIsAMCZNJvmPVROiqUSiAO69ks7UWWXcIKO1rGO_hXPmjlx72qsV1SF1VrEzRkaAjTQEALw_wcB', NULL), - ('EUROPE', 'FR', 'ESSCA경영대학', 'Ecole Superieure des Sciences Commerciales d\'Anger', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', '550', '800', '5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 30일)', 'https://www.essca.fr/en/international/exchange-student', '- 소속전공과 지원전공이 일치할 것을 권고함 : 경영학\n- 15ECTS 수강', 'https://pcee.azurewebsites.net', '- 미제공', NULL), - ('EUROPE', 'FR', 'IPSA', 'IPSA', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '65', '500', '700', '4.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '4', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', 'https://www.ipsa.fr/en/engineering-school/aeronautical-space', '- 소속전공과 지원전공이 일치 또는 유사하여야 함 : 전공이 제한적이므로 반드시 홈페이지에서 지원 가능 전공을 확인할 것\n- 최대 30ECTS 수강', NULL, '- 미제공\n- https://www.ipsa.fr/en/student-life/pratical-information/', NULL), - ('EUROPE', 'FR', 'ISEP', 'ISEP', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '79', '543', '785', '6.5', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '4', '- 어학성적표가 해당 대학 개강일까지 유효해야 함', 'http://en.isep.fr/studying-at-isep/exchange-students', '- 소속전공과 지원전공이 일치하여야 함\n- 교차수강 가능\n- 최소 15ECT, 최대 33ECTS까지 수강', 'https://en.isep.fr/studying-at-isep/course-catalog/', '- 미제공\n- 외부숙소 배정에 대해 지원', NULL), - ('EUROPE', 'FR', 'KEDGE경영대학', 'KEDGE Business School', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', '550', '785', '6', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 15일)', 'https://student.kedge.edu', '- 지원가능전공: 경영학계열\n- 학기당 최소 15ECTS, 최소 30~35ETS 수강해야 함\n(Program에 따라 상이함)', 'https://student.kedge.edu/exchange-programmes/academic-information', '- 미제공\n- https://student.kedge.edu/student-services/prepare-my-studies-abroad/student-accommodation-in-france', NULL), - ('EUROPE', 'FR', 'NEOMA경영대학', 'NEOMA Business School', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', NULL, 'https://neoma-bs.com/', '- 지원가능전공 : 경영학계열(소속대학에서 경영학 관련 기초 과목을 이수하여야 함)\n- 학기당 최소 20ECTS, 최대 30ECTS 수강', 'https://neoma-bs.com/welcome-to-neoma/steps/step-4-courses/', '- 교외 숙소 제공\n- 한달에 250~600€\n- https://neoma-bs.com/welcome-to-neoma/steps/step-3-housing/', NULL), - ('EUROPE', 'FR', '국제정치대학', 'HEIP', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', NULL, NULL, NULL, 'CEFR', 'B2', NULL, '2.5', '4', '6', NULL, 'www.heip.fr/', '- 소속 전공과 지원전공이 일치해야 함\n- 교차수강 가능\n- 최소 15ECTS, 최대 30ECTS 수강신청 가능', NULL, '- 미제공\n- 숙소관련 문의 : gdesforges@omneseducation\n- 관련 링크 : https://www.studapart.com/en/studapart/student-accommodation?gad=1&gclid=Cj0KCQjw7uSkBhDGARIsAMCZNJvmPVROiqUSiAO69ks7UWWXcIKO1rGO_hXPmjlx72qsV1SF1VrEzRkaAjTQEALw_wcB', NULL), - ('EUROPE', 'FR', '노르망디경영대학', 'EM NORMANDIE BUSINESS SCHOOL', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', NULL, '750', '5.5', NULL, NULL, NULL, NULL, '* 수강 과정에 따라 영어 성적이 상의하니 유의할 것\n- UNDERGRADUATE: IBT 72/ TOEIC 750/ IELTS 5.5 \n- GRADUATE: IBT 83/ TOEIC 790/ IELTS 6.0', NULL, NULL, '60ECTS 이상 이수해야 함', '- 어학성적표가 해당 대학 신청서 제출 시 유효해야 함(~10월 31일)\n- 최저 성적요건은 프로그램에 따라 상이함(별도 문의할 것)\n- 최저이수학기 : 2개 학기 이상 이수하고, 60ECTS 이상 취득하여야 함(세부 문의는 국제교류팀으로 연락)', 'https://www.em-normandie.com/fr', '- 개설전공 : 경영경상계열, 물류(소속전공과 지원전공이 일치할 것을 권고)\n- 전공간 교차수강 불가\n- 최소 수강학점 : 15ECTS', 'https://en.em-normandie.com/em-normandie-experience/open-world-studying-abroad/exchange-programmes', '- 미제공\n- https://en.em-normandie.com/em-normandie-experience/open-world-studying-abroad/exchange-programmes', NULL), - ('EUROPE', 'FR', '라로쉘경영대학', 'La Rochelle Business School', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '70', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.excelia-group.com/student-services', '- 소속전공과 지원전공이 반드시 일치할 필요는 없으나 기초과목을 이수해야 함\n- 최대 30ECTS 수강', 'https://www.excelia-group.com/student-services/international-students/exchange-students/programmes-details', '- 미제공\n- https://excelia-group.studapart.com/en/', '2024 봄학기 : January - April/May \n(depending on the program of study)'), - ('EUROPE', 'FR', '렌 경영대학', 'ESC Rennes School of Business', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '120ECTS 이상 이수해야 함\n(비고란 참조)', '-어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 20일)\n- 최저이수학기 : 120ECTS 이상 취득하여야 함(세부 문의는 국제교류팀으로 연락)', 'https://www.rennes-sb.com/programmes/exchange-programme/', '- 지원가능전공: 경영학계열(소속전공과 지원전공이 일치 또는 유사하여야 함, 기초과목을 이수해야 함)- 교차수강 불가- 최소 15에서 최대 34ECTS(프로그램에 따라 학기당 수강 가능 ECTS가 상이함)', 'https://www.rennes-sb.com/programmes/exchange-programme/incoming-exchange-students/', '- 미제공- 숙소 예약에 대한 가이드 제공- https://www.rennes-sb.com/student-life/accommodation-health/', NULL), - ('EUROPE', 'FR', '르아브르대학', 'University of Le Havre', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', NULL, 'https://www.univ-lehavre.fr', '- 교차수강 가능- 학기당 최소 15 ECTS 수강', '추후 이메일로 안내 예정', '- 기숙사제공(CROUS)- https://www.univ-lehavre.fr/spip.php?article70', NULL), - ('EUROPE', 'FR', '릴 가톨릭 대학', 'Lille Catholic University', '5', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, '2.75', '4', '2', NULL, 'http://www.univ-catholille.fr/', '- 주전공과 지원전공이 일치하여야 함- 최대 두 개의 establishment에서 수강 가능- 지원불가능전공: Medicine, Midwifery, Nursing, Physiotherapy, Chiropody, Law, Digital animations and Video gamrs / 2nd year of Master- 최소 20ECTS 수강', 'https://www.univ-catholille.fr/sites/default/files/fichiers/VF%20Catalogue%20cours%20en%20anglais%202022-2023_0.pdf', '- 기숙사 제공(선착순)- https://www.all-lacatho.fr/en/', NULL), - ('EUROPE', 'FR', '릴 가톨릭 대학(ESTICE)', 'Lille Catholic University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '70', '540', '780', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', '어학성적이 2024년 1월까지 유효해야 함', 'http://www.univ-catholille.fr/', '- 경영대학과 릴 카톨릭대학 ESTICE와의 별도 협약에 따라 경영대학 소속 학생에 한하여 선발함- 주전공과 지원전공이 일치할 것을 권고함(필수는 아님)- ESTIC에서 개설된 교과목만 수강 가능- 최소 12~최대 32ECTS 수강', 'https://estice.fr/programs/', '- 미제공- https://www.all-lacatho.fr/en/list-accommodation', NULL), - ('EUROPE', 'FR', '몽펠리에 대학교', 'Universite Montpellier 1', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '3', NULL, 'https://www.umontpellier.fr/university-of-montpell', '- 소속대전공과 지원전공이 일치할 필요 없음- 교차수강 불가하며 교확학생에게 오픈된 교과목만 수강 가능', 'https://iae.umontpellier.fr/en/institut/exchange-students', '- 제공(선착순)- 1200EUR/1학기-https://iae.umontpellier.fr/en/institut/accommodation', NULL), - ('EUROPE', 'FR', '발드센느대학', 'Ecole d\'Architecture, Paris Val de Seine', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'French language test', 'B1', '- 프랑스어 성적 제출이 필수는 아니나 대부분의 과목이 프랑스어로 진행되므로 프랑스어를 사전에 공부할 것을 권고함', NULL, NULL, '2', NULL, 'https://www.paris-valdeseine.archi.fr/', '- 지원가능전공: 건축학부 재학생에 함함', 'https://www.paris-valdeseine.archi.fr/fileadmin/mediatheque/document/International/etudiants_entrants/English_speaking_classes.pdf', '- 미제공', NULL), - ('EUROPE', 'FR', '부르군디 경영대학', 'Burgundy School of Business', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '750', '6', NULL, NULL, 'CEFR\n\n\nDuolingo', 'B1\n\n\n80', NULL, NULL, NULL, '4', '- 어학성적표가 11월 30일까지 유효하여야 함\n- 최저이수학기 관련 : "How to choose my courses" : https://international.bsb-education.com/course-catalogues/?lang=en 참조', 'https://www.bsb-education.com/', '- 서로 다른 프로그램, 언어, 학과 교차 수강 불가능\n- 제한 전공 : https://international.bsb-education.com/course-catalogues/?lang=en', 'https://international.bsb-education.com/course-catalogues/?lang=en', '- 학교 직영 기숙사는 없으나 CROUS, STUDAPART과 제휴한 숙소 제공(선착순)\n- https://www.studapart.com/fr', NULL), - ('EUROPE', 'FR', '오덴시아 낭트 경영대학', 'Audencia Business School', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '3', NULL, 'https://apply.exchangestudents.audencia.com/', '- 경영학계열, 소속 전공과 지원전공이 반드시 일치할 필요는 없으나 관련 기초과목을 이수하여야 함\n- 학기당 15~30ECTS 수강', 'https://apply.exchangestudents.audencia.com/index.cfm?FuseAction=Abroad.ViewLink&Parent_ID=FE40C425-5056-BA1F-74632B3DF27C287E&Link_ID=0365089E-9F2E-9844-AB9F3ABC4462019B', '- 미제공\n- https://www.expatistan.com/cost-of-living/nantes', '- https://international.audencia.com/student-life/before-you-arrive'), - ('EUROPE', 'FR', '장물랭리옹3세대학교', 'Universite Jean Moulin Lyon 3', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', NULL, '1020', '6.5', NULL, NULL, NULL, NULL, '- 영어점수는 아래 각 세부점수를 만족해야 함\n - TOEFL IBT: 모든 영역 20점 이상 \n - IELTS: 모든 영역에서 6 이상 \n - TOEIC: 말하기, 쓰기, 듣기, 읽기 영역 합산 1020점 이상', NULL, NULL, '3', NULL, 'https://www.univ-lyon3.fr/', '- 소속대 전공과 지원전공이 일치할 필요는 없으나 관련 기초과목을 이수하여야 함\n- 1년간 최소 8학점(프랑스어 5학점, 프랑스문화 3학점) 이상 이수\n- 교차수강 가능(2nd year of master law, LL.M제외)', 'https://www.univ-lyon3.fr/self-study-in-english-in-lyon-france', '- 외부숙소제공\n- https://associnterlyon3.fr/en/', NULL), - ('EUROPE', 'FR', '툴루즈 경영대학교', 'Toulouse Business school', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '79', NULL, NULL, '6', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 26일)\n* 최저이수학기\n- Bachelor Year 2과정 : 최소 2개 학기 이상 이수\n- Bachelor Year 3과정 : 최소 4개 학기 이상 이수', 'www.tbs-education.fr/en', '- 지원가능전공: 경영경상계열(소속대학 전공이 관련 학과여야 함)\n- 하나의 프로그램 내에서 수강 가능\n- 최소 24ECTS 수강', 'https://www.tbs-education.com/about-tbs/international/incoming-exchange-students/', 'https://www.tbs-education.com/about-tbs/student-services/', NULL), - ('EUROPE', 'FR', '툴루즈정치대학', 'Sciences Po Toulouse', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', 'http://www.sciencespo-toulouse.fr/home-english-version-589930.kjsp', '- 소속대 전공과 지원전공이 반드시 일치할 필요는 없으나 관련 기초과목을 이수하여야 함', 'https://www.sciencespo-toulouse.fr/en/courses/university-diploma-in-international-comparative-studies', '- 미제공', NULL), - ('EUROPE', 'FR', '파리8대학교', 'University of Paris 8', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, 'DELF', 'B2', NULL, '3', '4', '2', NULL, 'www.univ-paris8.fr/en/', '- 소속 전공과 지원 전공이 일치하여야 함\n- 교차 수강 가능\n- 최대 학기당 30ECTS 수강 가능', 'https://www.univ-paris8.fr/-Etudes-diplomes-', '- 제공(단, 제한적이며 선착순 배정)\n- 1600EUR/학기당\n- https://www.univ-paris8.fr/-Informations-pratiques-', NULL), - ('EUROPE', 'FI', 'LAB대학', 'Lahti University of Applied Sciences', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', NULL, NULL, '6', NULL, NULL, 'CEFR', 'B2', NULL, NULL, NULL, '2', NULL, 'https://www.lab.fi/en/exchange-student-guide/studies', '- 소속 전공과 지원전공이 일치해야 함 : 경영학, 경영정보공학으로 지원 가능 \n- 기초지식이 있는 경우에 한하여 교차수강 가능함(Design 및 Fine arts는 전공 학생만 수강 가능)\n- 학기당 30 ECTS 수강 권장', 'https://opinto-opas.lab.fi/70064/en/70060/70065?lang=en', '- 미제공\n- https://lab.fi/en/exchange-student-guide', '- 기존 "라티대학"이 "LAB대학"으로 교명 변경'), - ('EUROPE', 'FI', '라펜란타기술대학교', 'Lappeenranta University of Technology', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', '460', '785', '6', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '4', '- 어학성적표가 해당 대학 개강일까지 유효해야 함', 'https://www.lut.fi/en/studies/exchange-studies', '- 소속전공과 지원전공이 일치해야 함\n- 최소 수강 학점 20ECTS, 30ECTS 수강 권장', 'https://www.lut.fi/en/studies/exchange-studies/courses-exchange-students', '- 미제공\n- 교환학생의 경우 LOAS(www.loas.fi)를 통해 숙소를 신청함\n- 290~420EUR/1달\n- Lappeenranta Student Housing Foundation (LOAS) www.loas.fi/en', ''), - ('EUROPE', 'FI', '투르크 응용과학대학', 'Turku University of Applied Sciences', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, '', 'CEFR', 'B2', NULL, NULL, NULL, '3', NULL, 'https://www.tuas.fi/en/study-tuas/exchange-students/about/', '- 지원가능전공: Faculty of Engineering and Business 내 전공 지원 가능(우리대학의 경우 공과대학, 경영대학, 소프트융합대학 소속 학생만 신청 가능)\n- 소속대 전공과 지원전공이 일치해야 함\n- 학기당 최대 30ECTS 수강신청 가능', 'https://www.tuas.fi/en/study-tuas/exchange-students/courses/', '- 미제공\n- 한달에 280~370€\n- https://www.tuas.fi/en/study-tuas/exchange-students/accommodation/', NULL), - ('EUROPE', 'FR', '트루아 공과대학', 'Universite de Technologie de Troyes', '2', 'HOME_UNIVERSITY_PAYMENT', '1년만 가능', '42', '460', '550', '4', NULL, NULL, 'CEFR', 'B1', NULL, NULL, NULL, '2', NULL, 'https://www.utt.fr/study-at-utt/academic-programs', '- 소속전공과 지원전공이 일치 또는 유사하여야 함\n- 교차 수강 가능\n- 한 학기 30ECTS를 수강 권고', 'https://www.utt.fr/study-at-utt/courses-in-english', '- 미제공\n- https://www.utt.fr/study-at-utt/accommodation', NULL), - ('ASIA', 'BN', '브루나이 국립대학', 'Universiti Brunei Darussalam', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '80', '550', '750', '6', NULL, NULL, NULL, NULL, '-TOEFL IBT minimum Scores :\n Reading 18, Writing 23, Listing 18, Speaking 19\n- 영어성적은 교환학생을 하는 내내 유효한 성적표여야 함', '3', '5', '2', NULL, 'www.ubd.edu.bn/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음\n- 교차 수강 가능', NULL, '- 기숙사 보유(한학기에 250BND)', '- COVID 관련 안내 사이트 https://www.pmo.gov.bn/TAPressRelease/[FINAL]%20PR%20JKPC%2012023%20-%20Pengemaskinian%20Sukat-Sukat%20Pengawalan%20COVID-19.pdf\n-수업방식: 온라인, 오프라인 혼합'), - ('ASIA', 'SG', '싱가폴경영대학', 'Singapore Management University', '5', 'HOME_UNIVERSITY_PAYMENT', '무관', '93', NULL, NULL, '7', NULL, NULL, NULL, NULL, '- 유효한 영어공인인증시험점수는 2021년 9월 26일 이후 응시한 시험점수에 한함\n- 어학점수 미보유 시 어학요건 기준에 준하는 어학실력을 보유하고 있다는 내용을 담은 지도교수 추천서를 제출하여 어학점수 갈음 가능', NULL, NULL, '2', NULL, 'https://publiceservices.smu.edu.sg/psc/ps/EMPLOYEE/HRMS/c/SIS_CR.SIS_CLASS_SEARCH.GBL?&', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음\n- 교차 수강 가능', 'https://publiceservices.smu.edu.sg/psc/ps/EMPLOYEE/HRMS/c/SIS_CR.SIS_CLASS_SEARCH.GBL?&', '기숙사 없음', NULL), - ('ASIA', 'AZ', '아다대학교', 'ADA University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '605', '5.5', NULL, NULL, NULL, NULL, '- 유효한 영어공인인증시험점수는 2021년 11월 1일 이후 응시한 시험점수에 한함', '2', '4', '2', NULL, 'https://www.ada.edu.az/frq-content/plugins/policies_x1/entry/20220704132757_85764100.pdf', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음\n- 교차 수강 가능\n-지원불가 전공: Master of Science in Computer Science & Data Analytics and Master of Science in Electrical & Power Engineering', 'https://www.ada.edu.az/frq-content/plugins/policies_x1/entry/20221226165811_72744800.pdf', '- 기숙사 보유 (한학기700-950 USD)', '-COVID 관련 안내 사이트 \nhttps://koronavirusinfo.az/az'), - ('ASIA', 'ID', '비누스대학', 'BINUS University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', '550', NULL, '6', NULL, NULL, NULL, NULL, 'IELTS overall band score of 6\n- 유효한 영어공인인증시험점수는 2022년 3월 1일 이후 응시한 시험점수에 한함', '2.75', '4', '2', NULL, 'https://bit.ly/binusexchange', '- 캠퍼스별 수강 가능 전공 다 다름 (매학기 변동 가능)\n- 아래 캠퍼스 별 수강 가능 전공 참조\n-BINUS International Senayan Campus: Fashion, Business, Computer Science, Accounting, Graphic Design and New Media, Communication, Business Information System, \n\n-BINUS Kemanggisan: Hotel Management, Tourism, Computer Science, Accounting, Civil & Industrial Engineering, Management, Marketing communication, Business Law, English Literature, Architecture\n\n-BINUS Alam Sutera: International Business Management, Computer Science, International Relations, Food Technology\n\n-BINUS Bekasi: Bar & Hotel Management', 'https://linktr.ee/binusexchange', '- 기숙사 보유 (교환학생들은 도착 후 Limited Stary Permit이 나오는 한달동안 반드시 기숙사에서 거주해야함)\n- 2인실 한달에 USD240, 1인실 한달에 USD280\n-https://binus.ac.id/binussquare/', NULL), - ('ASIA', 'JP', '가나자와대학', 'Kanazawa University', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', NULL, '700', '5.5', NULL, 'N2', NULL, NULL, NULL, NULL, NULL, '2', 'College of Science and Engineering or College of Medical, Pharmaceutial and Health Sciences의 경우 3학년 이상만 신청 가능', 'https://kuglobal.w3.kanazawa-u.ac.jp/eg/sie/program/short-term-exchange-programs-at-kanazawa-university-2023-2024/', 'https://kuglobal.w3.kanazawa-u.ac.jp/wp/wp-content/uploads/KUEP2022-20231.pdf', 'https://eduweb.sta.kanazawa-.ac.jp/portal/Public/Syllabus/SearchMain.aspx', 'https://sgu.adm.kanazawa-u.ac.jp/international/wp-content/uploads/2022/04/03_GetStarted2022_upload.pdf', NULL), - ('ASIA', 'JP', '가쿠슈인대학', 'Gakushuin University', '1', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N4', NULL, NULL, 'Faculty of International social Sciences에 한해 영어점수 필요(TOEF L IBT 80, IELTS 6.0)\n-2년 이상의 일본어 공부 이력도 신청 가능', NULL, NULL, '2', NULL, 'https://www.univ.gakushuin.ac.jp/iss/en/program/registration.html', '지원불가 : Professional School of Law', NULL, 'WAKEIJUKU https://www.wakei.org/english/ KITAZONO WOMEN\'S STUDENT https://www.kitazono-j.co.jp/ CAMPUS VILLAGE KOTAKEMUKAIHARA https://749.jp/cd/2455/ CAMPUS VILLAGE AKATSUKASHINMACHI https://749.jp/cd/2442/', NULL), - ('ASIA', 'JP', '긴다이대학', 'Kindai University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '61', '500', NULL, '6', NULL, 'N2', NULL, NULL, NULL, '2.5', '4', '2', NULL, 'https://www.kindai.ac.jp/english/files/study-at-kindai/prospective.pdf', '- 학부생만 지원 가능\n- Higashiosaka campus만 지원 가능(약학과 지원불가)\n- 영어과정은 International Studies 또는 Business 계열만 가능', NULL, NULL, '기숙사 없음, 학교밖 숙소-약 270,000엔/학기'), - ('ASIA', 'JP', '니가타대학', 'Niigata University', '9', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, '교환학생 입학 기준은 별도로 없으나, 영어 일본어 교과목 수강을 위해서는 기본 언어능력 필요(일본어의 경우 N2)', NULL, NULL, '2', '2.30 points *NU 환산표 참조 : https://www.niigata-u.ac.jp/en/wp-content/uploads/2020/12/method.pdf', 'https://www.niigata-u.ac.jp/en/study/exchange/', NULL, 'https://www.niigata-u.ac.jp/en/study/exchange/', 'https://www.niigata-u.ac.jp/en/study/life/housing/', NULL), - ('ASIA', 'JP', '도요대학', 'Toyo University', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', NULL, '1530', '5.5', NULL, 'N3', NULL, NULL, 'TOEIC:TOEIC L&R+(TOEIC S&W*2.5)\n정규교과목 수강은 N2이상', '2.5', '4', '2', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/', '-Hakusan Campus에서만 수강 가능, \n-교환학생을 위한 교과목만 수강 가능 https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/#acl', NULL, 'https://www.toyo.ac.jp/contents/international-exchange/residence/index.php', NULL), - ('ASIA', 'JP', '도요대학', 'Toyo University', '3', 'HOME_UNIVERSITY_PAYMENT', '1년', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, NULL, '2.5', '4', '4', '*해당 학교 일정 상 10월초까지 서류제출 필요3+1 프로그램 신청자 대상으로 하는 별도 장학금 수혜 가능', 'https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/', 'Hakusan Campus에서만 수강 가능', NULL, 'https://www.toyo.ac.jp/contents/international-exchange/residence/index.php', '- [3+1 프로그램]만 지원 가능 (현지 취업을 위한 맞춤형 프로그램, 파견차수 학기 유의할 것)- 3+1 프로그램은 6차, 7차 학기를 일본대학에서 수학하면서 현지에서 직장을 구한 후 인하대에서 마지막 8차 학기 이수하고 일본으로 돌아가 직장생활을 하는 프로그램임'), - ('ASIA', 'JP', '돗쿄대학', 'Dokkyo University', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N4', NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.dokkyo.ac.jp/english/exchange/calendar/syllabus.html', NULL, NULL, 'https://www.dokkyo.ac.jp/english/exchange/student/accommodation.html', '기숙사 미보유, 교환학생을 위한 외부 숙소 보장- 280,000엔~330,000엔/학기 (2023년 기준)'), - ('ASIA', 'JP', '메이지대학', 'Meiji University', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, '2', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', NULL, 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL), - ('ASIA', 'JP', '바이카여자대학', 'BAIKA Women\'s University', '1', NULL, '무관', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, NULL, '2.3', '4', '2', '여학생만 신청가능', NULL, '교환학생 지원가능 : Department of Global English, Department of Japanese culture, Department of Media and Information, Department of Psychology.', NULL, 'https://dormy-ac.com/page/baika/', '기숙사 없음, 계약된 외부 기숙사 사용-“Maison de Claire Ibaraki” 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)'), - ('ASIA', 'JP', '분쿄가쿠인대학', 'Bunkyo Gakuin University', '3', 'HOME_UNIVERSITY_PAYMENT', '1년만 가능', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, NULL, NULL, NULL, '2', NULL, 'https://www.bgu.ac.jp/', NULL, NULL, NULL, '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월'), - ('ASIA', 'JP', '야마구치대학', 'Yamaguchi University', '10', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Graduate School of Ecomnomics 토플 79 이상 토익 730이상 필요', NULL, NULL, '2', '- 전공별 TO에 따라 선발 (최대인원을 넘지 않도록)\n- Faculty of Global and Science Studies : 최대 5명 \n- Faculty of Economics: 최대 8명\n- Faculty of Engineering: 최대 3명\n- Faculty of Humanities: 최대 2명\n- Others: 최대 2명', 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', NULL, '-', '교환학생은 off-campus dormitory 거주의무'), - ('ASIA', 'JP', '오사카가쿠인대학', 'Osaka Gakuin University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/schedule.html', NULL, 'https://www.ogu.ac.jp/english/int_exchange/ie_program/syllabi.html', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/housing.html', '-봄학기는 한 개 학기만 교환학생 파견 가능\n-교환학생 용 "International Exchange Program" 제공 (일본어 능력이 높은 학생에게만 오후에 제공되는 일반 과정 수강 가능)'), - ('ASIA', 'JP', '오츠마여자대학', 'Otsuma Women\'s University', '3', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, NULL, NULL, NULL, '2', '여학생만 신청가능', NULL, NULL, NULL, 'https://www.otsuma.ac.jp/english/international/dormitory.html', '36,500엔/월, 식비별도'), - ('ASIA', 'JP', '와세다대학', 'Waseda University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '730', '6.5', NULL, 'N1', NULL, NULL, '학부별로 기준 상이, 관련페이지 참조', '3', '4', '2', NULL, 'https://www.waseda.jp/inst/cie/en/exchange/application', 'Please refer to "Requirements and Course Lists" for further information. https://www.waseda.jp/inst/cie/en/exchange/application', NULL, 'https://www.waseda.jp/inst/rlc/en/student_dormitory/exchange/', 'https://www.waseda.jp/inst/rlc/en/student_dormitory/exchange/'), - ('ASIA', 'JP', '추오대학', 'Chuo University', '1', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, 'N2', NULL, NULL, 'Commerce / Science & Engineering/ Global Informatics', '2.5', '4', '2', NULL, 'https://www.chuo-u.ac.jp/english/admissions/exchange/semester-or-full-year/', '지원불가 : Professional Graduate Program (Law School, Business School)', NULL, 'https://www.chuo-u.ac.jp/english/admissions/residences/', 'Seiseki-Sakuragaoka dormitory (off-campus)는 꽤 경쟁이 치열함. On-campus와 Off-campus 모두 약 1,800-1,900 EUR/학기'), - ('ASIA', 'JP', '치바대학', 'Chiba University', '2', 'HOME_UNIVERSITY_PAYMENT', '1년만 가능', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'https://www.le.chiba-u.jp/(Japanese only)', '경제학, 국제통상학과만 지원 가능', NULL, 'https://www.chiba-u.ac.jp/international/isd/en/index.html', NULL), - ('ASIA', 'TR', '앙카라대학', 'ANKARA UNIVERSITY', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 유효한 영어공인인증시험점수는 2021년 10월 16일 이후 응시한 시험점수에 한함', '2.75', '4', '2', NULL, 'https://en.ankara.edu.tr/', '- 소속전공과 지원전공이 일치할 것을 권장함', 'iso.ankara.edu.tr', '- 기숙사 보유 (한달에 550 TL ~ 2000 TL)', '- 오프라인 수업으로 예상하나 온라인 수업 등으로 변동 가능성 있음- COVID 관련 안내 사이트 https://covid19.saglik.gov.tr/'), - ('ASIA', 'TR', '외즈예인대학교', 'Ozyegin University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '*TOEFL IBT minimum Scores : Reading 20, Writing 20, Listing 20, Speaking 20유효한 영어공인인증시험점수는 2021년 12월 1일 이후 응시한 시험점수에 한함https://www.ozyegin.edu.tr/en/student-services/application-admission/language-proficiency-requirement', NULL, NULL, '2', NULL, 'https://www.ozyegin.edu.tr/en', '-소속전공과 지원전공이 일치할것-학부 학생이 석사용 수업 수강할 수 없음', 'https://www.ozyegin.edu.tr/en/ects-course-catalog-courses-offered/courses-offered', '-기숙사 보유하나 입사 보장은 불가 (https://www.ozyegin.edu.tr/en/dormitories/housing-fees)', '-COVID 관련 안내 사이트 https://www.ozyegin.edu.tr/en/covid-19'), - ('ASIA', 'HK', '수인대학교', 'Shue Yan University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, '6', NULL, NULL, NULL, NULL, 'IELTS overall band score of 6 with no band lower than 5.5', '2.5', '4', '2', NULL, 'https://iu.hksyu.edu/inbound-exchange/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음- 교차 수강 가능', 'https://iu.hksyu.edu/wp-content/uploads/2022/08/CourseListForExchangeStudents2022-23-20220816.pdf', '- 기숙사 2인실(매달 263-302 USD)', '-COVID 관련 안내 사이트 https://www.coronavirus.gov.hk/eng/index.html'), - ('ASIA', 'HK', '홍콩시립대학', 'City Univ Hong Kong', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6.5', NULL, NULL, NULL, NULL, 'ielts overall band 6.5-유효한 영어공인인증시험점수는 2021년 10월 2일 이후 응시한 시험점수에 한함', NULL, NULL, '2', NULL, 'https://www.admo.cityu.edu.hk/exchange_visiting/exchange/info/', '-소속전공과 지원전공이 일치할것- College of Science(Chemistry/ Mathematics / Physics) 이 학과로 홍콩시립대에서 수강 시 위 학과에서 9학점 이상 이수 필수 수강을 해야함-지원제한 전공: Biomedical science-몇몇 수강인원이 제한된 학과는 교환학생을 받지 않음 주의-선행 과목 수강이 필수인 학과의 경우 수강 이력이 없을 시 수강 불가', 'https://www.cityu.edu.hk/admo/exchange/exchange_course_list_202402.pdf', '- 기숙사가 있으나 기숙사 배정이 상당히 어려움- 대략적 비용: 학기당 HKD$ 10,850 ~ HKD$ 21,700https://www.cityu.edu.hk/sro/StudentHousing/UGHalls/InboundExchangeStudents.htm', '- COVID 관련 안내 사이트 https://www.cityu.edu.hk/fmo/default.aspx?PageID=covid19infoctr'), - ('CHINA', 'TW', '국립정치대학교', 'National Chengchi University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, '700', '6', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', '2.75', '4', '2', NULL, 'https://oic.nccu.edu.tw/Post/833', '- 소속전공과 지원전공이 일치하여야 함- 수강 제한 전공: IMBA, EMBA, IMAS', 'https://qrysub.nccu.edu.tw/', '- 기숙사 보유 - 대략적 비용: 한학기에 NTD$11,000~ 33,500', NULL), - ('CHINA', 'TW', '국립중산대학', 'National Sun Yat-sen University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '72', '543', '785', '5.5', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함', NULL, NULL, '2', NULL, 'https://oia.nsysu.edu.tw/p/412-1308-20581.php?Lang=en', '-소속전공과 지원전공이 일치할것-중국어 실력이 유창하지 않으면 영어로 강의되는 수업만 수강 가능함- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', 'https://oia.nsysu.edu.tw/p/412-1308-20770.php?Lang=en', '- 기숙사 보유 (한학기235 USD-535 USD)https://oia.nsysu.edu.tw/p/412-1308-20581.php?Lang=en', '-COVID 관련 안내 사이트 https://www.cdc.gov.tw/En'), - ('CHINA', 'TW', '국립중앙대학교', 'National Central University', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', '550', '750', '6', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함- 영어성적은 지원일로부터 2년간 유효한 성적표여야 함', '70', '100', '4', NULL, 'http://oia.ncu.edu.tw/index.php/en/international-students/incoming-exchange-programs.html', '- 소속전공과 지원전공이 일치하여야 함- 지원가능 전공 확인 : https://www.ncu.edu.tw/en/pages/index.php?num=2- 교환학생용으로 제공되는 영어강의는 본래 대학원생용이라 학부 3학년 또는 4학년 학생이 국립중앙대로의 교환학생에 지원할 수 있음', 'https://cis.ncu.edu.tw/Course/main/news/announce', '기숙사 없음', '-COVID 관련 안내 사이트 https://www.cdc.gov.tw/En'), - ('CHINA', 'TW', '타이베이시립대학', 'University of Taipei', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', '72', '543', '785', '5.5', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함', NULL, NULL, '2', NULL, 'https://international.utaipei.edu.tw/index.php?Lang=en', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음- 교차 수강 가능- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', NULL, '- 기숙사 보유- 한 학기에 약 500-800$', '-COVID 관련 안내: 병원과 관련된 시설에서는 반드시 마스크 착용 해야함'), - ('CHINA', 'CN', '강남대학교', 'Jiangnan University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '59', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함- 중국어 연수 프로그램 : 영어,중국어 성적이 없어도 지원 가능- 학위과정수업(영어) : IBT 59 이상', '2.8', '4', '2', NULL, 'http://www.jiangnan.edu.cn/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음- 교차 수강 가능- 주로 인하대 학생들은 중국어 어학과정을 수강함- 최소 수강학점은 없으며, 주로 3~6과목 (2~4학점/1과목)을 수강함', 'English taught courses are generally determined by the schools at the end of the previous semester. We are not sure about it until then.', '- 학교에 기숙사가 있으나 입사 보장은 불가International Students Building (only for foreign students): Single room with bedding packs, air-conditioner and bathroom,500RMB Local Student Dormitory: 4-6 person room without bedding packs, 600-1000RMB', NULL), - ('CHINA', 'CN', '길림대학교', 'Jilin University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '79', NULL, NULL, '6', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함* 중국어 과정1) 중국어학연수과정 : 어학성적 필요 없음2) Diplomacy (School of International and Public Affairs) : 유효한 HSK 5급 180점 이상, TOEFL 550(PBT), 213(CBT), 79(IBT) 또는 IELTS 6.0.이상을 보유하여야 함(HSK및영어 동시만족)3) 중국어 과정(석사):HSK 5급 180점 이상4) 기타 중국어 과정 : HSK 4급 180점 이상 * 영어 과정 : 길림대 내 영어테스트가 있을 예정임- 영어과정을 들을 경우, 영어성적표을 제출하여야 하며 지원일로부터 2년간 유효한 성적표여야 함', NULL, NULL, '2', NULL, 'http://cie.jlu.edu.cn/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음- 지원제한 전공 : Medical Science', NULL, '- 기숙사 보유 (6개월 이상 거주 시 신청 가능)- 대략적 비용: 한달에 1500RMB', NULL), - ('CHINA', 'CN', '남경사범대학교', 'Nanjing Normal University', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함- 중국어로 된 전공 강의를 수강하기 희망할 경우, 최소 HSK 5급 180점 이상의 점수를 보유하여야 함-중국어학연수과정 : 어학성적 필요 없음', '75', '100', '2', NULL, 'http://gjc.njnu.edu.cn/index.htm', '- 소속전공과 지원전공이 일치하여야함(교차수강 불가능)- 대부분의 교환학생들은 중국어 어학 수업을 수강함', '- 영어수업을 제공할지 결정되지 않았음', '- 기숙사 보유 (한학기 5000RMB-6000RMB: 겨울방학 및 여름방학 제외)', NULL), - ('CHINA', 'CN', '동화대학교', 'Donghua University', '2', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, '6', NULL, NULL, NULL, NULL, '- 유효한 영어공인인증시험점수는 2021년 11월 16일 이후 응시한 시험점수에 한함- 본교 중국어 어학시험에 응시하여야 함', NULL, NULL, '2', NULL, 'http://english.dhu.edu.cn', '소속전공과 지원전공이 반드시 일치할 필요는 없으나 학과에 상관없이 교차 수강은 불가능-교환학생 대상으로 별도 제공된 수강 가능 교과목안에서만 수강 가능', 'https://english.dhu.edu.cn/incoming/list.htm', '- 기숙사 보유(한 학기 약 7800yuan)- https://korean.dhu.edu.cn/accommodation/list.htm', NULL), - ('CHINA', 'CN', '산동대학교(위해)', 'Shandong University, Weihai', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함 - 중국어 수업을 들을 학생은 HSK4급 보유를 권장함- 영어 수업을 들을 학생은 TOEFL IBT 80점, IELTS 6등급 보유를 권장함- 학점 4.0 만점에 2.3, 100점 만점에 70이상인 학생만 지원 가능', '2.3', '4', '2', NULL, 'https://en.wh.sdu.edu.cn/', '소속전공과 지원전공이 일치해야 함. 단, 중국어 어학과정 학생의 경우 전공 무관- 최소 학점은 없으며, 최대 24학점 수강 가능- 체육교육과 전공은 제공하지 않음- 지원 가능 전공 https://ipo.wh.sdu.edu.cn/kristudy/info/1021/1752.htm', NULL, '- 기숙사 보유 (한학기 4,000~5,000 RMB, 금액은 변동가능성있음)', NULL), - ('CHINA', 'CN', '연태대학교', 'Yantai University', '2', 'HOME_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함\n- 중국어 어학강좌만 수강할 경우 HSK 또는 영어성적 제시 불필요- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK level 4 이상이어야 함)', '2.5', '4', '2', NULL, 'https://en.ytu.edu.cn/', '- 소속전공과 지원전공이 일치하여야 함 (교차 수강 불가능)', NULL, '- 기숙사 보유(한 학기 3100-3600RMB)', NULL), - ('CHINA', 'CN', '절강사범대학교', 'Zhejiang Normal University', '3', 'HOME_UNIVERSITY_PAYMENT', '1개학기', '80', NULL, NULL, '5.5', NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함\n- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK4급이상 보유자만 가능)\n-유효한 영어공인인증시험점수는 2021년 12월 21일 이후 응시한 시험점수에 한함', '2.5', '4', '2', NULL, 'http://iso.zjnu.edu.cn/ywb/main.htm', '- 소속전공과 지원전공이 일치할 것', 'http://iso.zjnu.edu.cn/wistwofwwrogramsw2018/list.htm', '-기숙사 보유 (2인실/ 매달 450-600RMB)', NULL), - ('CHINA', 'CN', '중앙민족대학교', 'Minzu University of China', '1', 'HOME_UNIVERSITY_PAYMENT', '1개학기', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 본교 중국어 어학시험에 응시하여야 함\n- 중국어 연수 프로그램 : 어학성적 불필요\n- 영어강의 없고 중국어로 하는 전공수업만 제공함. HSK5급 보유자만 지원가능(확인중)', '2.5', '4', '2', NULL, 'https://oir.muc.edu.cn/', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음', NULL, '- 기숙사 보유 (한학기 1100-1500 USD)', NULL), - (NULL, NULL, 'SAF 프로그램', 'SAF Program', '10', 'OVERSEAS_UNIVERSITY_PAYMENT', '무관', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '- 어학요건이 대학별로 상이하므로 반드시 Program Guide 참고할 것', NULL, NULL, NULL, NULL, 'http://korea.studyabroadfoundation.org/', NULL, NULL, NULL, '- 지원 전 반드시 국제교류팀 담당자와 상담할 것'); \ No newline at end of file + ('TW', 'CHINA'); \ No newline at end of file From 10f39688f35587b2066d33d6ae17fb264fec9dc5 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 5 Feb 2024 21:17:44 +0900 Subject: [PATCH 026/158] =?UTF-8?q?refactor:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/UniversityInfoForApply.java | 3 - .../type/LanguageTestType.java | 2 +- .../type/SemesterAvailableForDispatch.java | 2 +- src/main/resources/data.sql | 308 +++++++++++++++++- 4 files changed, 309 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java b/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java index 5582ebd3c..0c6638dc6 100644 --- a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java +++ b/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java @@ -12,9 +12,6 @@ public class UniversityInfoForApply { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(length = 10) - private String semester; - @Column(nullable = false) private Integer studentCapacity; diff --git a/src/main/java/com/example/solidconnection/type/LanguageTestType.java b/src/main/java/com/example/solidconnection/type/LanguageTestType.java index 0d80fc019..b7c064747 100644 --- a/src/main/java/com/example/solidconnection/type/LanguageTestType.java +++ b/src/main/java/com/example/solidconnection/type/LanguageTestType.java @@ -1,5 +1,5 @@ package com.example.solidconnection.type; public enum LanguageTestType { - TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DOULINGO, CEFR + TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF } diff --git a/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java index 973fe28ec..6510496e0 100644 --- a/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java +++ b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java @@ -2,10 +2,10 @@ public enum SemesterAvailableForDispatch { ONE_SEMESTER("1개학기"), + FOUR_SEMESTER("4개학기"), ONE_YEAR("1년만 가능"), IRRELEVANT("무관"); - private final String koreanName; SemesterAvailableForDispatch(String koreanName) { diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 0fecd6bd8..4b9200a05 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -31,4 +31,310 @@ INSERT INTO country (country_code, region_code) VALUES ('FR', 'EUROPE'), ('FI', 'EUROPE'), ('CN', 'CHINA'), - ('TW', 'CHINA'); \ No newline at end of file + ('TW', 'CHINA'); + +INSERT INTO university +(country_code, region_code, english_name, format_name, korean_name, accommodation_url, english_course_url, homepage_url, details_for_local, logo_image_url, background_image_url) +VALUES + ('US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학(A형)', 'https://www.uog.edu/life-at-uog/residence-halls/', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/admissions/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png'), + ('US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학(B형)', 'https://www.uog.edu/life-at-uog/residence-halls/', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/admissions/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png'), + ('US', 'AMERICAS', 'University of Nevada, Las Vegas', 'university_of_nevada_las_vegas', '네바다주립대학 라스베이거스(B형)', 'https://www.unlv.edu/housing', 'https://www.unlv.edu/engineering/academic-programs', 'https://www.unlv.edu/engineering/eip', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png'), + ('US', 'AMERICAS', 'University of Nevada, Las Vegas', 'university_of_nevada_las_vegas', '네바다주립대학 라스베이거스(어학연수)', 'https://www.unlv.edu/housing', 'https://www.unlv.edu/engineering/academic-programs', 'https://www.unlv.edu/engineering/eip', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png'), + ('US', 'AMERICAS', 'University of Nebraska at Kearney', 'university_of_nebraska_at_kearney', '네브라스카 주립대학(A형)', 'https://www.unk.edu/offices/reslife/index.php', 'https://catalog.unk.edu/undergraduate/', 'https://www.unk.edu/index.php', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nebraska_at_kearney/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nebraska_at_kearney/1.png'), + ('US', 'AMERICAS', 'University of Nebraska at Kearney', 'university_of_nebraska_at_kearney', '네브라스카 주립대학(B형)', 'https://www.unk.edu/offices/reslife/index.php', 'https://catalog.unk.edu/undergraduate/', 'https://www.unk.edu/index.php', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nebraska_at_kearney/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nebraska_at_kearney/1.png'), + ('US', 'AMERICAS', 'North Park University', 'north_park_university', '노스파크대학(A형)', 'https://www.northpark.edu/campus-life-and-services/residence-life-and-housing/ㅍ', 'https://paygate.northpark.edu:8173/Student/Courses', 'https://www.northpark.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/north_park_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/north_park_university/1.png'), + ('US', 'AMERICAS', 'North Park University', 'north_park_university', '노스파크대학(B형)', 'https://www.northpark.edu/campus-life-and-services/residence-life-and-housing/', 'https://paygate.northpark.edu:8173/Student/Courses', 'https://www.northpark.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/north_park_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/north_park_university/1.png'), + ('US', 'AMERICAS', 'SUNY ESF', 'suny_esf', '뉴욕주립대 환경과학임학대학(A형)', 'https://www.esf.edu/housing/', 'https://www.esf.edu/catalog/courses/', 'https://www.esf.edu/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/suny_esf/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/suny_esf/1.png'), + ('US', 'AMERICAS', 'SUNY ESF', 'suny_esf', '뉴욕주립대 환경과학임학대학(B형)', 'https://www.esf.edu/housing/', 'https://www.esf.edu/catalog/courses/', 'https://www.esf.edu/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/suny_esf/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/suny_esf/1.png'), + ('US', 'AMERICAS', 'Stony Brook University', 'stony_brook_university', '뉴욕주립대학 스토니브룩(B형)', 'https://www.stonybrook.edu/commcms/studentaffairs/res/', 'https://www.stonybrook.edu/sb/bulletin/current/courses/browse/byabbreviation/', 'https://www.stonybrook.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/stony_brook_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/stony_brook_university/1.png'), + ('US', 'AMERICAS', 'New Jersey City Univeristy', 'new_jersey_city_univeristy', '뉴저지시티대학(A형)', 'https://www.njcu.edu/admissions-aid/international-students/international-student-housing', 'https://www.njcu.edu/admissions-aid/international-students/information-accepted-students/choosing-classes', 'https://www.njcu.edu/admissions-aid/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/new_jersey_city_univeristy/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/new_jersey_city_univeristy/1.png'), + ('US', 'AMERICAS', 'New Jersey City Univeristy', 'new_jersey_city_univeristy', '뉴저지시티대학(B형)', 'https://www.njcu.edu/admissions-aid/international-students/international-student-housing', 'https://www.njcu.edu/admissions-aid/international-students/information-accepted-students/choosing-classes', 'https://www.njcu.edu/admissions-aid/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/new_jersey_city_univeristy/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/new_jersey_city_univeristy/1.png'), + ('US', 'AMERICAS', 'Arkansas State University', 'arkansas_state_university', '아칸소주립대학(A형)', 'https://www.astate.edu/a/university-housing/housing-options/', 'https://ssb-prod.ec.astate.edu/PROD/bwckschd.p_disp_dyn_sched', 'https://www.astate.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/arkansas_state_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/arkansas_state_university/1.png'), + ('US', 'AMERICAS', 'Arkansas State University', 'arkansas_state_university', '아칸소주립대학(B형)', 'https://www.astate.edu/a/university-housing/housing-options/', 'https://ssb-prod.ec.astate.edu/PROD/bwckschd.p_disp_dyn_sched', 'https://www.astate.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/arkansas_state_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/arkansas_state_university/1.png'), + ('US', 'AMERICAS', 'Angelo State University', 'angelo_state_university', '안젤로주립대학(A형)', 'https://www.angelo.edu/dept/residential_programs', 'https://ssb.angelo.edu/prod/bwwkschd.p_disp_dyn_sched', 'https://www.angelo.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/angelo_state_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/angelo_state_university/1.png'), + ('US', 'AMERICAS', 'Angelo State University', 'angelo_state_university', '안젤로주립대학(B형)', 'https://www.angelo.edu/dept/residential_programs', 'https://ssb.angelo.edu/prod/bwwkschd.p_disp_dyn_sched', 'https://www.angelo.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/angelo_state_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/angelo_state_university/1.png'), + ('US', 'AMERICAS', 'The University of Alabama in Huntsville', 'the_university_of_alabama_in_huntsville', '앨러배마헌츠빌대학 (A형)', 'https://www.uah.edu/housing', 'https://catalog.uah.edu/undergrad/course-descriptions/', 'https://www.uah.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/the_university_of_alabama_in_huntsville/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/the_university_of_alabama_in_huntsville/1.png'), + ('US', 'AMERICAS', 'The University of Alabama in Huntsville', 'the_university_of_alabama_in_huntsville', '앨러배마헌츠빌대학 (B형)', 'https://www.uah.edu/housing', 'https://catalog.uah.edu/undergrad/course-descriptions/', 'https://www.uah.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/the_university_of_alabama_in_huntsville/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/the_university_of_alabama_in_huntsville/1.png'), + ('US', 'AMERICAS', 'Illinois Institute of Technology', 'illinois_institute_of_technology', '일리노이공과대학(교환학생 과정)', 'https://www.iit.edu/housing/housing-options/housing-rates', 'http://bulletin.iit.edu/undergraduate/courses/', 'https://www.iit.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/illinois_institute_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/illinois_institute_of_technology/1.png'), + ('US', 'AMERICAS', 'Illinois Institute of Technology', 'illinois_institute_of_technology', '일리노이공과대학(복수학위 과정)', 'https://www.iit.edu/housing/housing-options/housing-rates', 'http://bulletin.iit.edu/undergraduate/courses/', 'https://www.iit.edu/admissions-aid/undergraduate-admission/international-undergraduate-students/how-apply-international-undergraduate-students/international-transfer-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/illinois_institute_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/illinois_institute_of_technology/1.png'), + ('US', 'AMERICAS', 'Taylor university', 'taylor_university', '테일러대학', 'https://www.taylor.edu/life-at-taylor/residence-life/residence-halls/', 'https://www.taylor.edu/offices/registrar/class-schedules', 'https://www.taylor.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/taylor_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/taylor_university/1.png'), + ('US', 'AMERICAS', 'Temple University', 'temple_university', '템플대학(혼합형)', 'https://globalprograms.temple.edu/housing', 'https://prd-xereg.temple.edu/StudentRegistrationSsb/ssb/term/termSelection?mode=search', 'http://globalprograms.temple.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/temple_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/temple_university/1.png'), + ('US', 'AMERICAS', 'Troy University ', 'troy_university', '트로이주립대학(A형)', 'https://www.troy.edu/student-life-resources/housing/index.html', 'https://www.troy.edu/academics/catalogs/#Undergraduate', 'https://www.troy.edu/international/index.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/troy_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/troy_university/1.png'), + ('US', 'AMERICAS', 'Troy University ', 'troy_university', '트로이주립대학(B형)', 'https://www.troy.edu/student-life-resources/housing/index.html', 'https://www.troy.edu/academics/catalogs/#Undergraduate', 'https://www.troy.edu/international/index.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/troy_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/troy_university/1.png'), + ('US', 'AMERICAS', 'University of Hawaii at Manoa', 'university_of_hawaii_at_manoa', '하와이대학(B형)', 'https://manoa.hawaii.edu/mix/inbound/housing-meals/', 'https://www.sis.hawaii.edu/uhdad/avail.classes?i=MAN', 'https://www.hawaii.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hawaii_at_manoa/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hawaii_at_manoa/1.png'), + ('BR', 'AMERICAS', 'Pontificia Universidade Catolica de Minas Gerais', 'pontificia_universidade_catolica_de_minas_gerais', '카톨릭 대학 미나스제라이스', NULL, 'http://www1.pucminas.br/ari/index_padrao.php?pagina=5829', 'https://www.pucminas.br/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/pontificia_universidade_catolica_de_minas_gerais/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/pontificia_universidade_catolica_de_minas_gerais/1.png'), + ('BR', 'AMERICAS', 'UNIVERSITY OF FORTALEZA', 'university_of_fortaleza', '포르탈레자 대학', 'https://unifor.br/web/guest/international/exchange-students#tabs', 'https://unifor.br/web/guest/international/exchange-students#tabs', 'https://www.unifor.br/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_fortaleza/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_fortaleza/1.png'), + ('CA', 'AMERICAS', 'University of Regina', 'university_of_regina', '리자이나대학(A형)', 'https://www.uregina.ca/housing/housing-options/index.html', 'https://banner.uregina.ca/prod/sct/bwckschd.p_disp_dyn_sched', 'https://www.uregina.ca/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_regina/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_regina/1.png'), + ('CA', 'AMERICAS', 'University of Regina', 'university_of_regina', '리자이나대학(B형)', 'https://www.uregina.ca/housing/housing-options/index.html', 'https://banner.uregina.ca/prod/sct/bwckschd.p_disp_dyn_sched', 'https://www.uregina.ca/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_regina/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_regina/1.png'), + ('CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스(A형)', 'https://www.mun.ca/residences/', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'https://mun.ca/goabroad/visiting-students-inbound/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png'), + ('CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스(B형)', 'www.mun.ca/residences', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'https://mun.ca/goabroad/visiting-students-inbound/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png'), + ('CA', 'AMERICAS', 'Memorial University of Newfoundland, Grenfell Campus', 'memorial_university_of_newfoundland_grenfell_campus', '메모리얼대학 그랜펠(어학연수)', 'www.grenfell.mun.ca/housing', 'https://www.grenfell.mun.ca/e니', 'https://mun.ca/grenfellcampus/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_grenfell_campus/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_grenfell_campus/1.png'), + ('AU', 'AMERICAS', 'RMIT University', 'rmit_university', 'RMIT멜버른공과대학(A형)', 'https://www.rmit.edu.au/students/student-life/accommodation', 'https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/study-abroad/study-abroad-exchange-course-search', 'https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/rmit_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/rmit_university/1.png'), + ('AU', 'AMERICAS', 'University of Southern Queensland', 'university_of_southern_queensland', '서던퀸스랜드대학(A형)', 'https://www.unisq.edu.au/current-students/academic/residential-schools/accommodation', 'https://www.unisq.edu.au/course/specification/current/', 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/1.png'), + ('AU', 'AMERICAS', 'University of Southern Queensland', 'university_of_southern_queensland', '서던퀸스랜드대학(B형)', 'https://www.unisq.edu.au/current-students/support/accommodation', 'https://www.unisq.edu.au/course/specification/current/', 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/1.png'), + ('AU', 'AMERICAS', 'University of Sydney', 'university_of_sydney', '시드니대학', 'https://www.sydney.edu.au/study/accommodation.html', 'www.sydney.edu.au/sydney-abroad-units', 'https://www.sydney.edu.au/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_sydney/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_sydney/1.png'), + ('AU', 'AMERICAS', 'Curtin University', 'curtin_university', '커틴대학(A형)', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/1.png'), + ('AU', 'AMERICAS', 'Curtin University', 'curtin_university', '커틴대학(B형)', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/1.png'), + ('NL', 'EUROPE', 'University of Groningen', 'university_of_groningen', '그로닝겐 대학교', 'https://www.rug.nl/feb/education/exchange/incoming/practical-information/accommodation', 'https://www.rug.nl/feb/education/exchange/incoming/before/courses-exams', 'https://www.rug.nl/feb/education/exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_groningen/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_groningen/1.png'), + ('NL', 'EUROPE', 'Saxion University of Applied Sciences ', 'saxion_university_of_applied_sciences', '삭시온대학교', 'https://www.saxion.edu/studying-in-the-netherlands/practical-matters/accommodation/housing-via-saxion', 'https://www.saxion.edu/programmes', 'https://www.saxion.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saxion_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saxion_university_of_applied_sciences/1.png'), + ('NL', 'EUROPE', 'Fontys University of Applied Sciences ', 'fontys_university_of_applied_sciences', '폰티스대학', 'https://fontys.edu/Study-at-Fontys/Practical-information-1/Arriving-in-The-Netherlands-1/Accommodation.htm', 'https://www.fontys.nl/en/Study-at-Fontys/Exchange-programmes', 'https://www.fontys.nl/en/Home.htm', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/fontys_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/fontys_university_of_applied_sciences/1.png'), + ('NO', 'EUROPE', 'BI Norwegian Business School', 'bi_norwegian_business_school', '노르웨이 경영대학', 'https://www.bi.edu/study-at-bi/housing/', 'https://www.bi.edu/programmes-and-individual-courses/exchange-programme/', 'https://www.bi.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bi_norwegian_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bi_norwegian_business_school/1.png'), + ('DK', 'EUROPE', 'University of Southern Denmark', 'university_of_southern_denmark', '서던덴마크대학교', 'https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing', 'https://www.sdu.dk/en/uddannelse/exchange_programmes', 'https://www.sdu.dk/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/1.png'), + ('DK', 'EUROPE', 'IT University of Copenhagen', 'it_university_of_copenhagen', '코펜하겐 IT대학', 'https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students', 'https://en.itu.dk/Programmes/Exchange-students/Become-an-exchange-student-at-ITU', 'https://en.itu.dk/programmes/exchange-students/become-an-exchange-student-at-itu', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/1.png'), + ('DE', 'EUROPE', 'Neu-Ulm University of Applied Sciences', 'neu-ulm_university_of_applied_sciences', '노이울름 대학', 'https://www.hnu.de/fileadmin/user_upload/5_Internationales/International_Incomings/Bewerbung/Housing_Broschure.pdf', 'https://www.hnu.de/en/international/international-exchange-students/courses-taught-in-english', 'https://www.hnu.de/en/international', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neu-ulm_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neu-ulm_university_of_applied_sciences/1.png'), + ('DE', 'EUROPE', 'Deggendorf University of Applied Sciences', 'deggendorf_university_of_applied_sciences', '데겐도르프대학', 'https://www.th-deg.de/en/study-with-us/accommodation', 'https://th-deg.de/exchange-students#course-choices', 'www.th-deg.de/exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/deggendorf_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/deggendorf_university_of_applied_sciences/1.png'), + ('DE', 'EUROPE', 'RWU Ravensburg Weingarten University of Applied Sciences ', 'rwu_ravensburg_weingarten_university_of_applied_sciences', '라벤스부르크 바인가르텐 응용과학대학교', 'https://www.rwu.de/en/international/exchange-students/application ', 'https://www.rwu.de/en/international/exchange-students/study-and-course-offer', 'https://www.rwu.de/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/rwu_ravensburg_weingarten_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/rwu_ravensburg_weingarten_university_of_applied_sciences/1.png'), + ('DE', 'EUROPE', 'Reutlingen University', 'reutlingen_university', '로이틀링겐 대학', 'https://www.reutlingen-university.de/en/studies/student-life/student-accommodation', NULL, 'https://www.reutlingen-university.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/reutlingen_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/reutlingen_university/1.png'), + ('DE', 'EUROPE', 'Ludwigshafen University of Business and Society', 'ludwigshafen_university_of_business_and_society', '루트비히스하펜 경영사회대학교 ', 'https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/housing', 'https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/business-courses-in-english-bachelor', 'https://www.hwg-lu.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ludwigshafen_university_of_business_and_society/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ludwigshafen_university_of_business_and_society/1.png'), + ('DE', 'EUROPE', 'Baden-Wuerttemberg cooperative state univ.(DHBW)', 'baden-wuerttemberg_cooperative_state_univ(dhbw)', '바덴뷔르템베르크 산학협력대학', 'https://www.studierendenwerk-stuttgart.de/en/accommodation/', 'https://www.dhbw-stuttgart.de/studium/internationales/international-students/exchange-students/academic-information/', 'https://www.dhbw-stuttgart.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baden-wuerttemberg_cooperative_state_univ(dhbw)/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baden-wuerttemberg_cooperative_state_univ(dhbw)/1.png'), + ('DE', 'EUROPE', 'Freie Universitat Berlin', 'freie_universitat_berlin', '베를린자유대학교', 'http://www.fu-berlin.de/en/sites/unterbringung', 'https://www.fu-berlin.de/vv/en/fb ', 'https://www.fu-berlin.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/freie_universitat_berlin/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/freie_universitat_berlin/1.png'), + ('DE', 'EUROPE', 'Technical University of Applied Sciences Wurzburg-Schweinfurt', 'technical_university_of_applied_sciences_wurzburg-schweinfurt', '뷔르츠부르크-슈바인푸르트 대학', 'https://www.studentenwerk-wuerzburg.de/en/wohnen/move-in-guide.html', 'https://fwiwi.thws.de/en/international/incoming-exchange-students/studying-at-fhws/schedule/', 'https://fwiwi.thws.de/#', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/technical_university_of_applied_sciences_wurzburg-schweinfurt/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/technical_university_of_applied_sciences_wurzburg-schweinfurt/1.png'), + ('DE', 'EUROPE', 'Hochschule Schmalkalden University of Applied Sciences', 'hochschule_schmalkalden_university_of_applied_sciences', '슈말칼덴 응용과학대학', 'https://www.stw-thueringen.de/en/housing/', 'https://www.hs-schmalkalden.de/en/international/incoming-students/courses-for-incomings/exchange-students', 'https://www.hs-schmalkalden.de/en.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/hochschule_schmalkalden_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/hochschule_schmalkalden_university_of_applied_sciences/1.png'), + ('DE', 'EUROPE', 'Stuttgart University of Applied Sciences', 'stuttgart_university_of_applied_sciences', '슈투트가르트 공과대학', 'https://www.hft-stuttgart.com/international/incoming-students/services#c15978', 'https://www.hft-stuttgart.com/international/incoming-students/information-application', 'https://www.hft-stuttgart.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/stuttgart_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/stuttgart_university_of_applied_sciences/1.png'), + ('DE', 'EUROPE', 'University of Applied Sciences Aschaffenburg', 'university_of_applied_sciences_aschaffenburg', '아샤펜부르크 대학', 'https://www.studentenwerk-wuerzburg.de/en/aschaffenburg/student-residences.html', 'https://www.th-ab.de/en/education/exchange-students/course-offer', 'https://www.th-ab.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_aschaffenburg/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_aschaffenburg/1.png'), + ('DE', 'EUROPE', 'Augsburg University of Applied Sciences', 'augsburg_university_of_applied_sciences', '아우크스부르크대학', 'https://www.hs-augsburg.de/Binaries/Binary49994/EN-Guideline-for-finding-accomondation.pdf ', 'https://www.hs-augsburg.de/en/International/Course-Catalogue.html', 'https://www.hs-augsburg.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/augsburg_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/augsburg_university_of_applied_sciences/1.png'), + ('DE', 'EUROPE', 'Albstadt-Sigmaringen University of Applied Science', 'albstadt-sigmaringen_university_of_applied_science', '알브슈타트 지그마링엔 대학', 'https://www.my-stuwe.de/en/housing/halls-of-residence-albstadt/', 'https://www.hs-albsig.de/fileadmin/user_upload/hsas/International_Office/Courses_in_English_HS_AlbSig.pdf', 'https://www.hs-albsig.de/studieninfos/im-studium/international-office', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/albstadt-sigmaringen_university_of_applied_science/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/albstadt-sigmaringen_university_of_applied_science/1.png'), + ('DE', 'EUROPE', 'University of Erlangen Nuremberg', 'university_of_erlangen_nuremberg', '에어랑엔 뉘른베르크 대학 ', 'https://www.fau.eu/education/student-life/accommodation-2/', 'https://www.campo.fau.de/qisserver/pages/cm/exa/coursecatalog/showCourseCatalog.xhtml?_flowId=showCourseCatalog-flow&_flowExecutionKey=e1s1&noDBAction=y&init=y', 'https://www.fau.eu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_erlangen_nuremberg/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_erlangen_nuremberg/1.png'), + ('DE', 'EUROPE', 'Otto von Guericke University of Magdeburg', 'otto_von_guericke_university_of_magdeburg', '오토폰귀릭케마그데부르그 대학', 'https://tl1host.eu/SWMD/#admission', 'https://www.ovgu.de/unimagdeburg/en/International/Incoming+_+Ways+to+the+University/International+Students/Exchange+Programmes/Studying+as+a+WORLDWIDE+Exchange+Student-p-48750.html', 'https://www.ovgu.de/en/International/Incoming+_+Ways+to+the+University/International+Students/Exchange+Programmes.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otto_von_guericke_university_of_magdeburg/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otto_von_guericke_university_of_magdeburg/1.png'), + ('DE', 'EUROPE', 'Carl von Ossietzky University of Oldenburg', 'carl_von_ossietzky_university_of_oldenburg', '올덴부르크 대학', 'https://uol.de/en/exchange-studies/living-in-oldenburg', 'https://elearning.uni-oldenburg.de/plugins.php/veranstaltungsverzeichnis_lvsg/englishmodules?vvz_sem_select=e770f053fbe29f2d1bd56a01d0dde1d0', 'https://uol.de/en/exchange-studies', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/carl_von_ossietzky_university_of_oldenburg/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/carl_von_ossietzky_university_of_oldenburg/1.png'), + ('DE', 'EUROPE', 'Zepplin University', 'zepplin_university', '제플린대학', 'https://www.zeppelin-university.com/info-wAssets/universitaet/dokumente/international-office/housing.pdf', 'https://zuhause.zeppelin-university.net/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=ACTION&ARGUMENTS=-AXFLjV7~Wgj~xhfmwa1nYvK3AVqeNsVYlJE1s9BJUvPyN3hZATz-SN~fW4BQGvcqQrbg69LM7Vb2PmCS13njtn6vY7wlZ3PR0VVc--5~HKXDORfzpyZYMWO-LB2OopwYkzVvJJ~JUF3g150btYFH8iNVzj12-lBywRT6Aplt7cIeSaUbvmD5Cny-23I6rfUTkzn1OdViRhkbSGv0_', 'https://www.zu.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zepplin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zepplin_university/1.png'), + ('DE', 'EUROPE', 'Chemnitz University of Technology', 'chemnitz_university_of_technology', '쳄니츠 공과대학', 'https://www.swcz.de/en/student-housing/our-halls-of-residence/', 'https://www.tu-chemnitz.de/international/incoming/erasmus/vlvz.php.en', 'https://www.tu-chemnitz.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chemnitz_university_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chemnitz_university_of_technology/1.png'), + ('DE', 'EUROPE', 'Karlsruhe University of Applied Sciences', 'karlsruhe_university_of_applied_sciences', '칼스루에 대학', 'https://www.h-ka.de/en/accommodation', 'https://www.h-ka.de/en/internationalprogram/profile', 'https://www.h-ka.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/karlsruhe_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/karlsruhe_university_of_applied_sciences/1.png'), + ('DE', 'EUROPE', 'Heilbronn University of Applied Sciences', 'heilbronn_university_of_applied_sciences', '하일브론 과학대학교', 'https://www.hs-heilbronn.de/accommodation', 'https://cdn.hs-heilbronn.de/4641dc9db5209412/389f4f6cac1d/English-Course-Offer-and-Course-descriptions-Campus-Schw-bisch-Hall_WS-2023-24.pdf', 'https://www.hs-heilbronn.de/en/incoming-exchange-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/heilbronn_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/heilbronn_university_of_applied_sciences/1.png'), + ('SE', 'EUROPE', 'Malmo University', 'malmo_university', '말뫼대학', 'https://mau.se/en/education/housing/', 'https://mau.se/en/study-education/?r.PagingNumber=1&r.Languages=en&r.Query=&r.TypesSelected=67498&r.Sort=alphabetically', 'https://student.mau.se/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/malmo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/malmo_university/1.png'), + ('SE', 'EUROPE', 'University College of Boras', 'university_college_of_boras', '보라스 대학교', 'https://www.hb.se/en/accommodation', 'https://www.hb.se/exchangecourses', 'https://www.hb.se/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_college_of_boras/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_college_of_boras/1.png'), + ('CH', 'EUROPE', 'Zurich University of Applied Sciences', 'zurich_university_of_applied_sciences', '취리히응용과학기술대학', 'https://www.zhaw.ch/en/study/before-your-studies/student-accommodation', 'https://www.zhaw.ch/storage/engineering/studium/internationales-studium/vom-ausland-in-die-schweiz/spring_semester_zhaw_school_of_engineering.pdf', 'https://www.zhaw.ch/en/university/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zurich_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zurich_university_of_applied_sciences/1.png'), + ('ES', 'EUROPE', 'Universidad de Navarra', 'universidad_de_navarra', '나바라대학교 ', NULL, NULL, 'https://www.unav.edu/web/facultad-de-derecho/estudiantes/programas-de-intercambio/incoming-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universidad_de_navarra/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universidad_de_navarra/1.png'), + ('ES', 'EUROPE', 'Universidad Carlos III de Madrid', 'universidad_carlos_iii_de_madrid', '마드리드카를로스3세 대학교', 'https://www.uc3m.es/studies/international-exchage-students-in-uc3m/bachelor-degrees/Accommodation', 'https://www.uc3m.es/studies/international-exchage-students-in-UC3M/bachelor-degrees/course-offer', 'https://www.uc3m.es/studies/international-exchange-students-in-UC3M-/bachelor-degrees', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universidad_carlos_iii_de_madrid/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universidad_carlos_iii_de_madrid/1.png'), + ('ES', 'EUROPE', 'University of Lleida', 'university_of_lleida', '예이다대학교', 'http://www.udl.cat/ca/serveis/ori/estudiantat_estranger/eng/infoeng/accommodation/', 'https://www.udl.cat/ca/serveis/ori/estudiantat_estranger/eng/infoeng/subjects/', 'https://www.udl.cat/ca/serveis/ori/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_lleida/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_lleida/1.png'), + ('GB', 'EUROPE', 'University of Hull', 'university_of_hull', '헐대학', 'https://www.hull.ac.uk/Choose-Hull/Student-life/Accommodation/accommodation.aspx', 'https://universityofhull.app.box.com/s/mpvulz3yz0uijdt68rybce19nek0d8eh', 'https://www.hull.ac.uk/choose-hull/study-at-hull/need-to-know/key-dates', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hull/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hull/1.png'), + ('AT', 'EUROPE', 'University of Graz', 'university_of_graz', '그라츠 대학', 'https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/', 'https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf', 'https://www.uni-graz.at/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/1.png'), + ('AT', 'EUROPE', 'Graz University of Technology', 'graz_university_of_technology', '그라츠공과대학', 'https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033', 'https://tugraz.at/go/search-courses', 'https://www.tugraz.at/en/home', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/1.png'), + ('AT', 'EUROPE', 'Catholic Private University Linz', 'catholic_private_university_linz', '린츠 카톨릭 대학교', NULL, 'https://ku-linz.at/en/ku_international/incomings/kulis', 'https://ku-linz.at/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/1.png'), + ('AT', 'EUROPE', 'University of Applied Sciences Technikum Wien', 'university_of_applied_sciences_technikum_wien', '빈 공과대학교', NULL, 'https://www.technikum-wien.at/en/international/student-mobility/', 'https://www.technikum-wien.at/international/studierendenmobilitaet-2/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_technikum_wien/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_technikum_wien/1.png'), + ('IT', 'EUROPE', 'Polytechnic University of Milan', 'polytechnic_university_of_milan', '밀라노공과대학', NULL, NULL, 'https://www.polimi.it/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/polytechnic_university_of_milan/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/polytechnic_university_of_milan/1.png'), + ('IT', 'EUROPE', 'University of Bergamo', 'university_of_bergamo', '베르가모 대학', NULL, NULL, 'https://en.unibg.it/international/students-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_bergamo/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_bergamo/1.png'), + ('IT', 'EUROPE', 'University of Ca''Poscari', 'university_of_caposcari', '카포스카리대학교', NULL, 'https://www.unive.it/data/9639/', 'https://www.unive.it/pag/13526', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_caposcari/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_caposcari/1.png'), + ('CZ', 'EUROPE', 'University of Ostrava', 'university_of_ostrava', '오스트라바 대학', 'https://koleje.osu.eu/', 'https://www.osu.eu/22821/courses/', 'https://www.osu.eu', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_ostrava/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_ostrava/1.png'), + ('CZ', 'EUROPE', 'Czech University of Life Sciences Prague', 'czech_university_of_life_sciences_prague', '체코 생명과학대학', NULL, 'https://www.czu.cz/en/r-9190-international-relations/r-17025-course-offer-academic-year-2023-2024', 'https://www.czu.cz/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/czech_university_of_life_sciences_prague/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/czech_university_of_life_sciences_prague/1.png'), + ('CZ', 'EUROPE', 'Czech Technical University in Prague', 'czech_technical_university_in_prague', '프라하공과대학', 'https://www.suz.cvut.cz/cz', 'https://legacy.mobility.cvut.cz/prospectus/2023/index.php', 'https://international.cvut.cz/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/czech_technical_university_in_prague/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/czech_technical_university_in_prague/1.png'), + ('PT', 'EUROPE', 'INSTITUTO SUPERIOR TECNICO-Universidade de Lisboa', 'instituto_superior_tecnico-universidade_de_lisboa', '리스본대학 공과대학', NULL, NULL, 'https://tecnico.ulisboa.pt/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/instituto_superior_tecnico-universidade_de_lisboa/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/instituto_superior_tecnico-universidade_de_lisboa/1.png'), + ('PT', 'EUROPE', 'Catholic University of Portugal', 'catholic_university_of_portugal', '포르투갈 가톨릭대학', NULL, 'https://catolicabs.porto.ucp.pt/international-programmes-0', 'https://catolicabs.porto.ucp.pt/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_university_of_portugal/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_university_of_portugal/1.png'), + ('FR', 'EUROPE', 'EBS Paris', 'ebs_paris', 'EBS Paris', NULL, NULL, 'https://www.ebs-paris.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ebs_paris/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ebs_paris/1.png'), + ('FR', 'EUROPE', 'EPITA', 'epita', 'EPITA', 'http://housing.epitamasters.com', 'https://sway.office.com/u5VHBu9DbBH6Mr8F', 'https://sway.office.com/u5VHBu9DbBH6Mr8F?ref=Link', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/epita/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/epita/1.png'), + ('FR', 'EUROPE', 'EPITECH(L''echole de L''expertise Informatique)', 'epitech(lechole_de_lexpertise_informatique)', 'EPITECH', 'https://international.epitech.eu/student-life/', 'https://international.epitech.eu/', 'https://international.epitech.eu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/epitech(lechole_de_lexpertise_informatique)/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/epitech(lechole_de_lexpertise_informatique)/1.png'), + ('FR', 'EUROPE', 'ESCE International Business School', 'esce_international_business_school', 'ESCE 국제경영대학', 'https://www.studapart.com/en/studapart/student-accommodation?gad=1&gclid=Cj0KCQjw7uSkBhDGARIsAMCZNJvmPVROiqUSiAO69ks7UWWXcIKO1rGO_hXPmjlx72qsV1SF1VrEzRkaAjTQEALw_wcB', NULL, 'http://www.esce.fr/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/esce_international_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/esce_international_business_school/1.png'), + ('FR', 'EUROPE', 'Ecole Superieure des Sciences Commerciales d''Anger', 'ecole_superieure_des_sciences_commerciales_danger', 'ESSCA경영대학', NULL, 'https://pcee.azurewebsites.net', 'https://www.essca.fr/en/international/exchange-student', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ecole_superieure_des_sciences_commerciales_danger/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ecole_superieure_des_sciences_commerciales_danger/1.png'), + ('FR', 'EUROPE', 'IPSA', 'ipsa', 'IPSA', 'https://www.ipsa.fr/en/student-life/pratical-information/', NULL, 'https://www.ipsa.fr/en/engineering-school/aeronautical-space', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ipsa/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ipsa/1.png'), + ('FR', 'EUROPE', 'ISEP', 'isep', 'ISEP', NULL, 'https://en.isep.fr/studying-at-isep/course-catalog/', 'https://en.isep.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/isep/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/isep/1.png'), + ('FR', 'EUROPE', 'KEDGE Business School', 'kedge_business_school', 'KEDGE경영대학 ', 'https://student.kedge.edu/student-services/prepare-my-studies-abroad/student-accommodation-in-france', 'https://student.kedge.edu/exchange-programmes/academic-information', 'https://student.kedge.edu', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kedge_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kedge_business_school/1.png'), + ('FR', 'EUROPE', 'NEOMA Business School', 'neoma_business_school', 'NEOMA경영대학', 'https://neoma-bs.com/welcome-to-neoma/steps/step-3-housing/', 'https://neoma-bs.com/welcome-to-neoma/steps/step-4-courses/', 'https://neoma-bs.com/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neoma_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neoma_business_school/1.png'), + ('FR', 'EUROPE', 'HEIP', 'heip', '국제정치대학', 'https://www.studapart.com/en/studapart/student-accommodation?gad=1&gclid=Cj0KCQjw7uSkBhDGARIsAMCZNJvmPVROiqUSiAO69ks7UWWXcIKO1rGO_hXPmjlx72qsV1SF1VrEzRkaAjTQEALw_wcB', NULL, 'https://www.heip.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/heip/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/heip/1.png'), + ('FR', 'EUROPE', 'EM NORMANDIE BUSINESS SCHOOL', 'em_normandie_business_school', '노르망디경영대학', 'https://en.em-normandie.com/em-normandie-experience/open-world-studying-abroad/exchange-programmes', 'https://en.em-normandie.com/em-normandie-experience/open-world-studying-abroad/exchange-programmes', 'https://www.em-normandie.com/fr', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/em_normandie_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/em_normandie_business_school/1.png'), + ('FR', 'EUROPE', 'La Rochelle Business School', 'la_rochelle_business_school', '라로쉘경영대학 ', 'https://excelia-group.studapart.com/en/', 'https://www.excelia-group.com/student-services/international-students/exchange-students/programmes-details', 'https://www.excelia-group.com/student-services', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/la_rochelle_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/la_rochelle_business_school/1.png'), + ('FR', 'EUROPE', 'ESC Rennes School of Business', 'esc_rennes_school_of_business', '렌 경영대학', 'https://www.rennes-sb.com/student-life/accommodation-health/', 'https://www.rennes-sb.com/programmes/exchange-programme/incoming-exchange-students/', 'https://www.rennes-sb.com/programmes/exchange-programme/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/esc_rennes_school_of_business/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/esc_rennes_school_of_business/1.png'), + ('FR', 'EUROPE', 'University of Le Havre', 'university_of_le_havre', '르아브르대학', 'https://www.univ-lehavre.fr/spip.php?article70', NULL, 'https://www.univ-lehavre.fr', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_le_havre/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_le_havre/1.png'), + ('FR', 'EUROPE', 'Lille Catholic University', 'lille_catholic_university', '릴 가톨릭 대학', 'https://www.all-lacatho.fr/en/', 'https://www.univ-catholille.fr/sites/default/files/fichiers/VF%20Catalogue%20cours%20en%20anglais%202022-2023_0.pdf', 'http://www.univ-catholille.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lille_catholic_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lille_catholic_university/1.png'), + ('FR', 'EUROPE', 'Lille Catholic University', 'lille_catholic_university', '릴 가톨릭 대학(ESTICE)', 'https://www.all-lacatho.fr/en/list-accommodation', 'https://estice.fr/programs/', 'http://www.univ-catholille.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lille_catholic_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lille_catholic_university/1.png'), + ('FR', 'EUROPE', 'Universite Montpellier 1', 'universite_montpellier_1', '몽펠리에 대학교', 'https://iae.umontpellier.fr/en/institut/accommodation', 'https://iae.umontpellier.fr/en/institut/exchange-students', 'https://www.umontpellier.fr/university-of-montpell', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_montpellier_1/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_montpellier_1/1.png'), + ('FR', 'EUROPE', 'Ecole d''Architecture, Paris Val de Seine', 'ecole_darchitecture_paris_val_de_seine', '발드센느대학', NULL, 'https://www.paris-valdeseine.archi.fr/fileadmin/mediatheque/document/International/etudiants_entrants/English_speaking_classes.pdf', 'https://www.paris-valdeseine.archi.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ecole_darchitecture_paris_val_de_seine/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ecole_darchitecture_paris_val_de_seine/1.png'), + ('FR', 'EUROPE', 'Burgundy School of Business', 'burgundy_school_of_business', '부르군디 경영대학', 'https://www.studapart.com/fr', 'https://international.bsb-education.com/course-catalogues/?lang=en', 'https://www.bsb-education.com/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/burgundy_school_of_business/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/burgundy_school_of_business/1.png'), + ('FR', 'EUROPE', 'Audencia Business School', 'audencia_business_school', '오덴시아 낭트 경영대학', 'https://www.expatistan.com/cost-of-living/nantes', 'https://apply.exchangestudents.audencia.com/index.cfm?FuseAction=Abroad.ViewLink&Parent_ID=FE40C425-5056-BA1F-74632B3DF27C287E&Link_ID=0365089E-9F2E-9844-AB9F3ABC4462019B', 'https://apply.exchangestudents.audencia.com/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/audencia_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/audencia_business_school/1.png'), + ('FR', 'EUROPE', 'Universite Jean Moulin Lyon 3', 'universite_jean_moulin_lyon_3', '장물랭리옹3세대학교', 'https://associnterlyon3.fr/en/', 'https://www.univ-lyon3.fr/self-study-in-english-in-lyon-france', 'https://www.univ-lyon3.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_jean_moulin_lyon_3/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_jean_moulin_lyon_3/1.png'), + ('FR', 'EUROPE', 'Toulouse Business school', 'toulouse_business_school', '툴루즈 경영대학교', 'https://www.tbs-education.com/about-tbs/student-services/', 'https://www.tbs-education.com/about-tbs/international/incoming-exchange-students/', 'https://www.tbs-education.com/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toulouse_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toulouse_business_school/1.png'), + ('FR', 'EUROPE', 'Sciences Po Toulouse', 'sciences_po_toulouse', '툴루즈정치대학', NULL, 'https://www.sciencespo-toulouse.fr/en/courses/university-diploma-in-international-comparative-studies', 'http://www.sciencespo-toulouse.fr/home-english-version-589930.kjsp', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/sciences_po_toulouse/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/sciences_po_toulouse/1.png'), + ('FR', 'EUROPE', 'University of Paris 8', 'university_of_paris_8', '파리8대학교', 'https://www.univ-paris8.fr/-Informations-pratiques-', 'https://www.univ-paris8.fr/-Etudes-diplomes-', 'https://www.univ-paris8.fr/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_paris_8/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_paris_8/1.png'), + ('FI', 'EUROPE', 'Lahti University of Applied Sciences', 'lahti_university_of_applied_sciences', 'LAB대학', 'https://lab.fi/en/exchange-student-guide', 'https://opinto-opas.lab.fi/70064/en/70060/70065?lang=en', 'https://www.lab.fi/en/exchange-student-guide/studies', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lahti_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lahti_university_of_applied_sciences/1.png'), + ('FI', 'EUROPE', 'Lappeenranta University of Technology', 'lappeenranta_university_of_technology', '라펜란타기술대학교', 'https://www.loas.fi/en/loas/student-housing-foundation-region-lappeenranta-loas', 'https://www.lut.fi/en/studies/exchange-studies/courses-exchange-students', 'https://www.lut.fi/en/studies/exchange-studies', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lappeenranta_university_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lappeenranta_university_of_technology/1.png'), + ('FI', 'EUROPE', 'Turku University of Applied Sciences', 'turku_university_of_applied_sciences', '투르크 응용과학대학', 'https://www.tuas.fi/en/study-tuas/exchange-students/accommodation/', 'https://www.tuas.fi/en/study-tuas/exchange-students/courses/', 'https://www.tuas.fi/en/study-tuas/exchange-students/about/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/turku_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/turku_university_of_applied_sciences/1.png'), + ('FR', 'EUROPE', 'Universite de Technologie de Troyes', 'universite_de_technologie_de_troyes', '트루아 공과대학', 'https://www.utt.fr/study-at-utt/accommodation', 'https://www.utt.fr/study-at-utt/courses-in-english', 'https://www.utt.fr/study-at-utt/academic-programs', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_de_technologie_de_troyes/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_de_technologie_de_troyes/1.png'), + ('BN', 'ASIA', 'Universiti Brunei Darussalam', 'universiti_brunei_darussalam', '브루나이 국립대학', NULL, NULL, 'https://ubd.edu.bn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universiti_brunei_darussalam/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universiti_brunei_darussalam/1.png'), + ('SG', 'ASIA', 'Singapore Management University', 'singapore_management_university', '싱가폴경영대학', NULL, 'https://publiceservices.smu.edu.sg/psc/ps/EMPLOYEE/HRMS/c/SIS_CR.SIS_CLASS_SEARCH.GBL?&', 'https://publiceservices.smu.edu.sg/psc/ps/EMPLOYEE/HRMS/c/SIS_CR.SIS_CLASS_SEARCH.GBL?&', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/singapore_management_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/singapore_management_university/1.png'), + ('AZ', 'ASIA', 'ADA University', 'ada_university', '아다대학교', NULL, 'https://www.ada.edu.az/frq-content/plugins/policies_x1/entry/20221226165811_72744800.pdf ', 'https://www.ada.edu.az/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ada_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ada_university/1.png'), + ('ID', 'ASIA', 'BINUS University', 'binus_university', '비누스대학 ', 'https://binus.ac.id/binussquare/', 'https://linktr.ee/binusexchange', 'https://io.binus.ac.id/international-students/post/student-exchange-program-2/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/binus_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/binus_university/1.png'), + ('JP', 'ASIA', 'Kanazawa University', 'kanazawa_university', '가나자와대학', NULL, NULL, 'https://kuglobal.w3.kanazawa-u.ac.jp/eg/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kanazawa_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kanazawa_university/1.png'), + ('JP', 'ASIA', 'Gakushuin University', 'gakushuin_university', '가쿠슈인대학', NULL, 'https://www.univ.gakushuin.ac.jp/iss/en/education/subject.html', 'https://www.univ.gakushuin.ac.jp/iss/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/gakushuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/gakushuin_university/1.png'), + ('JP', 'ASIA', 'Kindai University', 'kindai_university', '긴다이대학', NULL, NULL, 'https://www.kindai.ac.jp/english/files/study-at-kindai/prospective.pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kindai_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kindai_university/1.png'), + ('JP', 'ASIA', 'Niigata University', 'niigata_university', '니가타대학', 'https://www.niigata-u.ac.jp/en/study/life/housing/', 'https://www.niigata-u.ac.jp/en/study/exchange/', 'https://www.niigata-u.ac.jp/en/study/exchange/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/niigata_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/niigata_university/1.png'), + ('JP', 'ASIA', 'Toyo University', 'toyo_university', '도요대학', 'https://www.toyo.ac.jp/contents/international-exchange/residence/index.php', NULL, 'https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toyo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toyo_university/1.png'), + ('JP', 'ASIA', 'Toyo University', 'toyo_university', '도요대학', 'https://www.toyo.ac.jp/contents/international-exchange/residence/index.php', NULL, 'https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toyo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toyo_university/1.png'), + ('JP', 'ASIA', 'Dokkyo University', 'dokkyo_university', '돗쿄대학', 'https://www.dokkyo.ac.jp/english/exchange/student/accommodation.html', 'https://www.dokkyo.ac.jp/english/exchange/calendar/syllabus.html', 'https://www.dokkyo.ac.jp/english/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/dokkyo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/dokkyo_university/1.png'), + ('JP', 'ASIA', 'Meiji University', 'meiji_university', '메이지대학', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png'), + ('JP', 'ASIA', 'BAIKA Women''s University', 'baika_womens_university', '바이카여자대학', 'https://dormy-ac.com/page/baika/', NULL, 'https://www.baika.ac.jp/english/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/1.png'), + ('JP', 'ASIA', 'Bunkyo Gakuin University', 'bunkyo_gakuin_university', '분쿄가쿠인대학', NULL, NULL, 'https://www.bgu.ac.jp/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/1.png'), + ('JP', 'ASIA', 'Yamaguchi University', 'yamaguchi_university', '야마구치대학', '-', NULL, 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yamaguchi_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yamaguchi_university/1.png'), + ('JP', 'ASIA', 'Osaka Gakuin University', 'osaka_gakuin_university', '오사카가쿠인대학', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/housing.html', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/syllabi.html', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/schedule.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/osaka_gakuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/osaka_gakuin_university/1.png'), + ('JP', 'ASIA', 'Otsuma Women''s University', 'otsuma_womens_university', '오츠마여자대학', 'https://www.otsuma.ac.jp/english/international/dormitory.html', NULL, 'https://www.otsuma.ac.jp/english/index.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otsuma_womens_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otsuma_womens_university/1.png'), + ('JP', 'ASIA', 'Waseda University', 'waseda_university', '와세다대학', 'https://www.waseda.jp/inst/rlc/en/student_dormitory/exchange/ ', NULL, 'https://www.waseda.jp/inst/cie/en/exchange/application ', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/waseda_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/waseda_university/1.png'), + ('JP', 'ASIA', 'Chuo University', 'chuo_university', '추오대학', 'https://www.chuo-u.ac.jp/english/admissions/residences/', NULL, 'https://www.chuo-u.ac.jp/english/admissions/exchange/semester-or-full-year/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chuo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chuo_university/1.png'), + ('JP', 'ASIA', 'Chiba University', 'chiba_university', '치바대학', 'https://www.chiba-u.ac.jp/international/isd/en/index.html', NULL, 'https://www.chiba-u.ac.jp/e/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chiba_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chiba_university/1.png'), + ('TR', 'ASIA', 'ANKARA UNIVERSITY', 'ankara_university', '앙카라대학', NULL, 'http://iso.ankara.edu.tr/en/mainpage/', 'https://en.ankara.edu.tr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ankara_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ankara_university/1.png'), + ('TR', 'ASIA', 'Ozyegin University', 'ozyegin_university', '외즈예인대학교', 'https://www.ozyegin.edu.tr/en/dormitories/housing-fees', 'https://www.ozyegin.edu.tr/en/ects-course-catalog-courses-offered/courses-offered', 'https://www.ozyegin.edu.tr/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ozyegin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ozyegin_university/1.png'), + ('HK', 'ASIA', 'Shue Yan University', 'shue_yan_university', '수인대학교', NULL, 'https://iu.hksyu.edu/wp-content/uploads/2022/08/CourseListForExchangeStudents2022-23-20220816.pdf', 'https://iu.hksyu.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/shue_yan_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/shue_yan_university/1.png'), + ('HK', 'ASIA', 'City Univ Hong Kong', 'city_univ_hong_kong', '홍콩시립대학', 'https://www.cityu.edu.hk/sro/StudentHousing/UGHalls/InboundExchangeStudents.htm', 'https://www.cityu.edu.hk/admo/exchange/exchange_course_list_202402.pdf', 'https://www.admo.cityu.edu.hk/exchange_visiting/exchange/info/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/city_univ_hong_kong/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/city_univ_hong_kong/1.png'), + ('TW', 'CHINA', 'National Chengchi University', 'national_chengchi_university', '국립정치대학교', NULL, 'https://qrysub.nccu.edu.tw/', 'https://oic.nccu.edu.tw/Post/833', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_chengchi_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_chengchi_university/1.png'), + ('TW', 'CHINA', 'National Sun Yat-sen University', 'national_sun_yat-sen_university', '국립중산대학', 'https://oia.nsysu.edu.tw/p/412-1308-20581.php?Lang=en', 'https://oia.nsysu.edu.tw/p/412-1308-20770.php?Lang=en', 'https://oia.nsysu.edu.tw/p/412-1308-20581.php?Lang=en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_sun_yat-sen_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_sun_yat-sen_university/1.png'), + ('TW', 'CHINA', 'National Central University', 'national_central_university', '국립중앙대학교', NULL, 'https://cis.ncu.edu.tw/Course/main/news/announce', 'http://oia.ncu.edu.tw/index.php/en/international-students/incoming-exchange-programs.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_central_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_central_university/1.png'), + ('TW', 'CHINA', 'University of Taipei', 'university_of_taipei', '타이베이시립대학', NULL, NULL, 'https://international.utaipei.edu.tw/index.php?Lang=en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_taipei/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_taipei/1.png'), + ('CN', 'CHINA', 'Jiangnan University', 'jiangnan_university', '강남대학교', NULL, NULL, 'http://www.jiangnan.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/jiangnan_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/jiangnan_university/1.png'), + ('CN', 'CHINA', 'Jilin University', 'jilin_university', '길림대학교', NULL, NULL, 'http://cie.jlu.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/jilin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/jilin_university/1.png'), + ('CN', 'CHINA', 'Nanjing Normal University', 'nanjing_normal_university', '남경사범대학교', NULL, NULL, 'http://gjc.njnu.edu.cn/index.htm', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/nanjing_normal_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/nanjing_normal_university/1.png'), + ('CN', 'CHINA', 'Donghua University', 'donghua_university', '동화대학교', 'https://korean.dhu.edu.cn/accommodation/list.htm', 'https://english.dhu.edu.cn/incoming/list.htm', 'http://english.dhu.edu.cn ', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/donghua_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/donghua_university/1.png'), + ('CN', 'CHINA', 'Shandong University, Weihai', 'shandong_university_weihai', '산동대학교(위해)', NULL, NULL, 'https://en.wh.sdu.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/shandong_university_weihai/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/shandong_university_weihai/1.png'), + ('CN', 'CHINA', 'Yantai University', 'yantai_university', '연태대학교', NULL, NULL, 'https://en.ytu.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yantai_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yantai_university/1.png'), + ('CN', 'CHINA', 'Zhejiang Normal University', 'zhejiang_normal_university', '절강사범대학교', NULL, 'http://iso.zjnu.edu.cn/wistwofwwrogramsw2018/list.htm', 'http://iso.zjnu.edu.cn/ywb/main.htm', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zhejiang_normal_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zhejiang_normal_university/1.png'), + ('CN', 'CHINA', 'Minzu University of China', 'minzu_university_of_china', '중앙민족대학교', NULL, NULL, 'https://oir.muc.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/minzu_university_of_china/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/minzu_university_of_china/1.png'), + (NULL, NULL, 'SAF Program', 'saf_program', 'SAF 프로그램', NULL, NULL, 'http://korea.studyabroadfoundation.org/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saf_program/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saf_program/1.png'); + +INSERT INTO university_info_for_apply +(university_id, semester_requirement, student_capacity, semester_available_for_dispatch, tuition_fee_type, details_for_major, details_for_apply, details_for_language, details_for_english_course, details_for_accommodation, details) +VALUES + (1, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), + (2, 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend'), + (3, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000
- 한 학기 등록금: 약 $7,500
- International Program and Service Fees $2,500'), + (4, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 6.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000
- International Program and Service Fees $2,500'), + (5, 2, 5, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함', NULL, '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', NULL, NULL, '※ On Campus 기숙사 신청 필수! (기숙사 미신청 시 해외대학등록금납부형(B형)으로 전환)
- 기숙사 관련 정보 : https://www.unk.edu/offices/reslife/housing-options.php
- 보험료 약 $2,273/학기 (가격변동가능), 보험 가입 필수!
https://www.unk.edu/international/international-student-services/medical-insurance.php'), + (6, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함', 'ELI(어학연수) 과정으로 지원시 영어 성적 무관 (https://www.unk.edu/international/english-language-institute/index.php)', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', NULL, NULL, '- ELI 어학연수 과정으로 지원시, 전공/ ESL 크레딧은 자체배치고사 점수에 따라 상이
- 기숙사 관련 정보 : https://www.unk.edu/offices/reslife/housing-options.php
- 등록금 관련 정보 : https://www.unk.edu/costs.php
- 보험료 약 $2,273/학기 (가격변동가능), 보험 가입 필수!
https://www.unk.edu/international/international-student-services/medical-insurance.php'), + (7, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
지원불가능전공 : Nursing, Athletic training, Education', NULL, '-영어 점수는 다음의 세부영역 점수를 각각
만족해야 함
- TOEFL iBT : 모든 영역에서 15점 이상
- IELTS : 모든 영역에서 5.5 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '한 학기 기숙사 비용: 약 $5,442 '), + (8, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
지원불가능전공 : Nursing, Athletic training, Education', NULL, '-영어 점수는 다음의 세부영역 점수를 각각
만족해야 함
- TOEFL iBT : 모든 영역에서 15점 이상
- IELTS : 모든 영역에서 5.5 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '한 학기 등록금: 약 $6,938 (in-state 적용, 2023-24기준)
한 학기 기숙사 비용: 약 $5,442'), + (9, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 쓰기 영역에서 5.0 이상', NULL, NULL, '교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음
- College Fee : 약 $1,070'), + (10, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 쓰기 영역에서 5.0 이상', NULL, NULL, '교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음
- 한 학기 등록금: 약 $9,450 (out of state)
- College Fee : 약 $1,070'), + (11, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원 불가 전공 : Writing (WRT), Health Sciences, Nursing, Pharmacology, Education
- Languages 강좌(독일어, 프랑스어 등), Dance, Theatre Arts and Cinema & Cultural Studies 수강 제한
- Business/Accounting 전공 학생은 최대 3학점까지만 전공 수업 수강 가능하며 나머지 학점은 다른 전공에서 수강 가능', NULL, NULL, NULL, NULL, '- 한 학기 기숙사 비용: 약 $4,500~6,200
- 한 학기 등록금: 약 $12,495 (out of state 적용)
- 등록금 및 기타 Fee Rates 관련 정보: https://www.stonybrook.edu/commcms/sfs/tuition/index.php'), + (12, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원 및 수강하는 전공과 본교 전공이 일치해야함
- 지원 불가 전공 : Nursing', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), + (13, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원 및 수강하는 전공과 본교 전공이 일치해야함
- 지원 불가 전공 : Nursing', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '한 학기 등록금: 약 $6,892 (In-state 적용)'), + (14, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', ' - 타전공 지원 및 수강 가능
지원 불가 전공: Nursing, Counseling, Music, Teaching, Occupational and Speech therapy', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), + (15, 2, 1, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', ' - 타전공 지원 및 수강 가능
지원 불가 전공: Nursing, Counseling, Music, Teaching, Occupational and Speech therapy', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '한 학기 등록금: 약 $6,000 (in state+10%) '), + (16, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원불가능전공:
Athletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, and Nursing', NULL, NULL, NULL, NULL, '- 모든 국제학생들은 안전과 영어향상을 위해 무조건 기숙사를 사용을 강제하는 International Studies Policy를 공지합니다'), + (17, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원불가능전공:
Athletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, and Nursing', NULL, NULL, NULL, NULL, '- 모든 국제학생들은 안전과 영어향상을 위해 무조건 기숙사를 사용을 강제하는 International Studies Policy를 공지합니다
- 등록금은 in-state rate 적용됨
- 1년 과정으로 봄학기 수학 후 가을학기 수학을 하는 경우, 여름학기 수강 가능 (Optional)'), + (18, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 18점 이상
- IELTS : 모든 영역에서 6.0 이상
- Duolingo : 모든 영역에서 95점 이상 (Duolingo의 경우, 토플과 아이엘츠 성적을 보유하지 않은 경우 예외적으로 적용되므로 합격 이후 필요시 파견교에서 영어능력에 대해 재확인할 수 있음/ https://www.uah.edu/admissions/undergraduate/apply-for-admission/international)
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, NULL), + (19, 2, 1, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 18점 이상
- IELTS : 모든 영역에서 6.0 이상
- Duolingo : 모든 영역에서 95점 이상 (Duolingo의 경우, 토플과 아이엘츠 성적을 보유하지 않은 경우 예외적으로 적용되므로 합격 이후 필요시 파견교에서 영어능력에 대해 재확인할 수 있음/ https://www.uah.edu/admissions/undergraduate/apply-for-admission/international)
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '등록금 관련 정보 : https://www.uah.edu/bursar/tuition'), + (20, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 선이수과목이 비숫해야 후에 IIT에서 전공과목을 수강할 수 있음
- 타전공 지원 및 수강 가능 (단, 각 학과의 사전허가 필요)', 'SAT시험 면제 조건으로 교양 및 전공 포함하여 최소 30학점 이수하여야 하며 입학사정시 전공과목 및 영어과목 위주로 검토 됨. ', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, '- 식비(Meal Plan) 정보
https://www.iit.edu/housing/dining-and-meal-plan/options-and-rates
- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid
- 학비관련 링크 : https://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate
- 한 학기 등록금: 약 $14.820 (방문학생 학비장학금 $10,000/학기 차감한 금액 기준, 징학금은 12크레딧 이상 full time 등록 시에만 지급 가능)
- 보험료 링크 : https://www.iit.edu/shwc/insurance/plan-info-and-requirements
- 세부사항 변동 가능'), + (21, 4, 5, 'FOUR_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 정확하게 일치해야함', 'SAT시험 면제 조건으로 교양 및 전공 포함하여 최소 30학점 이수하여야 하며 입학사정시 전공과목 및 영어과목 위주로 검토 됨.
- 2023-2학기에 4차 학기 이수 예정인 학생도 조건부 지원 가능
(반드시 정규학기 이수하여야 2024-1 파견 가능)', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, '- 식비(Meal Plan) 정보
https://www.iit.edu/housing/dining-and-meal-plan/options-and-rates
- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid
- 학비관련 링크 : https://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate
- 한 학기 등록금: 약 $14.820 (방문학생 학비장학금 $10,000/학기 차감한 금액 기준, 징학금은 12크레딧 이상 full time 등록 시에만 지급 가능)
- 보험료 링크 : https://www.iit.edu/shwc/insurance/plan-info-and-requirements
- 세부사항 변동 가능'), + (22, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, NULL, '※ 테일러대학은 기독교 정신을 기반으로 설립된 학교이므로 대다수의 학생들이 기독교를 믿고 있음. 기독교에 거부감이 없고 성실한 학교생활을 하며, 정기적인 교회 생활을 하고있는 학생들만 지원 권장
- 원칙은 1학기 지원이나, 2학기도 학생이 원하면 지원 가능
다만, 파견대학에서 학생의 교환학생 성과를 평가해 이에 미치지 못할 경우 2학기를 이어 진행하지 못하고 한 학기만 진행할 수 있으니 해당 사항 유의하기 바람. 이에 2학기를 지원하는 학생의 경우, 파견 지원 전 반드시 지역담당자에게 사전에 연락해 관련 내용에 대해 논의하고 지원하길 바람. '), + (23, 2, 5, 'IRRELEVANT', 'MIXED_PAYMENT', '- 타전공 지원 및 수강 가능
- 다음 전공들은 지원 가능하나 수강신청이 제한적일 수 있음 Architecture, Computer & Information Sciences, Business, Education, Performing Arts (Dance, Music, Theater), Professional Schools (Dentistry, Law, Medicine, Pharmacy, Podiatry), Visual Arts (Film/Media Arts, Graphic Design, Fine Arts, etc), Sport, Tourism and Hospitality Management
- Business 전공의 경우, 본교 경영학과 학생만 지원가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 6 이상
- 영어시험 성적 파견대학 지원시까지 유효하여야 함
- 최저 기준 어학 점수가 넘더라도, 선발대학에서 특정 섹션의 어학실력이 부족하다고 판단될 경우 파견 시 별도로 대학부설 어학코스(ielp) 수강이 필요할 수 있음 [수업료 외 별도,비용발생]', NULL, NULL, '※ 1개 학기로도 지원 가능
※ 혼합형은 첫 번째 학기는 템플대학교에 등록금 지불, 두 번째 학기는 인하대에 등록금 지불하는 유형 (2개 학기를 모두 마치고 올 경우에만 두 번쨰 학기에 템플대학교 등록금 면제 및 인하대에 등록금 지불 적용 가능)
- 한 학기 등록금: 약 $15,432 (out of rate, 관련정보: https://globalprograms.temple.edu/programs/inbound-study-abroad-exchange/costs-dates)
- 한 학기 기숙사: 약 $5,000 (기숙사 유형에 따라 상이)'), + (24, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원제힌전공: Nursing', NULL, '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', NULL, NULL, NULL), + (25, 2, 4, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원제힌전공: Nursing', NULL, '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', NULL, NULL, '한 학기 등록금: 약 $9,312 (50% tuition scholarship 지급)'), + (26, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, ' - 토플 IBT(100), 토플 ITP(600), IELTS(7.0) DUOLINGO(125) 미만시 파견후 별도 영어시험 필수이며 결과에 따라 1-3개 어학 수업 수강하게 될 수 있음
- 어학성적은 파견대학 지원시까지 유효하여야 함', NULL, NULL, '등록금: Hoakipa Visiting Student 유형으로 in-state 150% 적용'), + (27, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, NULL, '※ 영어강의 제공하지 않음, 모든 강의 포르투갈어로 진행', '교내 기숙사 미제공, International Affairs와 버디프로그램을 통해 교외숙소 계약을 도와줄 예정', NULL), + (28, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원불가전공 : Medicine', NULL, NULL, '※ 대부분의 강의 포르투갈어로 진행, 주로 Business Field에 영어수업', NULL, '포르투갈어 어학 수업 수강 가능'), + (29, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 아래 8개 Faculties 내에서만 수강 가능 :
Arts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science
- 지원 불가 전공: Nursing, Social work', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상 ', NULL, NULL, NULL), + (30, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 아래 8개 Faculties 내에서만 수강 가능 :
Arts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science
- 지원 불가 전공: Nursing, Social work', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상 ', NULL, NULL, '국제학생 등록금 적용(지원 전공 및 학점에 따라 금액 상이)
- 관련 링크: https://www.uregina.ca/fs/students/fee-schedule.html '), + (31, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', NULL, NULL, NULL), + (32, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', NULL, NULL, '국제학생 등록금 적용 (학점당 $2,080)'), + (33, 2, 4, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '※ 가장 기초 과정(IEP-G)의 최소 지원 요건이며 레벨과 지원과정에 따라 지원자격 상이하므로 fact sheet 참조 바람
- 학부과정 수강 가능한 IEBP-G 지원 시 다음의 세부영역 점수를 만족해야함
- IELTS : 쓰기 5.5 이상, 모든 영역에서 5.0 이상
- TOEFL : 쓰기 16점 이상
- 외국어 성적 유효기간이 파견학기 시작시까지 유효해야함', NULL, NULL, '선발 학생의 어학성적에 따라 레벨이 정해지며 비용 또한 상이.
- IEBP-G 레벨에 배정될 경우 학부 수업 1-2개 수강 가능하며 (선택 제한적) 학부수업에 대한 등록금은 면제 (국제처 홈페이지 내 대학 Fact Sheet 참조 바람)'), + (34, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원 유의 전공 : Fashion, Media and Communication, Design, Interior Design, Architecture Design
(참고 : https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange/how-to-search-for-your-courses)', NULL, '어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, NULL), + (35, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 각 영역 최소 5.5 이상
- 외국어 성적 유효기간이 파견대학의 지원 시점 기준까지 유효해야함 ', NULL, NULL, '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람'), + (36, 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 각 영역 최소 5.5 이상
- 외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함 ', NULL, NULL, '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람
- In-state 등록금 납부
(등록금 관련 정보 : https://www.unisq.edu.au/international/partnerships/study-abroad-exchange/fees-scholarships)'), + (37, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- MECO, CAEL, LAWS unit 수강 여석 제한 있음', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기/듣기/말하기 17점, 쓰기 19점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, 'OSHC(Overseas Student Health Cover) 국제학생 보험가입 의무 (2023년 기준 AUD 348/학기, 학기마다 비용 상이)'), + (38, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, '※ 24-1학기에 한하여 ''Destination Australia Cheung Kong Exchange Program Scholarship'' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), + (39, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 19점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, '한 학기 등록금: 약 10,400 AUD (in state 적용)
※ 24-1학기에 한하여 ''Destination Australia Cheung Kong Exchange Program Scholarship'' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), + (40, 2, 4, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'Faculty of Economics and Business로만 지원가능', NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, NULL, NULL), + (41, 3, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, '기숙사 여석 많지않음', NULL), + (42, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', 'https://fontys.edu/Short-term-programmes/Exchange-programmes/Exchange-programmes-per-faculty.htm', NULL, NULL, NULL, NULL, NULL), + (43, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 경영학 수업만 개설됨
- 경영학에 대한 사전의 기초적인 과목을 이수하여야 함
- 한 학기 최대 30ECTS까지 수강 가능함', NULL, NULL, NULL, '- 외부숙소 제공
', NULL), + (44, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함
- 교환학생에게 제공되는 수업만 수강 가능
- Faculty of Engineering 내에서 2/3이상의 수업을 수강하여야 함
- 30 ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 1일)', NULL, NULL, '- 교외 숙소', NULL), + (45, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 본교 기초과목 이수사항에 따라 지원이 제한될 수 있으나 소속전공과 정확하게 일치 하지 않아도 지원은 가능(연관 전공이어야 함)
- 최소 7.5 ECTS, 최대 30ECTS 수강 가능
- 교차 수강 가능(선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', NULL, NULL, '- 제공(학교 운영 기숙사 아님)
- 선착순 배정', NULL), + (46, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점
- TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점
외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, NULL), + (47, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 일반적으로 Business, Computer Sciences, Engineering, Tourism Field에서 영어강의를 교환학생에게 제공함 ', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, '교환 학생 프로그램에 독일 어학 수업 포함'), + (48, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- Language 관련 강의 수강 제한적, Social Faculty의 경우 영어강의 제공하지 않음', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- TOEIC: 말하기 160점 이상, 쓰기 150점 이상
- IELTS: 모든 영역에서 5.5 이상
- 어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, '※ BW Scholarship RWU : Fall term 2023/24 (Sept – Feb) 기간동안 한 달에 €850 장학금 수령 (제출서류 및 기한 등 자세한 정보는 국제처 홈페이지 해외대학정보 공지글 참고)'), + (49, 2, 9, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요', '각 단과대학별 상이하므로 국제처 홈페이지 해외대학정보 Fact sheet 및 홈페이지 참조 바람', NULL, NULL), + (50, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '보험 관련 정보: https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/health-insurance'), + (51, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- International Study Programme(ISP) 내 수업만 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, '기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음', NULL), + (52, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원 불가능 전공: Pharmacy, Human Medicine and Veterinary Medicine
- Biochemistry, Bioinformatics and Biology, Law 지원 제한적 (학과 사전승인 필요)
- Business Administration, Economics 학과 수업 대부분이 독일어로 진행, 독일어 가능자 지원 권장', NULL, '- Department of Humanities, Social Science, Business Administration and Economics 수강요건: 독일어 공인성적 B2 레벨 이상의 증빙 필수, Department of Natural Science 수강요건: 독일어 공인성적 B1 레벨 이상 증빙 필수
- John F Kennedy Institute for North American Studies 수강요건: 영어 공인성적 C1 레벨 이상의 증빙 필수', '※ 주로 Departments of English and North American Studies에서 영어강의 제공, 이 외의 학과 영어수업 제한적', '- 기숙사 신청 제한적, 여석 부족으로 기숙사 배정을 못 받을 가능성 있음', '- 보험 관련 정보: http://www.fu-berlin.de/en/studium/international/studium_fu/einreise_aufenthalt/krankenversicherung'), + (53, 4, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '※ Faculty of Economics, Business Administration에 한하여 지원 가능
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '보험 관련 정보: https://international.fhws.de/en/fhws-international/ways-to-fhws/applicants-and-student-support/before-your-arrival-at-fhws/ '), + (54, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, NULL, NULL, '- 기숙사 신청 제한적', '- 보험 관련정보: https://signupbarmer.de/?utm_source=barmer_schmalkalden'), + (55, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: International Project Management, Smart City Solution
- Civil Engineering, Surveying, Mathematic : 독일어 가능자 지원 권장, Architecture, Interior Architecture and General Management : 독일어 수업 수강 필수', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 모든 영역에서 6.0 이상', NULL, '기숙사 신청 제한적', NULL), + (56, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Health, Midwifery, Extra-occupational courses ', NULL, '어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, NULL), + (57, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함
- Department of Architecture and Civil Engineering의 경우 영어강의 제공하지 않음', NULL, NULL, NULL, NULL, NULL), + (58, 3, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 영어스피킹 중급 이상 학생 지원 권장
- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '- 독일 어학 수업 수강 필수
- 독일어 어학성적으로 지원하는 경우에 한하여 2학기로도 지원 가능'), + (59, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함
- 지원 불가능 전공: Human medicine, dentistry, pharmacy, law and psychology. Practical sports', NULL, '어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, NULL), + (60, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함
- 지원 불가 전공: Medicine', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, NULL), + (61, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: Medicine, Programmes offered by the Centre for Lifelong Learning', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC S/W : 말하기 120점 이상, 쓰기 120점 이상
- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, '보헙관련 정보: https://uol.de/en/exchange-studies/health-insurance'), + (62, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, NULL), + (63, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원 제한 전공 : Psychology', NULL, NULL, NULL, NULL, NULL), + (64, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, NULL), + (65, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '※ Faculty of Business Administration, Global Finance and Banking, Economics에 한하여 지원 가능
- Campus Schwäbisch Hall, Campus Künzelsau 내 강의 수강 가능', NULL, NULL, NULL, NULL, NULL), + (66, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함
- 학기당 최소 30ECTS 수강필수
- 지원불가전공: Nursing/Dentitstry/Odontology 등
- 교차 수강 가능', NULL, NULL, NULL, '- 재공하나 보장되는 것은 아님
- 1인실 기준으로 4830~5387SEK/1달
', NULL), + (67, 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없으나 일치하는 것을 권고함. 본교에서 기초과목을 이수하여야 함
- 학기당 최소 30ECTS 수강필수
- 교차 수강 가능(단, 선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)
- 지원불가 전공 : Fashion Designer, Textile Design', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT: 쓰기 20점 이상
- IELTS: 모든 영역에서 5.5이상', NULL, '- 미제공, 외부숙소 지원
- 2500~6000 SEK/1달', '2024년 봄학기 : 15 January - 2 June 2024
- Orientation Days : 11-12 January 2024.'), + (68, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 지원 권장', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함.
TOEIC 말하기 160점 이상, 쓰기 150점 이상', NULL, NULL, NULL), + (69, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- School of Law로만지원가능(지원가능전공:international relations, law)
- 반드시 24ECTS 이상을 수강하여야 함', '지원 전 권역 담당자와 사전상담 요망', NULL, NULL, NULL, NULL), + (70, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 영어과목이 제한적임. ', NULL, '영어과목이 다양하지 않으므로 신중한 지원 요망. 스페인어 공인어학성적(DELE 중급이상 성적)이 있을 시 추후 합격 후 담당자에게 제출하시길 권장드림(필수아님)', NULL, '기숙사 여석 많지않음', NULL), + (71, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, 'TOEIC의 경우 S/W 합산 320점 이상 추가로 필수 제출', NULL, NULL, NULL), + (72, 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '제한학과 많음. (Factsheet참조및Factsheet언급된 제한학과 외에도 학기마다 제한학과 발생가능성있음). 지원 전 권역 담당자랑 사전상담 요망. 학기당 30ECTS수강해야 LA승인남. 성적처리 늦은 편이라 8차 학기 수학자는 성적처리 늦은 거 감안하고 추가 이에 따른 불편함이 있음을 인지후 지원요망. ', '지원 전 권역 담당자와 사전상담 요망', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 듣기 및 쓰기 18점, 읽기 18점, 말하기 20점, 쓰기 18점 이상
- IELTS : 모든 영역에서 6.0이상', NULL, NULL, '영국 생활비 및 숙소비용 유럽권 지역 중 상대적으로 매우 높은편. 지원전 반드시 사전고려 요망'), + (73, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '학교인근 외부 숙소는 있지만, 외부업체운영숙소라 대학관할아님', NULL), + (74, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점 이상, 쓰기 17점 이상, 말하기 20점 이상, 듣기 17점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상
''- TOEIC의 경우 S/W 점수 합산 310점 이상 ', NULL, '자체기숙사는 없음. 교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있음(선착순 경우많음). 더블룸 기준약 한달에 € 340 per month (기숙사 종류게 따라 가격 차이 유) 예산잡으면됨.', NULL), + (75, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: History, Philosophy, Art History, theology
(영어과목 수가 그리 많지는 않으므로, 사전 확인필요)
''- 학기당 최소 15ECTS 수강신청해야 함', '봄학기에는 영어과목이 극히 제한적으로 열린다고 함. 지원 전 권역 담당자와 사전상담 요망', NULL, NULL, '학교에서 몇가지 기숙사 옵션 합격시 연결예정.', NULL), + (76, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '기숙사없음', NULL), + (77, 5, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, '지원 전 권역 담당자와 사전상담요망. 주로 학부보다 석사과정에 영어교과목이 개설된 편', NULL, NULL, NULL, '양교 교류협약에 따라, Bovisa 캠퍼스로만 지원가능'), + (78, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '지원 전 권역 담당자와 사전상담 요망', '지원 전 권역 담당자와 사전상담 요망. 기존 파견자 없어서 후기 자료없음.', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, NULL, NULL), + (79, 3, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, NULL, NULL), + (80, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 본인 전공이 일치하지 않아도 지원가능하나 수업따라가기가 어려울 수 있으므로 배경지식이 없다면 지원에 신중할것.. Faculty ofFine Arts/Music/Medicine은 교환학생 지원불가학부임.', '지원 전 권역 담당자와 사전상담 요망', NULL, NULL, NULL, NULL), + (81, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '기숙사 입사경쟁률 매우높음', NULL), + (82, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', NULL, '- TOEIC R/C 최소 385점 이상 TOIEC L/C 최소 400점 이상', NULL, '기숙사 월260유로 정도임', NULL), + (83, 3, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원 전 권역 담당자와 사전상담 요망', NULL, NULL, NULL, '- 기숙사 없음.', NULL), + (84, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, NULL, NULL), + (85, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원전공이 본교 소속 전공 또는 제2전공과 일치하여야 함 : 경영학 과목만 개설됨
- 교차 수강 가능
- 최소 9, 최대 36ECTS 수강신청 가능', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', NULL, NULL, '- 미제공', '- 2024년 봄학기 : 2024년 1월 ~ 4월'), + (86, 4, 7, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 지원 가능 전공 : IT계열(컴퓨터공학)
- 교차 수강 불가
- 학기당 최소 15ECTS, 최대 30ECTS 수강', NULL, NULL, NULL, '- 미제공하나 교환학생들이 외부 숙소를 찾을 수 있도록 지원', NULL), + (87, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: Computer Science / Information technology(소속전공과 지원전공이 일치하여야 함)
- 교차수강 불가 
- 학기당 최소 10ECTS, 최대 30ECTS 수강', '- 어학성적표가 2024년 11월 1일까지 유효하여야 함', NULL, NULL, '- 미제공하나 교환학생들이 외부 숙소를 찾을 수 있도록 지원', NULL), + (88, 6, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영학 계열(소속전공과 지원전공 일치) :
- 교차 수강 가능
- 학기당 최소 15ECTS, 최대 34ECTS 수강', NULL, NULL, NULL, '- 미제공
- 숙소관련 문의 : gdesforges@omneseducation', NULL), + (89, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것을 권고함 : 경영학
- 15ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 30일)', NULL, NULL, '- 미제공', NULL), + (90, 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치 또는 유사하여야 함 : 전공이 제한적이므로 반드시 홈페이지에서 지원 가능 전공을 확인할 것
- 최대 30ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', NULL, NULL, '- 미제공', NULL), + (91, 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차수강 가능
- 최소 15ECT, 최대 33ECTS까지 수강', '- 어학성적표가 해당 대학 개강일까지 유효해야 함', NULL, NULL, '- 미제공
- 외부숙소 배정에 대해 지원', NULL), + (92, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영학계열
- 학기당 최소 15ECTS, 최소 30~35ETS 수강해야 함
(Program에 따라 상이함)', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 15일)', NULL, NULL, '- 미제공', NULL), + (93, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공 : 경영학계열(소속대학에서 경영학 관련 기초 과목을 이수하여야 함)
- 학기당 최소 20ECTS, 최대 30ECTS 수강', NULL, NULL, NULL, '- 교외 숙소 제공
- 한달에 250~600€', NULL), + (94, 6, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속 전공과 지원전공이 일치해야 함
- 교차수강 가능
- 최소 15ECTS, 최대 30ECTS 수강신청 가능', NULL, NULL, NULL, '- 미제공
- 숙소관련 문의 : gdesforges@omneseducation', NULL), + (95, '60ECTS 이상 이수해야 함', 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 개설전공 : 경영경상계열, 물류(소속전공과 지원전공이 일치할 것을 권고)
- 전공간 교차수강 불가
- 최소 수강학점 : 15ECTS', '- 어학성적표가 해당 대학 신청서 제출 시 유효해야 함(~10월 31일)
- 최저 성적요건은 프로그램에 따라 상이함(별도 문의할 것)
- 최저이수학기 : 2개 학기 이상 이수하고, 60ECTS 이상 취득하여야 함(세부 문의는 국제교류팀으로 연락)', '* 수강 과정에 따라 영어 성적이 상의하니 유의할 것
- UNDERGRADUATE: IBT 72/ TOEIC 750/ IELTS 5.5
- GRADUATE: IBT 83/ TOEIC 790/ IELTS 6.0', NULL, '- 미제공', NULL), + (96, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없으나 기초과목을 이수해야 함
- 최대 30ECTS 수강', NULL, NULL, NULL, '- 미제공', '2024 봄학기 : January - April/May
(depending on the program of study)'), + (97, '120ECTS 이상 이수해야 함 (비고란 참조)', 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영학계열(소속전공과 지원전공이 일치 또는 유사하여야 함, 기초과목을 이수해야 함)
- 교차수강 불가
- 최소 15에서 최대 34ECTS
(프로그램에 따라 학기당 수강 가능 ECTS가 상이함)', '-어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 20일)
- 최저이수학기 : 120ECTS 이상 취득하여야 함(세부 문의는 국제교류팀으로 연락)', NULL, NULL, '- 미제공
- 숙소 예약에 대한 가이드 제공', NULL), + (98, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 교차수강 가능
- 학기당 최소 15 ECTS 수강', NULL, NULL, '추후 이메일로 안내 예정', '- 기숙사제공(CROUS)', NULL), + (99, 2, 5, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공과 지원전공이 일치하여야 함
- 최대 두 개의 establishment에서 수강 가능
- 지원불가능전공: Medicine, Midwifery, Nursing, Physiotherapy, Chiropody, Law, Digital animations and Video gamrs / 2nd year of Master
- 최소 20ECTS 수강', NULL, NULL, NULL, '- 기숙사 제공(선착순)', NULL), + (100, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 경영대학과 릴 카톨릭대학 ESTICE와의 별도 협약에 따라 경영대학 소속 학생에 한하여 선발함
- 주전공과 지원전공이 일치할 것을 권고함(필수는 아님)
- ESTIC에서 개설된 교과목만 수강 가능
- 최소 12~최대 32ECTS 수강', '어학성적이 2024년 1월까지 유효해야 함', NULL, NULL, '- 미제공', NULL), + (101, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속대전공과 지원전공이 일치할 필요 없음
- 교차수강 불가하며 교확학생에게 오픈된 교과목만 수강 가능', NULL, NULL, NULL, '- 제공(선착순)
- 1200EUR/1학기', NULL), + (102, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 건축학부 재학생에 함함', NULL, '- 프랑스어 성적 제출이 필수는 아니나 대부분의 과목이 프랑스어로 진행되므로 프랑스어를 사전에 공부할 것을 권고함', NULL, '- 미제공
', NULL), + (103, 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 서로 다른 프로그램, 언어, 학과 교차 수강 불가능
- 제한 전공 : https://international.bsb-education.com/course-catalogues/?lang=en', '- 어학성적표가 11월 30일까지 유효하여야 함
- 최저이수학기 관련 : "How to choose my courses" : https://international.bsb-education.com/course-catalogues/?lang=en 참조', NULL, NULL, '- 학교 직영 기숙사는 없으나 CROUS, STUDAPART과 제휴한 숙소 제공(선착순)', NULL), + (104, 3, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 경영학계열, 소속 전공과 지원전공이 반드시 일치할 필요는 없으나 관련 기초과목을 이수하여야 함
- 학기당 15~30ECTS 수강', NULL, NULL, NULL, '- 미제공', '- https://international.audencia.com/student-life/before-you-arrive'), + (105, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속대 전공과 지원전공이 일치할 필요는 없으나 관련 기초과목을 이수하여야 함
- 1년간 최소 8학점(프랑스어 5학점, 프랑스문화 3학점) 이상 이수
- 교차수강 가능(2nd year of master law, LL.M제외)', NULL, '- 영어점수는 아래 각 세부점수를 만족해야 함
- TOEFL IBT: 모든 영역 20점 이상
- IELTS: 모든 영역에서 6 이상
- TOEIC: 말하기, 쓰기, 듣기, 읽기 영역 합산 1020점 이상 ', NULL, '- 외부숙소제공', NULL), + (106, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영경상계열(소속대학 전공이 관련 학과여야 함)
- 하나의 프로그램 내에서 수강 가능
- 최소 24ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 26일)
* 최저이수학기
- Bachelor Year 2과정 : 최소 2개 학기 이상 이수
- Bachelor Year 3과정 : 최소 4개 학기 이상 이수', NULL, NULL, NULL, NULL), + (107, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속대 전공과 지원전공이 반드시 일치할 필요는 없으나 관련 기초과목을 이수하여야 함', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', NULL, NULL, '- 미제공', NULL), + (108, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속 전공과 지원 전공이 일치하여야 함
- 교차 수강 가능
- 최대 학기당 30ECTS 수강 가능', NULL, NULL, NULL, '- 제공(단, 제한적이며 선착순 배정)
- 1600EUR/학기당', NULL), + (109, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속 전공과 지원전공이 일치해야 함 : 경영학, 경영정보공학으로 지원 가능
- 기초지식이 있는 경우에 한하여 교차수강 가능함(Design 및 Fine arts는 전공 학생만 수강 가능)
- 학기당 30 ECTS 수강 권장', NULL, NULL, NULL, '- 미제공', '- 기존 "라티대학"이 "LAB대학"으로 교명 변경'), + (110, 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치해야 함
- 최소 수강 학점 20ECTS, 30ECTS 수강 권장', '- 어학성적표가 해당 대학 개강일까지 유효해야 함', NULL, NULL, '- 미제공
- 교환학생의 경우 LOAS(www.loas.fi)를 통해 숙소를 신청함
- 290~420EUR/1달', '
'), + (111, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: Faculty of Engineering and Business 내 전공 지원 가능(우리대학의 경우 공과대학, 경영대학, 소프트융합대학 소속 학생만 신청 가능)
- 소속대 전공과 지원전공이 일치해야 함
- 학기당 최대 30ECTS 수강신청 가능', NULL, NULL, NULL, '- 미제공
- 한달에 280~370€', NULL), + (112, 2, 2, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치 또는 유사하여야 함
- 교차 수강 가능
- 한 학기 30ECTS를 수강 권고', NULL, NULL, NULL, '- 미제공', NULL), + (113, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능', NULL, '-TOEFL IBT minimum Scores :
Reading 18, Writing 23, Listing 18, Speaking 19
- 영어성적은 교환학생을 하는 내내 유효한 성적표여야 함', NULL, '- 기숙사 보유(한학기에 250BND)', '- COVID 관련 안내 사이트 https://www.pmo.gov.bn/TAPressRelease/[FINAL]%20PR%20JKPC%2012023%20-%20Pengemaskinian%20Sukat-Sukat%20Pengawalan%20COVID-19.pdf
-수업방식: 온라인, 오프라인 혼합'), + (114, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능', NULL, '- 유효한 영어공인인증시험점수는 2021년 9월 26일 이후 응시한 시험점수에 한함
- 어학점수 미보유 시 어학요건 기준에 준하는 어학실력을 보유하고 있다는 내용을 담은 지도교수 추천서를 제출하여 어학점수 갈음 가능', NULL, '기숙사 없음', NULL), + (115, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능
-지원불가 전공: Master of Science in Computer Science & Data Analytics and Master of Science in Electrical & Power Engineering', NULL, '- 유효한 영어공인인증시험점수는 2021년 11월 1일 이후 응시한 시험점수에 한함', NULL, '- 기숙사 보유 (한학기700-950 USD)
', '- COVID 관련 안내 사이트
https://koronavirusinfo.az/az'), + (116, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 캠퍼스별 수강 가능 전공 다 다름 (매학기 변동 가능)
- 아래 캠퍼스 별 수강 가능 전공 참조
-BINUS International Senayan Campus: Fashion, Business, Computer Science, Accounting, Graphic Design and New Media, Communication, Business Information System,

-BINUS Kemanggisan: Hotel Management, Tourism, Computer Science, Accounting, Civil & Industrial Engineering, Management, Marketing communication, Business Law, English Literature, Architecture

-BINUS Alam Sutera: International Business Management, Computer Science, International Relations, Food Technology

-BINUS Bekasi: Bar & Hotel Management', NULL, 'IELTS overall band score of 6
- 유효한 영어공인인증시험점수는 2022년 3월 1일 이후 응시한 시험점수에 한함', NULL, '- 기숙사 보유 (교환학생들은 도착 후 Limited Stary Permit이 나오는 한달동안 반드시 기숙사에서 거주해야함)
- 2인실 한달에 USD240, 1인실 한달에 USD280
-https://binus.ac.id/binussquare/', NULL), + (117, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'https://kuglobal.w3.kanazawa-u.ac.jp/wp/wp-content/uploads/KUEP2022-20231.pdf', 'College of Science and Engineering or College of Medical, Pharmaceutial and Health Sciences의 경우 3학년 이상만 신청 가능', NULL, NULL, NULL, NULL), + (118, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원불가 : Professional School of Law', NULL, 'Faculty of International social Sciences에 한해 영어점수 필요(TOEF L IBT 80, IELTS 6.0)
''-2년 이상의 일본어 공부 이력도 신청 가능', NULL, '- WAKEIJUKU https://www.wakei.org/english/
- KITAZONO WOMEN''S STUDENT https://www.kitazono-j.co.jp/
- CAMPUS VILLAGE KOTAKEMUKAIHARA https://749.jp/cd/2455/
- CAMPUS VILLAGE AKATSUKASHINMACHI https://749.jp/cd/2442/', NULL), + (119, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 학부생만 지원 가능
- Higashiosaka campus만 지원 가능(약학과 지원불가)
- 영어과정은 International Studies 또는 Business 계열만 가능', NULL, NULL, NULL, '기숙사 없음, 학교밖 숙소는 약 270,000엔/학기', NULL), + (120, 2, 9, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '2.30 points *NU 환산표 참조 : https://www.niigata-u.ac.jp/en/wp-content/uploads/2020/12/method.pdf', '교환학생 입학 기준은 별도로 없으나, 영어 일본어 교과목 수강을 위해서는 기본 언어능력 필요(일본어의 경우 N2)', NULL, NULL, NULL), + (121, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-Hakusan Campus에서만 수강 가능,
''-교환학생을 위한 교과목만 수강 가능 https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/#acl ', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'TOEIC:TOEIC L&R+(TOEIC S&W*2.5)
정규교과목 수강은 N2이상 ', NULL, NULL, NULL), + (122, 4, 3, NULL, 'HOME_UNIVERSITY_PAYMENT', 'Hakusan Campus에서만 수강 가능', '*해당 학교 일정 상 10월초까지 서류제출 필요
3+1 프로그램 신청자 대상으로 하는 별도 장학금 수혜 가능', NULL, NULL, NULL, '- [3+1 프로그램]만 지원 가능 (현지 취업을 위한 맞춤형 프로그램, 파견차수 학기 유의할 것)
- 3+1 프로그램은 6차, 7차 학기를 일본대학에서 수학하면서 현지에서 직장을 구한 후 인하대에서 마지막 8차 학기 이수하고 일본으로 돌아가 직장생활을 하는 프로그램임'), + (123, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, '- 기숙사 미보유, 교환학생을 위한 외부 숙소 보장
- 280,000엔~330,000엔/학기 (2023년 기준)', NULL), + (124, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', '*해당 학교 일정 상 10월초까지 서류제출 필요', '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, NULL), + (125, 2, 1, 'IRRELEVANT', NULL, '교환학생 지원가능 : Department of Global English, Department of Japanese culture, Department of Media and Information, Department of Psychology.', '여학생만 신청가능', NULL, NULL, '기숙사 없음, 계약된 외부 기숙사 사용-“Maison de Claire Ibaraki” 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)', NULL), + (126, 2, 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월', NULL), + (127, 2, 10, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', '- 전공별 TO에 따라 선발 (최대인원을 넘지 않도록)
- Faculty of Global and Science Studies : 최대 5명
- Faculty of Economics: 최대 8명
- Faculty of Engineering: 최대 3명
- Faculty of Humanities: 최대 2명
- Others: 최대 2명', 'Graduate School of Ecomnomics 토플 79 이상 토익 730이상 필요', NULL, '교환학생은 off-campus dormitory 거주의무', NULL), + (128, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, '*해당 학교 일정 상 10월초까지 서류제출 필요', NULL, NULL, NULL, '-봄학기는 한 개 학기만 교환학생 파견 가능
-교환학생 용 "International Exchange Program" 제공 (일본어 능력이 높은 학생에게만 오후에 제공되는 일반 과정 수강 가능)'), + (129, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '여학생만 신청가능', NULL, NULL, '36,500엔/월, 식비별도', NULL), + (130, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', ' Please refer to "Requirements and Course Lists" for further information. https://www.waseda.jp/inst/cie/en/exchange/application ', NULL, '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, NULL), + (131, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원불가 : Professional Graduate Program (Law School, Business School)', NULL, 'Commerce / Science & Engineering/ Global Informatics', NULL, 'Seiseki-Sakuragaoka dormitory (off-campus)는 꽤 경쟁이 치열함. On-campus와 Off-campus 모두 약 1,800-1,900 EUR/학기', NULL), + (132, 2, 2, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', '경제학, 국제통상학과만 지원 가능', '*해당 학교 일정 상 10월초까지 서류제출 필요', NULL, NULL, NULL, NULL), + (133, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것을 권장함
', NULL, '- 유효한 영어공인인증시험점수는 2021년 10월 16일 이후 응시한 시험점수에 한함', NULL, '- 기숙사 보유 (한달에 550 TL ~ 2000 TL)
', '- 오프라인 수업으로 예상하나 온라인 수업 등으로 변동 가능성 있음
- COVID 관련 안내 사이트
https://covid19.saglik.gov.tr/'), + (134, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-소속전공과 지원전공이 일치할것
-학부 학생이 석사용 수업 수강할 수 없음', NULL, '*TOEFL IBT minimum Scores :
Reading 20, Writing 20, Listing 20, Speaking 20
유효한 영어공인인증시험점수는 2021년 12월 1일 이후 응시한 시험점수에 한함
https://www.ozyegin.edu.tr/en/student-services/application-admission/language-proficiency-requirement', NULL, '-기숙사 보유하나 입사 보장은 불가', '-COVID 관련 안내 사이트
https://www.ozyegin.edu.tr/en/covid-19'), + (135, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능', NULL, 'IELTS overall band score of 6 with no band lower than 5.5', NULL, '- 기숙사 2인실(매달 263-302 USD)', '-COVID 관련 안내 사이트
https://www.coronavirus.gov.hk/eng/index.html'), + (136, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-소속전공과 지원전공이 일치할것
- College of Science(Chemistry/ Mathematics / Physics) 이 학과로 홍콩시립대에서 수강 시 위 학과에서 9학점 이상 이수 필수 수강을 해야함
-지원제한 전공: Biomedical science
-몇몇 수강인원이 제한된 학과는 교환학생을 받지 않음 주의
-선행 과목 수강이 필수인 학과의 경우 수강 이력이 없을 시 수강 불가', NULL, 'ielts overall band 6.5
-유효한 영어공인인증시험점수는 2021년 10월 2일 이후 응시한 시험점수에 한함', NULL, '- 기숙사가 있으나 기숙사 배정이 상당히 어려움
- 대략적 비용: 학기당 HKD$ 10,850 ~ HKD$ 21,700', '- COVID 관련 안내 사이트 https://www.cityu.edu.hk/fmo/default.aspx?PageID=covid19infoctr'), + (137, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 수강 제한 전공: IMBA, EMBA, IMAS', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', NULL, '- 기숙사 보유
- 대략적 비용: 한학기에 NTD$11,000~ 33,500', NULL), + (138, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-소속전공과 지원전공이 일치할것
-중국어 실력이 유창하지 않으면 영어로 강의되는 수업만 수강 가능함
- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', NULL, ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유 (한학기235 USD-535 USD)', '-COVID 관련 안내 사이트
https://www.cdc.gov.tw/En'), + (139, 4, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 지원가능 전공 확인 : https://www.ncu.edu.tw/en/pages/index.php?num=2
- 교환학생용으로 제공되는 영어강의는 본래 대학원생용이라 학부 3학년 또는 4학년 학생이 국립중앙대로의 교환학생에 지원할 수 있음', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 영어성적은 지원일로부터 2년간 유효한 성적표여야 함', NULL, '기숙사 없음', '-COVID 관련 안내 사이트
https://www.cdc.gov.tw/En'), + (140, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능
- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', NULL, ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유
- 한 학기에 약 500-800$
', '-COVID 관련 안내: 병원과 관련된 시설에서는 반드시 마스크 착용 해야함'), + (141, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능
- 주로 인하대 학생들은 중국어 어학과정을 수강함
- 최소 수강학점은 없으며, 주로 3~6과목 (2~4학점/1과목)을 수강함
', NULL, '- 본교 중국어 어학시험에 응시하여야 함
- 중국어 연수 프로그램 : 영어,중국어 성적이 없어도 지원 가능
- 학위과정수업(영어) : IBT 59 이상', 'English taught courses are generally determined by the schools at the end of the previous semester. We are not sure about it until then. ', '- 학교에 기숙사가 있으나 입사 보장은 불가
International Students Building (only for foreign students): Single room with bedding packs, air-conditioner and bathroom,500RMB
Local Student Dormitory: 4-6 person room without bedding packs, 600-1000RMB ', NULL), + (142, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 지원제한 전공 : Medical Science', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
* 중국어 과정
1) 중국어학연수과정 : 어학성적 필요 없음
2) Diplomacy (School of International and Public Affairs) : 유효한 HSK 5급 180점 이상, TOEFL 550(PBT), 213(CBT), 79(IBT) 또는 IELTS 6.0.이상을 보유하여야 함(HSK및영어 동시만족)
3) 중국어 과정(석사):HSK 5급 180점 이상
4) 기타 중국어 과정 : HSK 4급 180점 이상
* 영어 과정 : 길림대 내 영어테스트가 있을 예정임
- 영어과정을 들을 경우, 영어성적표을 제출하여야 하며 지원일로부터 2년간 유효한 성적표여야 함', NULL, '- 기숙사 보유 (6개월 이상 거주 시 신청 가능)
- 대략적 비용: 한달에 1500RMB ', NULL), + (143, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야함(교차수강 불가능)
- 대부분의 교환학생들은 중국어 어학 수업을 수강함', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어로 된 전공 강의를 수강하기 희망할 경우, 최소 HSK 5급 180점 이상의 점수를 보유하여야 함
-중국어학연수과정 : 어학성적 필요 없음', '- 영어수업을 제공할지 결정되지 않았음', '- 기숙사 보유 (한학기 5000RMB-6000RMB: 겨울방학 및 여름방학 제외)
', NULL), + (144, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '소속전공과 지원전공이 반드시 일치할 필요는 없으나 학과에 상관없이 교차 수강은 불가능
-교환학생 대상으로 별도 제공된 수강 가능 교과목안에서만 수강 가능  ', NULL, '- 유효한 영어공인인증시험점수는 2021년 11월 16일 이후 응시한 시험점수에 한함
- 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유(한 학기 약 7800yuan)', NULL), + (145, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', ' 소속전공과 지원전공이 일치해야 함. 단, 중국어 어학과정 학생의 경우 전공 무관
- 최소 학점은 없으며, 최대 24학점 수강 가능
- 체육교육과 전공은 제공하지 않음
- 지원 가능 전공 https://ipo.wh.sdu.edu.cn/kristudy/info/1021/1752.htm', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어 수업을 들을 학생은 HSK4급 보유를 권장함
- 영어 수업을 들을 학생은 TOEFL IBT 80점, IELTS 6등급 보유를 권장함
- 학점 4.0 만점에 2.3, 100점 만점에 70이상인 학생만 지원 가능', NULL, '- 기숙사 보유 (한학기 4,000~5,000 RMB, 금액은 변동가능성있음)', NULL), + (146, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '''- 소속전공과 지원전공이 일치하여야 함 (교차 수강 불가능)', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어 어학강좌만 수강할 경우 HSK 또는 영어성적 제시 불필요
- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK level 4 이상이어야 함)', NULL, '- 기숙사 보유(한 학기 3100-3600RMB)', NULL), + (147, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
', NULL, '- 본교 중국어 어학시험에 응시하여야 함
- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK4급이상 보유자만 가능)
-유효한 영어공인인증시험점수는 2021년 12월 21일 이후 응시한 시험점수에 한함', NULL, '-기숙사 보유 (2인실/ 매달 450-600RMB)', NULL), + (148, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어 연수 프로그램 : 어학성적 불필요
- 영어강의 없고 중국어로 하는 전공수업만 제공함. HSK5급 보유자만 지원가능(확인중)', NULL, '- 기숙사 보유 (한학기 1100-1500 USD) ', NULL), + (149, NULL, 10, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '- 어학요건이 대학별로 상이하므로 반드시 Program Guide 참고할 것', NULL, NULL, '- 지원 전 반드시 국제교류팀 담당자와 상담할 것'); \ No newline at end of file From b6ef7f54ee3d95f943faaa1222cc55c238e2c992 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 5 Feb 2024 23:52:26 +0900 Subject: [PATCH 027/158] =?UTF-8?q?feat:=20=EB=8C=80=ED=95=99=EA=B5=90=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/JwtAuthenticationFilter.java | 4 + .../security/SecurityConfiguration.java | 4 +- .../custom/exception/ErrorCode.java | 2 + .../solidconnection/entity/Application.java | 2 + .../solidconnection/entity/Country.java | 2 + .../entity/GpaRequirement.java | 2 + .../entity/LanguageRequirement.java | 2 + .../solidconnection/entity/Region.java | 2 + .../solidconnection/entity/University.java | 2 + .../entity/UniversityInfoForApply.java | 16 +++- .../entity/WishUniversity.java | 2 + .../controller/UniversityController.java | 22 +++++ .../dto/LanguageRequirementDto.java | 24 ++++++ .../university/dto/UniversityDetailDto.java | 41 +++++++++ .../LanguageRequirementRepository.java | 12 +++ .../UniversityInfoForApplyRepository.java | 13 +++ .../repository/UniversityRepository.java | 9 ++ .../university/service/UniversityService.java | 85 +++++++++++++++++++ 18 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/university/controller/UniversityController.java create mode 100644 src/main/java/com/example/solidconnection/university/dto/LanguageRequirementDto.java create mode 100644 src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java create mode 100644 src/main/java/com/example/solidconnection/university/service/UniversityService.java diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 954d7999f..d11731af3 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -78,6 +78,10 @@ private HashSet getPermitAllEndpoints() { permitAllEndpoints.add("/auth/kakao"); permitAllEndpoints.add("/auth/sign-up"); + // 대학교 정보 + permitAllEndpoints.add("/university/detail/**"); + permitAllEndpoints.add("/university/search/**"); + return permitAllEndpoints; } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 1012c4256..47c5a2569 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -49,7 +49,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers( "/", "/index.html", "/favicon.ico", "/img/profile/pre", - "/auth/kakao", "/auth/sign-up") + "/auth/kakao", "/auth/sign-up", + "/university/detail/**", "/university/search/**" + ) .permitAll() .anyRequest().authenticated()) .addFilterBefore(this.jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index bd92d3bf5..1fa3b236c 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,6 +7,8 @@ @Getter @AllArgsConstructor public enum ErrorCode { + UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교 지원 정보입니다."), + UNIVERSITY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교입니다."), REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST.value(), "리다이렉트 uri가 잘못되었습니다."), NOT_DEFINED_ERROR(HttpStatus.BAD_REQUEST.value(), "에러가 발생했습니다."), S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), diff --git a/src/main/java/com/example/solidconnection/entity/Application.java b/src/main/java/com/example/solidconnection/entity/Application.java index 04bbdc9ce..dbb8abcdb 100644 --- a/src/main/java/com/example/solidconnection/entity/Application.java +++ b/src/main/java/com/example/solidconnection/entity/Application.java @@ -2,8 +2,10 @@ import com.example.solidconnection.type.LanguageTestType; import jakarta.persistence.*; +import lombok.Getter; @Entity +@Getter public class Application { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/solidconnection/entity/Country.java b/src/main/java/com/example/solidconnection/entity/Country.java index dd17decb7..870b40f52 100644 --- a/src/main/java/com/example/solidconnection/entity/Country.java +++ b/src/main/java/com/example/solidconnection/entity/Country.java @@ -2,8 +2,10 @@ import com.example.solidconnection.type.CountryCode; import jakarta.persistence.*; +import lombok.Getter; @Entity +@Getter public class Country { @Id @Column(length = 2, name = "country_code", columnDefinition = "VARCHAR(2)") diff --git a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java index 71415fb21..6c72ae054 100644 --- a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java +++ b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java @@ -1,8 +1,10 @@ package com.example.solidconnection.entity; import jakarta.persistence.*; +import lombok.Getter; @Entity +@Getter public class GpaRequirement { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java b/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java index a9ed6c103..1ce6d175a 100644 --- a/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java +++ b/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java @@ -2,8 +2,10 @@ import com.example.solidconnection.type.LanguageTestType; import jakarta.persistence.*; +import lombok.Getter; @Entity +@Getter public class LanguageRequirement { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/solidconnection/entity/Region.java b/src/main/java/com/example/solidconnection/entity/Region.java index db4ded28f..aaff18137 100644 --- a/src/main/java/com/example/solidconnection/entity/Region.java +++ b/src/main/java/com/example/solidconnection/entity/Region.java @@ -2,8 +2,10 @@ import com.example.solidconnection.type.RegionCode; import jakarta.persistence.*; +import lombok.Getter; @Entity +@Getter public class Region { @Id @Column(length = 10, name = "region_code", columnDefinition = "VARCHAR(10)") diff --git a/src/main/java/com/example/solidconnection/entity/University.java b/src/main/java/com/example/solidconnection/entity/University.java index e4e385491..7100054cf 100644 --- a/src/main/java/com/example/solidconnection/entity/University.java +++ b/src/main/java/com/example/solidconnection/entity/University.java @@ -1,8 +1,10 @@ package com.example.solidconnection.entity; import jakarta.persistence.*; +import lombok.Getter; @Entity +@Getter public class University { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java b/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java index 0c6638dc6..0e6b01466 100644 --- a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java +++ b/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java @@ -3,15 +3,20 @@ import com.example.solidconnection.type.SemesterAvailableForDispatch; import com.example.solidconnection.type.TuitionFeeType; import jakarta.persistence.*; +import lombok.Getter; import java.util.Set; @Entity +@Getter public class UniversityInfoForApply { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(length = 10) + private String term; + @Column(nullable = false) private Integer studentCapacity; @@ -24,11 +29,17 @@ public class UniversityInfoForApply { private SemesterAvailableForDispatch semesterAvailableForDispatch; @Column(length = 10) - private Integer semesterRequirement; + private String semesterRequirement; @Column(length = 1000) private String detailsForLanguage; + @Column(length = 5) + private String gpaRequirement; + + @Column(length = 5) + private String gpaRequirementCriteria; + @Column(length = 1000) private String detailsForApply; @@ -48,9 +59,6 @@ public class UniversityInfoForApply { @OneToMany(mappedBy = "universityInfoForApply", fetch = FetchType.LAZY) private Set languageRequirements; - @OneToMany(mappedBy = "universityInfoForApply", fetch = FetchType.LAZY) - private Set gpaRequirements; - @OneToOne @JoinColumn(name = "university_id") private University university; diff --git a/src/main/java/com/example/solidconnection/entity/WishUniversity.java b/src/main/java/com/example/solidconnection/entity/WishUniversity.java index 46f2e1086..65f65052e 100644 --- a/src/main/java/com/example/solidconnection/entity/WishUniversity.java +++ b/src/main/java/com/example/solidconnection/entity/WishUniversity.java @@ -1,8 +1,10 @@ package com.example.solidconnection.entity; import jakarta.persistence.*; +import lombok.Getter; @Entity +@Getter public class WishUniversity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java new file mode 100644 index 000000000..2c3814e43 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.university.controller; + +import com.example.solidconnection.custom.response.CustomResponse; +import com.example.solidconnection.custom.response.DataResponse; +import com.example.solidconnection.university.dto.UniversityDetailDto; +import com.example.solidconnection.university.service.UniversityService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("university") +@RequiredArgsConstructor +public class UniversityController { + + private final UniversityService universityService; + + @GetMapping("/detail/{universityInfoForApplyId}") + public CustomResponse getDetails(@PathVariable Long universityInfoForApplyId) { + UniversityDetailDto universityDetailDto = universityService.getDetail(universityInfoForApplyId); + return new DataResponse<>(universityDetailDto); + } +} diff --git a/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementDto.java b/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementDto.java new file mode 100644 index 000000000..38fb1ef98 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementDto.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.university.dto; + +import com.example.solidconnection.entity.LanguageRequirement; +import com.example.solidconnection.type.LanguageTestType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LanguageRequirementDto { + private LanguageTestType languageTestType; + private String minScore; + + public static LanguageRequirementDto fromEntity(LanguageRequirement languageRequirement){ + return LanguageRequirementDto.builder() + .languageTestType(languageRequirement.getLanguageTestType()) + .minScore(languageRequirement.getMinScore()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java new file mode 100644 index 000000000..866679642 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java @@ -0,0 +1,41 @@ +package com.example.solidconnection.university.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UniversityDetailDto { + private long id; + private String term; + private String koreanName; + private String englishName; + private String formatName; + private String region; + private String country; + private String homepageUrl; + private String logoImageUrl; + private String backgroundImageUrl; + private String detailsForLocal; + private int studentCapacity; + private String tuitionFeeType; + private String semesterAvailableForDispatch; + private List languageRequirements; + private String detailsForLanguage; + private String gpaRequirement; + private String gpaRequirementCriteria; + private String semesterRequirement; + private String detailsForApply; + private String detailsForMajor; + private String detailsForAccommodation; + private String detailsForEnglishCourse; + private String details; + private String accommodationUrl; + private String englishCourseUrl; +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java b/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java new file mode 100644 index 000000000..76015418e --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.university.repository; + +import com.example.solidconnection.entity.LanguageRequirement; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface LanguageRequirementRepository extends JpaRepository { + List findAllByUniversityInfoForApply_Id(Long id); +} diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java new file mode 100644 index 000000000..8fd0b7800 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.university.repository; + +import com.example.solidconnection.entity.University; +import com.example.solidconnection.entity.UniversityInfoForApply; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UniversityInfoForApplyRepository extends JpaRepository { + Optional findByUniversity(University university); +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java new file mode 100644 index 000000000..0ba600e41 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.university.repository; + +import com.example.solidconnection.entity.University; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UniversityRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java new file mode 100644 index 000000000..5598067d5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -0,0 +1,85 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.University; +import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.university.dto.LanguageRequirementDto; +import com.example.solidconnection.university.dto.UniversityDetailDto; +import com.example.solidconnection.university.repository.LanguageRequirementRepository; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.UniversityRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class UniversityService { + + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final UniversityRepository universityRepository; + private final LanguageRequirementRepository languageRequirementRepository; + + public UniversityDetailDto getDetail(Long universityInfoForApplyId){ + UniversityInfoForApply universityInfoForApply = getValidatedUniversityInfoForApply(universityInfoForApplyId); + University university = getValidatedUniversity(universityInfoForApply.getUniversity().getId()); + + List languageRequirements = languageRequirementRepository + .findAllByUniversityInfoForApply_Id(universityInfoForApplyId) + .stream() + .map(LanguageRequirementDto::fromEntity) + .toList(); + + String countryKoreanName = null; + String regionKoreanName = null; + if(university.getCountry() != null){ + countryKoreanName = university.getCountry().getCode().getKoreanName(); + } + if(university.getCountry() != null){ + regionKoreanName = university.getRegion().getCode().getKoreanName(); + } + + return UniversityDetailDto.builder() + .id(university.getId()) + .term(universityInfoForApply.getTerm()) + .koreanName(university.getKoreanName()) + .englishName(university.getEnglishName()) + .formatName(university.getFormatName()) + .region(regionKoreanName) + .country(countryKoreanName) + .homepageUrl(university.getHomepageUrl()) + .logoImageUrl(university.getLogoImageUrl()) + .backgroundImageUrl(university.getBackgroundImageUrl()) + .detailsForLocal(university.getDetailsForLocal()) + .studentCapacity(universityInfoForApply.getStudentCapacity()) + .tuitionFeeType(universityInfoForApply.getTuitionFeeType().getKoreanName()) + .semesterAvailableForDispatch(universityInfoForApply.getSemesterAvailableForDispatch().getKoreanName()) + .languageRequirements(languageRequirements) + .detailsForLanguage(universityInfoForApply.getDetailsForLanguage()) + .gpaRequirement(universityInfoForApply.getGpaRequirement()) + .gpaRequirementCriteria(universityInfoForApply.getGpaRequirementCriteria()) + .semesterRequirement(universityInfoForApply.getSemesterRequirement()) + .detailsForApply(universityInfoForApply.getDetailsForApply()) + .detailsForMajor(universityInfoForApply.getDetailsForMajor()) + .detailsForAccommodation(universityInfoForApply.getDetailsForAccommodation()) + .detailsForEnglishCourse(universityInfoForApply.getDetailsForEnglishCourse()) + .details(universityInfoForApply.getDetails()) + .accommodationUrl(university.getAccommodationUrl()) + .englishCourseUrl(university.getEnglishCourseUrl()) + .build(); + } + + private UniversityInfoForApply getValidatedUniversityInfoForApply(Long id){ + return universityInfoForApplyRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); + } + + private University getValidatedUniversity(Long id){ + return universityRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + } +} From e457c8b0f1c19aa916c95afa170770384f54c026 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 6 Feb 2024 05:11:34 +0900 Subject: [PATCH 028/158] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C/=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../auth/controller/AuthController.java | 3 +- .../auth/dto/SignUpRequestDto.java | 4 ++ .../auth/service/AuthService.java | 2 +- .../solidconnection/constants/constants.java | 5 ++ .../constants/validMessage.java | 5 ++ .../exception/CustomExceptionHandler.java | 27 ++++++-- .../custom/exception/ErrorCode.java | 2 + ...shUniversity.java => LikedUniversity.java} | 2 +- .../solidconnection/entity/SiteUser.java | 3 +- .../example/solidconnection/s3/S3Service.java | 2 +- .../siteuser/controller/MyPageController.java | 38 +++++++++++ .../siteuser/dto/MyPageDto.java | 34 ++++++++++ .../siteuser/dto/MyPageUpdateDto.java | 27 ++++++++ .../repository/LikedUniversityRepository.java | 11 ++++ .../siteuser/service/MyPageService.java | 64 +++++++++++++++++++ .../siteuser/service/SiteUserValidator.java | 2 +- 17 files changed, 219 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/constants/constants.java create mode 100644 src/main/java/com/example/solidconnection/constants/validMessage.java rename src/main/java/com/example/solidconnection/entity/{WishUniversity.java => LikedUniversity.java} (92%) create mode 100644 src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/MyPageDto.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateDto.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java diff --git a/build.gradle b/build.gradle index c577ad641..09e562894 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'io.lettuce:lettuce-core:6.2.5.RELEASE' implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' implementation 'com.amazonaws:aws-java-sdk-s3:1.12.470' + implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 348f7bf15..c352e7b82 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -9,6 +9,7 @@ import com.example.solidconnection.custom.response.CustomResponse; import com.example.solidconnection.custom.response.DataResponse; import com.example.solidconnection.custom.response.StatusResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -28,7 +29,7 @@ public CustomResponse kakaoOauth(@RequestBody KakaoCodeDto kakaoCodeDto) { } @PostMapping("/sign-up") - public CustomResponse signUp(@RequestBody SignUpRequestDto signUpRequestDto) { + public CustomResponse signUp(@Valid @RequestBody SignUpRequestDto signUpRequestDto) { SignUpResponseDto signUpResponseDto = authService.signUp(signUpRequestDto); return new DataResponse<>(signUpResponseDto); } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java index 3bf9f72be..ecd3b66ec 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java @@ -2,10 +2,13 @@ import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; +import jakarta.validation.constraints.NotBlank; import lombok.*; import java.util.List; +import static com.example.solidconnection.constants.validMessage.NICKNAME_NOT_BLANK; + @Getter @Setter @NoArgsConstructor @@ -16,6 +19,7 @@ public class SignUpRequestDto { private List interestedRegions; private List interestedCountries; private PreparationStatus preparationStatus; + @NotBlank(message = NICKNAME_NOT_BLANK) private String nickname; private String profileImageUrl; private Gender gender; diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index b14e54f1e..9edcd0710 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -80,7 +80,7 @@ public boolean signOut(String email){ } public boolean quit(String email){ - SiteUser siteUser = siteUserValidator.validateExistByEmail(email); + SiteUser siteUser = siteUserValidator.getValidatedUserByEmail(email); siteUser.setQuitedAt(LocalDate.now().plusDays(1)); return true; } diff --git a/src/main/java/com/example/solidconnection/constants/constants.java b/src/main/java/com/example/solidconnection/constants/constants.java new file mode 100644 index 000000000..035d26821 --- /dev/null +++ b/src/main/java/com/example/solidconnection/constants/constants.java @@ -0,0 +1,5 @@ +package com.example.solidconnection.constants; + +public class constants { + public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 7; +} diff --git a/src/main/java/com/example/solidconnection/constants/validMessage.java b/src/main/java/com/example/solidconnection/constants/validMessage.java new file mode 100644 index 000000000..fe6ef01b5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/constants/validMessage.java @@ -0,0 +1,5 @@ +package com.example.solidconnection.constants; + +public class validMessage { + public final static String NICKNAME_NOT_BLANK = "닉네임을 입력해주세요."; +} diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java index 08c7df5ee..f7764198f 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java @@ -5,22 +5,24 @@ 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.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import static com.example.solidconnection.custom.exception.ErrorCode.JSON_PARSING_FAILED; -import static com.example.solidconnection.custom.exception.ErrorCode.NOT_DEFINED_ERROR; +import java.util.ArrayList; +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; @Slf4j @ControllerAdvice public class CustomExceptionHandler { @ExceptionHandler(CustomException.class) - protected ResponseEntity handleCustomException(CustomException e) { - log.error(e.getMessage()); - e.printStackTrace(); - ErrorResponse errorResponse = new ErrorResponse(e); - return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(e.getCode())); + protected ResponseEntity handleCustomException(CustomException ex) { + ex.printStackTrace(); + ErrorResponse errorResponse = new ErrorResponse(ex); + return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(ex.getCode())); } @ExceptionHandler(InvalidFormatException.class) @@ -30,8 +32,19 @@ public ResponseEntity handleInvalidFormatException(InvalidFormatExceptio return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(JSON_PARSING_FAILED.getCode())); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { + List errors = new ArrayList<>(); + ex.getBindingResult().getFieldErrors().forEach((fieldError) -> { + errors.add(fieldError.getDefaultMessage()); + }); + ErrorResponse errorResponse = new ErrorResponse(INVALID_INPUT, errors.toString()); + return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(INVALID_INPUT.getCode())); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleOtherException(Exception ex) { + ex.printStackTrace(); String errorMessage = ex.getMessage(); ErrorResponse errorResponse = new ErrorResponse(NOT_DEFINED_ERROR, errorMessage); return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(NOT_DEFINED_ERROR.getCode())); diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 1fa3b236c..a35ce1019 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -7,6 +7,8 @@ @Getter @AllArgsConstructor public enum ErrorCode { + INVALID_INPUT(HttpStatus.BAD_REQUEST.value(), "값을 입력할 수 없습니다."), + CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 7일이 지나지 않았습니다."), UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교 지원 정보입니다."), UNIVERSITY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교입니다."), REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST.value(), "리다이렉트 uri가 잘못되었습니다."), diff --git a/src/main/java/com/example/solidconnection/entity/WishUniversity.java b/src/main/java/com/example/solidconnection/entity/LikedUniversity.java similarity index 92% rename from src/main/java/com/example/solidconnection/entity/WishUniversity.java rename to src/main/java/com/example/solidconnection/entity/LikedUniversity.java index 65f65052e..77b0105e7 100644 --- a/src/main/java/com/example/solidconnection/entity/WishUniversity.java +++ b/src/main/java/com/example/solidconnection/entity/LikedUniversity.java @@ -5,7 +5,7 @@ @Entity @Getter -public class WishUniversity { +public class LikedUniversity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/example/solidconnection/entity/SiteUser.java b/src/main/java/com/example/solidconnection/entity/SiteUser.java index 0ebea57fa..487bdc91b 100644 --- a/src/main/java/com/example/solidconnection/entity/SiteUser.java +++ b/src/main/java/com/example/solidconnection/entity/SiteUser.java @@ -7,6 +7,7 @@ import lombok.*; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Set; @Entity @@ -46,7 +47,7 @@ public class SiteUser { private Gender gender; @Setter - private LocalDate nicknameModifiedAt; + private LocalDateTime nicknameModifiedAt; @Setter private LocalDate quitedAt; diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 185400a3a..fb6eb945d 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -97,7 +97,7 @@ private void deleteFile(String fileName) { } private String getExProfileImageUrl(String email){ - SiteUser siteUser = siteUserValidator.validateExistByEmail(email); + SiteUser siteUser = siteUserValidator.getValidatedUserByEmail(email); String fileName = siteUser.getProfileImageUrl(); int domainStartIndex = fileName.indexOf(".com"); return fileName.substring(domainStartIndex + 5); diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java new file mode 100644 index 000000000..b79a1bee9 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java @@ -0,0 +1,38 @@ +package com.example.solidconnection.siteuser.controller; + +import com.example.solidconnection.custom.response.CustomResponse; +import com.example.solidconnection.custom.response.DataResponse; +import com.example.solidconnection.custom.response.StatusResponse; +import com.example.solidconnection.siteuser.dto.MyPageDto; +import com.example.solidconnection.siteuser.dto.MyPageUpdateDto; +import com.example.solidconnection.siteuser.service.MyPageService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; + +@RestController +@RequestMapping("/my-page") +@RequiredArgsConstructor +class MyPageController { + private final MyPageService myPageService; + + @GetMapping + public CustomResponse getMyPageInfo(Principal principal) { + MyPageDto myPageDto = myPageService.getMyPageInfo(principal.getName()); + return new DataResponse<>(myPageDto); + } + + @GetMapping("/update") + public CustomResponse getMyPageInfoToUpdate(Principal principal) { + MyPageUpdateDto myPageUpdateDto = myPageService.getMyPageInfoToUpdate(principal.getName()); + return new DataResponse<>(myPageUpdateDto); + } + + @PatchMapping("/update") + public CustomResponse update(Principal principal, @Valid @RequestBody MyPageUpdateDto myPageUpdateDto) { + myPageService.update(principal.getName(), myPageUpdateDto); + return new StatusResponse(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageDto.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageDto.java new file mode 100644 index 000000000..d0de0d0fa --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageDto.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.siteuser.dto; + +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.type.Role; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class MyPageDto { + private String nickname; + private String profileImageUrl; + private Role role; + private String birth; + private int likedPostCount; + private int likedMentorCount; + private int likedUniversityCount; + + public static MyPageDto fromEntity(SiteUser siteUser, int likedUniversityCount){ + return MyPageDto.builder() + .nickname(siteUser.getNickname()) + .profileImageUrl(siteUser.getProfileImageUrl()) + .role(siteUser.getRole()) + .birth(siteUser.getBirth()) + .likedUniversityCount(likedUniversityCount) + .likedMentorCount(0) // TODO: 멘토 기능 생기면 업데이트 필요 + .likedPostCount(0) // TODO: 커뮤니티 기능 생기면 업데이트 필요 + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateDto.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateDto.java new file mode 100644 index 000000000..e3dad0103 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateDto.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.siteuser.dto; + +import com.example.solidconnection.entity.SiteUser; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static com.example.solidconnection.constants.validMessage.NICKNAME_NOT_BLANK; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class MyPageUpdateDto { + @NotBlank(message = NICKNAME_NOT_BLANK) + private String nickname; + private String profileImageUrl; + + public static MyPageUpdateDto fromEntity(SiteUser siteUser){ + return MyPageUpdateDto.builder() + .nickname(siteUser.getNickname()) + .profileImageUrl(siteUser.getProfileImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java new file mode 100644 index 000000000..5eab45c53 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.siteuser.repository; + +import com.example.solidconnection.entity.LikedUniversity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface LikedUniversityRepository extends JpaRepository { + List findAllBySiteUser_Email(String email); + int countBySiteUser_Email(String email); +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java new file mode 100644 index 000000000..320999bfe --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -0,0 +1,64 @@ +package com.example.solidconnection.siteuser.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageDto; +import com.example.solidconnection.siteuser.dto.MyPageUpdateDto; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static com.example.solidconnection.constants.constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; + +@Service +@RequiredArgsConstructor +public class MyPageService { + private final SiteUserValidator siteUserValidator; + private final SiteUserRepository siteUserRepository; + private final LikedUniversityRepository likedUniversityRepository; + + public MyPageDto getMyPageInfo(String email) { + SiteUser siteUser = siteUserValidator.getValidatedUserByEmail(email); + int likedUniversityCount = likedUniversityRepository.countBySiteUser_Email(email); + return MyPageDto.fromEntity(siteUser, likedUniversityCount); + } + + public MyPageUpdateDto getMyPageInfoToUpdate(String email) { + SiteUser siteUser = siteUserValidator.getValidatedUserByEmail(email); + return MyPageUpdateDto.fromEntity(siteUser); + } + + @Transactional + public void update(String email, MyPageUpdateDto myPageUpdateDto) { + SiteUser siteUser = siteUserValidator.getValidatedUserByEmail(email); + validateNicknameDuplicated(myPageUpdateDto.getNickname()); + validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); + siteUser.setNickname(myPageUpdateDto.getNickname()); + siteUser.setProfileImageUrl(myPageUpdateDto.getProfileImageUrl()); + siteUser.setNicknameModifiedAt(LocalDateTime.now()); + } + + private void validateNicknameDuplicated(String nickname) { + if (siteUserRepository.existsByNickname(nickname)) { + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + + private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { + if (lastModifiedAt == null) { + return; + } + if (LocalDateTime.now().isBefore(lastModifiedAt.plusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES))) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + String formatLastModifiedAt = String.format("(마지막 수정 시간 : %s) ", formatter.format(lastModifiedAt)); + throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); + } + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java index 47143370a..2536bf5b0 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java @@ -13,7 +13,7 @@ public class SiteUserValidator { private final SiteUserRepository siteUserRepository; - public SiteUser validateExistByEmail(String email){ + public SiteUser getValidatedUserByEmail(String email){ return siteUserRepository.findByEmail(email) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); } From 91bc153ae0cc4ca1e270e764e8ed5bf66446261b Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 9 Feb 2024 22:25:19 +0900 Subject: [PATCH 029/158] =?UTF-8?q?feat:=20=EB=A7=9E=EC=B6=A4=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EB=8C=80=ED=95=99=EA=B5=90=20=EA=B8=B0=EB=8A=A5=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 --- .../auth/service/AuthService.java | 2 +- .../security/JwtAuthenticationFilter.java | 12 +++-- .../home/controller/HomeController.java | 31 +++++++++++ .../home/dto/PersonalHomeInfoDto.java | 14 +++++ .../home/dto/RecommendedUniversityDto.java | 23 ++++++++ .../InterestedCountyRepository.java | 3 +- .../InterestedRegionRepository.java | 3 +- .../example/solidconnection/s3/S3Service.java | 2 +- .../siteuser/service/SiteUserValidator.java | 2 +- .../repository/UniversityRepository.java | 9 ++++ .../university/service/UniversityService.java | 52 +++++++++++++++++++ 11 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/home/controller/HomeController.java create mode 100644 src/main/java/com/example/solidconnection/home/dto/PersonalHomeInfoDto.java create mode 100644 src/main/java/com/example/solidconnection/home/dto/RecommendedUniversityDto.java diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index b14e54f1e..048f7197c 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -80,7 +80,7 @@ public boolean signOut(String email){ } public boolean quit(String email){ - SiteUser siteUser = siteUserValidator.validateExistByEmail(email); + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); siteUser.setQuitedAt(LocalDate.now().plusDays(1)); return true; } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index d11731af3..75711a8c8 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -43,15 +43,17 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 - tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 - Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 - SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 - filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 + if (token != null) { // 토큰이 있어야 검증 - 토큰 유무에 대한 다른 처리를 컨트롤러에서 할 수 있음 + tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 + Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 + SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 + } } catch (AuthenticationException e) { jwtAuthenticationEntryPoint.commence(request, response, e); - } catch (CustomException e){ + } catch (CustomException e) { jwtAuthenticationEntryPoint.customCommence(request, response, e); } + filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 } private String resolveAccessTokenFromRequest(HttpServletRequest request) { diff --git a/src/main/java/com/example/solidconnection/home/controller/HomeController.java b/src/main/java/com/example/solidconnection/home/controller/HomeController.java new file mode 100644 index 000000000..93247fbcb --- /dev/null +++ b/src/main/java/com/example/solidconnection/home/controller/HomeController.java @@ -0,0 +1,31 @@ +package com.example.solidconnection.home.controller; + +import com.example.solidconnection.custom.response.CustomResponse; +import com.example.solidconnection.custom.response.DataResponse; +import com.example.solidconnection.home.dto.PersonalHomeInfoDto; +import com.example.solidconnection.university.service.UniversityService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +@RestController +@RequestMapping("/home") +@RequiredArgsConstructor +public class HomeController { + + private final UniversityService universityService; + + @GetMapping + public CustomResponse getHomeInfo(Principal principal) { + PersonalHomeInfoDto personalHomeInfoDto = new PersonalHomeInfoDto(); + if (principal == null) { + personalHomeInfoDto.setRecommendedUniversities(universityService.getGeneralRecommends()); + } else { + personalHomeInfoDto.setRecommendedUniversities(universityService.getPersonalRecommends(principal.getName())); + } + return new DataResponse<>(personalHomeInfoDto); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/home/dto/PersonalHomeInfoDto.java b/src/main/java/com/example/solidconnection/home/dto/PersonalHomeInfoDto.java new file mode 100644 index 000000000..56ed95fe2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/home/dto/PersonalHomeInfoDto.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.home.dto; + +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PersonalHomeInfoDto { + private List recommendedUniversities; +} diff --git a/src/main/java/com/example/solidconnection/home/dto/RecommendedUniversityDto.java b/src/main/java/com/example/solidconnection/home/dto/RecommendedUniversityDto.java new file mode 100644 index 000000000..ddd5f8b14 --- /dev/null +++ b/src/main/java/com/example/solidconnection/home/dto/RecommendedUniversityDto.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.home.dto; + +import com.example.solidconnection.entity.UniversityInfoForApply; +import lombok.*; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class RecommendedUniversityDto { + private long id; + private String koreanName; + private String backgroundImgUrl; + + public static RecommendedUniversityDto fromEntity(UniversityInfoForApply universityInfoForApply){ + return RecommendedUniversityDto.builder() + .id(universityInfoForApply.getId()) + .backgroundImgUrl(universityInfoForApply.getUniversity().getBackgroundImageUrl()) + .koreanName(universityInfoForApply.getUniversity().getKoreanName()) + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java b/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java index fff21cf38..1a5ba0f23 100644 --- a/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java @@ -1,6 +1,7 @@ package com.example.solidconnection.repositories; import com.example.solidconnection.entity.InterestedCountry; +import com.example.solidconnection.entity.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,5 +9,5 @@ @Repository public interface InterestedCountyRepository extends JpaRepository { - List findAllBySiteUser_Email(String email); + List findAllBySiteUser(SiteUser siteUser); } diff --git a/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java b/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java index 9b429b63c..ad3976b5d 100644 --- a/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java @@ -1,6 +1,7 @@ package com.example.solidconnection.repositories; import com.example.solidconnection.entity.InterestedRegion; +import com.example.solidconnection.entity.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,5 +9,5 @@ @Repository public interface InterestedRegionRepository extends JpaRepository { - List findAllBySiteUser_Email(String email); + List findAllBySiteUser(SiteUser siteUser); } diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 185400a3a..3e4fa1779 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -97,7 +97,7 @@ private void deleteFile(String fileName) { } private String getExProfileImageUrl(String email){ - SiteUser siteUser = siteUserValidator.validateExistByEmail(email); + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); String fileName = siteUser.getProfileImageUrl(); int domainStartIndex = fileName.indexOf(".com"); return fileName.substring(domainStartIndex + 5); diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java index 47143370a..a68a6ca7c 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java @@ -13,7 +13,7 @@ public class SiteUserValidator { private final SiteUserRepository siteUserRepository; - public SiteUser validateExistByEmail(String email){ + public SiteUser getValidatedSiteUserByEmail(String email){ return siteUserRepository.findByEmail(email) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); } diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java index 0ba600e41..77f0559e7 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java @@ -1,9 +1,18 @@ package com.example.solidconnection.university.repository; import com.example.solidconnection.entity.University; +import com.example.solidconnection.type.CountryCode; +import com.example.solidconnection.type.RegionCode; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface UniversityRepository extends JpaRepository { + + @Query("SELECT u FROM University u WHERE u.country.code IN :countries OR u.region.code IN :regions") + List findByCountryCodeInOrRegionCodeIn(@Param("countries") List countries, @Param("regions") List regions); } diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index 5598067d5..63a330b2c 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -1,8 +1,15 @@ package com.example.solidconnection.university.service; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.SiteUser; import com.example.solidconnection.entity.University; import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.home.dto.RecommendedUniversityDto; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.siteuser.service.SiteUserValidator; +import com.example.solidconnection.type.CountryCode; +import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.university.dto.LanguageRequirementDto; import com.example.solidconnection.university.dto.UniversityDetailDto; import com.example.solidconnection.university.repository.LanguageRequirementRepository; @@ -11,7 +18,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_NOT_FOUND; @@ -20,9 +29,52 @@ @RequiredArgsConstructor public class UniversityService { + private final static int RECOMMEND_NUM = 6; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final UniversityRepository universityRepository; private final LanguageRequirementRepository languageRequirementRepository; + private final SiteUserValidator siteUserValidator; + private final InterestedCountyRepository interestedCountyRepository; + private final InterestedRegionRepository interestedRegionRepository; + + public List getPersonalRecommends(String email){ + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + List interestedCountries = interestedCountyRepository.findAllBySiteUser(siteUser) + .stream().map(interestedCountry -> interestedCountry.getCountry().getCode()) + .toList(); + List interestedRegions = interestedRegionRepository.findAllBySiteUser(siteUser) + .stream().map(interestedRegion -> interestedRegion.getRegion().getCode()) + .toList(); + List recommendedUniversities = new java.util.ArrayList<>(universityRepository.findByCountryCodeInOrRegionCodeIn(interestedCountries, interestedRegions) + .stream().map(university -> { + return universityInfoForApplyRepository.findByUniversity(university).get(); + }) + .toList()); + + Collections.shuffle(recommendedUniversities); + List shuffledList = recommendedUniversities.subList(0, Math.min(RECOMMEND_NUM, recommendedUniversities.size())); + if(shuffledList.size() < 6){ + shuffledList.addAll(getRandomRecommendsExcept(shuffledList)); + } + + return shuffledList.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); + } + + public List getGeneralRecommends(){ + List list = new java.util.ArrayList<>(universityInfoForApplyRepository.findAll()); + Collections.shuffle(list); + List shuffledList = list.subList(0, RECOMMEND_NUM); + return shuffledList.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); + } + + private List getRandomRecommendsExcept(List alreadyPicked){ + List list = new java.util.ArrayList<>(universityInfoForApplyRepository.findAll()); + list.removeAll(alreadyPicked); + int sizeToPick = RECOMMEND_NUM - list.size(); + Collections.shuffle(list); + return list.subList(0, sizeToPick); + } public UniversityDetailDto getDetail(Long universityInfoForApplyId){ UniversityInfoForApply universityInfoForApply = getValidatedUniversityInfoForApply(universityInfoForApplyId); From 92707f6176d7ad4db121aef8ab33721baf57149b Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 9 Feb 2024 22:53:36 +0900 Subject: [PATCH 030/158] =?UTF-8?q?fix:=20conflict=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/siteuser/service/MyPageService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java index 320999bfe..80ccbcf09 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -25,19 +25,19 @@ public class MyPageService { private final LikedUniversityRepository likedUniversityRepository; public MyPageDto getMyPageInfo(String email) { - SiteUser siteUser = siteUserValidator.getValidatedUserByEmail(email); + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); int likedUniversityCount = likedUniversityRepository.countBySiteUser_Email(email); return MyPageDto.fromEntity(siteUser, likedUniversityCount); } public MyPageUpdateDto getMyPageInfoToUpdate(String email) { - SiteUser siteUser = siteUserValidator.getValidatedUserByEmail(email); + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); return MyPageUpdateDto.fromEntity(siteUser); } @Transactional public void update(String email, MyPageUpdateDto myPageUpdateDto) { - SiteUser siteUser = siteUserValidator.getValidatedUserByEmail(email); + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); validateNicknameDuplicated(myPageUpdateDto.getNickname()); validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); siteUser.setNickname(myPageUpdateDto.getNickname()); From 49a5e6a239f7d2746fe0a423d83212eec39db291 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 10 Feb 2024 00:44:18 +0900 Subject: [PATCH 031/158] =?UTF-8?q?refactor:=20=EC=A7=80=EC=A0=95=20?= =?UTF-8?q?=EB=8C=80=ED=95=99=EA=B5=90=20=EC=B6=94=EC=B2=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/SecurityConfiguration.java | 2 +- .../solidconnection/constants/constants.java | 11 ++++- .../custom/exception/ErrorCode.java | 4 +- .../UniversityInfoForApplyRepository.java | 1 + .../university/service/UniversityService.java | 28 ++++++------- .../GeneralRecommendUniversities.java | 42 +++++++++++++++++++ 6 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/university/singleton/GeneralRecommendUniversities.java diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 47c5a2569..eead6bc7d 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -50,7 +50,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/", "/index.html", "/favicon.ico", "/img/profile/pre", "/auth/kakao", "/auth/sign-up", - "/university/detail/**", "/university/search/**" + "/university/detail/**", "/university/search/**", "/home" ) .permitAll() .anyRequest().authenticated()) diff --git a/src/main/java/com/example/solidconnection/constants/constants.java b/src/main/java/com/example/solidconnection/constants/constants.java index 035d26821..20f493f95 100644 --- a/src/main/java/com/example/solidconnection/constants/constants.java +++ b/src/main/java/com/example/solidconnection/constants/constants.java @@ -1,5 +1,14 @@ package com.example.solidconnection.constants; public class constants { - public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 7; + public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; + public final static int RECOMMEND_UNIVERSITY_NUM = 6; + + // 기본 추천 대학 - 국문명 + public final static String RECOMMEND_UNIVERSITY_1 = "네바다주립대학 라스베이거스(B형)"; + public final static String RECOMMEND_UNIVERSITY_2 = "바덴뷔르템베르크 산학협력대학"; + public final static String RECOMMEND_UNIVERSITY_3 = "릴 가톨릭 대학"; + public final static String RECOMMEND_UNIVERSITY_4 = "그라츠공과대학"; + public final static String RECOMMEND_UNIVERSITY_5 = "RMIT멜버른공과대학(A형)"; + public final static String RECOMMEND_UNIVERSITY_6 = "오스트라바 대학"; } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index a35ce1019..9e986f929 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -4,11 +4,13 @@ import lombok.Getter; import org.springframework.http.HttpStatus; +import static com.example.solidconnection.constants.constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; + @Getter @AllArgsConstructor public enum ErrorCode { INVALID_INPUT(HttpStatus.BAD_REQUEST.value(), "값을 입력할 수 없습니다."), - CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 7일이 지나지 않았습니다."), + CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 "+MIN_DAYS_BETWEEN_NICKNAME_CHANGES+"일이 지나지 않았습니다."), UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교 지원 정보입니다."), UNIVERSITY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교입니다."), REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST.value(), "리다이렉트 uri가 잘못되었습니다."), diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index 8fd0b7800..f4ed9d886 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -10,4 +10,5 @@ @Repository public interface UniversityInfoForApplyRepository extends JpaRepository { Optional findByUniversity(University university); + Optional findByUniversity_KoreanName(String koreanName); } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index 63a330b2c..d029a24d7 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -15,6 +15,7 @@ import com.example.solidconnection.university.repository.LanguageRequirementRepository; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.UniversityRepository; +import com.example.solidconnection.university.singleton.GeneralRecommendUniversities; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -22,6 +23,7 @@ import java.util.List; import java.util.stream.Collectors; +import static com.example.solidconnection.constants.constants.RECOMMEND_UNIVERSITY_NUM; import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_NOT_FOUND; @@ -29,14 +31,13 @@ @RequiredArgsConstructor public class UniversityService { - private final static int RECOMMEND_NUM = 6; - private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final UniversityRepository universityRepository; private final LanguageRequirementRepository languageRequirementRepository; private final SiteUserValidator siteUserValidator; private final InterestedCountyRepository interestedCountyRepository; private final InterestedRegionRepository interestedRegionRepository; + private final GeneralRecommendUniversities generalRecommendUniversities; public List getPersonalRecommends(String email){ SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); @@ -53,27 +54,26 @@ public List getPersonalRecommends(String email){ .toList()); Collections.shuffle(recommendedUniversities); - List shuffledList = recommendedUniversities.subList(0, Math.min(RECOMMEND_NUM, recommendedUniversities.size())); + List shuffledList = recommendedUniversities.subList(0, Math.min(RECOMMEND_UNIVERSITY_NUM, recommendedUniversities.size())); if(shuffledList.size() < 6){ - shuffledList.addAll(getRandomRecommendsExcept(shuffledList)); + shuffledList.addAll(getGeneralRecommendsExcept(shuffledList)); } return shuffledList.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); } public List getGeneralRecommends(){ - List list = new java.util.ArrayList<>(universityInfoForApplyRepository.findAll()); - Collections.shuffle(list); - List shuffledList = list.subList(0, RECOMMEND_NUM); - return shuffledList.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); + List generalRecommend = new java.util.ArrayList<>(generalRecommendUniversities.getRecommendedUniversities()); + Collections.shuffle(generalRecommend); + return generalRecommend.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); } - private List getRandomRecommendsExcept(List alreadyPicked){ - List list = new java.util.ArrayList<>(universityInfoForApplyRepository.findAll()); - list.removeAll(alreadyPicked); - int sizeToPick = RECOMMEND_NUM - list.size(); - Collections.shuffle(list); - return list.subList(0, sizeToPick); + private List getGeneralRecommendsExcept(List alreadyPicked){ + List generalRecommend = new java.util.ArrayList<>(generalRecommendUniversities.getRecommendedUniversities()); + generalRecommend.removeAll(alreadyPicked); + int sizeToPick = RECOMMEND_UNIVERSITY_NUM - alreadyPicked.size(); + Collections.shuffle(generalRecommend); + return generalRecommend.subList(0, sizeToPick); } public UniversityDetailDto getDetail(Long universityInfoForApplyId){ diff --git a/src/main/java/com/example/solidconnection/university/singleton/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/singleton/GeneralRecommendUniversities.java new file mode 100644 index 000000000..dea3575e0 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/singleton/GeneralRecommendUniversities.java @@ -0,0 +1,42 @@ +package com.example.solidconnection.university.singleton; + +import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +import static com.example.solidconnection.constants.constants.*; + +@Component +@RequiredArgsConstructor +public class GeneralRecommendUniversities { + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private List recommendedUniversities; + + @PostConstruct + public void init() { + recommendedUniversities = new ArrayList<>(); + + UniversityInfoForApply univ1 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_1).get(); + UniversityInfoForApply univ2 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_2).get(); + UniversityInfoForApply univ3 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_3).get(); + UniversityInfoForApply univ4 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_4).get(); + UniversityInfoForApply univ5 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_5).get(); + UniversityInfoForApply univ6 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_6).get(); + + recommendedUniversities.add(univ1); + recommendedUniversities.add(univ2); + recommendedUniversities.add(univ3); + recommendedUniversities.add(univ4); + recommendedUniversities.add(univ5); + recommendedUniversities.add(univ6); + } + + public List getRecommendedUniversities() { + return recommendedUniversities; + } +} From 9779d4f8e7655186314a068ee6df616837b55051 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 13 Feb 2024 18:30:26 +0900 Subject: [PATCH 032/158] =?UTF-8?q?feat:=20=EC=97=91=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=ED=96=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 7 +++++++ .../auth/dto/ReissueResponseDto.java | 12 ++++++++++++ .../auth/service/AuthService.java | 16 ++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/auth/dto/ReissueResponseDto.java diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index c352e7b82..f1b10b2fa 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -1,5 +1,6 @@ package com.example.solidconnection.auth.controller; +import com.example.solidconnection.auth.dto.ReissueResponseDto; import com.example.solidconnection.auth.dto.SignUpRequestDto; import com.example.solidconnection.auth.dto.SignUpResponseDto; import com.example.solidconnection.auth.dto.kakao.KakaoCodeDto; @@ -45,4 +46,10 @@ public CustomResponse quit(Principal principal) { boolean status = authService.quit(principal.getName()); return new StatusResponse(status); } + + @PostMapping("/reissue") + public CustomResponse reissue(Principal principal) { + ReissueResponseDto reissueResponseDto = authService.reissue(principal.getName()); + return new DataResponse<>(reissueResponseDto); + } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponseDto.java new file mode 100644 index 000000000..e9192a04b --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponseDto.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.auth.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ReissueResponseDto { + private String accessToken; +} diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 048f7197c..f641adbf0 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -1,6 +1,7 @@ package com.example.solidconnection.auth.service; +import com.example.solidconnection.auth.dto.ReissueResponseDto; import com.example.solidconnection.auth.dto.SignUpRequestDto; import com.example.solidconnection.auth.dto.SignUpResponseDto; import com.example.solidconnection.config.token.TokenService; @@ -21,6 +22,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -85,6 +87,20 @@ public boolean quit(String email){ return true; } + public ReissueResponseDto reissue(String email) { + // 리프레시 토큰 만료 확인 + String refreshTokenKey= TokenType.REFRESH.getPrefix() + email; + String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); + if (ObjectUtils.isEmpty(refreshToken)) { + throw new CustomException(REFRESH_TOKEN_EXPIRED); + } + // 엑세스 토큰 재발급 + String newAccessToken = tokenService.generateToken(email, TokenType.ACCESS); + return ReissueResponseDto.builder() + .accessToken(newAccessToken) + .build(); + } + private void validateUserNotDuplicated(SignUpRequestDto signUpRequestDto){ String email = tokenService.getEmail(signUpRequestDto.getKakaoOauthToken()); if(siteUserRepository.existsByEmail(email)){ From 7e361171de0dfd307793a048ff04d4c44bc36498 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 14 Feb 2024 01:55:50 +0900 Subject: [PATCH 033/158] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=20=EC=84=B1?= =?UTF-8?q?=EC=A0=81,=20=ED=95=99=EA=B5=90=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApplicationController.java | 34 ++++++ .../application/dto/ScoreRequestDto.java | 31 ++++++ .../application/dto/UniversityRequestDto.java | 15 +++ .../repository/ApplicationRepository.java | 14 +++ .../service/ApplicationService.java | 100 ++++++++++++++++++ .../service/ApplicationValidator.java | 19 ++++ .../security/JwtAuthenticationEntryPoint.java | 12 +++ .../security/JwtAuthenticationFilter.java | 15 ++- .../solidconnection/constants/Constants.java | 7 ++ .../GeneralRecommendUniversities.java | 12 ++- .../constants/NicknameForApplyWords.java | 16 +++ .../solidconnection/constants/constants.java | 14 --- .../constants/validMessage.java | 7 ++ .../custom/exception/ErrorCode.java | 5 +- .../exception/JwtExpiredTokenException.java | 9 ++ .../solidconnection/entity/Application.java | 39 ++++++- .../entity/GpaRequirement.java | 23 ---- .../siteuser/service/MyPageService.java | 2 +- .../solidconnection/type/VerifyStatus.java | 5 + .../university/service/UniversityService.java | 20 +--- .../service/UniversityValidator.java | 29 +++++ 21 files changed, 366 insertions(+), 62 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/application/controller/ApplicationController.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/ScoreRequestDto.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java create mode 100644 src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java create mode 100644 src/main/java/com/example/solidconnection/application/service/ApplicationService.java create mode 100644 src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java create mode 100644 src/main/java/com/example/solidconnection/constants/Constants.java rename src/main/java/com/example/solidconnection/{university/singleton => constants}/GeneralRecommendUniversities.java (74%) create mode 100644 src/main/java/com/example/solidconnection/constants/NicknameForApplyWords.java delete mode 100644 src/main/java/com/example/solidconnection/constants/constants.java create mode 100644 src/main/java/com/example/solidconnection/custom/exception/JwtExpiredTokenException.java delete mode 100644 src/main/java/com/example/solidconnection/entity/GpaRequirement.java create mode 100644 src/main/java/com/example/solidconnection/type/VerifyStatus.java create mode 100644 src/main/java/com/example/solidconnection/university/service/UniversityValidator.java diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java new file mode 100644 index 000000000..944bff18b --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.application.controller; + +import com.example.solidconnection.application.dto.ScoreRequestDto; +import com.example.solidconnection.application.dto.UniversityRequestDto; +import com.example.solidconnection.application.service.ApplicationService; +import com.example.solidconnection.custom.response.CustomResponse; +import com.example.solidconnection.custom.response.StatusResponse; +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; + +import java.security.Principal; + +@RestController +@RequestMapping("/application") +@RequiredArgsConstructor +public class ApplicationController { + private final ApplicationService applicationService; + + @PostMapping("/score") + public CustomResponse registerScore(Principal principal, @Valid @RequestBody ScoreRequestDto scoreRequestDto) { + boolean result = applicationService.saveScore(principal.getName(), scoreRequestDto); + return new StatusResponse(result); + } + + @PostMapping("/university") + public CustomResponse registerUniversity(Principal principal, @Valid @RequestBody UniversityRequestDto universityRequestDto) { + boolean result = applicationService.saveUniversity(principal.getName(), universityRequestDto); + return new StatusResponse(result); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/application/dto/ScoreRequestDto.java b/src/main/java/com/example/solidconnection/application/dto/ScoreRequestDto.java new file mode 100644 index 000000000..711e33c6b --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/ScoreRequestDto.java @@ -0,0 +1,31 @@ +package com.example.solidconnection.application.dto; + + +import com.example.solidconnection.type.LanguageTestType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +import static com.example.solidconnection.constants.validMessage.*; + +@Getter +@Setter +public class ScoreRequestDto { + @NotNull(message = LANGUAGE_TEST_TYPE_NOT_BLANK) + private LanguageTestType languageTestType; + + @NotBlank(message = LANGUAGE_TEST_SCORE_NOT_BLANK) + private String languageTestScore; + + @NotBlank(message = LANGUAGE_TEST_REPORT_URL_NOT_BLANK) + private String languageTestReportUrl; + + @NotNull(message = GPA_NOT_BLANK) + private Float gpa; + + @NotNull(message = GPA_CRITERIA_NOT_BLANK) + private Float gpaCriteria; + + @NotBlank(message = GPA_REPORT_URL_NOT_BLANK) + private String gpaReportUrl; +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java b/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java new file mode 100644 index 000000000..1ba8c1485 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.application.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import static com.example.solidconnection.constants.validMessage.FIRST_CHOICE_UNIVERSITY_ID_NOT_BLANK; + +@Getter +@Setter +public class UniversityRequestDto { + @NotNull(message = FIRST_CHOICE_UNIVERSITY_ID_NOT_BLANK) + private Long firstChoiceUniversityId; + private Long secondChoiceUniversity; +} diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java new file mode 100644 index 000000000..9eab723fb --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.application.repository; + +import com.example.solidconnection.entity.Application; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ApplicationRepository extends JpaRepository { + boolean existsBySiteUser_Email(String email); + boolean existsByNicknameForApply(String nicknameForApply); + Optional findBySiteUser_Email(String email); +} diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java new file mode 100644 index 000000000..cbe52941f --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -0,0 +1,100 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.dto.ScoreRequestDto; +import com.example.solidconnection.application.dto.UniversityRequestDto; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.constants.NicknameForApplyWords; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.Application; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.siteuser.service.SiteUserValidator; +import com.example.solidconnection.university.service.UniversityValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; +import java.util.Random; + +import static com.example.solidconnection.constants.Constants.APPLICATION_UPDATE_COUNT_LIMIT; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; +import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; + +@Service +@RequiredArgsConstructor +@Transactional +public class ApplicationService { + private final ApplicationRepository applicationRepository; + private final UniversityValidator universityValidator; + private final SiteUserValidator siteUserValidator; + private final ApplicationValidator applicationValidator; + + public boolean saveScore(String email, ScoreRequestDto scoreRequestDto) { + // 한번 등록 후 수정 + if (applicationRepository.existsBySiteUser_Email(email)) { + Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); + // 수정 횟수 초과 에러 처리 + if (application.getUpdateCount() > APPLICATION_UPDATE_COUNT_LIMIT) { + throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); + } + application.setGpa(scoreRequestDto.getGpa()); + application.setGpaCriteria(scoreRequestDto.getGpaCriteria()); + application.setGpaReportUrl(scoreRequestDto.getGpaReportUrl()); + application.setLanguageTestScore(scoreRequestDto.getLanguageTestScore()); + application.setLanguageTestType(scoreRequestDto.getLanguageTestType()); + application.setLanguageTestReportUrl(scoreRequestDto.getLanguageTestReportUrl()); + application.setUpdateCount(application.getUpdateCount() + 1); + return true; + } + + // 최초 증록 + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + Application application = Application.saveScore(siteUser, scoreRequestDto); + applicationRepository.save(application); + return true; + } + + public boolean saveUniversity(String email, UniversityRequestDto universityRequestDto) { + // 수정 횟수 초과 에러 처리 + Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); + if (application.getUpdateCount() > APPLICATION_UPDATE_COUNT_LIMIT) { + throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); + } + + // 저장에 필요한 엔티티 불러오기 or 생성 + UniversityInfoForApply firstChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyById(universityRequestDto.getFirstChoiceUniversityId()); + UniversityInfoForApply secondChoiceUniversity; + try { + secondChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyById(universityRequestDto.getSecondChoiceUniversity()); + } catch (Exception e) { + secondChoiceUniversity = null; + } + + // 1,2 동일한 대학교 지망 에러 처리 + if (secondChoiceUniversity != null && Objects.equals(secondChoiceUniversity.getId(), firstChoiceUniversity.getId())) { + throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); + } + + // 수정 + application.setFirstChoiceUniversity(firstChoiceUniversity); + application.setFirstChoiceUniversity(secondChoiceUniversity); + + // 새로운 닉네임 부여 + String randomNickname = makeRandomNickname(); + while (applicationRepository.existsByNicknameForApply(randomNickname)) { + randomNickname = makeRandomNickname(); + } + application.setNicknameForApply(randomNickname); + return true; + } + + private String makeRandomNickname() { + Random random = new Random(); + int randomIndex1 = random.nextInt(NicknameForApplyWords.adjectives.size()); + String randomAdjective = NicknameForApplyWords.adjectives.get(randomIndex1); + int randomIndex2 = random.nextInt(NicknameForApplyWords.nouns.size()); + String randomNoun = NicknameForApplyWords.nouns.get(randomIndex2); + return randomAdjective + " " + randomNoun; + } +} diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java b/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java new file mode 100644 index 000000000..58820c6aa --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.Application; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.SCORE_SUBMIT_FIRST; + +@Service +@RequiredArgsConstructor +public class ApplicationValidator { + private final ApplicationRepository applicationRepository; + + public Application getValidatedApplicationBySiteUser_Email(String email){ + return applicationRepository.findBySiteUser_Email(email).orElseThrow(() -> new CustomException(SCORE_SUBMIT_FIRST)); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 7ca357158..7fc030ec3 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -12,6 +12,7 @@ import java.io.IOException; +import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; @Component @@ -23,6 +24,7 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); @@ -30,6 +32,16 @@ public void commence(HttpServletRequest request, HttpServletResponse response, response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } + public void expiredCommence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + + ErrorResponse errorResponse = new ErrorResponse(new CustomException(ACCESS_TOKEN_EXPIRED)); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } + public void customCommence(HttpServletRequest request, HttpServletResponse response, CustomException customException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(customException); diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 75711a8c8..99f4f7de8 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -3,6 +3,8 @@ import com.example.solidconnection.config.token.TokenService; import com.example.solidconnection.config.token.TokenValidator; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.JwtExpiredTokenException; +import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -19,6 +21,8 @@ import java.io.IOException; import java.util.HashSet; +import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; + @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -44,11 +48,20 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 if (token != null) { // 토큰이 있어야 검증 - 토큰 유무에 대한 다른 처리를 컨트롤러에서 할 수 있음 - tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 + try { + tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 + } catch (ExpiredJwtException e) { + throw new JwtExpiredTokenException(ACCESS_TOKEN_EXPIRED.getMessage()); + } Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 } + } catch (JwtExpiredTokenException e) { + SecurityContextHolder.clearContext(); + jwtAuthenticationEntryPoint.expiredCommence(request, response, e); + return; } catch (AuthenticationException e) { + SecurityContextHolder.clearContext(); jwtAuthenticationEntryPoint.commence(request, response, e); } catch (CustomException e) { jwtAuthenticationEntryPoint.customCommence(request, response, e); diff --git a/src/main/java/com/example/solidconnection/constants/Constants.java b/src/main/java/com/example/solidconnection/constants/Constants.java new file mode 100644 index 000000000..1ce5c7205 --- /dev/null +++ b/src/main/java/com/example/solidconnection/constants/Constants.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.constants; + +public class Constants { + public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; + public static final int APPLICATION_UPDATE_COUNT_LIMIT = 3; + public final static int RECOMMEND_UNIVERSITY_NUM = 6; +} diff --git a/src/main/java/com/example/solidconnection/university/singleton/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java similarity index 74% rename from src/main/java/com/example/solidconnection/university/singleton/GeneralRecommendUniversities.java rename to src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java index dea3575e0..a470184ba 100644 --- a/src/main/java/com/example/solidconnection/university/singleton/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.university.singleton; +package com.example.solidconnection.constants; import com.example.solidconnection.entity.UniversityInfoForApply; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; @@ -9,11 +9,17 @@ import java.util.ArrayList; import java.util.List; -import static com.example.solidconnection.constants.constants.*; - @Component @RequiredArgsConstructor public class GeneralRecommendUniversities { + // 기본 추천 대학 - 국문명 + public final static String RECOMMEND_UNIVERSITY_1 = "네바다주립대학 라스베이거스(B형)"; + public final static String RECOMMEND_UNIVERSITY_2 = "바덴뷔르템베르크 산학협력대학"; + public final static String RECOMMEND_UNIVERSITY_3 = "릴 가톨릭 대학"; + public final static String RECOMMEND_UNIVERSITY_4 = "그라츠공과대학"; + public final static String RECOMMEND_UNIVERSITY_5 = "RMIT멜버른공과대학(A형)"; + public final static String RECOMMEND_UNIVERSITY_6 = "오스트라바 대학"; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private List recommendedUniversities; diff --git a/src/main/java/com/example/solidconnection/constants/NicknameForApplyWords.java b/src/main/java/com/example/solidconnection/constants/NicknameForApplyWords.java new file mode 100644 index 000000000..3a7ae39f9 --- /dev/null +++ b/src/main/java/com/example/solidconnection/constants/NicknameForApplyWords.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.constants; + +import java.util.List; + +public class NicknameForApplyWords { + public static final List adjectives = List.of( + "기쁜", "행복한", "즐거운", "밝은", "따뜻한", "시원한", "고고한", "예쁜", "신선한", "풍부한", "깨끗한", + "귀한", "눈부신", "멋진", "고귀한", "화려한", "상큼한", "활기찬", "유쾌한", "똘똘한", "친절한", "눈부신", "좋은", + "영리한", "용감한", "정직한", "성실한", "강인한", "귀여운", "순수한", "희망찬", "발랄한", "나른한", "후한", "빛나는", + "따스한", "안락한", "편안한", "성공한"); + + public static final List nouns = List.of( + "청춘", "토끼", "기사", "곰", "사슴", "여우", "팬더", "이슬", "새싹", "햇빛", "나비", "별", "달", "구름", + "사탕", "젤리", "마법", "풍선", "캔디", "초코", "인형", "쿠키", "요정", "장미", "마녀", "보물", "꽃", "보석", + "달빛", "오리", "날개", "여행", "편지", "불꽃"); +} diff --git a/src/main/java/com/example/solidconnection/constants/constants.java b/src/main/java/com/example/solidconnection/constants/constants.java deleted file mode 100644 index 20f493f95..000000000 --- a/src/main/java/com/example/solidconnection/constants/constants.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.solidconnection.constants; - -public class constants { - public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; - public final static int RECOMMEND_UNIVERSITY_NUM = 6; - - // 기본 추천 대학 - 국문명 - public final static String RECOMMEND_UNIVERSITY_1 = "네바다주립대학 라스베이거스(B형)"; - public final static String RECOMMEND_UNIVERSITY_2 = "바덴뷔르템베르크 산학협력대학"; - public final static String RECOMMEND_UNIVERSITY_3 = "릴 가톨릭 대학"; - public final static String RECOMMEND_UNIVERSITY_4 = "그라츠공과대학"; - public final static String RECOMMEND_UNIVERSITY_5 = "RMIT멜버른공과대학(A형)"; - public final static String RECOMMEND_UNIVERSITY_6 = "오스트라바 대학"; -} diff --git a/src/main/java/com/example/solidconnection/constants/validMessage.java b/src/main/java/com/example/solidconnection/constants/validMessage.java index fe6ef01b5..ae7d87eec 100644 --- a/src/main/java/com/example/solidconnection/constants/validMessage.java +++ b/src/main/java/com/example/solidconnection/constants/validMessage.java @@ -2,4 +2,11 @@ public class validMessage { public final static String NICKNAME_NOT_BLANK = "닉네임을 입력해주세요."; + public final static String LANGUAGE_TEST_TYPE_NOT_BLANK = "어학 종류를 입력해주세요."; + public final static String LANGUAGE_TEST_SCORE_NOT_BLANK = "어학 점수를 입력해주세요."; + public final static String LANGUAGE_TEST_REPORT_URL_NOT_BLANK = "어학 증명서를 첨부해주세요."; + public final static String GPA_NOT_BLANK = "학점을 입력해주세요."; + public final static String GPA_CRITERIA_NOT_BLANK = "학점 기준을 입력해주세요."; + public final static String GPA_REPORT_URL_NOT_BLANK = "대학 성적 증명서를 첨부해주세요."; + public final static String FIRST_CHOICE_UNIVERSITY_ID_NOT_BLANK = "1지망 대학교를 입력해주세요."; } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 9e986f929..50e6c4012 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -4,11 +4,14 @@ import lombok.Getter; import org.springframework.http.HttpStatus; -import static com.example.solidconnection.constants.constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; +import static com.example.solidconnection.constants.Constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; @Getter @AllArgsConstructor public enum ErrorCode { + APPLY_UPDATE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST.value(), "지원 정보 수정은 3회까지만 가능합니다."), + CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2지망에 동일한 대학교를 입력할 수 없습니다."), + SCORE_SUBMIT_FIRST(HttpStatus.BAD_REQUEST.value(), "성적 입력 후 지망 대학을 입력할 수 있습니다."), INVALID_INPUT(HttpStatus.BAD_REQUEST.value(), "값을 입력할 수 없습니다."), CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 "+MIN_DAYS_BETWEEN_NICKNAME_CHANGES+"일이 지나지 않았습니다."), UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교 지원 정보입니다."), diff --git a/src/main/java/com/example/solidconnection/custom/exception/JwtExpiredTokenException.java b/src/main/java/com/example/solidconnection/custom/exception/JwtExpiredTokenException.java new file mode 100644 index 000000000..6fe911b8a --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/exception/JwtExpiredTokenException.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.custom.exception; + +import org.springframework.security.core.AuthenticationException; + +public class JwtExpiredTokenException extends AuthenticationException { + public JwtExpiredTokenException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/entity/Application.java b/src/main/java/com/example/solidconnection/entity/Application.java index dbb8abcdb..131b99e78 100644 --- a/src/main/java/com/example/solidconnection/entity/Application.java +++ b/src/main/java/com/example/solidconnection/entity/Application.java @@ -1,11 +1,19 @@ package com.example.solidconnection.entity; +import com.example.solidconnection.application.dto.ScoreRequestDto; import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.VerifyStatus; import jakarta.persistence.*; -import lombok.Getter; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; @Entity @Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@DynamicInsert public class Application { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -24,22 +32,45 @@ public class Application { @Column(nullable = false) private Float gpa; + @Column(nullable = false) + private Float gpaCriteria; + @Column(nullable = false, length = 500) private String gpaReportUrl; @Column(nullable = false, length = 50) - private String verifyStatus; + @Enumerated(EnumType.STRING) + private VerifyStatus verifyStatus; + + @Column(length = 100) + private String nicknameForApply; + + @Column(nullable = false) + private Integer updateCount; // 연관 관계 @ManyToOne @JoinColumn(name = "first_choice_univ_id") - private University firstChoiceUniversity; + private UniversityInfoForApply firstChoiceUniversity; @ManyToOne @JoinColumn(name = "second_choice_univ_id") - private University secondChoiceUniversity; + private UniversityInfoForApply secondChoiceUniversity; @ManyToOne @JoinColumn(name = "site_user_id") private SiteUser siteUser; + + public static Application saveScore(SiteUser siteUser, ScoreRequestDto scoreRequestDto){ + return Application.builder() + .siteUser(siteUser) + .languageTestType(scoreRequestDto.getLanguageTestType()) + .languageTestScore(scoreRequestDto.getLanguageTestScore()) + .languageTestReportUrl(scoreRequestDto.getLanguageTestReportUrl()) + .gpa(scoreRequestDto.getGpa()) + .gpaCriteria(scoreRequestDto.getGpaCriteria()) + .gpaReportUrl(scoreRequestDto.getGpaReportUrl()) + .verifyStatus(VerifyStatus.PENDING) + .build(); + } } diff --git a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java b/src/main/java/com/example/solidconnection/entity/GpaRequirement.java deleted file mode 100644 index 6c72ae054..000000000 --- a/src/main/java/com/example/solidconnection/entity/GpaRequirement.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.solidconnection.entity; - -import jakarta.persistence.*; -import lombok.Getter; - -@Entity -@Getter -public class GpaRequirement { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, length = 5) - private String scale; - - @Column(nullable = false) - private String minGpa; - - // 연관 관계 - @ManyToOne - @JoinColumn(name = "university_info_for_apply_id") - private UniversityInfoForApply universityInfoForApply; -} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java index 80ccbcf09..81c2be3fc 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -13,7 +13,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import static com.example.solidconnection.constants.constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; +import static com.example.solidconnection.constants.Constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; diff --git a/src/main/java/com/example/solidconnection/type/VerifyStatus.java b/src/main/java/com/example/solidconnection/type/VerifyStatus.java new file mode 100644 index 000000000..95f122715 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/VerifyStatus.java @@ -0,0 +1,5 @@ +package com.example.solidconnection.type; + +public enum VerifyStatus { + PENDING, REJECTED, APPROVED +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index d029a24d7..e8ee6fbc4 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -1,6 +1,5 @@ package com.example.solidconnection.university.service; -import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.SiteUser; import com.example.solidconnection.entity.University; import com.example.solidconnection.entity.UniversityInfoForApply; @@ -15,7 +14,7 @@ import com.example.solidconnection.university.repository.LanguageRequirementRepository; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.UniversityRepository; -import com.example.solidconnection.university.singleton.GeneralRecommendUniversities; +import com.example.solidconnection.constants.GeneralRecommendUniversities; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,9 +22,7 @@ import java.util.List; import java.util.stream.Collectors; -import static com.example.solidconnection.constants.constants.RECOMMEND_UNIVERSITY_NUM; -import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; -import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_NOT_FOUND; +import static com.example.solidconnection.constants.Constants.RECOMMEND_UNIVERSITY_NUM; @Service @RequiredArgsConstructor @@ -38,6 +35,7 @@ public class UniversityService { private final InterestedCountyRepository interestedCountyRepository; private final InterestedRegionRepository interestedRegionRepository; private final GeneralRecommendUniversities generalRecommendUniversities; + private final UniversityValidator universityValidator; public List getPersonalRecommends(String email){ SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); @@ -77,8 +75,8 @@ private List getGeneralRecommendsExcept(List languageRequirements = languageRequirementRepository .findAllByUniversityInfoForApply_Id(universityInfoForApplyId) @@ -125,13 +123,5 @@ public UniversityDetailDto getDetail(Long universityInfoForApplyId){ .build(); } - private UniversityInfoForApply getValidatedUniversityInfoForApply(Long id){ - return universityInfoForApplyRepository.findById(id) - .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); - } - private University getValidatedUniversity(Long id){ - return universityRepository.findById(id) - .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); - } } diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java new file mode 100644 index 000000000..2f3e80361 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.University; +import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.UniversityRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class UniversityValidator { + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final UniversityRepository universityRepository; + + public UniversityInfoForApply getValidatedUniversityInfoForApplyById(Long id){ + return universityInfoForApplyRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); + } + + public University getValidatedUniversityById(Long id){ + return universityRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + } +} From b3bdd3f58be9c9454b7511c7e9cb14dc2e5af9a9 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 14 Feb 2024 02:07:08 +0900 Subject: [PATCH 034/158] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=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 --- .../solidconnection/application/dto/UniversityRequestDto.java | 2 +- .../application/service/ApplicationService.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java b/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java index 1ba8c1485..3399b4cf4 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java @@ -11,5 +11,5 @@ public class UniversityRequestDto { @NotNull(message = FIRST_CHOICE_UNIVERSITY_ID_NOT_BLANK) private Long firstChoiceUniversityId; - private Long secondChoiceUniversity; + private Long secondChoiceUniversityId; } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index cbe52941f..ab7c5cb5f 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -66,7 +66,7 @@ public boolean saveUniversity(String email, UniversityRequestDto universityReque UniversityInfoForApply firstChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyById(universityRequestDto.getFirstChoiceUniversityId()); UniversityInfoForApply secondChoiceUniversity; try { - secondChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyById(universityRequestDto.getSecondChoiceUniversity()); + secondChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyById(universityRequestDto.getSecondChoiceUniversityId()); } catch (Exception e) { secondChoiceUniversity = null; } @@ -78,7 +78,7 @@ public boolean saveUniversity(String email, UniversityRequestDto universityReque // 수정 application.setFirstChoiceUniversity(firstChoiceUniversity); - application.setFirstChoiceUniversity(secondChoiceUniversity); + application.setSecondChoiceUniversity(secondChoiceUniversity); // 새로운 닉네임 부여 String randomNickname = makeRandomNickname(); From 89c8773286b688602da3143dd7076356a8793070 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 15 Feb 2024 02:42:29 +0900 Subject: [PATCH 035/158] =?UTF-8?q?feat:=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 16 +++++ .../solidconnection/constants/Constants.java | 1 + .../GeneralRecommendUniversities.java | 14 +++-- .../solidconnection/type/CountryCode.java | 7 +++ .../controller/UniversityController.java | 12 +++- .../university/dto/UniversityPreviewDto.java | 22 +++++++ .../UniversityInfoForApplyRepository.java | 5 +- .../repository/UniversityRepository.java | 3 +- .../custom/UniversityRepositoryForFilter.java | 11 ++++ .../UniversityRepositoryForFilterImpl.java | 63 +++++++++++++++++++ .../university/service/UniversityService.java | 57 +++++++++++++---- .../service/UniversityValidator.java | 8 ++- 12 files changed, 197 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java diff --git a/build.gradle b/build.gradle index 09e562894..ea69c73e1 100644 --- a/build.gradle +++ b/build.gradle @@ -35,11 +35,27 @@ dependencies { implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' implementation 'com.amazonaws:aws-java-sdk-s3:1.12.470' implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' + implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor( + 'com.querydsl:querydsl-apt:5.0.0:jakarta', + 'jakarta.persistence:jakarta.persistence-api:3.1.0', + 'jakarta.annotation:jakarta.annotation-api:2.1.1' + ) } tasks.named('test') { useJUnitPlatform() } + +sourceSets { + main.java.srcDirs += ['build/generated/sources/annotationProcessor/java/main'] +} + +compileJava { + options.annotationProcessorGeneratedSourcesDirectory = file('build/generated/sources/annotationProcessor/java/main') +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/constants/Constants.java b/src/main/java/com/example/solidconnection/constants/Constants.java index 1ce5c7205..a25a2778c 100644 --- a/src/main/java/com/example/solidconnection/constants/Constants.java +++ b/src/main/java/com/example/solidconnection/constants/Constants.java @@ -4,4 +4,5 @@ public class Constants { public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; public static final int APPLICATION_UPDATE_COUNT_LIMIT = 3; public final static int RECOMMEND_UNIVERSITY_NUM = 6; + public final static String TERM = "2024-1"; //TODO: 2024-2로 수정 필요 } diff --git a/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java index a470184ba..d03569637 100644 --- a/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java @@ -9,6 +9,8 @@ import java.util.ArrayList; import java.util.List; +import static com.example.solidconnection.constants.Constants.TERM; + @Component @RequiredArgsConstructor public class GeneralRecommendUniversities { @@ -27,12 +29,12 @@ public class GeneralRecommendUniversities { public void init() { recommendedUniversities = new ArrayList<>(); - UniversityInfoForApply univ1 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_1).get(); - UniversityInfoForApply univ2 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_2).get(); - UniversityInfoForApply univ3 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_3).get(); - UniversityInfoForApply univ4 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_4).get(); - UniversityInfoForApply univ5 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_5).get(); - UniversityInfoForApply univ6 = universityInfoForApplyRepository.findByUniversity_KoreanName(RECOMMEND_UNIVERSITY_6).get(); + UniversityInfoForApply univ1 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_1, TERM).get(); + UniversityInfoForApply univ2 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_2, TERM).get(); + UniversityInfoForApply univ3 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_3, TERM).get(); + UniversityInfoForApply univ4 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_4, TERM).get(); + UniversityInfoForApply univ5 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_5, TERM).get(); + UniversityInfoForApply univ6 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_6, TERM).get(); recommendedUniversities.add(univ1); recommendedUniversities.add(univ2); diff --git a/src/main/java/com/example/solidconnection/type/CountryCode.java b/src/main/java/com/example/solidconnection/type/CountryCode.java index abcd67407..3ba226875 100644 --- a/src/main/java/com/example/solidconnection/type/CountryCode.java +++ b/src/main/java/com/example/solidconnection/type/CountryCode.java @@ -3,6 +3,7 @@ import com.example.solidconnection.custom.exception.CustomException; import java.util.Arrays; +import java.util.List; import java.util.Optional; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COUNTRY_NAME; @@ -49,6 +50,12 @@ public static CountryCode getCountryCodeByKoreanName(String koreanName) { return matchingCountryCode.orElseThrow(() -> new CustomException(INVALID_COUNTRY_NAME, koreanName)); } + public static List getCountryCodeMatchesToKeyword(String keyword) { + return Arrays.stream(CountryCode.values()) + .filter(country -> country.koreanName.contains(keyword)) + .toList(); + } + public String getKoreanName() { return koreanName; } diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 2c3814e43..477760826 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -3,12 +3,15 @@ import com.example.solidconnection.custom.response.CustomResponse; import com.example.solidconnection.custom.response.DataResponse; import com.example.solidconnection.university.dto.UniversityDetailDto; +import com.example.solidconnection.university.dto.UniversityPreviewDto; import com.example.solidconnection.university.service.UniversityService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController -@RequestMapping("university") +@RequestMapping("/university") @RequiredArgsConstructor public class UniversityController { @@ -19,4 +22,11 @@ public CustomResponse getDetails(@PathVariable Long universityInfoForApplyId) { UniversityDetailDto universityDetailDto = universityService.getDetail(universityInfoForApplyId); return new DataResponse<>(universityDetailDto); } + + @GetMapping("/search") + public CustomResponse search(@RequestParam(required = false, defaultValue = "") String region, + @RequestParam(required = false, defaultValue = "") String keyword){ + List universityPreviewDto = universityService.search(region, keyword); + return new DataResponse<>(universityPreviewDto); + } } diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java b/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java new file mode 100644 index 000000000..14ee0fa28 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.university.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UniversityPreviewDto { + private long id; + private String koreanName; + private String region; + private String country; + private String logoImageUrl; + private int studentCapacity; + private List languageRequirements; +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index f4ed9d886..d2bcc3841 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -9,6 +9,7 @@ @Repository public interface UniversityInfoForApplyRepository extends JpaRepository { - Optional findByUniversity(University university); - Optional findByUniversity_KoreanName(String koreanName); + Optional findByUniversityAndTerm(University university, String term); + Optional findByUniversity_KoreanNameAndTerm(String koreanName, String term); + Optional findByIdAndTerm(Long id, String term); } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java index 77f0559e7..5b217d1e0 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java @@ -3,6 +3,7 @@ import com.example.solidconnection.entity.University; import com.example.solidconnection.type.CountryCode; import com.example.solidconnection.type.RegionCode; +import com.example.solidconnection.university.repository.custom.UniversityRepositoryForFilter; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -11,7 +12,7 @@ import java.util.List; @Repository -public interface UniversityRepository extends JpaRepository { +public interface UniversityRepository extends JpaRepository, UniversityRepositoryForFilter { @Query("SELECT u FROM University u WHERE u.country.code IN :countries OR u.region.code IN :regions") List findByCountryCodeInOrRegionCodeIn(@Param("countries") List countries, @Param("regions") List regions); diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java new file mode 100644 index 000000000..80ff57784 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.university.repository.custom; + +import com.example.solidconnection.entity.University; +import com.example.solidconnection.type.CountryCode; +import com.example.solidconnection.type.RegionCode; + +import java.util.List; + +public interface UniversityRepositoryForFilter { + List findByRegionAndKeyword(RegionCode regionCode, List countryCodes, String keyword); +} diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java new file mode 100644 index 000000000..55105a9b2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java @@ -0,0 +1,63 @@ +package com.example.solidconnection.university.repository.custom; + +import com.example.solidconnection.entity.QCountry; +import com.example.solidconnection.entity.QRegion; +import com.example.solidconnection.entity.QUniversity; +import com.example.solidconnection.entity.University; +import com.example.solidconnection.type.CountryCode; +import com.example.solidconnection.type.RegionCode; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class UniversityRepositoryForFilterImpl implements UniversityRepositoryForFilter { + private final JPAQueryFactory queryFactory; + + @Autowired + public UniversityRepositoryForFilterImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public List findByRegionAndKeyword(RegionCode regionCode, List countryCodes, String keyword) { + QUniversity university = QUniversity.university; + QCountry country = QCountry.country; + QRegion region = QRegion.region; + + return queryFactory + .selectFrom(university) + .join(university.country, country) + .join(country.region, region) + .where( + regionCodeEq(regionCode, region).and(keywordContainsInCountryOrName(countryCodes, country, keyword, university)) + ) + .fetch(); + } + + private BooleanExpression regionCodeEq(RegionCode regionCode, QRegion region) { + if(regionCode == null) { + return Expressions.asBoolean(true).isTrue(); + } + return region.code.eq(regionCode); + } + + private BooleanExpression keywordContainsInCountryOrName(List countryCodes, QCountry country, String keyword, QUniversity university) { + if (countryCodes == null || countryCodes.isEmpty()) { // 해당하는 국가가 없으면 + if (keyword == null || keyword.isEmpty()) { + return Expressions.asBoolean(true).isTrue(); + } + return university.koreanName.contains(keyword); // 키워드에 해당하는 식을 반환 + } + // 해당하는 국가가 있으면, + if (keyword == null || keyword.isEmpty()) { + return Expressions.asBoolean(true).isTrue(); + } + return country.code.in(countryCodes).or(university.koreanName.contains(keyword)); // 키워드에 해당하는 식을 반환 + } +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index e8ee6fbc4..563a0d217 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -1,5 +1,6 @@ package com.example.solidconnection.university.service; +import com.example.solidconnection.constants.GeneralRecommendUniversities; import com.example.solidconnection.entity.SiteUser; import com.example.solidconnection.entity.University; import com.example.solidconnection.entity.UniversityInfoForApply; @@ -11,10 +12,11 @@ import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.university.dto.LanguageRequirementDto; import com.example.solidconnection.university.dto.UniversityDetailDto; +import com.example.solidconnection.university.dto.UniversityPreviewDto; import com.example.solidconnection.university.repository.LanguageRequirementRepository; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.UniversityRepository; -import com.example.solidconnection.constants.GeneralRecommendUniversities; +import com.example.solidconnection.university.repository.custom.UniversityRepositoryForFilterImpl; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -36,8 +38,9 @@ public class UniversityService { private final InterestedRegionRepository interestedRegionRepository; private final GeneralRecommendUniversities generalRecommendUniversities; private final UniversityValidator universityValidator; + private final UniversityRepositoryForFilterImpl universityRepositoryForFilter; - public List getPersonalRecommends(String email){ + public List getPersonalRecommends(String email) { SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); List interestedCountries = interestedCountyRepository.findAllBySiteUser(siteUser) .stream().map(interestedCountry -> interestedCountry.getCountry().getCode()) @@ -46,27 +49,25 @@ public List getPersonalRecommends(String email){ .stream().map(interestedRegion -> interestedRegion.getRegion().getCode()) .toList(); List recommendedUniversities = new java.util.ArrayList<>(universityRepository.findByCountryCodeInOrRegionCodeIn(interestedCountries, interestedRegions) - .stream().map(university -> { - return universityInfoForApplyRepository.findByUniversity(university).get(); - }) + .stream().map(universityValidator::getValidatedUniversityInfoForApplyByUniversity) .toList()); Collections.shuffle(recommendedUniversities); List shuffledList = recommendedUniversities.subList(0, Math.min(RECOMMEND_UNIVERSITY_NUM, recommendedUniversities.size())); - if(shuffledList.size() < 6){ + if (shuffledList.size() < 6) { shuffledList.addAll(getGeneralRecommendsExcept(shuffledList)); } return shuffledList.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); } - public List getGeneralRecommends(){ + public List getGeneralRecommends() { List generalRecommend = new java.util.ArrayList<>(generalRecommendUniversities.getRecommendedUniversities()); Collections.shuffle(generalRecommend); return generalRecommend.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); } - private List getGeneralRecommendsExcept(List alreadyPicked){ + private List getGeneralRecommendsExcept(List alreadyPicked) { List generalRecommend = new java.util.ArrayList<>(generalRecommendUniversities.getRecommendedUniversities()); generalRecommend.removeAll(alreadyPicked); int sizeToPick = RECOMMEND_UNIVERSITY_NUM - alreadyPicked.size(); @@ -74,7 +75,7 @@ private List getGeneralRecommendsExcept(List search(String region, String keyword) { + RegionCode regionCode = null; + if (region != null && !region.isBlank()) { + regionCode = RegionCode.getRegionCodeByKoreanName(region); + } + + List countryCodes = null; + if (keyword != null && !keyword.isBlank()) { + countryCodes = CountryCode.getCountryCodeMatchesToKeyword(keyword); + } + + List universities = universityRepositoryForFilter.findByRegionAndKeyword(regionCode, countryCodes, keyword); + return universities.stream() + .map(university -> { + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); + return makeUniversityPreviewDto(university, universityInfoForApply); + }) + .toList(); + } + + private UniversityPreviewDto makeUniversityPreviewDto(University university, UniversityInfoForApply universityInfoForApply) { + return UniversityPreviewDto.builder() + .id(universityInfoForApply.getId()) + .region(university.getRegion().getCode().getKoreanName()) + .country(university.getCountry().getCode().getKoreanName()) + .logoImageUrl(university.getLogoImageUrl()) + .koreanName(university.getKoreanName()) + .studentCapacity(universityInfoForApply.getStudentCapacity()) + .languageRequirements(universityInfoForApply.getLanguageRequirements().stream() + .map(LanguageRequirementDto::fromEntity) + .toList() + ) + .build(); + } } diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java index 2f3e80361..feedce713 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java @@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import static com.example.solidconnection.constants.Constants.TERM; import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_NOT_FOUND; @@ -18,7 +19,12 @@ public class UniversityValidator { private final UniversityRepository universityRepository; public UniversityInfoForApply getValidatedUniversityInfoForApplyById(Long id){ - return universityInfoForApplyRepository.findById(id) + return universityInfoForApplyRepository.findByIdAndTerm(id, TERM) + .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); + } + + public UniversityInfoForApply getValidatedUniversityInfoForApplyByUniversity(University university){ + return universityInfoForApplyRepository.findByUniversityAndTerm(university, TERM) .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); } From 5c3f9a8960cadda02f947bf49368e3405656b8a3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 03:56:49 +0900 Subject: [PATCH 036/158] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=EC=9E=90=20?= =?UTF-8?q?=ED=98=84=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApplicationController.java | 16 +++- .../application/dto/ApplicantDto.java | 28 +++++++ .../application/dto/ApplicationsDto.java | 15 ++++ .../dto/UniversityApplicantsDto.java | 16 ++++ .../repository/ApplicationRepository.java | 5 ++ .../service/ApplicationService.java | 82 +++++++++++++++++-- .../service/ApplicationValidator.java | 5 +- .../custom/exception/ErrorCode.java | 3 +- .../custom/UniversityRepositoryForFilter.java | 2 +- .../UniversityRepositoryForFilterImpl.java | 8 +- .../university/service/UniversityService.java | 2 +- 11 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/application/dto/ApplicantDto.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/ApplicationsDto.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 944bff18b..0a20fa560 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -1,16 +1,15 @@ package com.example.solidconnection.application.controller; +import com.example.solidconnection.application.dto.ApplicationsDto; import com.example.solidconnection.application.dto.ScoreRequestDto; import com.example.solidconnection.application.dto.UniversityRequestDto; import com.example.solidconnection.application.service.ApplicationService; import com.example.solidconnection.custom.response.CustomResponse; +import com.example.solidconnection.custom.response.DataResponse; import com.example.solidconnection.custom.response.StatusResponse; 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; +import org.springframework.web.bind.annotation.*; import java.security.Principal; @@ -31,4 +30,13 @@ public CustomResponse registerUniversity(Principal principal, @Valid @RequestBod boolean result = applicationService.saveUniversity(principal.getName(), universityRequestDto); return new StatusResponse(result); } + + @GetMapping + public CustomResponse getApplicants( + Principal principal, + @RequestParam(required = false, defaultValue = "") String region, + @RequestParam(required = false, defaultValue = "") String keyword) { + ApplicationsDto result = applicationService.getApplicants(principal.getName(), region, keyword); + return new DataResponse<>(result); + } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicantDto.java b/src/main/java/com/example/solidconnection/application/dto/ApplicantDto.java new file mode 100644 index 000000000..18108a701 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicantDto.java @@ -0,0 +1,28 @@ +package com.example.solidconnection.application.dto; + +import com.example.solidconnection.entity.Application; +import com.example.solidconnection.type.LanguageTestType; +import lombok.*; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ApplicantDto { + private String nicknameForApply; + private float gpa; + private LanguageTestType testType; + private String testScore; + private boolean isMine; + + public static ApplicantDto fromEntity(Application application, boolean isMine) { + return ApplicantDto.builder() + .nicknameForApply(application.getNicknameForApply()) + .gpa(application.getGpa()) + .testType(application.getLanguageTestType()) + .testScore(application.getLanguageTestScore()) + .isMine(isMine) + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationsDto.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationsDto.java new file mode 100644 index 000000000..8094ba408 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationsDto.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.application.dto; + +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ApplicationsDto { + private List firstChoice; + private List secondChoice; +} diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java new file mode 100644 index 000000000..34394f021 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.application.dto; + +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UniversityApplicantsDto { + private String koreanName; + private int studentCapacity; + private List applicants; +} diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java index 9eab723fb..c2b4b90a6 100644 --- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -1,9 +1,12 @@ package com.example.solidconnection.application.repository; import com.example.solidconnection.entity.Application; +import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.type.VerifyStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -11,4 +14,6 @@ public interface ApplicationRepository extends JpaRepository boolean existsBySiteUser_Email(String email); boolean existsByNicknameForApply(String nicknameForApply); Optional findBySiteUser_Email(String email); + List findAllByFirstChoiceUniversityAndVerifyStatus(UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus); + List findAllBySecondChoiceUniversityAndVerifyStatus(UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus); } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index ab7c5cb5f..ad0540541 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -1,25 +1,29 @@ package com.example.solidconnection.application.service; -import com.example.solidconnection.application.dto.ScoreRequestDto; -import com.example.solidconnection.application.dto.UniversityRequestDto; +import com.example.solidconnection.application.dto.*; import com.example.solidconnection.application.repository.ApplicationRepository; import com.example.solidconnection.constants.NicknameForApplyWords; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.Application; import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.entity.University; import com.example.solidconnection.entity.UniversityInfoForApply; import com.example.solidconnection.siteuser.service.SiteUserValidator; +import com.example.solidconnection.type.CountryCode; +import com.example.solidconnection.type.RegionCode; +import com.example.solidconnection.type.VerifyStatus; +import com.example.solidconnection.university.repository.custom.UniversityRepositoryForFilterImpl; import com.example.solidconnection.university.service.UniversityValidator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Objects; import java.util.Random; import static com.example.solidconnection.constants.Constants.APPLICATION_UPDATE_COUNT_LIMIT; -import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; -import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; +import static com.example.solidconnection.custom.exception.ErrorCode.*; @Service @RequiredArgsConstructor @@ -29,8 +33,11 @@ public class ApplicationService { private final UniversityValidator universityValidator; private final SiteUserValidator siteUserValidator; private final ApplicationValidator applicationValidator; + private final UniversityRepositoryForFilterImpl universityRepositoryForFilter; public boolean saveScore(String email, ScoreRequestDto scoreRequestDto) { + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + // 한번 등록 후 수정 if (applicationRepository.existsBySiteUser_Email(email)) { Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); @@ -49,7 +56,6 @@ public boolean saveScore(String email, ScoreRequestDto scoreRequestDto) { } // 최초 증록 - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); Application application = Application.saveScore(siteUser, scoreRequestDto); applicationRepository.save(application); return true; @@ -97,4 +103,70 @@ private String makeRandomNickname() { String randomNoun = NicknameForApplyWords.nouns.get(randomIndex2); return randomAdjective + " " + randomNoun; } + + public ApplicationsDto getApplicants(String email, String region, String keyword) { + // 유저 검증 + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + // 지원했는지 검증 + Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); + // 승인되었는지 확인 + validateApproved(application); + + RegionCode regionCode = null; + if (region != null && !region.isBlank()) { + regionCode = RegionCode.getRegionCodeByKoreanName(region); + } + List countryCodes = null; + if (keyword != null && !keyword.isBlank()) { + countryCodes = CountryCode.getCountryCodeMatchesToKeyword(keyword); + } + + List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, keyword); + List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser); + List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser); + return ApplicationsDto.builder() + .firstChoice(firstChoiceApplicants) + .secondChoice(secondChoiceApplicants) + .build(); + } + + private void validateApproved(Application application) { + if (application.getVerifyStatus() != VerifyStatus.APPROVED) { + throw new CustomException(APPLICATION_NOT_APPROVED); + } + } + + private List getFirstChoiceApplicants(List universities, SiteUser siteUser) { + return universities.stream() + .map(university -> { + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); + List firstChoiceApplication = applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatus(universityInfoForApply, VerifyStatus.APPROVED); + List firstChoiceApplicant = firstChoiceApplication.stream() + .map(ap -> ApplicantDto.fromEntity(ap, Objects.equals(siteUser.getId(), ap.getSiteUser().getId()))) + .toList(); + return UniversityApplicantsDto.builder() + .koreanName(university.getKoreanName()) + .studentCapacity(universityInfoForApply.getStudentCapacity()) + .applicants(firstChoiceApplicant) + .build(); + }) + .toList(); + } + + private List getSecondChoiceApplicants(List universities, SiteUser siteUser) { + return universities.stream() + .map(university -> { + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); + List secondChoiceApplication = applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatus(universityInfoForApply, VerifyStatus.APPROVED); + List secondChoiceApplicant = secondChoiceApplication.stream() + .map(ap -> ApplicantDto.fromEntity(ap, Objects.equals(siteUser.getId(), ap.getSiteUser().getId()))) + .toList(); + return UniversityApplicantsDto.builder() + .koreanName(university.getKoreanName()) + .studentCapacity(universityInfoForApply.getStudentCapacity()) + .applicants(secondChoiceApplicant) + .build(); + }) + .toList(); + } } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java b/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java index 58820c6aa..e91a558c4 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java @@ -6,7 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import static com.example.solidconnection.custom.exception.ErrorCode.SCORE_SUBMIT_FIRST; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLICATION_NOT_FOUND; @Service @RequiredArgsConstructor @@ -14,6 +14,7 @@ public class ApplicationValidator { private final ApplicationRepository applicationRepository; public Application getValidatedApplicationBySiteUser_Email(String email){ - return applicationRepository.findBySiteUser_Email(email).orElseThrow(() -> new CustomException(SCORE_SUBMIT_FIRST)); + return applicationRepository.findBySiteUser_Email(email) + .orElseThrow(() -> new CustomException(APPLICATION_NOT_FOUND)); } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 50e6c4012..3c07db4ee 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -9,9 +9,10 @@ @Getter @AllArgsConstructor public enum ErrorCode { + APPLICATION_NOT_APPROVED(HttpStatus.BAD_REQUEST.value(), "성적표가 인증되지 않았습니다."), APPLY_UPDATE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST.value(), "지원 정보 수정은 3회까지만 가능합니다."), CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2지망에 동일한 대학교를 입력할 수 없습니다."), - SCORE_SUBMIT_FIRST(HttpStatus.BAD_REQUEST.value(), "성적 입력 후 지망 대학을 입력할 수 있습니다."), + APPLICATION_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "성적을 입력하지 않았습니다."), INVALID_INPUT(HttpStatus.BAD_REQUEST.value(), "값을 입력할 수 없습니다."), CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 "+MIN_DAYS_BETWEEN_NICKNAME_CHANGES+"일이 지나지 않았습니다."), UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교 지원 정보입니다."), diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java index 80ff57784..c00ccedcb 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java @@ -7,5 +7,5 @@ import java.util.List; public interface UniversityRepositoryForFilter { - List findByRegionAndKeyword(RegionCode regionCode, List countryCodes, String keyword); + List findByRegionAndCountryAndKeyword(RegionCode regionCode, List countryCodes, String keyword); } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java index 55105a9b2..49d0318f5 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java @@ -25,7 +25,7 @@ public UniversityRepositoryForFilterImpl(EntityManager em) { } @Override - public List findByRegionAndKeyword(RegionCode regionCode, List countryCodes, String keyword) { + public List findByRegionAndCountryAndKeyword(RegionCode regionCode, List countryCodes, String keyword) { QUniversity university = QUniversity.university; QCountry country = QCountry.country; QRegion region = QRegion.region; @@ -54,10 +54,10 @@ private BooleanExpression keywordContainsInCountryOrName(List count } return university.koreanName.contains(keyword); // 키워드에 해당하는 식을 반환 } - // 해당하는 국가가 있으면, - if (keyword == null || keyword.isEmpty()) { + + if (keyword == null || keyword.isEmpty()) { // 해당하는 국가가 있으면, return Expressions.asBoolean(true).isTrue(); } - return country.code.in(countryCodes).or(university.koreanName.contains(keyword)); // 키워드에 해당하는 식을 반환 + return country.code.in(countryCodes).or(university.koreanName.contains(keyword)); // 국가와 키워드에 해당하는 식을 반환 } } diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index 563a0d217..eba97dc14 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -136,7 +136,7 @@ public List search(String region, String keyword) { countryCodes = CountryCode.getCountryCodeMatchesToKeyword(keyword); } - List universities = universityRepositoryForFilter.findByRegionAndKeyword(regionCode, countryCodes, keyword); + List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, keyword); return universities.stream() .map(university -> { UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); From 156e1bc6ba46ca140c90818c7dadc7f93ced123c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 04:26:12 +0900 Subject: [PATCH 037/158] =?UTF-8?q?feat:=20=EC=84=B1=EC=A0=81=20=EC=A0=9C?= =?UTF-8?q?=EC=B6=9C=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=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 --- .../controller/ApplicationController.java | 7 +++++++ .../application/dto/VerifyStatusDto.java | 12 ++++++++++++ .../application/service/ApplicationService.java | 13 +++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 0a20fa560..24c6d8c4f 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -3,6 +3,7 @@ import com.example.solidconnection.application.dto.ApplicationsDto; import com.example.solidconnection.application.dto.ScoreRequestDto; import com.example.solidconnection.application.dto.UniversityRequestDto; +import com.example.solidconnection.application.dto.VerifyStatusDto; import com.example.solidconnection.application.service.ApplicationService; import com.example.solidconnection.custom.response.CustomResponse; import com.example.solidconnection.custom.response.DataResponse; @@ -39,4 +40,10 @@ public CustomResponse getApplicants( ApplicationsDto result = applicationService.getApplicants(principal.getName(), region, keyword); return new DataResponse<>(result); } + + @GetMapping("/status") + public CustomResponse getVerifyStatus(Principal principal) { + VerifyStatusDto result = applicationService.getVerifyStatus(principal.getName()); + return new DataResponse<>(result); + } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java b/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java new file mode 100644 index 000000000..c6cdc7653 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.application.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class VerifyStatusDto { + private String status; +} diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index ad0540541..a62b912de 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Random; import static com.example.solidconnection.constants.Constants.APPLICATION_UPDATE_COUNT_LIMIT; @@ -169,4 +170,16 @@ private List getSecondChoiceApplicants(List }) .toList(); } + + public VerifyStatusDto getVerifyStatus(String email) { + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + Optional application = applicationRepository.findBySiteUser_Email(siteUser.getEmail()); + if (application.isEmpty()) { + return new VerifyStatusDto("NOT_SUBMITTED"); + } + if (application.get().getVerifyStatus() == VerifyStatus.APPROVED) { + return new VerifyStatusDto("SUBMITTED_APPROVED"); + } + return new VerifyStatusDto("SUBMITTED_NOT_APPROVED"); + } } From bcbd91b578881f5fb7be043d2f3fe7a40018e1a3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 12:07:10 +0900 Subject: [PATCH 038/158] =?UTF-8?q?feat:=20=EC=9C=84=EC=8B=9C=20=ED=95=99?= =?UTF-8?q?=EA=B5=90=EC=97=90=20=EC=B6=94=EA=B0=80,=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/LikedUniversity.java | 10 ++++- .../siteuser/controller/MyPageController.java | 8 ++++ .../repository/LikedUniversityRepository.java | 4 ++ .../siteuser/service/MyPageService.java | 13 ++++++ .../controller/UniversityController.java | 8 ++++ .../university/dto/LikedResultDto.java | 14 +++++++ .../university/dto/UniversityPreviewDto.java | 16 ++++++++ .../university/service/UniversityService.java | 40 ++++++++++++------- 8 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/university/dto/LikedResultDto.java diff --git a/src/main/java/com/example/solidconnection/entity/LikedUniversity.java b/src/main/java/com/example/solidconnection/entity/LikedUniversity.java index 77b0105e7..1c44b2975 100644 --- a/src/main/java/com/example/solidconnection/entity/LikedUniversity.java +++ b/src/main/java/com/example/solidconnection/entity/LikedUniversity.java @@ -1,10 +1,16 @@ package com.example.solidconnection.entity; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor public class LikedUniversity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -12,8 +18,8 @@ public class LikedUniversity { // 연관 관계 @ManyToOne - @JoinColumn(name = "university_id") - private University university; + @JoinColumn(name = "university_info_for_apply_id") + private UniversityInfoForApply universityInfoForApply; @ManyToOne @JoinColumn(name = "site_user_id") diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java index b79a1bee9..75463e0c0 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java @@ -6,11 +6,13 @@ import com.example.solidconnection.siteuser.dto.MyPageDto; import com.example.solidconnection.siteuser.dto.MyPageUpdateDto; import com.example.solidconnection.siteuser.service.MyPageService; +import com.example.solidconnection.university.dto.UniversityPreviewDto; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.security.Principal; +import java.util.List; @RestController @RequestMapping("/my-page") @@ -35,4 +37,10 @@ public CustomResponse update(Principal principal, @Valid @RequestBody MyPageUpda myPageService.update(principal.getName(), myPageUpdateDto); return new StatusResponse(true); } + + @GetMapping("/wish-university") + public CustomResponse getWishUniversity(Principal principal) { + List wishUniversities = myPageService.getWishUniversity(principal.getName()); + return new DataResponse<>(wishUniversities); + } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java index 5eab45c53..fc4bcef1c 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java @@ -1,11 +1,15 @@ package com.example.solidconnection.siteuser.repository; import com.example.solidconnection.entity.LikedUniversity; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.entity.UniversityInfoForApply; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface LikedUniversityRepository extends JpaRepository { List findAllBySiteUser_Email(String email); int countBySiteUser_Email(String email); + Optional findBySiteUserAndUniversityInfoForApply(SiteUser siteUser, UniversityInfoForApply universityInfoForApply); } diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java index 81c2be3fc..c6ac59447 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -1,17 +1,21 @@ package com.example.solidconnection.siteuser.service; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.LikedUniversity; import com.example.solidconnection.entity.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageDto; import com.example.solidconnection.siteuser.dto.MyPageUpdateDto; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.dto.UniversityPreviewDto; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.List; import static com.example.solidconnection.constants.Constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; @@ -23,6 +27,7 @@ public class MyPageService { private final SiteUserValidator siteUserValidator; private final SiteUserRepository siteUserRepository; private final LikedUniversityRepository likedUniversityRepository; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; public MyPageDto getMyPageInfo(String email) { SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); @@ -61,4 +66,12 @@ private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); } } + + public List getWishUniversity(String email) { + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + List likedUniversities = likedUniversityRepository.findAllBySiteUser_Email(siteUser.getEmail()); + return likedUniversities.stream() + .map(likedUniversity -> UniversityPreviewDto.fromEntity(likedUniversity.getUniversityInfoForApply())) + .toList(); + } } diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 477760826..ad77bc121 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -2,12 +2,14 @@ import com.example.solidconnection.custom.response.CustomResponse; import com.example.solidconnection.custom.response.DataResponse; +import com.example.solidconnection.university.dto.LikedResultDto; import com.example.solidconnection.university.dto.UniversityDetailDto; import com.example.solidconnection.university.dto.UniversityPreviewDto; import com.example.solidconnection.university.service.UniversityService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; @RestController @@ -29,4 +31,10 @@ public CustomResponse search(@RequestParam(required = false, defaultValue = "") List universityPreviewDto = universityService.search(region, keyword); return new DataResponse<>(universityPreviewDto); } + + @PostMapping("/{universityInfoForApplyId}/like") + public CustomResponse like(Principal principal, @PathVariable Long universityInfoForApplyId){ + LikedResultDto likedResultDto = universityService.like(principal.getName(), universityInfoForApplyId); + return new DataResponse<>(likedResultDto); + } } diff --git a/src/main/java/com/example/solidconnection/university/dto/LikedResultDto.java b/src/main/java/com/example/solidconnection/university/dto/LikedResultDto.java new file mode 100644 index 000000000..dcea241a5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/LikedResultDto.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.university.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LikedResultDto { + private String result; +} diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java b/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java index 14ee0fa28..274a740d1 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java @@ -1,5 +1,6 @@ package com.example.solidconnection.university.dto; +import com.example.solidconnection.entity.UniversityInfoForApply; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -19,4 +20,19 @@ public class UniversityPreviewDto { private String logoImageUrl; private int studentCapacity; private List languageRequirements; + + public static UniversityPreviewDto fromEntity(UniversityInfoForApply universityInfoForApply) { + return UniversityPreviewDto.builder() + .id(universityInfoForApply.getId()) + .region(universityInfoForApply.getUniversity().getRegion().getCode().getKoreanName()) + .country(universityInfoForApply.getUniversity().getCountry().getCode().getKoreanName()) + .logoImageUrl(universityInfoForApply.getUniversity().getLogoImageUrl()) + .koreanName(universityInfoForApply.getUniversity().getKoreanName()) + .studentCapacity(universityInfoForApply.getStudentCapacity()) + .languageRequirements(universityInfoForApply.getLanguageRequirements().stream() + .map(LanguageRequirementDto::fromEntity) + .toList() + ) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index eba97dc14..f48d00ad1 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -1,16 +1,19 @@ package com.example.solidconnection.university.service; import com.example.solidconnection.constants.GeneralRecommendUniversities; +import com.example.solidconnection.entity.LikedUniversity; import com.example.solidconnection.entity.SiteUser; import com.example.solidconnection.entity.University; import com.example.solidconnection.entity.UniversityInfoForApply; import com.example.solidconnection.home.dto.RecommendedUniversityDto; import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.service.SiteUserValidator; import com.example.solidconnection.type.CountryCode; import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.university.dto.LanguageRequirementDto; +import com.example.solidconnection.university.dto.LikedResultDto; import com.example.solidconnection.university.dto.UniversityDetailDto; import com.example.solidconnection.university.dto.UniversityPreviewDto; import com.example.solidconnection.university.repository.LanguageRequirementRepository; @@ -22,6 +25,7 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static com.example.solidconnection.constants.Constants.RECOMMEND_UNIVERSITY_NUM; @@ -33,9 +37,10 @@ public class UniversityService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final UniversityRepository universityRepository; private final LanguageRequirementRepository languageRequirementRepository; - private final SiteUserValidator siteUserValidator; private final InterestedCountyRepository interestedCountyRepository; private final InterestedRegionRepository interestedRegionRepository; + private final LikedUniversityRepository likedUniversityRepository; + private final SiteUserValidator siteUserValidator; private final GeneralRecommendUniversities generalRecommendUniversities; private final UniversityValidator universityValidator; private final UniversityRepositoryForFilterImpl universityRepositoryForFilter; @@ -140,23 +145,30 @@ public List search(String region, String keyword) { return universities.stream() .map(university -> { UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); - return makeUniversityPreviewDto(university, universityInfoForApply); + return UniversityPreviewDto.fromEntity(universityInfoForApply); }) .toList(); } - private UniversityPreviewDto makeUniversityPreviewDto(University university, UniversityInfoForApply universityInfoForApply) { - return UniversityPreviewDto.builder() - .id(universityInfoForApply.getId()) - .region(university.getRegion().getCode().getKoreanName()) - .country(university.getCountry().getCode().getKoreanName()) - .logoImageUrl(university.getLogoImageUrl()) - .koreanName(university.getKoreanName()) - .studentCapacity(universityInfoForApply.getStudentCapacity()) - .languageRequirements(universityInfoForApply.getLanguageRequirements().stream() - .map(LanguageRequirementDto::fromEntity) - .toList() - ) + public LikedResultDto like(String email, Long universityInfoForApplyId) { + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyById(universityInfoForApplyId); + + Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); + if (alreadyLikedUniversity.isPresent()) { + likedUniversityRepository.delete(alreadyLikedUniversity.get()); + return LikedResultDto.builder() + .result("LIKE_CANCELED") + .build(); + } + + LikedUniversity likedUniversity = LikedUniversity.builder() + .universityInfoForApply(universityInfoForApply) + .siteUser(siteUser) + .build(); + likedUniversityRepository.save(likedUniversity); + return LikedResultDto.builder() + .result("LIKE_SUCCESS") .build(); } } From d2cc1b0120757c0f32dad55449dae36c6be6db87 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 13:01:05 +0900 Subject: [PATCH 039/158] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=9C=ED=96=89=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/security/JwtAuthenticationFilter.java | 12 +++++++++++- .../config/security/SecurityConfiguration.java | 2 +- .../solidconnection/config/token/TokenService.java | 13 +++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 99f4f7de8..35c9eb873 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -37,6 +37,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // 인증 정보를 저장할 필요 없는 url AntPathMatcher pathMatcher = new AntPathMatcher(); for (String endpoint : getPermitAllEndpoints()) { if (pathMatcher.match(endpoint, request.getRequestURI())) { @@ -45,10 +46,18 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } + // 토큰 검증 try { String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 if (token != null) { // 토큰이 있어야 검증 - 토큰 유무에 대한 다른 처리를 컨트롤러에서 할 수 있음 try { + String requestURI = request.getRequestURI(); + if(requestURI.equals("/auth/reissue")) { + Authentication auth = this.tokenService.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + filterChain.doFilter(request, response); + return; + } tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 } catch (ExpiredJwtException e) { throw new JwtExpiredTokenException(ACCESS_TOKEN_EXPIRED.getMessage()); @@ -63,8 +72,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } catch (AuthenticationException e) { SecurityContextHolder.clearContext(); jwtAuthenticationEntryPoint.commence(request, response, e); + return; } catch (CustomException e) { jwtAuthenticationEntryPoint.customCommence(request, response, e); + return; } filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 } @@ -94,7 +105,6 @@ private HashSet getPermitAllEndpoints() { permitAllEndpoints.add("/auth/sign-up"); // 대학교 정보 - permitAllEndpoints.add("/university/detail/**"); permitAllEndpoints.add("/university/search/**"); return permitAllEndpoints; diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index eead6bc7d..7e082b7f8 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -49,7 +49,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers( "/", "/index.html", "/favicon.ico", "/img/profile/pre", - "/auth/kakao", "/auth/sign-up", + "/auth/kakao", "/auth/sign-up", "/auth/reissue", "/university/detail/**", "/university/search/**", "/home" ) .permitAll() diff --git a/src/main/java/com/example/solidconnection/config/token/TokenService.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java index 31617f607..bcc9c187e 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenService.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenService.java @@ -3,6 +3,7 @@ import com.example.solidconnection.custom.userdetails.CustomUserDetailsService; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.RequiredArgsConstructor; @@ -58,9 +59,13 @@ public String getEmail(String token) { } private Claims getClaim(String token) { - return Jwts.parser() - .setSigningKey(this.secretKey) - .parseClaimsJws(token) - .getBody(); + try { + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } } } \ No newline at end of file From 47b47e82aa02f33d52a23ff9b247106cfef19087 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 13:01:26 +0900 Subject: [PATCH 040/158] =?UTF-8?q?refactor:=20=EB=8C=80=ED=95=99=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=EC=97=90=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=97=AC=EB=B6=80=20=EC=BB=AC=EB=9F=BC=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 --- .../university/controller/UniversityController.java | 6 +++++- .../university/dto/UniversityDetailDto.java | 7 +++---- .../university/service/UniversityService.java | 7 +++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index ad77bc121..8a1c6dd20 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -20,8 +20,12 @@ public class UniversityController { private final UniversityService universityService; @GetMapping("/detail/{universityInfoForApplyId}") - public CustomResponse getDetails(@PathVariable Long universityInfoForApplyId) { + public CustomResponse getDetails(Principal principal, @PathVariable Long universityInfoForApplyId) { UniversityDetailDto universityDetailDto = universityService.getDetail(universityInfoForApplyId); + if (principal != null) { + boolean isLiked = universityService.getIsLiked(principal.getName(), universityInfoForApplyId); + universityDetailDto.setLiked(isLiked); + } return new DataResponse<>(universityDetailDto); } diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java index 866679642..2cc1d919e 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java @@ -1,9 +1,6 @@ package com.example.solidconnection.university.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.util.List; @@ -13,6 +10,8 @@ @AllArgsConstructor public class UniversityDetailDto { private long id; + @Setter + private boolean isLiked = false; private String term; private String koreanName; private String englishName; diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index f48d00ad1..59d6dd704 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -171,4 +171,11 @@ public LikedResultDto like(String email, Long universityInfoForApplyId) { .result("LIKE_SUCCESS") .build(); } + + public boolean getIsLiked(String email, Long universityInfoForApplyId) { + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyById(universityInfoForApplyId); + return likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply) + .isPresent(); + } } From 17dab02dd537af9f2d3febce26bdd4679aca15e9 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 19:22:59 +0900 Subject: [PATCH 041/158] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=202=ED=95=99=EA=B8=B0=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 --- .../solidconnection/constants/Constants.java | 2 +- .../entity/UniversityInfoForApply.java | 4 +- .../solidconnection/type/CountryCode.java | 7 +- .../type/LanguageTestType.java | 2 +- .../type/SemesterAvailableForDispatch.java | 1 + src/main/resources/data.sql | 184 +++++++++++++++++- 6 files changed, 193 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/solidconnection/constants/Constants.java b/src/main/java/com/example/solidconnection/constants/Constants.java index a25a2778c..6650e798c 100644 --- a/src/main/java/com/example/solidconnection/constants/Constants.java +++ b/src/main/java/com/example/solidconnection/constants/Constants.java @@ -4,5 +4,5 @@ public class Constants { public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; public static final int APPLICATION_UPDATE_COUNT_LIMIT = 3; public final static int RECOMMEND_UNIVERSITY_NUM = 6; - public final static String TERM = "2024-1"; //TODO: 2024-2로 수정 필요 + public final static String TERM = "2024-2"; //TODO: 2024-2로 수정 필요 } diff --git a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java b/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java index 0e6b01466..5c480e86d 100644 --- a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java +++ b/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java @@ -34,10 +34,10 @@ public class UniversityInfoForApply { @Column(length = 1000) private String detailsForLanguage; - @Column(length = 5) + @Column(length = 20) private String gpaRequirement; - @Column(length = 5) + @Column(length = 20) private String gpaRequirementCriteria; @Column(length = 1000) diff --git a/src/main/java/com/example/solidconnection/type/CountryCode.java b/src/main/java/com/example/solidconnection/type/CountryCode.java index 3ba226875..497ff5095 100644 --- a/src/main/java/com/example/solidconnection/type/CountryCode.java +++ b/src/main/java/com/example/solidconnection/type/CountryCode.java @@ -35,7 +35,12 @@ public enum CountryCode { FR("프랑스"), FI("핀란드"), CN("중국"), - TW("대만"); + TW("대만"), + HU("헝가리"), + LT("리투아니아"), + TH("태국"), + UZ("우즈베키스탄") + ; private final String koreanName; diff --git a/src/main/java/com/example/solidconnection/type/LanguageTestType.java b/src/main/java/com/example/solidconnection/type/LanguageTestType.java index b7c064747..9d5cbc482 100644 --- a/src/main/java/com/example/solidconnection/type/LanguageTestType.java +++ b/src/main/java/com/example/solidconnection/type/LanguageTestType.java @@ -1,5 +1,5 @@ package com.example.solidconnection.type; public enum LanguageTestType { - TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF + TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF, TCF, TEF } diff --git a/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java index 6510496e0..406d74f88 100644 --- a/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java +++ b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java @@ -3,6 +3,7 @@ public enum SemesterAvailableForDispatch { ONE_SEMESTER("1개학기"), FOUR_SEMESTER("4개학기"), + ONE_OR_TWO_SEMESTER("1개 또는 2개 학기"), ONE_YEAR("1년만 가능"), IRRELEVANT("무관"); diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 4b9200a05..c0e5c7006 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -162,7 +162,7 @@ VALUES ('JP', 'ASIA', 'Meiji University', 'meiji_university', '메이지대학', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png'), ('JP', 'ASIA', 'BAIKA Women''s University', 'baika_womens_university', '바이카여자대학', 'https://dormy-ac.com/page/baika/', NULL, 'https://www.baika.ac.jp/english/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/1.png'), ('JP', 'ASIA', 'Bunkyo Gakuin University', 'bunkyo_gakuin_university', '분쿄가쿠인대학', NULL, NULL, 'https://www.bgu.ac.jp/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/1.png'), - ('JP', 'ASIA', 'Yamaguchi University', 'yamaguchi_university', '야마구치대학', '-', NULL, 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yamaguchi_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yamaguchi_university/1.png'), + ('JP', 'ASIA', 'Yamaguchi University', 'yamaguchi_university', '야마구치대학', NULL, NULL, 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yamaguchi_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yamaguchi_university/1.png'), ('JP', 'ASIA', 'Osaka Gakuin University', 'osaka_gakuin_university', '오사카가쿠인대학', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/housing.html', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/syllabi.html', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/schedule.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/osaka_gakuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/osaka_gakuin_university/1.png'), ('JP', 'ASIA', 'Otsuma Women''s University', 'otsuma_womens_university', '오츠마여자대학', 'https://www.otsuma.ac.jp/english/international/dormitory.html', NULL, 'https://www.otsuma.ac.jp/english/index.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otsuma_womens_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otsuma_womens_university/1.png'), ('JP', 'ASIA', 'Waseda University', 'waseda_university', '와세다대학', 'https://www.waseda.jp/inst/rlc/en/student_dormitory/exchange/ ', NULL, 'https://www.waseda.jp/inst/cie/en/exchange/application ', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/waseda_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/waseda_university/1.png'), @@ -186,6 +186,33 @@ VALUES ('CN', 'CHINA', 'Minzu University of China', 'minzu_university_of_china', '중앙민족대학교', NULL, NULL, 'https://oir.muc.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/minzu_university_of_china/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/minzu_university_of_china/1.png'), (NULL, NULL, 'SAF Program', 'saf_program', 'SAF 프로그램', NULL, NULL, 'http://korea.studyabroadfoundation.org/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saf_program/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saf_program/1.png'); +INSERT INTO `solid_connection`.`university` +(`country_code`, `id`, `region_code`, `english_name`, `format_name`, `korean_name`, `accommodation_url`, `background_image_url`, `english_course_url`, `homepage_url`, `logo_image_url`, `details_for_local`) +VALUES + ('US', 150, 'AMERICAS', 'Temple University', 'temple_university', '템플대학(A형)', 'https://globalprograms.temple.edu/housing', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/temple_university/1.webp', 'https://prd-xereg.temple.edu/StudentRegistrationSsb/ssb/term/termSelection?mode=search', 'http://globalprograms.temple.edu/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/temple_university/logo.webp', NULL), + ('US', 151, 'AMERICAS', 'University of Hawaii at Manoa', 'university_of_hawaii_at_manoa', '하와이대학(A형)', 'https://manoa.hawaii.edu/mix/inbound/housing-meals/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_hawaii_at_manoa/1.webp', 'https://www.sis.hawaii.edu/uhdad/avail.classes?i=MAN', 'https://www.hawaii.edu/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_hawaii_at_manoa/logo.webp', NULL), + ('US', 152, 'AMERICAS', 'University of North Carolina at Wilmington', 'university_of_north_carolina_at_wilmington', '노스캐롤라이나 윌밍턴대학(A형)', 'https://uncw.edu/seahawk-life/dining-housing/housing/?utm_source=housing&utm_medium=301&utm_id=REDIR1', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_north_carolina_at_wilmington/1.webp', 'https://catalogue.uncw.edu/content.php?catoid=70&navoid=9352', 'https://uncw.edu/seahawk-life/support-success/international/students-scholars/prospective-students/visiting-international/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_north_carolina_at_wilmington/logo.webp', NULL), + ('US', 153, 'AMERICAS', 'University of North Carolina at Wilmington', 'university_of_north_carolina_at_wilmington', '노스캐롤라이나 윌밍턴대학(B형)', 'https://uncw.edu/seahawk-life/dining-housing/housing/?utm_source=housing&utm_medium=301&utm_id=REDIR1', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_north_carolina_at_wilmington/1.webp', 'https://catalogue.uncw.edu/content.php?catoid=70&navoid=9352', 'https://uncw.edu/seahawk-life/support-success/international/students-scholars/prospective-students/visiting-international/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_north_carolina_at_wilmington/logo.webp', NULL), + ('DE', 154, 'EUROPE', 'FH Aachen University of Applied Sciences', 'fh_aachen_university_of_applied_sciences', '아헨응용과학대학', 'https://www.fh-aachen.de/fachbereiche/wirtschaft/internationales/incoming-students/student-support-service/finding-accomodation', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/fh_aachen_university_of_applied_sciences/1.webp', 'https://www.campus.fh-aachen.de/campus/all/groups.asp?lang=en&tguid=0xC49848E2A4294A2F8A1A833C4C2AB16B', 'https://www.fh-aachen.de/en/studies/applying/international-applicants/exchange-students', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/fh_aachen_university_of_applied_sciences/logo.webp', NULL), + ('AT', 155, 'EUROPE', 'Vorarlberg University of Applied Sciences', 'vorarlberg_university_of_applied_sciences', '포어아를베르크 응용과학대학교', 'https://www.fhv.at/en/fh/study-international/exchange-students-incomings/living-in-vorarlberg/accommodation-for-guest-students', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/vorarlberg_university_of_applied_sciences/1.webp', 'https://www.fhv.at/en/fh/study-international/exchange-students-incomings/study-related-information-for-guest-students/english-course-offer', 'https://www.fhv.at/en/fh/study-international/exchange-students-incomings', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/vorarlberg_university_of_applied_sciences/logo.webp', NULL), + ('HU', 156, 'EUROPE', 'Eotvos Lorand University', 'eotvos_lorand_university', '에어토보스로랑드대학', 'https://www.elte.hu/en/arrange-housing', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/eotvos_lorand_university/1.webp', 'https://www.elte.hu/en/incoming-mobility/courses', 'https://www.elte.hu/en/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/eotvos_lorand_university/logo.webp', NULL), + ('PT', 157, 'EUROPE', 'Lisbon School of Economics & Management', 'lisbon_school_of_economics__management', '리스본대학 경영학과', 'https://www.iseg.ulisboa.pt/aquila/unidade/ERASMUS/incoming-mobility/useful-links/accommodation', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/lisbon_school_of_economics__management/1.webp', 'https://www.iseg.ulisboa.pt/internacional/mobilidade-incoming/informacao-academica/', 'https://www.iseg.ulisboa.pt/en/study/international/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/lisbon_school_of_economics__management/logo.webp', NULL), + ('LT', 158, 'EUROPE', 'Vilnius Gediminas Technical University', 'vilnius_gediminas_technical_university', '빌니우스 게디미나스대학 공과대', 'https://vilniustech.lt/for-international-students/for-exchange-students/studies/319312?#319322', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/vilnius_gediminas_technical_university/1.webp', 'https://vilniustech.lt/files/5058/252/12/0_0/VILNIUS%20TECH%202023-2024%20spring.htm', 'https://vilniustech.lt/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/vilnius_gediminas_technical_university/logo.webp', NULL), + ('FR', 159, 'EUROPE', 'ECE Paris', 'ece_paris', 'ECE Paris', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/ece_paris/1.webp', NULL, 'https://ecole.ece.fr/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/ece_paris/logo.webp', NULL), + ('FR', 160, 'EUROPE', 'EDC Paris Business School', 'edc_paris_business_school', 'EDC 파리 경영대학', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/edc_paris_business_school/1.webp', NULL, 'https://www.edcparis.edu/fr/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/edc_paris_business_school/logo.webp', NULL), + ('ID', 161, 'ASIA', 'University of Indonesia', 'university_of_indonesia', '인도네시아 국립대학', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_indonesia/1.webp', NULL, 'https://www.ui.ac.id/en/universitas-indonesia/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_indonesia/logo.webp', NULL), + ('TH', 162, 'ASIA', 'Rangsit University', 'rangsit_university', '랑싯대학', 'https://www.rsuip.org/admissions/international-student-support/accommodation/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/rangsit_university/1.webp', 'https://www.rsuip.org/for-student/timetable/', 'https://www.rsuip.org/','https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/rangsit_university/logo.webp', NULL), + ('TH', 163, 'ASIA', 'Mae Fah Luang University', 'mae_fah_luang_university', '매파루앙 대학', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/mae_fah_luang_university/1.webp', NULL, 'https://waruneekae.wixsite.com/website/inbound-students', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/mae_fah_luang_university/logo.webp', NULL), + ('UZ', 164, 'ASIA', 'Fergana Polytechnic Institute', 'fergana_polytechnic_institute', '페르가나폴리텍대학', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/fergana_polytechnic_institute/1.webp', NULL, 'https://www.ferpi.uz/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/fergana_polytechnic_institute/logo.webp', NULL), + ('TR', 165, 'ASIA', 'Beykent University', 'beykent_university', '베이켄트대학교', 'https://www.beykent.edu.tr/en/student/life-at-beykent/accommodation/ayaza%C4%9Fa-dormitories', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beykent_university/1.webp', 'https://obs.beykent.edu.tr/oibs/bologna/index.aspx?lang=en', 'https://www.beykent.edu.tr/en/mainpage', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beykent_university/logo.webp', NULL), + ('TW', 166, 'ASIA', 'National DongHwa University', 'national_donghwa_university', '국립동화대학교', 'https://rb005.ndhu.edu.tw/p/412-1005-19158.php', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_donghwa_university/1.webp', 'https://sys.ndhu.edu.tw/aa/class/course/Default.aspx', 'https://www.ndhu.edu.tw/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_donghwa_university/logo.webp', NULL), + ('TW', 167, 'ASIA', 'National Taiwan Normal University', 'national_taiwan_normal_university', '국립대만사범대학교', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_taiwan_normal_university/1.webp', 'https://courseap2.itc.ntnu.edu.tw/acadmOpenCourse/index.jsp', 'https://en.ntnu.edu.tw/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_taiwan_normal_university/logo.webp', NULL), + ('TW', 168, 'ASIA', 'National Taiwan Univ. of Science and Technology', 'national_taiwan_univ_of_science_and_technology', '국립대만과학기술대학교', 'https://oia-r.ntust.edu.tw/p/412-1060-8919.php?Lang=en#:~:text=Europe%20(excluding%C2%A0Germany)-,Accomodation,-Due%20to%20heavy', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_taiwan_univ_of_science_and_technology/1.webp', 'https://querycourse.ntust.edu.tw/querycourse/#/', 'https://www.ntust.edu.tw/?Lang=en', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_taiwan_univ_of_science_and_technology/logo.webp', NULL), + ('TW', 169, 'ASIA', 'National Kaohsiung University of Science and Technology', 'national_kaohsiung_university_of_science_and_technology', '국립가오슝과기대학교', 'https://oia.nkust.edu.tw/en/unit-8-269-14.html', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_kaohsiung_university_of_science_and_technology/1.webp', 'https://oia.nkust.edu.tw/en/unit-8-271-5.html', 'https://eng.nkust.edu.tw/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_kaohsiung_university_of_science_and_technology/logo.webp', NULL), + ('CN', 170, 'ASIA', 'Dalian University of Technology', 'dalian_university_of_technology', '대련이공대학교', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/dalian_university_of_technology/1.webp', 'http://sie.dlut.edu.cn/zsxx/fxlxm/ ptgjjxs/xjjhs_xfs_xm.htm', 'https://en.dlut.edu.cn/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/dalian_university_of_technology/logo.webp', NULL), + ('CN', 171, 'ASIA', 'Beijing Foreign Studies University, IBS', 'beijing_foreign_studies_university_ibs', '북경외국어대학교 IBS', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beijing_foreign_studies_university_ibs/1.webp', 'https://solbridge.bfsu.edu.cn/', 'https://ibs.bfsu.edu.cn/en/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beijing_foreign_studies_university_ibs/logo.webp', NULL), + ('CN', 172, 'ASIA', 'Beijing Language and Culture University', 'beijing_language_and_culture_university', '북경어언대학교', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beijing_language_and_culture_university/1.webp', 'https://enexchange.blcu.edu.cn/col/col3931/index.html', 'http://english.blcu.edu.cn/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beijing_language_and_culture_university/logo.webp', NULL); + INSERT INTO university_info_for_apply (university_id, semester_requirement, student_capacity, semester_available_for_dispatch, tuition_fee_type, details_for_major, details_for_apply, details_for_language, details_for_english_course, details_for_accommodation, details) VALUES @@ -337,4 +364,157 @@ VALUES (146, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '''- 소속전공과 지원전공이 일치하여야 함 (교차 수강 불가능)', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어 어학강좌만 수강할 경우 HSK 또는 영어성적 제시 불필요
- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK level 4 이상이어야 함)', NULL, '- 기숙사 보유(한 학기 3100-3600RMB)', NULL), (147, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
', NULL, '- 본교 중국어 어학시험에 응시하여야 함
- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK4급이상 보유자만 가능)
-유효한 영어공인인증시험점수는 2021년 12월 21일 이후 응시한 시험점수에 한함', NULL, '-기숙사 보유 (2인실/ 매달 450-600RMB)', NULL), (148, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어 연수 프로그램 : 어학성적 불필요
- 영어강의 없고 중국어로 하는 전공수업만 제공함. HSK5급 보유자만 지원가능(확인중)', NULL, '- 기숙사 보유 (한학기 1100-1500 USD) ', NULL), - (149, NULL, 10, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '- 어학요건이 대학별로 상이하므로 반드시 Program Guide 참고할 것', NULL, NULL, '- 지원 전 반드시 국제교류팀 담당자와 상담할 것'); \ No newline at end of file + (149, NULL, 10, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '- 어학요건이 대학별로 상이하므로 반드시 Program Guide 참고할 것', NULL, NULL, '- 지원 전 반드시 국제교류팀 담당자와 상담할 것'); + +INSERT INTO university_info_for_apply ( + id, + university_id, + semester_requirement, + student_capacity, + semester_available_for_dispatch, + tuition_fee_type, + details_for_major, + details_for_apply, + details_for_language, + details_for_english_course, + details_for_accommodation, + details, + term, + gpa_requirement, + gpa_requirement_criteria +) VALUES + (150, 1, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', NULL, NULL, NULL, NULL, NULL, '2024-2', 2.5, 4), + (151, 2, 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', NULL, NULL, NULL, NULL, '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend', '2024-2', 2.5, 4), + (152, 4, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 6.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능, Fact Sheet 참고)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $2,700~5,900
- International Program and Service Fees : $2,500', '2024-2', 3, 4), + (153, 3, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능, Fact Sheet 참고)
- 한 학기 등록금: 약 $7,500
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $2,700~5,900
- International Program and Service Fees : $2,500', '2024-2', 2.5, 4), + (154, 5, 2, 5, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함', NULL, NULL, NULL, NULL, '※ On Campus Room and Board 신청 필수! (기숙사 미신청 시 해외대학등록금납부형(B형)으로 전환)
- 기숙사 관련 정보: https://www.unk.edu/offices/reslife/housing-options.php
- 보험 관련 정보: https://www.internationalstudentinsurance.com/student-health-insurance/?gad=1&gclid=Cj0KCQjw9fqnBhDSARIsAHlcQYQO_d0Rmq607FC8cauQ-e_vyEKWNw4DnXsYpgm_nnjrFmx-BJLBBwUaAg9OEALw_wcB', '2024-2', 2.5, 4), + (155, 6, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함', '※ ELI(어학연수) 과정으로 지원시 영어 성적 무관 (https://www.unk.edu/international/english-language-institute/index.php)', NULL, NULL, NULL, '※ ELI 어학연수 과정으로 지원시, 전공/ESL 크레딧은 자체배치고사 점수에 따라 상이
- 등록금 관련 정보: https://www.unk.edu/costs.php
- 기숙사 관련 정보: https://www.unk.edu/offices/reslife/housing-options.php
- 보험 관련 정보: https://www.internationalstudentinsurance.com/student-health-insurance/?gad=1&gclid=Cj0KCQjw9fqnBhDSARIsAHlcQYQO_d0Rmq607FC8cauQ-e_vyEKWNw4DnXsYpgm_nnjrFmx-BJLBBwUaAg9OEALw_wcB', '2024-2', 2.5, 4), + (156, 152, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요
- 타전공 과목 수강 시, 대부분의 Advanced Classes는 Pre-requisite을 충족하여야 수강 가능', NULL, NULL, NULL, NULL, '- 한 학기 기숙사 비용: 약 $3,813~4,808', '2024-2', 2, 4), + (157, 153, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요
- 타전공 과목 수강 시, 대부분의 Advanced Classes는 Pre-requisite을 충족하여야 수강 가능', NULL, NULL, NULL, NULL, '- 한 학기 등록금: 약 $914.70/per credit (2023/24 rate)
- 한 학기 기숙사 비용: 약 $3,813~4,808', '2024-2', 2, 4), + (158, 7, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Nursing, Athletic training, Education, School of Professional Studies Programs', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 모든 영역에서 15점 이상
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, '- 한 학기 기숙사 비용: 약 $3,900~$5,500', '2024-2', 2.75, 4), + (159, 8, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Nursing, Athletic training, Education, School of Professional Studies Programs', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 모든 영역에서 15점 이상
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, '- 한 학기 등록금: 약 $6,938 (In-state, 2023/24 기준)
- 한 학기 기숙사 비용: 약 $3,900~$5,500', '2024-2', 2.75, 4), + (160, 9, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 쓰기 영역에서 5.0 이상', NULL, NULL, '- 교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음
- College Fee : 약 $1,070', '2024-2', 3, 4), + (161, 10, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 쓰기 영역에서 5.0 이상', NULL, NULL, '- 한 학기 등록금: 약 $9,450 (out of state, 2023/24 기준)
- 교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음
- College Fee : 약 $1,070', '2024-2', 3, 4), + (162, 11, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원 불가 전공 : Health Sciences, Pharmacology, Nursing, 일부 WRT/CSE 과목
- 수강 제한 전공 : Theatre Arts, Dance, 일부 Languages 과목
- ACC/BUS, CSE/ISE 전공 학생은 최대 2과목 까지만 전공 내에서 수강이 가능하며, 나머지 학점은 다른 전공에서 수강
- 학과에 지원 전제조건이 있을 경우 충족해야 함', NULL, NULL, NULL, NULL, '- 한 학기 기숙사 비용: 약 $4,500~6,200
- 한 학기 등록금: 약 $13,430 (out of state)
- 등록금 및 기타 Fee Rates 관련 정보: https://www.stonybrook.edu/commcms/sfs/tuition/index.php', '2024-2', 2.8, 4), + (163, 12, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Nursing', NULL, NULL, NULL, NULL, NULL, '2024-2', 2.5, 4), + (164, 13, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Nursing', NULL, NULL, NULL, NULL, '한 학기 등록금: 약 $6,892 (In-state, 2023/24 기준)', '2024-2', 2.5, 4), + (165, 16, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 제한 전공: Athletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, Nursing', NULL, NULL, NULL, NULL, '- 모든 국제학생들의 안전과 영어향상을 위해 기숙사 사용을 강제하는 International Studies Policy (기숙사 신청은 선착순이므로 입학 허가서 수령 직후 기숙사를 신청해야 배정 받을 수 있음)', '2024-2', 2.5, 4), + (166, 17, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 제한 전공: Athletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, Nursing', NULL, NULL, NULL, NULL, '- 등록금은 In-state rate 적용
- 모든 국제학생들의 안전과 영어향상을 위해 기숙사 사용을 강제하는 International Studies Policy (기숙사 신청은 선착순이므로 입학 허가서 수령 직후 기숙사를 신청해야 배정 받을 수 있음)', '2024-2', 2.5, 4), + (167, 20, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능 (단, 타전공 지원시 각 학과의 사전허가 필요)
- 선이수과목이 비숫해야 후에 IIT에서 전공과목을 수강할 수 있음', ' - SAT시험 면제 조건으로 동일계 학과에서 최소 30학점 이수하여야 하며, 입학사정시 전공과목 및 영어과목 위주로 검토 됨', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상', NULL, '- 식비(Meal Plan) 정보
https://www.iit.edu/housing/dining-and-meal-plan/options-and-rates
- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid

- 학비관련 site
https://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate
- 한 학기 등록금: 약 $14,820 (방문학생 학비장학금 $10,000/학기 차감한 금액, 징학금은 12크레딧 이상 full time 등록 시에만 지급 가능, 2023/24 rate)
- 보험료 site
https://www.iit.edu/shwc/insurance/plan-info-and-requirements
- 세부사항 변동 가능', '2024-2', 3.4, 4.5), + (168, 21, 4, 5, 'FOUR_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 정확하게 일치해야함
- 선이수과목이 비숫해야 후에 IIT에서 전공과목을 수강할 수 있음', ' - SAT시험 면제 조건으로 동일계 학과에서 최소 30학점 이수하여야 하며, 입학사정시 전공과목 및 영어과목 위주로 검토 됨
- 2024-1학기에 4차 학기 이수 예정인 학생도 조건부 지원 가능
(반드시 정규학기 이수하여야 2024-2 파견 가능)', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상', NULL, '- 식비(Meal Plan) 정보
https://www.iit.edu/housing/dining-and-meal-plan/options-and-rates
- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid
- 학비관련 site
https://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate
- 한 학기 등록금: 약 $24,820, 복수학위 학비장학금 연간 1.5만~3만 달러 지급 가능 (징학금은 12크레딧 이상 full time 등록 시에만 지급 가능, 2023/24 rate)
- 보험료 site
https://www.iit.edu/shwc/insurance/plan-info-and-requirements
- 세부사항 변동 가능', '2024-2', 3.4, 4.5), + (169, 22, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, NULL, '※ 테일러대학은 기독교 정신을 기반으로 설립된 학교로 대다수의 학생들이 기독교를 믿고 있습니다. 이러한 테일러대학의 특성을 고려하여, 성실하고 정기적인 교회 생활을 이어가고 있는 학생들에 한해서 지원을 받고 있습니다. 지원 과정에서 목사추천서 등의 자료를 필수적으로 제출하여야 하며, 필요 시 간단한 면접을 진행할 수 있습니다.
- 원칙은 1학기 지원이나, 2학기도 학생이 원하면 지원 가능.
다만, 파견대학에서 학생의 교환학생 성과를 평가해 이에 미치지 못할 경우 2학기를 이어 진행하지 못하고 한 학기만 진행할 수 있으니 해당 사항 유의하기 바람. 이에 2학기를 지원하는 학생의 경우, 파견 지원 전 반드시 지역담당자에게 사전에 연락해 관련 내용에 대해 논의하고 지원하길 바람. ', '2024-2', 3, 4), + (170, 150, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 다음 전공들은 지원 가능하나 수강신청이 제한적일 수 있음 Architecture, Computer & Information Sciences, Education, Performing Arts (Dance, Music, Theater), Professional Schools (Dentistry, Law, Medicine, Pharmacy, Podiatry), Visual Arts (Film/Media Arts, Graphic Design, Fine Arts, etc), Sport, Tourism and Hospitality Management
- Business 전공의 경우, 본교 경영학과 학생만 지원가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC : 모든 영역에서 390점 이상
- 최저 기준 어학 점수가 넘더라도, 선발대학에서 특정 섹션의 어학실력이 부족하다고 판단될 경우 파견 시 별도로 대학부설 어학코스(ielp) 수강이 필요할 수 있음 (수업료 외 별도 비용 발생, https://tcalc.temple.edu/)', NULL, NULL, '- 한 학기 기숙사 비용: 약 $5,800 (기숙사 유형에 따라 상이)', '2024-2', 3, 4), + (171, 23, 2, 5, 'IRRELEVANT', 'MIXED_PAYMENT', '- 타전공 지원 및 수강 가능
- 다음 전공들은 지원 가능하나 수강신청이 제한적일 수 있음 Architecture, Computer & Information Sciences, Education, Performing Arts (Dance, Music, Theater), Professional Schools (Dentistry, Law, Medicine, Pharmacy, Podiatry), Visual Arts (Film/Media Arts, Graphic Design, Fine Arts, etc), Sport, Tourism and Hospitality Management
- Business 전공의 경우, 본교 경영학과 학생만 지원가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC : 모든 영역에서 390점 이상
- 최저 기준 어학 점수가 넘더라도, 선발대학에서 특정 섹션의 어학실력이 부족하다고 판단될 경우 파견 시 별도로 대학부설 어학코스(ielp) 수강이 필요할 수 있음 (수업료 외 별도 비용 발생, https://tcalc.temple.edu/)', NULL, NULL, '※ 1개 학기로도 지원 가능 (해외대학등록금납부형 적용)
※ 혼합형은 첫 번째 학기는 템플대학교에 등록금 지불, 두 번째 학기는 인하대에 등록금 지불하는 유형 (첫 번째 학기를 모두 마친 경우에만 두 번쨰 학기에 템플대학교 등록금 면제 및 인하대에 등록금 지불 적용 가능)
- 한 학기 등록금: 약 $16,188 (out of rate, 2023/24 기준, https://globalprograms.temple.edu/programs/inbound-study-abroad-exchange/costs-dates)
- 한 학기 기숙사 비용: 약 $5,800 (기숙사 유형에 따라 상이)', '2024-2', 3, 4), + (172, 24, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원제힌전공: Nursing', NULL, NULL, NULL, NULL, NULL, '2024-2', 2.5, 4), + (173, 25, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원제힌전공: Nursing', NULL, NULL, NULL, NULL, '한 학기 등록금: 약 $9,792 (50% tuition scholarship, 2023/24 기준)', '2024-2', 2.5, 4), + (174, 151, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', ' - 최소 2개 학기, 24 transferable semester credits 이상 이수한 학생에 한하여 지원 가능. 이 때, English language/composition, religious doctrine, remedial/technical, field studies, internships 등 일부 수업은 인정되지 않을 수 있음
(관련 정보 : https://manoa.hawaii.edu/mix/inbound/official-transcript-requirements/)', ' - 토플 IBT(100), 토플 ITP(600), IELTS(7.0) 미만시 파견 후 별도의 영어시험을 응시해야하며, 결과에 따라 1~3개의 어학 수업을 수강하게 될 수 있음', NULL, NULL, '※ 하외이대학 지원 마감일정 상 3월 15일(금)까지 서류 제출 필요 (추후 합격자에 한하여 사전 안내 예정)', '2024-2', 2.5, 4), + (175, 26, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', ' - 최소 2개 학기, 24 transferable semester credits 이상 이수한 학생에 한하여 지원 가능. 이 때, English language/composition, religious doctrine, remedial/technical, field studies, internships 등 일부 수업은 인정되지 않을 수 있음
(관련 정보 : https://manoa.hawaii.edu/mix/inbound/official-transcript-requirements/)', ' - 토플 IBT(100), 토플 ITP(600), IELTS(7.0) 미만시 파견 후 별도의 영어시험을 응시해야하며, 결과에 따라 1~3개의 어학 수업을 수강하게 될 수 있음', NULL, NULL, '※ 하외이대학 지원 마감일정 상 3월 15일(금)까지 서류 제출 필요 (추후 합격자에 한하여 사전 안내 예정)
- 한 학기 등록금: 약 $8,478 (Hoakipa Visiting Student 유형으로Resident Tuition의 150%)', '2024-2', 2.5, 4), + (176, 27, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', '모든 강의는 포르투갈어로 진행하며 영어강의 미제공, 포르투갈어 가능자만 지원 권장', NULL, '※ 영어강의 제공하지 않음, 모든 강의 포르투갈어로 진행', NULL, '- 교내 기숙사 미제공, International Affairs와 버디프로그램을 통해 교외숙소 계약을 도와줄 예정', '2024-2', NULL, NULL), + (177, 29, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 아래 8개 Faculties 내에서만 수강 가능 :
Arts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science
- 지원 불가 전공: Nursing, Social work', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상 ', NULL, NULL, NULL, '2024-2', 2.5, 4), + (178, 30, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 아래 8개 Faculties 내에서만 수강 가능 :
Arts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science
- 지원 불가 전공: Nursing, Social work', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상 ', NULL, NULL, '한 학기 등록금 : International Student Fee 적용, 지원 전공 및 학점에 따라 금액 상이 (https://www.uregina.ca/fs/students/fee-schedule.html)', '2024-2', 2.5, 4), + (179, 31, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 아래 6개의 Faculties 내에서 수강 가능 :
Business Administration, Education, Engineering and Applied Sciences, Human Kinetics and Recreation, Humanities and Social Sciences, Science
- 지원 불가 전공: Medicine, Pharmacy, Social work, Nursing
- 지원 제한 전공: Music, Computer Science', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상', NULL, NULL, NULL, '2024-2', 2.5, 4), + (180, 32, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 아래 6개의 Faculties 내에서 수강 가능 :
Business Administration, Education, Engineering and Applied Sciences, Human Kinetics and Recreation, Humanities and Social Sciences, Science
- 지원 불가 전공: Medicine, Pharmacy, Social work, Nursing
- 지원 제한 전공: Music, Computer Science', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상', NULL, NULL, 'International Students Tuition Fee 적용 (약 $20,790, 2023/24 기준)', '2024-2', 2.5, 4), + (181, 33, 2, 3, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '※ 가장 기초 과정(IEP-G)의 최소 지원 요건이며 레벨과 지원과정에 따라 지원자격이 상이하므로 fact sheet 참조 바람
- 학부과정 수강 가능한 IEBP-G 지원 시 다음의 세부영역 점수를 만족해야함
- IELTS : 모든 영역에서 5.0 이상, 쓰기 5.5 이상
- TOEFL iBT : 쓰기 16점 이상', NULL, NULL, '- 선발 학생의 어학성적에 따라 레벨이 정해지며, 레벨에 따라 등록금 상이
- IEBP-G에 배정될 경우, 학부 수업 1~2개 수강 가능하며 (선택 제한적) 학부수업에 대한 등록금은 면제 (국제처 홈페이지 내 대학 Fact Sheet 참고)', '2024-2', 2.5, 4), + (182, 34, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원 유의 전공: Interior Design, Architecture Design
- Media and Communication, Design, Fashion 전공은 본교에서 동일한 전공을 이수 중인 학생만 지원 가능
(참고 : https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange/how-to-search-for-your-courses)', NULL, NULL, NULL, NULL, NULL, '2024-2', 2, 4), + (183, 35, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- Business, Engineering, Law, IT, Biology 학과 수강 가능 (그 외의 학과는 Trimester 1,2에 지원 권고)
- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 지원 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 모든 영역에서 5.5 이상
- TOEFL: 모든 영역에서 19점 이상', NULL, NULL, '- 서던퀸스랜드대학은 3학기제로 운영되며, 본교 가을학기 파견교환학생은 Trimester 3로 수학', '2024-2', 2.5, 4), + (184, 36, 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '- Business, Engineering, Law, IT, Biology 학과 수강 가능 (그 외의 학과는 Trimester 1,2에 지원 권고)
- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 지원 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 모든 영역에서 5.5 이상
- TOEFL: 모든 영역에서 19점 이상', NULL, NULL, '- 서던퀸스랜드대학은 3학기제로 운영되며, 본교 가을학기 파견교환학생은 Trimester 3로 수학
- 한 학기 등록금: AU$2,375 per course (In-state, 2023/24 기준)', '2024-2', 2.5, 4), + (185, 38, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상', NULL, NULL, NULL, '2024-2', 3, 4), + (186, 39, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 19점 이상', NULL, NULL, '한 학기 등록금: 약 AU$10,400 (In-state)', '2024-2', 3, 4), + (187, 46, 2, 5, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점
- TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점', NULL, NULL, NULL, '2024-2', NULL, NULL), + (188, 47, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 일반적으로 Business, Computer Sciences, Engineering, Tourism Field에서 영어강의를 교환학생에게 제공함 ', NULL, '독일어 공인성적으로 지원할 경우, B1 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- 교환 학생 프로그램에 독일어 어학 수업 포함
- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), + (189, 48, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC: 읽기 385점 이상, 듣기 400점 이상
- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, NULL, '2024-2', NULL, NULL), + (190, 49, 2, 6, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요', '각 단과대학별 상이하므로 국제처 홈페이지 해외대학정보 Fact sheet 및 홈페이지 참조 바람', NULL, NULL, '2024-2', NULL, NULL), + (191, 50, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙 필요', NULL, NULL, NULL, NULL, '- 보험 관련 정보: https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/health-insurance', '2024-2', NULL, NULL), + (192, 51, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- International Study Programme(ISP) 내 수업만 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- Public German Health insurance 가입 의무', '2024-2', 2.5, 5), + (193, 52, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)
- 지원 불가능 전공: Pharmacy, Human Medicine and Veterinary Medicine
- Biochemistry, Bioinformatics and Biology, Law 지원 제한적 (학과 사전승인 필요)
- Business Administration, Economics 학과 수업 대부분이 독일어로 진행, 독일어 가능자 지원 권장', NULL, '- Department of Humanities, Social Science, Business Administration and Economics 수강요건: 독일어 공인성적 B2 레벨 이상의 증빙 필수
- Department of Natural Science 수강요건: 독일어 공인성적 B1 레벨 이상 증빙 필수
- John F Kennedy Institute for North American Studies 수강요건: 영어 공인성적 C1 레벨 이상의 증빙 필수', '※ 주로 Departments of English and North American Studies에서 영어강의 제공, 이 외의 학과 영어수업 제한적', NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- 보험 관련 정보: http://www.fu-berlin.de/en/studium/international/studium_fu/einreise_aufenthalt/krankenversicherung', '2024-2', NULL, NULL), + (194, 53, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)', '※ Faculty of Economics, Business Administration에 한하여 지원 가능', '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- 보험 관련 정보: https://international.fhws.de/en/fhws-international/ways-to-fhws/applicants-and-student-support/before-your-arrival-at-fhws/ ', '2024-2', 2.5, 5), + (195, 54, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 아래 링크에 안내된 강의 내에서 자유롭게 수강 가능 (https://www.hs-schmalkalden.de/en/international/incoming-students/courses-for-incomings/exchange-students)', NULL, NULL, NULL, NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- Public German Health insurance 가입 의무 (관련 정보: https://feather-insurance.com/en/public-health-insurance/barmer?utm_source=barmer_schmalkalden)', '2024-2', NULL, NULL), + (196, 55, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 제한 전공: International Project Management, Smart City Solution
- Civil Engineering, Surveying, Mathematics : 독일어 B1 이상 가능자 지원 권장
- Architecture, Interior Architecture and General Management : 독일어 A1 이상 가능자 지원 권장, 독일어 수업 수강 필수
- 주로 Architecture, Interior Architecture, Business Management, General Management, Business School에서 영어 강의 제공', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 모든 영역에서 6.0 이상', NULL, NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), + (197, 56, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Health, Midwifery, Extra-occupational courses ', NULL, NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), + (198, 57, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)', NULL, NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), + (199, 154, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- Architecture, Civil Engineering 학과의 경우 독일어 B1 이상 증빙 필수', NULL, NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), + (200, 58, 3, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '- 독일어 공인성적으로 지원할 경우, B1 레벨에 준하는 성적을 보유하여야 함
- 영어스피킹 중급 이상 학생 지원 권장', NULL, NULL, '- 독일 어학 수업 수강 필수
- Public German Health insurance 가입 의무', '2024-2', 2, 4), + (201, 59, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함
- 지원 불가능 전공: Human medicine, Dentistry, Pharmacy, Law, Psychology and Practical Sports', NULL, NULL, NULL, NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), + (202, 60, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: Medicine', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- Public German Health insurance 가입 의무', '2024-2', 3, 5), + (203, 61, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함
- 지원 불가 전공: Medicine, Programmes offered by the Centre for Lifelong Learning', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC S/W : 말하기 120점 이상, 쓰기 120점 이상
- 독일어 공인성적으로 지원할 경우, B1 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- 보헙관련 정보: https://uol.de/en/exchange-studies/health-insurance', '2024-2', NULL, NULL), + (204, 62, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), + (205, 63, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 제한 전공 : Psychology', NULL, NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), + (206, 64, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, NULL, NULL, NULL, '- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), + (207, 65, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- Campus Schwäbisch Hall 내 강의 수강 가능하며, Campus Künzelsau에서도 일부 수강 가능', '※ Faculty of Business Administration, Global Finance and Banking, Economics에 한하여 지원 가능', NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), + (208, 82, 1, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고(단, 건축학부로의 지원은 건축학부 전공자만 가능)
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능-
- 학기당 최소 수강학점 : 15 ECTS credits', NULL, '어학성적 유효기간 내 인정', NULL, '- 교환학생 합격자 대상 기숙사 신청 방법 공지 예정
- 기숙사 이용 비용 : 월 300$ 수준', '- 보험 가입 의무 : www.vzp.cz', '2024-2', 1.5, 4), + (209, 81, 3, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능(우측 영어강의리스트 참고 필요)
- 학기당 최소 수강학점 : 최소 5, 최대 30 ECTS credits', NULL, NULL, NULL, '- 기숙사 제공 가능
- 기숙사 이용 비용 : 월 약 7,500 CZK', NULL, '2024-2', NULL, NULL), + (210, 80, NULL, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
※ Faculty of Fine Arts and Music으로 교환학생 지원 시 전공 일치 필요
※ Faculty of Medicine : 교환 학생 지원 불가
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 최소 수강학점 : 20 ECTS credits
※ 일반적 학기당 수강 학점 : 30 ECTS credits
- 수강 학점의 최소 55%는 1개 전공 내에서 수강해야 함.', NULL, '- 유효기간 경과한 어학성적도 인정
- 기타 CEFR English B2+ level에 상응하는 어학성적 또한 인정', NULL, NULL, '- 기숙사 : 해외 교환학생 우선 배정 정책
- 보험 가입 의무 : www.vzp.cz(대학 차원에서의 의무화는 아니나, 국가 관련 법령에 의해 해외 교환학생들은 상해보험에 가입하게 되어 있음.)', '2024-2', 2.5, 4), + (211, 45, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 15, 최대 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정
- 기타 인정 어학성적 : Cambridge Certificate of Advanced English (CAE) or Cambridge Certificate of Proficiency in English (CPE) : PASSED ', NULL, '- 숙소 제공 보장(Guaranteed)/Off-campus Housing', '- COVID-19 관련 : https://en.coronasmitte.dk/', '2024-2', NULL, NULL), + (212, 44, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- ''주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고)
- 수강과목들은 2개 학부 이내에서 수강할 것을 강력히 권장
- 기초 선수과목 적용 교과목들은 본교 사전 이수 필요
- 학기당 수강학점
· 기본(최소) 30 ECTS credits
· 최대 35 ECTS credits : 기본 30ECTS에 덴마크어 학습 강좌(5ECTS)만 추가 가능', '- 최저 2학기 이수 의미 : 60 ECTS credits 이수에 상응', '- 어학성적 : 기재된 성적은 교환 프로그램 참여를 위한 어학 능력 정도를 의미, 제출 필수 아님.
제출 시 선발에 플러스 요인으로 작용할 수 있음.

- Language Requirement Form : 제출 필수(인하대 국제교류담당자 서명 포함)
※ fact sheet 참조 : www.sdu.dk/sduinternational
', NULL, '- 기한 내 신청 시 숙소 제공 보장(Guaranteed)/Off-campus Housing', '- 기본 생활비 소요
· 숙소비 : € 350~450/월
· 식비 : € 350~400/월
· 대중교통 이용료 : € 50/월
· 기타(교재 구입 등) : € 100~200
- 덴마크로 오기 전 수학기간 전체를 커버하는 보험구입 권장(덴마크에서도 보험 가입 가능하나, 보장 내역 등이 덴마크어로만 제공가능할 수 있음.) ', '2024-2', NULL, NULL), + (213, 73, 2, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공과) 유관 학과로 지원 필요
※ ''Translation/Interpretation/Transcultural communication with German''(전공명)으로의 지원자는 확인 가능한 인하대 관련 교과과정 이수 내역 제출 필요

- 교과목 수강 관련
· 학부 교환학생 : 여러 전공에 걸쳐 수강 가능
· 석/박사 교환학생 : 본인 소속 전공에 한정하여 전공 지원 가능
- 학기당 수강학점 : 최소 24, 최대 30 ECTS credits 이수 권장(실제 이수 기준은 소속 대학(인하대) 기준을 따름.)', NULL, '- 독일어 진행 수업/영어 진행 수업을 들을지에 따라 최소 독일어 B2 수준, 영어 B2 수준의 어학 실력 권고
- 어학성적 유효기간 내 인정', NULL, '- 숙소 제공 미보장(unguaranteed) - 이용 숙소가 대학 소유가 아님.
- 도시 내 이용 가능한 다양한 숙소들이 있으며, 안내된 사이트를 통하여 ASAP 개별적으로 예약 진행 필요', '- 연구실 배정(Lab placements)는 보장되지 않음.
- 보험 가입 : 2개 학기 교환학생에 한하여 의무(비용은 약 € 65)', '2024-2', NULL, NULL), + (214, 74, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공과) 유관 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 16 ECTS credits', NULL, '- 어학성적 유효기간 내 인정(2년 이내 발급본)
- TOEIC으로 어학성적 제출 시, 기본 L&R(듣기, 읽기) 최소 785점, S&W(말하기, 쓰기) 최소 310점 모두 제출 필요', NULL, NULL, '- 대학 자체 기숙사는 없으며, 사이트에 소개된 숙소 개별적 신청/이용. (교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있어 ASAP 신청 필요)
- 더블룸 기준 한달에 약 € 350 (숙소별 차이 有)
- 보험 가입 : 2개 학기 교환학생에 한하여 의무(Austrian National Health Insurance (ÖGK))', '2024-2', NULL, NULL), + (215, 76, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공과) 유관 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 15, 최대 30 ECTS credits', '※ 기타 어학자격 최저 요건 하기 링크 참조
https://media-hp.technikum-wien.at/media/20230309080942/Proof-of-English-Language-Ability.pdf ', '- 유효기간 경과한 어학성적도 인정', NULL, '- 대학 자체 기숙사 없음', '- 보험가입 의무 없음.', '2024-2', NULL, NULL), + (216, 155, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 유관 학과로 지원 필요
- 신청한 전공 내에서 과목 수강 진행 필요
- 학기당 수강학점 : 최소 18 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 교환학생 합격자는 아래 링크를 통해 개별 이용 신청 가능(선착순 배정)', '- 보험가입 의무 없음.', '2024-2', NULL, NULL), + (217, 111, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 신청한 전공 내에서 과목 수강 진행 필요
- 학기당 수강학점 : 최소 15, 최대 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 교환학생 합격자는 링크를 통해 개별 이용 신청', '- 보험가입 의무 없음.', '2024-2', NULL, NULL), + (218, 109, NULL, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 지원전공 내 과목 수강 권고. 타 전공 분야 수강 가능(수강 자격 충족 시)
- 학기당 수강학점 : 30 ECTS credits 이수 권고', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 교환학생 합격자는 링크를 통해 개별 이용 신청', '- 출국 전 자체적 교환 기간 전체를 보장하는 보험 가입 권고', '2024-2', NULL, NULL), + (219, 110, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', ' - 소속전공과 지원전공이 일치해야 함.
- 경영학, 공학 계열 학부생만 지원 가능
- 합격 후 과목 수강은 교환학생 전용 강좌에 한하여 수강신청 가능
- 학기당 수강학점 : 최소 20 ECTS credits', '모집 인원(3명) 구분 : 학부 경영학 계열 2명, 공학 계열 1명', '- 유효기간 경과한 어학성적도 인정', NULL, '- 기본 미제공
- 교환학생의 경우 LOAS를 통해 숙소를 신청 가능', '- 보험 관련 : https://migri.fi/en/insurance ', '2024-2', NULL, NULL), + (220, 79, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 3 ECTS credits', NULL, 'https://www.unive.it/pag/40609/', NULL, '- 기본 미제공(unguaranteed)
- 숙소 관련 지원 : Ca’ Foscari University Housing Office', '- 보험가입 의무
- 보험 관련 : https://www.unive.it/pag/12525/?MP=12525-12518', '2024-2', NULL, NULL), + (221, 77, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', ' - 공학 계열 내 유사 전공으로 지원 가능
- 공학 계열 학생만 지원 가능
- Architecture and Design 학과 과목 수강신청 불가
- 학기당 수강학점 : 최대 40 ECTS credits
- 관련 사이트 : https://www.polimi.it/en/exchange-students-incoming/after-acceptance/exchange-programmes/before-arrival', '- 모집인원 (3명) 구분 : 공학 계열 3명
- 지원 전 권역 담당자와 사전상담요망. 주로 학부보다 석사과정에 영어교과목이 개설된 편', NULL, '- Bachelor : https://www.polimi.it/en/programmes/laurea-equivalent-to-bachelor-of-science
- Master of science : https://www.polimi.it/en/programmes/laurea-magistrale-equivalent-to-master-of-science', '- 기본 미제공(unguaranteed)
- 숙소 정보 및 개별 신청 : https://www.residenze.polimi.it/en', '양교 교류협약에 따라, Bovisa 캠퍼스로만 지원가능', '2024-2', NULL, NULL), + (222, 78, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 12 ECTS credits', '- 지원 전 권역 담당자와 사전상담 요망. 기존 파견자 없어서 후기 자료 없음.', '- 어학성적 유효기간 내 인정(2년 이내 발급본)
- CEFR English B1에 상응하는 어학성적
- 또는 CEFR Italian A1', NULL, '- Single Room : € 350유로/월 수준
- Shared Double : € 290/월 수준', '- 보험가입 의무 : 수학기간 전체를 포함하는 자체 사보험 가입 필요', '2024-2', NULL, NULL), + (223, 156, 1, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 신청한 전공 내에서 대부분 과목 수강 진행 권고 / 타전공 교과는 2개 내로 수강 허용
- 학기당 수강학점 : 최소 10 ECTS credits', '- 최저 이수학기 기타 세부사항
· The Faculty of Law : (학부) 최소 4개 학기 이수, (석사) 최소 1개 학기 이수
· Biology, Chemistry and Environmental Sciences(fields at the Faculty of Science at ELTE)으로는 석, 박사과정생만 지원 가능
· 그 외 학과 최저 이수학기 : 1개 학기

- 대학 학사일정 : https://www.elte.hu/en/academic-calendar ', '- 어학성적 유효기간 내 인정(2년 이내 발급본)
- CEFR English B2에 상응하는 어학성적', NULL, '- 주로 대학 기숙사 이용
- 숙소 관련 지원 : Housing Office(housing@elte.hu)
- 기숙사(2인실) 이용료 : 월 약 HUF 70,000', '- 보험가입 의무 : 대학에서 소개하는 보험 가입 필수', '2024-2', NULL, NULL), + (224, 67, 4, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- Fashion Design or Textile Design 전공 과목 수강 불허
- 학기당 수강학점 : 30 ECTS credits 이수 필요(residency permit에 연동되는 기준)', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 기본 미제공(unguaranteed)', NULL, '2024-2', NULL, NULL), + (225, 66, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고(자격 요건 Admission requirement 확인 필요)
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 30 ECTS credits 이수 필요(residency permit에 연동되는 기준)
- 우측 열 영어강의 리스트 상의 과목 중 일부는 교환학생 수강 가능여부 변동 가능. 노미네이션 완료 후 말뫼 대학에서 최종 과목 리스트 송부 예정', NULL, NULL, NULL, '- 기본 미제공(unguaranteed)', '- 말뫼대학에서 수학하는 학생들은 대학 보험의 적용을 받음.
※ 관련 정보 : https://www.kammarkollegiet.se/engelska/start/all-services/insurance/insurance-for-students-and-foreign-visitors/insurance-for-exchange-students-in-sweden-student-in', '2024-2', NULL, NULL), + (226, 157, NULL, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학부 학생은 대학원 과정 수업 수강 불가
- 학기당 수강학점 : 최소 18 / 최대 36 ECTS credits', NULL, '- 어학성적 유효기간 내 인정
- 기타 유효 어학성적 종류
· Duolingo English Test : 110
· Cambridge B2 First : 170', NULL, '- 기본 미제공(unguaranteed)', NULL, '2024-2', NULL, NULL), + (227, 83, 5, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최대 42 ECTS credits
- 기타 세부사항 Factsheet 참조', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 대학 기숙사 신청 가능하나 신청 경쟁률 높으며, 선착순 배정', NULL, '2024-2', NULL, NULL), + (228, 70, NULL, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 12 ECTS credits', '영어 진행 과목이 다양하지 않으므로 신중한 지원 요망. 스페인어 공인어학성적(DELE 중급이상 성적)이 있을 시 추후 합격 후 담당자에게 제출 권장', '- 어학성적 유효기간 내 인정', NULL, '- 대학 기숙사 신청 가능하나 신청 경쟁률 높으며, 선착순 배정, 대학 내 기숙사 외 다른 숙소 옵션 선택 가능', NULL, '2024-2', NULL, NULL), + (229, 42, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 유관 학과로 지원 필요
- 정보통신공학 계열(Information and Communication Technology) 과목만 수강 가능
- 학기당 수강학점 : 최대 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 대학 기숙사 신청 가능하나 신청 경쟁률 높으며, 선착순 배정', '- 보험가입 의무', '2024-2', NULL, NULL), + (230, 40, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 유관 학과로 지원 필요
- 교환학생 전용 교과목만 수강 가능
- 학기당 수강학점 : 최대 40 ECTS credits', NULL, '- 어학성적 유효기간 내 인정(2년 이내 발급본)
- 세부내용 링크 참조 : https://www.rug.nl/feb/education/exchange/incoming/before/english-proficiency', NULL, '- 기본 미제공(unguaranteed)', '- 보험가입 의무 : 약 월 €100-120 수준', '2024-2', NULL, NULL), + (231, 41, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 전공별 지원 자격 상이(관련 사이트 통하여 전공별 지원 자격 확인 필요)
- 학기당 수강학점 기준 : 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 기본 미제공(unguaranteed)', '- 보험가입 의무 : 약 €340 수준 (보험료 변동 가능)', '2024-2', NULL, NULL), + (232, 158, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학부-대학원 과목 교차 수강 가능
- 학기당 수강학점 : 최소 20/최대 35 ECTS credits', '최저 이수학기 이수 내역을 증명하는 성적증명서(transcript) 업로드 필요', '- 어학성적 유효기간 내 인정', NULL, '- 대학 기숙사 이용 신청 가능
- 합격자에 한하여 신청 관련 정보 발송
- 기숙사비 €200-240/월 수준', '- 보험가입 의무 : 수학기간 전체를 커버하는 자체 사보험 가입 필요', '2024-2', NULL, NULL), + (233, 43, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- Business 계열 과목 이수 가능
- 학기당 수강학점 : 최대 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 숙소 제공 보장(Guaranteed)', '- 보험가입 의무 : 수학기간 전체를 커버하는 자체 사보험 가입 필요', '2024-2', NULL, NULL), + (234, 92, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영학계열
- 학기당 최소 15ECTS, 최대 35ECTS 수강해야 함
(Program에 따라 상이함)', '- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월15일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (235, 90, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 기계공학, 전기공학, 전자공학 전공 학생만 지원할 수 있음
- 교차수강 불가능
- 최대 30ECTS 수강', '- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월15일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (236, 91, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 필요 없음
- 교차수강 가능
- 최소 15ECTS, 최대 33ECTS까지 수강', '- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월30일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공
- 외부숙소 배정에 대해 지원', NULL, '2024-2', 2.5, 4), + (237, 96, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차수강 불가능
- 최대 30ECTS 수강', NULL, NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (238, 101, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: IAE School of Management
- 소속전공과 지원전공이 일치하여야 함
- 교차수강 불가능
- 최대 36ECTS 수강', NULL, NULL, NULL, '- 제한된 기숙사 여석
- 1200EUR/1학기', NULL, '2024-2', NULL, NULL), + (239, 95, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 경영학 전공 학생만 지원할 수 있음
-지원 전, 경영학 중 어떤 세부 전공이 지원 불가한지는 지원 예정 학생이 수강 제공 과목 문서를 국제교류팀으로 문의 후 스스로 파악한 후 지원할 것
- 교차수강 불가능
- 최소 15ECTS, 최대 30ECTS까지 수강', '- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월15일까지 유효한 성적표여야 함)
- 최저 성적요건은 프로그램에 따라 상이함(지원전 별도 문의할 것)
', '* 학년에 따라 영어 성적이 상의하니 유의할 것
- 2-3학년: IBT 72/ TOEIC 750/ IELTS 5.5
- 4학년: IBT 83/ TOEIC 790/ IELTS 6.0', NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (240, 103, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-소속전공과 지원전공이 일치하여야 함
-서로 다른 프로그램, 언어, 학과 교차 수강 불가능
', '''- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월20일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (241, 86, 2, 7, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 컴퓨터공학 전공 학생만 지원할 수 있음
- 교차 수강 불가
- 학기당 최소 12ECTS, 최대 30ECTS 수강', '- 2024년 12월 31일까지 유효한 어학 성적표여야 함', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (242, 88, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 경영학(Marketing, Finance, Management) 전공 학생만 지원할 수 있음
- 교차 수강 불가
- 학기당 최소 15ECTS, 최대 34ECTS 수강', '- 2024년 12월 31일까지 유효한 어학 성적표여야 함', NULL, NULL, '- 미제공', '-강의는 온라인 및 오프라인 혼합형태로 이루어질 수 있음
-한 달 예측 일반주거비용은 800~2500 USD', '2024-2', 2.5, 4), + (243, 94, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 교차 수강 불가
- 영어로 진행되는 전공수업: Diplomacy and International Relations
- 프랑스어로 진행되는 전공수업: International Relations and Political Science
- 최소 15ECTS, 최대 34ECTS 수강신청 가능', '- 2024년 12월 31일까지 유효한 어학 성적표여야 함', '-IBT minimum score: Reading 13, Writing 21, Listening 13, Speaking 18', NULL, '- 미제공', '-강의는 온라인 및 오프라인 혼합형태로 이루어질 수 있음
-한 달 예측 일반주거비용은 800~2500 USD', '2024-2', 2.5, 4), + (244, 105, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차수강 불가능
- 모든 교환학생은 해당 대학에서 교환학생시 최소 8 ECTS학점(프랑스어 5 ECTS학점, 프랑스문화 3ECTS학점) 이상 이수가 필수임
-2nd year of master law, International MBA 지원 불가 및 수강 불가', '- 2025년 2월 1일까지 유효한 어학 성적표여야 함. ', '- 영어점수는 아래 각 세부점수를 만족해야 함
- TOEFL IBT: 모든 영역 20점 이상
- TOEFL ITP: 모든 영역 50점 이상
- IELTS: 모든 영역에서 6 이상
', NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (245, 93, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
-지원가능전공 : 경영학(본교에서 경영학 관련 기초 과목을 이수한 사람만 지원 가능)
-교차 수강 불가
- 학기당 최소 20ECTS, 최대 30ECTS 수강', NULL, NULL, NULL, '- 기숙사는 미제공. 교외 숙소를 학생 스스로 구해야 함
- 교외숙소 비용은 한달에 대략 250~600€로 다양함', NULL, '2024-2', NULL, NULL), + (246, 102, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 건축학부 재학생에 함함
- 발드센느대학에서 제공하는 건축학 수업은 이 대학의 석사레벨 (4-5학년) 학생들에게 제공하는 수업 레벨임을 참고할것
- 소속전공과 지원전공이 일치하여야 함
-교차수강불가
- 학기당 최대 30ECTS 수강', NULL, '- 프랑스어 성적 제출이 필수는 아니나 대부분의 과목이 프랑스어로 진행되므로 프랑스어를 사전에 공부할 것을 권고함', NULL, '- 미제공
', NULL, '2024-2', NULL, NULL), + (247, 159, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-공학 및 과학 전공 학생만 지원 가능
-현재 3, 4학년 학생만 지원 가능
- 학기당 최소 5ECTS, 최대 30ECTS 수강
-소속 전공과 지원 전공이 일치하여야 하고 다른 학년 및 전공 수업 교차수강 불가
-공학 및 과학 계열 관련 기초 과목을 이수한 사람만 지원 가능
-지원 전, 공학 및 과학 중 어떤 세부 전공이 지원 불가한지는 지원 예정 학생이 factsheet 문서를 국제교류팀으로 문의 후 스스로 파악한 후 지원할 것', '- 2024년 10월 30일까지 유효한 어학 성적표여야 함', '-IBT minimum score: Reading 13, Writing 21, Listening 13, Speaking 18', NULL, '- 미제공
- 한 학기에 소요되는 대략적일반주거비용은 1000~2500 USD', NULL, '2024-2', NULL, NULL), + (248, 107, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차수강 불가능
-학기당 최소 5ECTS,최대 40ECTS 수강', '- 2024년 5월 31일까지 유효한 어학 성적표여야 함', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (249, 108, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 교차 수강 가능
- 학기당 최소 5ECTS, 최대 30ECTS 수강 가능', '-대부분의 정규과정이 프랑스어로만 진행되므로 해당학교에서 DELF B2성적을 가진 학생이 지원하기를 강력히 추천함', '-DELF B2 성적을 소유하고 있는 학생이 지원하기를 강력히 추천함', NULL, '- 기숙사 있으나 제한적이며 선착순 배정
- 1600-1800EUR/학기당', NULL, '2024-2', NULL, NULL), + (250, 160, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 교차 수강 가능
- 학기당 최소 5ECTS, 최대 30ECTS 수강 가능', '- 2024년 5월 31일까지 유효한 어학 성적표여야 함', '-DELF B2: score 50/100 (minimum)', NULL, '- 미제공', NULL, '2024-2', NULL, NULL), + (251, 99, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 교차수강 가능
- 지원불가능전공: Medicine, Midwifery, Nursing, Physiotherapy, Chiropody,Faculty of Law: 2nd year of Master (Master de Droit), Digital animations and Video gamrs / 2nd year of Master
- 최소 20ECTS 수강', NULL, NULL, NULL, '- 제한된 여석 기숙사, 선착순
- 2300~3000EUR/1학기', NULL, '2024-2', 2.75, 4), + (252, 106, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차 수강 불가 (학생들은 B2 year 또는 B3 year중에서 수업을 골라야 함
- 3학기 이상 경영학 수업을 들은 사람만 지원 가능
- 학기당 최소 25ECTS 수강', '''- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년4월25일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공', NULL, '2024-2', 2.5, 4), + (253, 129, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, '※JLPT 성적 소지자만 지원 가능', '영어강의 미제공 ', NULL, '※해당교에서 지정한 보험 가입 필수 (약 2000엔)', '2024-2', NULL, NULL), + (254, 123, NULL, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※일본어 코스 이외에도 정규 코스 수강 가능 (수강 조건 부합시)
※한 학기당 최소 6과목 이상 수강, 최대 24학점까지 수강 가능
※정규 코스 대부분이 일본어수업, 약 30개 정도가 외국어로 강의됨
※정규 코스 신청시, 코디네이터 허가 필요

https://www.dokkyo.ac.jp/english/exchange/calendar/gradingsystem.html', '가을학기 지원 자격: N5, 봄학기 지원 자격: N4 ', '영어강의 미제공 ', '※기숙사 없으나 학교측에서 off-campus 보증
※2023년 기준 매달 58,000~70,000엔', '※2023학년 기준 보험가입 비용 한 학기: 5,400엔 / 1년 8,200엔
※3개월 이상 거주시 국민건강보험 가입 의무 (연간 1만엔)', '2024-2', NULL, NULL), + (255, 128, 1, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※대부분의 교환학생이 국제처 프로그램대로 수업 수강
※JLPT 성적이 높을시 몇몇 학부 수업 수강 가능', NULL, '※대부분 일본어수업으로 진행', '※교환학생들에게 기숙사 미제공', '※대부분의 아파트 계약이 1년 단위이므로, 교환학생일시 아파트 계약 어려움 ', '2024-2', NULL, NULL), + (256, 122, NULL, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※해당 학교 일정상 3월 초까지 서류 제출 필요
※하쿠산 캠퍼스에서만 수강 가능
※수강하고 싶은 과목 요구조건 일치 필요
※비자요건 충족요건으로 매주 최소 7과목 수강', 'TOEIC L&R + S&W = 1530', NULL, NULL, NULL, '2024-2', 2.5, 4), + (257, 124, 6, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', '※해당 학교 일정상 3월 초까지 서류 제출 필요
※최소 6과목 이상 수강
※최대 24학점까지 이수 가능', '※JLPT 성적 소지자만 지원 가능
※학부별로 요구 어학성적기준 상이, 관련페이지 참조
※2021년 9월부터의 영어성적 제출', NULL, NULL, NULL, '2024-2', NULL, NULL), + (258, 125, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '※DEPARTMENT OF GLOBAL ENGLISH, DEPARTMENT OF JAPANESE CULTURE, DEPARTMENT OF MEDIA AND INFORMATION, DEPARTMENT OF PSYCHOLOGY 지원 가능', '※여학생들만 지원 가능', NULL, '※대부분 일본어수업으로 진행', '※기숙사 없음, 계약된 Off-campus 방 존재', NULL, '2024-2', 2.3, 4), + (259, 120, NULL, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '일반일본어강의 수강시 N1 혹은 N2 필요', '※2.30 points 이상 지원 가능
※니가타 대학 학점 환산표 참조', NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), + (260, 127, NULL, 10, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, '※지원 학부/전공별로 자격요건 상이', NULL, NULL, NULL, '2024-2', NULL, NULL), + (261, 131, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'Professional Graduate Program (Law School, Business School)지원불가', '※최소/최대 수강학점은 없으나, 비자조건을 충족하려면 교환학생들은 6과목 이상을 수강하거나 연구시간이 주에 10시간 이상이어야 함 ', NULL, NULL, 'On-campus, Off-campus 둘 다 존재', '기숙사비용 한 학기 27만엔, 두 학기 59만엔 ', '2024-2', 2.5, 4), + (262, 119, NULL, 4, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'A코스 : https://docs.google.com/spreadsheets/d/1jbW2kI1igRHPVvC3d9QoNNIdERbyXtLYqm_RJJ509_I/edit#gid=1999179472

B코스 :
https://docs.google.com/spreadsheets/d/1TAyAhi78Eufz9nbnR7a6_5xvteThT0zC0rBYono3fG4/edit#gid=1847514433', '※A코스(영어)/B 코스(일본어)로 선택 가능
※해당 학교 일정상 3월 초까지 서류 제출 필요
※한 학기당 최소 7과목 수강', '※유효기간 2년 이내의 영어성적 제출
※A코스시, 영어성적이 없는 학생의 경우 추천서 필요
【Letter of Language Competence】
https://docs.google.com/document/d/1nq4mYDT0WN9O1ylpA4WG-sJjU_9MHk-j/edit?usp=drive_link', NULL, '※대학 측에서 지정한 숙소에서 묵어야 함
※입국 전, 한 학기분의 렌트비용 (240,000~300,000엔)을 내야하며, 신용카드로만 결제 가능', NULL, '2024-2', 2.5, 4), + (263, 136, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※12-18 ECTS 수강 가능', '※College of Science에 한해 지원 가능
※지원일까지 유효한 영어성적 제출', NULL, '※기숙사 할당량 제한 ', NULL, '2024-2', NULL, NULL), + (264, 135, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '※학부의 허락 하에 원하는 교과목 수강 가능', '※18-23 ECTS 수강 가능 (4-5 HKSYU courses)', '※2년간 유효한 IELTS 성적 제출', NULL, NULL, NULL, '2024-2', 2.5, 3), + (265, 161, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '※인하대에서의 전공과 같아야함 ', '※학기당 최소 9학점, 최대 15학점까지 수강 가능', '※2년간 유효한 영어성적 제출', NULL, NULL, NULL, '2024-2', NULL, NULL), + (266, 133, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※30학점까지 수강 가능', '※3년간 유효한 영어성적 제출', NULL, '※한 달에 약 25 $-120 $ ', NULL, '2024-2', 3, 4), + (267, 134, NULL, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '※학부 코디네이너의 허가 하에, 다른 학부 수업 수강 가능', '※정규코스는 30학점으로 구성되어있으며, 학기당 최대 36학점까지 수강 가능. 추가수강 희망시 해당교의 허가 필요', 'https://www.ozyegin.edu.tr/en/student-services/application-admission/language-proficiency-requirement', NULL, '※교환학생의 경우 기숙사 보증금 불필요', '※모든 교환학생들은 도착 후 residence permit 신청
※Immigration Office에서의 조건에 부합하는 유효한 건강보험과, 튀르키예에서 가입한 건강보험 둘 다 필요', '2024-2', NULL, NULL), + (268, 162, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '※지도교수의 허락 하에 다른 학부 수업 수강 가능', '※최소 학점 9학점, 최대 21학점 수강 가능
※최소 15학점 수강 권장
※1학기 (8월-12월), 2학기 (1월-5월)

https://www.rsuip.org/for-student/timetable/', NULL, NULL, NULL, '※출국 전 보험가입 필수', '2024-2', 2.5, 4), + (269, 163, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※최소 9UCTS 수강', NULL, NULL, 'On-Campus (4인실): 학기당 1,500-2,000 USD
Off-Campus (1인실): 매달 150 USD', NULL, '2024-2', NULL, NULL), + (270, 114, NULL, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※3월에 성적이 나오므로, 졸업 전 막학기 학생 지원 비권장
※최소 2, 최대 4 SMU course 수강 가능 ', '※영어어학성적 대신 Support letter 접수 가능
※COVID-19의 상황을 고려해, TOEFL iBT® Special Home Edition 혹은 IELTS Indicator 성적 또한 제출 가능
※지원 시점에서 2년간 유효한 영어성적 제출', NULL, 'SMU 할인이 적용된 방 옵션 리스트 제공', '※필수경비(학기당 약 SGD250)에 보험가입비 포함됨 ', '2024-2', NULL, NULL), + (271, 164, NULL, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, '※영어과목일시 자유롭게 수강 가능
※최소 15학점, 최대 30학점 수강 가능', '※2년간 유효한 영어성적 제출', NULL, '※1년 이하 체류 학생들에게 기숙사 제공 가능
※한 학기당 50-100 EUR', NULL, '2024-2', NULL, NULL), + (272, 115, 2, 3, NULL, 'HOME_UNIVERSITY_PAYMENT', '※전공과 다른 학부 지원 가능', '※ 대학원 학위과정 Master of Science in Computer Science and Data Analytics, Master of Science in Electrical and Power Engineering with George Washington University 수강 불가
※최대 30 ECTS 수강 가능', '※영어어학성적 대신 지도교수로부터 B2 이상의 영어성적을 가지고 있다는 Support letter 로도 대체 가능 ', NULL, '※기숙사를 보유하고 있지 않으나, 학교측에서 교환학생들이 숙소를 찾는 것에 직접적인 도움 제공
※숙소 예약은 학교 측에서 보증되며, 260-400 AZN정도 발생', NULL, '2024-2', 2, 4), + (273, 165, NULL, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '※English Language and Literature, Radio, TV and Cinema, Finance and Banking, Economics, Business Administration, Political Science and Public Administration, International Relations, International Trade and Finance, New Media, Computer Engineering, Industrial Engineering, Industrial Design, Interior Architecture, Civil Engineering, Mechanical Engineering, Architecture 수강 가능', '※최대 36학점까지 수강 가능', '※B2 수준의 영어 혹은 튀르키예어 어학성적 요구
※3년간 유효한 영어성적 제출 ', NULL, '※기숙사 보유, 선착순 신청
※업데이트 된 비용문의 erasmus@beykent.edu.tr', NULL, '2024-2', NULL, NULL), + (274, 139, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능', '- 영어강의의 대부분이 대학원 과정으로, 학부 3/4학년에게만 수강이 가능하도록 해둠. 따라서, 중국어는 잘 하지 못하나 영어에 자신이 있는 경우에는 학부 3/4학년에 지원하길 바람', ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 보유(https://in.ncu.edu.tw/~ncu7221/OSDS/index.php)
- 여실 여부 미확정으로, 여실이 없을 시 도보 5~15분 거리의 오프캠퍼스 숙소 이용 가능', '- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', '70 (경영대 - 80)', 100), + (275, 166, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능', '-', ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 보유 (NTD 10,000/1학기)', '- 보험가입의무(NTD 500/월)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', NULL, NULL), + (276, 167, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능', '-', '- 2022.05 이후 발급된 성적
- 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 보유 (4인실, NTD 18,000/1학기)
- 여실 여부 미확정', '- 보험가입의무(최소 5천만원 이상의 보장성을 지닌 한국여행자보험 가입 필수)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 3, 4.3), + (277, 168, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능
- 교환학생 지원 가능 학과/프로그램
(https://drive.google.com/file/d/1jCGkEjimtykwscl_ZLSzyGlEOtXzhZlq/view?usp=drive_link) _이 외 불가', '- 3, 4학년만 지원가능', '- 특정 어학요건은 없으나, 모든 강의는 영어 혹은 중국어로 이루어짐으로 영어를 우수하게 하는 학생들만 지원 요망
- 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 미보유, 관련내용 확인', '- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 2.44, 4.3), + (278, 138, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능
- 교환학생 지원불가 전공: International Business Bachelor Program, Master of Business Administration Program in International Business, Global Human Resource Management MBA Program, Si-Wan College.', NULL, ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 보유, 홈페이지 하단부 확인
- 거의 기숙사는 보장되나, 확실히 보장된다고 말하기는 어려움. 지원시 기숙사 동시지원필요', '- 보험가입의무(NTD 326/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', NULL, NULL), + (279, 169, '4 - 학부생
1 - 대학원생', 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능', '- 3, 4학년만 지원가능(학부)', '- 최소 2024년 말일까지 유효한 성적
- 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유
- 여실 여부 미확정으로, 여실이 없을 시 오프캠퍼스 숙소 이용가능', '- 보험가입의무(NTD 3500/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 3, 4.3), + (280, 170, '2 - 학부생
1 - 대학원생', 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차수강 가능', NULL, '- 중어과정의 경우
Science, Engineering, Economics, Management (HSK4, 180)
Humanities, Foreign languages (HSK5, 180)', NULL, '-기숙사 보유
3개월 이상 / 싱글(월 900RMB), 더블(월 600RMB)', '- 보험가입의무(RMB 400/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 75, 100), + (281, 171, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차수강 불가능
- 북경외대 IBS만 신청가능', NULL, '- 최소 2024년 12월까지 유효한 성적
- 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유(800 - 2,000USD/1학기)
- 선착순, 대략 95%의 신청자가 합격된다고 함.
(Youtube : @ibsbfsu_official)', '- 보험가입의무(USD 60/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 2.8, 4), + (282, 145, '2 - 학부생
1 - 대학원생', 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 특히 예술대학의 경우 HSK4급 이상 필요
- 교차수강 불가능', NULL, NULL, NULL, '- 기숙사 보유(1인/1침대/1일 33위안)', '- 보험가입의무(RMB 400/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망
- 개인상황에 따라 다르나 한달 생활비 1천-2천 위안 예상', '2024-2', '70 (2.3)', '100 (4.0)'), + (283, 172, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 중국어 어학과정 외 타 과정 수강 불가
- 교환학생은 Chinese in College of Advanced Chinese Training에서 수강', '- Chinese Government Scholarship / Confucius Institute Scholarship 수혜자 지원불가', NULL, NULL, '- 기숙사 보유(더블룸:80RMB/일, 싱글룸:150RMB/일)
※ 지원간 기숙사 신청 미리 필요
- 중국 입국 24시간 내에 입주신고 필요', '- 보험가입의무(RMB 400/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', NULL, NULL), + (284, 149, NULL, 10, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '- 어학요건이 대학별로 상이하므로 반드시 Program Guide 참고할 것', NULL, NULL, '- 지원 전 반드시 국제교류팀 담당자와 상담할 것', '2024-2', NULL, NULL); \ No newline at end of file From 235d490e9ba55f67e3df779cc93ad7dd8cf78911 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 22:09:00 +0900 Subject: [PATCH 042/158] =?UTF-8?q?feat:=202=ED=95=99=EA=B8=B0=20=EB=8C=80?= =?UTF-8?q?=ED=95=99=EB=A7=8C=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../university/repository/UniversityInfoForApplyRepository.java | 1 + .../solidconnection/university/service/UniversityService.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index d2bcc3841..8153fa291 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -10,6 +10,7 @@ @Repository public interface UniversityInfoForApplyRepository extends JpaRepository { Optional findByUniversityAndTerm(University university, String term); + Boolean existsByUniversityAndTerm(University university, String term); Optional findByUniversity_KoreanNameAndTerm(String koreanName, String term); Optional findByIdAndTerm(Long id, String term); } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index 59d6dd704..a3d77598d 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import static com.example.solidconnection.constants.Constants.RECOMMEND_UNIVERSITY_NUM; +import static com.example.solidconnection.constants.Constants.TERM; @Service @RequiredArgsConstructor @@ -143,6 +144,7 @@ public List search(String region, String keyword) { List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, keyword); return universities.stream() + .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) .map(university -> { UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); return UniversityPreviewDto.fromEntity(universityInfoForApply); From 1699466db32ecb3f3124073dc1fdb67af139512d Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 22:09:41 +0900 Subject: [PATCH 043/158] =?UTF-8?q?feat:=20applicants=20dto=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/UniversityApplicantsDto.java | 2 ++ .../application/service/ApplicationService.java | 9 +++++++++ .../type/SemesterAvailableForDispatch.java | 4 +++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java index 34394f021..ddeb56e99 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java @@ -12,5 +12,7 @@ public class UniversityApplicantsDto { private String koreanName; private int studentCapacity; + private String region; + private String country; private List applicants; } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index a62b912de..1e001501e 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -12,6 +12,7 @@ import com.example.solidconnection.type.CountryCode; import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.type.VerifyStatus; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.custom.UniversityRepositoryForFilterImpl; import com.example.solidconnection.university.service.UniversityValidator; import lombok.RequiredArgsConstructor; @@ -24,6 +25,7 @@ import java.util.Random; import static com.example.solidconnection.constants.Constants.APPLICATION_UPDATE_COUNT_LIMIT; +import static com.example.solidconnection.constants.Constants.TERM; import static com.example.solidconnection.custom.exception.ErrorCode.*; @Service @@ -31,6 +33,7 @@ @Transactional public class ApplicationService { private final ApplicationRepository applicationRepository; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final UniversityValidator universityValidator; private final SiteUserValidator siteUserValidator; private final ApplicationValidator applicationValidator; @@ -139,6 +142,7 @@ private void validateApproved(Application application) { private List getFirstChoiceApplicants(List universities, SiteUser siteUser) { return universities.stream() + .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) .map(university -> { UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); List firstChoiceApplication = applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatus(universityInfoForApply, VerifyStatus.APPROVED); @@ -148,6 +152,8 @@ private List getFirstChoiceApplicants(List return UniversityApplicantsDto.builder() .koreanName(university.getKoreanName()) .studentCapacity(universityInfoForApply.getStudentCapacity()) + .region(university.getRegion().getCode().getKoreanName()) + .country(university.getCountry().getCode().getKoreanName()) .applicants(firstChoiceApplicant) .build(); }) @@ -156,6 +162,7 @@ private List getFirstChoiceApplicants(List private List getSecondChoiceApplicants(List universities, SiteUser siteUser) { return universities.stream() + .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) .map(university -> { UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); List secondChoiceApplication = applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatus(universityInfoForApply, VerifyStatus.APPROVED); @@ -165,6 +172,8 @@ private List getSecondChoiceApplicants(List return UniversityApplicantsDto.builder() .koreanName(university.getKoreanName()) .studentCapacity(universityInfoForApply.getStudentCapacity()) + .region(university.getRegion().getCode().getKoreanName()) + .country(university.getCountry().getCode().getKoreanName()) .applicants(secondChoiceApplicant) .build(); }) diff --git a/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java index 406d74f88..c31415bb7 100644 --- a/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java +++ b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java @@ -5,7 +5,9 @@ public enum SemesterAvailableForDispatch { FOUR_SEMESTER("4개학기"), ONE_OR_TWO_SEMESTER("1개 또는 2개 학기"), ONE_YEAR("1년만 가능"), - IRRELEVANT("무관"); + IRRELEVANT("무관"), + NO_DATA("데이터 없음") + ; private final String koreanName; From a780602b28401d5cef1a31576bbfa2578403205d Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 22:50:55 +0900 Subject: [PATCH 044/158] =?UTF-8?q?refacter:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EB=BF=90=20=EC=95=84=EB=8B=88=EB=9D=BC=20=EB=8B=A4=EB=A5=B8?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EB=8F=84=20=EC=98=AC=EB=A6=B4=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../custom/exception/ErrorCode.java | 4 ++-- .../solidconnection/s3/S3Controller.java | 18 +++++++++--------- .../example/solidconnection/s3/S3Service.java | 8 ++++---- ...mageUrlDto.java => UploadedFileURLDto.java} | 4 ++-- .../solidconnection/type/LanguageTestType.java | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) rename src/main/java/com/example/solidconnection/s3/{ImageUrlDto.java => UploadedFileURLDto.java} (65%) diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 3c07db4ee..4f6c35da8 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -22,8 +22,8 @@ public enum ErrorCode { S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), S3_CLIENT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 클라이언트 에러 발생"), FILE_NOT_EXIST(HttpStatus.UNAUTHORIZED.value(), "파일이 없습니다."), - INVALID_FILE_EXTENSIONS(HttpStatus.UNAUTHORIZED.value(), "파일 형식이 유효하지 않습니다."), - NOT_IMG_FILE_EXTENSIONS(HttpStatus.UNAUTHORIZED.value(), "이미지만 업로드 할 수 있습니다."), + INVALID_FILE_EXTENSIONS(HttpStatus.BAD_REQUEST.value(), "파일 형식이 유효하지 않습니다."), + NOT_ALLOWED_FILE_EXTENSIONS(HttpStatus.BAD_REQUEST.value(), "허용되지 않은 확장자입니다."), USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), USER_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 회원입니다."), JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱 에러"), diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java index 4b1dc7c28..33d240591 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -14,32 +14,32 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/img") +@RequestMapping("/file") public class S3Controller { private final S3Service s3Service; @PostMapping("/profile/pre") - public CustomResponse uploadPreProfileImage(@RequestParam("imageFile") MultipartFile imageFile) { - ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.PROFILE); + public CustomResponse uploadPreProfileImage(@RequestParam("file") MultipartFile imageFile) { + UploadedFileURLDto profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); return new DataResponse<>(profileImageUrl); } @PostMapping("/profile/post") - public CustomResponse uploadPostProfileImage(@RequestParam("imageFile") MultipartFile imageFile, Principal principal) { - ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.PROFILE); + public CustomResponse uploadPostProfileImage(@RequestParam("file") MultipartFile imageFile, Principal principal) { + UploadedFileURLDto profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); s3Service.deleteExProfile(principal.getName()); return new DataResponse<>(profileImageUrl); } @PostMapping("/gpa") - public CustomResponse uploadGpaImage(@RequestParam("imageFile") MultipartFile imageFile) { - ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.GPA); + public CustomResponse uploadGpaImage(@RequestParam("file") MultipartFile imageFile) { + UploadedFileURLDto profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA); return new DataResponse<>(profileImageUrl); } @PostMapping("/language-test") - public CustomResponse uploadLanguageImage(@RequestParam("imageFile") MultipartFile imageFile) { - ImageUrlDto profileImageUrl = s3Service.uploadImgFile(imageFile, ImgType.LANGUAGE_TEST); + public CustomResponse uploadLanguageImage(@RequestParam("file") MultipartFile imageFile) { + UploadedFileURLDto profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST); return new DataResponse<>(profileImageUrl); } } diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 3e4fa1779..2c2b37b98 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -33,7 +33,7 @@ public class S3Service { private final AmazonS3Client amazonS3; private final SiteUserValidator siteUserValidator; - public ImageUrlDto uploadImgFile(MultipartFile multipartFile, ImgType imageFile) { + public UploadedFileURLDto uploadFile(MultipartFile multipartFile, ImgType imageFile) { validateImgFile(multipartFile); String contentType = multipartFile.getContentType(); ObjectMetadata metadata = new ObjectMetadata(); @@ -54,7 +54,7 @@ public ImageUrlDto uploadImgFile(MultipartFile multipartFile, ImgType imageFile) throw new CustomException(S3_CLIENT_EXCEPTION); } - return new ImageUrlDto(amazonS3.getUrl(bucket, fileName).toString()); + return new UploadedFileURLDto(amazonS3.getUrl(bucket, fileName).toString()); } private void validateImgFile(MultipartFile file) { @@ -65,9 +65,9 @@ private void validateImgFile(MultipartFile file) { String fileName = Objects.requireNonNull(file.getOriginalFilename()); String fileExtension = getFileExtension(fileName).toLowerCase(); - List allowedExtensions = Arrays.asList("jpg", "jpeg", "png"); + List allowedExtensions = Arrays.asList("jpg", "jpeg", "png", "webp", "pdf", "word", "docx"); if (!allowedExtensions.contains(fileExtension)) { - throw new CustomException(NOT_IMG_FILE_EXTENSIONS, "허용된 형식: " + allowedExtensions); + throw new CustomException(NOT_ALLOWED_FILE_EXTENSIONS, "허용된 형식: " + allowedExtensions); } } diff --git a/src/main/java/com/example/solidconnection/s3/ImageUrlDto.java b/src/main/java/com/example/solidconnection/s3/UploadedFileURLDto.java similarity index 65% rename from src/main/java/com/example/solidconnection/s3/ImageUrlDto.java rename to src/main/java/com/example/solidconnection/s3/UploadedFileURLDto.java index 1690f56dd..5af4a0503 100644 --- a/src/main/java/com/example/solidconnection/s3/ImageUrlDto.java +++ b/src/main/java/com/example/solidconnection/s3/UploadedFileURLDto.java @@ -6,6 +6,6 @@ @Setter @NoArgsConstructor @AllArgsConstructor -public class ImageUrlDto { - private String imageUrl; +public class UploadedFileURLDto { + private String fileUrl; } diff --git a/src/main/java/com/example/solidconnection/type/LanguageTestType.java b/src/main/java/com/example/solidconnection/type/LanguageTestType.java index 9d5cbc482..cad6f9b49 100644 --- a/src/main/java/com/example/solidconnection/type/LanguageTestType.java +++ b/src/main/java/com/example/solidconnection/type/LanguageTestType.java @@ -1,5 +1,5 @@ package com.example.solidconnection.type; public enum LanguageTestType { - TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF, TCF, TEF + TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF, TCF, TEF, DALF } From 443ef91a0012572ef482abb1c2fe6e1534658a23 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sat, 17 Feb 2024 22:56:01 +0900 Subject: [PATCH 045/158] =?UTF-8?q?refactor:=20URI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/security/JwtAuthenticationFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 35c9eb873..2d9839578 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -98,7 +98,7 @@ private HashSet getPermitAllEndpoints() { permitAllEndpoints.add("/favicon.ico"); // 이미지 업로드 - permitAllEndpoints.add("/img/profile/pre"); + permitAllEndpoints.add("/file/profile/pre"); // 토큰이 필요하지 않은 인증 permitAllEndpoints.add("/auth/kakao"); @@ -109,4 +109,4 @@ private HashSet getPermitAllEndpoints() { return permitAllEndpoints; } -} \ No newline at end of file +} From 2e8f98cd0397008d1c8a0d42543b226b98e9ce9b Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sat, 17 Feb 2024 22:56:26 +0900 Subject: [PATCH 046/158] =?UTF-8?q?refactor:=20URI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/config/security/SecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 7e082b7f8..2078aa1de 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -48,7 +48,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { -> authorizeRequest .requestMatchers( "/", "/index.html", "/favicon.ico", - "/img/profile/pre", + "/file/profile/pre", "/auth/kakao", "/auth/sign-up", "/auth/reissue", "/university/detail/**", "/university/search/**", "/home" ) From f6c4c4c488c0f80625a48bb44932f370c8c7665f Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 23:15:19 +0900 Subject: [PATCH 047/158] =?UTF-8?q?refactor:=20REJECT=EC=99=80=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=83=81=ED=83=9C=EB=8F=84=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=97=90=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/VerifyStatusDto.java | 5 +++++ .../application/service/ApplicationService.java | 16 ++++++++++++---- .../type/ApplicationStatusResponse.java | 5 +++++ 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java diff --git a/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java b/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java index c6cdc7653..e636e3d75 100644 --- a/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java +++ b/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java @@ -9,4 +9,9 @@ @NoArgsConstructor public class VerifyStatusDto { private String status; + private int updateCount = 0; + + public VerifyStatusDto(String status){ + this.status = status; + } } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index 1e001501e..2abd44cbb 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -9,6 +9,7 @@ import com.example.solidconnection.entity.University; import com.example.solidconnection.entity.UniversityInfoForApply; import com.example.solidconnection.siteuser.service.SiteUserValidator; +import com.example.solidconnection.type.ApplicationStatusResponse; import com.example.solidconnection.type.CountryCode; import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.type.VerifyStatus; @@ -183,12 +184,19 @@ private List getSecondChoiceApplicants(List public VerifyStatusDto getVerifyStatus(String email) { SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); Optional application = applicationRepository.findBySiteUser_Email(siteUser.getEmail()); + if (application.isEmpty()) { - return new VerifyStatusDto("NOT_SUBMITTED"); + return new VerifyStatusDto(ApplicationStatusResponse.NOT_SUBMITTED.name()); + } + if (application.get().getVerifyStatus() == VerifyStatus.PENDING) { + return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_PENDING.name()); } - if (application.get().getVerifyStatus() == VerifyStatus.APPROVED) { - return new VerifyStatusDto("SUBMITTED_APPROVED"); + if (application.get().getVerifyStatus() == VerifyStatus.REJECTED) { + return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_REJECTED.name()); } - return new VerifyStatusDto("SUBMITTED_NOT_APPROVED"); + return VerifyStatusDto.builder() + .status(ApplicationStatusResponse.SUBMITTED_APPROVED.name()) + .updateCount(application.get().getUpdateCount()) + .build(); } } diff --git a/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java b/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java new file mode 100644 index 000000000..1df41df07 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java @@ -0,0 +1,5 @@ +package com.example.solidconnection.type; + +public enum ApplicationStatusResponse { + NOT_SUBMITTED, SUBMITTED_PENDING, SUBMITTED_REJECTED, SUBMITTED_APPROVED +} From 2707fd6b6d9beaae9713fe26ba13b669e9444880 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 17 Feb 2024 23:57:45 +0900 Subject: [PATCH 048/158] =?UTF-8?q?refactor:=20=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=99=20=EC=88=98=EC=A0=95=ED=96=88=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=EB=A7=8C=20updateCount=20=EB=8A=98=EB=A6=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApplicationController.java | 8 ++--- .../service/ApplicationService.java | 32 ++++++++++--------- .../type/LanguageTestType.java | 2 +- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 24c6d8c4f..c626b4c28 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -21,14 +21,14 @@ public class ApplicationController { private final ApplicationService applicationService; @PostMapping("/score") - public CustomResponse registerScore(Principal principal, @Valid @RequestBody ScoreRequestDto scoreRequestDto) { - boolean result = applicationService.saveScore(principal.getName(), scoreRequestDto); + public CustomResponse submitScore(Principal principal, @Valid @RequestBody ScoreRequestDto scoreRequestDto) { + boolean result = applicationService.submitScore(principal.getName(), scoreRequestDto); return new StatusResponse(result); } @PostMapping("/university") - public CustomResponse registerUniversity(Principal principal, @Valid @RequestBody UniversityRequestDto universityRequestDto) { - boolean result = applicationService.saveUniversity(principal.getName(), universityRequestDto); + public CustomResponse submitUniversityChoice(Principal principal, @Valid @RequestBody UniversityRequestDto universityRequestDto) { + boolean result = applicationService.submitUniversityChoice(principal.getName(), universityRequestDto); return new StatusResponse(result); } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index 2abd44cbb..01df6fc17 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -40,33 +40,29 @@ public class ApplicationService { private final ApplicationValidator applicationValidator; private final UniversityRepositoryForFilterImpl universityRepositoryForFilter; - public boolean saveScore(String email, ScoreRequestDto scoreRequestDto) { + public boolean submitScore(String email, ScoreRequestDto scoreRequestDto) { SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - // 한번 등록 후 수정 + // 수정 if (applicationRepository.existsBySiteUser_Email(email)) { Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); - // 수정 횟수 초과 에러 처리 - if (application.getUpdateCount() > APPLICATION_UPDATE_COUNT_LIMIT) { - throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); - } application.setGpa(scoreRequestDto.getGpa()); application.setGpaCriteria(scoreRequestDto.getGpaCriteria()); application.setGpaReportUrl(scoreRequestDto.getGpaReportUrl()); application.setLanguageTestScore(scoreRequestDto.getLanguageTestScore()); application.setLanguageTestType(scoreRequestDto.getLanguageTestType()); application.setLanguageTestReportUrl(scoreRequestDto.getLanguageTestReportUrl()); - application.setUpdateCount(application.getUpdateCount() + 1); + application.setVerifyStatus(VerifyStatus.PENDING); return true; } - // 최초 증록 + // 최초 등록 Application application = Application.saveScore(siteUser, scoreRequestDto); applicationRepository.save(application); return true; } - public boolean saveUniversity(String email, UniversityRequestDto universityRequestDto) { + public boolean submitUniversityChoice(String email, UniversityRequestDto universityRequestDto) { // 수정 횟수 초과 에러 처리 Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); if (application.getUpdateCount() > APPLICATION_UPDATE_COUNT_LIMIT) { @@ -87,6 +83,12 @@ public boolean saveUniversity(String email, UniversityRequestDto universityReque throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); } + + // 수정이면 update count 1 증가 + if (application.getFirstChoiceUniversity() != null) { + application.setUpdateCount(application.getUpdateCount() + 1); + } + // 수정 application.setFirstChoiceUniversity(firstChoiceUniversity); application.setSecondChoiceUniversity(secondChoiceUniversity); @@ -97,6 +99,7 @@ public boolean saveUniversity(String email, UniversityRequestDto universityReque randomNickname = makeRandomNickname(); } application.setNicknameForApply(randomNickname); + return true; } @@ -188,15 +191,14 @@ public VerifyStatusDto getVerifyStatus(String email) { if (application.isEmpty()) { return new VerifyStatusDto(ApplicationStatusResponse.NOT_SUBMITTED.name()); } + + int updateCount = application.get().getUpdateCount(); if (application.get().getVerifyStatus() == VerifyStatus.PENDING) { - return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_PENDING.name()); + return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_PENDING.name(), updateCount); } if (application.get().getVerifyStatus() == VerifyStatus.REJECTED) { - return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_REJECTED.name()); + return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_REJECTED.name(), updateCount); } - return VerifyStatusDto.builder() - .status(ApplicationStatusResponse.SUBMITTED_APPROVED.name()) - .updateCount(application.get().getUpdateCount()) - .build(); + return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_APPROVED.name(), updateCount); } } diff --git a/src/main/java/com/example/solidconnection/type/LanguageTestType.java b/src/main/java/com/example/solidconnection/type/LanguageTestType.java index cad6f9b49..d01d11544 100644 --- a/src/main/java/com/example/solidconnection/type/LanguageTestType.java +++ b/src/main/java/com/example/solidconnection/type/LanguageTestType.java @@ -2,4 +2,4 @@ public enum LanguageTestType { TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF, TCF, TEF, DALF -} +} \ No newline at end of file From 722318fb9a2e34b94afbdb0f7235d6dad1bfed34 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 18 Feb 2024 01:57:42 +0900 Subject: [PATCH 049/158] =?UTF-8?q?feat:=20=EB=82=98=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=A7=9E=EC=B6=A4=20=EB=8C=80=ED=95=99=20=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ApplicationService.java | 4 +-- .../custom/exception/ErrorCode.java | 1 + .../solidconnection/type/CountryCode.java | 17 +++++++----- .../type/LanguageTestType.java | 15 ++++++++++- .../controller/UniversityController.java | 9 ++++--- .../LanguageRequirementRepository.java | 8 ++++++ .../custom/UniversityRepositoryForFilter.java | 2 +- .../UniversityRepositoryForFilterImpl.java | 27 ++++++++++++++----- .../university/service/UniversityService.java | 18 +++++++------ .../service/UniversityValidator.java | 2 ++ 10 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index 01df6fc17..6c747c144 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -126,10 +126,10 @@ public ApplicationsDto getApplicants(String email, String region, String keyword } List countryCodes = null; if (keyword != null && !keyword.isBlank()) { - countryCodes = CountryCode.getCountryCodeMatchesToKeyword(keyword); + countryCodes = CountryCode.getCountryCodeMatchesToKeyword(List.of(keyword)); } - List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, keyword); + List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, List.of(keyword)); List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser); List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser); return ApplicationsDto.builder() diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 4f6c35da8..5d31223da 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -9,6 +9,7 @@ @Getter @AllArgsConstructor public enum ErrorCode { + INVALID_TEST_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않은 어학 시험 종류입니다."), APPLICATION_NOT_APPROVED(HttpStatus.BAD_REQUEST.value(), "성적표가 인증되지 않았습니다."), APPLY_UPDATE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST.value(), "지원 정보 수정은 3회까지만 가능합니다."), CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2지망에 동일한 대학교를 입력할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/type/CountryCode.java b/src/main/java/com/example/solidconnection/type/CountryCode.java index 497ff5095..8062f1ddb 100644 --- a/src/main/java/com/example/solidconnection/type/CountryCode.java +++ b/src/main/java/com/example/solidconnection/type/CountryCode.java @@ -3,6 +3,7 @@ import com.example.solidconnection.custom.exception.CustomException; import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -39,8 +40,7 @@ public enum CountryCode { HU("헝가리"), LT("리투아니아"), TH("태국"), - UZ("우즈베키스탄") - ; + UZ("우즈베키스탄"); private final String koreanName; @@ -55,10 +55,15 @@ public static CountryCode getCountryCodeByKoreanName(String koreanName) { return matchingCountryCode.orElseThrow(() -> new CustomException(INVALID_COUNTRY_NAME, koreanName)); } - public static List getCountryCodeMatchesToKeyword(String keyword) { - return Arrays.stream(CountryCode.values()) - .filter(country -> country.koreanName.contains(keyword)) - .toList(); + public static List getCountryCodeMatchesToKeyword(List keywords) { + List matchedCountryCodes = new LinkedList<>(); + keywords.forEach( keyword -> { + List countryCodes = Arrays.stream(CountryCode.values()) + .filter(country -> country.koreanName.contains(keyword)) + .toList(); + matchedCountryCodes.addAll(countryCodes); + }); + return matchedCountryCodes; } public String getKoreanName() { diff --git a/src/main/java/com/example/solidconnection/type/LanguageTestType.java b/src/main/java/com/example/solidconnection/type/LanguageTestType.java index d01d11544..36c0638b2 100644 --- a/src/main/java/com/example/solidconnection/type/LanguageTestType.java +++ b/src/main/java/com/example/solidconnection/type/LanguageTestType.java @@ -1,5 +1,18 @@ package com.example.solidconnection.type; +import com.example.solidconnection.custom.exception.CustomException; + +import java.util.Arrays; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TEST_TYPE; + public enum LanguageTestType { - TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF, TCF, TEF, DALF + TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF, TCF, TEF, DALF; + + public static LanguageTestType getLanguageTestTypeForString(String name) { + return Arrays.stream(LanguageTestType.values()) + .filter(lt -> lt.toString().equals(name)) + .findFirst() + .orElseThrow(() -> new CustomException(INVALID_TEST_TYPE, name)); + } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 8a1c6dd20..18669cdf0 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -2,6 +2,7 @@ import com.example.solidconnection.custom.response.CustomResponse; import com.example.solidconnection.custom.response.DataResponse; +import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.dto.LikedResultDto; import com.example.solidconnection.university.dto.UniversityDetailDto; import com.example.solidconnection.university.dto.UniversityPreviewDto; @@ -31,13 +32,15 @@ public CustomResponse getDetails(Principal principal, @PathVariable Long univers @GetMapping("/search") public CustomResponse search(@RequestParam(required = false, defaultValue = "") String region, - @RequestParam(required = false, defaultValue = "") String keyword){ - List universityPreviewDto = universityService.search(region, keyword); + @RequestParam(required = false, defaultValue = "") List keyword, + @RequestParam(required = false, defaultValue = "") String testType, + @RequestParam(required = false, defaultValue = "") String testScore) { + List universityPreviewDto = universityService.search(region, keyword, LanguageTestType.getLanguageTestTypeForString(testType), testScore); return new DataResponse<>(universityPreviewDto); } @PostMapping("/{universityInfoForApplyId}/like") - public CustomResponse like(Principal principal, @PathVariable Long universityInfoForApplyId){ + public CustomResponse like(Principal principal, @PathVariable Long universityInfoForApplyId) { LikedResultDto likedResultDto = universityService.like(principal.getName(), universityInfoForApplyId); return new DataResponse<>(likedResultDto); } diff --git a/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java b/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java index 76015418e..01f3bbcbb 100644 --- a/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java @@ -1,12 +1,20 @@ package com.example.solidconnection.university.repository; import com.example.solidconnection.entity.LanguageRequirement; +import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.type.LanguageTestType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface LanguageRequirementRepository extends JpaRepository { List findAllByUniversityInfoForApply_Id(Long id); + + @Query("SELECT lr FROM LanguageRequirement lr WHERE lr.minScore <= :myScore AND lr.languageTestType = :testType AND lr.universityInfoForApply = :universityInfoForApply ORDER BY lr.minScore ASC") + Optional findByUniversityInfoForApplyAndLanguageTestTypeAndLessThanMyScore(@Param("universityInfoForApply") UniversityInfoForApply universityInfoForApply, @Param("testType") LanguageTestType testType, @Param("myScore") String myScore); } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java index c00ccedcb..e30119428 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java @@ -7,5 +7,5 @@ import java.util.List; public interface UniversityRepositoryForFilter { - List findByRegionAndCountryAndKeyword(RegionCode regionCode, List countryCodes, String keyword); + List findByRegionAndCountryAndKeyword(RegionCode regionCode, List countryCodes, List keywords); } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java index 49d0318f5..c208f5be0 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java @@ -25,17 +25,21 @@ public UniversityRepositoryForFilterImpl(EntityManager em) { } @Override - public List findByRegionAndCountryAndKeyword(RegionCode regionCode, List countryCodes, String keyword) { + public List findByRegionAndCountryAndKeyword(RegionCode regionCode, List countryCodes, List keywords) { QUniversity university = QUniversity.university; QCountry country = QCountry.country; QRegion region = QRegion.region; + System.out.println(keywords); + System.out.println(countryCodes); + System.out.println(regionCodeEq(regionCode, region).and(keywordContainsInCountryOrName(countryCodes, country, keywords, university))); + return queryFactory .selectFrom(university) .join(university.country, country) .join(country.region, region) .where( - regionCodeEq(regionCode, region).and(keywordContainsInCountryOrName(countryCodes, country, keyword, university)) + regionCodeEq(regionCode, region).and(keywordContainsInCountryOrName(countryCodes, country, keywords, university)) ) .fetch(); } @@ -47,17 +51,26 @@ private BooleanExpression regionCodeEq(RegionCode regionCode, QRegion region) { return region.code.eq(regionCode); } - private BooleanExpression keywordContainsInCountryOrName(List countryCodes, QCountry country, String keyword, QUniversity university) { + private BooleanExpression keywordContainsInCountryOrName(List countryCodes, QCountry country, List keywords, QUniversity university) { if (countryCodes == null || countryCodes.isEmpty()) { // 해당하는 국가가 없으면 - if (keyword == null || keyword.isEmpty()) { + if (keywords == null || keywords.isEmpty()) { return Expressions.asBoolean(true).isTrue(); } - return university.koreanName.contains(keyword); // 키워드에 해당하는 식을 반환 + return keywords.stream() + .map(university.koreanName::contains) + .reduce(BooleanExpression::or) + .orElse(Expressions.asBoolean(true).isFalse()); } - if (keyword == null || keyword.isEmpty()) { // 해당하는 국가가 있으면, + BooleanExpression countryCondition = country.code.in(countryCodes); + if (keywords == null || keywords.isEmpty()) { return Expressions.asBoolean(true).isTrue(); } - return country.code.in(countryCodes).or(university.koreanName.contains(keyword)); // 국가와 키워드에 해당하는 식을 반환 + + BooleanExpression keywordCondition = keywords.stream() + .map(university.koreanName::contains) + .reduce(BooleanExpression::or) + .orElse(Expressions.asBoolean(true).isFalse()); + return countryCondition.or(keywordCondition); } } diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index a3d77598d..76531372c 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -1,16 +1,14 @@ package com.example.solidconnection.university.service; import com.example.solidconnection.constants.GeneralRecommendUniversities; -import com.example.solidconnection.entity.LikedUniversity; -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.entity.University; -import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.entity.*; import com.example.solidconnection.home.dto.RecommendedUniversityDto; import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.service.SiteUserValidator; import com.example.solidconnection.type.CountryCode; +import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.type.RegionCode; import com.example.solidconnection.university.dto.LanguageRequirementDto; import com.example.solidconnection.university.dto.LikedResultDto; @@ -131,20 +129,24 @@ public UniversityDetailDto getDetail(Long universityInfoForApplyId) { } - public List search(String region, String keyword) { + public List search(String region, List keywords, LanguageTestType testType, String testScore) { RegionCode regionCode = null; if (region != null && !region.isBlank()) { regionCode = RegionCode.getRegionCodeByKoreanName(region); } List countryCodes = null; - if (keyword != null && !keyword.isBlank()) { - countryCodes = CountryCode.getCountryCodeMatchesToKeyword(keyword); + if (keywords != null && !keywords.isEmpty()) { + countryCodes = CountryCode.getCountryCodeMatchesToKeyword(keywords); } - List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, keyword); + List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, keywords); return universities.stream() .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) + .filter(university -> { + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); + return languageRequirementRepository.findByUniversityInfoForApplyAndLanguageTestTypeAndLessThanMyScore(universityInfoForApply, testType, testScore).isPresent(); + }) .map(university -> { UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); return UniversityPreviewDto.fromEntity(universityInfoForApply); diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java index feedce713..520984e3b 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java @@ -3,6 +3,7 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.University; import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.university.repository.LanguageRequirementRepository; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.UniversityRepository; import lombok.RequiredArgsConstructor; @@ -17,6 +18,7 @@ public class UniversityValidator { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final UniversityRepository universityRepository; + private final LanguageRequirementRepository languageRequirementRepository; public UniversityInfoForApply getValidatedUniversityInfoForApplyById(Long id){ return universityInfoForApplyRepository.findByIdAndTerm(id, TERM) From c71493389baedfafc85a783ceb42161d22840256 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sun, 18 Feb 2024 02:25:06 +0900 Subject: [PATCH 050/158] =?UTF-8?q?refactor:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/UniversityController.java | 3 +-- .../LanguageRequirementRepository.java | 2 ++ .../UniversityRepositoryForFilterImpl.java | 4 +--- .../university/service/UniversityService.java | 16 +++++++++++++--- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 18669cdf0..03a081471 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -2,7 +2,6 @@ import com.example.solidconnection.custom.response.CustomResponse; import com.example.solidconnection.custom.response.DataResponse; -import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.dto.LikedResultDto; import com.example.solidconnection.university.dto.UniversityDetailDto; import com.example.solidconnection.university.dto.UniversityPreviewDto; @@ -35,7 +34,7 @@ public CustomResponse search(@RequestParam(required = false, defaultValue = "") @RequestParam(required = false, defaultValue = "") List keyword, @RequestParam(required = false, defaultValue = "") String testType, @RequestParam(required = false, defaultValue = "") String testScore) { - List universityPreviewDto = universityService.search(region, keyword, LanguageTestType.getLanguageTestTypeForString(testType), testScore); + List universityPreviewDto = universityService.search(region, keyword, testType, testScore); return new DataResponse<>(universityPreviewDto); } diff --git a/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java b/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java index 01f3bbcbb..223a9854c 100644 --- a/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java @@ -17,4 +17,6 @@ public interface LanguageRequirementRepository extends JpaRepository findByUniversityInfoForApplyAndLanguageTestTypeAndLessThanMyScore(@Param("universityInfoForApply") UniversityInfoForApply universityInfoForApply, @Param("testType") LanguageTestType testType, @Param("myScore") String myScore); + + boolean existsByUniversityInfoForApplyAndLanguageTestType(UniversityInfoForApply universityInfoForApply, LanguageTestType languageTestType); } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java index c208f5be0..dd6b65825 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java @@ -30,9 +30,7 @@ public List findByRegionAndCountryAndKeyword(RegionCode regionCode, QCountry country = QCountry.country; QRegion region = QRegion.region; - System.out.println(keywords); - System.out.println(countryCodes); - System.out.println(regionCodeEq(regionCode, region).and(keywordContainsInCountryOrName(countryCodes, country, keywords, university))); + // System.out.println(regionCodeEq(regionCode, region).and(keywordContainsInCountryOrName(countryCodes, country, keywords, university))); return queryFactory .selectFrom(university) diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index 76531372c..79b529e75 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -1,7 +1,10 @@ package com.example.solidconnection.university.service; import com.example.solidconnection.constants.GeneralRecommendUniversities; -import com.example.solidconnection.entity.*; +import com.example.solidconnection.entity.LikedUniversity; +import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.entity.University; +import com.example.solidconnection.entity.UniversityInfoForApply; import com.example.solidconnection.home.dto.RecommendedUniversityDto; import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; @@ -129,7 +132,7 @@ public UniversityDetailDto getDetail(Long universityInfoForApplyId) { } - public List search(String region, List keywords, LanguageTestType testType, String testScore) { + public List search(String region, List keywords, String testType, String testScore) { RegionCode regionCode = null; if (region != null && !region.isBlank()) { regionCode = RegionCode.getRegionCodeByKoreanName(region); @@ -145,7 +148,14 @@ public List search(String region, List keywords, L .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) .filter(university -> { UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); - return languageRequirementRepository.findByUniversityInfoForApplyAndLanguageTestTypeAndLessThanMyScore(universityInfoForApply, testType, testScore).isPresent(); + if (!testType.isBlank()) { + LanguageTestType languageTestType = LanguageTestType.getLanguageTestTypeForString(testType); + if (!testScore.isBlank()) { + return languageRequirementRepository.findByUniversityInfoForApplyAndLanguageTestTypeAndLessThanMyScore(universityInfoForApply, languageTestType, testScore).isPresent(); + } + return languageRequirementRepository.existsByUniversityInfoForApplyAndLanguageTestType(universityInfoForApply, languageTestType); + } + return true; }) .map(university -> { UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); From 574ea2c0138278ed93933e9d199eb703a9a240bd Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sun, 18 Feb 2024 14:09:55 +0900 Subject: [PATCH 051/158] Update SecurityConfiguration.java --- .../solidconnection/config/security/SecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 2078aa1de..103157abb 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -29,7 +29,7 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("https://www.solid-connect.net", "http://localhost:8080", "https://www.api.solid-connect.net")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("Authorization", "content-type")); + configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); From b8bd0cb30a94dcbd7afdbf94211452cb6ccaba16 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 20 Feb 2024 19:56:02 +0900 Subject: [PATCH 052/158] =?UTF-8?q?refactor:=20cors=20=ED=97=88=EC=9A=A9?= =?UTF-8?q?=20origin=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/solidconnection/config/cors/WebConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java index 030b68250..9143d6558 100644 --- a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java +++ b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java @@ -10,7 +10,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:8080") + .allowedOrigins("http://localhost:8080", "http://localhost:3000", "https://www.solid-connect.net") .allowedMethods("*") .allowedHeaders("*") .allowCredentials(true); From 67a212c3e5987b0040afe5dddad94b3b0cd61c41 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 20 Feb 2024 19:56:37 +0900 Subject: [PATCH 053/158] =?UTF-8?q?refactor:=20cors=20=ED=97=88=EC=9A=A9?= =?UTF-8?q?=20origin=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/config/security/SecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 103157abb..e9885b8ec 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -27,7 +27,7 @@ public class SecurityConfiguration { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("https://www.solid-connect.net", "http://localhost:8080", "https://www.api.solid-connect.net")); + configuration.setAllowedOrigins(Arrays.asList("https://www.solid-connect.net", "http://localhost:8080", "https://www.api.solid-connect.net", "http://localhost:3000")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); From 29ea6617e36edc3da8bba2a804fce1fd7e97d974 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 20 Feb 2024 20:20:03 +0900 Subject: [PATCH 054/158] =?UTF-8?q?refactor:=20=EB=8C=80=ED=95=99=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=97=90=20term=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=ED=95=B4=EC=84=9C=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ApplicationService.java | 8 ++++---- .../university/dto/UniversityPreviewDto.java | 2 ++ .../university/service/UniversityService.java | 6 +++--- .../university/service/UniversityValidator.java | 11 ++++++++--- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index 6c747c144..798a30bd1 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -70,10 +70,10 @@ public boolean submitUniversityChoice(String email, UniversityRequestDto univers } // 저장에 필요한 엔티티 불러오기 or 생성 - UniversityInfoForApply firstChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyById(universityRequestDto.getFirstChoiceUniversityId()); + UniversityInfoForApply firstChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyByIdAndTerm(universityRequestDto.getFirstChoiceUniversityId()); UniversityInfoForApply secondChoiceUniversity; try { - secondChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyById(universityRequestDto.getSecondChoiceUniversityId()); + secondChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyByIdAndTerm(universityRequestDto.getSecondChoiceUniversityId()); } catch (Exception e) { secondChoiceUniversity = null; } @@ -148,7 +148,7 @@ private List getFirstChoiceApplicants(List return universities.stream() .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) .map(university -> { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversityAndTerm(university); List firstChoiceApplication = applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatus(universityInfoForApply, VerifyStatus.APPROVED); List firstChoiceApplicant = firstChoiceApplication.stream() .map(ap -> ApplicantDto.fromEntity(ap, Objects.equals(siteUser.getId(), ap.getSiteUser().getId()))) @@ -168,7 +168,7 @@ private List getSecondChoiceApplicants(List return universities.stream() .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) .map(university -> { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversityAndTerm(university); List secondChoiceApplication = applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatus(universityInfoForApply, VerifyStatus.APPROVED); List secondChoiceApplicant = secondChoiceApplication.stream() .map(ap -> ApplicantDto.fromEntity(ap, Objects.equals(siteUser.getId(), ap.getSiteUser().getId()))) diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java b/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java index 274a740d1..13b90ea9e 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java @@ -14,6 +14,7 @@ @AllArgsConstructor public class UniversityPreviewDto { private long id; + private String term; private String koreanName; private String region; private String country; @@ -24,6 +25,7 @@ public class UniversityPreviewDto { public static UniversityPreviewDto fromEntity(UniversityInfoForApply universityInfoForApply) { return UniversityPreviewDto.builder() .id(universityInfoForApply.getId()) + .term(universityInfoForApply.getTerm()) .region(universityInfoForApply.getUniversity().getRegion().getCode().getKoreanName()) .country(universityInfoForApply.getUniversity().getCountry().getCode().getKoreanName()) .logoImageUrl(universityInfoForApply.getUniversity().getLogoImageUrl()) diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index 79b529e75..92053af22 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -56,7 +56,7 @@ public List getPersonalRecommends(String email) { .stream().map(interestedRegion -> interestedRegion.getRegion().getCode()) .toList(); List recommendedUniversities = new java.util.ArrayList<>(universityRepository.findByCountryCodeInOrRegionCodeIn(interestedCountries, interestedRegions) - .stream().map(universityValidator::getValidatedUniversityInfoForApplyByUniversity) + .stream().map(universityValidator::getValidatedUniversityInfoForApplyByUniversityAndTerm) .toList()); Collections.shuffle(recommendedUniversities); @@ -147,7 +147,7 @@ public List search(String region, List keywords, S return universities.stream() .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) .filter(university -> { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversityAndTerm(university); if (!testType.isBlank()) { LanguageTestType languageTestType = LanguageTestType.getLanguageTestTypeForString(testType); if (!testScore.isBlank()) { @@ -158,7 +158,7 @@ public List search(String region, List keywords, S return true; }) .map(university -> { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversity(university); + UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversityAndTerm(university); return UniversityPreviewDto.fromEntity(universityInfoForApply); }) .toList(); diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java index 520984e3b..213a23a87 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java @@ -20,17 +20,22 @@ public class UniversityValidator { private final UniversityRepository universityRepository; private final LanguageRequirementRepository languageRequirementRepository; - public UniversityInfoForApply getValidatedUniversityInfoForApplyById(Long id){ + public UniversityInfoForApply getValidatedUniversityInfoForApplyByIdAndTerm(Long id) { return universityInfoForApplyRepository.findByIdAndTerm(id, TERM) .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); } - public UniversityInfoForApply getValidatedUniversityInfoForApplyByUniversity(University university){ + public UniversityInfoForApply getValidatedUniversityInfoForApplyById(Long id) { + return universityInfoForApplyRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); + } + + public UniversityInfoForApply getValidatedUniversityInfoForApplyByUniversityAndTerm(University university) { return universityInfoForApplyRepository.findByUniversityAndTerm(university, TERM) .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); } - public University getValidatedUniversityById(Long id){ + public University getValidatedUniversityById(Long id) { return universityRepository.findById(id) .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); } From 5ff3f8a79dd89a03a169e9137894b7eaf48c79c5 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 20 Feb 2024 20:31:05 +0900 Subject: [PATCH 055/158] =?UTF-8?q?refactor:=20=EC=84=B1=EC=A0=81=EB=A7=8C?= =?UTF-8?q?=20=EC=A0=9C=EC=B6=9C=ED=95=98=EA=B3=A0,=20=EB=8C=80=ED=95=99?= =?UTF-8?q?=EC=9D=80=20=EC=9E=85=EB=A0=A5=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EC=83=81=ED=83=9C=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ApplicationService.java | 3 +++ .../solidconnection/type/ApplicationStatusResponse.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index 798a30bd1..1a107198a 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -191,6 +191,9 @@ public VerifyStatusDto getVerifyStatus(String email) { if (application.isEmpty()) { return new VerifyStatusDto(ApplicationStatusResponse.NOT_SUBMITTED.name()); } + if( application.get().getVerifyStatus() != VerifyStatus.REJECTED && application.get().getFirstChoiceUniversity() == null) { + return new VerifyStatusDto(ApplicationStatusResponse.SCORE_SUBMITTED.name(), 0); + } int updateCount = application.get().getUpdateCount(); if (application.get().getVerifyStatus() == VerifyStatus.PENDING) { diff --git a/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java b/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java index 1df41df07..30794c62c 100644 --- a/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java +++ b/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java @@ -1,5 +1,5 @@ package com.example.solidconnection.type; public enum ApplicationStatusResponse { - NOT_SUBMITTED, SUBMITTED_PENDING, SUBMITTED_REJECTED, SUBMITTED_APPROVED + NOT_SUBMITTED, SUBMITTED_PENDING, SUBMITTED_REJECTED, SUBMITTED_APPROVED, SCORE_SUBMITTED } From efb9288d4971e148d4b87d3bc460b0eb549c4709 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Sat, 2 Mar 2024 22:06:23 +0900 Subject: [PATCH 056/158] =?UTF-8?q?refactor:=20=EC=84=B1=EC=A0=81=EB=A7=8C?= =?UTF-8?q?=20=EC=A0=9C=EC=B6=9C=ED=95=98=EA=B3=A0,=20=EB=8C=80=ED=95=99?= =?UTF-8?q?=EC=9D=80=20=EC=9E=85=EB=A0=A5=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EC=83=81=ED=83=9C=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ApplicationService.java | 37 +++++++++++++++---- .../solidconnection/constants/Constants.java | 2 +- .../solidconnection/entity/Application.java | 11 +++++- .../type/ApplicationStatusResponse.java | 7 +++- .../UniversityInfoForApplyRepository.java | 5 +++ .../university/service/UniversityService.java | 5 ++- .../service/UniversityValidator.java | 5 +++ 7 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index 1a107198a..b2eb16e06 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -63,11 +63,7 @@ public boolean submitScore(String email, ScoreRequestDto scoreRequestDto) { } public boolean submitUniversityChoice(String email, UniversityRequestDto universityRequestDto) { - // 수정 횟수 초과 에러 처리 - Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); - if (application.getUpdateCount() > APPLICATION_UPDATE_COUNT_LIMIT) { - throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); - } + SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); // 저장에 필요한 엔티티 불러오기 or 생성 UniversityInfoForApply firstChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyByIdAndTerm(universityRequestDto.getFirstChoiceUniversityId()); @@ -83,6 +79,21 @@ public boolean submitUniversityChoice(String email, UniversityRequestDto univers throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); } + Application application; + + // 대학 최초 등록이면 + if (applicationRepository.findBySiteUser_Email(siteUser.getEmail()).isEmpty()) { + application = Application.saveUniversity(siteUser, firstChoiceUniversity, secondChoiceUniversity); + application.setNicknameForApply(makeRandomNickname()); + applicationRepository.save(application); + return true; + } + + // 수정 횟수 초과 에러 처리 + application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); + if (application.getUpdateCount() > APPLICATION_UPDATE_COUNT_LIMIT) { + throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); + } // 수정이면 update count 1 증가 if (application.getFirstChoiceUniversity() != null) { @@ -188,20 +199,30 @@ public VerifyStatusDto getVerifyStatus(String email) { SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); Optional application = applicationRepository.findBySiteUser_Email(siteUser.getEmail()); + // 아무것도 제출 안함 if (application.isEmpty()) { return new VerifyStatusDto(ApplicationStatusResponse.NOT_SUBMITTED.name()); } - if( application.get().getVerifyStatus() != VerifyStatus.REJECTED && application.get().getFirstChoiceUniversity() == null) { - return new VerifyStatusDto(ApplicationStatusResponse.SCORE_SUBMITTED.name(), 0); - } int updateCount = application.get().getUpdateCount(); + // 제출한 상태 if (application.get().getVerifyStatus() == VerifyStatus.PENDING) { + // 지망 대학만 제출 + if (application.get().getGpaReportUrl() == null) { + return new VerifyStatusDto(ApplicationStatusResponse.SCORE_SUBMITTED.name(), updateCount); + } + // 성적만 제출 + if (application.get().getFirstChoiceUniversity() == null) { + return new VerifyStatusDto(ApplicationStatusResponse.SCORE_SUBMITTED.name(), 0); + } + // 성적 승인 대기 중 return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_PENDING.name(), updateCount); } + // 성적 승인 반려 if (application.get().getVerifyStatus() == VerifyStatus.REJECTED) { return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_REJECTED.name(), updateCount); } + // 성적 승인 완료 return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_APPROVED.name(), updateCount); } } diff --git a/src/main/java/com/example/solidconnection/constants/Constants.java b/src/main/java/com/example/solidconnection/constants/Constants.java index 6650e798c..97f03edb3 100644 --- a/src/main/java/com/example/solidconnection/constants/Constants.java +++ b/src/main/java/com/example/solidconnection/constants/Constants.java @@ -4,5 +4,5 @@ public class Constants { public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; public static final int APPLICATION_UPDATE_COUNT_LIMIT = 3; public final static int RECOMMEND_UNIVERSITY_NUM = 6; - public final static String TERM = "2024-2"; //TODO: 2024-2로 수정 필요 + public final static String TERM = "2024-2"; } diff --git a/src/main/java/com/example/solidconnection/entity/Application.java b/src/main/java/com/example/solidconnection/entity/Application.java index 131b99e78..363a2eee3 100644 --- a/src/main/java/com/example/solidconnection/entity/Application.java +++ b/src/main/java/com/example/solidconnection/entity/Application.java @@ -61,7 +61,7 @@ public class Application { @JoinColumn(name = "site_user_id") private SiteUser siteUser; - public static Application saveScore(SiteUser siteUser, ScoreRequestDto scoreRequestDto){ + public static Application saveScore(SiteUser siteUser, ScoreRequestDto scoreRequestDto) { return Application.builder() .siteUser(siteUser) .languageTestType(scoreRequestDto.getLanguageTestType()) @@ -73,4 +73,13 @@ public static Application saveScore(SiteUser siteUser, ScoreRequestDto scoreRequ .verifyStatus(VerifyStatus.PENDING) .build(); } + + public static Application saveUniversity(SiteUser siteUser, UniversityInfoForApply firstChoiceUniversity, + UniversityInfoForApply secondChoiceUniversity) { + return Application.builder() + .siteUser(siteUser) + .firstChoiceUniversity(firstChoiceUniversity) + .secondChoiceUniversity(secondChoiceUniversity) + .build(); + } } diff --git a/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java b/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java index 30794c62c..b6c6fcfee 100644 --- a/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java +++ b/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java @@ -1,5 +1,10 @@ package com.example.solidconnection.type; public enum ApplicationStatusResponse { - NOT_SUBMITTED, SUBMITTED_PENDING, SUBMITTED_REJECTED, SUBMITTED_APPROVED, SCORE_SUBMITTED + NOT_SUBMITTED, // 어떤 것도 제출하지 않음 + COLLEGE_SUBMITTED, // 지망 대학만 제출 + SCORE_SUBMITTED, // 성적만 제출 + SUBMITTED_PENDING, // 성적 인증 대기 중 + SUBMITTED_REJECTED, // 성적 인증 승인 완료 + SUBMITTED_APPROVED // 성적 인증 반려 } diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index 8153fa291..f7a5c6c09 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -10,7 +10,12 @@ @Repository public interface UniversityInfoForApplyRepository extends JpaRepository { Optional findByUniversityAndTerm(University university, String term); + Boolean existsByUniversityAndTerm(University university, String term); + Optional findByUniversity_KoreanNameAndTerm(String koreanName, String term); + Optional findByIdAndTerm(Long id, String term); + + Optional findByUniversity(University university); } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index 92053af22..eb805abd2 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -56,7 +57,9 @@ public List getPersonalRecommends(String email) { .stream().map(interestedRegion -> interestedRegion.getRegion().getCode()) .toList(); List recommendedUniversities = new java.util.ArrayList<>(universityRepository.findByCountryCodeInOrRegionCodeIn(interestedCountries, interestedRegions) - .stream().map(universityValidator::getValidatedUniversityInfoForApplyByUniversityAndTerm) + .stream() + .map(universityValidator::getValidatedUniversityInfoForApplyByUniversityAndTermNoException) + .filter(Objects::nonNull) .toList()); Collections.shuffle(recommendedUniversities); diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java index 213a23a87..ca3e82cee 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java @@ -35,6 +35,11 @@ public UniversityInfoForApply getValidatedUniversityInfoForApplyByUniversityAndT .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); } + public UniversityInfoForApply getValidatedUniversityInfoForApplyByUniversityAndTermNoException(University university) { + return universityInfoForApplyRepository.findByUniversityAndTerm(university, TERM) + .orElse(null); + } + public University getValidatedUniversityById(Long id) { return universityRepository.findById(id) .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); From ee8eda85fa59abe535d888a4375545cc18b32f9c Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Sun, 3 Mar 2024 19:28:08 +0900 Subject: [PATCH 057/158] =?UTF-8?q?fix:=20application=20status=20=EC=A7=80?= =?UTF-8?q?=EB=A7=9D=20=EB=8C=80=ED=95=99=EB=A7=8C=20=EC=A0=9C=EC=B6=9C=20?= =?UTF-8?q?=EC=8B=9C=20status=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/application/service/ApplicationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java index b2eb16e06..de2bfe773 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java @@ -209,7 +209,7 @@ public VerifyStatusDto getVerifyStatus(String email) { if (application.get().getVerifyStatus() == VerifyStatus.PENDING) { // 지망 대학만 제출 if (application.get().getGpaReportUrl() == null) { - return new VerifyStatusDto(ApplicationStatusResponse.SCORE_SUBMITTED.name(), updateCount); + return new VerifyStatusDto(ApplicationStatusResponse.COLLEGE_SUBMITTED.name(), updateCount); } // 성적만 제출 if (application.get().getFirstChoiceUniversity() == null) { From 522dc2cd95703d4d87ea7ec8bb52eccf604cd56c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 4 Apr 2024 15:06:05 +0900 Subject: [PATCH 058/158] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/constants/Constants.java | 2 +- .../constants/GeneralRecommendUniversities.java | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/constants/Constants.java b/src/main/java/com/example/solidconnection/constants/Constants.java index 97f03edb3..60e487b95 100644 --- a/src/main/java/com/example/solidconnection/constants/Constants.java +++ b/src/main/java/com/example/solidconnection/constants/Constants.java @@ -4,5 +4,5 @@ public class Constants { public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; public static final int APPLICATION_UPDATE_COUNT_LIMIT = 3; public final static int RECOMMEND_UNIVERSITY_NUM = 6; - public final static String TERM = "2024-2"; + public final static String TERM = "2024-2-a"; } diff --git a/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java index d03569637..43975db9c 100644 --- a/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java @@ -15,12 +15,21 @@ @RequiredArgsConstructor public class GeneralRecommendUniversities { // 기본 추천 대학 - 국문명 - public final static String RECOMMEND_UNIVERSITY_1 = "네바다주립대학 라스베이거스(B형)"; +/* public final static String RECOMMEND_UNIVERSITY_1 = "네바다주립대학 라스베이거스(B형)"; public final static String RECOMMEND_UNIVERSITY_2 = "바덴뷔르템베르크 산학협력대학"; public final static String RECOMMEND_UNIVERSITY_3 = "릴 가톨릭 대학"; public final static String RECOMMEND_UNIVERSITY_4 = "그라츠공과대학"; public final static String RECOMMEND_UNIVERSITY_5 = "RMIT멜버른공과대학(A형)"; - public final static String RECOMMEND_UNIVERSITY_6 = "오스트라바 대학"; + public final static String RECOMMEND_UNIVERSITY_6 = "오스트라바 대학";*/ + + // 2024-2 추가선발 + public final static String RECOMMEND_UNIVERSITY_1 = "밀라노공과대학"; + public final static String RECOMMEND_UNIVERSITY_2 = "파리8대학교"; + public final static String RECOMMEND_UNIVERSITY_3 = "안젤로주립대학(A형)"; + public final static String RECOMMEND_UNIVERSITY_4 = "제플린대학"; + public final static String RECOMMEND_UNIVERSITY_5 = "리스본대학 경영학과"; + public final static String RECOMMEND_UNIVERSITY_6 = "몽펠리에 대학교"; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private List recommendedUniversities; From 3f5aa6d831b15d16ad81074a3c997b796c712f00 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:50:02 +0900 Subject: [PATCH 059/158] =?UTF-8?q?Feat:=20docker-compose=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80:=20redis,=20nginx=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: docker-compose 추가: redis, nginx * Feat: update nginx.conf to support https connect, and +a * Fix: nginx.conf 오류 수정 * Chore: nginx 컨테이너에 인증서 volume 추가 * Chore: nginx 443 port 추가 --- .gitignore | 3 ++- docker-compose.yml | 33 +++++++++++++++++++++++++++++++++ nginx.conf | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 docker-compose.yml create mode 100644 nginx.conf diff --git a/.gitignore b/.gitignore index 488d587d8..aedec0c70 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ out/ .vscode/ ### YML ### -*.yml \ No newline at end of file +*.yml +!docker-compose.yml \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..86e7e3a1e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + redis: + image: redis:latest + container_name: redis + ports: + - "6379:6379" + + solid-connect-server: + build: + context: . + dockerfile: Dockerfile + container_name: solid-connect-server + ports: + - "8080:8080" + environment: + - SPRING_DATA_REDIS_HOST=redis + - SPRING_DATA_REDIS_PORT=6379 + depends_on: + - redis + + nginx: + image: nginx:latest + container_name: nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf + - /etc/letsencrypt:/etc/letsencrypt + depends_on: + - solid-connect-server diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 000000000..0be40fd00 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,43 @@ +server { + listen 80; + +# http를 사용하는 경우 주석 해제 +# location / { +# proxy_pass http://solid-connect-server:8080; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + + ssl_certificate /etc/letsencrypt/live/api.solid-connect.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.solid-connect.net/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; # 클라이언트 보다 서버의 암호화 알고리즘을 우선하도록 설정 + ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"; + ssl_session_cache shared:SSL:10m; # SSL 세션 캐시 설정 + ssl_session_timeout 10m; + ssl_stapling on; # OCSP 스테이플링 활성화 + ssl_stapling_verify on; + + location / { + proxy_pass http://solid-connect-server:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~ /.well-known/acme-challenge { # 인증서 갱신에 필요한 경로 설정 + allow all; + } +} \ No newline at end of file From 5f30d06978fff5dc207b2ab7792c2c61ae63ac83 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sun, 14 Jul 2024 02:27:21 +0900 Subject: [PATCH 060/158] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 컨트롤러 코드 개선 - 함수 이름을 동사구로 통일 - 어노테이션 순서 통일 - 개행 통일 * refactor: Validator 클래스 제거 레포지토리에서 단순 조회를 하고, 없으면 예외를 던지기 위해 생성했던 Validator 클래스를 없애고, 레포지토리의 default 함수로 만들어주었다. * refactor: 닉네임 랜덤 생성 클래스 분리 - 랜덤으로 닉네임을 생성하는 함수와, 그 재료들이 한 클래스에 위치하도록 변경 * refactor: 지원 정보 제출 코드 개선 - 지원 정보를 담당하는 Service 클래스를 QueryService 와 SubmissionService 로 분리 * refactor: embedded 활용 - JPA의 embedded를 활용해서 도메인 객체의 응집도를 높인다. * test: 데이터베이스 연결 및 테이블 존재 여부 테스트 - 테스트 시, 인메모리 데이터베이스 H2를 사용하도록 설정한다. * test: 레디스 연결 테스트 * chore: 주석 추가 * style: 주석 추가 및 줄바꿈 통일 * refactor: 로그인, 회원가입 로직 분리 - AuthService 에서 로그인과 회원가입 로직을 각각 SignInService 와 SignUpService로 분리한다. - SignInService 에 주석을 추가한다. - SignInService 와 KakaoOAuthService 의 코드를 개선한다. * test: 테스트 데이터 초기화 Extension 생성 - 인수 테스트와 통합 테스트를 위해 매번 DB를 초기화해준다. * refactor: 카카오 사용자 정보를 담는 dto 개선 - 여러 클래스로 나누지 않고, 중첩 클래스를 사용한다. * refactor: SiteUser 생성자 추가 - 빌더를 사용하는 방식은 초기화 필수 데이터가 누락될 수 있다. 또한 생성자보다 비용이 더 많이 든다. - 따라서 생성자를 사용하는 방법으로 바꾼다. * refactor: dto 형식 통일 - 요청-응답에 해당하는 dto 는 각각 Request, Response 를 사용한다. - 어플리케이션 안에서 사용되는 dto 는 Dto를 붙여 표시한다. * test: 로그인 E2E 테스트 작성 * refactor: 토큰 키 생성로직 캡슐화 * test: 탈퇴한 회원의 로그인 테스트 작성 * test: DatabaseCleaner 에 레디스 초기화 로직 추가 * refactor: 카카오 oauth 코드 개선 * refactor: 응답 객체 record로 변경 * chore: 로컬 개발용 데이터 수정 * refactor: 엔티티 코드 개선 - 의미 없는 어노테이션을 삭제한다. - 줄바꿈 형식을 통일한다. * refactor: Country 와 Region 테이블 변경 - 코드의 자료형을 enum 대신 String 으로 바꾼다. - 데이터베이스에 종속적이지 않은 테이블을 만들 수 있다. (enum 은 MySQL만 지원) * refactor: 회원가입 코드 개선 * test: 회원가입 테스트 추가 * refactor: dto 네이밍 컨벤션에 맞게 이름 변경 * refactor: ApplicationQueryService 코드 개선 - 중복 코드를 제거한다. * refactor: dto 를 record 로 변경 * refactor: 승인 상태 조회 로직 분리 - ApplicationQueryService 에서 VerifyStatusQueryService 로 분리한다. * refactor: UniversityService 코드 개선 - 대학 필터링 코드를 query dsl 로 옮겨서 service 코드를 간략화한다. * refactor: 코드 형식 통일 및 개선 - 코드 개행 규칙을 통일한다. - class 를 사용하는 dto 를 record 로 바꿔준다. - 주석을 수정한다. - 안쓰는 클래스를 삭제한다. - 클래스나 함수 이름을 적절히 변경한다. * refactor: 예외 처리 부분에 로깅 추가 * refactor: 예외 코드 분류 * test: 회원가입 시 발생할 수 있는 예외 케이스 추가 * refactor: 추천 대학 조회 로직 분리 * refactor: 사용자 삭제 로직 분리 * refactor: 시험 별 점수 비교 함수 추가 - JLPT 는 유일하게 더 작을수록 좋은 점수이다. * refactor: 환경 별 다른 값을 사용하도록 수정 - yml로 설정한 변수를 잘 받아오도록 static 제거한다. * refactor: 개행 및 형식 통일 * test: 마이페이지 조회 테스트 추가 * test: 마이페이지 수정 테스트 추가 * test: 대학교 좋아요 테스트 추가 * test: 테스트 형식 통일 * test: 지원 성적 제출 테스트 추가 * test: 지원 정보 제출 예외 테스트 추가 * refactor: 함수 이름 변경 * test: 지원자 조회 테스트 추가 * test: 지원 상태 조회 테스트 추가 * test: 대학 검색 테스트 추가 * test: 대학 좋아요 여부 조회 테스트 추가 * feat: 어학 성적 종류대로 정렬해서 응답하는 기능 추가 * feat: 어학 시험 종류만으로 검색했을 때도 검색되게하는 기능 추가 * feat: 어학 시험 종류만으로 검색했을 때도 검색되게하는 기능 추가 * test: 대학 상세 정보 조회 테스트 추가 * style: 코드 재정렬 * feat: Application 컨트롤러 스웨거 데코레이션 추가 * feat: 인증 관련 컨트롤러 스웨거 데코레이션 추가 * feat: 이미지 업로드 스웨거 데코레이션 추가 * feat: SiteUser 관련 스웨거 데코레이션 추가 * feat: University 관련 스웨거 데코레이션 추가 * style: 불필요한 주석 제거 --- README.md | Bin 50 -> 950 bytes build.gradle | 6 +- .../application/domain/QApplication.java | 69 ++ .../application/domain/QGpa.java | 41 + .../application/domain/QLanguageTest.java | 41 + .../solidconnection/entity/QCountry.java | 53 ++ .../entity/QInterestedCountry.java | 54 ++ .../entity/QInterestedRegion.java | 54 ++ .../solidconnection/entity/QRegion.java | 39 + .../siteuser/domain/QSiteUser.java | 55 ++ .../domain/QLanguageRequirement.java | 55 ++ .../university/domain/QLikedUniversity.java | 54 ++ .../university/domain/QUniversity.java | 72 ++ .../domain/QUniversityInfoForApply.java | 79 ++ .../controller/ApplicationController.java | 74 +- .../ApplicationControllerSwagger.java | 104 +++ .../application/domain/Application.java | 88 +++ .../application/domain/Gpa.java | 23 + .../application/domain/LanguageTest.java | 27 + .../application/dto/ApplicantDto.java | 28 - .../application/dto/ApplicantResponse.java | 35 + .../dto/ApplicationSubmissionResponse.java | 10 + .../application/dto/ApplicationsDto.java | 15 - .../application/dto/ApplicationsResponse.java | 16 + .../application/dto/ScoreRequest.java | 51 ++ .../application/dto/ScoreRequestDto.java | 31 - .../dto/UniversityApplicantsDto.java | 18 - .../dto/UniversityApplicantsResponse.java | 34 + .../dto/UniversityChoiceRequest.java | 15 + .../application/dto/UniversityRequestDto.java | 15 - .../application/dto/VerifyStatusDto.java | 17 - .../application/dto/VerifyStatusResponse.java | 13 + .../repository/ApplicationRepository.java | 20 +- .../service/ApplicationQueryService.java | 97 +++ .../service/ApplicationService.java | 228 ------ .../service/ApplicationSubmissionService.java | 108 +++ .../service/ApplicationValidator.java | 20 - .../application/service/NicknameCreator.java | 31 + .../service/VerifyStatusQueryService.java | 70 ++ .../auth/client/KakaoOAuthClient.java | 101 +++ .../auth/controller/AuthController.java | 66 +- .../controller/AuthControllerSwagger.java | 116 +++ .../auth/dto/ReissueResponse.java | 9 + .../auth/dto/ReissueResponseDto.java | 12 - .../auth/dto/SignInResponse.java | 17 + .../auth/dto/SignInResponseDto.java | 15 - .../auth/dto/SignUpRequest.java | 53 ++ .../auth/dto/SignUpRequestDto.java | 27 - .../auth/dto/SignUpResponse.java | 12 + .../auth/dto/SignUpResponseDto.java | 13 - .../auth/dto/kakao/FirstAccessResponse.java | 32 + .../dto/kakao/FirstAccessResponseDto.java | 26 - .../auth/dto/kakao/KakaoAccount.java | 19 - .../auth/dto/kakao/KakaoCodeDto.java | 12 - .../auth/dto/kakao/KakaoCodeRequest.java | 9 + ...sponseDto.java => KakaoOauthResponse.java} | 2 +- .../auth/dto/kakao/KakaoProfile.java | 19 - .../auth/dto/kakao/KakaoTokenDto.java | 18 +- .../auth/dto/kakao/KakaoUserInfoDto.java | 27 +- .../auth/service/AuthService.java | 162 +--- .../auth/service/KakaoOAuthService.java | 121 --- .../auth/service/SignInService.java | 72 ++ .../auth/service/SignUpService.java | 100 +++ .../config/redis/RedisConfig.java | 12 +- .../config/rest/RestTemplateConfig.java | 2 +- .../security/JwtAuthenticationEntryPoint.java | 8 +- .../security/JwtAuthenticationFilter.java | 6 +- .../security/SecurityConfiguration.java | 9 +- .../config/swagger/SwaggerConfig.java | 40 + .../config/token/TokenService.java | 9 +- .../config/token/TokenType.java | 7 +- .../config/token/TokenValidator.java | 19 +- .../solidconnection/constants/Constants.java | 8 - .../GeneralRecommendUniversities.java | 59 -- .../constants/NicknameForApplyWords.java | 16 - .../constants/validMessage.java | 12 - .../custom/exception/CustomException.java | 4 +- .../exception/CustomExceptionHandler.java | 53 +- .../custom/exception/ErrorCode.java | 70 +- .../exception/JwtExpiredTokenException.java | 3 +- .../custom/response/CustomResponse.java | 4 - .../custom/response/DataResponse.java | 13 - .../custom/response/ErrorResponse.java | 23 +- .../custom/response/StatusResponse.java | 12 - .../custom/userdetails/CustomUserDetails.java | 7 +- .../userdetails/CustomUserDetailsService.java | 3 +- .../solidconnection/entity/Application.java | 85 -- .../solidconnection/entity/Country.java | 30 +- .../entity/InterestedCountry.java | 25 +- .../entity/InterestedRegion.java | 25 +- .../entity/LanguageRequirement.java | 25 - .../solidconnection/entity/Region.java | 26 +- .../solidconnection/entity/SiteUser.java | 64 -- .../home/controller/HomeController.java | 31 - .../home/dto/PersonalHomeInfoDto.java | 14 - .../home/dto/RecommendedUniversityDto.java | 23 - .../repositories/CountryRepository.java | 18 +- .../InterestedCountyRepository.java | 2 +- .../InterestedRegionRepository.java | 2 +- .../repositories/RegionRepository.java | 15 +- .../solidconnection/s3/AmazonS3Config.java | 1 + .../solidconnection/s3/S3Controller.java | 36 +- .../s3/S3ControllerSwagger.java | 119 +++ .../example/solidconnection/s3/S3Service.java | 56 +- .../s3/UploadedFileURLDto.java | 11 - .../s3/UploadedFileUrlResponse.java | 9 + .../scheduler/UserRemovalScheduler.java | 25 +- .../siteuser/controller/MyPageController.java | 46 -- .../controller/SiteUserController.java | 47 ++ .../controller/SiteUserControllerSwagger.java | 78 ++ .../siteuser/domain/SiteUser.java | 78 ++ .../siteuser/dto/MyPageDto.java | 34 - .../siteuser/dto/MyPageResponse.java | 42 + .../siteuser/dto/MyPageUpdateDto.java | 27 - .../siteuser/dto/MyPageUpdateRequest.java | 22 + .../siteuser/dto/MyPageUpdateResponse.java | 20 + .../repository/LikedUniversityRepository.java | 9 +- .../repository/SiteUserRepository.java | 15 +- .../siteuser/service/MyPageService.java | 77 -- .../siteuser/service/SiteUserService.java | 94 ++- .../siteuser/service/SiteUserValidator.java | 20 - .../type/ApplicationStatusResponse.java | 10 - .../solidconnection/type/CountryCode.java | 72 -- .../example/solidconnection/type/ImgType.java | 2 +- .../type/LanguageTestType.java | 41 +- .../solidconnection/type/RegionCode.java | 32 - .../type/SemesterAvailableForDispatch.java | 2 +- .../controller/UniversityController.java | 91 ++- .../UniversityControllerSwagger.java | 124 +++ .../domain/LanguageRequirement.java | 37 + .../domain}/LikedUniversity.java | 15 +- .../domain}/University.java | 22 +- .../domain}/UniversityInfoForApply.java | 46 +- .../university/dto/IsLikeResponse.java | 9 + .../dto/LanguageRequirementDto.java | 24 - .../dto/LanguageRequirementResponse.java | 25 + .../university/dto/LikeResultResponse.java | 9 + .../university/dto/LikedResultDto.java | 14 - .../university/dto/UniversityDetailDto.java | 40 - .../dto/UniversityDetailResponse.java | 125 +++ ...UniversityInfoForApplyPreviewResponse.java | 54 ++ .../university/dto/UniversityPreviewDto.java | 40 - .../dto/UniversityRecommendsResponse.java | 12 + .../LanguageRequirementRepository.java | 8 +- .../UniversityInfoForApplyRepository.java | 50 +- .../repository/UniversityRepository.java | 20 +- .../custom/UniversityFilterRepository.java | 15 + .../UniversityFilterRepositoryImpl.java | 112 +++ .../custom/UniversityRepositoryForFilter.java | 11 - .../UniversityRepositoryForFilterImpl.java | 74 -- .../service/GeneralRecommendUniversities.java | 46 ++ .../service/UniversityRecommendService.java | 74 ++ .../university/service/UniversityService.java | 225 ++---- .../service/UniversityValidator.java | 47 -- src/main/resources/data.sql | 738 ++++++------------ .../database/DatabaseConnectionTest.java | 54 ++ .../database/RedisConnectionTest.java | 30 + .../e2e/ApplicantsQueryTest.java | 187 +++++ .../e2e/ApplicationSubmissionTest.java | 218 ++++++ .../solidconnection/e2e/BaseEndToEndTest.java | 23 + .../solidconnection/e2e/DynamicFixture.java | 91 +++ .../solidconnection/e2e/MyPageTest.java | 57 ++ .../solidconnection/e2e/MyPageUpdateTest.java | 138 ++++ .../solidconnection/e2e/SignInTest.java | 135 ++++ .../solidconnection/e2e/SignUpTest.java | 185 +++++ .../e2e/UniversityDataSetUpEndToEndTest.java | 301 +++++++ .../e2e/UniversityDetailTest.java | 88 +++ .../e2e/UniversityLikeTest.java | 150 ++++ .../e2e/UniversitySearchTest.java | 178 +++++ .../e2e/VerifyStatusQueryTest.java | 150 ++++ .../support/DatabaseCleaner.java | 50 ++ .../support/DatabaseClearExtension.java | 19 + 172 files changed, 6146 insertions(+), 2744 deletions(-) create mode 100644 src/main/generated/com/example/solidconnection/application/domain/QApplication.java create mode 100644 src/main/generated/com/example/solidconnection/application/domain/QGpa.java create mode 100644 src/main/generated/com/example/solidconnection/application/domain/QLanguageTest.java create mode 100644 src/main/generated/com/example/solidconnection/entity/QCountry.java create mode 100644 src/main/generated/com/example/solidconnection/entity/QInterestedCountry.java create mode 100644 src/main/generated/com/example/solidconnection/entity/QInterestedRegion.java create mode 100644 src/main/generated/com/example/solidconnection/entity/QRegion.java create mode 100644 src/main/generated/com/example/solidconnection/siteuser/domain/QSiteUser.java create mode 100644 src/main/generated/com/example/solidconnection/university/domain/QLanguageRequirement.java create mode 100644 src/main/generated/com/example/solidconnection/university/domain/QLikedUniversity.java create mode 100644 src/main/generated/com/example/solidconnection/university/domain/QUniversity.java create mode 100644 src/main/generated/com/example/solidconnection/university/domain/QUniversityInfoForApply.java create mode 100644 src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java create mode 100644 src/main/java/com/example/solidconnection/application/domain/Application.java create mode 100644 src/main/java/com/example/solidconnection/application/domain/Gpa.java create mode 100644 src/main/java/com/example/solidconnection/application/domain/LanguageTest.java delete mode 100644 src/main/java/com/example/solidconnection/application/dto/ApplicantDto.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java delete mode 100644 src/main/java/com/example/solidconnection/application/dto/ApplicationsDto.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/ScoreRequest.java delete mode 100644 src/main/java/com/example/solidconnection/application/dto/ScoreRequestDto.java delete mode 100644 src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java delete mode 100644 src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java delete mode 100644 src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java create mode 100644 src/main/java/com/example/solidconnection/application/dto/VerifyStatusResponse.java create mode 100644 src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java delete mode 100644 src/main/java/com/example/solidconnection/application/service/ApplicationService.java create mode 100644 src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java delete mode 100644 src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java create mode 100644 src/main/java/com/example/solidconnection/application/service/NicknameCreator.java create mode 100644 src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java create mode 100644 src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java create mode 100644 src/main/java/com/example/solidconnection/auth/controller/AuthControllerSwagger.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/ReissueResponseDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignUpResponseDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponseDto.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoAccount.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java rename src/main/java/com/example/solidconnection/auth/dto/kakao/{KakaoOauthResponseDto.java => KakaoOauthResponse.java} (59%) delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoProfile.java delete mode 100644 src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/SignInService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/SignUpService.java create mode 100644 src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java delete mode 100644 src/main/java/com/example/solidconnection/constants/Constants.java delete mode 100644 src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java delete mode 100644 src/main/java/com/example/solidconnection/constants/NicknameForApplyWords.java delete mode 100644 src/main/java/com/example/solidconnection/constants/validMessage.java delete mode 100644 src/main/java/com/example/solidconnection/custom/response/CustomResponse.java delete mode 100644 src/main/java/com/example/solidconnection/custom/response/DataResponse.java delete mode 100644 src/main/java/com/example/solidconnection/custom/response/StatusResponse.java delete mode 100644 src/main/java/com/example/solidconnection/entity/Application.java delete mode 100644 src/main/java/com/example/solidconnection/entity/LanguageRequirement.java delete mode 100644 src/main/java/com/example/solidconnection/entity/SiteUser.java delete mode 100644 src/main/java/com/example/solidconnection/home/controller/HomeController.java delete mode 100644 src/main/java/com/example/solidconnection/home/dto/PersonalHomeInfoDto.java delete mode 100644 src/main/java/com/example/solidconnection/home/dto/RecommendedUniversityDto.java create mode 100644 src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java delete mode 100644 src/main/java/com/example/solidconnection/s3/UploadedFileURLDto.java create mode 100644 src/main/java/com/example/solidconnection/s3/UploadedFileUrlResponse.java delete mode 100644 src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java delete mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/MyPageDto.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java delete mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateDto.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateRequest.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java delete mode 100644 src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java delete mode 100644 src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java delete mode 100644 src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java delete mode 100644 src/main/java/com/example/solidconnection/type/CountryCode.java delete mode 100644 src/main/java/com/example/solidconnection/type/RegionCode.java create mode 100644 src/main/java/com/example/solidconnection/university/controller/UniversityControllerSwagger.java create mode 100644 src/main/java/com/example/solidconnection/university/domain/LanguageRequirement.java rename src/main/java/com/example/solidconnection/{entity => university/domain}/LikedUniversity.java (57%) rename src/main/java/com/example/solidconnection/{entity => university/domain}/University.java (60%) rename src/main/java/com/example/solidconnection/{entity => university/domain}/UniversityInfoForApply.java (52%) create mode 100644 src/main/java/com/example/solidconnection/university/dto/IsLikeResponse.java delete mode 100644 src/main/java/com/example/solidconnection/university/dto/LanguageRequirementDto.java create mode 100644 src/main/java/com/example/solidconnection/university/dto/LanguageRequirementResponse.java create mode 100644 src/main/java/com/example/solidconnection/university/dto/LikeResultResponse.java delete mode 100644 src/main/java/com/example/solidconnection/university/dto/LikedResultDto.java delete mode 100644 src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java create mode 100644 src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java create mode 100644 src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java delete mode 100644 src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java create mode 100644 src/main/java/com/example/solidconnection/university/dto/UniversityRecommendsResponse.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepository.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java delete mode 100644 src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java delete mode 100644 src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java create mode 100644 src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java create mode 100644 src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java delete mode 100644 src/main/java/com/example/solidconnection/university/service/UniversityValidator.java create mode 100644 src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java create mode 100644 src/test/java/com/example/solidconnection/database/RedisConnectionTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/DynamicFixture.java create mode 100644 src/test/java/com/example/solidconnection/e2e/MyPageTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/SignInTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/SignUpTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java create mode 100644 src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java create mode 100644 src/test/java/com/example/solidconnection/support/DatabaseCleaner.java create mode 100644 src/test/java/com/example/solidconnection/support/DatabaseClearExtension.java diff --git a/README.md b/README.md index 6bfa33a00e9f4cb8be7136c4656da88556a79961..ef24e835ce7010709de45f879e8dbfcc9bed17a1 100644 GIT binary patch literal 950 zcmZuvO=uHA6#fJcK@cxJ|6U@#AN^Ha}2oZYd!9%Vdgi?KPH%lfmKiT#inDeShh~ zslbV=DS?ari$PWpwM^Cm=4szRVprfMzBvMR@4!ix?H)(0d>L8?T>-=UZKgffJT|Y5 zb9_GJ|6ysh&sp9np&r%WN`d{xv1N53Pu#`!juUtXtU``)V!h!-I~;GQ*A0&Uz9;QW zX{|0$9pu%T7viw5)y?fD2Zqwx%zV@x)nbG*GG$}O6!;`7&QfJ60yplazrySE9bYIu^DSjOrq*Lx1 zPsla%H9>b)efax7d>uTm33FByGfxhRo%+{Mt=zeYlcQP1T0d+-ITvV$Ufdg29cyD2 cFJUxOzIO_D)GsrkQi!&+L&li((I$HQ3r|#3>Hq)$ delta 5 McmdnSZZyFN00ox;vH$=8 diff --git a/build.gradle b/build.gradle index ea69c73e1..b3e68dd00 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ repositories { mavenCentral() } -dependencies { +dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거 implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'com.mysql:mysql-connector-j:8.2.0' @@ -36,9 +36,13 @@ dependencies { implementation 'com.amazonaws:aws-java-sdk-s3:1.12.470' implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.h2database:h2:2.2.224' + testImplementation 'io.rest-assured:rest-assured:5.4.0' implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor( diff --git a/src/main/generated/com/example/solidconnection/application/domain/QApplication.java b/src/main/generated/com/example/solidconnection/application/domain/QApplication.java new file mode 100644 index 000000000..742603411 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/application/domain/QApplication.java @@ -0,0 +1,69 @@ +package com.example.solidconnection.application.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QApplication is a Querydsl query type for Application + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QApplication extends EntityPathBase { + + private static final long serialVersionUID = -122324166L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QApplication application = new QApplication("application"); + + public final com.example.solidconnection.university.domain.QUniversityInfoForApply firstChoiceUniversity; + + public final QGpa gpa; + + public final NumberPath id = createNumber("id", Long.class); + + public final QLanguageTest languageTest; + + public final StringPath nicknameForApply = createString("nicknameForApply"); + + public final com.example.solidconnection.university.domain.QUniversityInfoForApply secondChoiceUniversity; + + public final com.example.solidconnection.siteuser.domain.QSiteUser siteUser; + + public final NumberPath updateCount = createNumber("updateCount", Integer.class); + + public final EnumPath verifyStatus = createEnum("verifyStatus", com.example.solidconnection.type.VerifyStatus.class); + + public QApplication(String variable) { + this(Application.class, forVariable(variable), INITS); + } + + public QApplication(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QApplication(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QApplication(PathMetadata metadata, PathInits inits) { + this(Application.class, metadata, inits); + } + + public QApplication(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.firstChoiceUniversity = inits.isInitialized("firstChoiceUniversity") ? new com.example.solidconnection.university.domain.QUniversityInfoForApply(forProperty("firstChoiceUniversity"), inits.get("firstChoiceUniversity")) : null; + this.gpa = inits.isInitialized("gpa") ? new QGpa(forProperty("gpa")) : null; + this.languageTest = inits.isInitialized("languageTest") ? new QLanguageTest(forProperty("languageTest")) : null; + this.secondChoiceUniversity = inits.isInitialized("secondChoiceUniversity") ? new com.example.solidconnection.university.domain.QUniversityInfoForApply(forProperty("secondChoiceUniversity"), inits.get("secondChoiceUniversity")) : null; + this.siteUser = inits.isInitialized("siteUser") ? new com.example.solidconnection.siteuser.domain.QSiteUser(forProperty("siteUser")) : null; + } + +} + diff --git a/src/main/generated/com/example/solidconnection/application/domain/QGpa.java b/src/main/generated/com/example/solidconnection/application/domain/QGpa.java new file mode 100644 index 000000000..e6f55fd97 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/application/domain/QGpa.java @@ -0,0 +1,41 @@ +package com.example.solidconnection.application.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QGpa is a Querydsl query type for Gpa + */ +@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer") +public class QGpa extends BeanPath { + + private static final long serialVersionUID = -51081086L; + + public static final QGpa gpa1 = new QGpa("gpa1"); + + public final NumberPath gpa = createNumber("gpa", Double.class); + + public final NumberPath gpaCriteria = createNumber("gpaCriteria", Double.class); + + public final StringPath gpaReportUrl = createString("gpaReportUrl"); + + public QGpa(String variable) { + super(Gpa.class, forVariable(variable)); + } + + public QGpa(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QGpa(PathMetadata metadata) { + super(Gpa.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/solidconnection/application/domain/QLanguageTest.java b/src/main/generated/com/example/solidconnection/application/domain/QLanguageTest.java new file mode 100644 index 000000000..d22d113d2 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/application/domain/QLanguageTest.java @@ -0,0 +1,41 @@ +package com.example.solidconnection.application.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QLanguageTest is a Querydsl query type for LanguageTest + */ +@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer") +public class QLanguageTest extends BeanPath { + + private static final long serialVersionUID = 1768826720L; + + public static final QLanguageTest languageTest = new QLanguageTest("languageTest"); + + public final StringPath languageTestReportUrl = createString("languageTestReportUrl"); + + public final StringPath languageTestScore = createString("languageTestScore"); + + public final EnumPath languageTestType = createEnum("languageTestType", com.example.solidconnection.type.LanguageTestType.class); + + public QLanguageTest(String variable) { + super(LanguageTest.class, forVariable(variable)); + } + + public QLanguageTest(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QLanguageTest(PathMetadata metadata) { + super(LanguageTest.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/solidconnection/entity/QCountry.java b/src/main/generated/com/example/solidconnection/entity/QCountry.java new file mode 100644 index 000000000..ab42f8004 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/entity/QCountry.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QCountry is a Querydsl query type for Country + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QCountry extends EntityPathBase { + + private static final long serialVersionUID = -2001953983L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QCountry country = new QCountry("country"); + + public final StringPath code = createString("code"); + + public final StringPath koreanName = createString("koreanName"); + + public final QRegion region; + + public QCountry(String variable) { + this(Country.class, forVariable(variable), INITS); + } + + public QCountry(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QCountry(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QCountry(PathMetadata metadata, PathInits inits) { + this(Country.class, metadata, inits); + } + + public QCountry(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.region = inits.isInitialized("region") ? new QRegion(forProperty("region")) : null; + } + +} + diff --git a/src/main/generated/com/example/solidconnection/entity/QInterestedCountry.java b/src/main/generated/com/example/solidconnection/entity/QInterestedCountry.java new file mode 100644 index 000000000..9c8b3c6d0 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/entity/QInterestedCountry.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QInterestedCountry is a Querydsl query type for InterestedCountry + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QInterestedCountry extends EntityPathBase { + + private static final long serialVersionUID = 1105130488L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QInterestedCountry interestedCountry = new QInterestedCountry("interestedCountry"); + + public final QCountry country; + + public final NumberPath id = createNumber("id", Long.class); + + public final com.example.solidconnection.siteuser.domain.QSiteUser siteUser; + + public QInterestedCountry(String variable) { + this(InterestedCountry.class, forVariable(variable), INITS); + } + + public QInterestedCountry(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QInterestedCountry(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QInterestedCountry(PathMetadata metadata, PathInits inits) { + this(InterestedCountry.class, metadata, inits); + } + + public QInterestedCountry(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.country = inits.isInitialized("country") ? new QCountry(forProperty("country"), inits.get("country")) : null; + this.siteUser = inits.isInitialized("siteUser") ? new com.example.solidconnection.siteuser.domain.QSiteUser(forProperty("siteUser")) : null; + } + +} + diff --git a/src/main/generated/com/example/solidconnection/entity/QInterestedRegion.java b/src/main/generated/com/example/solidconnection/entity/QInterestedRegion.java new file mode 100644 index 000000000..b60554f01 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/entity/QInterestedRegion.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QInterestedRegion is a Querydsl query type for InterestedRegion + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QInterestedRegion extends EntityPathBase { + + private static final long serialVersionUID = -1345685934L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QInterestedRegion interestedRegion = new QInterestedRegion("interestedRegion"); + + public final NumberPath id = createNumber("id", Long.class); + + public final QRegion region; + + public final com.example.solidconnection.siteuser.domain.QSiteUser siteUser; + + public QInterestedRegion(String variable) { + this(InterestedRegion.class, forVariable(variable), INITS); + } + + public QInterestedRegion(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QInterestedRegion(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QInterestedRegion(PathMetadata metadata, PathInits inits) { + this(InterestedRegion.class, metadata, inits); + } + + public QInterestedRegion(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.region = inits.isInitialized("region") ? new QRegion(forProperty("region")) : null; + this.siteUser = inits.isInitialized("siteUser") ? new com.example.solidconnection.siteuser.domain.QSiteUser(forProperty("siteUser")) : null; + } + +} + diff --git a/src/main/generated/com/example/solidconnection/entity/QRegion.java b/src/main/generated/com/example/solidconnection/entity/QRegion.java new file mode 100644 index 000000000..23bfeafb4 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/entity/QRegion.java @@ -0,0 +1,39 @@ +package com.example.solidconnection.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QRegion is a Querydsl query type for Region + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QRegion extends EntityPathBase { + + private static final long serialVersionUID = 1047937513L; + + public static final QRegion region = new QRegion("region"); + + public final StringPath code = createString("code"); + + public final StringPath koreanName = createString("koreanName"); + + public QRegion(String variable) { + super(Region.class, forVariable(variable)); + } + + public QRegion(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QRegion(PathMetadata metadata) { + super(Region.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/solidconnection/siteuser/domain/QSiteUser.java b/src/main/generated/com/example/solidconnection/siteuser/domain/QSiteUser.java new file mode 100644 index 000000000..a1879a555 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/siteuser/domain/QSiteUser.java @@ -0,0 +1,55 @@ +package com.example.solidconnection.siteuser.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QSiteUser is a Querydsl query type for SiteUser + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QSiteUser extends EntityPathBase { + + private static final long serialVersionUID = 1080517302L; + + public static final QSiteUser siteUser = new QSiteUser("siteUser"); + + public final StringPath birth = createString("birth"); + + public final StringPath email = createString("email"); + + public final EnumPath gender = createEnum("gender", com.example.solidconnection.type.Gender.class); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath nickname = createString("nickname"); + + public final DateTimePath nicknameModifiedAt = createDateTime("nicknameModifiedAt", java.time.LocalDateTime.class); + + public final EnumPath preparationStage = createEnum("preparationStage", com.example.solidconnection.type.PreparationStatus.class); + + public final StringPath profileImageUrl = createString("profileImageUrl"); + + public final DatePath quitedAt = createDate("quitedAt", java.time.LocalDate.class); + + public final EnumPath role = createEnum("role", com.example.solidconnection.type.Role.class); + + public QSiteUser(String variable) { + super(SiteUser.class, forVariable(variable)); + } + + public QSiteUser(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QSiteUser(PathMetadata metadata) { + super(SiteUser.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/solidconnection/university/domain/QLanguageRequirement.java b/src/main/generated/com/example/solidconnection/university/domain/QLanguageRequirement.java new file mode 100644 index 000000000..b5b49ddeb --- /dev/null +++ b/src/main/generated/com/example/solidconnection/university/domain/QLanguageRequirement.java @@ -0,0 +1,55 @@ +package com.example.solidconnection.university.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QLanguageRequirement is a Querydsl query type for LanguageRequirement + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QLanguageRequirement extends EntityPathBase { + + private static final long serialVersionUID = 443667787L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QLanguageRequirement languageRequirement = new QLanguageRequirement("languageRequirement"); + + public final NumberPath id = createNumber("id", Long.class); + + public final EnumPath languageTestType = createEnum("languageTestType", com.example.solidconnection.type.LanguageTestType.class); + + public final StringPath minScore = createString("minScore"); + + public final QUniversityInfoForApply universityInfoForApply; + + public QLanguageRequirement(String variable) { + this(LanguageRequirement.class, forVariable(variable), INITS); + } + + public QLanguageRequirement(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QLanguageRequirement(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QLanguageRequirement(PathMetadata metadata, PathInits inits) { + this(LanguageRequirement.class, metadata, inits); + } + + public QLanguageRequirement(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.universityInfoForApply = inits.isInitialized("universityInfoForApply") ? new QUniversityInfoForApply(forProperty("universityInfoForApply"), inits.get("universityInfoForApply")) : null; + } + +} + diff --git a/src/main/generated/com/example/solidconnection/university/domain/QLikedUniversity.java b/src/main/generated/com/example/solidconnection/university/domain/QLikedUniversity.java new file mode 100644 index 000000000..e82fc8c65 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/university/domain/QLikedUniversity.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.university.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QLikedUniversity is a Querydsl query type for LikedUniversity + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QLikedUniversity extends EntityPathBase { + + private static final long serialVersionUID = 142590363L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QLikedUniversity likedUniversity = new QLikedUniversity("likedUniversity"); + + public final NumberPath id = createNumber("id", Long.class); + + public final com.example.solidconnection.siteuser.domain.QSiteUser siteUser; + + public final QUniversityInfoForApply universityInfoForApply; + + public QLikedUniversity(String variable) { + this(LikedUniversity.class, forVariable(variable), INITS); + } + + public QLikedUniversity(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QLikedUniversity(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QLikedUniversity(PathMetadata metadata, PathInits inits) { + this(LikedUniversity.class, metadata, inits); + } + + public QLikedUniversity(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.siteUser = inits.isInitialized("siteUser") ? new com.example.solidconnection.siteuser.domain.QSiteUser(forProperty("siteUser")) : null; + this.universityInfoForApply = inits.isInitialized("universityInfoForApply") ? new QUniversityInfoForApply(forProperty("universityInfoForApply"), inits.get("universityInfoForApply")) : null; + } + +} + diff --git a/src/main/generated/com/example/solidconnection/university/domain/QUniversity.java b/src/main/generated/com/example/solidconnection/university/domain/QUniversity.java new file mode 100644 index 000000000..fee1f2ca4 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/university/domain/QUniversity.java @@ -0,0 +1,72 @@ +package com.example.solidconnection.university.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QUniversity is a Querydsl query type for University + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QUniversity extends EntityPathBase { + + private static final long serialVersionUID = 1195314958L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QUniversity university = new QUniversity("university"); + + public final StringPath accommodationUrl = createString("accommodationUrl"); + + public final StringPath backgroundImageUrl = createString("backgroundImageUrl"); + + public final com.example.solidconnection.entity.QCountry country; + + public final StringPath detailsForLocal = createString("detailsForLocal"); + + public final StringPath englishCourseUrl = createString("englishCourseUrl"); + + public final StringPath englishName = createString("englishName"); + + public final StringPath formatName = createString("formatName"); + + public final StringPath homepageUrl = createString("homepageUrl"); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath koreanName = createString("koreanName"); + + public final StringPath logoImageUrl = createString("logoImageUrl"); + + public final com.example.solidconnection.entity.QRegion region; + + public QUniversity(String variable) { + this(University.class, forVariable(variable), INITS); + } + + public QUniversity(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QUniversity(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QUniversity(PathMetadata metadata, PathInits inits) { + this(University.class, metadata, inits); + } + + public QUniversity(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.country = inits.isInitialized("country") ? new com.example.solidconnection.entity.QCountry(forProperty("country"), inits.get("country")) : null; + this.region = inits.isInitialized("region") ? new com.example.solidconnection.entity.QRegion(forProperty("region")) : null; + } + +} + diff --git a/src/main/generated/com/example/solidconnection/university/domain/QUniversityInfoForApply.java b/src/main/generated/com/example/solidconnection/university/domain/QUniversityInfoForApply.java new file mode 100644 index 000000000..5ad64cfd7 --- /dev/null +++ b/src/main/generated/com/example/solidconnection/university/domain/QUniversityInfoForApply.java @@ -0,0 +1,79 @@ +package com.example.solidconnection.university.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QUniversityInfoForApply is a Querydsl query type for UniversityInfoForApply + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QUniversityInfoForApply extends EntityPathBase { + + private static final long serialVersionUID = 31331617L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QUniversityInfoForApply universityInfoForApply = new QUniversityInfoForApply("universityInfoForApply"); + + public final StringPath details = createString("details"); + + public final StringPath detailsForAccommodation = createString("detailsForAccommodation"); + + public final StringPath detailsForApply = createString("detailsForApply"); + + public final StringPath detailsForEnglishCourse = createString("detailsForEnglishCourse"); + + public final StringPath detailsForLanguage = createString("detailsForLanguage"); + + public final StringPath detailsForMajor = createString("detailsForMajor"); + + public final StringPath gpaRequirement = createString("gpaRequirement"); + + public final StringPath gpaRequirementCriteria = createString("gpaRequirementCriteria"); + + public final NumberPath id = createNumber("id", Long.class); + + public final SetPath languageRequirements = this.createSet("languageRequirements", LanguageRequirement.class, QLanguageRequirement.class, PathInits.DIRECT2); + + public final EnumPath semesterAvailableForDispatch = createEnum("semesterAvailableForDispatch", com.example.solidconnection.type.SemesterAvailableForDispatch.class); + + public final StringPath semesterRequirement = createString("semesterRequirement"); + + public final NumberPath studentCapacity = createNumber("studentCapacity", Integer.class); + + public final StringPath term = createString("term"); + + public final EnumPath tuitionFeeType = createEnum("tuitionFeeType", com.example.solidconnection.type.TuitionFeeType.class); + + public final QUniversity university; + + public QUniversityInfoForApply(String variable) { + this(UniversityInfoForApply.class, forVariable(variable), INITS); + } + + public QUniversityInfoForApply(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QUniversityInfoForApply(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QUniversityInfoForApply(PathMetadata metadata, PathInits inits) { + this(UniversityInfoForApply.class, metadata, inits); + } + + public QUniversityInfoForApply(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.university = inits.isInitialized("university") ? new QUniversity(forProperty("university"), inits.get("university")) : null; + } + +} + diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index c626b4c28..3234e45b4 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -1,49 +1,69 @@ package com.example.solidconnection.application.controller; -import com.example.solidconnection.application.dto.ApplicationsDto; -import com.example.solidconnection.application.dto.ScoreRequestDto; -import com.example.solidconnection.application.dto.UniversityRequestDto; -import com.example.solidconnection.application.dto.VerifyStatusDto; -import com.example.solidconnection.application.service.ApplicationService; -import com.example.solidconnection.custom.response.CustomResponse; -import com.example.solidconnection.custom.response.DataResponse; -import com.example.solidconnection.custom.response.StatusResponse; +import com.example.solidconnection.application.dto.ApplicationSubmissionResponse; +import com.example.solidconnection.application.dto.ApplicationsResponse; +import com.example.solidconnection.application.dto.ScoreRequest; +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.application.dto.VerifyStatusResponse; +import com.example.solidconnection.application.service.ApplicationQueryService; +import com.example.solidconnection.application.service.ApplicationSubmissionService; +import com.example.solidconnection.application.service.VerifyStatusQueryService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.security.Principal; -@RestController -@RequestMapping("/application") @RequiredArgsConstructor -public class ApplicationController { - private final ApplicationService applicationService; +@RequestMapping("/application") +@RestController +public class ApplicationController implements ApplicationControllerSwagger { + + private final ApplicationSubmissionService applicationSubmissionService; + private final ApplicationQueryService applicationQueryService; + private final VerifyStatusQueryService verifyStatusQueryService; @PostMapping("/score") - public CustomResponse submitScore(Principal principal, @Valid @RequestBody ScoreRequestDto scoreRequestDto) { - boolean result = applicationService.submitScore(principal.getName(), scoreRequestDto); - return new StatusResponse(result); + public ResponseEntity submitScore( + Principal principal, + @Valid @RequestBody ScoreRequest scoreRequest) { + boolean result = applicationSubmissionService.submitScore(principal.getName(), scoreRequest); + return ResponseEntity + .status(HttpStatus.OK) + .body(new ApplicationSubmissionResponse(result)); } @PostMapping("/university") - public CustomResponse submitUniversityChoice(Principal principal, @Valid @RequestBody UniversityRequestDto universityRequestDto) { - boolean result = applicationService.submitUniversityChoice(principal.getName(), universityRequestDto); - return new StatusResponse(result); - } + public ResponseEntity submitUniversityChoice( + Principal principal, + @Valid @RequestBody UniversityChoiceRequest universityChoiceRequest) { + boolean result = applicationSubmissionService.submitUniversityChoice(principal.getName(), universityChoiceRequest); + return ResponseEntity + .status(HttpStatus.OK) + .body(new ApplicationSubmissionResponse(result)); + } @GetMapping - public CustomResponse getApplicants( + public ResponseEntity getApplicants( Principal principal, @RequestParam(required = false, defaultValue = "") String region, @RequestParam(required = false, defaultValue = "") String keyword) { - ApplicationsDto result = applicationService.getApplicants(principal.getName(), region, keyword); - return new DataResponse<>(result); + ApplicationsResponse result = applicationQueryService.getApplicants(principal.getName(), region, keyword); + return ResponseEntity + .ok(result); } @GetMapping("/status") - public CustomResponse getVerifyStatus(Principal principal) { - VerifyStatusDto result = applicationService.getVerifyStatus(principal.getName()); - return new DataResponse<>(result); + public ResponseEntity getApplicationVerifyStatus(Principal principal) { + VerifyStatusResponse result = verifyStatusQueryService.getVerifyStatus(principal.getName()); + return ResponseEntity + .ok(result); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java new file mode 100644 index 000000000..f531923ac --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java @@ -0,0 +1,104 @@ +package com.example.solidconnection.application.controller; + +import com.example.solidconnection.application.dto.ApplicationSubmissionResponse; +import com.example.solidconnection.application.dto.ApplicationsResponse; +import com.example.solidconnection.application.dto.ScoreRequest; +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.application.dto.VerifyStatusResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; +import io.swagger.v3.oas.annotations.parameters.RequestBody; + +import java.security.Principal; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@Tag(name = "Application", description = "지원 정보 API") +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public interface ApplicationControllerSwagger { + + @Operation( + summary = "대학 성적과 어학 성적 제출", + requestBody = @RequestBody( + description = "대학 성적과 어학 성적", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ScoreRequest.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "대학 성적과 어학 성적 제출 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApplicationSubmissionResponse.class) + ) + ) + } + ) + ResponseEntity submitScore(Principal principal, @Valid @RequestBody ScoreRequest scoreRequest); + + @Operation( + summary = "지망 대학 제출", + requestBody = @RequestBody( + description = "지망 대학", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UniversityChoiceRequest.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "지망 대학 제출 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApplicationSubmissionResponse.class) + ) + ) + } + ) + ResponseEntity submitUniversityChoice(Principal principal, @Valid @RequestBody UniversityChoiceRequest universityChoiceRequest); + + @Operation( + summary = "지원자 목록 조회", + responses = { + @ApiResponse( + responseCode = "200", + description = "지원자 목록 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApplicationsResponse.class) + ) + ) + } + ) + ResponseEntity getApplicants(Principal principal, @RequestParam(required = false) String region, @RequestParam(required = false) String keyword); + + @Operation( + summary = "성적 승인 상태 확인", + responses = { + @ApiResponse( + responseCode = "200", + description = "성적 승인 상태 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = VerifyStatusResponse.class) + ) + ) + } + ) + ResponseEntity getApplicationVerifyStatus(Principal principal); +} diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java new file mode 100644 index 000000000..b16e6433f --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/domain/Application.java @@ -0,0 +1,88 @@ +package com.example.solidconnection.application.domain; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.VerifyStatus; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import static com.example.solidconnection.type.VerifyStatus.PENDING; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@DynamicUpdate +@DynamicInsert +@Entity +public class Application { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private Gpa gpa; + + @Embedded + private LanguageTest languageTest; + + @Setter + @Column(columnDefinition = "varchar(50) not null default 'PENDING'") + @Enumerated(EnumType.STRING) + private VerifyStatus verifyStatus; + + @Column(length = 100) + private String nicknameForApply; + + @Column(columnDefinition = "int not null default 0") + private Integer updateCount; + + @ManyToOne + private UniversityInfoForApply firstChoiceUniversity; + + @ManyToOne + private UniversityInfoForApply secondChoiceUniversity; + + @ManyToOne + private SiteUser siteUser; + + public Application( + SiteUser siteUser, + Gpa gpa, + LanguageTest languageTest) { + this.siteUser = siteUser; + this.gpa = gpa; + this.languageTest = languageTest; + } + + public void updateGpaAndLanguageTest( + Gpa gpa, + LanguageTest languageTest) { + this.gpa = gpa; + this.languageTest = languageTest; + this.verifyStatus = PENDING; + } + + public void updateUniversityChoice( + UniversityInfoForApply firstChoiceUniversity, + UniversityInfoForApply secondChoiceUniversity, + String nicknameForApply) { + if (this.firstChoiceUniversity != null) { + this.updateCount++; + } + this.firstChoiceUniversity = firstChoiceUniversity; + this.secondChoiceUniversity = secondChoiceUniversity; + this.nicknameForApply = nicknameForApply; + } +} diff --git a/src/main/java/com/example/solidconnection/application/domain/Gpa.java b/src/main/java/com/example/solidconnection/application/domain/Gpa.java new file mode 100644 index 000000000..0c737a7d2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/domain/Gpa.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.application.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@Embeddable +public class Gpa { + + @Column(nullable = false, name = "gpa") + private Double gpa; + + @Column(nullable = false, name = "gpa_creteria") + private Double gpaCriteria; + + @Column(nullable = false, name = "gpa_report_url", length = 500) + private String gpaReportUrl; +} diff --git a/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java b/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java new file mode 100644 index 000000000..a1e579ad8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.application.domain; + +import com.example.solidconnection.type.LanguageTestType; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@Embeddable +public class LanguageTest { + + @Column(nullable = false, name = "language_test_type", length = 10) + @Enumerated(EnumType.STRING) + private LanguageTestType languageTestType; + + @Column(nullable = false, name = "language_test_score") + private String languageTestScore; + + @Column(nullable = false, name = "language_test_report_url", length = 500) + private String languageTestReportUrl; +} diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicantDto.java b/src/main/java/com/example/solidconnection/application/dto/ApplicantDto.java deleted file mode 100644 index 18108a701..000000000 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicantDto.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.solidconnection.application.dto; - -import com.example.solidconnection.entity.Application; -import com.example.solidconnection.type.LanguageTestType; -import lombok.*; - -@Getter -@Setter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class ApplicantDto { - private String nicknameForApply; - private float gpa; - private LanguageTestType testType; - private String testScore; - private boolean isMine; - - public static ApplicantDto fromEntity(Application application, boolean isMine) { - return ApplicantDto.builder() - .nicknameForApply(application.getNicknameForApply()) - .gpa(application.getGpa()) - .testType(application.getLanguageTestType()) - .testScore(application.getLanguageTestScore()) - .isMine(isMine) - .build(); - } -} diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java new file mode 100644 index 000000000..d03f1c9a3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java @@ -0,0 +1,35 @@ +package com.example.solidconnection.application.dto; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.type.LanguageTestType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "지원자") +public record ApplicantResponse( + + @Schema(description = "닉네임", example = "행복한 개발자") + String nicknameForApply, + + @Schema(description = "GPA", example = "3.85") + double gpa, + + @Schema(description = "어학 시험 유형", example = "TOEFL_IBT") + LanguageTestType testType, + + @Schema(description = "어학 시험 점수", example = "110") + String testScore, + + @Schema(description = "현재 사용자가 해당 지원지인지", example = "true") + boolean isMine) { + + public static ApplicantResponse of(Application application, boolean isMine) { + return new ApplicantResponse( + application.getNicknameForApply(), + application.getGpa().getGpa(), + application.getLanguageTest().getLanguageTestType(), + application.getLanguageTest().getLanguageTestScore(), + isMine + ); + } +} diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java new file mode 100644 index 000000000..279f2b150 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "지원 정보 제출 성공 여부") +public record ApplicationSubmissionResponse( + + @Schema(description = "제출 성공 여부", example = "true") + boolean isSuccess) { +} diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationsDto.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationsDto.java deleted file mode 100644 index 8094ba408..000000000 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicationsDto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.solidconnection.application.dto; - -import lombok.*; - -import java.util.List; - -@Getter -@Setter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class ApplicationsDto { - private List firstChoice; - private List secondChoice; -} diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java new file mode 100644 index 000000000..fb93b7ff5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.application.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "1지망과 2지망 대학과 그 대학에 지원한 지원자 정보") +public record ApplicationsResponse( + + @ArraySchema(arraySchema = @Schema(description = "1지망 대학에 지원한 지원자 목록")) + List firstChoice, + + @ArraySchema(arraySchema = @Schema(description = "2지망 대학에 지원한 지원자 목록")) + List secondChoice) { +} diff --git a/src/main/java/com/example/solidconnection/application/dto/ScoreRequest.java b/src/main/java/com/example/solidconnection/application/dto/ScoreRequest.java new file mode 100644 index 000000000..1f17be430 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/ScoreRequest.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.application.dto; + + +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.type.LanguageTestType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "대학 성적과 어학 시험 성적") +public record ScoreRequest( + @NotNull(message = "어학 종류를 입력해주세요.") + @Schema(description = "어학 시험 종류", example = "TOEFL", required = true) + LanguageTestType languageTestType, + + @NotBlank(message = "어학 점수를 입력해주세요.") + @Schema(description = "어학 시험 점수", example = "115", required = true) + String languageTestScore, + + @NotBlank(message = "어학 증명서를 첨부해주세요.") + @Schema(description = "어학 증명서 URL", example = "http://example.com/test-report.pdf", required = true) + String languageTestReportUrl, + + @NotNull(message = "학점을 입력해주세요.") + @Schema(description = "GPA", example = "3.5", required = true) + Double gpa, + + @NotNull(message = "학점 기준을 입력해주세요.") + @Schema(description = "GPA 계산 기준", example = "4.0", required = true) + Double gpaCriteria, + + @NotBlank(message = "대학 성적 증명서를 첨부해주세요.") + @Schema(description = "대학 성적 증명서 URL", example = "http://example.com/gpa-report.pdf", required = true) + String gpaReportUrl) { + + public Gpa toGpa() { + return new Gpa( + this.gpa, + this.gpaCriteria, + this.gpaReportUrl); + } + + public LanguageTest toLanguageTest() { + return new LanguageTest( + this.languageTestType, + this.languageTestScore, + this.languageTestReportUrl + ); + } +} diff --git a/src/main/java/com/example/solidconnection/application/dto/ScoreRequestDto.java b/src/main/java/com/example/solidconnection/application/dto/ScoreRequestDto.java deleted file mode 100644 index 711e33c6b..000000000 --- a/src/main/java/com/example/solidconnection/application/dto/ScoreRequestDto.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.solidconnection.application.dto; - - -import com.example.solidconnection.type.LanguageTestType; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.*; - -import static com.example.solidconnection.constants.validMessage.*; - -@Getter -@Setter -public class ScoreRequestDto { - @NotNull(message = LANGUAGE_TEST_TYPE_NOT_BLANK) - private LanguageTestType languageTestType; - - @NotBlank(message = LANGUAGE_TEST_SCORE_NOT_BLANK) - private String languageTestScore; - - @NotBlank(message = LANGUAGE_TEST_REPORT_URL_NOT_BLANK) - private String languageTestReportUrl; - - @NotNull(message = GPA_NOT_BLANK) - private Float gpa; - - @NotNull(message = GPA_CRITERIA_NOT_BLANK) - private Float gpaCriteria; - - @NotBlank(message = GPA_REPORT_URL_NOT_BLANK) - private String gpaReportUrl; -} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java deleted file mode 100644 index ddeb56e99..000000000 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsDto.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.solidconnection.application.dto; - -import lombok.*; - -import java.util.List; - -@Getter -@Setter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class UniversityApplicantsDto { - private String koreanName; - private int studentCapacity; - private String region; - private String country; - private List applicants; -} diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java new file mode 100644 index 000000000..a78c98c20 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.application.dto; + +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "대학과 그 대학에 지원한 지원자 정보") +public record UniversityApplicantsResponse( + @Schema(description = "대학의 한국어 이름", example = "괌대학") + String koreanName, + + @Schema(description = "선발 인원", example = "4") + int studentCapacity, + + @Schema(description = "지역", example = "영미권") + String region, + + @Schema(description = "국가", example = "미국") + String country, + + @ArraySchema(schema = @Schema(description = "지원자 목록", implementation = ApplicantResponse.class)) + List applicants) { + + public static UniversityApplicantsResponse of(UniversityInfoForApply universityInfoForApply, List applicant) { + return new UniversityApplicantsResponse( + universityInfoForApply.getUniversity().getKoreanName(), + universityInfoForApply.getStudentCapacity(), + universityInfoForApply.getUniversity().getRegion().getKoreanName(), + universityInfoForApply.getUniversity().getCountry().getKoreanName(), + applicant); + } +} diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java new file mode 100644 index 000000000..b179404c8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "지망 대학") +public record UniversityChoiceRequest( + + @NotNull(message = "1지망 대학교를 입력해주세요.") + @Schema(description = "1지망 대학교의 지원 정보 ID", example = "1") + Long firstChoiceUniversityId, + + @Schema(description = "2지망 대학교의 지원 정보 ID (선택사항)", example = "2", nullable = true) + Long secondChoiceUniversityId) { +} diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java b/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java deleted file mode 100644 index 3399b4cf4..000000000 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityRequestDto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.solidconnection.application.dto; - -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; - -import static com.example.solidconnection.constants.validMessage.FIRST_CHOICE_UNIVERSITY_ID_NOT_BLANK; - -@Getter -@Setter -public class UniversityRequestDto { - @NotNull(message = FIRST_CHOICE_UNIVERSITY_ID_NOT_BLANK) - private Long firstChoiceUniversityId; - private Long secondChoiceUniversityId; -} diff --git a/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java b/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java deleted file mode 100644 index e636e3d75..000000000 --- a/src/main/java/com/example/solidconnection/application/dto/VerifyStatusDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.solidconnection.application.dto; - -import lombok.*; - -@Getter -@Setter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class VerifyStatusDto { - private String status; - private int updateCount = 0; - - public VerifyStatusDto(String status){ - this.status = status; - } -} diff --git a/src/main/java/com/example/solidconnection/application/dto/VerifyStatusResponse.java b/src/main/java/com/example/solidconnection/application/dto/VerifyStatusResponse.java new file mode 100644 index 000000000..8019e9f8e --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/VerifyStatusResponse.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "지원 상태와 지망 대학 변경 횟수") +public record VerifyStatusResponse( + + @Schema(description = "지원 상태", example = "SUBMITTED_PENDING") + String status, + + @Schema(description = "지망 대학 변경 횟수", example = "1") + int updateCount) { +} diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java index c2b4b90a6..3cbf7c88f 100644 --- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -1,19 +1,33 @@ package com.example.solidconnection.application.repository; -import com.example.solidconnection.entity.Application; -import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.VerifyStatus; +import com.example.solidconnection.university.domain.UniversityInfoForApply; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLICATION_NOT_FOUND; + @Repository public interface ApplicationRepository extends JpaRepository { - boolean existsBySiteUser_Email(String email); + boolean existsByNicknameForApply(String nicknameForApply); + Optional findBySiteUser_Email(String email); + + Optional findBySiteUser(SiteUser siteUser); + List findAllByFirstChoiceUniversityAndVerifyStatus(UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus); + List findAllBySecondChoiceUniversityAndVerifyStatus(UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus); + + default Application getApplicationBySiteUser(SiteUser siteUser) { + return findBySiteUser(siteUser) + .orElseThrow(() -> new CustomException(APPLICATION_NOT_FOUND)); + } } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java new file mode 100644 index 000000000..e4f314225 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -0,0 +1,97 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.dto.ApplicantResponse; +import com.example.solidconnection.application.dto.ApplicationsResponse; +import com.example.solidconnection.application.dto.UniversityApplicantsResponse; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.VerifyStatus; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.custom.UniversityFilterRepositoryImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static com.example.solidconnection.custom.exception.ErrorCode.APPLICATION_NOT_APPROVED; + +@RequiredArgsConstructor +@Service +public class ApplicationQueryService { + + private final ApplicationRepository applicationRepository; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final SiteUserRepository siteUserRepository; + private final UniversityFilterRepositoryImpl universityFilterRepository; + @Value("${university.term}") + public String term; + + /* + * 다른 지원자들의 성적을 조회한다. + * - 유저가 다른 지원자들을 볼 수 있는지 검증한다. + * - 지역과 키워드를 통해 대학을 필터링한다. + * - 1지망, 2지망 지원자들을 조회한다. + * */ + @Transactional(readOnly = true) + public ApplicationsResponse getApplicants(String email, String regionCode, String keyword) { + // 유저가 다른 지원자들을 볼 수 있는지 검증 + SiteUser siteUser = siteUserRepository.getByEmail(email); + validateSiteUserCanViewApplicants(siteUser); + + // 국가와 키워드와 지역을 통해 대학을 필터링한다. + List universities + = universityFilterRepository.findByRegionCodeAndKeywords(regionCode, List.of(keyword)); + + // 1지망, 2지망 지원자들을 조회한다. + List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser); + List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser); + return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants); + } + + private void validateSiteUserCanViewApplicants(SiteUser siteUser) { + VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUser(siteUser).getVerifyStatus(); + if (verifyStatus != VerifyStatus.APPROVED) { + throw new CustomException(APPLICATION_NOT_APPROVED); + } + } + + private List getFirstChoiceApplicants(List universities, SiteUser siteUser) { + return getApplicantsByChoice( + universities, + siteUser, + uia -> applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatus(uia, VerifyStatus.APPROVED) + ); + } + + private List getSecondChoiceApplicants(List universities, SiteUser siteUser) { + return getApplicantsByChoice( + universities, + siteUser, + uia -> applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatus(uia, VerifyStatus.APPROVED) + ); + } + + private List getApplicantsByChoice( + List searchedUniversities, + SiteUser siteUser, + Function> findApplicationsByChoice) { + return universityInfoForApplyRepository.findByUniversitiesAndTerm(searchedUniversities, term).stream() + .map(universityInfoForApply -> UniversityApplicantsResponse.of( + universityInfoForApply, + findApplicationsByChoice.apply(universityInfoForApply).stream() + .map(ap -> ApplicantResponse.of( + ap, + Objects.equals(siteUser.getId(), ap.getSiteUser().getId()))) + .toList())) + .toList(); + } +} diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationService.java deleted file mode 100644 index de2bfe773..000000000 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationService.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.example.solidconnection.application.service; - -import com.example.solidconnection.application.dto.*; -import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.constants.NicknameForApplyWords; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.Application; -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.entity.University; -import com.example.solidconnection.entity.UniversityInfoForApply; -import com.example.solidconnection.siteuser.service.SiteUserValidator; -import com.example.solidconnection.type.ApplicationStatusResponse; -import com.example.solidconnection.type.CountryCode; -import com.example.solidconnection.type.RegionCode; -import com.example.solidconnection.type.VerifyStatus; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import com.example.solidconnection.university.repository.custom.UniversityRepositoryForFilterImpl; -import com.example.solidconnection.university.service.UniversityValidator; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Random; - -import static com.example.solidconnection.constants.Constants.APPLICATION_UPDATE_COUNT_LIMIT; -import static com.example.solidconnection.constants.Constants.TERM; -import static com.example.solidconnection.custom.exception.ErrorCode.*; - -@Service -@RequiredArgsConstructor -@Transactional -public class ApplicationService { - private final ApplicationRepository applicationRepository; - private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final UniversityValidator universityValidator; - private final SiteUserValidator siteUserValidator; - private final ApplicationValidator applicationValidator; - private final UniversityRepositoryForFilterImpl universityRepositoryForFilter; - - public boolean submitScore(String email, ScoreRequestDto scoreRequestDto) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - - // 수정 - if (applicationRepository.existsBySiteUser_Email(email)) { - Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); - application.setGpa(scoreRequestDto.getGpa()); - application.setGpaCriteria(scoreRequestDto.getGpaCriteria()); - application.setGpaReportUrl(scoreRequestDto.getGpaReportUrl()); - application.setLanguageTestScore(scoreRequestDto.getLanguageTestScore()); - application.setLanguageTestType(scoreRequestDto.getLanguageTestType()); - application.setLanguageTestReportUrl(scoreRequestDto.getLanguageTestReportUrl()); - application.setVerifyStatus(VerifyStatus.PENDING); - return true; - } - - // 최초 등록 - Application application = Application.saveScore(siteUser, scoreRequestDto); - applicationRepository.save(application); - return true; - } - - public boolean submitUniversityChoice(String email, UniversityRequestDto universityRequestDto) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - - // 저장에 필요한 엔티티 불러오기 or 생성 - UniversityInfoForApply firstChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyByIdAndTerm(universityRequestDto.getFirstChoiceUniversityId()); - UniversityInfoForApply secondChoiceUniversity; - try { - secondChoiceUniversity = universityValidator.getValidatedUniversityInfoForApplyByIdAndTerm(universityRequestDto.getSecondChoiceUniversityId()); - } catch (Exception e) { - secondChoiceUniversity = null; - } - - // 1,2 동일한 대학교 지망 에러 처리 - if (secondChoiceUniversity != null && Objects.equals(secondChoiceUniversity.getId(), firstChoiceUniversity.getId())) { - throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); - } - - Application application; - - // 대학 최초 등록이면 - if (applicationRepository.findBySiteUser_Email(siteUser.getEmail()).isEmpty()) { - application = Application.saveUniversity(siteUser, firstChoiceUniversity, secondChoiceUniversity); - application.setNicknameForApply(makeRandomNickname()); - applicationRepository.save(application); - return true; - } - - // 수정 횟수 초과 에러 처리 - application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); - if (application.getUpdateCount() > APPLICATION_UPDATE_COUNT_LIMIT) { - throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); - } - - // 수정이면 update count 1 증가 - if (application.getFirstChoiceUniversity() != null) { - application.setUpdateCount(application.getUpdateCount() + 1); - } - - // 수정 - application.setFirstChoiceUniversity(firstChoiceUniversity); - application.setSecondChoiceUniversity(secondChoiceUniversity); - - // 새로운 닉네임 부여 - String randomNickname = makeRandomNickname(); - while (applicationRepository.existsByNicknameForApply(randomNickname)) { - randomNickname = makeRandomNickname(); - } - application.setNicknameForApply(randomNickname); - - return true; - } - - private String makeRandomNickname() { - Random random = new Random(); - int randomIndex1 = random.nextInt(NicknameForApplyWords.adjectives.size()); - String randomAdjective = NicknameForApplyWords.adjectives.get(randomIndex1); - int randomIndex2 = random.nextInt(NicknameForApplyWords.nouns.size()); - String randomNoun = NicknameForApplyWords.nouns.get(randomIndex2); - return randomAdjective + " " + randomNoun; - } - - public ApplicationsDto getApplicants(String email, String region, String keyword) { - // 유저 검증 - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - // 지원했는지 검증 - Application application = applicationValidator.getValidatedApplicationBySiteUser_Email(email); - // 승인되었는지 확인 - validateApproved(application); - - RegionCode regionCode = null; - if (region != null && !region.isBlank()) { - regionCode = RegionCode.getRegionCodeByKoreanName(region); - } - List countryCodes = null; - if (keyword != null && !keyword.isBlank()) { - countryCodes = CountryCode.getCountryCodeMatchesToKeyword(List.of(keyword)); - } - - List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, List.of(keyword)); - List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser); - List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser); - return ApplicationsDto.builder() - .firstChoice(firstChoiceApplicants) - .secondChoice(secondChoiceApplicants) - .build(); - } - - private void validateApproved(Application application) { - if (application.getVerifyStatus() != VerifyStatus.APPROVED) { - throw new CustomException(APPLICATION_NOT_APPROVED); - } - } - - private List getFirstChoiceApplicants(List universities, SiteUser siteUser) { - return universities.stream() - .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) - .map(university -> { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversityAndTerm(university); - List firstChoiceApplication = applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatus(universityInfoForApply, VerifyStatus.APPROVED); - List firstChoiceApplicant = firstChoiceApplication.stream() - .map(ap -> ApplicantDto.fromEntity(ap, Objects.equals(siteUser.getId(), ap.getSiteUser().getId()))) - .toList(); - return UniversityApplicantsDto.builder() - .koreanName(university.getKoreanName()) - .studentCapacity(universityInfoForApply.getStudentCapacity()) - .region(university.getRegion().getCode().getKoreanName()) - .country(university.getCountry().getCode().getKoreanName()) - .applicants(firstChoiceApplicant) - .build(); - }) - .toList(); - } - - private List getSecondChoiceApplicants(List universities, SiteUser siteUser) { - return universities.stream() - .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) - .map(university -> { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversityAndTerm(university); - List secondChoiceApplication = applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatus(universityInfoForApply, VerifyStatus.APPROVED); - List secondChoiceApplicant = secondChoiceApplication.stream() - .map(ap -> ApplicantDto.fromEntity(ap, Objects.equals(siteUser.getId(), ap.getSiteUser().getId()))) - .toList(); - return UniversityApplicantsDto.builder() - .koreanName(university.getKoreanName()) - .studentCapacity(universityInfoForApply.getStudentCapacity()) - .region(university.getRegion().getCode().getKoreanName()) - .country(university.getCountry().getCode().getKoreanName()) - .applicants(secondChoiceApplicant) - .build(); - }) - .toList(); - } - - public VerifyStatusDto getVerifyStatus(String email) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - Optional application = applicationRepository.findBySiteUser_Email(siteUser.getEmail()); - - // 아무것도 제출 안함 - if (application.isEmpty()) { - return new VerifyStatusDto(ApplicationStatusResponse.NOT_SUBMITTED.name()); - } - - int updateCount = application.get().getUpdateCount(); - // 제출한 상태 - if (application.get().getVerifyStatus() == VerifyStatus.PENDING) { - // 지망 대학만 제출 - if (application.get().getGpaReportUrl() == null) { - return new VerifyStatusDto(ApplicationStatusResponse.COLLEGE_SUBMITTED.name(), updateCount); - } - // 성적만 제출 - if (application.get().getFirstChoiceUniversity() == null) { - return new VerifyStatusDto(ApplicationStatusResponse.SCORE_SUBMITTED.name(), 0); - } - // 성적 승인 대기 중 - return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_PENDING.name(), updateCount); - } - // 성적 승인 반려 - if (application.get().getVerifyStatus() == VerifyStatus.REJECTED) { - return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_REJECTED.name(), updateCount); - } - // 성적 승인 완료 - return new VerifyStatusDto(ApplicationStatusResponse.SUBMITTED_APPROVED.name(), updateCount); - } -} diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java new file mode 100644 index 000000000..dc56e6da3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -0,0 +1,108 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.dto.ScoreRequest; +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; +import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; +import static com.example.solidconnection.custom.exception.ErrorCode.SCORE_SHOULD_SUBMITTED_FIRST; + +@RequiredArgsConstructor +@Service +public class ApplicationSubmissionService { + + public static final int APPLICATION_UPDATE_COUNT_LIMIT = 3; + + private final ApplicationRepository applicationRepository; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final SiteUserRepository siteUserRepository; + + @Value("${university.term}") + public String term; + + /* + * 학점과 영어 성적을 제출한다. + * - 기존에 제출한 적이 있다면, 수정한다. + * - 수정을 하고 나면, 성적 승인 상태(verifyStatus)를 PENDING 상태로 변경한다. + * */ + @Transactional + public boolean submitScore(String email, ScoreRequest scoreRequest) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + Gpa gpa = scoreRequest.toGpa(); + LanguageTest languageTest = scoreRequest.toLanguageTest(); + + applicationRepository.findBySiteUser_Email(email) + .ifPresentOrElse( + // 수정 + application -> application.updateGpaAndLanguageTest(gpa, languageTest), + + // 최초 등록 + () -> applicationRepository.save( + new Application(siteUser, gpa, languageTest) + ) + ); + return true; + } + + /* + * 지망 대학교를 제출한다. + * - 첫번째 지망과 두번째 지망이 같은지 검증한다. + * - 지원 정보 제출 내역이 없다면, 지금의 프로세스(성적 제출 후 지망대학 제출)에 벗어나는 요청이므로 예외를 응답한다. + * - 기존에 제출한 적이 있다면, 수정한다. + * - 수정 횟수 제한을 초과하지 않았는지 검증한다. + * - 새로운 '제출 닉네임'을 부여한다. (악의적으로 타인의 변경 기록을 추적하는 것을 막기 위해) + * - 성적 승인 상태(verifyStatus) 는 변경하지 않는다. + * */ + @Transactional + public boolean submitUniversityChoice(String email, UniversityChoiceRequest universityChoiceRequest) { + validateFirstAndSecondChoiceIdDifferent(universityChoiceRequest); + Application application = applicationRepository.findBySiteUser_Email(email) + .orElseThrow(() -> new CustomException(SCORE_SHOULD_SUBMITTED_FIRST)); + + UniversityInfoForApply firstChoiceUniversity = universityInfoForApplyRepository + .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term); + UniversityInfoForApply secondChoiceUniversity = universityInfoForApplyRepository + .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.secondChoiceUniversityId(), term); + + validateUpdateLimitNotExceed(application); + application.updateUniversityChoice(firstChoiceUniversity, secondChoiceUniversity, getRandomNickname()); + return true; + } + + private String getRandomNickname() { + String randomNickname = NicknameCreator.createRandomNickname(); + while (applicationRepository.existsByNicknameForApply(randomNickname)) { + randomNickname = NicknameCreator.createRandomNickname(); + } + return randomNickname; + } + + private void validateUpdateLimitNotExceed(Application application) { + if (application.getUpdateCount() >= APPLICATION_UPDATE_COUNT_LIMIT) { + throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); + } + } + + private void validateFirstAndSecondChoiceIdDifferent(UniversityChoiceRequest universityChoiceRequest) { + if (Objects.equals( + universityChoiceRequest.firstChoiceUniversityId(), + universityChoiceRequest.secondChoiceUniversityId())) { + throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); + } + } +} diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java b/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java deleted file mode 100644 index e91a558c4..000000000 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationValidator.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.solidconnection.application.service; - -import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.Application; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import static com.example.solidconnection.custom.exception.ErrorCode.APPLICATION_NOT_FOUND; - -@Service -@RequiredArgsConstructor -public class ApplicationValidator { - private final ApplicationRepository applicationRepository; - - public Application getValidatedApplicationBySiteUser_Email(String email){ - return applicationRepository.findBySiteUser_Email(email) - .orElseThrow(() -> new CustomException(APPLICATION_NOT_FOUND)); - } -} diff --git a/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java b/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java new file mode 100644 index 000000000..21a36dfab --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java @@ -0,0 +1,31 @@ +package com.example.solidconnection.application.service; + +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Random; +import java.util.Set; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +class NicknameCreator { + + public static final List ADJECTIVES = List.copyOf( + Set.of("기쁜", "행복한", "즐거운", "밝은", "따뜻한", "시원한", "고고한", "예쁜", "신선한", "풍부한", "깨끗한", + "귀한", "눈부신", "멋진", "고귀한", "화려한", "상큼한", "활기찬", "유쾌한", "똘똘한", "친절한", "좋은", + "영리한", "용감한", "정직한", "성실한", "강인한", "귀여운", "순수한", "희망찬", "발랄한", "나른한", "후한", "빛나는", + "따스한", "안락한", "편안한", "성공한", "재미난", "청량한", "찬란한", "소중한", "특별한", "단순한", "반가운", "그리운") + ); + public static final List NOUNS = List.copyOf( + Set.of("청춘", "토끼", "기사", "곰", "사슴", "여우", "팬더", "이슬", "새싹", "햇빛", "나비", "별", "달", "구름", + "사탕", "젤리", "마법", "풍선", "캔디", "초코", "인형", "쿠키", "요정", "장미", "마녀", "보물", "꽃", "보석", + "달빛", "오리", "날개", "여행", "편지", "불꽃", "파도", "별빛", "구슬", "노래", "음표", "선율", "미소", "가방", "거울", + "씨앗", "열매", "바다", "약속", "구두", "공기", "등불", "촛불", "진주", "꿀벌", "예감", "바람", "오전", "오후", "아침", "점심", "저녁") + ); + private static final Random RANDOM = new Random(); + + public static String createRandomNickname() { + String randomAdjective = ADJECTIVES.get(RANDOM.nextInt(ADJECTIVES.size())); + String randomNoun = NOUNS.get(RANDOM.nextInt(NOUNS.size())); + return randomAdjective + " " + randomNoun; + } +} diff --git a/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java b/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java new file mode 100644 index 000000000..a5f7bf752 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java @@ -0,0 +1,70 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.dto.VerifyStatusResponse; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.VerifyStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.NOT_SUBMITTED; +import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.SCORE_SUBMITTED; +import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.SUBMITTED_APPROVED; +import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.SUBMITTED_PENDING; +import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.SUBMITTED_REJECTED; + +@RequiredArgsConstructor +@Service +public class VerifyStatusQueryService { + + private final ApplicationRepository applicationRepository; + private final SiteUserRepository siteUserRepository; + + /* + * 지원 상태를 조회한다. + * */ + @Transactional(readOnly = true) + public VerifyStatusResponse getVerifyStatus(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + Optional application = applicationRepository.findBySiteUser_Email(siteUser.getEmail()); + + // 아무것도 제출 안함 + if (application.isEmpty()) { + return new VerifyStatusResponse(NOT_SUBMITTED.name(), 0); + } + + int updateCount = application.get().getUpdateCount(); + + // 제출한 상태 + if (application.get().getVerifyStatus() == VerifyStatus.PENDING) { + // 성적만 제출 + if (application.get().getFirstChoiceUniversity() == null) { + return new VerifyStatusResponse(SCORE_SUBMITTED.name(), 0); + } + // 성적 승인 대기 중 + return new VerifyStatusResponse(SUBMITTED_PENDING.name(), updateCount); + } + + // 성적 승인 반려 + if (application.get().getVerifyStatus() == VerifyStatus.REJECTED) { + return new VerifyStatusResponse(SUBMITTED_REJECTED.name(), updateCount); + } + + // 성적 승인 완료 + return new VerifyStatusResponse(SUBMITTED_APPROVED.name(), updateCount); + } + + public enum ApplicationStatusResponse { + NOT_SUBMITTED, // 어떤 것도 제출하지 않음 + COLLEGE_SUBMITTED, // 지망 대학만 제출 + SCORE_SUBMITTED, // 성적만 제출 + SUBMITTED_PENDING, // 성적 인증 대기 중 + SUBMITTED_REJECTED, // 성적 인증 승인 완료 + SUBMITTED_APPROVED // 성적 인증 반려 + } +} diff --git a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java new file mode 100644 index 000000000..fa9d1f265 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java @@ -0,0 +1,101 @@ +package com.example.solidconnection.auth.client; + +import com.example.solidconnection.auth.dto.kakao.KakaoTokenDto; +import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.custom.exception.CustomException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Objects; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_OR_EXPIRED_KAKAO_AUTH_CODE; +import static com.example.solidconnection.custom.exception.ErrorCode.KAKAO_REDIRECT_URI_MISMATCH; +import static com.example.solidconnection.custom.exception.ErrorCode.KAKAO_USER_INFO_FAIL; + +@Component +@RequiredArgsConstructor +public class KakaoOAuthClient { + + private final RestTemplate restTemplate; + @Value("${kakao.redirect_uri}") + public String redirectUri; + @Value("${kakao.client_id}") + private String clientId; + @Value("${kakao.token_url}") + private String tokenUrl; + @Value("${kakao.user_info_url}") + private String userInfoUrl; + + /* + * 클라이언트에서 사용자가 카카오 로그인을 하면, 클라이언트는 '카카오 인가 코드'를 받아, 서버에 넘겨준다. + * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code + * 서버는 카카오 인증 코드를 사용해 카카오 서버로부터 '카카오 토큰'을 받아온다. + * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token + * 그리고 카카오 엑세스 토큰으로 카카오 서버에 요청해 '카카오 사용자 정보'를 받아온다. + * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + * */ + public KakaoUserInfoDto processOauth(String code) { + String kakaoAccessToken = getKakaoAccessToken(code); + return getKakaoUserInfo(kakaoAccessToken); + } + + // 카카오 토큰 요청 + private String getKakaoAccessToken(String code) { + try { + ResponseEntity response = restTemplate.exchange( + buildTokenUri(code), + HttpMethod.POST, + null, + KakaoTokenDto.class + ); + return Objects.requireNonNull(response.getBody()).accessToken(); + } catch (Exception e) { + if (e.getMessage().contains("KOE303")) { + throw new CustomException(KAKAO_REDIRECT_URI_MISMATCH); + } + if (e.getMessage().contains("KOE320")) { + throw new CustomException(INVALID_OR_EXPIRED_KAKAO_AUTH_CODE); + } + throw new CustomException(INVALID_OR_EXPIRED_KAKAO_AUTH_CODE); + } + } + + // 카카오 엑세스 토큰 요청하는 URI 생성 + private String buildTokenUri(String code) { + return UriComponentsBuilder.fromHttpUrl(tokenUrl) + .queryParam("grant_type", "authorization_code") + .queryParam("client_id", clientId) + .queryParam("redirect_uri", redirectUri) + .queryParam("code", code) + .toUriString(); + } + + // 카카오 사용자 정보 요청 + private KakaoUserInfoDto getKakaoUserInfo(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + + // 사용자의 정보 요청 + ResponseEntity response = restTemplate.exchange( + userInfoUrl, + HttpMethod.GET, + new HttpEntity<>(headers), + KakaoUserInfoDto.class + ); + + // 응답 예외처리 + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + return response.getBody(); + } else { + throw new CustomException(KAKAO_USER_INFO_FAIL); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index f1b10b2fa..c126da674 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -1,55 +1,63 @@ package com.example.solidconnection.auth.controller; -import com.example.solidconnection.auth.dto.ReissueResponseDto; -import com.example.solidconnection.auth.dto.SignUpRequestDto; -import com.example.solidconnection.auth.dto.SignUpResponseDto; -import com.example.solidconnection.auth.dto.kakao.KakaoCodeDto; -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponseDto; +import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.auth.dto.SignUpResponse; +import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; +import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; import com.example.solidconnection.auth.service.AuthService; -import com.example.solidconnection.auth.service.KakaoOAuthService; -import com.example.solidconnection.custom.response.CustomResponse; -import com.example.solidconnection.custom.response.DataResponse; -import com.example.solidconnection.custom.response.StatusResponse; +import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.auth.service.SignUpService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +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; import java.security.Principal; -@RestController -@RequestMapping("auth") @RequiredArgsConstructor -public class AuthController { - private final KakaoOAuthService kakaoOAuthService; +@RequestMapping("/auth") +@RestController +public class AuthController implements AuthControllerSwagger { + private final AuthService authService; + private final SignUpService signUpService; + private final SignInService signInService; @PostMapping("/kakao") - public CustomResponse kakaoOauth(@RequestBody KakaoCodeDto kakaoCodeDto) { - KakaoOauthResponseDto kakaoOauthResponseDto = kakaoOAuthService.processOauth(kakaoCodeDto.getCode()); - return new DataResponse<>(kakaoOauthResponseDto); + public ResponseEntity processKakaoOauth(@RequestBody KakaoCodeRequest kakaoCodeRequest) { + KakaoOauthResponse kakaoOauthResponse = signInService.signIn(kakaoCodeRequest); + return ResponseEntity + .ok(kakaoOauthResponse); } @PostMapping("/sign-up") - public CustomResponse signUp(@Valid @RequestBody SignUpRequestDto signUpRequestDto) { - SignUpResponseDto signUpResponseDto = authService.signUp(signUpRequestDto); - return new DataResponse<>(signUpResponseDto); + public ResponseEntity signUp(@Valid @RequestBody SignUpRequest signUpRequest) { + SignUpResponse signUpResponseDto = signUpService.signUp(signUpRequest); + return ResponseEntity + .ok(signUpResponseDto); } @PostMapping("/sign-out") - public CustomResponse signOut(Principal principal) { - boolean status = authService.signOut(principal.getName()); - return new StatusResponse(status); + public ResponseEntity signOut(Principal principal) { + authService.signOut(principal.getName()); + return ResponseEntity.ok().build(); } @PatchMapping("/quit") - public CustomResponse quit(Principal principal) { - boolean status = authService.quit(principal.getName()); - return new StatusResponse(status); + public ResponseEntity quit(Principal principal) { + authService.quit(principal.getName()); + return ResponseEntity.ok().build(); } @PostMapping("/reissue") - public CustomResponse reissue(Principal principal) { - ReissueResponseDto reissueResponseDto = authService.reissue(principal.getName()); - return new DataResponse<>(reissueResponseDto); + public ResponseEntity reissueToken(Principal principal) { + ReissueResponse reissueResponse = authService.reissue(principal.getName()); + return ResponseEntity + .ok(reissueResponse); } } diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthControllerSwagger.java b/src/main/java/com/example/solidconnection/auth/controller/AuthControllerSwagger.java new file mode 100644 index 000000000..28ce96f69 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthControllerSwagger.java @@ -0,0 +1,116 @@ +package com.example.solidconnection.auth.controller; + +import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.auth.dto.SignUpResponse; +import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; +import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; +import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; + +import java.security.Principal; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@Tag(name = "Auth", description = "인증 API") +public interface AuthControllerSwagger { + + @Operation( + summary = "카카오 OAuth 처리", + requestBody = @RequestBody( + description = "클라이언트가 받아온 카카오 인증 코드", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = KakaoCodeRequest.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "로그인 성공 또는 회원가입을 위한 사용자 정보 불러오기 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(oneOf = {SignInResponse.class, FirstAccessResponse.class}) + ) + ) + } + ) + ResponseEntity processKakaoOauth(@RequestBody KakaoCodeRequest kakaoCodeRequest); + + @Operation( + summary = "회원가입", + requestBody = @RequestBody( + description = "회원가입 요청 정보", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = SignUpRequest.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "회원가입 성공, 엑세스 토큰과 리프레시 토큰 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = SignUpResponse.class) + ) + ) + } + ) + ResponseEntity signUp(@Valid @RequestBody SignUpRequest signUpRequest); + + @SecurityRequirements + @SecurityRequirement(name = ACCESS_TOKEN) + @Operation( + summary = "로그아웃", + responses = { + @ApiResponse( + responseCode = "204", + description = "로그아웃 성공" + ) + } + ) + ResponseEntity signOut(Principal principal); + + @SecurityRequirements + @SecurityRequirement(name = ACCESS_TOKEN) + @Operation( + summary = "회원 탈퇴", + responses = { + @ApiResponse( + responseCode = "200", + description = "회원 탈퇴 성공" + ) + } + ) + ResponseEntity quit(Principal principal); + + @SecurityRequirements + @SecurityRequirement(name = ACCESS_TOKEN) + @Operation( + summary = "토큰 재발급", + responses = { + @ApiResponse( + responseCode = "200", + description = "토큰 재발급 성공, 새 토큰 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ReissueResponse.class) + ) + ) + } + ) + ResponseEntity reissueToken(Principal principal); +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java new file mode 100644 index 000000000..5e6097d38 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "토큰 재발급 응답") +public record ReissueResponse( + @Schema(description = "새로 발급된 액세스 토큰", example = "newAccessToken123") + String accessToken) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponseDto.java deleted file mode 100644 index e9192a04b..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponseDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.solidconnection.auth.dto; - -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ReissueResponseDto { - private String accessToken; -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java new file mode 100644 index 000000000..ec89bbf16 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.auth.dto; + +import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "로그인 응답 데이터") +public record SignInResponse( + @Schema(description = "사용자 등록 여부", example = "true") + boolean isRegistered, + + @Schema(description = "발급된 액세스 토큰", example = "accessTokenExample123") + String accessToken, + + @Schema(description = "발급된 리프레시 토큰", example = "refreshTokenExample123") + String refreshToken) implements KakaoOauthResponse { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java deleted file mode 100644 index 16d0cf162..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponseDto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.solidconnection.auth.dto; - -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponseDto; -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class SignInResponseDto extends KakaoOauthResponseDto { - private boolean registered; - private String accessToken; - private String refreshToken; -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java new file mode 100644 index 000000000..60b2c6cc1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.auth.dto; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +import java.util.List; + +@Schema(description = "회원가입 요청 데이터") +public record SignUpRequest( + @Schema(description = "카카오 인증 토큰", example = "kakaoToken123") + String kakaoOauthToken, + + @ArraySchema(schema = @Schema(description = "관심 지역 목록", example = "[\"아시아\", \"유럽\"]")) + List interestedRegions, + + @ArraySchema(schema = @Schema(description = "관심 국가 목록", example = "[\"일본\", \"독일\"]")) + List interestedCountries, + + @Schema(description = "지원 준비 단계", example = "CONSIDERING") + PreparationStatus preparationStatus, + + @NotBlank(message = "닉네임을 입력해주세요.") + @Schema(description = "닉네임", example = "nickname123") + String nickname, + + @Schema(description = "프로필 이미지 URL", example = "http://example.com/profile.jpg") + String profileImageUrl, + + @Schema(description = "성별", example = "MALE") + Gender gender, + + @JsonFormat(pattern = "yyyy-MM-dd") + @Schema(description = "생년월일", example = "1999-01-01") + String birth) { + + public SiteUser toSiteUser(String email, Role role) { + return new SiteUser( + email, + this.nickname, + this.profileImageUrl, + this.birth, + this.preparationStatus, + role, + this.gender + ); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java deleted file mode 100644 index ecd3b66ec..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequestDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.solidconnection.auth.dto; - -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PreparationStatus; -import jakarta.validation.constraints.NotBlank; -import lombok.*; - -import java.util.List; - -import static com.example.solidconnection.constants.validMessage.NICKNAME_NOT_BLANK; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class SignUpRequestDto { - private String kakaoOauthToken; - private List interestedRegions; - private List interestedCountries; - private PreparationStatus preparationStatus; - @NotBlank(message = NICKNAME_NOT_BLANK) - private String nickname; - private String profileImageUrl; - private Gender gender; - private String birth; -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java new file mode 100644 index 000000000..b43eb95e8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "회원가입 후 응답 데이터") +public record SignUpResponse( + @Schema(description = "액세스 토큰", example = "accessTokenSignup123") + String accessToken, + + @Schema(description = "리프레시 토큰", example = "refreshTokenSignup123") + String refreshToken) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponseDto.java deleted file mode 100644 index 3db1c11f4..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponseDto.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.solidconnection.auth.dto; - -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class SignUpResponseDto { - private String accessToken; - private String refreshToken; -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java new file mode 100644 index 000000000..2f777c3b7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java @@ -0,0 +1,32 @@ +package com.example.solidconnection.auth.dto.kakao; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "등록되지 않은 사용자의 최초 접속 시 응답 데이터") +public record FirstAccessResponse( + + @Schema(description = "사용자 등록 여부", example = "false") + boolean isRegistered, + + @Schema(description = "카카오 닉네임", example = "홍길동") + String nickname, + + @Schema(description = "이메일", example = "user@example.com") + String email, + + @Schema(description = "카카오 프로필 이미지 URL", example = "http://example.com/image.jpg") + String profileImageUrl, + + @Schema(description = "우리 서비스에사 발급한 카카오 인증 토큰", example = "abc123xyz") + String kakaoOauthToken) implements KakaoOauthResponse { + + public static FirstAccessResponse of(KakaoUserInfoDto kakaoUserInfoDto, String kakaoOauthToken) { + return new FirstAccessResponse( + false, + kakaoUserInfoDto.kakaoAccountDto().profile().nickname(), + kakaoUserInfoDto.kakaoAccountDto().email(), + kakaoUserInfoDto.kakaoAccountDto().profile().profileImageUrl(), + kakaoOauthToken + ); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponseDto.java deleted file mode 100644 index 531d3e77f..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponseDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class FirstAccessResponseDto extends KakaoOauthResponseDto { - private boolean registered; - private String nickname; - private String email; - private String profileImageUrl; - private String kakaoOauthToken; - - public static FirstAccessResponseDto fromKakaoUserInfo(KakaoUserInfoDto kakaoUserInfoDto, String kakaoOauthToken){ - return FirstAccessResponseDto.builder() - .registered(false) - .email(kakaoUserInfoDto.getKakaoAccount().getEmail()) - .profileImageUrl(kakaoUserInfoDto.getKakaoAccount().getProfile().getProfileImageUrl()) - .nickname(kakaoUserInfoDto.getKakaoAccount().getProfile().getNickname()) - .kakaoOauthToken(kakaoOauthToken) - .build(); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoAccount.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoAccount.java deleted file mode 100644 index 3c1791516..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoAccount.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@JsonIgnoreProperties(ignoreUnknown = true) -@AllArgsConstructor -@NoArgsConstructor -public class KakaoAccount { - @JsonProperty("profile") - private KakaoProfile profile; - private String email; -} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeDto.java deleted file mode 100644 index 3c55e728d..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeDto.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@AllArgsConstructor -@NoArgsConstructor -public class KakaoCodeDto { - private String code; -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java new file mode 100644 index 000000000..fcdbefb40 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.auth.dto.kakao; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "클라이언트에서 받은 카카오 코드") +public record KakaoCodeRequest( + @Schema(description = "카카오 코드", example = "ABCD1234") + String code) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponseDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java similarity index 59% rename from src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponseDto.java rename to src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java index 57f4dd3da..1e2320e35 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponseDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java @@ -1,4 +1,4 @@ package com.example.solidconnection.auth.dto.kakao; -public class KakaoOauthResponseDto { +public interface KakaoOauthResponse { } diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoProfile.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoProfile.java deleted file mode 100644 index 4f8dcf2c5..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoProfile.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@JsonIgnoreProperties(ignoreUnknown = true) -@AllArgsConstructor -@NoArgsConstructor -public class KakaoProfile { - private String nickname; - @JsonProperty("profile_image_url") - private String profileImageUrl; -} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java index 5a4b65704..767645e3b 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java @@ -2,19 +2,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -@Getter -@Setter @JsonIgnoreProperties(ignoreUnknown = true) -@AllArgsConstructor -@NoArgsConstructor -public class KakaoTokenDto { - @JsonProperty("access_token") - private String accessToken; - @JsonProperty("refresh_token") - private String refreshToken; -} \ No newline at end of file +public record KakaoTokenDto( + @JsonProperty("access_token") String accessToken, + @JsonProperty("refresh_token") String refreshToken) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java index 88f25eaf7..85aea091d 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java @@ -2,17 +2,20 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -@Getter -@Setter @JsonIgnoreProperties(ignoreUnknown = true) -@AllArgsConstructor -@NoArgsConstructor -public class KakaoUserInfoDto { - @JsonProperty("kakao_account") - private KakaoAccount kakaoAccount; -} \ No newline at end of file +public record KakaoUserInfoDto( + @JsonProperty("kakao_account") KakaoAccountDto kakaoAccountDto) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record KakaoAccountDto( + @JsonProperty("profile") KakaoProfileDto profile, + String email) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record KakaoProfileDto( + @JsonProperty("profile_image_url") String profileImageUrl, + String nickname) { + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index f641adbf0..caf78074d 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -1,23 +1,12 @@ package com.example.solidconnection.auth.service; -import com.example.solidconnection.auth.dto.ReissueResponseDto; -import com.example.solidconnection.auth.dto.SignUpRequestDto; -import com.example.solidconnection.auth.dto.SignUpResponseDto; +import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.config.token.TokenService; import com.example.solidconnection.config.token.TokenType; -import com.example.solidconnection.config.token.TokenValidator; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.*; -import com.example.solidconnection.repositories.CountryRepository; -import com.example.solidconnection.repositories.InterestedCountyRepository; -import com.example.solidconnection.repositories.InterestedRegionRepository; -import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.siteuser.service.SiteUserValidator; -import com.example.solidconnection.type.CountryCode; -import com.example.solidconnection.type.RegionCode; -import com.example.solidconnection.type.Role; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -25,143 +14,62 @@ import org.springframework.util.ObjectUtils; import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.config.token.TokenValidator.SIGN_OUT_VALUE; +import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; -@Service -@Transactional @RequiredArgsConstructor +@Service public class AuthService { private final RedisTemplate redisTemplate; - private final TokenValidator tokenValidator; private final TokenService tokenService; - private final SiteUserValidator siteUserValidator; private final SiteUserRepository siteUserRepository; - private final RegionRepository regionRepository; - private final InterestedRegionRepository interestedRegionRepository; - private final CountryRepository countryRepository; - private final InterestedCountyRepository interestedCountyRepository; - - public SignUpResponseDto signUp(SignUpRequestDto signUpRequestDto) { - tokenValidator.validateKakaoToken(signUpRequestDto.getKakaoOauthToken()); - validateUserNotDuplicated(signUpRequestDto); - validateNicknameDuplicated(signUpRequestDto.getNickname()); - validateBirthFormat(signUpRequestDto.getBirth()); - - SiteUser siteUser = makeSiteUserEntity(signUpRequestDto); - SiteUser savedSiteUser = siteUserRepository.save(siteUser); - - saveInterestedRegion(signUpRequestDto, savedSiteUser); - saveInterestedCountry(signUpRequestDto, savedSiteUser); - - String email = savedSiteUser.getEmail(); - String accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); - return SignUpResponseDto.builder() - .accessToken(accessToken) - .refreshToken(refreshToken) - .build(); - } - - public boolean signOut(String email){ + /* + * 로그아웃 한다. + * - 리프레시 토큰을 무효화하기 위해 리프레시 토큰의 value 를 변경한다. + * - 어떤 사용자가 엑세스 토큰으로 인증이 필요한 기능을 사용하려 할 때, 로그아웃 검증이 진행되는데, + * - 이때 리프레시 토큰의 value 가 SIGN_OUT_VALUE 이면 예외 응답이 반환된다. + * - (TokenValidator.validateNotSignOut() 참고) + * */ + public void signOut(String email) { redisTemplate.opsForValue().set( - TokenType.REFRESH.getPrefix() + email, - "signOut", + TokenType.REFRESH.addTokenPrefixToSubject(email), + SIGN_OUT_VALUE, TokenType.REFRESH.getExpireTime(), TimeUnit.MILLISECONDS ); - return true; } - public boolean quit(String email){ - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - siteUser.setQuitedAt(LocalDate.now().plusDays(1)); - return true; + /* + * 탈퇴한다. + * - 탈퇴한 시점의 다음날을 탈퇴일로 잡는다. + * - e.g. 2024-01-01 18:00 탈퇴 시, 2024-01-02 00:00 가 탈퇴일이 된다. + * */ + @Transactional + public void quit(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + LocalDate tomorrow = LocalDate.now().plusDays(1); + siteUser.setQuitedAt(tomorrow); } - public ReissueResponseDto reissue(String email) { + /* + * 액세스 토큰을 재발급한다. + * - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다. + * - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다. + * */ + public ReissueResponse reissue(String email) { // 리프레시 토큰 만료 확인 - String refreshTokenKey= TokenType.REFRESH.getPrefix() + email; + String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(email); String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); if (ObjectUtils.isEmpty(refreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } - // 엑세스 토큰 재발급 + // 액세스 토큰 재발급 String newAccessToken = tokenService.generateToken(email, TokenType.ACCESS); - return ReissueResponseDto.builder() - .accessToken(newAccessToken) - .build(); - } - - private void validateUserNotDuplicated(SignUpRequestDto signUpRequestDto){ - String email = tokenService.getEmail(signUpRequestDto.getKakaoOauthToken()); - if(siteUserRepository.existsByEmail(email)){ - throw new CustomException(USER_ALREADY_EXISTED); - } - } - - private void validateNicknameDuplicated(String nickname){ - if(siteUserRepository.existsByNickname(nickname)){ - throw new CustomException(NICKNAME_ALREADY_EXISTED); - } - } - - private void validateBirthFormat(String birthInput) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - try { - LocalDate.parse(birthInput, formatter); - } catch (DateTimeParseException e) { - throw new CustomException(INVALID_BIRTH_FORMAT); - } - } - - private SiteUser makeSiteUserEntity(SignUpRequestDto signUpRequestDto) { - return SiteUser.builder() - .email(tokenService.getEmail(signUpRequestDto.getKakaoOauthToken())) - .nickname(signUpRequestDto.getNickname()) - .preparationStage(signUpRequestDto.getPreparationStatus()) - .profileImageUrl(signUpRequestDto.getProfileImageUrl()) - .gender(signUpRequestDto.getGender()) - .birth(signUpRequestDto.getBirth()) - .role(Role.MENTEE) - .build(); - } - - private void saveInterestedCountry(SignUpRequestDto signUpRequestDto, SiteUser savedSiteUser) { - List interestedCountries = signUpRequestDto.getInterestedCountries().stream() - .map(CountryCode::getCountryCodeByKoreanName) - .map(countryCode -> { - Country country = countryRepository.findByCode(countryCode) - .orElseThrow(() -> new RuntimeException("Country Code enum이랑 table이랑 다름 : " + countryCode.name())); - return InterestedCountry.builder() - .siteUser(savedSiteUser) - .country(country) - .build(); - }) - .collect(Collectors.toList()); - interestedCountyRepository.saveAll(interestedCountries); - } - - private void saveInterestedRegion(SignUpRequestDto signUpRequestDto, SiteUser savedSiteUser) { - List interestedRegions = signUpRequestDto.getInterestedRegions().stream() - .map(RegionCode::getRegionCodeByKoreanName) - .map(regionCode -> { - Region region = regionRepository.findByCode(regionCode) - .orElseThrow(() -> new RuntimeException("Region Code enum이랑 table이랑 다름 : " + regionCode.name())); - return InterestedRegion.builder() - .siteUser(savedSiteUser) - .region(region) - .build(); - }) - .collect(Collectors.toList()); - interestedRegionRepository.saveAll(interestedRegions); + tokenService.saveToken(newAccessToken, TokenType.ACCESS); + return new ReissueResponse(newAccessToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java deleted file mode 100644 index b04bce9d7..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/KakaoOAuthService.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.dto.SignInResponseDto; -import com.example.solidconnection.auth.dto.kakao.*; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.siteuser.service.SiteUserValidator; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; - -import java.util.Arrays; -import java.util.Objects; - -import static com.example.solidconnection.custom.exception.ErrorCode.*; - -@Service -@Transactional -@RequiredArgsConstructor -public class KakaoOAuthService { - - private final RestTemplate restTemplate; - private final TokenService tokenService; - private final SiteUserValidator siteUserValidator; - private final SiteUserRepository siteUserRepository; - - @Value("${kakao.client_id}") - private String clientId; - @Value("${kakao.redirect_uri}") - private String redirectUri; - @Value("${kakao.token_url}") - private String tokenUrl; - @Value("${kakao.user_info_url}") - private String userInfoUrl; - - public KakaoOauthResponseDto processOauth(String code) throws CustomException { - String kakaoAccessToken = getKakaoAccessToken(code); - KakaoUserInfoDto kakaoUserInfoDto = getKakaoUserInfo(kakaoAccessToken); - String email = kakaoUserInfoDto.getKakaoAccount().getEmail(); - boolean isAlreadyRegistered = siteUserRepository.existsByEmail(email); - if (isAlreadyRegistered) { - resetQuitedAt(email); - return kakaoSignIn(email); - } - String kakaoOauthToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); - return FirstAccessResponseDto.fromKakaoUserInfo(kakaoUserInfoDto, kakaoOauthToken); - } - - private String getKakaoAccessToken(String code) { - // 카카오 엑세스 토큰 요청 - try { - ResponseEntity response = restTemplate.exchange( - buildTokenUri(code), - HttpMethod.POST, - null, - KakaoTokenDto.class - ); - return Objects.requireNonNull(response.getBody()).getAccessToken(); - } catch (Exception e) { - if (e.getMessage().contains("KOE303")) { - throw new CustomException(REDIRECT_URI_MISMATCH); - } - throw new CustomException(INVALID_KAKAO_AUTH_CODE); - } - } - - // 카카오에게 엑세스 토큰 발급 요청하는 URI 생성 - private String buildTokenUri(String code) { - return UriComponentsBuilder.fromHttpUrl(tokenUrl) - .queryParam("grant_type", "authorization_code") - .queryParam("client_id", clientId) - .queryParam("redirect_uri", redirectUri) - .queryParam("code", code) - .toUriString(); - } - - private KakaoUserInfoDto getKakaoUserInfo(String accessToken) { - // 카카오 엑세스 토큰을 헤더에 담은 HttpEntity - HttpHeaders headers = new HttpHeaders(); - headers.setBearerAuth(accessToken); - HttpEntity entity = new HttpEntity<>(headers); - - // 사용자의 정보 요청 - ResponseEntity response = restTemplate.exchange( - userInfoUrl, - HttpMethod.GET, - entity, - KakaoUserInfoDto.class - ); - - // 응답 예외처리 - if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { - return response.getBody(); - } else { - throw new CustomException(KAKAO_USER_INFO_FAIL); - } - } - - private SignInResponseDto kakaoSignIn(String email) { - String accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); - return SignInResponseDto.builder() - .registered(true) - .accessToken(accessToken) - .refreshToken(refreshToken) - .build(); - } - - public void resetQuitedAt(String email) { - SiteUser siteUser = siteUserRepository.findByEmail(email).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - siteUser.setQuitedAt(null); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java new file mode 100644 index 000000000..f6adda20d --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -0,0 +1,72 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.client.KakaoOAuthClient; +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; +import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; +import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; +import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class SignInService { + + private final TokenService tokenService; + private final SiteUserRepository siteUserRepository; + private final KakaoOAuthClient kakaoOAuthClient; + + /* + * 카카오에서 받아온 사용자 정보에 있는 이메일을 통해 기존 회원인지, 신규 회원인지 판별하고, 이에 따라 다르게 응답한다. + * 기존 회원 : 로그인 + * - 우리 서비스의 탈퇴 회원 방침을 적용한다. (계정 복구 기간 안에 접속하면 탈퇴를 무효화) + * - 액세스 토큰과 리프레시 토큰을 발급한다. + * 신규 회원 : 회원가입 페이지로 리다이렉트할 때 필요한 정보 제공 + * - 회원가입 시 입력하는 '닉네임'과 '프로필 사진' 부분을 미리 채우기 위해 사용자 정보를 리턴한다. + * - 또한, 우리 서비스에서 카카오 인증을 받았는지 나타내기 위한 'kakaoOauthToken' 을 발급해서 응답한다. + * - 회원가입할 때 클라이언트는 이때 발급받은 kakaoOauthToken 를 요청에 포함해 요청한다. (SignUpService 참고) + * */ + @Transactional + public KakaoOauthResponse signIn(KakaoCodeRequest kakaoCodeRequest) { + KakaoUserInfoDto kakaoUserInfoDto = kakaoOAuthClient.processOauth(kakaoCodeRequest.code()); + String email = kakaoUserInfoDto.kakaoAccountDto().email(); + boolean isAlreadyRegistered = siteUserRepository.existsByEmail(email); + + if (isAlreadyRegistered) { + resetQuitedAt(email); + return getSignInInfo(email); + } + + return getFirstAccessInfo(kakaoUserInfoDto); + } + + // 계적 복구 기한이 지난 회원은 자정마다 삭제된다. (UserRemovalScheduler 참고) + // 따라서 DB 에서 조회되었다면 아직 기한이 지나지 않았다는 뜻이므로, 탈퇴 날짜를 초기화한다. + private void resetQuitedAt(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + if (siteUser.getQuitedAt() == null) { + return; + } + + siteUser.setQuitedAt(null); + } + + private SignInResponse getSignInInfo(String email) { + String accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + return new SignInResponse(true, accessToken, refreshToken); + } + + private FirstAccessResponse getFirstAccessInfo(KakaoUserInfoDto kakaoUserInfoDto) { + String kakaoOauthToken = tokenService.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); + tokenService.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); + return FirstAccessResponse.of(kakaoUserInfoDto, kakaoOauthToken); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java new file mode 100644 index 000000000..f10f40dbd --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -0,0 +1,100 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.auth.dto.SignUpResponse; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.config.token.TokenValidator; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.InterestedCountry; +import com.example.solidconnection.entity.InterestedRegion; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Role; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; + +@RequiredArgsConstructor +@Service +public class SignUpService { + + private final TokenValidator tokenValidator; + private final TokenService tokenService; + private final SiteUserRepository siteUserRepository; + private final RegionRepository regionRepository; + private final InterestedRegionRepository interestedRegionRepository; + private final CountryRepository countryRepository; + private final InterestedCountyRepository interestedCountyRepository; + + /* + * 회원가입을 한다. + * - 카카오로 최초 로그인 시 우리 서비스에서 발급한 카카오 토큰 kakaoOauthToken 을 검증한다. + * - 이는 '카카오 인증을 하지 않고 회원가입 api 만으로 회원가입 하는 상황'을 방지하기 위함이다. + * - 만약 api 만으로 회원가입을 한다면, 카카오 인증과 이메일에 대한 검증 없이 회원가입이 가능해진다. + * - 이메일은 우리 서비스에서 사용자를 식별하는 중요한 정보이기 때문에 '우리 서비스에서 발급한 카카오 토큰인지 검증하는' 단계가 필요하다. + * - 사용자 정보를 DB에 저장한다. + * - 관심 국가와 지역을 DB에 저장한다. + * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. + * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. + * */ + @Transactional + public SignUpResponse signUp(SignUpRequest signUpRequest) { + // 검증 + tokenValidator.validateKakaoToken(signUpRequest.kakaoOauthToken()); + String email = tokenService.getEmail(signUpRequest.kakaoOauthToken()); + validateNicknameDuplicated(signUpRequest.nickname()); + validateUserNotDuplicated(email); + + // 사용자 저장 + SiteUser siteUser = signUpRequest.toSiteUser(email, Role.MENTEE); + SiteUser savedSiteUser = siteUserRepository.save(siteUser); + + // 관심 지역, 국가 저장 + saveInterestedRegion(signUpRequest, savedSiteUser); + saveInterestedCountry(signUpRequest, savedSiteUser); + + // 토큰 발급 + String accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + return new SignUpResponse(accessToken, refreshToken); + } + + private void validateUserNotDuplicated(String email) { + if (siteUserRepository.existsByEmail(email)) { + throw new CustomException(USER_ALREADY_EXISTED); + } + } + + private void validateNicknameDuplicated(String nickname) { + if (siteUserRepository.existsByNickname(nickname)) { + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + + private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) { + List interestedRegionNames = signUpRequest.interestedRegions(); + List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream() + .map(region -> new InterestedRegion(savedSiteUser, region)) + .toList(); + interestedRegionRepository.saveAll(interestedRegions); + } + + private void saveInterestedCountry(SignUpRequest signUpRequest, SiteUser savedSiteUser) { + List interestedCountryNames = signUpRequest.interestedCountries(); + List interestedCountries = countryRepository.findByKoreanNames(interestedCountryNames).stream() + .map(country -> new InterestedCountry(savedSiteUser, country)) + .toList(); + interestedCountyRepository.saveAll(interestedCountries); + } +} diff --git a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java index 1c02f7196..9c416de9a 100644 --- a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java +++ b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java @@ -17,17 +17,17 @@ public class RedisConfig { private final int redisPort; - @Bean - public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(redisHost, redisPort); - } - public RedisConfig(@Value("${spring.data.redis.host}") final String redisHost, @Value("${spring.data.redis.port}") final int redisPort) { this.redisHost = redisHost; this.redisPort = redisPort; } + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + @Bean public RedisTemplate redisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); @@ -36,4 +36,4 @@ public RedisTemplate redisTemplate() { redisTemplate.setConnectionFactory(redisConnectionFactory()); return redisTemplate; } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java b/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java index 4bd0354d2..51f7205be 100644 --- a/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java +++ b/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java @@ -17,4 +17,4 @@ public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { .setReadTimeout(Duration.ofSeconds(5)) .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 7fc030ec3..69f5a2f2d 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -23,8 +23,7 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { - + AuthenticationException authException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); @@ -33,8 +32,7 @@ public void commence(HttpServletRequest request, HttpServletResponse response, } public void expiredCommence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { - + AuthenticationException authException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(new CustomException(ACCESS_TOKEN_EXPIRED)); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); @@ -43,7 +41,7 @@ public void expiredCommence(HttpServletRequest request, HttpServletResponse resp } public void customCommence(HttpServletRequest request, HttpServletResponse response, - CustomException customException) throws IOException { + CustomException customException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(customException); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 2d9839578..7e103f911 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -52,7 +52,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (token != null) { // 토큰이 있어야 검증 - 토큰 유무에 대한 다른 처리를 컨트롤러에서 할 수 있음 try { String requestURI = request.getRequestURI(); - if(requestURI.equals("/auth/reissue")) { + if (requestURI.equals("/auth/reissue")) { Authentication auth = this.tokenService.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); filterChain.doFilter(request, response); @@ -107,6 +107,10 @@ private HashSet getPermitAllEndpoints() { // 대학교 정보 permitAllEndpoints.add("/university/search/**"); + // API 문서 + permitAllEndpoints.add("/swagger-ui/**"); + permitAllEndpoints.add("/v3/api-docs/**"); + return permitAllEndpoints; } } diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index e9885b8ec..e3415e9c4 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -29,7 +29,7 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("https://www.solid-connect.net", "http://localhost:8080", "https://www.api.solid-connect.net", "http://localhost:3000")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); @@ -49,9 +49,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers( "/", "/index.html", "/favicon.ico", "/file/profile/pre", - "/auth/kakao", "/auth/sign-up", "/auth/reissue", - "/university/detail/**", "/university/search/**", "/home" - ) + "/auth/kakao", "/auth/sign-up", "/auth/reissue", + "/university/detail/**", "/university/search/**", "/home", + "/swagger-ui/**", "/v3/api-docs/**" + ) .permitAll() .anyRequest().authenticated()) .addFilterBefore(this.jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java b/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java new file mode 100644 index 000000000..f74a57fb9 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java @@ -0,0 +1,40 @@ +package com.example.solidconnection.config.swagger; + +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static io.swagger.v3.oas.annotations.enums.SecuritySchemeType.HTTP; + +@Configuration +@SecurityScheme( + name = "access_token", + type = HTTP, + in = SecuritySchemeIn.HEADER, + scheme = "bearer", + bearerFormat = "JWT", + paramName = "Authorization", + description = "엑세스 토큰을 입력하세요. (Bearer 포함 X)" +) +public class SwaggerConfig { + + public static final String ACCESS_TOKEN = "access_token"; + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .components(new Components()) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("솔리드 커넥션 API 문서✈️") + .description("솔리드 커넥션의 API 문서입니다. \n\"Authorize\" 버튼을 눌러 인증을 하면 인증이 필요한 API를 호출할 수 있습니다.") + .version("1.0.0"); + } +} diff --git a/src/main/java/com/example/solidconnection/config/token/TokenService.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java index bcc9c187e..fc9ccea31 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenService.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenService.java @@ -1,7 +1,6 @@ package com.example.solidconnection.config.token; import com.example.solidconnection.custom.userdetails.CustomUserDetailsService; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; @@ -17,11 +16,11 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -@Component @RequiredArgsConstructor +@Component public class TokenService { + private final RedisTemplate redisTemplate; - private final SiteUserRepository siteUserRepository; private final CustomUserDetailsService customUserDetailsService; @Value("${jwt.secret}") @@ -41,7 +40,7 @@ public String generateToken(String email, TokenType tokenType) { public void saveToken(String token, TokenType tokenType) { redisTemplate.opsForValue().set( - tokenType.getPrefix() + getClaim(token).getSubject(), + tokenType.addTokenPrefixToSubject(getClaim(token).getSubject()), token, tokenType.getExpireTime(), TimeUnit.MILLISECONDS @@ -68,4 +67,4 @@ private Claims getClaim(String token) { return e.getClaims(); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/config/token/TokenType.java b/src/main/java/com/example/solidconnection/config/token/TokenType.java index fb6e7c5a9..d5fc1717f 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenType.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenType.java @@ -4,6 +4,7 @@ @Getter public enum TokenType { + ACCESS("", 1000 * 60 * 60), REFRESH("refresh:", 1000 * 60 * 60 * 24 * 7), KAKAO_OAUTH("kakao:", 1000 * 60 * 60); @@ -11,8 +12,12 @@ public enum TokenType { private final String prefix; private final int expireTime; - TokenType(String prefix, int expireTime){ + TokenType(String prefix, int expireTime) { this.prefix = prefix; this.expireTime = expireTime; } + + public String addTokenPrefixToSubject(String subject) { + return prefix + subject; + } } diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java index e66846196..a95a504ed 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java @@ -12,11 +12,18 @@ import java.util.Date; import java.util.Objects; -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; +import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; @Component @RequiredArgsConstructor public class TokenValidator { + + public static final String SIGN_OUT_VALUE = "signOut"; + private final RedisTemplate redisTemplate; @Value("${jwt.secret}") @@ -31,14 +38,14 @@ public void validateAccessToken(String token) { private void validateRefreshToken(String token) { String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email) == null) { + if (redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)) == null) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } } private void validateNotSignOut(String token) { String email = getClaim(token).getSubject(); - if ("signOut".equals(redisTemplate.opsForValue().get(TokenType.REFRESH.getPrefix() + email))) { + if (SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)))) { throw new CustomException(USER_ALREADY_SIGN_OUT); } } @@ -51,8 +58,8 @@ public void validateKakaoToken(String token) { private void validateKakaoTokenNotUsed(String token) { String email = getClaim(token).getSubject(); - if (Objects.equals(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.getPrefix() + email), token)) { - throw new CustomException(INVALID_KAKAO_TOKEN); + if (!Objects.equals(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email)), token)) { + throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } @@ -64,7 +71,7 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { throw new CustomException(ACCESS_TOKEN_EXPIRED); } if (token.equals(TokenType.KAKAO_OAUTH)) { - throw new CustomException(INVALID_KAKAO_TOKEN); + throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } } diff --git a/src/main/java/com/example/solidconnection/constants/Constants.java b/src/main/java/com/example/solidconnection/constants/Constants.java deleted file mode 100644 index 60e487b95..000000000 --- a/src/main/java/com/example/solidconnection/constants/Constants.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.solidconnection.constants; - -public class Constants { - public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; - public static final int APPLICATION_UPDATE_COUNT_LIMIT = 3; - public final static int RECOMMEND_UNIVERSITY_NUM = 6; - public final static String TERM = "2024-2-a"; -} diff --git a/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java deleted file mode 100644 index 43975db9c..000000000 --- a/src/main/java/com/example/solidconnection/constants/GeneralRecommendUniversities.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.example.solidconnection.constants; - -import com.example.solidconnection.entity.UniversityInfoForApply; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; - -import static com.example.solidconnection.constants.Constants.TERM; - -@Component -@RequiredArgsConstructor -public class GeneralRecommendUniversities { - // 기본 추천 대학 - 국문명 -/* public final static String RECOMMEND_UNIVERSITY_1 = "네바다주립대학 라스베이거스(B형)"; - public final static String RECOMMEND_UNIVERSITY_2 = "바덴뷔르템베르크 산학협력대학"; - public final static String RECOMMEND_UNIVERSITY_3 = "릴 가톨릭 대학"; - public final static String RECOMMEND_UNIVERSITY_4 = "그라츠공과대학"; - public final static String RECOMMEND_UNIVERSITY_5 = "RMIT멜버른공과대학(A형)"; - public final static String RECOMMEND_UNIVERSITY_6 = "오스트라바 대학";*/ - - // 2024-2 추가선발 - public final static String RECOMMEND_UNIVERSITY_1 = "밀라노공과대학"; - public final static String RECOMMEND_UNIVERSITY_2 = "파리8대학교"; - public final static String RECOMMEND_UNIVERSITY_3 = "안젤로주립대학(A형)"; - public final static String RECOMMEND_UNIVERSITY_4 = "제플린대학"; - public final static String RECOMMEND_UNIVERSITY_5 = "리스본대학 경영학과"; - public final static String RECOMMEND_UNIVERSITY_6 = "몽펠리에 대학교"; - - - private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private List recommendedUniversities; - - @PostConstruct - public void init() { - recommendedUniversities = new ArrayList<>(); - - UniversityInfoForApply univ1 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_1, TERM).get(); - UniversityInfoForApply univ2 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_2, TERM).get(); - UniversityInfoForApply univ3 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_3, TERM).get(); - UniversityInfoForApply univ4 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_4, TERM).get(); - UniversityInfoForApply univ5 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_5, TERM).get(); - UniversityInfoForApply univ6 = universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(RECOMMEND_UNIVERSITY_6, TERM).get(); - - recommendedUniversities.add(univ1); - recommendedUniversities.add(univ2); - recommendedUniversities.add(univ3); - recommendedUniversities.add(univ4); - recommendedUniversities.add(univ5); - recommendedUniversities.add(univ6); - } - - public List getRecommendedUniversities() { - return recommendedUniversities; - } -} diff --git a/src/main/java/com/example/solidconnection/constants/NicknameForApplyWords.java b/src/main/java/com/example/solidconnection/constants/NicknameForApplyWords.java deleted file mode 100644 index 3a7ae39f9..000000000 --- a/src/main/java/com/example/solidconnection/constants/NicknameForApplyWords.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.solidconnection.constants; - -import java.util.List; - -public class NicknameForApplyWords { - public static final List adjectives = List.of( - "기쁜", "행복한", "즐거운", "밝은", "따뜻한", "시원한", "고고한", "예쁜", "신선한", "풍부한", "깨끗한", - "귀한", "눈부신", "멋진", "고귀한", "화려한", "상큼한", "활기찬", "유쾌한", "똘똘한", "친절한", "눈부신", "좋은", - "영리한", "용감한", "정직한", "성실한", "강인한", "귀여운", "순수한", "희망찬", "발랄한", "나른한", "후한", "빛나는", - "따스한", "안락한", "편안한", "성공한"); - - public static final List nouns = List.of( - "청춘", "토끼", "기사", "곰", "사슴", "여우", "팬더", "이슬", "새싹", "햇빛", "나비", "별", "달", "구름", - "사탕", "젤리", "마법", "풍선", "캔디", "초코", "인형", "쿠키", "요정", "장미", "마녀", "보물", "꽃", "보석", - "달빛", "오리", "날개", "여행", "편지", "불꽃"); -} diff --git a/src/main/java/com/example/solidconnection/constants/validMessage.java b/src/main/java/com/example/solidconnection/constants/validMessage.java deleted file mode 100644 index ae7d87eec..000000000 --- a/src/main/java/com/example/solidconnection/constants/validMessage.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.solidconnection.constants; - -public class validMessage { - public final static String NICKNAME_NOT_BLANK = "닉네임을 입력해주세요."; - public final static String LANGUAGE_TEST_TYPE_NOT_BLANK = "어학 종류를 입력해주세요."; - public final static String LANGUAGE_TEST_SCORE_NOT_BLANK = "어학 점수를 입력해주세요."; - public final static String LANGUAGE_TEST_REPORT_URL_NOT_BLANK = "어학 증명서를 첨부해주세요."; - public final static String GPA_NOT_BLANK = "학점을 입력해주세요."; - public final static String GPA_CRITERIA_NOT_BLANK = "학점 기준을 입력해주세요."; - public final static String GPA_REPORT_URL_NOT_BLANK = "대학 성적 증명서를 첨부해주세요."; - public final static String FIRST_CHOICE_UNIVERSITY_ID_NOT_BLANK = "1지망 대학교를 입력해주세요."; -} diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java index 209d3aa7e..367d704eb 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java @@ -7,12 +7,12 @@ public class CustomException extends RuntimeException { private final int code; private final String message; - public CustomException(ErrorCode errorCode){ + public CustomException(ErrorCode errorCode) { code = errorCode.getCode(); message = errorCode.getMessage(); } - public CustomException(ErrorCode errorCode, String detail){ + public CustomException(ErrorCode errorCode, String detail) { code = errorCode.getCode(); message = errorCode.getMessage() + " : " + detail; } diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java index f7764198f..7394203cb 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java @@ -2,6 +2,7 @@ import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import io.jsonwebtoken.JwtException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -12,7 +13,10 @@ import java.util.ArrayList; import java.util.List; -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_INPUT; +import static com.example.solidconnection.custom.exception.ErrorCode.JSON_PARSING_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; +import static com.example.solidconnection.custom.exception.ErrorCode.NOT_DEFINED_ERROR; @Slf4j @ControllerAdvice @@ -20,33 +24,56 @@ public class CustomExceptionHandler { @ExceptionHandler(CustomException.class) protected ResponseEntity handleCustomException(CustomException ex) { - ex.printStackTrace(); + log.error("커스텀 예외 발생 : {}", ex.getMessage()); ErrorResponse errorResponse = new ErrorResponse(ex); - return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(ex.getCode())); + return ResponseEntity + .status(ex.getCode()) + .body(errorResponse); } @ExceptionHandler(InvalidFormatException.class) public ResponseEntity handleInvalidFormatException(InvalidFormatException ex) { String errorMessage = ex.getValue() + " 은(는) 유효하지 않은 값입니다."; + log.error("JSON 파싱 예외 발생 : {}", errorMessage); ErrorResponse errorResponse = new ErrorResponse(JSON_PARSING_FAILED, errorMessage); - return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(JSON_PARSING_FAILED.getCode())); + return ResponseEntity + .status(JSON_PARSING_FAILED.getCode()) + .body(errorResponse); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { List errors = new ArrayList<>(); - ex.getBindingResult().getFieldErrors().forEach((fieldError) -> { - errors.add(fieldError.getDefaultMessage()); - }); - ErrorResponse errorResponse = new ErrorResponse(INVALID_INPUT, errors.toString()); - return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(INVALID_INPUT.getCode())); + ex.getBindingResult() + .getFieldErrors() + .forEach(fieldError -> errors.add(fieldError.getDefaultMessage())); + + String errorMessage = errors.toString(); + log.error("입력값 검증 예외 발생 : {}", errorMessage); + ErrorResponse errorResponse = new ErrorResponse(INVALID_INPUT, errorMessage); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(errorResponse); } + @ExceptionHandler(JwtException.class) + public ResponseEntity handleJwtException(JwtException ex) { + String errorMessage = ex.getMessage(); + log.error("JWT 예외 발생 : {}", errorMessage); + ErrorResponse errorResponse = new ErrorResponse(JWT_EXCEPTION, errorMessage); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(errorResponse); + } + + @ExceptionHandler(Exception.class) public ResponseEntity handleOtherException(Exception ex) { - ex.printStackTrace(); String errorMessage = ex.getMessage(); + log.error("알 수 없는 예외 발생 : {}", errorMessage); ErrorResponse errorResponse = new ErrorResponse(NOT_DEFINED_ERROR, errorMessage); - return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(NOT_DEFINED_ERROR.getCode())); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 5d31223da..ea1507b04 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -4,42 +4,58 @@ import lombok.Getter; import org.springframework.http.HttpStatus; -import static com.example.solidconnection.constants.Constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; +import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; +import static com.example.solidconnection.siteuser.service.SiteUserService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; @Getter @AllArgsConstructor public enum ErrorCode { - INVALID_TEST_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않은 어학 시험 종류입니다."), - APPLICATION_NOT_APPROVED(HttpStatus.BAD_REQUEST.value(), "성적표가 인증되지 않았습니다."), - APPLY_UPDATE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST.value(), "지원 정보 수정은 3회까지만 가능합니다."), - CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2지망에 동일한 대학교를 입력할 수 없습니다."), - APPLICATION_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "성적을 입력하지 않았습니다."), - INVALID_INPUT(HttpStatus.BAD_REQUEST.value(), "값을 입력할 수 없습니다."), - CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 "+MIN_DAYS_BETWEEN_NICKNAME_CHANGES+"일이 지나지 않았습니다."), - UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교 지원 정보입니다."), - UNIVERSITY_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 대학교입니다."), - REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST.value(), "리다이렉트 uri가 잘못되었습니다."), - NOT_DEFINED_ERROR(HttpStatus.BAD_REQUEST.value(), "에러가 발생했습니다."), + + // kakao + KAKAO_REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST.value(), "리다이렉트 uri가 잘못되었습니다."), + INVALID_OR_EXPIRED_KAKAO_AUTH_CODE(HttpStatus.BAD_REQUEST.value(), "사용할 수 없는 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), + KAKAO_ACCESS_TOKEN_FAIL(HttpStatus.NOT_FOUND.value(), "카카오 엑세스 토큰 발급에 실패했습니다"), + KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(), "카카오 사용자 정보 조회에 실패했습니다."), + INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN(HttpStatus.BAD_REQUEST.value(), "우리 서비스에서 발급한 카카오 토큰이 아닙니다"), + + // data not found + UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 대학교 지원 정보입니다."), + UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND_FOR_TERM(HttpStatus.NOT_FOUND.value(), "해당하는 대학교가 이번 모집 기간에 열리지 않았습니다."), + APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "사용자의 대학 지원 정보를 찾을 수 없습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원을 찾을 수 없습니다."), + UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "대학교를 찾을 수 없습니다."), + REGION_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 지역을 찾을 수 없습니다."), + COUNTRY_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 국가를 찾을 수 없습니다."), + + // auth + USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), + ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), + REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), + + // s3 S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), S3_CLIENT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 클라이언트 에러 발생"), - FILE_NOT_EXIST(HttpStatus.UNAUTHORIZED.value(), "파일이 없습니다."), - INVALID_FILE_EXTENSIONS(HttpStatus.BAD_REQUEST.value(), "파일 형식이 유효하지 않습니다."), + FILE_NOT_EXIST(HttpStatus.BAD_REQUEST.value(), "파일이 없습니다."), NOT_ALLOWED_FILE_EXTENSIONS(HttpStatus.BAD_REQUEST.value(), "허용되지 않은 확장자입니다."), - USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), + INVALID_FILE_EXTENSIONS(HttpStatus.BAD_REQUEST.value(), "파일 형식이 유효하지 않습니다."), + + // invalid operation + SCORE_SHOULD_SUBMITTED_FIRST(HttpStatus.BAD_REQUEST.value(), "성적을 먼저 제출해주세요."), USER_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 회원입니다."), - JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱 에러"), - INVALID_REGION_NAME(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 지역명입니다."), - INVALID_COUNTRY_NAME(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 국가명입니다."), - INVALID_BIRTH_FORMAT(HttpStatus.BAD_REQUEST.value(), "잘못된 생년월일 형식입니다."), NICKNAME_ALREADY_EXISTED(HttpStatus.CONFLICT.value(), "이미 존재하는 닉네임입니다."), - INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), - AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), - USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "회원 정보를 찾을 수 없습니다."), - INVALID_KAKAO_AUTH_CODE(HttpStatus.BAD_REQUEST.value(),"사용할 수 없는 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), - KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(),"카카오 사용자 정보 조회에 실패했습니다."), - ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), - INVALID_KAKAO_TOKEN(HttpStatus.UNAUTHORIZED.value(),"사용할 수 없는 카카오 로그인 토큰입니다."), - REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(),"리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), + INVALID_TEST_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않은 어학 시험 종류입니다."), + APPLICATION_NOT_APPROVED(HttpStatus.BAD_REQUEST.value(), "성적표가 인증되지 않았습니다."), + APPLY_UPDATE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST.value(), "지원 정보 수정은 " + APPLICATION_UPDATE_COUNT_LIMIT + "회까지만 가능합니다."), + CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2지망에 동일한 대학교를 입력할 수 없습니다."), + CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 " + MIN_DAYS_BETWEEN_NICKNAME_CHANGES + "일이 지나지 않았습니다."), + + // general + JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), + JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."), + INVALID_INPUT(HttpStatus.BAD_REQUEST.value(), "값을 입력할 수 없습니다."), + NOT_DEFINED_ERROR(HttpStatus.BAD_REQUEST.value(), "에러가 발생했습니다."), ; private final int code; diff --git a/src/main/java/com/example/solidconnection/custom/exception/JwtExpiredTokenException.java b/src/main/java/com/example/solidconnection/custom/exception/JwtExpiredTokenException.java index 6fe911b8a..b0a52e9fa 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/JwtExpiredTokenException.java +++ b/src/main/java/com/example/solidconnection/custom/exception/JwtExpiredTokenException.java @@ -3,7 +3,8 @@ import org.springframework.security.core.AuthenticationException; public class JwtExpiredTokenException extends AuthenticationException { + public JwtExpiredTokenException(String msg) { super(msg); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/custom/response/CustomResponse.java b/src/main/java/com/example/solidconnection/custom/response/CustomResponse.java deleted file mode 100644 index 18c5d35b2..000000000 --- a/src/main/java/com/example/solidconnection/custom/response/CustomResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.solidconnection.custom.response; - -public class CustomResponse { -} diff --git a/src/main/java/com/example/solidconnection/custom/response/DataResponse.java b/src/main/java/com/example/solidconnection/custom/response/DataResponse.java deleted file mode 100644 index c92e6ec43..000000000 --- a/src/main/java/com/example/solidconnection/custom/response/DataResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.solidconnection.custom.response; - -import lombok.Getter; - -@Getter -public class DataResponse extends CustomResponse { - private final Boolean success = true; - private final T data; - - public DataResponse(T data){ - this.data = data; - } -} diff --git a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java index 5ad96a938..22c173f1d 100644 --- a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java +++ b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java @@ -2,29 +2,14 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -@Getter -@Setter -public class ErrorResponse extends CustomResponse { - private final Boolean success = false; - private ErrorDetail error; - - @Getter - @AllArgsConstructor - private static class ErrorDetail { - int code; - String message; - } +public record ErrorResponse(String message) { public ErrorResponse(CustomException e) { - this.error = new ErrorDetail(e.getCode(), e.getMessage()); + this(e.getMessage()); } - public ErrorResponse(ErrorCode e, String detail){ - String detailedMessage = e.getMessage() + " : " + detail; - this.error = new ErrorDetail(e.getCode(), detailedMessage); + public ErrorResponse(ErrorCode e, String detail) { + this(e.getMessage() + " : " + detail); } } diff --git a/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java b/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java deleted file mode 100644 index 6a1e5d453..000000000 --- a/src/main/java/com/example/solidconnection/custom/response/StatusResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.solidconnection.custom.response; - -import lombok.Getter; - -@Getter -public class StatusResponse extends CustomResponse { - private final boolean status; - - public StatusResponse(boolean status) { - this.status = status; - } -} diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java index 53af94c94..5d992adaf 100644 --- a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java +++ b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java @@ -1,19 +1,20 @@ package com.example.solidconnection.custom.userdetails; -import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; -public class CustomUserDetails implements UserDetails { +public class CustomUserDetails implements UserDetails {//todo: principal 을 썼을 때 바로 SiteUser를 반환하게 하면 안되나?? + private final SiteUser siteUser; public CustomUserDetails(SiteUser siteUser) { this.siteUser = siteUser; } - public String getEmail(){ + public String getEmail() { return siteUser.getEmail(); } diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java index 51cfb78b9..c9f1b1606 100644 --- a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java +++ b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java @@ -1,7 +1,7 @@ package com.example.solidconnection.custom.userdetails; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; @@ -13,6 +13,7 @@ @Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { + private final SiteUserRepository siteUserRepository; @Override diff --git a/src/main/java/com/example/solidconnection/entity/Application.java b/src/main/java/com/example/solidconnection/entity/Application.java deleted file mode 100644 index 363a2eee3..000000000 --- a/src/main/java/com/example/solidconnection/entity/Application.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.example.solidconnection.entity; - -import com.example.solidconnection.application.dto.ScoreRequestDto; -import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.type.VerifyStatus; -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.DynamicInsert; - -@Entity -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -@DynamicInsert -public class Application { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, length = 10) - @Enumerated(EnumType.STRING) - private LanguageTestType languageTestType; - - @Column(nullable = false) - private String languageTestScore; - - @Column(nullable = false, length = 500) - private String languageTestReportUrl; - - @Column(nullable = false) - private Float gpa; - - @Column(nullable = false) - private Float gpaCriteria; - - @Column(nullable = false, length = 500) - private String gpaReportUrl; - - @Column(nullable = false, length = 50) - @Enumerated(EnumType.STRING) - private VerifyStatus verifyStatus; - - @Column(length = 100) - private String nicknameForApply; - - @Column(nullable = false) - private Integer updateCount; - - // 연관 관계 - @ManyToOne - @JoinColumn(name = "first_choice_univ_id") - private UniversityInfoForApply firstChoiceUniversity; - - @ManyToOne - @JoinColumn(name = "second_choice_univ_id") - private UniversityInfoForApply secondChoiceUniversity; - - @ManyToOne - @JoinColumn(name = "site_user_id") - private SiteUser siteUser; - - public static Application saveScore(SiteUser siteUser, ScoreRequestDto scoreRequestDto) { - return Application.builder() - .siteUser(siteUser) - .languageTestType(scoreRequestDto.getLanguageTestType()) - .languageTestScore(scoreRequestDto.getLanguageTestScore()) - .languageTestReportUrl(scoreRequestDto.getLanguageTestReportUrl()) - .gpa(scoreRequestDto.getGpa()) - .gpaCriteria(scoreRequestDto.getGpaCriteria()) - .gpaReportUrl(scoreRequestDto.getGpaReportUrl()) - .verifyStatus(VerifyStatus.PENDING) - .build(); - } - - public static Application saveUniversity(SiteUser siteUser, UniversityInfoForApply firstChoiceUniversity, - UniversityInfoForApply secondChoiceUniversity) { - return Application.builder() - .siteUser(siteUser) - .firstChoiceUniversity(firstChoiceUniversity) - .secondChoiceUniversity(secondChoiceUniversity) - .build(); - } -} diff --git a/src/main/java/com/example/solidconnection/entity/Country.java b/src/main/java/com/example/solidconnection/entity/Country.java index 870b40f52..0a5d974d7 100644 --- a/src/main/java/com/example/solidconnection/entity/Country.java +++ b/src/main/java/com/example/solidconnection/entity/Country.java @@ -1,19 +1,33 @@ package com.example.solidconnection.entity; -import com.example.solidconnection.type.CountryCode; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NoArgsConstructor; -@Entity @Getter +@EqualsAndHashCode(of = {"code", "koreanName"}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity public class Country { + @Id - @Column(length = 2, name = "country_code", columnDefinition = "VARCHAR(2)") - @Enumerated(EnumType.STRING) - private CountryCode code; + @Column(length = 2) + private String code; + + @Column(nullable = false, length = 100) + private String koreanName; - // 연관 관계 @ManyToOne - @JoinColumn(name = "region_code", referencedColumnName="region_code") private Region region; + + public Country(String code, String koreanName, Region region) { + this.code = code; + this.koreanName = koreanName; + this.region = region; + } } diff --git a/src/main/java/com/example/solidconnection/entity/InterestedCountry.java b/src/main/java/com/example/solidconnection/entity/InterestedCountry.java index baadab398..8b8b4e735 100644 --- a/src/main/java/com/example/solidconnection/entity/InterestedCountry.java +++ b/src/main/java/com/example/solidconnection/entity/InterestedCountry.java @@ -1,27 +1,32 @@ package com.example.solidconnection.entity; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -@Entity -@Builder -@NoArgsConstructor -@AllArgsConstructor @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity public class InterestedCountry { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // 연관 관계 @ManyToOne - @JoinColumn(name = "site_user_id") private SiteUser siteUser; @ManyToOne - @JoinColumn(name = "country_code") private Country country; + + public InterestedCountry(SiteUser siteUser, Country country) { + this.siteUser = siteUser; + this.country = country; + } } diff --git a/src/main/java/com/example/solidconnection/entity/InterestedRegion.java b/src/main/java/com/example/solidconnection/entity/InterestedRegion.java index 8bc83ee15..7ec8fa50c 100644 --- a/src/main/java/com/example/solidconnection/entity/InterestedRegion.java +++ b/src/main/java/com/example/solidconnection/entity/InterestedRegion.java @@ -1,27 +1,32 @@ package com.example.solidconnection.entity; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -@Entity -@Builder -@NoArgsConstructor -@AllArgsConstructor @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity public class InterestedRegion { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // 연관 관계 @ManyToOne - @JoinColumn(name = "site_user_id") private SiteUser siteUser; @ManyToOne - @JoinColumn(name = "region_code") private Region region; + + public InterestedRegion(SiteUser siteUser, Region region) { + this.siteUser = siteUser; + this.region = region; + } } diff --git a/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java b/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java deleted file mode 100644 index 1ce6d175a..000000000 --- a/src/main/java/com/example/solidconnection/entity/LanguageRequirement.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.solidconnection.entity; - -import com.example.solidconnection.type.LanguageTestType; -import jakarta.persistence.*; -import lombok.Getter; - -@Entity -@Getter -public class LanguageRequirement { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, length = 10) - @Enumerated(EnumType.STRING) - private LanguageTestType languageTestType; - - @Column(nullable = false) - private String minScore; - - // 연관 관계 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "university_info_for_apply_id") - private UniversityInfoForApply universityInfoForApply; -} diff --git a/src/main/java/com/example/solidconnection/entity/Region.java b/src/main/java/com/example/solidconnection/entity/Region.java index aaff18137..6bd64c5cc 100644 --- a/src/main/java/com/example/solidconnection/entity/Region.java +++ b/src/main/java/com/example/solidconnection/entity/Region.java @@ -1,14 +1,28 @@ package com.example.solidconnection.entity; -import com.example.solidconnection.type.RegionCode; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NoArgsConstructor; -@Entity @Getter +@EqualsAndHashCode(of = {"code", "koreanName"}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity public class Region { + @Id - @Column(length = 10, name = "region_code", columnDefinition = "VARCHAR(10)") - @Enumerated(EnumType.STRING) - private RegionCode code; + @Column(length = 10) + private String code; + + @Column(nullable = false, length = 100) + private String koreanName; + + public Region(String code, String koreanName) { + this.code = code; + this.koreanName = koreanName; + } } diff --git a/src/main/java/com/example/solidconnection/entity/SiteUser.java b/src/main/java/com/example/solidconnection/entity/SiteUser.java deleted file mode 100644 index 487bdc91b..000000000 --- a/src/main/java/com/example/solidconnection/entity/SiteUser.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.solidconnection.entity; - -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import jakarta.persistence.*; -import lombok.*; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.Set; - -@Entity -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Getter -public class SiteUser { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, length = 100) - private String email; - - @Column(nullable = false, length = 100) - @Setter - private String nickname; - - @Column(length = 500) - @Setter - private String profileImageUrl; - - @Column(nullable = false, length = 20) - private String birth; - - @Column(nullable = false, length = 50) - @Enumerated(EnumType.STRING) - private PreparationStatus preparationStage; - - @Column(nullable = false, length = 50) - @Enumerated(EnumType.STRING) - private Role role; - - @Column(nullable = false, length = 20) - @Enumerated(EnumType.STRING) - private Gender gender; - - @Setter - private LocalDateTime nicknameModifiedAt; - - @Setter - private LocalDate quitedAt; - - // 연관관계 - @OneToMany(mappedBy = "siteUser") - private Set interestedRegions; - - @OneToMany(mappedBy = "siteUser") - private Set interestedCountries; - - @OneToMany(mappedBy = "siteUser") - private Set applications; -} diff --git a/src/main/java/com/example/solidconnection/home/controller/HomeController.java b/src/main/java/com/example/solidconnection/home/controller/HomeController.java deleted file mode 100644 index 93247fbcb..000000000 --- a/src/main/java/com/example/solidconnection/home/controller/HomeController.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.solidconnection.home.controller; - -import com.example.solidconnection.custom.response.CustomResponse; -import com.example.solidconnection.custom.response.DataResponse; -import com.example.solidconnection.home.dto.PersonalHomeInfoDto; -import com.example.solidconnection.university.service.UniversityService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.security.Principal; - -@RestController -@RequestMapping("/home") -@RequiredArgsConstructor -public class HomeController { - - private final UniversityService universityService; - - @GetMapping - public CustomResponse getHomeInfo(Principal principal) { - PersonalHomeInfoDto personalHomeInfoDto = new PersonalHomeInfoDto(); - if (principal == null) { - personalHomeInfoDto.setRecommendedUniversities(universityService.getGeneralRecommends()); - } else { - personalHomeInfoDto.setRecommendedUniversities(universityService.getPersonalRecommends(principal.getName())); - } - return new DataResponse<>(personalHomeInfoDto); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/home/dto/PersonalHomeInfoDto.java b/src/main/java/com/example/solidconnection/home/dto/PersonalHomeInfoDto.java deleted file mode 100644 index 56ed95fe2..000000000 --- a/src/main/java/com/example/solidconnection/home/dto/PersonalHomeInfoDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.solidconnection.home.dto; - -import lombok.*; - -import java.util.List; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class PersonalHomeInfoDto { - private List recommendedUniversities; -} diff --git a/src/main/java/com/example/solidconnection/home/dto/RecommendedUniversityDto.java b/src/main/java/com/example/solidconnection/home/dto/RecommendedUniversityDto.java deleted file mode 100644 index ddd5f8b14..000000000 --- a/src/main/java/com/example/solidconnection/home/dto/RecommendedUniversityDto.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.solidconnection.home.dto; - -import com.example.solidconnection.entity.UniversityInfoForApply; -import lombok.*; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class RecommendedUniversityDto { - private long id; - private String koreanName; - private String backgroundImgUrl; - - public static RecommendedUniversityDto fromEntity(UniversityInfoForApply universityInfoForApply){ - return RecommendedUniversityDto.builder() - .id(universityInfoForApply.getId()) - .backgroundImgUrl(universityInfoForApply.getUniversity().getBackgroundImageUrl()) - .koreanName(universityInfoForApply.getUniversity().getKoreanName()) - .build(); - } -} diff --git a/src/main/java/com/example/solidconnection/repositories/CountryRepository.java b/src/main/java/com/example/solidconnection/repositories/CountryRepository.java index 6ef237e2e..aff9470b4 100644 --- a/src/main/java/com/example/solidconnection/repositories/CountryRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/CountryRepository.java @@ -1,13 +1,27 @@ package com.example.solidconnection.repositories; import com.example.solidconnection.entity.Country; -import com.example.solidconnection.type.CountryCode; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface CountryRepository extends JpaRepository { - Optional findByCode(CountryCode countryCode); + + Optional findByKoreanName(String koreanName); + +/* default Country getByKoreanName(String koreanName) { + return findByKoreanName(koreanName) + .orElseThrow(() -> new CustomException(COUNTRY_NOT_FOUND_BY_KOREAN_NAME)); + }*/ + + @Query("SELECT c FROM Country c WHERE c.koreanName IN :names") + List findByKoreanNames(@Param(value = "names") List names); + + @Query("SELECT c FROM Country c WHERE c.koreanName LIKE %:keyword%") + List findByKoreanNameContaining(@Param("keyword") String keyword); } diff --git a/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java b/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java index 1a5ba0f23..68e10b320 100644 --- a/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/InterestedCountyRepository.java @@ -1,7 +1,7 @@ package com.example.solidconnection.repositories; import com.example.solidconnection.entity.InterestedCountry; -import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java b/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java index ad3976b5d..df5acd696 100644 --- a/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/InterestedRegionRepository.java @@ -1,7 +1,7 @@ package com.example.solidconnection.repositories; import com.example.solidconnection.entity.InterestedRegion; -import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/solidconnection/repositories/RegionRepository.java b/src/main/java/com/example/solidconnection/repositories/RegionRepository.java index 8d8746480..9a26233bc 100644 --- a/src/main/java/com/example/solidconnection/repositories/RegionRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/RegionRepository.java @@ -1,13 +1,24 @@ package com.example.solidconnection.repositories; import com.example.solidconnection.entity.Region; -import com.example.solidconnection.type.RegionCode; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface RegionRepository extends JpaRepository { - Optional findByCode(RegionCode regionCode); + + Optional findByKoreanName(String koreanName); + +/* default Region getByKoreanName(String koreanName) { + return findByKoreanName(koreanName) + .orElseThrow(() -> new CustomException(REGION_NOT_FOUND_BY_KOREAN_NAME)); + }*/ + + @Query("SELECT r FROM Region r WHERE r.koreanName IN :names") + List findByKoreanNames(@Param(value = "names") List names); } diff --git a/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java b/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java index 8f027505a..c12f067dd 100644 --- a/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java +++ b/src/main/java/com/example/solidconnection/s3/AmazonS3Config.java @@ -10,6 +10,7 @@ @Configuration public class AmazonS3Config { + @Value("${cloud.aws.credentials.access-key}") private String accessKey; diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java index 33d240591..8da2984ea 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -1,9 +1,8 @@ package com.example.solidconnection.s3; -import com.example.solidconnection.custom.response.CustomResponse; -import com.example.solidconnection.custom.response.DataResponse; import com.example.solidconnection.type.ImgType; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -13,33 +12,38 @@ import java.security.Principal; @RequiredArgsConstructor -@RestController @RequestMapping("/file") -public class S3Controller { +@RestController +public class S3Controller implements S3ControllerSwagger { + private final S3Service s3Service; @PostMapping("/profile/pre") - public CustomResponse uploadPreProfileImage(@RequestParam("file") MultipartFile imageFile) { - UploadedFileURLDto profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); - return new DataResponse<>(profileImageUrl); + public ResponseEntity uploadPreProfileImage( + @RequestParam("file") MultipartFile imageFile) { + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); + return ResponseEntity.ok(profileImageUrl); } @PostMapping("/profile/post") - public CustomResponse uploadPostProfileImage(@RequestParam("file") MultipartFile imageFile, Principal principal) { - UploadedFileURLDto profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); + public ResponseEntity uploadPostProfileImage( + @RequestParam("file") MultipartFile imageFile, Principal principal) { + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); s3Service.deleteExProfile(principal.getName()); - return new DataResponse<>(profileImageUrl); + return ResponseEntity.ok(profileImageUrl); } @PostMapping("/gpa") - public CustomResponse uploadGpaImage(@RequestParam("file") MultipartFile imageFile) { - UploadedFileURLDto profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA); - return new DataResponse<>(profileImageUrl); + public ResponseEntity uploadGpaImage( + @RequestParam("file") MultipartFile imageFile) { + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA); + return ResponseEntity.ok(profileImageUrl); } @PostMapping("/language-test") - public CustomResponse uploadLanguageImage(@RequestParam("file") MultipartFile imageFile) { - UploadedFileURLDto profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST); - return new DataResponse<>(profileImageUrl); + public ResponseEntity uploadLanguageImage( + @RequestParam("file") MultipartFile imageFile) { + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST); + return ResponseEntity.ok(profileImageUrl); } } diff --git a/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java b/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java new file mode 100644 index 000000000..aa432b195 --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java @@ -0,0 +1,119 @@ +package com.example.solidconnection.s3; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import java.security.Principal; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@Tag(name = "ImageUpload", description = "S3 파일 업로드 API") +public interface S3ControllerSwagger { + + @Operation( + summary = "회원가입 전 프로필 이미지 업로드 - 프로필 이미지 설정", + requestBody = @RequestBody( + description = "업로드할 프로필 이미지 파일", + required = true, + content = @Content( + mediaType = "multipart/form-data", + schema = @Schema(implementation = MultipartFile.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "프로필 이미지 업로드 성공, URL 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UploadedFileUrlResponse.class) + ) + ) + } + ) + ResponseEntity uploadPreProfileImage(@RequestParam("file") MultipartFile imageFile); + + @SecurityRequirements + @SecurityRequirement(name = ACCESS_TOKEN) + @Operation( + summary = "회원가입 후 프로필 이미지 업로드 - 프로필 이미지 수정", + requestBody = @RequestBody( + description = "업로드할 프로필 이미지 파일", + required = true, + content = @Content( + mediaType = "multipart/form-data", + schema = @Schema(implementation = MultipartFile.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "프로필 이미지 업로드 성공 후 기존 이미지 삭제, URL 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UploadedFileUrlResponse.class) + ) + ) + } + ) + ResponseEntity uploadPostProfileImage(@RequestParam("file") MultipartFile imageFile, Principal principal); + + @SecurityRequirements + @SecurityRequirement(name = ACCESS_TOKEN) + @Operation( + summary = "GPA 증명서 이미지 업로드", + requestBody = @RequestBody( + description = "업로드할 GPA 증명서 이미지 파일", + required = true, + content = @Content( + mediaType = "multipart/form-data", + schema = @Schema(implementation = MultipartFile.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "GPA 증명서 이미지 업로드 성공, URL 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UploadedFileUrlResponse.class) + ) + ) + } + ) + ResponseEntity uploadGpaImage(@RequestParam("file") MultipartFile imageFile); + + @SecurityRequirements + @SecurityRequirement(name = ACCESS_TOKEN) + @Operation( + summary = "어학 시험 증명서 이미지 업로드", + requestBody = @RequestBody( + description = "업로드할 어학 시험 증명서 이미지 파일", + required = true, + content = @Content( + mediaType = "multipart/form-data", + schema = @Schema(implementation = MultipartFile.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "어학 시험 증명서 이미지 업로드 성공, URL 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UploadedFileUrlResponse.class) + ) + ) + } + ) + ResponseEntity uploadLanguageImage(@RequestParam("file") MultipartFile imageFile); +} diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 2c2b37b98..49e515872 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -8,10 +8,12 @@ import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.siteuser.service.SiteUserValidator; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.ImgType; import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -22,39 +24,56 @@ import java.util.Objects; import java.util.UUID; -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.custom.exception.ErrorCode.FILE_NOT_EXIST; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_FILE_EXTENSIONS; +import static com.example.solidconnection.custom.exception.ErrorCode.NOT_ALLOWED_FILE_EXTENSIONS; +import static com.example.solidconnection.custom.exception.ErrorCode.S3_CLIENT_EXCEPTION; +import static com.example.solidconnection.custom.exception.ErrorCode.S3_SERVICE_EXCEPTION; @Service @RequiredArgsConstructor public class S3Service { - @Value("${cloud.aws.s3.bucket}") - private String bucket; + private static final Logger log = LoggerFactory.getLogger(S3Service.class); private final AmazonS3Client amazonS3; - private final SiteUserValidator siteUserValidator; + private final SiteUserRepository siteUserRepository; + @Value("${cloud.aws.s3.bucket}") + private String bucket; - public UploadedFileURLDto uploadFile(MultipartFile multipartFile, ImgType imageFile) { + /* + * 파일을 S3에 업로드한다. + * - 파일이 존재하는지 검증한다. + * - 파일 확장자가 허용된 확장자인지 검증한다. + * - 파일에 대한 메타 데이터를 생성한다. + * - 임의의 랜덤한 문자열로 파일 이름을 생성한다. + * - S3에 파일을 업로드한다. + * */ + public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType imageFile) { + // 파일 검증 validateImgFile(multipartFile); + + // 메타데이터 생성 String contentType = multipartFile.getContentType(); ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(contentType); metadata.setContentLength(multipartFile.getSize()); + // 파일 이름 생성 UUID randomUUID = UUID.randomUUID(); - String fileName = imageFile.getType() + "/"+ randomUUID; + String fileName = imageFile.getType() + "/" + randomUUID; try { amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata) .withCannedAcl(CannedAccessControlList.PublicRead)); } catch (AmazonServiceException e) { - e.printStackTrace(); + log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage()); throw new CustomException(S3_SERVICE_EXCEPTION); } catch (SdkClientException | IOException e) { - e.printStackTrace(); + log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); throw new CustomException(S3_CLIENT_EXCEPTION); } - return new UploadedFileURLDto(amazonS3.getUrl(bucket, fileName).toString()); + return new UploadedFileUrlResponse(amazonS3.getUrl(bucket, fileName).toString()); } private void validateImgFile(MultipartFile file) { @@ -79,7 +98,12 @@ private String getFileExtension(String fileName) { return fileName.substring(dotIndex + 1); } - public void deleteExProfile(String email){ + /* + * 기존 파일을 삭제한다. + * - 기존 파일의 key(S3파일명)를 찾는다. + * - S3에서 파일을 삭제한다. + * */ + public void deleteExProfile(String email) { String key = getExProfileImageUrl(email); deleteFile(key); } @@ -88,16 +112,16 @@ private void deleteFile(String fileName) { try { amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); } catch (AmazonServiceException e) { - e.printStackTrace(); + log.error("파일 삭제 중 s3 서비스 예외 발생 : {}", e.getMessage()); throw new CustomException(S3_SERVICE_EXCEPTION); } catch (SdkClientException e) { - e.printStackTrace(); + log.error("파일 삭제 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); throw new CustomException(S3_CLIENT_EXCEPTION); } } - private String getExProfileImageUrl(String email){ - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); + private String getExProfileImageUrl(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); String fileName = siteUser.getProfileImageUrl(); int domainStartIndex = fileName.indexOf(".com"); return fileName.substring(domainStartIndex + 5); diff --git a/src/main/java/com/example/solidconnection/s3/UploadedFileURLDto.java b/src/main/java/com/example/solidconnection/s3/UploadedFileURLDto.java deleted file mode 100644 index 5af4a0503..000000000 --- a/src/main/java/com/example/solidconnection/s3/UploadedFileURLDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.solidconnection.s3; - -import lombok.*; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -public class UploadedFileURLDto { - private String fileUrl; -} diff --git a/src/main/java/com/example/solidconnection/s3/UploadedFileUrlResponse.java b/src/main/java/com/example/solidconnection/s3/UploadedFileUrlResponse.java new file mode 100644 index 000000000..090f206f7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/UploadedFileUrlResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.s3; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "업로드된 파일의 URL 응답") +public record UploadedFileUrlResponse( + @Schema(description = "파일 URL", example = "http://example.com/uploads/profile.jpg") + String fileUrl) { +} diff --git a/src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java b/src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java index 07c67ca2c..0af509833 100644 --- a/src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java +++ b/src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java @@ -1,17 +1,30 @@ package com.example.solidconnection.scheduler; -import com.example.solidconnection.siteuser.service.SiteUserService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -@Component +import java.time.LocalDate; +import java.util.List; + @RequiredArgsConstructor +@Component public class UserRemovalScheduler { - private final SiteUserService siteUserService; - @Scheduled(cron = "0 0 0 * * ?") // 매일 자정에 실행 + public static final String EVERY_MIDNIGHT = "0 0 0 * * ?"; + public static final int ACCOUNT_RECOVER_DURATION = 30; + + private final SiteUserRepository siteUserRepository; + + /* + * 탈퇴 후 계정 복구 기한까지 방문하지 않은 사용자를 삭제한다. + * */ + @Scheduled(cron = EVERY_MIDNIGHT) public void scheduledUserRemoval() { - siteUserService.deleteUsersNeverVisitedAfterQuited(); + LocalDate cutoffDate = LocalDate.now().minusDays(ACCOUNT_RECOVER_DURATION); + List usersToRemove = siteUserRepository.findUsersToBeRemoved(cutoffDate); + siteUserRepository.deleteAll(usersToRemove); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java b/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java deleted file mode 100644 index 75463e0c0..000000000 --- a/src/main/java/com/example/solidconnection/siteuser/controller/MyPageController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.solidconnection.siteuser.controller; - -import com.example.solidconnection.custom.response.CustomResponse; -import com.example.solidconnection.custom.response.DataResponse; -import com.example.solidconnection.custom.response.StatusResponse; -import com.example.solidconnection.siteuser.dto.MyPageDto; -import com.example.solidconnection.siteuser.dto.MyPageUpdateDto; -import com.example.solidconnection.siteuser.service.MyPageService; -import com.example.solidconnection.university.dto.UniversityPreviewDto; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.security.Principal; -import java.util.List; - -@RestController -@RequestMapping("/my-page") -@RequiredArgsConstructor -class MyPageController { - private final MyPageService myPageService; - - @GetMapping - public CustomResponse getMyPageInfo(Principal principal) { - MyPageDto myPageDto = myPageService.getMyPageInfo(principal.getName()); - return new DataResponse<>(myPageDto); - } - - @GetMapping("/update") - public CustomResponse getMyPageInfoToUpdate(Principal principal) { - MyPageUpdateDto myPageUpdateDto = myPageService.getMyPageInfoToUpdate(principal.getName()); - return new DataResponse<>(myPageUpdateDto); - } - - @PatchMapping("/update") - public CustomResponse update(Principal principal, @Valid @RequestBody MyPageUpdateDto myPageUpdateDto) { - myPageService.update(principal.getName(), myPageUpdateDto); - return new StatusResponse(true); - } - - @GetMapping("/wish-university") - public CustomResponse getWishUniversity(Principal principal) { - List wishUniversities = myPageService.getWishUniversity(principal.getName()); - return new DataResponse<>(wishUniversities); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java new file mode 100644 index 000000000..d7fe1a1fc --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -0,0 +1,47 @@ +package com.example.solidconnection.siteuser.controller; + +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.MyPageUpdateRequest; +import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.service.SiteUserService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +@RequiredArgsConstructor +@RequestMapping("/my-page") +@RestController +class SiteUserController implements SiteUserControllerSwagger { + + private final SiteUserService siteUserService; + + @GetMapping + public ResponseEntity getMyPageInfo(Principal principal) { + MyPageResponse myPageResponse = siteUserService.getMyPageInfo(principal.getName()); + return ResponseEntity + .ok(myPageResponse); + } + + @GetMapping("/update") + public ResponseEntity getMyPageInfoToUpdate(Principal principal) { + MyPageUpdateResponse myPageUpdateDto = siteUserService.getMyPageInfoToUpdate(principal.getName()); + return ResponseEntity + .ok(myPageUpdateDto); + } + + @PatchMapping("/update") + public ResponseEntity updateMyPageInfo( + Principal principal, + @Valid @RequestBody MyPageUpdateRequest myPageUpdateDto) { + MyPageUpdateResponse myPageUpdateResponse = siteUserService.update(principal.getName(), myPageUpdateDto); + return ResponseEntity + .ok(myPageUpdateResponse); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java new file mode 100644 index 000000000..e17b41c3e --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java @@ -0,0 +1,78 @@ +package com.example.solidconnection.siteuser.controller; + +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.MyPageUpdateRequest; +import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; + +import java.security.Principal; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@Tag(name = "SiteUser", description = "사용자 API") +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public interface SiteUserControllerSwagger { + + @Operation( + summary = "마이 페이지 페이지 정보 조회", + responses = { + @ApiResponse( + responseCode = "200", + description = "마이 페이지 정보 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = MyPageResponse.class) + ) + ) + } + ) + ResponseEntity getMyPageInfo(Principal principal); + + @Operation( + summary = "마이 페이지 정보 수정을 위한 데이터 조회", + responses = { + @ApiResponse( + responseCode = "200", + description = "수정 가능한 정보 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = MyPageUpdateResponse.class) + ) + ) + } + ) + ResponseEntity getMyPageInfoToUpdate(Principal principal); + + @Operation( + summary = "마이 페이지 정보 수정", + requestBody = @RequestBody( + description = "업데이트할 정보", + required = true, + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = MyPageUpdateRequest.class) + ) + ), + responses = { + @ApiResponse( + responseCode = "200", + description = "마이 페이지 정보 수정 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = MyPageUpdateResponse.class) + ) + ) + } + ) + ResponseEntity updateMyPageInfo(Principal principal, @Valid @RequestBody MyPageUpdateRequest myPageUpdateDto); +} diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java new file mode 100644 index 000000000..b13a26e9a --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -0,0 +1,78 @@ +package com.example.solidconnection.siteuser.domain; + +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class SiteUser { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String email; + + @Setter + @Column(nullable = false, length = 100) + private String nickname; + + @Setter + @Column(length = 500) + private String profileImageUrl; + + @Column(nullable = false, length = 20) + private String birth; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private PreparationStatus preparationStage; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Role role; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Gender gender; + + @Setter + private LocalDateTime nicknameModifiedAt; + + @Setter + private LocalDate quitedAt; + + public SiteUser( + String email, + String nickname, + String profileImageUrl, + String birth, + PreparationStatus preparationStage, + Role role, + Gender gender) { + this.email = email; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.birth = birth; + this.preparationStage = preparationStage; + this.role = role; + this.gender = gender; + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageDto.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageDto.java deleted file mode 100644 index d0de0d0fa..000000000 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageDto.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.solidconnection.siteuser.dto; - -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.type.Role; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Getter -public class MyPageDto { - private String nickname; - private String profileImageUrl; - private Role role; - private String birth; - private int likedPostCount; - private int likedMentorCount; - private int likedUniversityCount; - - public static MyPageDto fromEntity(SiteUser siteUser, int likedUniversityCount){ - return MyPageDto.builder() - .nickname(siteUser.getNickname()) - .profileImageUrl(siteUser.getProfileImageUrl()) - .role(siteUser.getRole()) - .birth(siteUser.getBirth()) - .likedUniversityCount(likedUniversityCount) - .likedMentorCount(0) // TODO: 멘토 기능 생기면 업데이트 필요 - .likedPostCount(0) // TODO: 커뮤니티 기능 생기면 업데이트 필요 - .build(); - } -} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java new file mode 100644 index 000000000..0f035c905 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java @@ -0,0 +1,42 @@ +package com.example.solidconnection.siteuser.dto; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.Role; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "마이페이지 페이지 정보 응답") +public record MyPageResponse( + @Schema(description = "닉네임", example = "nickname") + String nickname, + + @Schema(description = "프로필 이미지 URL", example = "http://example.com/profile.jpg") + String profileImageUrl, + + @Schema(description = "역할", example = "MENTEE") + Role role, + + @Schema(description = "생년월일", example = "1990-01-01") + String birth, + + @Schema(description = "좋아요 누른 게시물 수", example = "0") + int likedPostCount, + + @Schema(description = "좋아요 누른 멘토 수", example = "0") + int likedMentorCount, + + @Schema(description = "좋아요 누른 대학 수", example = "3") + int likedUniversityCount) { + + public static MyPageResponse of(SiteUser siteUser, int likedUniversityCount) { + return new MyPageResponse( + siteUser.getNickname(), + siteUser.getProfileImageUrl(), + siteUser.getRole(), + siteUser.getBirth(), + 0, // TODO: 커뮤니티 기능 생기면 업데이트 필요 + 0, // TODO: 멘토 기능 생기면 업데이트 필요 + likedUniversityCount + ); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateDto.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateDto.java deleted file mode 100644 index e3dad0103..000000000 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.solidconnection.siteuser.dto; - -import com.example.solidconnection.entity.SiteUser; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import static com.example.solidconnection.constants.validMessage.NICKNAME_NOT_BLANK; - -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Getter -public class MyPageUpdateDto { - @NotBlank(message = NICKNAME_NOT_BLANK) - private String nickname; - private String profileImageUrl; - - public static MyPageUpdateDto fromEntity(SiteUser siteUser){ - return MyPageUpdateDto.builder() - .nickname(siteUser.getNickname()) - .profileImageUrl(siteUser.getProfileImageUrl()) - .build(); - } -} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateRequest.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateRequest.java new file mode 100644 index 000000000..bb40e075d --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateRequest.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.siteuser.dto; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "마이 페이지 정보 수정 요청") +public record MyPageUpdateRequest( + @NotBlank(message = "닉네임을 입력해주세요.") + @Schema(description = "변경할 닉네임", example = "NewNickname") + String nickname, + + @Schema(description = "변경할 프로필 이미지 URL", example = "http://example.com/new-profile.jpg", nullable = true) + String profileImageUrl) { + + public static MyPageUpdateRequest from(SiteUser siteUser) { + return new MyPageUpdateRequest( + siteUser.getNickname(), + siteUser.getProfileImageUrl() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java new file mode 100644 index 000000000..d186b08af --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.siteuser.dto; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "마이 페이지 정보 수정 응답") +public record MyPageUpdateResponse( + @Schema(description = "업데이트된 사용자 닉네임", example = "UpdatedNickname") + String nickname, + + @Schema(description = "업데이트된 프로필 이미지 URL", example = "http://example.com/updated-profile.jpg") + String profileImageUrl) { + + public static MyPageUpdateResponse from(SiteUser siteUser) { + return new MyPageUpdateResponse( + siteUser.getNickname(), + siteUser.getProfileImageUrl() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java index fc4bcef1c..c3793eb06 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java @@ -1,15 +1,18 @@ package com.example.solidconnection.siteuser.repository; -import com.example.solidconnection.entity.LikedUniversity; -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.UniversityInfoForApply; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; import java.util.Optional; public interface LikedUniversityRepository extends JpaRepository { + List findAllBySiteUser_Email(String email); + int countBySiteUser_Email(String email); + Optional findBySiteUserAndUniversityInfoForApply(SiteUser siteUser, UniversityInfoForApply universityInfoForApply); } diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java index f557becb9..6b77252c5 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java @@ -1,6 +1,7 @@ package com.example.solidconnection.siteuser.repository; -import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -10,12 +11,22 @@ import java.util.List; import java.util.Optional; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; + @Repository public interface SiteUserRepository extends JpaRepository { + Optional findByEmail(String email); + boolean existsByEmail(String email); + boolean existsByNickname(String nickname); @Query("SELECT u FROM SiteUser u WHERE u.quitedAt <= :cutoffDate") List findUsersToBeRemoved(@Param("cutoffDate") LocalDate cutoffDate); -} \ No newline at end of file + + default SiteUser getByEmail(String email) { + return findByEmail(email) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java deleted file mode 100644 index c6ac59447..000000000 --- a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.example.solidconnection.siteuser.service; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.LikedUniversity; -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.siteuser.dto.MyPageDto; -import com.example.solidconnection.siteuser.dto.MyPageUpdateDto; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.university.dto.UniversityPreviewDto; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; - -import static com.example.solidconnection.constants.Constants.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; -import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; - -@Service -@RequiredArgsConstructor -public class MyPageService { - private final SiteUserValidator siteUserValidator; - private final SiteUserRepository siteUserRepository; - private final LikedUniversityRepository likedUniversityRepository; - private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - - public MyPageDto getMyPageInfo(String email) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - int likedUniversityCount = likedUniversityRepository.countBySiteUser_Email(email); - return MyPageDto.fromEntity(siteUser, likedUniversityCount); - } - - public MyPageUpdateDto getMyPageInfoToUpdate(String email) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - return MyPageUpdateDto.fromEntity(siteUser); - } - - @Transactional - public void update(String email, MyPageUpdateDto myPageUpdateDto) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - validateNicknameDuplicated(myPageUpdateDto.getNickname()); - validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); - siteUser.setNickname(myPageUpdateDto.getNickname()); - siteUser.setProfileImageUrl(myPageUpdateDto.getProfileImageUrl()); - siteUser.setNicknameModifiedAt(LocalDateTime.now()); - } - - private void validateNicknameDuplicated(String nickname) { - if (siteUserRepository.existsByNickname(nickname)) { - throw new CustomException(NICKNAME_ALREADY_EXISTED); - } - } - - private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { - if (lastModifiedAt == null) { - return; - } - if (LocalDateTime.now().isBefore(lastModifiedAt.plusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES))) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - String formatLastModifiedAt = String.format("(마지막 수정 시간 : %s) ", formatter.format(lastModifiedAt)); - throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); - } - } - - public List getWishUniversity(String email) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - List likedUniversities = likedUniversityRepository.findAllBySiteUser_Email(siteUser.getEmail()); - return likedUniversities.stream() - .map(likedUniversity -> UniversityPreviewDto.fromEntity(likedUniversity.getUniversityInfoForApply())) - .toList(); - } -} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index 6d700d3ff..e44af3a3e 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -1,21 +1,99 @@ package com.example.solidconnection.siteuser.service; -import com.example.solidconnection.entity.SiteUser; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.MyPageUpdateRequest; +import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; -@Service +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; + @RequiredArgsConstructor +@Service public class SiteUserService { + + public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; + public static final DateTimeFormatter NICKNAME_LAST_CHANGE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + private final SiteUserRepository siteUserRepository; + private final LikedUniversityRepository likedUniversityRepository; + + /* + * 마이페이지 정보를 조회한다. + * */ + @Transactional(readOnly = true) + public MyPageResponse getMyPageInfo(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + int likedUniversityCount = likedUniversityRepository.countBySiteUser_Email(email); + return MyPageResponse.of(siteUser, likedUniversityCount); + } + + /* + * 내 정보를 수정하기 위한 마이페이지 정보를 조회한다. (닉네임, 프로필 사진) + * */ + @Transactional(readOnly = true) + public MyPageUpdateResponse getMyPageInfoToUpdate(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + return MyPageUpdateResponse.from(siteUser); + } + + /* + * 마이페이지 정보를 수정한다. + * - 닉네임 중복을 검증한다. + * - '닉네임 변경 최소 기간'이 지나지 않았는데 변경하려 하는지 검증한다. + * */ + @Transactional + public MyPageUpdateResponse update(String email, MyPageUpdateRequest pageUpdateRequest) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + + validateNicknameDuplicated(pageUpdateRequest.nickname()); + validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); + + siteUser.setNickname(pageUpdateRequest.nickname()); + siteUser.setProfileImageUrl(pageUpdateRequest.profileImageUrl()); + siteUser.setNicknameModifiedAt(LocalDateTime.now()); + siteUserRepository.save(siteUser); + return MyPageUpdateResponse.from(siteUser); + } + + private void validateNicknameDuplicated(String nickname) { + if (siteUserRepository.existsByNickname(nickname)) { + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + + private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { + if (lastModifiedAt == null) { + return; + } + if (LocalDateTime.now().isBefore(lastModifiedAt.plusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES))) { + String formatLastModifiedAt + = String.format("(마지막 수정 시간 : %s)", NICKNAME_LAST_CHANGE_DATE_FORMAT.format(lastModifiedAt)); + throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); + } + } - public void deleteUsersNeverVisitedAfterQuited() { - LocalDate cutoffDate = LocalDate.now().minusDays(30); - List usersToRemove = siteUserRepository.findUsersToBeRemoved(cutoffDate); - siteUserRepository.deleteAll(usersToRemove); + /* + * 관심 대학교 목록을 조회한다. + * */ + @Transactional(readOnly = true) + public List getWishUniversity(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + List likedUniversities = likedUniversityRepository.findAllBySiteUser_Email(siteUser.getEmail()); + return likedUniversities.stream() + .map(likedUniversity -> UniversityInfoForApplyPreviewResponse.from(likedUniversity.getUniversityInfoForApply())) + .toList(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java deleted file mode 100644 index a68a6ca7c..000000000 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserValidator.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.solidconnection.siteuser.service; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; - -@Service -@RequiredArgsConstructor -public class SiteUserValidator { - private final SiteUserRepository siteUserRepository; - - public SiteUser getValidatedSiteUserByEmail(String email){ - return siteUserRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - } -} diff --git a/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java b/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java deleted file mode 100644 index b6c6fcfee..000000000 --- a/src/main/java/com/example/solidconnection/type/ApplicationStatusResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.solidconnection.type; - -public enum ApplicationStatusResponse { - NOT_SUBMITTED, // 어떤 것도 제출하지 않음 - COLLEGE_SUBMITTED, // 지망 대학만 제출 - SCORE_SUBMITTED, // 성적만 제출 - SUBMITTED_PENDING, // 성적 인증 대기 중 - SUBMITTED_REJECTED, // 성적 인증 승인 완료 - SUBMITTED_APPROVED // 성적 인증 반려 -} diff --git a/src/main/java/com/example/solidconnection/type/CountryCode.java b/src/main/java/com/example/solidconnection/type/CountryCode.java deleted file mode 100644 index 8062f1ddb..000000000 --- a/src/main/java/com/example/solidconnection/type/CountryCode.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.example.solidconnection.type; - -import com.example.solidconnection.custom.exception.CustomException; - -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COUNTRY_NAME; - -public enum CountryCode { - BN("브루나이"), - SG("싱가포르"), - AZ("아제르바이잔"), - ID("인도네시아"), - JP("일본"), - TR("튀르키예"), - HK("홍콩"), - US("미국"), - CA("캐나다"), - AU("호주"), - BR("브라질"), - NL("네덜란드"), - NO("노르웨이"), - DK("덴마크"), - DE("독일"), - SE("스웨덴"), - CH("스위스"), - ES("스페인"), - GB("영국"), - AT("오스트리아"), - IT("이탈리아"), - CZ("체코"), - PT("포르투갈"), - FR("프랑스"), - FI("핀란드"), - CN("중국"), - TW("대만"), - HU("헝가리"), - LT("리투아니아"), - TH("태국"), - UZ("우즈베키스탄"); - - private final String koreanName; - - CountryCode(String koreanName) { - this.koreanName = koreanName; - } - - public static CountryCode getCountryCodeByKoreanName(String koreanName) { - Optional matchingCountryCode = Arrays.stream(CountryCode.values()) - .filter(countryCode -> countryCode.getKoreanName().equals(koreanName)) - .findFirst(); - return matchingCountryCode.orElseThrow(() -> new CustomException(INVALID_COUNTRY_NAME, koreanName)); - } - - public static List getCountryCodeMatchesToKeyword(List keywords) { - List matchedCountryCodes = new LinkedList<>(); - keywords.forEach( keyword -> { - List countryCodes = Arrays.stream(CountryCode.values()) - .filter(country -> country.koreanName.contains(keyword)) - .toList(); - matchedCountryCodes.addAll(countryCodes); - }); - return matchedCountryCodes; - } - - public String getKoreanName() { - return koreanName; - } -} diff --git a/src/main/java/com/example/solidconnection/type/ImgType.java b/src/main/java/com/example/solidconnection/type/ImgType.java index 6f34ec267..4cba0787c 100644 --- a/src/main/java/com/example/solidconnection/type/ImgType.java +++ b/src/main/java/com/example/solidconnection/type/ImgType.java @@ -8,7 +8,7 @@ public enum ImgType { private final String type; - ImgType(String type){ + ImgType(String type) { this.type = type; } } diff --git a/src/main/java/com/example/solidconnection/type/LanguageTestType.java b/src/main/java/com/example/solidconnection/type/LanguageTestType.java index 36c0638b2..30249dc82 100644 --- a/src/main/java/com/example/solidconnection/type/LanguageTestType.java +++ b/src/main/java/com/example/solidconnection/type/LanguageTestType.java @@ -1,18 +1,37 @@ package com.example.solidconnection.type; -import com.example.solidconnection.custom.exception.CustomException; +import java.util.Comparator; -import java.util.Arrays; +public enum LanguageTestType { -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TEST_TYPE; + CEFR((s1, s2) -> s1.compareTo(s2)), + JLPT((s1, s2) -> s2.compareTo(s1)), + DALF(LanguageTestType::compareIntegerScores), + DELF(LanguageTestType::compareIntegerScores), + DUOLINGO(LanguageTestType::compareIntegerScores), + IELTS(LanguageTestType::compareDoubleScores), + NEW_HSK(LanguageTestType::compareIntegerScores), + TCF(LanguageTestType::compareIntegerScores), + TEF(LanguageTestType::compareIntegerScores), + TOEFL_IBT(LanguageTestType::compareIntegerScores), + TOEFL_ITP(LanguageTestType::compareIntegerScores), + TOEIC(LanguageTestType::compareIntegerScores); -public enum LanguageTestType { - TOEFL_IBT, TOEFL_ITP, TOEIC, IELTS, NEW_HSK, JLPT, DUOLINGO, CEFR, DELF, TCF, TEF, DALF; + private final Comparator comparator; + + LanguageTestType(Comparator comparator) { + this.comparator = comparator; + } + + private static int compareIntegerScores(String s1, String s2) { + return Integer.compare(Integer.parseInt(s1), Integer.parseInt(s2)); + } + + private static int compareDoubleScores(String s1, String s2) { + return Double.compare(Double.parseDouble(s1), Double.parseDouble(s2)); + } - public static LanguageTestType getLanguageTestTypeForString(String name) { - return Arrays.stream(LanguageTestType.values()) - .filter(lt -> lt.toString().equals(name)) - .findFirst() - .orElseThrow(() -> new CustomException(INVALID_TEST_TYPE, name)); + public int compare(String s1, String s2) { + return comparator.compare(s1, s2); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/type/RegionCode.java b/src/main/java/com/example/solidconnection/type/RegionCode.java deleted file mode 100644 index 6a37ec6c0..000000000 --- a/src/main/java/com/example/solidconnection/type/RegionCode.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.solidconnection.type; - -import com.example.solidconnection.custom.exception.CustomException; - -import java.util.Arrays; -import java.util.Optional; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_REGION_NAME; - -public enum RegionCode { - ASIA("아시아권"), - AMERICAS("미주권"), - CHINA("중국권"), - EUROPE("유럽권"); - - private final String koreanName; - - RegionCode(String koreanName) { - this.koreanName = koreanName; - } - - public static RegionCode getRegionCodeByKoreanName(String koreanName) { - Optional matchingRegionCode = Arrays.stream(RegionCode.values()) - .filter(regionCode -> regionCode.getKoreanName().equals(koreanName)) - .findFirst(); - return matchingRegionCode.orElseThrow(() -> new CustomException(INVALID_REGION_NAME, koreanName)); - } - - public String getKoreanName() { - return koreanName; - } -} diff --git a/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java index c31415bb7..261db87af 100644 --- a/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java +++ b/src/main/java/com/example/solidconnection/type/SemesterAvailableForDispatch.java @@ -6,7 +6,7 @@ public enum SemesterAvailableForDispatch { ONE_OR_TWO_SEMESTER("1개 또는 2개 학기"), ONE_YEAR("1년만 가능"), IRRELEVANT("무관"), - NO_DATA("데이터 없음") + NO_DATA("데이터 없음"), ; private final String koreanName; diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 03a081471..3b19c683e 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -1,46 +1,85 @@ package com.example.solidconnection.university.controller; -import com.example.solidconnection.custom.response.CustomResponse; -import com.example.solidconnection.custom.response.DataResponse; -import com.example.solidconnection.university.dto.LikedResultDto; -import com.example.solidconnection.university.dto.UniversityDetailDto; -import com.example.solidconnection.university.dto.UniversityPreviewDto; +import com.example.solidconnection.siteuser.service.SiteUserService; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import com.example.solidconnection.university.dto.UniversityDetailResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import com.example.solidconnection.university.service.UniversityRecommendService; import com.example.solidconnection.university.service.UniversityService; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.security.Principal; import java.util.List; -@RestController -@RequestMapping("/university") @RequiredArgsConstructor -public class UniversityController { +@RequestMapping("/university") +@RestController +public class UniversityController implements UniversityControllerSwagger { private final UniversityService universityService; + private final UniversityRecommendService universityRecommendService; + private final SiteUserService siteUserService; - @GetMapping("/detail/{universityInfoForApplyId}") - public CustomResponse getDetails(Principal principal, @PathVariable Long universityInfoForApplyId) { - UniversityDetailDto universityDetailDto = universityService.getDetail(universityInfoForApplyId); - if (principal != null) { - boolean isLiked = universityService.getIsLiked(principal.getName(), universityInfoForApplyId); - universityDetailDto.setLiked(isLiked); + @GetMapping("/recommends") + public ResponseEntity getUniversityRecommends( + Principal principal) { + if (principal == null) { + return ResponseEntity.ok(universityRecommendService.getGeneralRecommends()); + } else { + return ResponseEntity.ok(universityRecommendService.getPersonalRecommends(principal.getName())); } - return new DataResponse<>(universityDetailDto); } - @GetMapping("/search") - public CustomResponse search(@RequestParam(required = false, defaultValue = "") String region, - @RequestParam(required = false, defaultValue = "") List keyword, - @RequestParam(required = false, defaultValue = "") String testType, - @RequestParam(required = false, defaultValue = "") String testScore) { - List universityPreviewDto = universityService.search(region, keyword, testType, testScore); - return new DataResponse<>(universityPreviewDto); + @GetMapping("/like") + public ResponseEntity> getMyWishUniversity(Principal principal) { + List wishUniversities + = siteUserService.getWishUniversity(principal.getName()); + return ResponseEntity + .ok(wishUniversities); + } + + @GetMapping("/{universityInfoForApplyId}/like") + public ResponseEntity getIsLiked( + Principal principal, + @PathVariable Long universityInfoForApplyId) { + IsLikeResponse isLiked = universityService.getIsLiked(principal.getName(), universityInfoForApplyId); + return ResponseEntity.ok(isLiked); } @PostMapping("/{universityInfoForApplyId}/like") - public CustomResponse like(Principal principal, @PathVariable Long universityInfoForApplyId) { - LikedResultDto likedResultDto = universityService.like(principal.getName(), universityInfoForApplyId); - return new DataResponse<>(likedResultDto); + public ResponseEntity addWishUniversity( + Principal principal, + @PathVariable Long universityInfoForApplyId) { + LikeResultResponse likeResultResponse = universityService.likeUniversity(principal.getName(), universityInfoForApplyId); + return ResponseEntity + .ok(likeResultResponse); + } + + @GetMapping("/detail/{universityInfoForApplyId}") + public ResponseEntity getUniversityDetails( + @PathVariable Long universityInfoForApplyId) { + UniversityDetailResponse universityDetailResponse = universityService.getUniversityDetail(universityInfoForApplyId); + return ResponseEntity.ok(universityDetailResponse); + } + + @GetMapping("/search") + public ResponseEntity> searchUniversity( + @RequestParam(required = false, defaultValue = "") String region, + @RequestParam(required = false, defaultValue = "") List keyword, + @RequestParam(required = false, defaultValue = "") LanguageTestType testType, + @RequestParam(required = false, defaultValue = "") String testScore) { + List universityInfoForApplyPreviewResponse + = universityService.searchUniversity(region, keyword, testType, testScore); + return ResponseEntity.ok(universityInfoForApplyPreviewResponse); } } diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityControllerSwagger.java b/src/main/java/com/example/solidconnection/university/controller/UniversityControllerSwagger.java new file mode 100644 index 000000000..e140917f2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityControllerSwagger.java @@ -0,0 +1,124 @@ +package com.example.solidconnection.university.controller; + +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import com.example.solidconnection.university.dto.UniversityDetailResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.security.Principal; +import java.util.List; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@Tag(name = "University", description = "대학 및 대학 지원을 위한 정보 API") +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public interface UniversityControllerSwagger { + + @Operation( + summary = "대학 추천 목록 조회", + responses = { + @ApiResponse( + responseCode = "200", + description = "사용자별 개인화 된 대학 추천 목록 또는 일반 추천 목록 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UniversityRecommendsResponse.class) + ) + ) + } + ) + ResponseEntity getUniversityRecommends(Principal principal); + + @Operation( + summary = "좋아요한 대학 목록 조회", + responses = { + @ApiResponse( + responseCode = "200", + description = "사용자가 좋아요한 대학 목록 반환", + content = @Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = UniversityInfoForApplyPreviewResponse.class)) + ) + ) + } + ) + ResponseEntity> getMyWishUniversity(Principal principal); + + @Operation( + summary = "대학 좋아요 여부 확인", + responses = { + @ApiResponse( + responseCode = "200", + description = "대학 좋아요 여부 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = IsLikeResponse.class) + ) + ) + } + ) + ResponseEntity getIsLiked(Principal principal, @PathVariable Long universityInfoForApplyId); + + @Operation( + summary = "대학 좋아요 하기", + responses = { + @ApiResponse( + responseCode = "200", + description = "대학 좋아요 결과 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = LikeResultResponse.class) + ) + ) + } + ) + ResponseEntity addWishUniversity(Principal principal, @PathVariable Long universityInfoForApplyId); + + @Operation( + summary = "대학 상세 정보 조회", + responses = { + @ApiResponse( + responseCode = "200", + description = "대학 상세 정보 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = UniversityDetailResponse.class) + ) + ) + } + ) + ResponseEntity getUniversityDetails(@PathVariable Long universityInfoForApplyId); + + @Operation( + summary = "대학 검색", + responses = { + @ApiResponse( + responseCode = "200", + description = "검색 조건에 맞는 대학 목록 반환", + content = @Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = UniversityInfoForApplyPreviewResponse.class)) + ) + ) + } + ) + ResponseEntity> searchUniversity( + @RequestParam(required = false, defaultValue = "") String region, + @RequestParam(required = false, defaultValue = "") List keyword, + @RequestParam(required = false, defaultValue = "") LanguageTestType testType, + @RequestParam(required = false, defaultValue = "") String testScore); +} diff --git a/src/main/java/com/example/solidconnection/university/domain/LanguageRequirement.java b/src/main/java/com/example/solidconnection/university/domain/LanguageRequirement.java new file mode 100644 index 000000000..508c7531e --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/domain/LanguageRequirement.java @@ -0,0 +1,37 @@ +package com.example.solidconnection.university.domain; + +import com.example.solidconnection.type.LanguageTestType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor(access = AccessLevel.PUBLIC) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class LanguageRequirement { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private LanguageTestType languageTestType; + + @Column(nullable = false) + private String minScore; + + @ManyToOne(fetch = FetchType.LAZY) + private UniversityInfoForApply universityInfoForApply; +} diff --git a/src/main/java/com/example/solidconnection/entity/LikedUniversity.java b/src/main/java/com/example/solidconnection/university/domain/LikedUniversity.java similarity index 57% rename from src/main/java/com/example/solidconnection/entity/LikedUniversity.java rename to src/main/java/com/example/solidconnection/university/domain/LikedUniversity.java index 1c44b2975..ad7ee02c8 100644 --- a/src/main/java/com/example/solidconnection/entity/LikedUniversity.java +++ b/src/main/java/com/example/solidconnection/university/domain/LikedUniversity.java @@ -1,6 +1,11 @@ -package com.example.solidconnection.entity; +package com.example.solidconnection.university.domain; -import jakarta.persistence.*; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -12,16 +17,14 @@ @AllArgsConstructor @NoArgsConstructor public class LikedUniversity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // 연관 관계 @ManyToOne - @JoinColumn(name = "university_info_for_apply_id") private UniversityInfoForApply universityInfoForApply; @ManyToOne - @JoinColumn(name = "site_user_id") private SiteUser siteUser; -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/entity/University.java b/src/main/java/com/example/solidconnection/university/domain/University.java similarity index 60% rename from src/main/java/com/example/solidconnection/entity/University.java rename to src/main/java/com/example/solidconnection/university/domain/University.java index 7100054cf..c3021385e 100644 --- a/src/main/java/com/example/solidconnection/entity/University.java +++ b/src/main/java/com/example/solidconnection/university/domain/University.java @@ -1,11 +1,24 @@ -package com.example.solidconnection.entity; - -import jakarta.persistence.*; +package com.example.solidconnection.university.domain; + +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.Region; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity +@AllArgsConstructor(access = AccessLevel.PUBLIC) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class University { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -37,12 +50,9 @@ public class University { @Column(length = 1000) private String detailsForLocal; - // 연관 관계 @ManyToOne - @JoinColumn(name = "country_code") private Country country; @ManyToOne - @JoinColumn(name = "region_code") private Region region; } diff --git a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java b/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java similarity index 52% rename from src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java rename to src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java index 5c480e86d..bbb4dd015 100644 --- a/src/main/java/com/example/solidconnection/entity/UniversityInfoForApply.java +++ b/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java @@ -1,43 +1,59 @@ -package com.example.solidconnection.entity; +package com.example.solidconnection.university.domain; import com.example.solidconnection.type.SemesterAvailableForDispatch; import com.example.solidconnection.type.TuitionFeeType; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.HashSet; import java.util.Set; -@Entity @Getter +@AllArgsConstructor(access = AccessLevel.PUBLIC) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity public class UniversityInfoForApply { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(length = 10) + @Column(length = 50, nullable = false) private String term; - @Column(nullable = false) + @Column private Integer studentCapacity; - @Column(nullable = false, length = 50) + @Column @Enumerated(EnumType.STRING) private TuitionFeeType tuitionFeeType; - @Column(nullable = false, length = 50) + @Column @Enumerated(EnumType.STRING) private SemesterAvailableForDispatch semesterAvailableForDispatch; - @Column(length = 10) + @Column(length = 100) private String semesterRequirement; @Column(length = 1000) private String detailsForLanguage; - @Column(length = 20) + @Column(length = 100) private String gpaRequirement; - @Column(length = 20) + @Column(length = 100) private String gpaRequirementCriteria; @Column(length = 1000) @@ -55,11 +71,13 @@ public class UniversityInfoForApply { @Column(length = 500) private String details; - // 연관 관계 @OneToMany(mappedBy = "universityInfoForApply", fetch = FetchType.LAZY) - private Set languageRequirements; + private Set languageRequirements = new HashSet<>(); - @OneToOne - @JoinColumn(name = "university_id") + @ManyToOne(fetch = FetchType.LAZY) private University university; + + public void addLanguageRequirements(LanguageRequirement languageRequirements) { + this.languageRequirements.add(languageRequirements); + } } diff --git a/src/main/java/com/example/solidconnection/university/dto/IsLikeResponse.java b/src/main/java/com/example/solidconnection/university/dto/IsLikeResponse.java new file mode 100644 index 000000000..bd62505bd --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/IsLikeResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.university.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "대학교 '좋아요' 여부") +public record IsLikeResponse( + @Schema(description = "대학교 '좋아요' 여부", example = "true") + boolean isLike) { +} diff --git a/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementDto.java b/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementDto.java deleted file mode 100644 index 38fb1ef98..000000000 --- a/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.solidconnection.university.dto; - -import com.example.solidconnection.entity.LanguageRequirement; -import com.example.solidconnection.type.LanguageTestType; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class LanguageRequirementDto { - private LanguageTestType languageTestType; - private String minScore; - - public static LanguageRequirementDto fromEntity(LanguageRequirement languageRequirement){ - return LanguageRequirementDto.builder() - .languageTestType(languageRequirement.getLanguageTestType()) - .minScore(languageRequirement.getMinScore()) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementResponse.java b/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementResponse.java new file mode 100644 index 000000000..7b02108a2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementResponse.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.university.dto; + +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.domain.LanguageRequirement; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "어학 성적 요구사항 응답") +public record LanguageRequirementResponse( + @Schema(description = "어학 시험 유형", example = "TOEFL_IBT") + LanguageTestType languageTestType, + + @Schema(description = "최소 점수 요구사항", example = "100") + String minScore) implements Comparable { + + public static LanguageRequirementResponse from(LanguageRequirement languageRequirement) { + return new LanguageRequirementResponse( + languageRequirement.getLanguageTestType(), + languageRequirement.getMinScore()); + } + + @Override + public int compareTo(LanguageRequirementResponse other) { + return this.languageTestType.name().compareTo(other.languageTestType.name()); + } +} diff --git a/src/main/java/com/example/solidconnection/university/dto/LikeResultResponse.java b/src/main/java/com/example/solidconnection/university/dto/LikeResultResponse.java new file mode 100644 index 000000000..29e81bc95 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/LikeResultResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.university.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "좋아요 결과 응답 데이터") +public record LikeResultResponse( + @Schema(description = "좋아요 결과", example = "LIKE_SUCCESS") + String result) { +} diff --git a/src/main/java/com/example/solidconnection/university/dto/LikedResultDto.java b/src/main/java/com/example/solidconnection/university/dto/LikedResultDto.java deleted file mode 100644 index dcea241a5..000000000 --- a/src/main/java/com/example/solidconnection/university/dto/LikedResultDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.solidconnection.university.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class LikedResultDto { - private String result; -} diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java deleted file mode 100644 index 2cc1d919e..000000000 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailDto.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.solidconnection.university.dto; - -import lombok.*; - -import java.util.List; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class UniversityDetailDto { - private long id; - @Setter - private boolean isLiked = false; - private String term; - private String koreanName; - private String englishName; - private String formatName; - private String region; - private String country; - private String homepageUrl; - private String logoImageUrl; - private String backgroundImageUrl; - private String detailsForLocal; - private int studentCapacity; - private String tuitionFeeType; - private String semesterAvailableForDispatch; - private List languageRequirements; - private String detailsForLanguage; - private String gpaRequirement; - private String gpaRequirementCriteria; - private String semesterRequirement; - private String detailsForApply; - private String detailsForMajor; - private String detailsForAccommodation; - private String detailsForEnglishCourse; - private String details; - private String accommodationUrl; - private String englishCourseUrl; -} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java new file mode 100644 index 000000000..d5db9d74a --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java @@ -0,0 +1,125 @@ +package com.example.solidconnection.university.dto; + +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "대학 세부 사항 응답 데이터") +public record UniversityDetailResponse( + + @Schema(description = "대학 지원을 위한 정보 id", example = "1") + long id, + + @Schema(description = "모집 시기", example = "2024-2") + String term, + + @Schema(description = "국문 이름", example = "그라츠 대학") + String koreanName, + + @Schema(description = "영문 이름", example = "University of Graz") + String englishName, + + @Schema(description = "서브스에서 사용되는 이름", example = "university_of_graz") + String formatName, + + @Schema(description = "지역", example = "유럽") + String region, + + @Schema(description = "국가", example = "오스트리아") + String country, + + @Schema(description = "대학 홈페이지 URL", example = "http://www.graz.ac.kr") + String homepageUrl, + + @Schema(description = "대학 로고 이미지 URL", example = "http://example.com/logo.jpg") + String logoImageUrl, + + @Schema(description = "대학 배경 이미지 URL", example = "http://example.com/background.jpg") + String backgroundImageUrl, + + @Schema(description = "현지에 대한 세부 사항", example = "Detailed information about local conditions.") + String detailsForLocal, + + @Schema(description = "모집 인원", example = "2") + int studentCapacity, + + @Schema(description = "등록금 유형", example = "본교납부형") + String tuitionFeeType, + + @Schema(description = "파견 가능 학기", example = "1") + String semesterAvailableForDispatch, + + @ArraySchema(arraySchema = @Schema(description = "어학 성적 요구사항")) + List languageRequirements, + + @Schema(description = "어학 성적 세부 사항", example = "Minimum TOEFL score required is 80.") + String detailsForLanguage, + + @Schema(description = "GPA", example = "3.5") + String gpaRequirement, + + @Schema(description = "GPA 계산 기준", example = "4.0") + String gpaRequirementCriteria, + + @Schema(description = "필요 학기", example = "2") + String semesterRequirement, + + @Schema(description = "지원에 대한 세부 사항", example = "Application process detailed here.") + String detailsForApply, + + @Schema(description = "전공에 대한 세부 사항", example = "Major requirements detailed here.") + String detailsForMajor, + + @Schema(description = "숙박에 대한 세부 사항", example = "Accommodation details provided.") + String detailsForAccommodation, + + @Schema(description = "영어 과정 세부 사항", example = "English courses available for international students.") + String detailsForEnglishCourse, + + @Schema(description = "기타 세부 사항", example = "Additional university details.") + String details, + + @Schema(description = "숙박 시설 URL", example = "http://example.com/accommodation") + String accommodationUrl, + + @Schema(description = "영어 수업 정보 URL", example = "http://example.com/englishCourses") + String englishCourseUrl) { + + public static UniversityDetailResponse of( + University university, + UniversityInfoForApply universityInfoForApply) { + return new UniversityDetailResponse( + university.getId(), + universityInfoForApply.getTerm(), + university.getKoreanName(), + university.getEnglishName(), + university.getFormatName(), + university.getRegion().getKoreanName(), + university.getCountry().getKoreanName(), + university.getHomepageUrl(), + university.getLogoImageUrl(), + university.getBackgroundImageUrl(), + university.getDetailsForLocal(), + universityInfoForApply.getStudentCapacity(), + universityInfoForApply.getTuitionFeeType().getKoreanName(), + universityInfoForApply.getSemesterAvailableForDispatch().getKoreanName(), + universityInfoForApply.getLanguageRequirements().stream() + .map(LanguageRequirementResponse::from) + .toList(), + universityInfoForApply.getDetailsForLanguage(), + universityInfoForApply.getGpaRequirement(), + universityInfoForApply.getGpaRequirementCriteria(), + universityInfoForApply.getSemesterRequirement(), + universityInfoForApply.getDetailsForApply(), + universityInfoForApply.getDetailsForMajor(), + universityInfoForApply.getDetailsForAccommodation(), + universityInfoForApply.getDetailsForEnglishCourse(), + universityInfoForApply.getDetails(), + university.getAccommodationUrl(), + university.getEnglishCourseUrl() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java new file mode 100644 index 000000000..f0a0c73cb --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.university.dto; + +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.Collections; +import java.util.List; + +@Schema(description = "대학 미리보기 응답") +public record UniversityInfoForApplyPreviewResponse( + @Schema(description = "대학 지원을 위한 정보 id", example = "1") + long id, + + @Schema(description = "모집 시기", example = "2024-2") + String term, + + @Schema(description = "국문 이름", example = "그라츠대학") + String koreanName, + + @Schema(description = "지역", example = "유럽") + String region, + + @Schema(description = "국가", example = "오스트리아") + String country, + + @Schema(description = "대학 로고 이미지 URL", example = "http://example.com/logo.jpg") + String logoImageUrl, + + @Schema(description = "모집 인원", example = "2") + int studentCapacity, + + @ArraySchema(arraySchema = @Schema(description = "어학시험 요구사항")) + List languageRequirements) { + + public static UniversityInfoForApplyPreviewResponse from(UniversityInfoForApply universityInfoForApply) { + List languageRequirementResponses = new java.util.ArrayList<>( + universityInfoForApply.getLanguageRequirements().stream() + .map(LanguageRequirementResponse::from) + .toList()); + Collections.sort(languageRequirementResponses); + + return new UniversityInfoForApplyPreviewResponse( + universityInfoForApply.getId(), + universityInfoForApply.getTerm(), + universityInfoForApply.getUniversity().getKoreanName(), + universityInfoForApply.getUniversity().getRegion().getKoreanName(), + universityInfoForApply.getUniversity().getCountry().getKoreanName(), + universityInfoForApply.getUniversity().getLogoImageUrl(), + universityInfoForApply.getStudentCapacity(), + languageRequirementResponses + ); + } +} diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java b/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java deleted file mode 100644 index 13b90ea9e..000000000 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityPreviewDto.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.solidconnection.university.dto; - -import com.example.solidconnection.entity.UniversityInfoForApply; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class UniversityPreviewDto { - private long id; - private String term; - private String koreanName; - private String region; - private String country; - private String logoImageUrl; - private int studentCapacity; - private List languageRequirements; - - public static UniversityPreviewDto fromEntity(UniversityInfoForApply universityInfoForApply) { - return UniversityPreviewDto.builder() - .id(universityInfoForApply.getId()) - .term(universityInfoForApply.getTerm()) - .region(universityInfoForApply.getUniversity().getRegion().getCode().getKoreanName()) - .country(universityInfoForApply.getUniversity().getCountry().getCode().getKoreanName()) - .logoImageUrl(universityInfoForApply.getUniversity().getLogoImageUrl()) - .koreanName(universityInfoForApply.getUniversity().getKoreanName()) - .studentCapacity(universityInfoForApply.getStudentCapacity()) - .languageRequirements(universityInfoForApply.getLanguageRequirements().stream() - .map(LanguageRequirementDto::fromEntity) - .toList() - ) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityRecommendsResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityRecommendsResponse.java new file mode 100644 index 000000000..947a7e78c --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityRecommendsResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.university.dto; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "추천 대학 목록 응답") +public record UniversityRecommendsResponse( + @ArraySchema(arraySchema = @Schema(description = "추천된 대학 목록", implementation = UniversityInfoForApplyPreviewResponse.class)) + List recommendedUniversities) { +} diff --git a/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java b/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java index 223a9854c..4cbebc6f5 100644 --- a/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/LanguageRequirementRepository.java @@ -1,22 +1,18 @@ package com.example.solidconnection.university.repository; -import com.example.solidconnection.entity.LanguageRequirement; -import com.example.solidconnection.entity.UniversityInfoForApply; import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.domain.LanguageRequirement; +import com.example.solidconnection.university.domain.UniversityInfoForApply; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.Optional; @Repository public interface LanguageRequirementRepository extends JpaRepository { - List findAllByUniversityInfoForApply_Id(Long id); @Query("SELECT lr FROM LanguageRequirement lr WHERE lr.minScore <= :myScore AND lr.languageTestType = :testType AND lr.universityInfoForApply = :universityInfoForApply ORDER BY lr.minScore ASC") Optional findByUniversityInfoForApplyAndLanguageTestTypeAndLessThanMyScore(@Param("universityInfoForApply") UniversityInfoForApply universityInfoForApply, @Param("testType") LanguageTestType testType, @Param("myScore") String myScore); - - boolean existsByUniversityInfoForApplyAndLanguageTestType(UniversityInfoForApply universityInfoForApply, LanguageTestType languageTestType); } diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index f7a5c6c09..6c9120fab 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -1,21 +1,57 @@ package com.example.solidconnection.university.repository; -import com.example.solidconnection.entity.University; -import com.example.solidconnection.entity.UniversityInfoForApply; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND_FOR_TERM; + @Repository public interface UniversityInfoForApplyRepository extends JpaRepository { - Optional findByUniversityAndTerm(University university, String term); - Boolean existsByUniversityAndTerm(University university, String term); + Optional findByIdAndTerm(Long id, String term); Optional findByUniversity_KoreanNameAndTerm(String koreanName, String term); - Optional findByIdAndTerm(Long id, String term); + @Query("SELECT c FROM UniversityInfoForApply c WHERE c.university IN :universities AND c.term = :term") + List findByUniversitiesAndTerm(@Param("universities") List universities, @Param("term") String term); + + @Query(""" + SELECT uifa + FROM UniversityInfoForApply uifa + JOIN University u ON uifa.university = u + WHERE (u.country.code IN ( + SELECT c.code + FROM InterestedCountry ic + JOIN ic.country c + WHERE ic.siteUser = :siteUser + ) + OR u.region.code IN ( + SELECT r.code + FROM InterestedRegion ir + JOIN ir.region r + WHERE ir.siteUser = :siteUser + )) + AND uifa.term = :term + """) + List findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(@Param("siteUser") SiteUser siteUser, @Param("term") String term); + + default UniversityInfoForApply getUniversityInfoForApplyById(Long id) { + return findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); + } - Optional findByUniversity(University university); -} \ No newline at end of file + default UniversityInfoForApply getUniversityInfoForApplyByIdAndTerm(Long id, String term) { + return findByIdAndTerm(id, term) + .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND_FOR_TERM)); + } +} diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java index 5b217d1e0..e4cdeade2 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java @@ -1,9 +1,8 @@ package com.example.solidconnection.university.repository; -import com.example.solidconnection.entity.University; -import com.example.solidconnection.type.CountryCode; -import com.example.solidconnection.type.RegionCode; -import com.example.solidconnection.university.repository.custom.UniversityRepositoryForFilter; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.repository.custom.UniversityFilterRepository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -11,9 +10,16 @@ import java.util.List; +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_NOT_FOUND; + @Repository -public interface UniversityRepository extends JpaRepository, UniversityRepositoryForFilter { +public interface UniversityRepository extends JpaRepository, UniversityFilterRepository { + + @Query("SELECT u FROM University u WHERE u.country.code IN :countryCodes OR u.region.code IN :regionCodes") + List findByCountryCodeInOrRegionCodeIn(@Param("countryCodes") List countryCodes, @Param("regionCodes") List regionCodes); - @Query("SELECT u FROM University u WHERE u.country.code IN :countries OR u.region.code IN :regions") - List findByCountryCodeInOrRegionCodeIn(@Param("countries") List countries, @Param("regions") List regions); + default University getUniversityById(Long id) { + return findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + } } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepository.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepository.java new file mode 100644 index 000000000..bc2306737 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepository.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.university.repository.custom; + +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; + +import java.util.List; + +public interface UniversityFilterRepository { + + List findByRegionCodeAndKeywords(String regionCode, List keywords); + + List findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScore( + String regionCode, List keywords, LanguageTestType testType, String testScore); +} diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java new file mode 100644 index 000000000..994618a6b --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java @@ -0,0 +1,112 @@ +package com.example.solidconnection.university.repository.custom; + +import com.example.solidconnection.entity.QCountry; +import com.example.solidconnection.entity.QRegion; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.domain.QLanguageRequirement; +import com.example.solidconnection.university.domain.QUniversity; +import com.example.solidconnection.university.domain.QUniversityInfoForApply; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringPath; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class UniversityFilterRepositoryImpl implements UniversityFilterRepository { + + private final JPAQueryFactory queryFactory; + + @Autowired + public UniversityFilterRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public List findByRegionCodeAndKeywords(String regionCode, List keywords) { + QUniversity university = QUniversity.university; + QCountry country = QCountry.country; + QRegion region = QRegion.region; + + return queryFactory + .selectFrom(university) + .join(university.country, country) + .join(country.region, region) + .where(regionCodeEq(region, regionCode) + .and(countryOrUniversityContainsKeyword(country, university, keywords)) + ) + .fetch(); + } + + private BooleanExpression regionCodeEq(QRegion region, String regionCode) { + if (regionCode == null || regionCode.isEmpty()) { + return Expressions.asBoolean(true).isTrue(); + } + return region.code.eq(regionCode); + } + + private BooleanExpression countryOrUniversityContainsKeyword(QCountry country, QUniversity university, List keywords) { + if (keywords == null || keywords.isEmpty()) { + return Expressions.TRUE; + } + BooleanExpression countryCondition = createKeywordCondition(country.koreanName, keywords); + BooleanExpression universityCondition = createKeywordCondition(university.koreanName, keywords); + return countryCondition.or(universityCondition); + } + + private BooleanExpression createKeywordCondition(StringPath namePath, List keywords) { + return keywords.stream() + .map(namePath::contains) + .reduce(BooleanExpression::or) + .orElse(Expressions.FALSE); + } + + @Override + public List findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScore( + String regionCode, List keywords, LanguageTestType testType, String testScore) { + + QUniversity university = QUniversity.university; + QCountry country = QCountry.country; + QRegion region = QRegion.region; + QUniversityInfoForApply universityInfoForApply = QUniversityInfoForApply.universityInfoForApply; + QLanguageRequirement languageRequirement = QLanguageRequirement.languageRequirement; + + List filteredUniversityInfoForApply = queryFactory + .selectFrom(universityInfoForApply) + .join(universityInfoForApply.university, university) + .join(university.country, country) + .join(country.region, region) + .join(universityInfoForApply.languageRequirements, languageRequirement) + .where(regionCodeEq(region, regionCode) + .and(countryOrUniversityContainsKeyword(country, university, keywords))) + .fetch(); + + if(testType == null || testScore == null || testScore.isEmpty()) { + if(testType != null) { + return filteredUniversityInfoForApply.stream() + .filter(uifa -> uifa.getLanguageRequirements().stream() + .anyMatch(lr -> lr.getLanguageTestType().equals(testType))) + .toList(); + } + return filteredUniversityInfoForApply; + } + + return filteredUniversityInfoForApply.stream() + .filter(uifa -> compareMyTestScoreToMinPassScore(uifa, testType, testScore) >= 0) + .toList(); + } + + private int compareMyTestScoreToMinPassScore(UniversityInfoForApply universityInfoForApply, LanguageTestType testType, String testScore) { + return universityInfoForApply.getLanguageRequirements().stream() + .filter(languageRequirement -> languageRequirement.getLanguageTestType().equals(testType)) + .findFirst() + .map(requirement -> testType.compare(testScore, requirement.getMinScore())) + .orElse(-1); + } +} diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java deleted file mode 100644 index e30119428..000000000 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilter.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.solidconnection.university.repository.custom; - -import com.example.solidconnection.entity.University; -import com.example.solidconnection.type.CountryCode; -import com.example.solidconnection.type.RegionCode; - -import java.util.List; - -public interface UniversityRepositoryForFilter { - List findByRegionAndCountryAndKeyword(RegionCode regionCode, List countryCodes, List keywords); -} diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java deleted file mode 100644 index dd6b65825..000000000 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityRepositoryForFilterImpl.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.solidconnection.university.repository.custom; - -import com.example.solidconnection.entity.QCountry; -import com.example.solidconnection.entity.QRegion; -import com.example.solidconnection.entity.QUniversity; -import com.example.solidconnection.entity.University; -import com.example.solidconnection.type.CountryCode; -import com.example.solidconnection.type.RegionCode; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.Expressions; -import com.querydsl.jpa.impl.JPAQueryFactory; -import jakarta.persistence.EntityManager; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public class UniversityRepositoryForFilterImpl implements UniversityRepositoryForFilter { - private final JPAQueryFactory queryFactory; - - @Autowired - public UniversityRepositoryForFilterImpl(EntityManager em) { - this.queryFactory = new JPAQueryFactory(em); - } - - @Override - public List findByRegionAndCountryAndKeyword(RegionCode regionCode, List countryCodes, List keywords) { - QUniversity university = QUniversity.university; - QCountry country = QCountry.country; - QRegion region = QRegion.region; - - // System.out.println(regionCodeEq(regionCode, region).and(keywordContainsInCountryOrName(countryCodes, country, keywords, university))); - - return queryFactory - .selectFrom(university) - .join(university.country, country) - .join(country.region, region) - .where( - regionCodeEq(regionCode, region).and(keywordContainsInCountryOrName(countryCodes, country, keywords, university)) - ) - .fetch(); - } - - private BooleanExpression regionCodeEq(RegionCode regionCode, QRegion region) { - if(regionCode == null) { - return Expressions.asBoolean(true).isTrue(); - } - return region.code.eq(regionCode); - } - - private BooleanExpression keywordContainsInCountryOrName(List countryCodes, QCountry country, List keywords, QUniversity university) { - if (countryCodes == null || countryCodes.isEmpty()) { // 해당하는 국가가 없으면 - if (keywords == null || keywords.isEmpty()) { - return Expressions.asBoolean(true).isTrue(); - } - return keywords.stream() - .map(university.koreanName::contains) - .reduce(BooleanExpression::or) - .orElse(Expressions.asBoolean(true).isFalse()); - } - - BooleanExpression countryCondition = country.code.in(countryCodes); - if (keywords == null || keywords.isEmpty()) { - return Expressions.asBoolean(true).isTrue(); - } - - BooleanExpression keywordCondition = keywords.stream() - .map(university.koreanName::contains) - .reduce(BooleanExpression::or) - .orElse(Expressions.asBoolean(true).isFalse()); - return countryCondition.or(keywordCondition); - } -} diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java new file mode 100644 index 000000000..f9441c61d --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; + +@RequiredArgsConstructor +@Component +public class GeneralRecommendUniversities { + + /* + * 매 선발 시기(term) 마다 지원할 수 있는 대학교가 달라지므르, 추천 대학교도 달라져야 한다. + * 하지만 매번 추천 대학교를 바꾸기에는 번거롭다. + * 따라서 '추천 대학교 후보'들을 설정하고, DB 에서 현재 term 에 대해 찾아지는 대학교만 추천 대학교로 지정한다. + * */ + @Getter + private final List recommendUniversities; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final List candidates = List.of( + "오스트라바 대학", "RMIT멜버른공과대학(A형)", "알브슈타트 지그마링엔 대학", + "뉴저지시티대학(A형)", "도요대학", "템플대학(A형)", "빈 공과대학교", + "리스본대학 공과대학", "바덴뷔르템베르크 산학협력대학", "긴다이대학", "네바다주립대학 라스베이거스(B형)", "릴 가톨릭 대학", + "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학" + ); + + @Value("${university.term}") + public String term; + + @PostConstruct + public void init() { + int i = 0; + while (recommendUniversities.size() < RECOMMEND_UNIVERSITY_NUM && i < candidates.size()) { + universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(candidates.get(i), term) + .ifPresent(recommendUniversities::add); + i++; + } + } +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java new file mode 100644 index 000000000..cf58f801a --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -0,0 +1,74 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class UniversityRecommendService { + + public static final int RECOMMEND_UNIVERSITY_NUM = 6; + + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final GeneralRecommendUniversities generalRecommendUniversities; + private final SiteUserRepository siteUserRepository; + @Value("${university.term}") + public String term; + + /* + * 사용자 맞춤 추천 대학교를 불러온다. + * - 회원가입 시 선택한 관심 지역과 관심 국가에 해당하는 대학 중, 이번 term 에 열리는 학교들을 불러온다. + * - 불러온 맞춤 추천 대학교의 순서를 무작위로 섞는다. + * - 맞춤 추천 대학교의 수가 6개보다 적다면, 공통 추천 대학교를 부족한 수 만큼 불러온다. + * */ + @Transactional(readOnly = true) + public UniversityRecommendsResponse getPersonalRecommends(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + // 맞춤 추천 대학교를 불러온다. + List personalRecommends = universityInfoForApplyRepository + .findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(siteUser, term); + List shuffledList + = personalRecommends.subList(0, Math.min(RECOMMEND_UNIVERSITY_NUM, personalRecommends.size())); + Collections.shuffle(personalRecommends); + + // 맞춤 추천 대학교의 수가 6개보다 적다면, 일반 추천 대학교를 부족한 수 만큼 불러온다. + if (shuffledList.size() < 6) { + shuffledList.addAll(getGeneralRecommendsExcludingSelected(shuffledList)); + } + + return new UniversityRecommendsResponse(shuffledList.stream() + .map(UniversityInfoForApplyPreviewResponse::from) + .toList()); + } + + private List getGeneralRecommendsExcludingSelected(List alreadyPicked) { + List generalRecommend = new ArrayList<>(generalRecommendUniversities.getRecommendUniversities()); + generalRecommend.removeAll(alreadyPicked); + Collections.shuffle(generalRecommend); + return generalRecommend.subList(0, RECOMMEND_UNIVERSITY_NUM - alreadyPicked.size()); + } + + /* + * 공통 추천 대학교를 불러온다. + * */ + @Transactional(readOnly = true) + public UniversityRecommendsResponse getGeneralRecommends() { + List generalRecommends = new ArrayList<>(generalRecommendUniversities.getRecommendUniversities()); + Collections.shuffle(generalRecommends); + return new UniversityRecommendsResponse(generalRecommends.stream() + .map(UniversityInfoForApplyPreviewResponse::from) + .toList()); + } +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index eb805abd2..cbc97e4f6 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -1,182 +1,83 @@ package com.example.solidconnection.university.service; -import com.example.solidconnection.constants.GeneralRecommendUniversities; -import com.example.solidconnection.entity.LikedUniversity; -import com.example.solidconnection.entity.SiteUser; -import com.example.solidconnection.entity.University; -import com.example.solidconnection.entity.UniversityInfoForApply; -import com.example.solidconnection.home.dto.RecommendedUniversityDto; -import com.example.solidconnection.repositories.InterestedCountyRepository; -import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; -import com.example.solidconnection.siteuser.service.SiteUserValidator; -import com.example.solidconnection.type.CountryCode; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.type.RegionCode; -import com.example.solidconnection.university.dto.LanguageRequirementDto; -import com.example.solidconnection.university.dto.LikedResultDto; -import com.example.solidconnection.university.dto.UniversityDetailDto; -import com.example.solidconnection.university.dto.UniversityPreviewDto; -import com.example.solidconnection.university.repository.LanguageRequirementRepository; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import com.example.solidconnection.university.dto.UniversityDetailResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import com.example.solidconnection.university.repository.UniversityRepository; -import com.example.solidconnection.university.repository.custom.UniversityRepositoryForFilterImpl; +import com.example.solidconnection.university.repository.custom.UniversityFilterRepositoryImpl; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; -import static com.example.solidconnection.constants.Constants.RECOMMEND_UNIVERSITY_NUM; -import static com.example.solidconnection.constants.Constants.TERM; - -@Service @RequiredArgsConstructor +@Service public class UniversityService { + public static final String LIKE_SUCCESS_MESSAGE = "LIKE_SUCCESS"; + public static final String LIKE_CANCELED_MESSAGE = "LIKE_CANCELED"; + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final UniversityRepository universityRepository; - private final LanguageRequirementRepository languageRequirementRepository; - private final InterestedCountyRepository interestedCountyRepository; - private final InterestedRegionRepository interestedRegionRepository; private final LikedUniversityRepository likedUniversityRepository; - private final SiteUserValidator siteUserValidator; - private final GeneralRecommendUniversities generalRecommendUniversities; - private final UniversityValidator universityValidator; - private final UniversityRepositoryForFilterImpl universityRepositoryForFilter; - - public List getPersonalRecommends(String email) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - List interestedCountries = interestedCountyRepository.findAllBySiteUser(siteUser) - .stream().map(interestedCountry -> interestedCountry.getCountry().getCode()) - .toList(); - List interestedRegions = interestedRegionRepository.findAllBySiteUser(siteUser) - .stream().map(interestedRegion -> interestedRegion.getRegion().getCode()) - .toList(); - List recommendedUniversities = new java.util.ArrayList<>(universityRepository.findByCountryCodeInOrRegionCodeIn(interestedCountries, interestedRegions) - .stream() - .map(universityValidator::getValidatedUniversityInfoForApplyByUniversityAndTermNoException) - .filter(Objects::nonNull) - .toList()); - - Collections.shuffle(recommendedUniversities); - List shuffledList = recommendedUniversities.subList(0, Math.min(RECOMMEND_UNIVERSITY_NUM, recommendedUniversities.size())); - if (shuffledList.size() < 6) { - shuffledList.addAll(getGeneralRecommendsExcept(shuffledList)); - } - - return shuffledList.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); - } - - public List getGeneralRecommends() { - List generalRecommend = new java.util.ArrayList<>(generalRecommendUniversities.getRecommendedUniversities()); - Collections.shuffle(generalRecommend); - return generalRecommend.stream().map(RecommendedUniversityDto::fromEntity).collect(Collectors.toList()); - } - - private List getGeneralRecommendsExcept(List alreadyPicked) { - List generalRecommend = new java.util.ArrayList<>(generalRecommendUniversities.getRecommendedUniversities()); - generalRecommend.removeAll(alreadyPicked); - int sizeToPick = RECOMMEND_UNIVERSITY_NUM - alreadyPicked.size(); - Collections.shuffle(generalRecommend); - return generalRecommend.subList(0, sizeToPick); + private final UniversityFilterRepositoryImpl universityFilterRepository; + private final SiteUserRepository siteUserRepository; + + @Value("${university.term}") + public String term; + + /* + * 대학교 상세 정보를 불러온다. + * - 대학교(University) 정보와 대학 지원 정보(UniversityInfoForApply) 정보를 조합하여 반환한다. + * */ + @Transactional(readOnly = true) + public UniversityDetailResponse getUniversityDetail(Long universityInfoForApplyId) { + UniversityInfoForApply universityInfoForApply + = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); + University university = universityInfoForApply.getUniversity(); + + return UniversityDetailResponse.of(university, universityInfoForApply); } - public UniversityDetailDto getDetail(Long universityInfoForApplyId) { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyById(universityInfoForApplyId); - University university = universityValidator.getValidatedUniversityById(universityInfoForApply.getUniversity().getId()); - - List languageRequirements = languageRequirementRepository - .findAllByUniversityInfoForApply_Id(universityInfoForApplyId) + /* + * 대학교 검색 결과를 불러온다. + * - 권역, 키워드, 언어 시험 종류, 언어 시험 점수를 조건으로 검색하여 결과를 반환한다. + * - 키워드는 국가명 또는 대학명에 포함되는 것이 조건이다. + * - 언어 시험 점수는 합격 최소 점수보다 높은 것이 조건이다. + * */ + @Transactional(readOnly = true) + public List searchUniversity( + String regionCode, List keywords, LanguageTestType testType, String testScore) { + return universityFilterRepository + .findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScore(regionCode, keywords, testType, testScore) .stream() - .map(LanguageRequirementDto::fromEntity) + .map(UniversityInfoForApplyPreviewResponse::from) .toList(); - - String countryKoreanName = null; - String regionKoreanName = null; - if (university.getCountry() != null) { - countryKoreanName = university.getCountry().getCode().getKoreanName(); - } - if (university.getCountry() != null) { - regionKoreanName = university.getRegion().getCode().getKoreanName(); - } - - return UniversityDetailDto.builder() - .id(university.getId()) - .term(universityInfoForApply.getTerm()) - .koreanName(university.getKoreanName()) - .englishName(university.getEnglishName()) - .formatName(university.getFormatName()) - .region(regionKoreanName) - .country(countryKoreanName) - .homepageUrl(university.getHomepageUrl()) - .logoImageUrl(university.getLogoImageUrl()) - .backgroundImageUrl(university.getBackgroundImageUrl()) - .detailsForLocal(university.getDetailsForLocal()) - .studentCapacity(universityInfoForApply.getStudentCapacity()) - .tuitionFeeType(universityInfoForApply.getTuitionFeeType().getKoreanName()) - .semesterAvailableForDispatch(universityInfoForApply.getSemesterAvailableForDispatch().getKoreanName()) - .languageRequirements(languageRequirements) - .detailsForLanguage(universityInfoForApply.getDetailsForLanguage()) - .gpaRequirement(universityInfoForApply.getGpaRequirement()) - .gpaRequirementCriteria(universityInfoForApply.getGpaRequirementCriteria()) - .semesterRequirement(universityInfoForApply.getSemesterRequirement()) - .detailsForApply(universityInfoForApply.getDetailsForApply()) - .detailsForMajor(universityInfoForApply.getDetailsForMajor()) - .detailsForAccommodation(universityInfoForApply.getDetailsForAccommodation()) - .detailsForEnglishCourse(universityInfoForApply.getDetailsForEnglishCourse()) - .details(universityInfoForApply.getDetails()) - .accommodationUrl(university.getAccommodationUrl()) - .englishCourseUrl(university.getEnglishCourseUrl()) - .build(); } - - public List search(String region, List keywords, String testType, String testScore) { - RegionCode regionCode = null; - if (region != null && !region.isBlank()) { - regionCode = RegionCode.getRegionCodeByKoreanName(region); - } - - List countryCodes = null; - if (keywords != null && !keywords.isEmpty()) { - countryCodes = CountryCode.getCountryCodeMatchesToKeyword(keywords); - } - - List universities = universityRepositoryForFilter.findByRegionAndCountryAndKeyword(regionCode, countryCodes, keywords); - return universities.stream() - .filter(university -> universityInfoForApplyRepository.existsByUniversityAndTerm(university, TERM)) - .filter(university -> { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversityAndTerm(university); - if (!testType.isBlank()) { - LanguageTestType languageTestType = LanguageTestType.getLanguageTestTypeForString(testType); - if (!testScore.isBlank()) { - return languageRequirementRepository.findByUniversityInfoForApplyAndLanguageTestTypeAndLessThanMyScore(universityInfoForApply, languageTestType, testScore).isPresent(); - } - return languageRequirementRepository.existsByUniversityInfoForApplyAndLanguageTestType(universityInfoForApply, languageTestType); - } - return true; - }) - .map(university -> { - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyByUniversityAndTerm(university); - return UniversityPreviewDto.fromEntity(universityInfoForApply); - }) - .toList(); - } - - public LikedResultDto like(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyById(universityInfoForApplyId); + /* + * 대학교를 '좋아요' 한다. + * - 이미 좋아요가 눌러져있다면, 좋아요를 취소한다. + * */ + @Transactional + public LikeResultResponse likeUniversity(String email, Long universityInfoForApplyId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); if (alreadyLikedUniversity.isPresent()) { likedUniversityRepository.delete(alreadyLikedUniversity.get()); - return LikedResultDto.builder() - .result("LIKE_CANCELED") - .build(); + return new LikeResultResponse(LIKE_CANCELED_MESSAGE); } LikedUniversity likedUniversity = LikedUniversity.builder() @@ -184,15 +85,17 @@ public LikedResultDto like(String email, Long universityInfoForApplyId) { .siteUser(siteUser) .build(); likedUniversityRepository.save(likedUniversity); - return LikedResultDto.builder() - .result("LIKE_SUCCESS") - .build(); + return new LikeResultResponse(LIKE_SUCCESS_MESSAGE); } - public boolean getIsLiked(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserValidator.getValidatedSiteUserByEmail(email); - UniversityInfoForApply universityInfoForApply = universityValidator.getValidatedUniversityInfoForApplyById(universityInfoForApplyId); - return likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply) - .isPresent(); + /* + * '좋아요'한 대학교인지 확인한다. + * */ + @Transactional(readOnly = true) + public IsLikeResponse getIsLiked(String email, Long universityInfoForApplyId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); + boolean isLike = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply).isPresent(); + return new IsLikeResponse(isLike); } } diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java b/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java deleted file mode 100644 index ca3e82cee..000000000 --- a/src/main/java/com/example/solidconnection/university/service/UniversityValidator.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.example.solidconnection.university.service; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.University; -import com.example.solidconnection.entity.UniversityInfoForApply; -import com.example.solidconnection.university.repository.LanguageRequirementRepository; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import com.example.solidconnection.university.repository.UniversityRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import static com.example.solidconnection.constants.Constants.TERM; -import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; -import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_NOT_FOUND; - -@Service -@RequiredArgsConstructor -public class UniversityValidator { - private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final UniversityRepository universityRepository; - private final LanguageRequirementRepository languageRequirementRepository; - - public UniversityInfoForApply getValidatedUniversityInfoForApplyByIdAndTerm(Long id) { - return universityInfoForApplyRepository.findByIdAndTerm(id, TERM) - .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); - } - - public UniversityInfoForApply getValidatedUniversityInfoForApplyById(Long id) { - return universityInfoForApplyRepository.findById(id) - .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); - } - - public UniversityInfoForApply getValidatedUniversityInfoForApplyByUniversityAndTerm(University university) { - return universityInfoForApplyRepository.findByUniversityAndTerm(university, TERM) - .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); - } - - public UniversityInfoForApply getValidatedUniversityInfoForApplyByUniversityAndTermNoException(University university) { - return universityInfoForApplyRepository.findByUniversityAndTerm(university, TERM) - .orElse(null); - } - - public University getValidatedUniversityById(Long id) { - return universityRepository.findById(id) - .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); - } -} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index c0e5c7006..c3fa34ce4 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,520 +1,224 @@ -INSERT INTO region (region_code) VALUES - ('ASIA'), - ('AMERICAS'), - ('CHINA'), - ('EUROPE'); +INSERT INTO region (code, korean_name) +VALUES ('ASIA', '아시아권'), + ('AMERICAS', '미주권'), + ('CHINA', '중국권'), + ('EUROPE', '유럽권'); -INSERT INTO country (country_code, region_code) VALUES - ('BN', 'ASIA'), - ('SG', 'ASIA'), - ('AZ', 'ASIA'), - ('ID', 'ASIA'), - ('JP', 'ASIA'), - ('TR', 'EUROPE'), - ('HK', 'ASIA'), - ('US', 'AMERICAS'), - ('CA', 'AMERICAS'), - ('AU', 'AMERICAS'), - ('BR', 'AMERICAS'), - ('NL', 'EUROPE'), - ('NO', 'EUROPE'), - ('DK', 'EUROPE'), - ('DE', 'EUROPE'), - ('SE', 'EUROPE'), - ('CH', 'EUROPE'), - ('ES', 'EUROPE'), - ('GB', 'EUROPE'), - ('AT', 'EUROPE'), - ('IT', 'EUROPE'), - ('CZ', 'EUROPE'), - ('PT', 'EUROPE'), - ('FR', 'EUROPE'), - ('FI', 'EUROPE'), - ('CN', 'CHINA'), - ('TW', 'CHINA'); +INSERT INTO country (code, korean_name, region_code) +VALUES ('BN', '브루나이', 'ASIA'), + ('SG', '싱가포르', 'ASIA'), + ('AZ', '아제르바이잔', 'ASIA'), + ('ID', '인도네시아', 'ASIA'), + ('JP', '일본', 'ASIA'), + ('TR', '튀르키예', 'ASIA'), + ('HK', '홍콩', 'ASIA'), + ('US', '미국', 'AMERICAS'), + ('CA', '캐나다', 'AMERICAS'), + ('AU', '호주', 'ASIA'), + ('BR', '브라질', 'AMERICAS'), + ('NL', '네덜란드', 'EUROPE'), + ('NO', '노르웨이', 'EUROPE'), + ('DK', '덴마크', 'EUROPE'), + ('DE', '독일', 'EUROPE'), + ('SE', '스웨덴', 'EUROPE'), + ('CH', '스위스', 'EUROPE'), + ('ES', '스페인', 'EUROPE'), + ('GB', '영국', 'EUROPE'), + ('AT', '오스트리아', 'EUROPE'), + ('IT', '이탈리아', 'EUROPE'), + ('CZ', '체코', 'EUROPE'), + ('PT', '포르투갈', 'EUROPE'), + ('FR', '프랑스', 'EUROPE'), + ('FI', '핀란드', 'EUROPE'), + ('CN', '중국', 'CHINA'), + ('TW', '대만', 'CHINA'), + ('HU', '헝가리', 'EUROPE'), + ('LT', '리투아니아', 'EUROPE'), + ('TH', '태국', 'ASIA'), + ('UZ', '우즈베키스탄', 'ASIA'); -INSERT INTO university -(country_code, region_code, english_name, format_name, korean_name, accommodation_url, english_course_url, homepage_url, details_for_local, logo_image_url, background_image_url) -VALUES - ('US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학(A형)', 'https://www.uog.edu/life-at-uog/residence-halls/', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/admissions/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png'), - ('US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학(B형)', 'https://www.uog.edu/life-at-uog/residence-halls/', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/admissions/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png'), - ('US', 'AMERICAS', 'University of Nevada, Las Vegas', 'university_of_nevada_las_vegas', '네바다주립대학 라스베이거스(B형)', 'https://www.unlv.edu/housing', 'https://www.unlv.edu/engineering/academic-programs', 'https://www.unlv.edu/engineering/eip', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png'), - ('US', 'AMERICAS', 'University of Nevada, Las Vegas', 'university_of_nevada_las_vegas', '네바다주립대학 라스베이거스(어학연수)', 'https://www.unlv.edu/housing', 'https://www.unlv.edu/engineering/academic-programs', 'https://www.unlv.edu/engineering/eip', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png'), - ('US', 'AMERICAS', 'University of Nebraska at Kearney', 'university_of_nebraska_at_kearney', '네브라스카 주립대학(A형)', 'https://www.unk.edu/offices/reslife/index.php', 'https://catalog.unk.edu/undergraduate/', 'https://www.unk.edu/index.php', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nebraska_at_kearney/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nebraska_at_kearney/1.png'), - ('US', 'AMERICAS', 'University of Nebraska at Kearney', 'university_of_nebraska_at_kearney', '네브라스카 주립대학(B형)', 'https://www.unk.edu/offices/reslife/index.php', 'https://catalog.unk.edu/undergraduate/', 'https://www.unk.edu/index.php', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nebraska_at_kearney/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nebraska_at_kearney/1.png'), - ('US', 'AMERICAS', 'North Park University', 'north_park_university', '노스파크대학(A형)', 'https://www.northpark.edu/campus-life-and-services/residence-life-and-housing/ㅍ', 'https://paygate.northpark.edu:8173/Student/Courses', 'https://www.northpark.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/north_park_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/north_park_university/1.png'), - ('US', 'AMERICAS', 'North Park University', 'north_park_university', '노스파크대학(B형)', 'https://www.northpark.edu/campus-life-and-services/residence-life-and-housing/', 'https://paygate.northpark.edu:8173/Student/Courses', 'https://www.northpark.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/north_park_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/north_park_university/1.png'), - ('US', 'AMERICAS', 'SUNY ESF', 'suny_esf', '뉴욕주립대 환경과학임학대학(A형)', 'https://www.esf.edu/housing/', 'https://www.esf.edu/catalog/courses/', 'https://www.esf.edu/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/suny_esf/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/suny_esf/1.png'), - ('US', 'AMERICAS', 'SUNY ESF', 'suny_esf', '뉴욕주립대 환경과학임학대학(B형)', 'https://www.esf.edu/housing/', 'https://www.esf.edu/catalog/courses/', 'https://www.esf.edu/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/suny_esf/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/suny_esf/1.png'), - ('US', 'AMERICAS', 'Stony Brook University', 'stony_brook_university', '뉴욕주립대학 스토니브룩(B형)', 'https://www.stonybrook.edu/commcms/studentaffairs/res/', 'https://www.stonybrook.edu/sb/bulletin/current/courses/browse/byabbreviation/', 'https://www.stonybrook.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/stony_brook_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/stony_brook_university/1.png'), - ('US', 'AMERICAS', 'New Jersey City Univeristy', 'new_jersey_city_univeristy', '뉴저지시티대학(A형)', 'https://www.njcu.edu/admissions-aid/international-students/international-student-housing', 'https://www.njcu.edu/admissions-aid/international-students/information-accepted-students/choosing-classes', 'https://www.njcu.edu/admissions-aid/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/new_jersey_city_univeristy/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/new_jersey_city_univeristy/1.png'), - ('US', 'AMERICAS', 'New Jersey City Univeristy', 'new_jersey_city_univeristy', '뉴저지시티대학(B형)', 'https://www.njcu.edu/admissions-aid/international-students/international-student-housing', 'https://www.njcu.edu/admissions-aid/international-students/information-accepted-students/choosing-classes', 'https://www.njcu.edu/admissions-aid/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/new_jersey_city_univeristy/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/new_jersey_city_univeristy/1.png'), - ('US', 'AMERICAS', 'Arkansas State University', 'arkansas_state_university', '아칸소주립대학(A형)', 'https://www.astate.edu/a/university-housing/housing-options/', 'https://ssb-prod.ec.astate.edu/PROD/bwckschd.p_disp_dyn_sched', 'https://www.astate.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/arkansas_state_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/arkansas_state_university/1.png'), - ('US', 'AMERICAS', 'Arkansas State University', 'arkansas_state_university', '아칸소주립대학(B형)', 'https://www.astate.edu/a/university-housing/housing-options/', 'https://ssb-prod.ec.astate.edu/PROD/bwckschd.p_disp_dyn_sched', 'https://www.astate.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/arkansas_state_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/arkansas_state_university/1.png'), - ('US', 'AMERICAS', 'Angelo State University', 'angelo_state_university', '안젤로주립대학(A형)', 'https://www.angelo.edu/dept/residential_programs', 'https://ssb.angelo.edu/prod/bwwkschd.p_disp_dyn_sched', 'https://www.angelo.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/angelo_state_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/angelo_state_university/1.png'), - ('US', 'AMERICAS', 'Angelo State University', 'angelo_state_university', '안젤로주립대학(B형)', 'https://www.angelo.edu/dept/residential_programs', 'https://ssb.angelo.edu/prod/bwwkschd.p_disp_dyn_sched', 'https://www.angelo.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/angelo_state_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/angelo_state_university/1.png'), - ('US', 'AMERICAS', 'The University of Alabama in Huntsville', 'the_university_of_alabama_in_huntsville', '앨러배마헌츠빌대학 (A형)', 'https://www.uah.edu/housing', 'https://catalog.uah.edu/undergrad/course-descriptions/', 'https://www.uah.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/the_university_of_alabama_in_huntsville/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/the_university_of_alabama_in_huntsville/1.png'), - ('US', 'AMERICAS', 'The University of Alabama in Huntsville', 'the_university_of_alabama_in_huntsville', '앨러배마헌츠빌대학 (B형)', 'https://www.uah.edu/housing', 'https://catalog.uah.edu/undergrad/course-descriptions/', 'https://www.uah.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/the_university_of_alabama_in_huntsville/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/the_university_of_alabama_in_huntsville/1.png'), - ('US', 'AMERICAS', 'Illinois Institute of Technology', 'illinois_institute_of_technology', '일리노이공과대학(교환학생 과정)', 'https://www.iit.edu/housing/housing-options/housing-rates', 'http://bulletin.iit.edu/undergraduate/courses/', 'https://www.iit.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/illinois_institute_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/illinois_institute_of_technology/1.png'), - ('US', 'AMERICAS', 'Illinois Institute of Technology', 'illinois_institute_of_technology', '일리노이공과대학(복수학위 과정)', 'https://www.iit.edu/housing/housing-options/housing-rates', 'http://bulletin.iit.edu/undergraduate/courses/', 'https://www.iit.edu/admissions-aid/undergraduate-admission/international-undergraduate-students/how-apply-international-undergraduate-students/international-transfer-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/illinois_institute_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/illinois_institute_of_technology/1.png'), - ('US', 'AMERICAS', 'Taylor university', 'taylor_university', '테일러대학', 'https://www.taylor.edu/life-at-taylor/residence-life/residence-halls/', 'https://www.taylor.edu/offices/registrar/class-schedules', 'https://www.taylor.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/taylor_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/taylor_university/1.png'), - ('US', 'AMERICAS', 'Temple University', 'temple_university', '템플대학(혼합형)', 'https://globalprograms.temple.edu/housing', 'https://prd-xereg.temple.edu/StudentRegistrationSsb/ssb/term/termSelection?mode=search', 'http://globalprograms.temple.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/temple_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/temple_university/1.png'), - ('US', 'AMERICAS', 'Troy University ', 'troy_university', '트로이주립대학(A형)', 'https://www.troy.edu/student-life-resources/housing/index.html', 'https://www.troy.edu/academics/catalogs/#Undergraduate', 'https://www.troy.edu/international/index.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/troy_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/troy_university/1.png'), - ('US', 'AMERICAS', 'Troy University ', 'troy_university', '트로이주립대학(B형)', 'https://www.troy.edu/student-life-resources/housing/index.html', 'https://www.troy.edu/academics/catalogs/#Undergraduate', 'https://www.troy.edu/international/index.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/troy_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/troy_university/1.png'), - ('US', 'AMERICAS', 'University of Hawaii at Manoa', 'university_of_hawaii_at_manoa', '하와이대학(B형)', 'https://manoa.hawaii.edu/mix/inbound/housing-meals/', 'https://www.sis.hawaii.edu/uhdad/avail.classes?i=MAN', 'https://www.hawaii.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hawaii_at_manoa/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hawaii_at_manoa/1.png'), - ('BR', 'AMERICAS', 'Pontificia Universidade Catolica de Minas Gerais', 'pontificia_universidade_catolica_de_minas_gerais', '카톨릭 대학 미나스제라이스', NULL, 'http://www1.pucminas.br/ari/index_padrao.php?pagina=5829', 'https://www.pucminas.br/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/pontificia_universidade_catolica_de_minas_gerais/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/pontificia_universidade_catolica_de_minas_gerais/1.png'), - ('BR', 'AMERICAS', 'UNIVERSITY OF FORTALEZA', 'university_of_fortaleza', '포르탈레자 대학', 'https://unifor.br/web/guest/international/exchange-students#tabs', 'https://unifor.br/web/guest/international/exchange-students#tabs', 'https://www.unifor.br/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_fortaleza/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_fortaleza/1.png'), - ('CA', 'AMERICAS', 'University of Regina', 'university_of_regina', '리자이나대학(A형)', 'https://www.uregina.ca/housing/housing-options/index.html', 'https://banner.uregina.ca/prod/sct/bwckschd.p_disp_dyn_sched', 'https://www.uregina.ca/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_regina/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_regina/1.png'), - ('CA', 'AMERICAS', 'University of Regina', 'university_of_regina', '리자이나대학(B형)', 'https://www.uregina.ca/housing/housing-options/index.html', 'https://banner.uregina.ca/prod/sct/bwckschd.p_disp_dyn_sched', 'https://www.uregina.ca/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_regina/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_regina/1.png'), - ('CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스(A형)', 'https://www.mun.ca/residences/', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'https://mun.ca/goabroad/visiting-students-inbound/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png'), - ('CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스(B형)', 'www.mun.ca/residences', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'https://mun.ca/goabroad/visiting-students-inbound/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png'), - ('CA', 'AMERICAS', 'Memorial University of Newfoundland, Grenfell Campus', 'memorial_university_of_newfoundland_grenfell_campus', '메모리얼대학 그랜펠(어학연수)', 'www.grenfell.mun.ca/housing', 'https://www.grenfell.mun.ca/e니', 'https://mun.ca/grenfellcampus/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_grenfell_campus/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_grenfell_campus/1.png'), - ('AU', 'AMERICAS', 'RMIT University', 'rmit_university', 'RMIT멜버른공과대학(A형)', 'https://www.rmit.edu.au/students/student-life/accommodation', 'https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/study-abroad/study-abroad-exchange-course-search', 'https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/rmit_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/rmit_university/1.png'), - ('AU', 'AMERICAS', 'University of Southern Queensland', 'university_of_southern_queensland', '서던퀸스랜드대학(A형)', 'https://www.unisq.edu.au/current-students/academic/residential-schools/accommodation', 'https://www.unisq.edu.au/course/specification/current/', 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/1.png'), - ('AU', 'AMERICAS', 'University of Southern Queensland', 'university_of_southern_queensland', '서던퀸스랜드대학(B형)', 'https://www.unisq.edu.au/current-students/support/accommodation', 'https://www.unisq.edu.au/course/specification/current/', 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/1.png'), - ('AU', 'AMERICAS', 'University of Sydney', 'university_of_sydney', '시드니대학', 'https://www.sydney.edu.au/study/accommodation.html', 'www.sydney.edu.au/sydney-abroad-units', 'https://www.sydney.edu.au/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_sydney/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_sydney/1.png'), - ('AU', 'AMERICAS', 'Curtin University', 'curtin_university', '커틴대학(A형)', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/1.png'), - ('AU', 'AMERICAS', 'Curtin University', 'curtin_university', '커틴대학(B형)', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/1.png'), - ('NL', 'EUROPE', 'University of Groningen', 'university_of_groningen', '그로닝겐 대학교', 'https://www.rug.nl/feb/education/exchange/incoming/practical-information/accommodation', 'https://www.rug.nl/feb/education/exchange/incoming/before/courses-exams', 'https://www.rug.nl/feb/education/exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_groningen/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_groningen/1.png'), - ('NL', 'EUROPE', 'Saxion University of Applied Sciences ', 'saxion_university_of_applied_sciences', '삭시온대학교', 'https://www.saxion.edu/studying-in-the-netherlands/practical-matters/accommodation/housing-via-saxion', 'https://www.saxion.edu/programmes', 'https://www.saxion.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saxion_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saxion_university_of_applied_sciences/1.png'), - ('NL', 'EUROPE', 'Fontys University of Applied Sciences ', 'fontys_university_of_applied_sciences', '폰티스대학', 'https://fontys.edu/Study-at-Fontys/Practical-information-1/Arriving-in-The-Netherlands-1/Accommodation.htm', 'https://www.fontys.nl/en/Study-at-Fontys/Exchange-programmes', 'https://www.fontys.nl/en/Home.htm', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/fontys_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/fontys_university_of_applied_sciences/1.png'), - ('NO', 'EUROPE', 'BI Norwegian Business School', 'bi_norwegian_business_school', '노르웨이 경영대학', 'https://www.bi.edu/study-at-bi/housing/', 'https://www.bi.edu/programmes-and-individual-courses/exchange-programme/', 'https://www.bi.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bi_norwegian_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bi_norwegian_business_school/1.png'), - ('DK', 'EUROPE', 'University of Southern Denmark', 'university_of_southern_denmark', '서던덴마크대학교', 'https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing', 'https://www.sdu.dk/en/uddannelse/exchange_programmes', 'https://www.sdu.dk/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/1.png'), - ('DK', 'EUROPE', 'IT University of Copenhagen', 'it_university_of_copenhagen', '코펜하겐 IT대학', 'https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students', 'https://en.itu.dk/Programmes/Exchange-students/Become-an-exchange-student-at-ITU', 'https://en.itu.dk/programmes/exchange-students/become-an-exchange-student-at-itu', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/1.png'), - ('DE', 'EUROPE', 'Neu-Ulm University of Applied Sciences', 'neu-ulm_university_of_applied_sciences', '노이울름 대학', 'https://www.hnu.de/fileadmin/user_upload/5_Internationales/International_Incomings/Bewerbung/Housing_Broschure.pdf', 'https://www.hnu.de/en/international/international-exchange-students/courses-taught-in-english', 'https://www.hnu.de/en/international', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neu-ulm_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neu-ulm_university_of_applied_sciences/1.png'), - ('DE', 'EUROPE', 'Deggendorf University of Applied Sciences', 'deggendorf_university_of_applied_sciences', '데겐도르프대학', 'https://www.th-deg.de/en/study-with-us/accommodation', 'https://th-deg.de/exchange-students#course-choices', 'www.th-deg.de/exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/deggendorf_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/deggendorf_university_of_applied_sciences/1.png'), - ('DE', 'EUROPE', 'RWU Ravensburg Weingarten University of Applied Sciences ', 'rwu_ravensburg_weingarten_university_of_applied_sciences', '라벤스부르크 바인가르텐 응용과학대학교', 'https://www.rwu.de/en/international/exchange-students/application ', 'https://www.rwu.de/en/international/exchange-students/study-and-course-offer', 'https://www.rwu.de/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/rwu_ravensburg_weingarten_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/rwu_ravensburg_weingarten_university_of_applied_sciences/1.png'), - ('DE', 'EUROPE', 'Reutlingen University', 'reutlingen_university', '로이틀링겐 대학', 'https://www.reutlingen-university.de/en/studies/student-life/student-accommodation', NULL, 'https://www.reutlingen-university.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/reutlingen_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/reutlingen_university/1.png'), - ('DE', 'EUROPE', 'Ludwigshafen University of Business and Society', 'ludwigshafen_university_of_business_and_society', '루트비히스하펜 경영사회대학교 ', 'https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/housing', 'https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/business-courses-in-english-bachelor', 'https://www.hwg-lu.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ludwigshafen_university_of_business_and_society/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ludwigshafen_university_of_business_and_society/1.png'), - ('DE', 'EUROPE', 'Baden-Wuerttemberg cooperative state univ.(DHBW)', 'baden-wuerttemberg_cooperative_state_univ(dhbw)', '바덴뷔르템베르크 산학협력대학', 'https://www.studierendenwerk-stuttgart.de/en/accommodation/', 'https://www.dhbw-stuttgart.de/studium/internationales/international-students/exchange-students/academic-information/', 'https://www.dhbw-stuttgart.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baden-wuerttemberg_cooperative_state_univ(dhbw)/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baden-wuerttemberg_cooperative_state_univ(dhbw)/1.png'), - ('DE', 'EUROPE', 'Freie Universitat Berlin', 'freie_universitat_berlin', '베를린자유대학교', 'http://www.fu-berlin.de/en/sites/unterbringung', 'https://www.fu-berlin.de/vv/en/fb ', 'https://www.fu-berlin.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/freie_universitat_berlin/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/freie_universitat_berlin/1.png'), - ('DE', 'EUROPE', 'Technical University of Applied Sciences Wurzburg-Schweinfurt', 'technical_university_of_applied_sciences_wurzburg-schweinfurt', '뷔르츠부르크-슈바인푸르트 대학', 'https://www.studentenwerk-wuerzburg.de/en/wohnen/move-in-guide.html', 'https://fwiwi.thws.de/en/international/incoming-exchange-students/studying-at-fhws/schedule/', 'https://fwiwi.thws.de/#', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/technical_university_of_applied_sciences_wurzburg-schweinfurt/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/technical_university_of_applied_sciences_wurzburg-schweinfurt/1.png'), - ('DE', 'EUROPE', 'Hochschule Schmalkalden University of Applied Sciences', 'hochschule_schmalkalden_university_of_applied_sciences', '슈말칼덴 응용과학대학', 'https://www.stw-thueringen.de/en/housing/', 'https://www.hs-schmalkalden.de/en/international/incoming-students/courses-for-incomings/exchange-students', 'https://www.hs-schmalkalden.de/en.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/hochschule_schmalkalden_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/hochschule_schmalkalden_university_of_applied_sciences/1.png'), - ('DE', 'EUROPE', 'Stuttgart University of Applied Sciences', 'stuttgart_university_of_applied_sciences', '슈투트가르트 공과대학', 'https://www.hft-stuttgart.com/international/incoming-students/services#c15978', 'https://www.hft-stuttgart.com/international/incoming-students/information-application', 'https://www.hft-stuttgart.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/stuttgart_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/stuttgart_university_of_applied_sciences/1.png'), - ('DE', 'EUROPE', 'University of Applied Sciences Aschaffenburg', 'university_of_applied_sciences_aschaffenburg', '아샤펜부르크 대학', 'https://www.studentenwerk-wuerzburg.de/en/aschaffenburg/student-residences.html', 'https://www.th-ab.de/en/education/exchange-students/course-offer', 'https://www.th-ab.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_aschaffenburg/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_aschaffenburg/1.png'), - ('DE', 'EUROPE', 'Augsburg University of Applied Sciences', 'augsburg_university_of_applied_sciences', '아우크스부르크대학', 'https://www.hs-augsburg.de/Binaries/Binary49994/EN-Guideline-for-finding-accomondation.pdf ', 'https://www.hs-augsburg.de/en/International/Course-Catalogue.html', 'https://www.hs-augsburg.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/augsburg_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/augsburg_university_of_applied_sciences/1.png'), - ('DE', 'EUROPE', 'Albstadt-Sigmaringen University of Applied Science', 'albstadt-sigmaringen_university_of_applied_science', '알브슈타트 지그마링엔 대학', 'https://www.my-stuwe.de/en/housing/halls-of-residence-albstadt/', 'https://www.hs-albsig.de/fileadmin/user_upload/hsas/International_Office/Courses_in_English_HS_AlbSig.pdf', 'https://www.hs-albsig.de/studieninfos/im-studium/international-office', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/albstadt-sigmaringen_university_of_applied_science/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/albstadt-sigmaringen_university_of_applied_science/1.png'), - ('DE', 'EUROPE', 'University of Erlangen Nuremberg', 'university_of_erlangen_nuremberg', '에어랑엔 뉘른베르크 대학 ', 'https://www.fau.eu/education/student-life/accommodation-2/', 'https://www.campo.fau.de/qisserver/pages/cm/exa/coursecatalog/showCourseCatalog.xhtml?_flowId=showCourseCatalog-flow&_flowExecutionKey=e1s1&noDBAction=y&init=y', 'https://www.fau.eu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_erlangen_nuremberg/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_erlangen_nuremberg/1.png'), - ('DE', 'EUROPE', 'Otto von Guericke University of Magdeburg', 'otto_von_guericke_university_of_magdeburg', '오토폰귀릭케마그데부르그 대학', 'https://tl1host.eu/SWMD/#admission', 'https://www.ovgu.de/unimagdeburg/en/International/Incoming+_+Ways+to+the+University/International+Students/Exchange+Programmes/Studying+as+a+WORLDWIDE+Exchange+Student-p-48750.html', 'https://www.ovgu.de/en/International/Incoming+_+Ways+to+the+University/International+Students/Exchange+Programmes.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otto_von_guericke_university_of_magdeburg/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otto_von_guericke_university_of_magdeburg/1.png'), - ('DE', 'EUROPE', 'Carl von Ossietzky University of Oldenburg', 'carl_von_ossietzky_university_of_oldenburg', '올덴부르크 대학', 'https://uol.de/en/exchange-studies/living-in-oldenburg', 'https://elearning.uni-oldenburg.de/plugins.php/veranstaltungsverzeichnis_lvsg/englishmodules?vvz_sem_select=e770f053fbe29f2d1bd56a01d0dde1d0', 'https://uol.de/en/exchange-studies', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/carl_von_ossietzky_university_of_oldenburg/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/carl_von_ossietzky_university_of_oldenburg/1.png'), - ('DE', 'EUROPE', 'Zepplin University', 'zepplin_university', '제플린대학', 'https://www.zeppelin-university.com/info-wAssets/universitaet/dokumente/international-office/housing.pdf', 'https://zuhause.zeppelin-university.net/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=ACTION&ARGUMENTS=-AXFLjV7~Wgj~xhfmwa1nYvK3AVqeNsVYlJE1s9BJUvPyN3hZATz-SN~fW4BQGvcqQrbg69LM7Vb2PmCS13njtn6vY7wlZ3PR0VVc--5~HKXDORfzpyZYMWO-LB2OopwYkzVvJJ~JUF3g150btYFH8iNVzj12-lBywRT6Aplt7cIeSaUbvmD5Cny-23I6rfUTkzn1OdViRhkbSGv0_', 'https://www.zu.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zepplin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zepplin_university/1.png'), - ('DE', 'EUROPE', 'Chemnitz University of Technology', 'chemnitz_university_of_technology', '쳄니츠 공과대학', 'https://www.swcz.de/en/student-housing/our-halls-of-residence/', 'https://www.tu-chemnitz.de/international/incoming/erasmus/vlvz.php.en', 'https://www.tu-chemnitz.de/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chemnitz_university_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chemnitz_university_of_technology/1.png'), - ('DE', 'EUROPE', 'Karlsruhe University of Applied Sciences', 'karlsruhe_university_of_applied_sciences', '칼스루에 대학', 'https://www.h-ka.de/en/accommodation', 'https://www.h-ka.de/en/internationalprogram/profile', 'https://www.h-ka.de/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/karlsruhe_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/karlsruhe_university_of_applied_sciences/1.png'), - ('DE', 'EUROPE', 'Heilbronn University of Applied Sciences', 'heilbronn_university_of_applied_sciences', '하일브론 과학대학교', 'https://www.hs-heilbronn.de/accommodation', 'https://cdn.hs-heilbronn.de/4641dc9db5209412/389f4f6cac1d/English-Course-Offer-and-Course-descriptions-Campus-Schw-bisch-Hall_WS-2023-24.pdf', 'https://www.hs-heilbronn.de/en/incoming-exchange-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/heilbronn_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/heilbronn_university_of_applied_sciences/1.png'), - ('SE', 'EUROPE', 'Malmo University', 'malmo_university', '말뫼대학', 'https://mau.se/en/education/housing/', 'https://mau.se/en/study-education/?r.PagingNumber=1&r.Languages=en&r.Query=&r.TypesSelected=67498&r.Sort=alphabetically', 'https://student.mau.se/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/malmo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/malmo_university/1.png'), - ('SE', 'EUROPE', 'University College of Boras', 'university_college_of_boras', '보라스 대학교', 'https://www.hb.se/en/accommodation', 'https://www.hb.se/exchangecourses', 'https://www.hb.se/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_college_of_boras/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_college_of_boras/1.png'), - ('CH', 'EUROPE', 'Zurich University of Applied Sciences', 'zurich_university_of_applied_sciences', '취리히응용과학기술대학', 'https://www.zhaw.ch/en/study/before-your-studies/student-accommodation', 'https://www.zhaw.ch/storage/engineering/studium/internationales-studium/vom-ausland-in-die-schweiz/spring_semester_zhaw_school_of_engineering.pdf', 'https://www.zhaw.ch/en/university/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zurich_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zurich_university_of_applied_sciences/1.png'), - ('ES', 'EUROPE', 'Universidad de Navarra', 'universidad_de_navarra', '나바라대학교 ', NULL, NULL, 'https://www.unav.edu/web/facultad-de-derecho/estudiantes/programas-de-intercambio/incoming-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universidad_de_navarra/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universidad_de_navarra/1.png'), - ('ES', 'EUROPE', 'Universidad Carlos III de Madrid', 'universidad_carlos_iii_de_madrid', '마드리드카를로스3세 대학교', 'https://www.uc3m.es/studies/international-exchage-students-in-uc3m/bachelor-degrees/Accommodation', 'https://www.uc3m.es/studies/international-exchage-students-in-UC3M/bachelor-degrees/course-offer', 'https://www.uc3m.es/studies/international-exchange-students-in-UC3M-/bachelor-degrees', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universidad_carlos_iii_de_madrid/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universidad_carlos_iii_de_madrid/1.png'), - ('ES', 'EUROPE', 'University of Lleida', 'university_of_lleida', '예이다대학교', 'http://www.udl.cat/ca/serveis/ori/estudiantat_estranger/eng/infoeng/accommodation/', 'https://www.udl.cat/ca/serveis/ori/estudiantat_estranger/eng/infoeng/subjects/', 'https://www.udl.cat/ca/serveis/ori/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_lleida/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_lleida/1.png'), - ('GB', 'EUROPE', 'University of Hull', 'university_of_hull', '헐대학', 'https://www.hull.ac.uk/Choose-Hull/Student-life/Accommodation/accommodation.aspx', 'https://universityofhull.app.box.com/s/mpvulz3yz0uijdt68rybce19nek0d8eh', 'https://www.hull.ac.uk/choose-hull/study-at-hull/need-to-know/key-dates', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hull/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hull/1.png'), - ('AT', 'EUROPE', 'University of Graz', 'university_of_graz', '그라츠 대학', 'https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/', 'https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf', 'https://www.uni-graz.at/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/1.png'), - ('AT', 'EUROPE', 'Graz University of Technology', 'graz_university_of_technology', '그라츠공과대학', 'https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033', 'https://tugraz.at/go/search-courses', 'https://www.tugraz.at/en/home', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/1.png'), - ('AT', 'EUROPE', 'Catholic Private University Linz', 'catholic_private_university_linz', '린츠 카톨릭 대학교', NULL, 'https://ku-linz.at/en/ku_international/incomings/kulis', 'https://ku-linz.at/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/1.png'), - ('AT', 'EUROPE', 'University of Applied Sciences Technikum Wien', 'university_of_applied_sciences_technikum_wien', '빈 공과대학교', NULL, 'https://www.technikum-wien.at/en/international/student-mobility/', 'https://www.technikum-wien.at/international/studierendenmobilitaet-2/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_technikum_wien/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_technikum_wien/1.png'), - ('IT', 'EUROPE', 'Polytechnic University of Milan', 'polytechnic_university_of_milan', '밀라노공과대학', NULL, NULL, 'https://www.polimi.it/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/polytechnic_university_of_milan/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/polytechnic_university_of_milan/1.png'), - ('IT', 'EUROPE', 'University of Bergamo', 'university_of_bergamo', '베르가모 대학', NULL, NULL, 'https://en.unibg.it/international/students-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_bergamo/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_bergamo/1.png'), - ('IT', 'EUROPE', 'University of Ca''Poscari', 'university_of_caposcari', '카포스카리대학교', NULL, 'https://www.unive.it/data/9639/', 'https://www.unive.it/pag/13526', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_caposcari/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_caposcari/1.png'), - ('CZ', 'EUROPE', 'University of Ostrava', 'university_of_ostrava', '오스트라바 대학', 'https://koleje.osu.eu/', 'https://www.osu.eu/22821/courses/', 'https://www.osu.eu', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_ostrava/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_ostrava/1.png'), - ('CZ', 'EUROPE', 'Czech University of Life Sciences Prague', 'czech_university_of_life_sciences_prague', '체코 생명과학대학', NULL, 'https://www.czu.cz/en/r-9190-international-relations/r-17025-course-offer-academic-year-2023-2024', 'https://www.czu.cz/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/czech_university_of_life_sciences_prague/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/czech_university_of_life_sciences_prague/1.png'), - ('CZ', 'EUROPE', 'Czech Technical University in Prague', 'czech_technical_university_in_prague', '프라하공과대학', 'https://www.suz.cvut.cz/cz', 'https://legacy.mobility.cvut.cz/prospectus/2023/index.php', 'https://international.cvut.cz/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/czech_technical_university_in_prague/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/czech_technical_university_in_prague/1.png'), - ('PT', 'EUROPE', 'INSTITUTO SUPERIOR TECNICO-Universidade de Lisboa', 'instituto_superior_tecnico-universidade_de_lisboa', '리스본대학 공과대학', NULL, NULL, 'https://tecnico.ulisboa.pt/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/instituto_superior_tecnico-universidade_de_lisboa/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/instituto_superior_tecnico-universidade_de_lisboa/1.png'), - ('PT', 'EUROPE', 'Catholic University of Portugal', 'catholic_university_of_portugal', '포르투갈 가톨릭대학', NULL, 'https://catolicabs.porto.ucp.pt/international-programmes-0', 'https://catolicabs.porto.ucp.pt/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_university_of_portugal/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_university_of_portugal/1.png'), - ('FR', 'EUROPE', 'EBS Paris', 'ebs_paris', 'EBS Paris', NULL, NULL, 'https://www.ebs-paris.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ebs_paris/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ebs_paris/1.png'), - ('FR', 'EUROPE', 'EPITA', 'epita', 'EPITA', 'http://housing.epitamasters.com', 'https://sway.office.com/u5VHBu9DbBH6Mr8F', 'https://sway.office.com/u5VHBu9DbBH6Mr8F?ref=Link', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/epita/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/epita/1.png'), - ('FR', 'EUROPE', 'EPITECH(L''echole de L''expertise Informatique)', 'epitech(lechole_de_lexpertise_informatique)', 'EPITECH', 'https://international.epitech.eu/student-life/', 'https://international.epitech.eu/', 'https://international.epitech.eu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/epitech(lechole_de_lexpertise_informatique)/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/epitech(lechole_de_lexpertise_informatique)/1.png'), - ('FR', 'EUROPE', 'ESCE International Business School', 'esce_international_business_school', 'ESCE 국제경영대학', 'https://www.studapart.com/en/studapart/student-accommodation?gad=1&gclid=Cj0KCQjw7uSkBhDGARIsAMCZNJvmPVROiqUSiAO69ks7UWWXcIKO1rGO_hXPmjlx72qsV1SF1VrEzRkaAjTQEALw_wcB', NULL, 'http://www.esce.fr/international/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/esce_international_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/esce_international_business_school/1.png'), - ('FR', 'EUROPE', 'Ecole Superieure des Sciences Commerciales d''Anger', 'ecole_superieure_des_sciences_commerciales_danger', 'ESSCA경영대학', NULL, 'https://pcee.azurewebsites.net', 'https://www.essca.fr/en/international/exchange-student', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ecole_superieure_des_sciences_commerciales_danger/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ecole_superieure_des_sciences_commerciales_danger/1.png'), - ('FR', 'EUROPE', 'IPSA', 'ipsa', 'IPSA', 'https://www.ipsa.fr/en/student-life/pratical-information/', NULL, 'https://www.ipsa.fr/en/engineering-school/aeronautical-space', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ipsa/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ipsa/1.png'), - ('FR', 'EUROPE', 'ISEP', 'isep', 'ISEP', NULL, 'https://en.isep.fr/studying-at-isep/course-catalog/', 'https://en.isep.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/isep/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/isep/1.png'), - ('FR', 'EUROPE', 'KEDGE Business School', 'kedge_business_school', 'KEDGE경영대학 ', 'https://student.kedge.edu/student-services/prepare-my-studies-abroad/student-accommodation-in-france', 'https://student.kedge.edu/exchange-programmes/academic-information', 'https://student.kedge.edu', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kedge_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kedge_business_school/1.png'), - ('FR', 'EUROPE', 'NEOMA Business School', 'neoma_business_school', 'NEOMA경영대학', 'https://neoma-bs.com/welcome-to-neoma/steps/step-3-housing/', 'https://neoma-bs.com/welcome-to-neoma/steps/step-4-courses/', 'https://neoma-bs.com/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neoma_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neoma_business_school/1.png'), - ('FR', 'EUROPE', 'HEIP', 'heip', '국제정치대학', 'https://www.studapart.com/en/studapart/student-accommodation?gad=1&gclid=Cj0KCQjw7uSkBhDGARIsAMCZNJvmPVROiqUSiAO69ks7UWWXcIKO1rGO_hXPmjlx72qsV1SF1VrEzRkaAjTQEALw_wcB', NULL, 'https://www.heip.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/heip/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/heip/1.png'), - ('FR', 'EUROPE', 'EM NORMANDIE BUSINESS SCHOOL', 'em_normandie_business_school', '노르망디경영대학', 'https://en.em-normandie.com/em-normandie-experience/open-world-studying-abroad/exchange-programmes', 'https://en.em-normandie.com/em-normandie-experience/open-world-studying-abroad/exchange-programmes', 'https://www.em-normandie.com/fr', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/em_normandie_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/em_normandie_business_school/1.png'), - ('FR', 'EUROPE', 'La Rochelle Business School', 'la_rochelle_business_school', '라로쉘경영대학 ', 'https://excelia-group.studapart.com/en/', 'https://www.excelia-group.com/student-services/international-students/exchange-students/programmes-details', 'https://www.excelia-group.com/student-services', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/la_rochelle_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/la_rochelle_business_school/1.png'), - ('FR', 'EUROPE', 'ESC Rennes School of Business', 'esc_rennes_school_of_business', '렌 경영대학', 'https://www.rennes-sb.com/student-life/accommodation-health/', 'https://www.rennes-sb.com/programmes/exchange-programme/incoming-exchange-students/', 'https://www.rennes-sb.com/programmes/exchange-programme/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/esc_rennes_school_of_business/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/esc_rennes_school_of_business/1.png'), - ('FR', 'EUROPE', 'University of Le Havre', 'university_of_le_havre', '르아브르대학', 'https://www.univ-lehavre.fr/spip.php?article70', NULL, 'https://www.univ-lehavre.fr', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_le_havre/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_le_havre/1.png'), - ('FR', 'EUROPE', 'Lille Catholic University', 'lille_catholic_university', '릴 가톨릭 대학', 'https://www.all-lacatho.fr/en/', 'https://www.univ-catholille.fr/sites/default/files/fichiers/VF%20Catalogue%20cours%20en%20anglais%202022-2023_0.pdf', 'http://www.univ-catholille.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lille_catholic_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lille_catholic_university/1.png'), - ('FR', 'EUROPE', 'Lille Catholic University', 'lille_catholic_university', '릴 가톨릭 대학(ESTICE)', 'https://www.all-lacatho.fr/en/list-accommodation', 'https://estice.fr/programs/', 'http://www.univ-catholille.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lille_catholic_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lille_catholic_university/1.png'), - ('FR', 'EUROPE', 'Universite Montpellier 1', 'universite_montpellier_1', '몽펠리에 대학교', 'https://iae.umontpellier.fr/en/institut/accommodation', 'https://iae.umontpellier.fr/en/institut/exchange-students', 'https://www.umontpellier.fr/university-of-montpell', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_montpellier_1/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_montpellier_1/1.png'), - ('FR', 'EUROPE', 'Ecole d''Architecture, Paris Val de Seine', 'ecole_darchitecture_paris_val_de_seine', '발드센느대학', NULL, 'https://www.paris-valdeseine.archi.fr/fileadmin/mediatheque/document/International/etudiants_entrants/English_speaking_classes.pdf', 'https://www.paris-valdeseine.archi.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ecole_darchitecture_paris_val_de_seine/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ecole_darchitecture_paris_val_de_seine/1.png'), - ('FR', 'EUROPE', 'Burgundy School of Business', 'burgundy_school_of_business', '부르군디 경영대학', 'https://www.studapart.com/fr', 'https://international.bsb-education.com/course-catalogues/?lang=en', 'https://www.bsb-education.com/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/burgundy_school_of_business/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/burgundy_school_of_business/1.png'), - ('FR', 'EUROPE', 'Audencia Business School', 'audencia_business_school', '오덴시아 낭트 경영대학', 'https://www.expatistan.com/cost-of-living/nantes', 'https://apply.exchangestudents.audencia.com/index.cfm?FuseAction=Abroad.ViewLink&Parent_ID=FE40C425-5056-BA1F-74632B3DF27C287E&Link_ID=0365089E-9F2E-9844-AB9F3ABC4462019B', 'https://apply.exchangestudents.audencia.com/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/audencia_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/audencia_business_school/1.png'), - ('FR', 'EUROPE', 'Universite Jean Moulin Lyon 3', 'universite_jean_moulin_lyon_3', '장물랭리옹3세대학교', 'https://associnterlyon3.fr/en/', 'https://www.univ-lyon3.fr/self-study-in-english-in-lyon-france', 'https://www.univ-lyon3.fr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_jean_moulin_lyon_3/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_jean_moulin_lyon_3/1.png'), - ('FR', 'EUROPE', 'Toulouse Business school', 'toulouse_business_school', '툴루즈 경영대학교', 'https://www.tbs-education.com/about-tbs/student-services/', 'https://www.tbs-education.com/about-tbs/international/incoming-exchange-students/', 'https://www.tbs-education.com/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toulouse_business_school/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toulouse_business_school/1.png'), - ('FR', 'EUROPE', 'Sciences Po Toulouse', 'sciences_po_toulouse', '툴루즈정치대학', NULL, 'https://www.sciencespo-toulouse.fr/en/courses/university-diploma-in-international-comparative-studies', 'http://www.sciencespo-toulouse.fr/home-english-version-589930.kjsp', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/sciences_po_toulouse/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/sciences_po_toulouse/1.png'), - ('FR', 'EUROPE', 'University of Paris 8', 'university_of_paris_8', '파리8대학교', 'https://www.univ-paris8.fr/-Informations-pratiques-', 'https://www.univ-paris8.fr/-Etudes-diplomes-', 'https://www.univ-paris8.fr/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_paris_8/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_paris_8/1.png'), - ('FI', 'EUROPE', 'Lahti University of Applied Sciences', 'lahti_university_of_applied_sciences', 'LAB대학', 'https://lab.fi/en/exchange-student-guide', 'https://opinto-opas.lab.fi/70064/en/70060/70065?lang=en', 'https://www.lab.fi/en/exchange-student-guide/studies', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lahti_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lahti_university_of_applied_sciences/1.png'), - ('FI', 'EUROPE', 'Lappeenranta University of Technology', 'lappeenranta_university_of_technology', '라펜란타기술대학교', 'https://www.loas.fi/en/loas/student-housing-foundation-region-lappeenranta-loas', 'https://www.lut.fi/en/studies/exchange-studies/courses-exchange-students', 'https://www.lut.fi/en/studies/exchange-studies', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lappeenranta_university_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/lappeenranta_university_of_technology/1.png'), - ('FI', 'EUROPE', 'Turku University of Applied Sciences', 'turku_university_of_applied_sciences', '투르크 응용과학대학', 'https://www.tuas.fi/en/study-tuas/exchange-students/accommodation/', 'https://www.tuas.fi/en/study-tuas/exchange-students/courses/', 'https://www.tuas.fi/en/study-tuas/exchange-students/about/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/turku_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/turku_university_of_applied_sciences/1.png'), - ('FR', 'EUROPE', 'Universite de Technologie de Troyes', 'universite_de_technologie_de_troyes', '트루아 공과대학', 'https://www.utt.fr/study-at-utt/accommodation', 'https://www.utt.fr/study-at-utt/courses-in-english', 'https://www.utt.fr/study-at-utt/academic-programs', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_de_technologie_de_troyes/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universite_de_technologie_de_troyes/1.png'), - ('BN', 'ASIA', 'Universiti Brunei Darussalam', 'universiti_brunei_darussalam', '브루나이 국립대학', NULL, NULL, 'https://ubd.edu.bn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universiti_brunei_darussalam/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/universiti_brunei_darussalam/1.png'), - ('SG', 'ASIA', 'Singapore Management University', 'singapore_management_university', '싱가폴경영대학', NULL, 'https://publiceservices.smu.edu.sg/psc/ps/EMPLOYEE/HRMS/c/SIS_CR.SIS_CLASS_SEARCH.GBL?&', 'https://publiceservices.smu.edu.sg/psc/ps/EMPLOYEE/HRMS/c/SIS_CR.SIS_CLASS_SEARCH.GBL?&', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/singapore_management_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/singapore_management_university/1.png'), - ('AZ', 'ASIA', 'ADA University', 'ada_university', '아다대학교', NULL, 'https://www.ada.edu.az/frq-content/plugins/policies_x1/entry/20221226165811_72744800.pdf ', 'https://www.ada.edu.az/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ada_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ada_university/1.png'), - ('ID', 'ASIA', 'BINUS University', 'binus_university', '비누스대학 ', 'https://binus.ac.id/binussquare/', 'https://linktr.ee/binusexchange', 'https://io.binus.ac.id/international-students/post/student-exchange-program-2/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/binus_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/binus_university/1.png'), - ('JP', 'ASIA', 'Kanazawa University', 'kanazawa_university', '가나자와대학', NULL, NULL, 'https://kuglobal.w3.kanazawa-u.ac.jp/eg/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kanazawa_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kanazawa_university/1.png'), - ('JP', 'ASIA', 'Gakushuin University', 'gakushuin_university', '가쿠슈인대학', NULL, 'https://www.univ.gakushuin.ac.jp/iss/en/education/subject.html', 'https://www.univ.gakushuin.ac.jp/iss/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/gakushuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/gakushuin_university/1.png'), - ('JP', 'ASIA', 'Kindai University', 'kindai_university', '긴다이대학', NULL, NULL, 'https://www.kindai.ac.jp/english/files/study-at-kindai/prospective.pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kindai_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/kindai_university/1.png'), - ('JP', 'ASIA', 'Niigata University', 'niigata_university', '니가타대학', 'https://www.niigata-u.ac.jp/en/study/life/housing/', 'https://www.niigata-u.ac.jp/en/study/exchange/', 'https://www.niigata-u.ac.jp/en/study/exchange/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/niigata_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/niigata_university/1.png'), - ('JP', 'ASIA', 'Toyo University', 'toyo_university', '도요대학', 'https://www.toyo.ac.jp/contents/international-exchange/residence/index.php', NULL, 'https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toyo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toyo_university/1.png'), - ('JP', 'ASIA', 'Toyo University', 'toyo_university', '도요대학', 'https://www.toyo.ac.jp/contents/international-exchange/residence/index.php', NULL, 'https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toyo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/toyo_university/1.png'), - ('JP', 'ASIA', 'Dokkyo University', 'dokkyo_university', '돗쿄대학', 'https://www.dokkyo.ac.jp/english/exchange/student/accommodation.html', 'https://www.dokkyo.ac.jp/english/exchange/calendar/syllabus.html', 'https://www.dokkyo.ac.jp/english/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/dokkyo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/dokkyo_university/1.png'), - ('JP', 'ASIA', 'Meiji University', 'meiji_university', '메이지대학', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png'), - ('JP', 'ASIA', 'BAIKA Women''s University', 'baika_womens_university', '바이카여자대학', 'https://dormy-ac.com/page/baika/', NULL, 'https://www.baika.ac.jp/english/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/1.png'), - ('JP', 'ASIA', 'Bunkyo Gakuin University', 'bunkyo_gakuin_university', '분쿄가쿠인대학', NULL, NULL, 'https://www.bgu.ac.jp/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/1.png'), - ('JP', 'ASIA', 'Yamaguchi University', 'yamaguchi_university', '야마구치대학', NULL, NULL, 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yamaguchi_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yamaguchi_university/1.png'), - ('JP', 'ASIA', 'Osaka Gakuin University', 'osaka_gakuin_university', '오사카가쿠인대학', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/housing.html', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/syllabi.html', 'https://www.ogu.ac.jp/english/int_exchange/ie_program/schedule.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/osaka_gakuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/osaka_gakuin_university/1.png'), - ('JP', 'ASIA', 'Otsuma Women''s University', 'otsuma_womens_university', '오츠마여자대학', 'https://www.otsuma.ac.jp/english/international/dormitory.html', NULL, 'https://www.otsuma.ac.jp/english/index.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otsuma_womens_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/otsuma_womens_university/1.png'), - ('JP', 'ASIA', 'Waseda University', 'waseda_university', '와세다대학', 'https://www.waseda.jp/inst/rlc/en/student_dormitory/exchange/ ', NULL, 'https://www.waseda.jp/inst/cie/en/exchange/application ', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/waseda_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/waseda_university/1.png'), - ('JP', 'ASIA', 'Chuo University', 'chuo_university', '추오대학', 'https://www.chuo-u.ac.jp/english/admissions/residences/', NULL, 'https://www.chuo-u.ac.jp/english/admissions/exchange/semester-or-full-year/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chuo_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chuo_university/1.png'), - ('JP', 'ASIA', 'Chiba University', 'chiba_university', '치바대학', 'https://www.chiba-u.ac.jp/international/isd/en/index.html', NULL, 'https://www.chiba-u.ac.jp/e/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chiba_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/chiba_university/1.png'), - ('TR', 'ASIA', 'ANKARA UNIVERSITY', 'ankara_university', '앙카라대학', NULL, 'http://iso.ankara.edu.tr/en/mainpage/', 'https://en.ankara.edu.tr/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ankara_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ankara_university/1.png'), - ('TR', 'ASIA', 'Ozyegin University', 'ozyegin_university', '외즈예인대학교', 'https://www.ozyegin.edu.tr/en/dormitories/housing-fees', 'https://www.ozyegin.edu.tr/en/ects-course-catalog-courses-offered/courses-offered', 'https://www.ozyegin.edu.tr/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ozyegin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ozyegin_university/1.png'), - ('HK', 'ASIA', 'Shue Yan University', 'shue_yan_university', '수인대학교', NULL, 'https://iu.hksyu.edu/wp-content/uploads/2022/08/CourseListForExchangeStudents2022-23-20220816.pdf', 'https://iu.hksyu.edu/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/shue_yan_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/shue_yan_university/1.png'), - ('HK', 'ASIA', 'City Univ Hong Kong', 'city_univ_hong_kong', '홍콩시립대학', 'https://www.cityu.edu.hk/sro/StudentHousing/UGHalls/InboundExchangeStudents.htm', 'https://www.cityu.edu.hk/admo/exchange/exchange_course_list_202402.pdf', 'https://www.admo.cityu.edu.hk/exchange_visiting/exchange/info/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/city_univ_hong_kong/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/city_univ_hong_kong/1.png'), - ('TW', 'CHINA', 'National Chengchi University', 'national_chengchi_university', '국립정치대학교', NULL, 'https://qrysub.nccu.edu.tw/', 'https://oic.nccu.edu.tw/Post/833', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_chengchi_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_chengchi_university/1.png'), - ('TW', 'CHINA', 'National Sun Yat-sen University', 'national_sun_yat-sen_university', '국립중산대학', 'https://oia.nsysu.edu.tw/p/412-1308-20581.php?Lang=en', 'https://oia.nsysu.edu.tw/p/412-1308-20770.php?Lang=en', 'https://oia.nsysu.edu.tw/p/412-1308-20581.php?Lang=en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_sun_yat-sen_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_sun_yat-sen_university/1.png'), - ('TW', 'CHINA', 'National Central University', 'national_central_university', '국립중앙대학교', NULL, 'https://cis.ncu.edu.tw/Course/main/news/announce', 'http://oia.ncu.edu.tw/index.php/en/international-students/incoming-exchange-programs.html', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_central_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/national_central_university/1.png'), - ('TW', 'CHINA', 'University of Taipei', 'university_of_taipei', '타이베이시립대학', NULL, NULL, 'https://international.utaipei.edu.tw/index.php?Lang=en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_taipei/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_taipei/1.png'), - ('CN', 'CHINA', 'Jiangnan University', 'jiangnan_university', '강남대학교', NULL, NULL, 'http://www.jiangnan.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/jiangnan_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/jiangnan_university/1.png'), - ('CN', 'CHINA', 'Jilin University', 'jilin_university', '길림대학교', NULL, NULL, 'http://cie.jlu.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/jilin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/jilin_university/1.png'), - ('CN', 'CHINA', 'Nanjing Normal University', 'nanjing_normal_university', '남경사범대학교', NULL, NULL, 'http://gjc.njnu.edu.cn/index.htm', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/nanjing_normal_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/nanjing_normal_university/1.png'), - ('CN', 'CHINA', 'Donghua University', 'donghua_university', '동화대학교', 'https://korean.dhu.edu.cn/accommodation/list.htm', 'https://english.dhu.edu.cn/incoming/list.htm', 'http://english.dhu.edu.cn ', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/donghua_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/donghua_university/1.png'), - ('CN', 'CHINA', 'Shandong University, Weihai', 'shandong_university_weihai', '산동대학교(위해)', NULL, NULL, 'https://en.wh.sdu.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/shandong_university_weihai/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/shandong_university_weihai/1.png'), - ('CN', 'CHINA', 'Yantai University', 'yantai_university', '연태대학교', NULL, NULL, 'https://en.ytu.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yantai_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/yantai_university/1.png'), - ('CN', 'CHINA', 'Zhejiang Normal University', 'zhejiang_normal_university', '절강사범대학교', NULL, 'http://iso.zjnu.edu.cn/wistwofwwrogramsw2018/list.htm', 'http://iso.zjnu.edu.cn/ywb/main.htm', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zhejiang_normal_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/zhejiang_normal_university/1.png'), - ('CN', 'CHINA', 'Minzu University of China', 'minzu_university_of_china', '중앙민족대학교', NULL, NULL, 'https://oir.muc.edu.cn/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/minzu_university_of_china/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/minzu_university_of_china/1.png'), - (NULL, NULL, 'SAF Program', 'saf_program', 'SAF 프로그램', NULL, NULL, 'http://korea.studyabroadfoundation.org/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saf_program/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/saf_program/1.png'); +INSERT INTO university(id, country_code, region_code, english_name, format_name, korean_name, + accommodation_url, english_course_url, homepage_url, + details_for_local, logo_image_url, background_image_url) +VALUES (1, 'US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학(A형)', + 'https://www.uog.edu/life-at-uog/residence-halls/', 'https://www.uog.edu/admissions/course-schedule', + 'https://www.uog.edu/admissions/international-students', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png'), + (2, 'US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학(B형)', + 'https://www.uog.edu/life-at-uog/residence-halls/', 'https://www.uog.edu/admissions/course-schedule', + 'https://www.uog.edu/admissions/international-students', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png'), + (3, 'US', 'AMERICAS', 'University of Nevada, Las Vegas', 'university_of_nevada_las_vegas', '네바다주립대학 라스베이거스(B형)', + 'https://www.unlv.edu/housing', 'https://www.unlv.edu/engineering/academic-programs', + 'https://www.unlv.edu/engineering/eip', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png'), + (4, 'CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', + 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스(A형)', 'https://www.mun.ca/residences/', + 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', + 'https://mun.ca/goabroad/visiting-students-inbound/', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png'), + (5, 'CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', + 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스(B형)', 'www.mun.ca/residences', + 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', + 'https://mun.ca/goabroad/visiting-students-inbound/', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png'), + (6, 'AU', 'AMERICAS', 'University of Southern Queensland', 'university_of_southern_queensland', '서던퀸스랜드대학(B형)', + 'https://www.unisq.edu.au/current-students/support/accommodation', + 'https://www.unisq.edu.au/course/specification/current/', + 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/1.png'), + (7, 'AU', 'AMERICAS', 'University of Sydney', 'university_of_sydney', '시드니대학', + 'https://www.sydney.edu.au/study/accommodation.html', 'www.sydney.edu.au/sydney-abroad-units', + 'https://www.sydney.edu.au/', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_sydney/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_sydney/1.png'), + (8, 'AU', 'AMERICAS', 'Curtin University', 'curtin_university', '커틴대학(A형)', + 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', 'https://handbook.curtin.edu.au/', + 'https://www.curtin.edu.au/', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/1.png'), + (9, 'DK', 'EUROPE', 'University of Southern Denmark', 'university_of_southern_denmark', '서던덴마크대학교', + 'https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing', + 'https://www.sdu.dk/en/uddannelse/exchange_programmes', 'https://www.sdu.dk/en', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/1.png'), + (10, 'DK', 'EUROPE', 'IT University of Copenhagen', 'it_university_of_copenhagen', '코펜하겐 IT대학', + 'https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students', + 'https://en.itu.dk/Programmes/Exchange-students/Become-an-exchange-student-at-ITU', + 'https://en.itu.dk/programmes/exchange-students/become-an-exchange-student-at-itu', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/1.png'), + (11, 'DE', 'EUROPE', 'Neu-Ulm University of Applied Sciences', 'neu-ulm_university_of_applied_sciences', + '노이울름 대학', + 'https://www.hnu.de/fileadmin/user_upload/5_Internationales/International_Incomings/Bewerbung/Housing_Broschure.pdf', + 'https://www.hnu.de/en/international/international-exchange-students/courses-taught-in-english', + 'https://www.hnu.de/en/international', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neu-ulm_university_of_applied_sciences/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neu-ulm_university_of_applied_sciences/1.png'), + (12, 'GB', 'EUROPE', 'University of Hull', 'university_of_hull', '헐대학', + 'https://www.hull.ac.uk/Choose-Hull/Student-life/Accommodation/accommodation.aspx', + 'https://universityofhull.app.box.com/s/mpvulz3yz0uijdt68rybce19nek0d8eh', + 'https://www.hull.ac.uk/choose-hull/study-at-hull/need-to-know/key-dates', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hull/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hull/1.png'), + (13, 'AT', 'EUROPE', 'University of Graz', 'university_of_graz', '그라츠 대학', + 'https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/', + 'https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf', + 'https://www.uni-graz.at/en/', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/1.png'), + (14, 'AT', 'EUROPE', 'Graz University of Technology', 'graz_university_of_technology', '그라츠공과대학', + 'https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033', + 'https://tugraz.at/go/search-courses', 'https://www.tugraz.at/en/home', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/1.png'), + (15, 'AT', 'EUROPE', 'Catholic Private University Linz', 'catholic_private_university_linz', '린츠 카톨릭 대학교', NULL, + 'https://ku-linz.at/en/ku_international/incomings/kulis', 'https://ku-linz.at/en', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/1.png'), + (16, 'AT', 'EUROPE', 'University of Applied Sciences Technikum Wien', + 'university_of_applied_sciences_technikum_wien', '빈 공과대학교', NULL, + 'https://www.technikum-wien.at/en/international/student-mobility/', + 'https://www.technikum-wien.at/international/studierendenmobilitaet-2/', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_technikum_wien/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_technikum_wien/1.png'), + (17, 'FR', 'EUROPE', 'IPSA', 'ipsa', 'IPSA', 'https://www.ipsa.fr/en/student-life/pratical-information/', NULL, + 'https://www.ipsa.fr/en/engineering-school/aeronautical-space', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ipsa/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ipsa/1.png'), + (18, 'JP', 'ASIA', 'Meiji University', 'meiji_university', '메이지대학', + 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, + 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png'), + (19, 'JP', 'ASIA', 'BAIKA Women''s University', 'baika_womens_university', '바이카여자대학', + 'https://dormy-ac.com/page/baika/', NULL, 'https://www.baika.ac.jp/english/', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/1.png'), + (20, 'JP', 'ASIA', 'Bunkyo Gakuin University', 'bunkyo_gakuin_university', '분쿄가쿠인대학', NULL, NULL, + 'https://www.bgu.ac.jp/', NULL, + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/logo.png', + 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/1.png'); -INSERT INTO `solid_connection`.`university` -(`country_code`, `id`, `region_code`, `english_name`, `format_name`, `korean_name`, `accommodation_url`, `background_image_url`, `english_course_url`, `homepage_url`, `logo_image_url`, `details_for_local`) -VALUES - ('US', 150, 'AMERICAS', 'Temple University', 'temple_university', '템플대학(A형)', 'https://globalprograms.temple.edu/housing', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/temple_university/1.webp', 'https://prd-xereg.temple.edu/StudentRegistrationSsb/ssb/term/termSelection?mode=search', 'http://globalprograms.temple.edu/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/temple_university/logo.webp', NULL), - ('US', 151, 'AMERICAS', 'University of Hawaii at Manoa', 'university_of_hawaii_at_manoa', '하와이대학(A형)', 'https://manoa.hawaii.edu/mix/inbound/housing-meals/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_hawaii_at_manoa/1.webp', 'https://www.sis.hawaii.edu/uhdad/avail.classes?i=MAN', 'https://www.hawaii.edu/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_hawaii_at_manoa/logo.webp', NULL), - ('US', 152, 'AMERICAS', 'University of North Carolina at Wilmington', 'university_of_north_carolina_at_wilmington', '노스캐롤라이나 윌밍턴대학(A형)', 'https://uncw.edu/seahawk-life/dining-housing/housing/?utm_source=housing&utm_medium=301&utm_id=REDIR1', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_north_carolina_at_wilmington/1.webp', 'https://catalogue.uncw.edu/content.php?catoid=70&navoid=9352', 'https://uncw.edu/seahawk-life/support-success/international/students-scholars/prospective-students/visiting-international/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_north_carolina_at_wilmington/logo.webp', NULL), - ('US', 153, 'AMERICAS', 'University of North Carolina at Wilmington', 'university_of_north_carolina_at_wilmington', '노스캐롤라이나 윌밍턴대학(B형)', 'https://uncw.edu/seahawk-life/dining-housing/housing/?utm_source=housing&utm_medium=301&utm_id=REDIR1', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_north_carolina_at_wilmington/1.webp', 'https://catalogue.uncw.edu/content.php?catoid=70&navoid=9352', 'https://uncw.edu/seahawk-life/support-success/international/students-scholars/prospective-students/visiting-international/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_north_carolina_at_wilmington/logo.webp', NULL), - ('DE', 154, 'EUROPE', 'FH Aachen University of Applied Sciences', 'fh_aachen_university_of_applied_sciences', '아헨응용과학대학', 'https://www.fh-aachen.de/fachbereiche/wirtschaft/internationales/incoming-students/student-support-service/finding-accomodation', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/fh_aachen_university_of_applied_sciences/1.webp', 'https://www.campus.fh-aachen.de/campus/all/groups.asp?lang=en&tguid=0xC49848E2A4294A2F8A1A833C4C2AB16B', 'https://www.fh-aachen.de/en/studies/applying/international-applicants/exchange-students', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/fh_aachen_university_of_applied_sciences/logo.webp', NULL), - ('AT', 155, 'EUROPE', 'Vorarlberg University of Applied Sciences', 'vorarlberg_university_of_applied_sciences', '포어아를베르크 응용과학대학교', 'https://www.fhv.at/en/fh/study-international/exchange-students-incomings/living-in-vorarlberg/accommodation-for-guest-students', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/vorarlberg_university_of_applied_sciences/1.webp', 'https://www.fhv.at/en/fh/study-international/exchange-students-incomings/study-related-information-for-guest-students/english-course-offer', 'https://www.fhv.at/en/fh/study-international/exchange-students-incomings', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/vorarlberg_university_of_applied_sciences/logo.webp', NULL), - ('HU', 156, 'EUROPE', 'Eotvos Lorand University', 'eotvos_lorand_university', '에어토보스로랑드대학', 'https://www.elte.hu/en/arrange-housing', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/eotvos_lorand_university/1.webp', 'https://www.elte.hu/en/incoming-mobility/courses', 'https://www.elte.hu/en/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/eotvos_lorand_university/logo.webp', NULL), - ('PT', 157, 'EUROPE', 'Lisbon School of Economics & Management', 'lisbon_school_of_economics__management', '리스본대학 경영학과', 'https://www.iseg.ulisboa.pt/aquila/unidade/ERASMUS/incoming-mobility/useful-links/accommodation', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/lisbon_school_of_economics__management/1.webp', 'https://www.iseg.ulisboa.pt/internacional/mobilidade-incoming/informacao-academica/', 'https://www.iseg.ulisboa.pt/en/study/international/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/lisbon_school_of_economics__management/logo.webp', NULL), - ('LT', 158, 'EUROPE', 'Vilnius Gediminas Technical University', 'vilnius_gediminas_technical_university', '빌니우스 게디미나스대학 공과대', 'https://vilniustech.lt/for-international-students/for-exchange-students/studies/319312?#319322', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/vilnius_gediminas_technical_university/1.webp', 'https://vilniustech.lt/files/5058/252/12/0_0/VILNIUS%20TECH%202023-2024%20spring.htm', 'https://vilniustech.lt/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/vilnius_gediminas_technical_university/logo.webp', NULL), - ('FR', 159, 'EUROPE', 'ECE Paris', 'ece_paris', 'ECE Paris', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/ece_paris/1.webp', NULL, 'https://ecole.ece.fr/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/ece_paris/logo.webp', NULL), - ('FR', 160, 'EUROPE', 'EDC Paris Business School', 'edc_paris_business_school', 'EDC 파리 경영대학', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/edc_paris_business_school/1.webp', NULL, 'https://www.edcparis.edu/fr/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/edc_paris_business_school/logo.webp', NULL), - ('ID', 161, 'ASIA', 'University of Indonesia', 'university_of_indonesia', '인도네시아 국립대학', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_indonesia/1.webp', NULL, 'https://www.ui.ac.id/en/universitas-indonesia/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/university_of_indonesia/logo.webp', NULL), - ('TH', 162, 'ASIA', 'Rangsit University', 'rangsit_university', '랑싯대학', 'https://www.rsuip.org/admissions/international-student-support/accommodation/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/rangsit_university/1.webp', 'https://www.rsuip.org/for-student/timetable/', 'https://www.rsuip.org/','https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/rangsit_university/logo.webp', NULL), - ('TH', 163, 'ASIA', 'Mae Fah Luang University', 'mae_fah_luang_university', '매파루앙 대학', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/mae_fah_luang_university/1.webp', NULL, 'https://waruneekae.wixsite.com/website/inbound-students', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/mae_fah_luang_university/logo.webp', NULL), - ('UZ', 164, 'ASIA', 'Fergana Polytechnic Institute', 'fergana_polytechnic_institute', '페르가나폴리텍대학', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/fergana_polytechnic_institute/1.webp', NULL, 'https://www.ferpi.uz/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/fergana_polytechnic_institute/logo.webp', NULL), - ('TR', 165, 'ASIA', 'Beykent University', 'beykent_university', '베이켄트대학교', 'https://www.beykent.edu.tr/en/student/life-at-beykent/accommodation/ayaza%C4%9Fa-dormitories', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beykent_university/1.webp', 'https://obs.beykent.edu.tr/oibs/bologna/index.aspx?lang=en', 'https://www.beykent.edu.tr/en/mainpage', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beykent_university/logo.webp', NULL), - ('TW', 166, 'ASIA', 'National DongHwa University', 'national_donghwa_university', '국립동화대학교', 'https://rb005.ndhu.edu.tw/p/412-1005-19158.php', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_donghwa_university/1.webp', 'https://sys.ndhu.edu.tw/aa/class/course/Default.aspx', 'https://www.ndhu.edu.tw/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_donghwa_university/logo.webp', NULL), - ('TW', 167, 'ASIA', 'National Taiwan Normal University', 'national_taiwan_normal_university', '국립대만사범대학교', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_taiwan_normal_university/1.webp', 'https://courseap2.itc.ntnu.edu.tw/acadmOpenCourse/index.jsp', 'https://en.ntnu.edu.tw/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_taiwan_normal_university/logo.webp', NULL), - ('TW', 168, 'ASIA', 'National Taiwan Univ. of Science and Technology', 'national_taiwan_univ_of_science_and_technology', '국립대만과학기술대학교', 'https://oia-r.ntust.edu.tw/p/412-1060-8919.php?Lang=en#:~:text=Europe%20(excluding%C2%A0Germany)-,Accomodation,-Due%20to%20heavy', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_taiwan_univ_of_science_and_technology/1.webp', 'https://querycourse.ntust.edu.tw/querycourse/#/', 'https://www.ntust.edu.tw/?Lang=en', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_taiwan_univ_of_science_and_technology/logo.webp', NULL), - ('TW', 169, 'ASIA', 'National Kaohsiung University of Science and Technology', 'national_kaohsiung_university_of_science_and_technology', '국립가오슝과기대학교', 'https://oia.nkust.edu.tw/en/unit-8-269-14.html', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_kaohsiung_university_of_science_and_technology/1.webp', 'https://oia.nkust.edu.tw/en/unit-8-271-5.html', 'https://eng.nkust.edu.tw/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/national_kaohsiung_university_of_science_and_technology/logo.webp', NULL), - ('CN', 170, 'ASIA', 'Dalian University of Technology', 'dalian_university_of_technology', '대련이공대학교', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/dalian_university_of_technology/1.webp', 'http://sie.dlut.edu.cn/zsxx/fxlxm/ ptgjjxs/xjjhs_xfs_xm.htm', 'https://en.dlut.edu.cn/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/dalian_university_of_technology/logo.webp', NULL), - ('CN', 171, 'ASIA', 'Beijing Foreign Studies University, IBS', 'beijing_foreign_studies_university_ibs', '북경외국어대학교 IBS', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beijing_foreign_studies_university_ibs/1.webp', 'https://solbridge.bfsu.edu.cn/', 'https://ibs.bfsu.edu.cn/en/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beijing_foreign_studies_university_ibs/logo.webp', NULL), - ('CN', 172, 'ASIA', 'Beijing Language and Culture University', 'beijing_language_and_culture_university', '북경어언대학교', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beijing_language_and_culture_university/1.webp', 'https://enexchange.blcu.edu.cn/col/col3931/index.html', 'http://english.blcu.edu.cn/', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/resize/beijing_language_and_culture_university/logo.webp', NULL); - -INSERT INTO university_info_for_apply -(university_id, semester_requirement, student_capacity, semester_available_for_dispatch, tuition_fee_type, details_for_major, details_for_apply, details_for_language, details_for_english_course, details_for_accommodation, details) -VALUES - (1, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), - (2, 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend'), - (3, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000
- 한 학기 등록금: 약 $7,500
- International Program and Service Fees $2,500'), - (4, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 6.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000
- International Program and Service Fees $2,500'), - (5, 2, 5, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함', NULL, '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', NULL, NULL, '※ On Campus 기숙사 신청 필수! (기숙사 미신청 시 해외대학등록금납부형(B형)으로 전환)
- 기숙사 관련 정보 : https://www.unk.edu/offices/reslife/housing-options.php
- 보험료 약 $2,273/학기 (가격변동가능), 보험 가입 필수!
https://www.unk.edu/international/international-student-services/medical-insurance.php'), - (6, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함', 'ELI(어학연수) 과정으로 지원시 영어 성적 무관 (https://www.unk.edu/international/english-language-institute/index.php)', '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', NULL, NULL, '- ELI 어학연수 과정으로 지원시, 전공/ ESL 크레딧은 자체배치고사 점수에 따라 상이
- 기숙사 관련 정보 : https://www.unk.edu/offices/reslife/housing-options.php
- 등록금 관련 정보 : https://www.unk.edu/costs.php
- 보험료 약 $2,273/학기 (가격변동가능), 보험 가입 필수!
https://www.unk.edu/international/international-student-services/medical-insurance.php'), - (7, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
지원불가능전공 : Nursing, Athletic training, Education', NULL, '-영어 점수는 다음의 세부영역 점수를 각각
만족해야 함
- TOEFL iBT : 모든 영역에서 15점 이상
- IELTS : 모든 영역에서 5.5 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '한 학기 기숙사 비용: 약 $5,442 '), - (8, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
지원불가능전공 : Nursing, Athletic training, Education', NULL, '-영어 점수는 다음의 세부영역 점수를 각각
만족해야 함
- TOEFL iBT : 모든 영역에서 15점 이상
- IELTS : 모든 영역에서 5.5 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '한 학기 등록금: 약 $6,938 (in-state 적용, 2023-24기준)
한 학기 기숙사 비용: 약 $5,442'), - (9, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 쓰기 영역에서 5.0 이상', NULL, NULL, '교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음
- College Fee : 약 $1,070'), - (10, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 쓰기 영역에서 5.0 이상', NULL, NULL, '교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음
- 한 학기 등록금: 약 $9,450 (out of state)
- College Fee : 약 $1,070'), - (11, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원 불가 전공 : Writing (WRT), Health Sciences, Nursing, Pharmacology, Education
- Languages 강좌(독일어, 프랑스어 등), Dance, Theatre Arts and Cinema & Cultural Studies 수강 제한
- Business/Accounting 전공 학생은 최대 3학점까지만 전공 수업 수강 가능하며 나머지 학점은 다른 전공에서 수강 가능', NULL, NULL, NULL, NULL, '- 한 학기 기숙사 비용: 약 $4,500~6,200
- 한 학기 등록금: 약 $12,495 (out of state 적용)
- 등록금 및 기타 Fee Rates 관련 정보: https://www.stonybrook.edu/commcms/sfs/tuition/index.php'), - (12, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원 및 수강하는 전공과 본교 전공이 일치해야함
- 지원 불가 전공 : Nursing', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), - (13, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원 및 수강하는 전공과 본교 전공이 일치해야함
- 지원 불가 전공 : Nursing', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '한 학기 등록금: 약 $6,892 (In-state 적용)'), - (14, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', ' - 타전공 지원 및 수강 가능
지원 불가 전공: Nursing, Counseling, Music, Teaching, Occupational and Speech therapy', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), - (15, 2, 1, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', ' - 타전공 지원 및 수강 가능
지원 불가 전공: Nursing, Counseling, Music, Teaching, Occupational and Speech therapy', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '한 학기 등록금: 약 $6,000 (in state+10%) '), - (16, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원불가능전공:
Athletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, and Nursing', NULL, NULL, NULL, NULL, '- 모든 국제학생들은 안전과 영어향상을 위해 무조건 기숙사를 사용을 강제하는 International Studies Policy를 공지합니다'), - (17, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원불가능전공:
Athletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, and Nursing', NULL, NULL, NULL, NULL, '- 모든 국제학생들은 안전과 영어향상을 위해 무조건 기숙사를 사용을 강제하는 International Studies Policy를 공지합니다
- 등록금은 in-state rate 적용됨
- 1년 과정으로 봄학기 수학 후 가을학기 수학을 하는 경우, 여름학기 수강 가능 (Optional)'), - (18, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 18점 이상
- IELTS : 모든 영역에서 6.0 이상
- Duolingo : 모든 영역에서 95점 이상 (Duolingo의 경우, 토플과 아이엘츠 성적을 보유하지 않은 경우 예외적으로 적용되므로 합격 이후 필요시 파견교에서 영어능력에 대해 재확인할 수 있음/ https://www.uah.edu/admissions/undergraduate/apply-for-admission/international)
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, NULL), - (19, 2, 1, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 18점 이상
- IELTS : 모든 영역에서 6.0 이상
- Duolingo : 모든 영역에서 95점 이상 (Duolingo의 경우, 토플과 아이엘츠 성적을 보유하지 않은 경우 예외적으로 적용되므로 합격 이후 필요시 파견교에서 영어능력에 대해 재확인할 수 있음/ https://www.uah.edu/admissions/undergraduate/apply-for-admission/international)
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '등록금 관련 정보 : https://www.uah.edu/bursar/tuition'), - (20, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 선이수과목이 비숫해야 후에 IIT에서 전공과목을 수강할 수 있음
- 타전공 지원 및 수강 가능 (단, 각 학과의 사전허가 필요)', 'SAT시험 면제 조건으로 교양 및 전공 포함하여 최소 30학점 이수하여야 하며 입학사정시 전공과목 및 영어과목 위주로 검토 됨. ', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, '- 식비(Meal Plan) 정보
https://www.iit.edu/housing/dining-and-meal-plan/options-and-rates
- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid
- 학비관련 링크 : https://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate
- 한 학기 등록금: 약 $14.820 (방문학생 학비장학금 $10,000/학기 차감한 금액 기준, 징학금은 12크레딧 이상 full time 등록 시에만 지급 가능)
- 보험료 링크 : https://www.iit.edu/shwc/insurance/plan-info-and-requirements
- 세부사항 변동 가능'), - (21, 4, 5, 'FOUR_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 정확하게 일치해야함', 'SAT시험 면제 조건으로 교양 및 전공 포함하여 최소 30학점 이수하여야 하며 입학사정시 전공과목 및 영어과목 위주로 검토 됨.
- 2023-2학기에 4차 학기 이수 예정인 학생도 조건부 지원 가능
(반드시 정규학기 이수하여야 2024-1 파견 가능)', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, '- 식비(Meal Plan) 정보
https://www.iit.edu/housing/dining-and-meal-plan/options-and-rates
- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid
- 학비관련 링크 : https://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate
- 한 학기 등록금: 약 $14.820 (방문학생 학비장학금 $10,000/학기 차감한 금액 기준, 징학금은 12크레딧 이상 full time 등록 시에만 지급 가능)
- 보험료 링크 : https://www.iit.edu/shwc/insurance/plan-info-and-requirements
- 세부사항 변동 가능'), - (22, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, NULL, '※ 테일러대학은 기독교 정신을 기반으로 설립된 학교이므로 대다수의 학생들이 기독교를 믿고 있음. 기독교에 거부감이 없고 성실한 학교생활을 하며, 정기적인 교회 생활을 하고있는 학생들만 지원 권장
- 원칙은 1학기 지원이나, 2학기도 학생이 원하면 지원 가능
다만, 파견대학에서 학생의 교환학생 성과를 평가해 이에 미치지 못할 경우 2학기를 이어 진행하지 못하고 한 학기만 진행할 수 있으니 해당 사항 유의하기 바람. 이에 2학기를 지원하는 학생의 경우, 파견 지원 전 반드시 지역담당자에게 사전에 연락해 관련 내용에 대해 논의하고 지원하길 바람. '), - (23, 2, 5, 'IRRELEVANT', 'MIXED_PAYMENT', '- 타전공 지원 및 수강 가능
- 다음 전공들은 지원 가능하나 수강신청이 제한적일 수 있음 Architecture, Computer & Information Sciences, Business, Education, Performing Arts (Dance, Music, Theater), Professional Schools (Dentistry, Law, Medicine, Pharmacy, Podiatry), Visual Arts (Film/Media Arts, Graphic Design, Fine Arts, etc), Sport, Tourism and Hospitality Management
- Business 전공의 경우, 본교 경영학과 학생만 지원가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 6 이상
- 영어시험 성적 파견대학 지원시까지 유효하여야 함
- 최저 기준 어학 점수가 넘더라도, 선발대학에서 특정 섹션의 어학실력이 부족하다고 판단될 경우 파견 시 별도로 대학부설 어학코스(ielp) 수강이 필요할 수 있음 [수업료 외 별도,비용발생]', NULL, NULL, '※ 1개 학기로도 지원 가능
※ 혼합형은 첫 번째 학기는 템플대학교에 등록금 지불, 두 번째 학기는 인하대에 등록금 지불하는 유형 (2개 학기를 모두 마치고 올 경우에만 두 번쨰 학기에 템플대학교 등록금 면제 및 인하대에 등록금 지불 적용 가능)
- 한 학기 등록금: 약 $15,432 (out of rate, 관련정보: https://globalprograms.temple.edu/programs/inbound-study-abroad-exchange/costs-dates)
- 한 학기 기숙사: 약 $5,000 (기숙사 유형에 따라 상이)'), - (24, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원제힌전공: Nursing', NULL, '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', NULL, NULL, NULL), - (25, 2, 4, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원제힌전공: Nursing', NULL, '외국어 성적 유효기간이 파견대학의 지원하는 시점까지 유효해야함', NULL, NULL, '한 학기 등록금: 약 $9,312 (50% tuition scholarship 지급)'), - (26, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, ' - 토플 IBT(100), 토플 ITP(600), IELTS(7.0) DUOLINGO(125) 미만시 파견후 별도 영어시험 필수이며 결과에 따라 1-3개 어학 수업 수강하게 될 수 있음
- 어학성적은 파견대학 지원시까지 유효하여야 함', NULL, NULL, '등록금: Hoakipa Visiting Student 유형으로 in-state 150% 적용'), - (27, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, NULL, '※ 영어강의 제공하지 않음, 모든 강의 포르투갈어로 진행', '교내 기숙사 미제공, International Affairs와 버디프로그램을 통해 교외숙소 계약을 도와줄 예정', NULL), - (28, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원불가전공 : Medicine', NULL, NULL, '※ 대부분의 강의 포르투갈어로 진행, 주로 Business Field에 영어수업', NULL, '포르투갈어 어학 수업 수강 가능'), - (29, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 아래 8개 Faculties 내에서만 수강 가능 :
Arts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science
- 지원 불가 전공: Nursing, Social work', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상 ', NULL, NULL, NULL), - (30, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 아래 8개 Faculties 내에서만 수강 가능 :
Arts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science
- 지원 불가 전공: Nursing, Social work', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상 ', NULL, NULL, '국제학생 등록금 적용(지원 전공 및 학점에 따라 금액 상이)
- 관련 링크: https://www.uregina.ca/fs/students/fee-schedule.html '), - (31, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', NULL, NULL, NULL), - (32, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', NULL, NULL, '국제학생 등록금 적용 (학점당 $2,080)'), - (33, 2, 4, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '※ 가장 기초 과정(IEP-G)의 최소 지원 요건이며 레벨과 지원과정에 따라 지원자격 상이하므로 fact sheet 참조 바람
- 학부과정 수강 가능한 IEBP-G 지원 시 다음의 세부영역 점수를 만족해야함
- IELTS : 쓰기 5.5 이상, 모든 영역에서 5.0 이상
- TOEFL : 쓰기 16점 이상
- 외국어 성적 유효기간이 파견학기 시작시까지 유효해야함', NULL, NULL, '선발 학생의 어학성적에 따라 레벨이 정해지며 비용 또한 상이.
- IEBP-G 레벨에 배정될 경우 학부 수업 1-2개 수강 가능하며 (선택 제한적) 학부수업에 대한 등록금은 면제 (국제처 홈페이지 내 대학 Fact Sheet 참조 바람)'), - (34, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원 유의 전공 : Fashion, Media and Communication, Design, Interior Design, Architecture Design
(참고 : https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange/how-to-search-for-your-courses)', NULL, '어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, NULL), - (35, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 각 영역 최소 5.5 이상
- 외국어 성적 유효기간이 파견대학의 지원 시점 기준까지 유효해야함 ', NULL, NULL, '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람'), - (36, 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 각 영역 최소 5.5 이상
- 외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함 ', NULL, NULL, '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람
- In-state 등록금 납부
(등록금 관련 정보 : https://www.unisq.edu.au/international/partnerships/study-abroad-exchange/fees-scholarships)'), - (37, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- MECO, CAEL, LAWS unit 수강 여석 제한 있음', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기/듣기/말하기 17점, 쓰기 19점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, 'OSHC(Overseas Student Health Cover) 국제학생 보험가입 의무 (2023년 기준 AUD 348/학기, 학기마다 비용 상이)'), - (38, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, '※ 24-1학기에 한하여 ''Destination Australia Cheung Kong Exchange Program Scholarship'' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), - (39, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 19점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, '한 학기 등록금: 약 10,400 AUD (in state 적용)
※ 24-1학기에 한하여 ''Destination Australia Cheung Kong Exchange Program Scholarship'' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), - (40, 2, 4, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'Faculty of Economics and Business로만 지원가능', NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, NULL, NULL), - (41, 3, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, '기숙사 여석 많지않음', NULL), - (42, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', 'https://fontys.edu/Short-term-programmes/Exchange-programmes/Exchange-programmes-per-faculty.htm', NULL, NULL, NULL, NULL, NULL), - (43, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 경영학 수업만 개설됨
- 경영학에 대한 사전의 기초적인 과목을 이수하여야 함
- 한 학기 최대 30ECTS까지 수강 가능함', NULL, NULL, NULL, '- 외부숙소 제공
', NULL), - (44, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함
- 교환학생에게 제공되는 수업만 수강 가능
- Faculty of Engineering 내에서 2/3이상의 수업을 수강하여야 함
- 30 ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 1일)', NULL, NULL, '- 교외 숙소', NULL), - (45, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 본교 기초과목 이수사항에 따라 지원이 제한될 수 있으나 소속전공과 정확하게 일치 하지 않아도 지원은 가능(연관 전공이어야 함)
- 최소 7.5 ECTS, 최대 30ECTS 수강 가능
- 교차 수강 가능(선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', NULL, NULL, '- 제공(학교 운영 기숙사 아님)
- 선착순 배정', NULL), - (46, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점
- TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점
외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, NULL), - (47, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 일반적으로 Business, Computer Sciences, Engineering, Tourism Field에서 영어강의를 교환학생에게 제공함 ', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, '교환 학생 프로그램에 독일 어학 수업 포함'), - (48, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- Language 관련 강의 수강 제한적, Social Faculty의 경우 영어강의 제공하지 않음', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- TOEIC: 말하기 160점 이상, 쓰기 150점 이상
- IELTS: 모든 영역에서 5.5 이상
- 어학성적은 파견 학기 지원 마감일까지 유효 하여야함', NULL, NULL, '※ BW Scholarship RWU : Fall term 2023/24 (Sept – Feb) 기간동안 한 달에 €850 장학금 수령 (제출서류 및 기한 등 자세한 정보는 국제처 홈페이지 해외대학정보 공지글 참고)'), - (49, 2, 9, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요', '각 단과대학별 상이하므로 국제처 홈페이지 해외대학정보 Fact sheet 및 홈페이지 참조 바람', NULL, NULL), - (50, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, NULL, '보험 관련 정보: https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/health-insurance'), - (51, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- International Study Programme(ISP) 내 수업만 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함', NULL, '기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음', NULL), - (52, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원 불가능 전공: Pharmacy, Human Medicine and Veterinary Medicine
- Biochemistry, Bioinformatics and Biology, Law 지원 제한적 (학과 사전승인 필요)
- Business Administration, Economics 학과 수업 대부분이 독일어로 진행, 독일어 가능자 지원 권장', NULL, '- Department of Humanities, Social Science, Business Administration and Economics 수강요건: 독일어 공인성적 B2 레벨 이상의 증빙 필수, Department of Natural Science 수강요건: 독일어 공인성적 B1 레벨 이상 증빙 필수
- John F Kennedy Institute for North American Studies 수강요건: 영어 공인성적 C1 레벨 이상의 증빙 필수', '※ 주로 Departments of English and North American Studies에서 영어강의 제공, 이 외의 학과 영어수업 제한적', '- 기숙사 신청 제한적, 여석 부족으로 기숙사 배정을 못 받을 가능성 있음', '- 보험 관련 정보: http://www.fu-berlin.de/en/studium/international/studium_fu/einreise_aufenthalt/krankenversicherung'), - (53, 4, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '※ Faculty of Economics, Business Administration에 한하여 지원 가능
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '보험 관련 정보: https://international.fhws.de/en/fhws-international/ways-to-fhws/applicants-and-student-support/before-your-arrival-at-fhws/ '), - (54, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, NULL, NULL, '- 기숙사 신청 제한적', '- 보험 관련정보: https://signupbarmer.de/?utm_source=barmer_schmalkalden'), - (55, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: International Project Management, Smart City Solution
- Civil Engineering, Surveying, Mathematic : 독일어 가능자 지원 권장, Architecture, Interior Architecture and General Management : 독일어 수업 수강 필수', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 모든 영역에서 6.0 이상', NULL, '기숙사 신청 제한적', NULL), - (56, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Health, Midwifery, Extra-occupational courses ', NULL, '어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, NULL), - (57, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함
- Department of Architecture and Civil Engineering의 경우 영어강의 제공하지 않음', NULL, NULL, NULL, NULL, NULL), - (58, 3, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 영어스피킹 중급 이상 학생 지원 권장
- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, '- 독일 어학 수업 수강 필수
- 독일어 어학성적으로 지원하는 경우에 한하여 2학기로도 지원 가능'), - (59, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함
- 지원 불가능 전공: Human medicine, dentistry, pharmacy, law and psychology. Practical sports', NULL, '어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, NULL), - (60, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함
- 지원 불가 전공: Medicine', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, NULL), - (61, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: Medicine, Programmes offered by the Centre for Lifelong Learning', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC S/W : 말하기 120점 이상, 쓰기 120점 이상
- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, '보헙관련 정보: https://uol.de/en/exchange-studies/health-insurance'), - (62, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 어학성적 파견 대학 지원시까지 유효하여야함', NULL, NULL, NULL), - (63, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원 제한 전공 : Psychology', NULL, NULL, NULL, NULL, NULL), - (64, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, NULL), - (65, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '※ Faculty of Business Administration, Global Finance and Banking, Economics에 한하여 지원 가능
- Campus Schwäbisch Hall, Campus Künzelsau 내 강의 수강 가능', NULL, NULL, NULL, NULL, NULL), - (66, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함
- 학기당 최소 30ECTS 수강필수
- 지원불가전공: Nursing/Dentitstry/Odontology 등
- 교차 수강 가능', NULL, NULL, NULL, '- 재공하나 보장되는 것은 아님
- 1인실 기준으로 4830~5387SEK/1달
', NULL), - (67, 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없으나 일치하는 것을 권고함. 본교에서 기초과목을 이수하여야 함
- 학기당 최소 30ECTS 수강필수
- 교차 수강 가능(단, 선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)
- 지원불가 전공 : Fashion Designer, Textile Design', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT: 쓰기 20점 이상
- IELTS: 모든 영역에서 5.5이상', NULL, '- 미제공, 외부숙소 지원
- 2500~6000 SEK/1달', '2024년 봄학기 : 15 January - 2 June 2024
- Orientation Days : 11-12 January 2024.'), - (68, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 지원 권장', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함.
TOEIC 말하기 160점 이상, 쓰기 150점 이상', NULL, NULL, NULL), - (69, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- School of Law로만지원가능(지원가능전공:international relations, law)
- 반드시 24ECTS 이상을 수강하여야 함', '지원 전 권역 담당자와 사전상담 요망', NULL, NULL, NULL, NULL), - (70, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 영어과목이 제한적임. ', NULL, '영어과목이 다양하지 않으므로 신중한 지원 요망. 스페인어 공인어학성적(DELE 중급이상 성적)이 있을 시 추후 합격 후 담당자에게 제출하시길 권장드림(필수아님)', NULL, '기숙사 여석 많지않음', NULL), - (71, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, 'TOEIC의 경우 S/W 합산 320점 이상 추가로 필수 제출', NULL, NULL, NULL), - (72, 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '제한학과 많음. (Factsheet참조및Factsheet언급된 제한학과 외에도 학기마다 제한학과 발생가능성있음). 지원 전 권역 담당자랑 사전상담 요망. 학기당 30ECTS수강해야 LA승인남. 성적처리 늦은 편이라 8차 학기 수학자는 성적처리 늦은 거 감안하고 추가 이에 따른 불편함이 있음을 인지후 지원요망. ', '지원 전 권역 담당자와 사전상담 요망', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 듣기 및 쓰기 18점, 읽기 18점, 말하기 20점, 쓰기 18점 이상
- IELTS : 모든 영역에서 6.0이상', NULL, NULL, '영국 생활비 및 숙소비용 유럽권 지역 중 상대적으로 매우 높은편. 지원전 반드시 사전고려 요망'), - (73, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '학교인근 외부 숙소는 있지만, 외부업체운영숙소라 대학관할아님', NULL), - (74, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점 이상, 쓰기 17점 이상, 말하기 20점 이상, 듣기 17점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상
''- TOEIC의 경우 S/W 점수 합산 310점 이상 ', NULL, '자체기숙사는 없음. 교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있음(선착순 경우많음). 더블룸 기준약 한달에 € 340 per month (기숙사 종류게 따라 가격 차이 유) 예산잡으면됨.', NULL), - (75, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: History, Philosophy, Art History, theology
(영어과목 수가 그리 많지는 않으므로, 사전 확인필요)
''- 학기당 최소 15ECTS 수강신청해야 함', '봄학기에는 영어과목이 극히 제한적으로 열린다고 함. 지원 전 권역 담당자와 사전상담 요망', NULL, NULL, '학교에서 몇가지 기숙사 옵션 합격시 연결예정.', NULL), - (76, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '기숙사없음', NULL), - (77, 5, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, '지원 전 권역 담당자와 사전상담요망. 주로 학부보다 석사과정에 영어교과목이 개설된 편', NULL, NULL, NULL, '양교 교류협약에 따라, Bovisa 캠퍼스로만 지원가능'), - (78, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '지원 전 권역 담당자와 사전상담 요망', '지원 전 권역 담당자와 사전상담 요망. 기존 파견자 없어서 후기 자료없음.', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 쓰기 19점 이상, 말하기 19점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상', NULL, NULL, NULL), - (79, 3, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, NULL, NULL), - (80, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 본인 전공이 일치하지 않아도 지원가능하나 수업따라가기가 어려울 수 있으므로 배경지식이 없다면 지원에 신중할것.. Faculty ofFine Arts/Music/Medicine은 교환학생 지원불가학부임.', '지원 전 권역 담당자와 사전상담 요망', NULL, NULL, NULL, NULL), - (81, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '기숙사 입사경쟁률 매우높음', NULL), - (82, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', NULL, '- TOEIC R/C 최소 385점 이상 TOIEC L/C 최소 400점 이상', NULL, '기숙사 월260유로 정도임', NULL), - (83, 3, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원 전 권역 담당자와 사전상담 요망', NULL, NULL, NULL, '- 기숙사 없음.', NULL), - (84, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, NULL, NULL), - (85, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원전공이 본교 소속 전공 또는 제2전공과 일치하여야 함 : 경영학 과목만 개설됨
- 교차 수강 가능
- 최소 9, 최대 36ECTS 수강신청 가능', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', NULL, NULL, '- 미제공', '- 2024년 봄학기 : 2024년 1월 ~ 4월'), - (86, 4, 7, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 지원 가능 전공 : IT계열(컴퓨터공학)
- 교차 수강 불가
- 학기당 최소 15ECTS, 최대 30ECTS 수강', NULL, NULL, NULL, '- 미제공하나 교환학생들이 외부 숙소를 찾을 수 있도록 지원', NULL), - (87, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: Computer Science / Information technology(소속전공과 지원전공이 일치하여야 함)
- 교차수강 불가 
- 학기당 최소 10ECTS, 최대 30ECTS 수강', '- 어학성적표가 2024년 11월 1일까지 유효하여야 함', NULL, NULL, '- 미제공하나 교환학생들이 외부 숙소를 찾을 수 있도록 지원', NULL), - (88, 6, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영학 계열(소속전공과 지원전공 일치) :
- 교차 수강 가능
- 학기당 최소 15ECTS, 최대 34ECTS 수강', NULL, NULL, NULL, '- 미제공
- 숙소관련 문의 : gdesforges@omneseducation', NULL), - (89, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것을 권고함 : 경영학
- 15ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 30일)', NULL, NULL, '- 미제공', NULL), - (90, 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치 또는 유사하여야 함 : 전공이 제한적이므로 반드시 홈페이지에서 지원 가능 전공을 확인할 것
- 최대 30ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', NULL, NULL, '- 미제공', NULL), - (91, 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차수강 가능
- 최소 15ECT, 최대 33ECTS까지 수강', '- 어학성적표가 해당 대학 개강일까지 유효해야 함', NULL, NULL, '- 미제공
- 외부숙소 배정에 대해 지원', NULL), - (92, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영학계열
- 학기당 최소 15ECTS, 최소 30~35ETS 수강해야 함
(Program에 따라 상이함)', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 15일)', NULL, NULL, '- 미제공', NULL), - (93, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공 : 경영학계열(소속대학에서 경영학 관련 기초 과목을 이수하여야 함)
- 학기당 최소 20ECTS, 최대 30ECTS 수강', NULL, NULL, NULL, '- 교외 숙소 제공
- 한달에 250~600€', NULL), - (94, 6, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속 전공과 지원전공이 일치해야 함
- 교차수강 가능
- 최소 15ECTS, 최대 30ECTS 수강신청 가능', NULL, NULL, NULL, '- 미제공
- 숙소관련 문의 : gdesforges@omneseducation', NULL), - (95, '60ECTS 이상 이수해야 함', 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 개설전공 : 경영경상계열, 물류(소속전공과 지원전공이 일치할 것을 권고)
- 전공간 교차수강 불가
- 최소 수강학점 : 15ECTS', '- 어학성적표가 해당 대학 신청서 제출 시 유효해야 함(~10월 31일)
- 최저 성적요건은 프로그램에 따라 상이함(별도 문의할 것)
- 최저이수학기 : 2개 학기 이상 이수하고, 60ECTS 이상 취득하여야 함(세부 문의는 국제교류팀으로 연락)', '* 수강 과정에 따라 영어 성적이 상의하니 유의할 것
- UNDERGRADUATE: IBT 72/ TOEIC 750/ IELTS 5.5
- GRADUATE: IBT 83/ TOEIC 790/ IELTS 6.0', NULL, '- 미제공', NULL), - (96, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없으나 기초과목을 이수해야 함
- 최대 30ECTS 수강', NULL, NULL, NULL, '- 미제공', '2024 봄학기 : January - April/May
(depending on the program of study)'), - (97, '120ECTS 이상 이수해야 함 (비고란 참조)', 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영학계열(소속전공과 지원전공이 일치 또는 유사하여야 함, 기초과목을 이수해야 함)
- 교차수강 불가
- 최소 15에서 최대 34ECTS
(프로그램에 따라 학기당 수강 가능 ECTS가 상이함)', '-어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 20일)
- 최저이수학기 : 120ECTS 이상 취득하여야 함(세부 문의는 국제교류팀으로 연락)', NULL, NULL, '- 미제공
- 숙소 예약에 대한 가이드 제공', NULL), - (98, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 교차수강 가능
- 학기당 최소 15 ECTS 수강', NULL, NULL, '추후 이메일로 안내 예정', '- 기숙사제공(CROUS)', NULL), - (99, 2, 5, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공과 지원전공이 일치하여야 함
- 최대 두 개의 establishment에서 수강 가능
- 지원불가능전공: Medicine, Midwifery, Nursing, Physiotherapy, Chiropody, Law, Digital animations and Video gamrs / 2nd year of Master
- 최소 20ECTS 수강', NULL, NULL, NULL, '- 기숙사 제공(선착순)', NULL), - (100, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 경영대학과 릴 카톨릭대학 ESTICE와의 별도 협약에 따라 경영대학 소속 학생에 한하여 선발함
- 주전공과 지원전공이 일치할 것을 권고함(필수는 아님)
- ESTIC에서 개설된 교과목만 수강 가능
- 최소 12~최대 32ECTS 수강', '어학성적이 2024년 1월까지 유효해야 함', NULL, NULL, '- 미제공', NULL), - (101, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속대전공과 지원전공이 일치할 필요 없음
- 교차수강 불가하며 교확학생에게 오픈된 교과목만 수강 가능', NULL, NULL, NULL, '- 제공(선착순)
- 1200EUR/1학기', NULL), - (102, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 건축학부 재학생에 함함', NULL, '- 프랑스어 성적 제출이 필수는 아니나 대부분의 과목이 프랑스어로 진행되므로 프랑스어를 사전에 공부할 것을 권고함', NULL, '- 미제공
', NULL), - (103, 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 서로 다른 프로그램, 언어, 학과 교차 수강 불가능
- 제한 전공 : https://international.bsb-education.com/course-catalogues/?lang=en', '- 어학성적표가 11월 30일까지 유효하여야 함
- 최저이수학기 관련 : "How to choose my courses" : https://international.bsb-education.com/course-catalogues/?lang=en 참조', NULL, NULL, '- 학교 직영 기숙사는 없으나 CROUS, STUDAPART과 제휴한 숙소 제공(선착순)', NULL), - (104, 3, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 경영학계열, 소속 전공과 지원전공이 반드시 일치할 필요는 없으나 관련 기초과목을 이수하여야 함
- 학기당 15~30ECTS 수강', NULL, NULL, NULL, '- 미제공', '- https://international.audencia.com/student-life/before-you-arrive'), - (105, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속대 전공과 지원전공이 일치할 필요는 없으나 관련 기초과목을 이수하여야 함
- 1년간 최소 8학점(프랑스어 5학점, 프랑스문화 3학점) 이상 이수
- 교차수강 가능(2nd year of master law, LL.M제외)', NULL, '- 영어점수는 아래 각 세부점수를 만족해야 함
- TOEFL IBT: 모든 영역 20점 이상
- IELTS: 모든 영역에서 6 이상
- TOEIC: 말하기, 쓰기, 듣기, 읽기 영역 합산 1020점 이상 ', NULL, '- 외부숙소제공', NULL), - (106, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영경상계열(소속대학 전공이 관련 학과여야 함)
- 하나의 프로그램 내에서 수강 가능
- 최소 24ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 26일)
* 최저이수학기
- Bachelor Year 2과정 : 최소 2개 학기 이상 이수
- Bachelor Year 3과정 : 최소 4개 학기 이상 이수', NULL, NULL, NULL, NULL), - (107, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속대 전공과 지원전공이 반드시 일치할 필요는 없으나 관련 기초과목을 이수하여야 함', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', NULL, NULL, '- 미제공', NULL), - (108, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속 전공과 지원 전공이 일치하여야 함
- 교차 수강 가능
- 최대 학기당 30ECTS 수강 가능', NULL, NULL, NULL, '- 제공(단, 제한적이며 선착순 배정)
- 1600EUR/학기당', NULL), - (109, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속 전공과 지원전공이 일치해야 함 : 경영학, 경영정보공학으로 지원 가능
- 기초지식이 있는 경우에 한하여 교차수강 가능함(Design 및 Fine arts는 전공 학생만 수강 가능)
- 학기당 30 ECTS 수강 권장', NULL, NULL, NULL, '- 미제공', '- 기존 "라티대학"이 "LAB대학"으로 교명 변경'), - (110, 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치해야 함
- 최소 수강 학점 20ECTS, 30ECTS 수강 권장', '- 어학성적표가 해당 대학 개강일까지 유효해야 함', NULL, NULL, '- 미제공
- 교환학생의 경우 LOAS(www.loas.fi)를 통해 숙소를 신청함
- 290~420EUR/1달', '
'), - (111, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: Faculty of Engineering and Business 내 전공 지원 가능(우리대학의 경우 공과대학, 경영대학, 소프트융합대학 소속 학생만 신청 가능)
- 소속대 전공과 지원전공이 일치해야 함
- 학기당 최대 30ECTS 수강신청 가능', NULL, NULL, NULL, '- 미제공
- 한달에 280~370€', NULL), - (112, 2, 2, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치 또는 유사하여야 함
- 교차 수강 가능
- 한 학기 30ECTS를 수강 권고', NULL, NULL, NULL, '- 미제공', NULL), - (113, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능', NULL, '-TOEFL IBT minimum Scores :
Reading 18, Writing 23, Listing 18, Speaking 19
- 영어성적은 교환학생을 하는 내내 유효한 성적표여야 함', NULL, '- 기숙사 보유(한학기에 250BND)', '- COVID 관련 안내 사이트 https://www.pmo.gov.bn/TAPressRelease/[FINAL]%20PR%20JKPC%2012023%20-%20Pengemaskinian%20Sukat-Sukat%20Pengawalan%20COVID-19.pdf
-수업방식: 온라인, 오프라인 혼합'), - (114, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능', NULL, '- 유효한 영어공인인증시험점수는 2021년 9월 26일 이후 응시한 시험점수에 한함
- 어학점수 미보유 시 어학요건 기준에 준하는 어학실력을 보유하고 있다는 내용을 담은 지도교수 추천서를 제출하여 어학점수 갈음 가능', NULL, '기숙사 없음', NULL), - (115, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능
-지원불가 전공: Master of Science in Computer Science & Data Analytics and Master of Science in Electrical & Power Engineering', NULL, '- 유효한 영어공인인증시험점수는 2021년 11월 1일 이후 응시한 시험점수에 한함', NULL, '- 기숙사 보유 (한학기700-950 USD)
', '- COVID 관련 안내 사이트
https://koronavirusinfo.az/az'), - (116, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 캠퍼스별 수강 가능 전공 다 다름 (매학기 변동 가능)
- 아래 캠퍼스 별 수강 가능 전공 참조
-BINUS International Senayan Campus: Fashion, Business, Computer Science, Accounting, Graphic Design and New Media, Communication, Business Information System,

-BINUS Kemanggisan: Hotel Management, Tourism, Computer Science, Accounting, Civil & Industrial Engineering, Management, Marketing communication, Business Law, English Literature, Architecture

-BINUS Alam Sutera: International Business Management, Computer Science, International Relations, Food Technology

-BINUS Bekasi: Bar & Hotel Management', NULL, 'IELTS overall band score of 6
- 유효한 영어공인인증시험점수는 2022년 3월 1일 이후 응시한 시험점수에 한함', NULL, '- 기숙사 보유 (교환학생들은 도착 후 Limited Stary Permit이 나오는 한달동안 반드시 기숙사에서 거주해야함)
- 2인실 한달에 USD240, 1인실 한달에 USD280
-https://binus.ac.id/binussquare/', NULL), - (117, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'https://kuglobal.w3.kanazawa-u.ac.jp/wp/wp-content/uploads/KUEP2022-20231.pdf', 'College of Science and Engineering or College of Medical, Pharmaceutial and Health Sciences의 경우 3학년 이상만 신청 가능', NULL, NULL, NULL, NULL), - (118, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원불가 : Professional School of Law', NULL, 'Faculty of International social Sciences에 한해 영어점수 필요(TOEF L IBT 80, IELTS 6.0)
''-2년 이상의 일본어 공부 이력도 신청 가능', NULL, '- WAKEIJUKU https://www.wakei.org/english/
- KITAZONO WOMEN''S STUDENT https://www.kitazono-j.co.jp/
- CAMPUS VILLAGE KOTAKEMUKAIHARA https://749.jp/cd/2455/
- CAMPUS VILLAGE AKATSUKASHINMACHI https://749.jp/cd/2442/', NULL), - (119, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 학부생만 지원 가능
- Higashiosaka campus만 지원 가능(약학과 지원불가)
- 영어과정은 International Studies 또는 Business 계열만 가능', NULL, NULL, NULL, '기숙사 없음, 학교밖 숙소는 약 270,000엔/학기', NULL), - (120, 2, 9, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '2.30 points *NU 환산표 참조 : https://www.niigata-u.ac.jp/en/wp-content/uploads/2020/12/method.pdf', '교환학생 입학 기준은 별도로 없으나, 영어 일본어 교과목 수강을 위해서는 기본 언어능력 필요(일본어의 경우 N2)', NULL, NULL, NULL), - (121, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-Hakusan Campus에서만 수강 가능,
''-교환학생을 위한 교과목만 수강 가능 https://www.toyo.ac.jp/en/international-exchange/prospective/Exchange-Program/#acl ', '*해당 학교 일정 상 10월초까지 서류제출 필요', 'TOEIC:TOEIC L&R+(TOEIC S&W*2.5)
정규교과목 수강은 N2이상 ', NULL, NULL, NULL), - (122, 4, 3, NULL, 'HOME_UNIVERSITY_PAYMENT', 'Hakusan Campus에서만 수강 가능', '*해당 학교 일정 상 10월초까지 서류제출 필요
3+1 프로그램 신청자 대상으로 하는 별도 장학금 수혜 가능', NULL, NULL, NULL, '- [3+1 프로그램]만 지원 가능 (현지 취업을 위한 맞춤형 프로그램, 파견차수 학기 유의할 것)
- 3+1 프로그램은 6차, 7차 학기를 일본대학에서 수학하면서 현지에서 직장을 구한 후 인하대에서 마지막 8차 학기 이수하고 일본으로 돌아가 직장생활을 하는 프로그램임'), - (123, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, '- 기숙사 미보유, 교환학생을 위한 외부 숙소 보장
- 280,000엔~330,000엔/학기 (2023년 기준)', NULL), - (124, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', '*해당 학교 일정 상 10월초까지 서류제출 필요', '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, NULL), - (125, 2, 1, 'IRRELEVANT', NULL, '교환학생 지원가능 : Department of Global English, Department of Japanese culture, Department of Media and Information, Department of Psychology.', '여학생만 신청가능', NULL, NULL, '기숙사 없음, 계약된 외부 기숙사 사용-“Maison de Claire Ibaraki” 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)', NULL), - (126, 2, 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월', NULL), - (127, 2, 10, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'http://www.isc.yamaguchi-u.ac.jp/inbound/FGSS_course_list/pdf', '- 전공별 TO에 따라 선발 (최대인원을 넘지 않도록)
- Faculty of Global and Science Studies : 최대 5명
- Faculty of Economics: 최대 8명
- Faculty of Engineering: 최대 3명
- Faculty of Humanities: 최대 2명
- Others: 최대 2명', 'Graduate School of Ecomnomics 토플 79 이상 토익 730이상 필요', NULL, '교환학생은 off-campus dormitory 거주의무', NULL), - (128, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, '*해당 학교 일정 상 10월초까지 서류제출 필요', NULL, NULL, NULL, '-봄학기는 한 개 학기만 교환학생 파견 가능
-교환학생 용 "International Exchange Program" 제공 (일본어 능력이 높은 학생에게만 오후에 제공되는 일반 과정 수강 가능)'), - (129, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '여학생만 신청가능', NULL, NULL, '36,500엔/월, 식비별도', NULL), - (130, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', ' Please refer to "Requirements and Course Lists" for further information. https://www.waseda.jp/inst/cie/en/exchange/application ', NULL, '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, NULL), - (131, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원불가 : Professional Graduate Program (Law School, Business School)', NULL, 'Commerce / Science & Engineering/ Global Informatics', NULL, 'Seiseki-Sakuragaoka dormitory (off-campus)는 꽤 경쟁이 치열함. On-campus와 Off-campus 모두 약 1,800-1,900 EUR/학기', NULL), - (132, 2, 2, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', '경제학, 국제통상학과만 지원 가능', '*해당 학교 일정 상 10월초까지 서류제출 필요', NULL, NULL, NULL, NULL), - (133, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것을 권장함
', NULL, '- 유효한 영어공인인증시험점수는 2021년 10월 16일 이후 응시한 시험점수에 한함', NULL, '- 기숙사 보유 (한달에 550 TL ~ 2000 TL)
', '- 오프라인 수업으로 예상하나 온라인 수업 등으로 변동 가능성 있음
- COVID 관련 안내 사이트
https://covid19.saglik.gov.tr/'), - (134, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-소속전공과 지원전공이 일치할것
-학부 학생이 석사용 수업 수강할 수 없음', NULL, '*TOEFL IBT minimum Scores :
Reading 20, Writing 20, Listing 20, Speaking 20
유효한 영어공인인증시험점수는 2021년 12월 1일 이후 응시한 시험점수에 한함
https://www.ozyegin.edu.tr/en/student-services/application-admission/language-proficiency-requirement', NULL, '-기숙사 보유하나 입사 보장은 불가', '-COVID 관련 안내 사이트
https://www.ozyegin.edu.tr/en/covid-19'), - (135, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능', NULL, 'IELTS overall band score of 6 with no band lower than 5.5', NULL, '- 기숙사 2인실(매달 263-302 USD)', '-COVID 관련 안내 사이트
https://www.coronavirus.gov.hk/eng/index.html'), - (136, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-소속전공과 지원전공이 일치할것
- College of Science(Chemistry/ Mathematics / Physics) 이 학과로 홍콩시립대에서 수강 시 위 학과에서 9학점 이상 이수 필수 수강을 해야함
-지원제한 전공: Biomedical science
-몇몇 수강인원이 제한된 학과는 교환학생을 받지 않음 주의
-선행 과목 수강이 필수인 학과의 경우 수강 이력이 없을 시 수강 불가', NULL, 'ielts overall band 6.5
-유효한 영어공인인증시험점수는 2021년 10월 2일 이후 응시한 시험점수에 한함', NULL, '- 기숙사가 있으나 기숙사 배정이 상당히 어려움
- 대략적 비용: 학기당 HKD$ 10,850 ~ HKD$ 21,700', '- COVID 관련 안내 사이트 https://www.cityu.edu.hk/fmo/default.aspx?PageID=covid19infoctr'), - (137, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 수강 제한 전공: IMBA, EMBA, IMAS', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', NULL, '- 기숙사 보유
- 대략적 비용: 한학기에 NTD$11,000~ 33,500', NULL), - (138, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-소속전공과 지원전공이 일치할것
-중국어 실력이 유창하지 않으면 영어로 강의되는 수업만 수강 가능함
- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', NULL, ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유 (한학기235 USD-535 USD)', '-COVID 관련 안내 사이트
https://www.cdc.gov.tw/En'), - (139, 4, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 지원가능 전공 확인 : https://www.ncu.edu.tw/en/pages/index.php?num=2
- 교환학생용으로 제공되는 영어강의는 본래 대학원생용이라 학부 3학년 또는 4학년 학생이 국립중앙대로의 교환학생에 지원할 수 있음', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 영어성적은 지원일로부터 2년간 유효한 성적표여야 함', NULL, '기숙사 없음', '-COVID 관련 안내 사이트
https://www.cdc.gov.tw/En'), - (140, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능
- 영어성적표 지원일로부터 2년간 유효한 성적표여야 함', NULL, ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유
- 한 학기에 약 500-800$
', '-COVID 관련 안내: 병원과 관련된 시설에서는 반드시 마스크 착용 해야함'), - (141, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차 수강 가능
- 주로 인하대 학생들은 중국어 어학과정을 수강함
- 최소 수강학점은 없으며, 주로 3~6과목 (2~4학점/1과목)을 수강함
', NULL, '- 본교 중국어 어학시험에 응시하여야 함
- 중국어 연수 프로그램 : 영어,중국어 성적이 없어도 지원 가능
- 학위과정수업(영어) : IBT 59 이상', 'English taught courses are generally determined by the schools at the end of the previous semester. We are not sure about it until then. ', '- 학교에 기숙사가 있으나 입사 보장은 불가
International Students Building (only for foreign students): Single room with bedding packs, air-conditioner and bathroom,500RMB
Local Student Dormitory: 4-6 person room without bedding packs, 600-1000RMB ', NULL), - (142, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 지원제한 전공 : Medical Science', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
* 중국어 과정
1) 중국어학연수과정 : 어학성적 필요 없음
2) Diplomacy (School of International and Public Affairs) : 유효한 HSK 5급 180점 이상, TOEFL 550(PBT), 213(CBT), 79(IBT) 또는 IELTS 6.0.이상을 보유하여야 함(HSK및영어 동시만족)
3) 중국어 과정(석사):HSK 5급 180점 이상
4) 기타 중국어 과정 : HSK 4급 180점 이상
* 영어 과정 : 길림대 내 영어테스트가 있을 예정임
- 영어과정을 들을 경우, 영어성적표을 제출하여야 하며 지원일로부터 2년간 유효한 성적표여야 함', NULL, '- 기숙사 보유 (6개월 이상 거주 시 신청 가능)
- 대략적 비용: 한달에 1500RMB ', NULL), - (143, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야함(교차수강 불가능)
- 대부분의 교환학생들은 중국어 어학 수업을 수강함', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어로 된 전공 강의를 수강하기 희망할 경우, 최소 HSK 5급 180점 이상의 점수를 보유하여야 함
-중국어학연수과정 : 어학성적 필요 없음', '- 영어수업을 제공할지 결정되지 않았음', '- 기숙사 보유 (한학기 5000RMB-6000RMB: 겨울방학 및 여름방학 제외)
', NULL), - (144, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '소속전공과 지원전공이 반드시 일치할 필요는 없으나 학과에 상관없이 교차 수강은 불가능
-교환학생 대상으로 별도 제공된 수강 가능 교과목안에서만 수강 가능  ', NULL, '- 유효한 영어공인인증시험점수는 2021년 11월 16일 이후 응시한 시험점수에 한함
- 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유(한 학기 약 7800yuan)', NULL), - (145, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', ' 소속전공과 지원전공이 일치해야 함. 단, 중국어 어학과정 학생의 경우 전공 무관
- 최소 학점은 없으며, 최대 24학점 수강 가능
- 체육교육과 전공은 제공하지 않음
- 지원 가능 전공 https://ipo.wh.sdu.edu.cn/kristudy/info/1021/1752.htm', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어 수업을 들을 학생은 HSK4급 보유를 권장함
- 영어 수업을 들을 학생은 TOEFL IBT 80점, IELTS 6등급 보유를 권장함
- 학점 4.0 만점에 2.3, 100점 만점에 70이상인 학생만 지원 가능', NULL, '- 기숙사 보유 (한학기 4,000~5,000 RMB, 금액은 변동가능성있음)', NULL), - (146, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '''- 소속전공과 지원전공이 일치하여야 함 (교차 수강 불가능)', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어 어학강좌만 수강할 경우 HSK 또는 영어성적 제시 불필요
- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK level 4 이상이어야 함)', NULL, '- 기숙사 보유(한 학기 3100-3600RMB)', NULL), - (147, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
', NULL, '- 본교 중국어 어학시험에 응시하여야 함
- 중국어로된 전공강의 수강(학위과정)을 희망할 경우 별도 문의 (HSK4급이상 보유자만 가능)
-유효한 영어공인인증시험점수는 2021년 12월 21일 이후 응시한 시험점수에 한함', NULL, '-기숙사 보유 (2인실/ 매달 450-600RMB)', NULL), - (148, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음', NULL, ' - 본교 중국어 어학시험에 응시하여야 함
- 중국어 연수 프로그램 : 어학성적 불필요
- 영어강의 없고 중국어로 하는 전공수업만 제공함. HSK5급 보유자만 지원가능(확인중)', NULL, '- 기숙사 보유 (한학기 1100-1500 USD) ', NULL), - (149, NULL, 10, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '- 어학요건이 대학별로 상이하므로 반드시 Program Guide 참고할 것', NULL, NULL, '- 지원 전 반드시 국제교류팀 담당자와 상담할 것'); - -INSERT INTO university_info_for_apply ( - id, - university_id, - semester_requirement, - student_capacity, - semester_available_for_dispatch, - tuition_fee_type, - details_for_major, - details_for_apply, - details_for_language, - details_for_english_course, - details_for_accommodation, - details, - term, - gpa_requirement, - gpa_requirement_criteria -) VALUES - (150, 1, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', NULL, NULL, NULL, NULL, NULL, '2024-2', 2.5, 4), - (151, 2, 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', NULL, NULL, NULL, NULL, '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend', '2024-2', 2.5, 4), - (152, 4, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 6.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능, Fact Sheet 참고)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $2,700~5,900
- International Program and Service Fees : $2,500', '2024-2', 3, 4), - (153, 3, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능, Fact Sheet 참고)
- 한 학기 등록금: 약 $7,500
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $2,700~5,900
- International Program and Service Fees : $2,500', '2024-2', 2.5, 4), - (154, 5, 2, 5, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함', NULL, NULL, NULL, NULL, '※ On Campus Room and Board 신청 필수! (기숙사 미신청 시 해외대학등록금납부형(B형)으로 전환)
- 기숙사 관련 정보: https://www.unk.edu/offices/reslife/housing-options.php
- 보험 관련 정보: https://www.internationalstudentinsurance.com/student-health-insurance/?gad=1&gclid=Cj0KCQjw9fqnBhDSARIsAHlcQYQO_d0Rmq607FC8cauQ-e_vyEKWNw4DnXsYpgm_nnjrFmx-BJLBBwUaAg9OEALw_wcB', '2024-2', 2.5, 4), - (155, 6, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함', '※ ELI(어학연수) 과정으로 지원시 영어 성적 무관 (https://www.unk.edu/international/english-language-institute/index.php)', NULL, NULL, NULL, '※ ELI 어학연수 과정으로 지원시, 전공/ESL 크레딧은 자체배치고사 점수에 따라 상이
- 등록금 관련 정보: https://www.unk.edu/costs.php
- 기숙사 관련 정보: https://www.unk.edu/offices/reslife/housing-options.php
- 보험 관련 정보: https://www.internationalstudentinsurance.com/student-health-insurance/?gad=1&gclid=Cj0KCQjw9fqnBhDSARIsAHlcQYQO_d0Rmq607FC8cauQ-e_vyEKWNw4DnXsYpgm_nnjrFmx-BJLBBwUaAg9OEALw_wcB', '2024-2', 2.5, 4), - (156, 152, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요
- 타전공 과목 수강 시, 대부분의 Advanced Classes는 Pre-requisite을 충족하여야 수강 가능', NULL, NULL, NULL, NULL, '- 한 학기 기숙사 비용: 약 $3,813~4,808', '2024-2', 2, 4), - (157, 153, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요
- 타전공 과목 수강 시, 대부분의 Advanced Classes는 Pre-requisite을 충족하여야 수강 가능', NULL, NULL, NULL, NULL, '- 한 학기 등록금: 약 $914.70/per credit (2023/24 rate)
- 한 학기 기숙사 비용: 약 $3,813~4,808', '2024-2', 2, 4), - (158, 7, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Nursing, Athletic training, Education, School of Professional Studies Programs', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 모든 영역에서 15점 이상
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, '- 한 학기 기숙사 비용: 약 $3,900~$5,500', '2024-2', 2.75, 4), - (159, 8, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Nursing, Athletic training, Education, School of Professional Studies Programs', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 모든 영역에서 15점 이상
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, '- 한 학기 등록금: 약 $6,938 (In-state, 2023/24 기준)
- 한 학기 기숙사 비용: 약 $3,900~$5,500', '2024-2', 2.75, 4), - (160, 9, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 쓰기 영역에서 5.0 이상', NULL, NULL, '- 교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음
- College Fee : 약 $1,070', '2024-2', 3, 4), - (161, 10, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 쓰기 영역에서 5.0 이상', NULL, NULL, '- 한 학기 등록금: 약 $9,450 (out of state, 2023/24 기준)
- 교내 기숙사가 한정되어있어 배정 받지 못할 가능성 있음
- College Fee : 약 $1,070', '2024-2', 3, 4), - (162, 11, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원 불가 전공 : Health Sciences, Pharmacology, Nursing, 일부 WRT/CSE 과목
- 수강 제한 전공 : Theatre Arts, Dance, 일부 Languages 과목
- ACC/BUS, CSE/ISE 전공 학생은 최대 2과목 까지만 전공 내에서 수강이 가능하며, 나머지 학점은 다른 전공에서 수강
- 학과에 지원 전제조건이 있을 경우 충족해야 함', NULL, NULL, NULL, NULL, '- 한 학기 기숙사 비용: 약 $4,500~6,200
- 한 학기 등록금: 약 $13,430 (out of state)
- 등록금 및 기타 Fee Rates 관련 정보: https://www.stonybrook.edu/commcms/sfs/tuition/index.php', '2024-2', 2.8, 4), - (163, 12, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Nursing', NULL, NULL, NULL, NULL, NULL, '2024-2', 2.5, 4), - (164, 13, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Nursing', NULL, NULL, NULL, NULL, '한 학기 등록금: 약 $6,892 (In-state, 2023/24 기준)', '2024-2', 2.5, 4), - (165, 16, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 제한 전공: Athletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, Nursing', NULL, NULL, NULL, NULL, '- 모든 국제학생들의 안전과 영어향상을 위해 기숙사 사용을 강제하는 International Studies Policy (기숙사 신청은 선착순이므로 입학 허가서 수령 직후 기숙사를 신청해야 배정 받을 수 있음)', '2024-2', 2.5, 4), - (166, 17, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 제한 전공: Athletic Training, Border and Homeland Security, Border Security, Intelligence, Security, Studies and Analysis, Nursing', NULL, NULL, NULL, NULL, '- 등록금은 In-state rate 적용
- 모든 국제학생들의 안전과 영어향상을 위해 기숙사 사용을 강제하는 International Studies Policy (기숙사 신청은 선착순이므로 입학 허가서 수령 직후 기숙사를 신청해야 배정 받을 수 있음)', '2024-2', 2.5, 4), - (167, 20, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능 (단, 타전공 지원시 각 학과의 사전허가 필요)
- 선이수과목이 비숫해야 후에 IIT에서 전공과목을 수강할 수 있음', ' - SAT시험 면제 조건으로 동일계 학과에서 최소 30학점 이수하여야 하며, 입학사정시 전공과목 및 영어과목 위주로 검토 됨', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상', NULL, '- 식비(Meal Plan) 정보
https://www.iit.edu/housing/dining-and-meal-plan/options-and-rates
- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid

- 학비관련 site
https://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate
- 한 학기 등록금: 약 $14,820 (방문학생 학비장학금 $10,000/학기 차감한 금액, 징학금은 12크레딧 이상 full time 등록 시에만 지급 가능, 2023/24 rate)
- 보험료 site
https://www.iit.edu/shwc/insurance/plan-info-and-requirements
- 세부사항 변동 가능', '2024-2', 3.4, 4.5), - (168, 21, 4, 5, 'FOUR_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 정확하게 일치해야함
- 선이수과목이 비숫해야 후에 IIT에서 전공과목을 수강할 수 있음', ' - SAT시험 면제 조건으로 동일계 학과에서 최소 30학점 이수하여야 하며, 입학사정시 전공과목 및 영어과목 위주로 검토 됨
- 2024-1학기에 4차 학기 이수 예정인 학생도 조건부 지원 가능
(반드시 정규학기 이수하여야 2024-2 파견 가능)', '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상', NULL, '- 식비(Meal Plan) 정보
https://www.iit.edu/housing/dining-and-meal-plan/options-and-rates
- 세부사항 변동 가능', '※ IIT 사이트 요약 : https://www.iit.edu/admissions-aid/tuition-and-aid/undergraduate-costs-and-aid
- 학비관련 site
https://web.iit.edu/student-accounting/tuition-fees/current-tuition/main-campus-undergraduate
- 한 학기 등록금: 약 $24,820, 복수학위 학비장학금 연간 1.5만~3만 달러 지급 가능 (징학금은 12크레딧 이상 full time 등록 시에만 지급 가능, 2023/24 rate)
- 보험료 site
https://www.iit.edu/shwc/insurance/plan-info-and-requirements
- 세부사항 변동 가능', '2024-2', 3.4, 4.5), - (169, 22, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, NULL, '※ 테일러대학은 기독교 정신을 기반으로 설립된 학교로 대다수의 학생들이 기독교를 믿고 있습니다. 이러한 테일러대학의 특성을 고려하여, 성실하고 정기적인 교회 생활을 이어가고 있는 학생들에 한해서 지원을 받고 있습니다. 지원 과정에서 목사추천서 등의 자료를 필수적으로 제출하여야 하며, 필요 시 간단한 면접을 진행할 수 있습니다.
- 원칙은 1학기 지원이나, 2학기도 학생이 원하면 지원 가능.
다만, 파견대학에서 학생의 교환학생 성과를 평가해 이에 미치지 못할 경우 2학기를 이어 진행하지 못하고 한 학기만 진행할 수 있으니 해당 사항 유의하기 바람. 이에 2학기를 지원하는 학생의 경우, 파견 지원 전 반드시 지역담당자에게 사전에 연락해 관련 내용에 대해 논의하고 지원하길 바람. ', '2024-2', 3, 4), - (170, 150, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 다음 전공들은 지원 가능하나 수강신청이 제한적일 수 있음 Architecture, Computer & Information Sciences, Education, Performing Arts (Dance, Music, Theater), Professional Schools (Dentistry, Law, Medicine, Pharmacy, Podiatry), Visual Arts (Film/Media Arts, Graphic Design, Fine Arts, etc), Sport, Tourism and Hospitality Management
- Business 전공의 경우, 본교 경영학과 학생만 지원가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC : 모든 영역에서 390점 이상
- 최저 기준 어학 점수가 넘더라도, 선발대학에서 특정 섹션의 어학실력이 부족하다고 판단될 경우 파견 시 별도로 대학부설 어학코스(ielp) 수강이 필요할 수 있음 (수업료 외 별도 비용 발생, https://tcalc.temple.edu/)', NULL, NULL, '- 한 학기 기숙사 비용: 약 $5,800 (기숙사 유형에 따라 상이)', '2024-2', 3, 4), - (171, 23, 2, 5, 'IRRELEVANT', 'MIXED_PAYMENT', '- 타전공 지원 및 수강 가능
- 다음 전공들은 지원 가능하나 수강신청이 제한적일 수 있음 Architecture, Computer & Information Sciences, Education, Performing Arts (Dance, Music, Theater), Professional Schools (Dentistry, Law, Medicine, Pharmacy, Podiatry), Visual Arts (Film/Media Arts, Graphic Design, Fine Arts, etc), Sport, Tourism and Hospitality Management
- Business 전공의 경우, 본교 경영학과 학생만 지원가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC : 모든 영역에서 390점 이상
- 최저 기준 어학 점수가 넘더라도, 선발대학에서 특정 섹션의 어학실력이 부족하다고 판단될 경우 파견 시 별도로 대학부설 어학코스(ielp) 수강이 필요할 수 있음 (수업료 외 별도 비용 발생, https://tcalc.temple.edu/)', NULL, NULL, '※ 1개 학기로도 지원 가능 (해외대학등록금납부형 적용)
※ 혼합형은 첫 번째 학기는 템플대학교에 등록금 지불, 두 번째 학기는 인하대에 등록금 지불하는 유형 (첫 번째 학기를 모두 마친 경우에만 두 번쨰 학기에 템플대학교 등록금 면제 및 인하대에 등록금 지불 적용 가능)
- 한 학기 등록금: 약 $16,188 (out of rate, 2023/24 기준, https://globalprograms.temple.edu/programs/inbound-study-abroad-exchange/costs-dates)
- 한 학기 기숙사 비용: 약 $5,800 (기숙사 유형에 따라 상이)', '2024-2', 3, 4), - (172, 24, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원제힌전공: Nursing', NULL, NULL, NULL, NULL, NULL, '2024-2', 2.5, 4), - (173, 25, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원제힌전공: Nursing', NULL, NULL, NULL, NULL, '한 학기 등록금: 약 $9,792 (50% tuition scholarship, 2023/24 기준)', '2024-2', 2.5, 4), - (174, 151, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', ' - 최소 2개 학기, 24 transferable semester credits 이상 이수한 학생에 한하여 지원 가능. 이 때, English language/composition, religious doctrine, remedial/technical, field studies, internships 등 일부 수업은 인정되지 않을 수 있음
(관련 정보 : https://manoa.hawaii.edu/mix/inbound/official-transcript-requirements/)', ' - 토플 IBT(100), 토플 ITP(600), IELTS(7.0) 미만시 파견 후 별도의 영어시험을 응시해야하며, 결과에 따라 1~3개의 어학 수업을 수강하게 될 수 있음', NULL, NULL, '※ 하외이대학 지원 마감일정 상 3월 15일(금)까지 서류 제출 필요 (추후 합격자에 한하여 사전 안내 예정)', '2024-2', 2.5, 4), - (175, 26, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', ' - 최소 2개 학기, 24 transferable semester credits 이상 이수한 학생에 한하여 지원 가능. 이 때, English language/composition, religious doctrine, remedial/technical, field studies, internships 등 일부 수업은 인정되지 않을 수 있음
(관련 정보 : https://manoa.hawaii.edu/mix/inbound/official-transcript-requirements/)', ' - 토플 IBT(100), 토플 ITP(600), IELTS(7.0) 미만시 파견 후 별도의 영어시험을 응시해야하며, 결과에 따라 1~3개의 어학 수업을 수강하게 될 수 있음', NULL, NULL, '※ 하외이대학 지원 마감일정 상 3월 15일(금)까지 서류 제출 필요 (추후 합격자에 한하여 사전 안내 예정)
- 한 학기 등록금: 약 $8,478 (Hoakipa Visiting Student 유형으로Resident Tuition의 150%)', '2024-2', 2.5, 4), - (176, 27, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', '모든 강의는 포르투갈어로 진행하며 영어강의 미제공, 포르투갈어 가능자만 지원 권장', NULL, '※ 영어강의 제공하지 않음, 모든 강의 포르투갈어로 진행', NULL, '- 교내 기숙사 미제공, International Affairs와 버디프로그램을 통해 교외숙소 계약을 도와줄 예정', '2024-2', NULL, NULL), - (177, 29, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 아래 8개 Faculties 내에서만 수강 가능 :
Arts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science
- 지원 불가 전공: Nursing, Social work', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상 ', NULL, NULL, NULL, '2024-2', 2.5, 4), - (178, 30, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 아래 8개 Faculties 내에서만 수강 가능 :
Arts, Business Administration, Education, Engineering and Applied Science, Kinesiology, La Cite, Media/Art/Performance, Science
- 지원 불가 전공: Nursing, Social work', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 모든 영역에서 20점 이상
- IELTS : 모든 영역에서 6.0 이상 ', NULL, NULL, '한 학기 등록금 : International Student Fee 적용, 지원 전공 및 학점에 따라 금액 상이 (https://www.uregina.ca/fs/students/fee-schedule.html)', '2024-2', 2.5, 4), - (179, 31, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 아래 6개의 Faculties 내에서 수강 가능 :
Business Administration, Education, Engineering and Applied Sciences, Human Kinetics and Recreation, Humanities and Social Sciences, Science
- 지원 불가 전공: Medicine, Pharmacy, Social work, Nursing
- 지원 제한 전공: Music, Computer Science', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상', NULL, NULL, NULL, '2024-2', 2.5, 4), - (180, 32, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 아래 6개의 Faculties 내에서 수강 가능 :
Business Administration, Education, Engineering and Applied Sciences, Human Kinetics and Recreation, Humanities and Social Sciences, Science
- 지원 불가 전공: Medicine, Pharmacy, Social work, Nursing
- 지원 제한 전공: Music, Computer Science', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상', NULL, NULL, 'International Students Tuition Fee 적용 (약 $20,790, 2023/24 기준)', '2024-2', 2.5, 4), - (181, 33, 2, 3, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '※ 가장 기초 과정(IEP-G)의 최소 지원 요건이며 레벨과 지원과정에 따라 지원자격이 상이하므로 fact sheet 참조 바람
- 학부과정 수강 가능한 IEBP-G 지원 시 다음의 세부영역 점수를 만족해야함
- IELTS : 모든 영역에서 5.0 이상, 쓰기 5.5 이상
- TOEFL iBT : 쓰기 16점 이상', NULL, NULL, '- 선발 학생의 어학성적에 따라 레벨이 정해지며, 레벨에 따라 등록금 상이
- IEBP-G에 배정될 경우, 학부 수업 1~2개 수강 가능하며 (선택 제한적) 학부수업에 대한 등록금은 면제 (국제처 홈페이지 내 대학 Fact Sheet 참고)', '2024-2', 2.5, 4), - (182, 34, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 지원 유의 전공: Interior Design, Architecture Design
- Media and Communication, Design, Fashion 전공은 본교에서 동일한 전공을 이수 중인 학생만 지원 가능
(참고 : https://www.rmit.edu.au/study-with-us/international-students/programs-for-international-students/study-abroad-and-exchange/student-exchange/how-to-search-for-your-courses)', NULL, NULL, NULL, NULL, NULL, '2024-2', 2, 4), - (183, 35, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- Business, Engineering, Law, IT, Biology 학과 수강 가능 (그 외의 학과는 Trimester 1,2에 지원 권고)
- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 지원 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 모든 영역에서 5.5 이상
- TOEFL: 모든 영역에서 19점 이상', NULL, NULL, '- 서던퀸스랜드대학은 3학기제로 운영되며, 본교 가을학기 파견교환학생은 Trimester 3로 수학', '2024-2', 2.5, 4), - (184, 36, 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '- Business, Engineering, Law, IT, Biology 학과 수강 가능 (그 외의 학과는 Trimester 1,2에 지원 권고)
- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 지원 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 모든 영역에서 5.5 이상
- TOEFL: 모든 영역에서 19점 이상', NULL, NULL, '- 서던퀸스랜드대학은 3학기제로 운영되며, 본교 가을학기 파견교환학생은 Trimester 3로 수학
- 한 학기 등록금: AU$2,375 per course (In-state, 2023/24 기준)', '2024-2', 2.5, 4), - (185, 38, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상', NULL, NULL, NULL, '2024-2', 3, 4), - (186, 39, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 19점 이상', NULL, NULL, '한 학기 등록금: 약 AU$10,400 (In-state)', '2024-2', 3, 4), - (187, 46, 2, 5, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점
- TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점', NULL, NULL, NULL, '2024-2', NULL, NULL), - (188, 47, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)
- 일반적으로 Business, Computer Sciences, Engineering, Tourism Field에서 영어강의를 교환학생에게 제공함 ', NULL, '독일어 공인성적으로 지원할 경우, B1 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- 교환 학생 프로그램에 독일어 어학 수업 포함
- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), - (189, 48, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC: 읽기 385점 이상, 듣기 400점 이상
- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, NULL, '2024-2', NULL, NULL), - (190, 49, 2, 6, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함
- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙필요', '각 단과대학별 상이하므로 국제처 홈페이지 해외대학정보 Fact sheet 및 홈페이지 참조 바람', NULL, NULL, '2024-2', NULL, NULL), - (191, 50, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 경영학(독일어 강의) 수강요건: 독일어 B2 이상의 증빙 필요', NULL, NULL, NULL, NULL, '- 보험 관련 정보: https://www.hwg-lu.de/international/exchange-students-from-partner-institutions/before-mobility/health-insurance', '2024-2', NULL, NULL), - (192, 51, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- International Study Programme(ISP) 내 수업만 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- Public German Health insurance 가입 의무', '2024-2', 2.5, 5), - (193, 52, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함(복수전공, 부전공 가능)
- 지원 불가능 전공: Pharmacy, Human Medicine and Veterinary Medicine
- Biochemistry, Bioinformatics and Biology, Law 지원 제한적 (학과 사전승인 필요)
- Business Administration, Economics 학과 수업 대부분이 독일어로 진행, 독일어 가능자 지원 권장', NULL, '- Department of Humanities, Social Science, Business Administration and Economics 수강요건: 독일어 공인성적 B2 레벨 이상의 증빙 필수
- Department of Natural Science 수강요건: 독일어 공인성적 B1 레벨 이상 증빙 필수
- John F Kennedy Institute for North American Studies 수강요건: 영어 공인성적 C1 레벨 이상의 증빙 필수', '※ 주로 Departments of English and North American Studies에서 영어강의 제공, 이 외의 학과 영어수업 제한적', NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- 보험 관련 정보: http://www.fu-berlin.de/en/studium/international/studium_fu/einreise_aufenthalt/krankenversicherung', '2024-2', NULL, NULL), - (194, 53, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)', '※ Faculty of Economics, Business Administration에 한하여 지원 가능', '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- 보험 관련 정보: https://international.fhws.de/en/fhws-international/ways-to-fhws/applicants-and-student-support/before-your-arrival-at-fhws/ ', '2024-2', 2.5, 5), - (195, 54, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 아래 링크에 안내된 강의 내에서 자유롭게 수강 가능 (https://www.hs-schmalkalden.de/en/international/incoming-students/courses-for-incomings/exchange-students)', NULL, NULL, NULL, NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- Public German Health insurance 가입 의무 (관련 정보: https://feather-insurance.com/en/public-health-insurance/barmer?utm_source=barmer_schmalkalden)', '2024-2', NULL, NULL), - (196, 55, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 제한 전공: International Project Management, Smart City Solution
- Civil Engineering, Surveying, Mathematics : 독일어 B1 이상 가능자 지원 권장
- Architecture, Interior Architecture and General Management : 독일어 A1 이상 가능자 지원 권장, 독일어 수업 수강 필수
- 주로 Architecture, Interior Architecture, Business Management, General Management, Business School에서 영어 강의 제공', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 모든 영역에서 6.0 이상', NULL, NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), - (197, 56, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공 : Health, Midwifery, Extra-occupational courses ', NULL, NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), - (198, 57, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함 (복수전공, 부전공 가능)', NULL, NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), - (199, 154, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- Architecture, Civil Engineering 학과의 경우 독일어 B1 이상 증빙 필수', NULL, NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), - (200, 58, 3, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '- 독일어 공인성적으로 지원할 경우, B1 레벨에 준하는 성적을 보유하여야 함
- 영어스피킹 중급 이상 학생 지원 권장', NULL, NULL, '- 독일 어학 수업 수강 필수
- Public German Health insurance 가입 의무', '2024-2', 2, 4), - (201, 59, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 파견대학에 지원하는 전공과 본교 전공이 일치해야함
- 지원 불가능 전공: Human medicine, Dentistry, Pharmacy, Law, Psychology and Practical Sports', NULL, NULL, NULL, NULL, '- 기숙사 여석 부족으로 기숙사 배정을 못 받을 가능성 있음
- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), - (202, 60, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 불가 전공: Medicine', NULL, '- 독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- Public German Health insurance 가입 의무', '2024-2', 3, 5), - (203, 61, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 학과에 지원 전제조건이 있을 경우 충족해야 함
- 지원 불가 전공: Medicine, Programmes offered by the Centre for Lifelong Learning', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEIC S/W : 말하기 120점 이상, 쓰기 120점 이상
- 독일어 공인성적으로 지원할 경우, B1 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- 보헙관련 정보: https://uol.de/en/exchange-studies/health-insurance', '2024-2', NULL, NULL), - (204, 62, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, '독일어 공인성적으로 지원할 경우, B2 레벨에 준하는 성적을 보유하여야 함', NULL, NULL, '- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), - (205, 63, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 지원 제한 전공 : Psychology', NULL, NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), - (206, 64, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능', NULL, NULL, NULL, NULL, '- Public German Health insurance 가입 의무', '2024-2', NULL, NULL), - (207, 65, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- Campus Schwäbisch Hall 내 강의 수강 가능하며, Campus Künzelsau에서도 일부 수강 가능', '※ Faculty of Business Administration, Global Finance and Banking, Economics에 한하여 지원 가능', NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), - (208, 82, 1, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고(단, 건축학부로의 지원은 건축학부 전공자만 가능)
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능-
- 학기당 최소 수강학점 : 15 ECTS credits', NULL, '어학성적 유효기간 내 인정', NULL, '- 교환학생 합격자 대상 기숙사 신청 방법 공지 예정
- 기숙사 이용 비용 : 월 300$ 수준', '- 보험 가입 의무 : www.vzp.cz', '2024-2', 1.5, 4), - (209, 81, 3, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능(우측 영어강의리스트 참고 필요)
- 학기당 최소 수강학점 : 최소 5, 최대 30 ECTS credits', NULL, NULL, NULL, '- 기숙사 제공 가능
- 기숙사 이용 비용 : 월 약 7,500 CZK', NULL, '2024-2', NULL, NULL), - (210, 80, NULL, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
※ Faculty of Fine Arts and Music으로 교환학생 지원 시 전공 일치 필요
※ Faculty of Medicine : 교환 학생 지원 불가
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 최소 수강학점 : 20 ECTS credits
※ 일반적 학기당 수강 학점 : 30 ECTS credits
- 수강 학점의 최소 55%는 1개 전공 내에서 수강해야 함.', NULL, '- 유효기간 경과한 어학성적도 인정
- 기타 CEFR English B2+ level에 상응하는 어학성적 또한 인정', NULL, NULL, '- 기숙사 : 해외 교환학생 우선 배정 정책
- 보험 가입 의무 : www.vzp.cz(대학 차원에서의 의무화는 아니나, 국가 관련 법령에 의해 해외 교환학생들은 상해보험에 가입하게 되어 있음.)', '2024-2', 2.5, 4), - (211, 45, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 15, 최대 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정
- 기타 인정 어학성적 : Cambridge Certificate of Advanced English (CAE) or Cambridge Certificate of Proficiency in English (CPE) : PASSED ', NULL, '- 숙소 제공 보장(Guaranteed)/Off-campus Housing', '- COVID-19 관련 : https://en.coronasmitte.dk/', '2024-2', NULL, NULL), - (212, 44, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- ''주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고)
- 수강과목들은 2개 학부 이내에서 수강할 것을 강력히 권장
- 기초 선수과목 적용 교과목들은 본교 사전 이수 필요
- 학기당 수강학점
· 기본(최소) 30 ECTS credits
· 최대 35 ECTS credits : 기본 30ECTS에 덴마크어 학습 강좌(5ECTS)만 추가 가능', '- 최저 2학기 이수 의미 : 60 ECTS credits 이수에 상응', '- 어학성적 : 기재된 성적은 교환 프로그램 참여를 위한 어학 능력 정도를 의미, 제출 필수 아님.
제출 시 선발에 플러스 요인으로 작용할 수 있음.

- Language Requirement Form : 제출 필수(인하대 국제교류담당자 서명 포함)
※ fact sheet 참조 : www.sdu.dk/sduinternational
', NULL, '- 기한 내 신청 시 숙소 제공 보장(Guaranteed)/Off-campus Housing', '- 기본 생활비 소요
· 숙소비 : € 350~450/월
· 식비 : € 350~400/월
· 대중교통 이용료 : € 50/월
· 기타(교재 구입 등) : € 100~200
- 덴마크로 오기 전 수학기간 전체를 커버하는 보험구입 권장(덴마크에서도 보험 가입 가능하나, 보장 내역 등이 덴마크어로만 제공가능할 수 있음.) ', '2024-2', NULL, NULL), - (213, 73, 2, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공과) 유관 학과로 지원 필요
※ ''Translation/Interpretation/Transcultural communication with German''(전공명)으로의 지원자는 확인 가능한 인하대 관련 교과과정 이수 내역 제출 필요

- 교과목 수강 관련
· 학부 교환학생 : 여러 전공에 걸쳐 수강 가능
· 석/박사 교환학생 : 본인 소속 전공에 한정하여 전공 지원 가능
- 학기당 수강학점 : 최소 24, 최대 30 ECTS credits 이수 권장(실제 이수 기준은 소속 대학(인하대) 기준을 따름.)', NULL, '- 독일어 진행 수업/영어 진행 수업을 들을지에 따라 최소 독일어 B2 수준, 영어 B2 수준의 어학 실력 권고
- 어학성적 유효기간 내 인정', NULL, '- 숙소 제공 미보장(unguaranteed) - 이용 숙소가 대학 소유가 아님.
- 도시 내 이용 가능한 다양한 숙소들이 있으며, 안내된 사이트를 통하여 ASAP 개별적으로 예약 진행 필요', '- 연구실 배정(Lab placements)는 보장되지 않음.
- 보험 가입 : 2개 학기 교환학생에 한하여 의무(비용은 약 € 65)', '2024-2', NULL, NULL), - (214, 74, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공과) 유관 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 16 ECTS credits', NULL, '- 어학성적 유효기간 내 인정(2년 이내 발급본)
- TOEIC으로 어학성적 제출 시, 기본 L&R(듣기, 읽기) 최소 785점, S&W(말하기, 쓰기) 최소 310점 모두 제출 필요', NULL, NULL, '- 대학 자체 기숙사는 없으며, 사이트에 소개된 숙소 개별적 신청/이용. (교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있어 ASAP 신청 필요)
- 더블룸 기준 한달에 약 € 350 (숙소별 차이 有)
- 보험 가입 : 2개 학기 교환학생에 한하여 의무(Austrian National Health Insurance (ÖGK))', '2024-2', NULL, NULL), - (215, 76, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공과) 유관 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 15, 최대 30 ECTS credits', '※ 기타 어학자격 최저 요건 하기 링크 참조
https://media-hp.technikum-wien.at/media/20230309080942/Proof-of-English-Language-Ability.pdf ', '- 유효기간 경과한 어학성적도 인정', NULL, '- 대학 자체 기숙사 없음', '- 보험가입 의무 없음.', '2024-2', NULL, NULL), - (216, 155, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 유관 학과로 지원 필요
- 신청한 전공 내에서 과목 수강 진행 필요
- 학기당 수강학점 : 최소 18 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 교환학생 합격자는 아래 링크를 통해 개별 이용 신청 가능(선착순 배정)', '- 보험가입 의무 없음.', '2024-2', NULL, NULL), - (217, 111, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 신청한 전공 내에서 과목 수강 진행 필요
- 학기당 수강학점 : 최소 15, 최대 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 교환학생 합격자는 링크를 통해 개별 이용 신청', '- 보험가입 의무 없음.', '2024-2', NULL, NULL), - (218, 109, NULL, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 지원전공 내 과목 수강 권고. 타 전공 분야 수강 가능(수강 자격 충족 시)
- 학기당 수강학점 : 30 ECTS credits 이수 권고', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 교환학생 합격자는 링크를 통해 개별 이용 신청', '- 출국 전 자체적 교환 기간 전체를 보장하는 보험 가입 권고', '2024-2', NULL, NULL), - (219, 110, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', ' - 소속전공과 지원전공이 일치해야 함.
- 경영학, 공학 계열 학부생만 지원 가능
- 합격 후 과목 수강은 교환학생 전용 강좌에 한하여 수강신청 가능
- 학기당 수강학점 : 최소 20 ECTS credits', '모집 인원(3명) 구분 : 학부 경영학 계열 2명, 공학 계열 1명', '- 유효기간 경과한 어학성적도 인정', NULL, '- 기본 미제공
- 교환학생의 경우 LOAS를 통해 숙소를 신청 가능', '- 보험 관련 : https://migri.fi/en/insurance ', '2024-2', NULL, NULL), - (220, 79, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 3 ECTS credits', NULL, 'https://www.unive.it/pag/40609/', NULL, '- 기본 미제공(unguaranteed)
- 숙소 관련 지원 : Ca’ Foscari University Housing Office', '- 보험가입 의무
- 보험 관련 : https://www.unive.it/pag/12525/?MP=12525-12518', '2024-2', NULL, NULL), - (221, 77, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', ' - 공학 계열 내 유사 전공으로 지원 가능
- 공학 계열 학생만 지원 가능
- Architecture and Design 학과 과목 수강신청 불가
- 학기당 수강학점 : 최대 40 ECTS credits
- 관련 사이트 : https://www.polimi.it/en/exchange-students-incoming/after-acceptance/exchange-programmes/before-arrival', '- 모집인원 (3명) 구분 : 공학 계열 3명
- 지원 전 권역 담당자와 사전상담요망. 주로 학부보다 석사과정에 영어교과목이 개설된 편', NULL, '- Bachelor : https://www.polimi.it/en/programmes/laurea-equivalent-to-bachelor-of-science
- Master of science : https://www.polimi.it/en/programmes/laurea-magistrale-equivalent-to-master-of-science', '- 기본 미제공(unguaranteed)
- 숙소 정보 및 개별 신청 : https://www.residenze.polimi.it/en', '양교 교류협약에 따라, Bovisa 캠퍼스로만 지원가능', '2024-2', NULL, NULL), - (222, 78, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 12 ECTS credits', '- 지원 전 권역 담당자와 사전상담 요망. 기존 파견자 없어서 후기 자료 없음.', '- 어학성적 유효기간 내 인정(2년 이내 발급본)
- CEFR English B1에 상응하는 어학성적
- 또는 CEFR Italian A1', NULL, '- Single Room : € 350유로/월 수준
- Shared Double : € 290/월 수준', '- 보험가입 의무 : 수학기간 전체를 포함하는 자체 사보험 가입 필요', '2024-2', NULL, NULL), - (223, 156, 1, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 신청한 전공 내에서 대부분 과목 수강 진행 권고 / 타전공 교과는 2개 내로 수강 허용
- 학기당 수강학점 : 최소 10 ECTS credits', '- 최저 이수학기 기타 세부사항
· The Faculty of Law : (학부) 최소 4개 학기 이수, (석사) 최소 1개 학기 이수
· Biology, Chemistry and Environmental Sciences(fields at the Faculty of Science at ELTE)으로는 석, 박사과정생만 지원 가능
· 그 외 학과 최저 이수학기 : 1개 학기

- 대학 학사일정 : https://www.elte.hu/en/academic-calendar ', '- 어학성적 유효기간 내 인정(2년 이내 발급본)
- CEFR English B2에 상응하는 어학성적', NULL, '- 주로 대학 기숙사 이용
- 숙소 관련 지원 : Housing Office(housing@elte.hu)
- 기숙사(2인실) 이용료 : 월 약 HUF 70,000', '- 보험가입 의무 : 대학에서 소개하는 보험 가입 필수', '2024-2', NULL, NULL), - (224, 67, 4, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- Fashion Design or Textile Design 전공 과목 수강 불허
- 학기당 수강학점 : 30 ECTS credits 이수 필요(residency permit에 연동되는 기준)', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 기본 미제공(unguaranteed)', NULL, '2024-2', NULL, NULL), - (225, 66, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고(자격 요건 Admission requirement 확인 필요)
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 30 ECTS credits 이수 필요(residency permit에 연동되는 기준)
- 우측 열 영어강의 리스트 상의 과목 중 일부는 교환학생 수강 가능여부 변동 가능. 노미네이션 완료 후 말뫼 대학에서 최종 과목 리스트 송부 예정', NULL, NULL, NULL, '- 기본 미제공(unguaranteed)', '- 말뫼대학에서 수학하는 학생들은 대학 보험의 적용을 받음.
※ 관련 정보 : https://www.kammarkollegiet.se/engelska/start/all-services/insurance/insurance-for-students-and-foreign-visitors/insurance-for-exchange-students-in-sweden-student-in', '2024-2', NULL, NULL), - (226, 157, NULL, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학부 학생은 대학원 과정 수업 수강 불가
- 학기당 수강학점 : 최소 18 / 최대 36 ECTS credits', NULL, '- 어학성적 유효기간 내 인정
- 기타 유효 어학성적 종류
· Duolingo English Test : 110
· Cambridge B2 First : 170', NULL, '- 기본 미제공(unguaranteed)', NULL, '2024-2', NULL, NULL), - (227, 83, 5, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최대 42 ECTS credits
- 기타 세부사항 Factsheet 참조', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 대학 기숙사 신청 가능하나 신청 경쟁률 높으며, 선착순 배정', NULL, '2024-2', NULL, NULL), - (228, 70, NULL, 3, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학기당 수강학점 : 최소 12 ECTS credits', '영어 진행 과목이 다양하지 않으므로 신중한 지원 요망. 스페인어 공인어학성적(DELE 중급이상 성적)이 있을 시 추후 합격 후 담당자에게 제출 권장', '- 어학성적 유효기간 내 인정', NULL, '- 대학 기숙사 신청 가능하나 신청 경쟁률 높으며, 선착순 배정, 대학 내 기숙사 외 다른 숙소 옵션 선택 가능', NULL, '2024-2', NULL, NULL), - (229, 42, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 유관 학과로 지원 필요
- 정보통신공학 계열(Information and Communication Technology) 과목만 수강 가능
- 학기당 수강학점 : 최대 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 대학 기숙사 신청 가능하나 신청 경쟁률 높으며, 선착순 배정', '- 보험가입 의무', '2024-2', NULL, NULL), - (230, 40, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 유관 학과로 지원 필요
- 교환학생 전용 교과목만 수강 가능
- 학기당 수강학점 : 최대 40 ECTS credits', NULL, '- 어학성적 유효기간 내 인정(2년 이내 발급본)
- 세부내용 링크 참조 : https://www.rug.nl/feb/education/exchange/incoming/before/english-proficiency', NULL, '- 기본 미제공(unguaranteed)', '- 보험가입 의무 : 약 월 €100-120 수준', '2024-2', NULL, NULL), - (231, 41, NULL, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 권고
- 전공별 지원 자격 상이(관련 사이트 통하여 전공별 지원 자격 확인 필요)
- 학기당 수강학점 기준 : 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 기본 미제공(unguaranteed)', '- 보험가입 의무 : 약 €340 수준 (보험료 변동 가능)', '2024-2', NULL, NULL), - (232, 158, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- 합격 후 과목 수강은 여러 전공 분야 걸쳐 수강신청 가능
- 학부-대학원 과목 교차 수강 가능
- 학기당 수강학점 : 최소 20/최대 35 ECTS credits', '최저 이수학기 이수 내역을 증명하는 성적증명서(transcript) 업로드 필요', '- 어학성적 유효기간 내 인정', NULL, '- 대학 기숙사 이용 신청 가능
- 합격자에 한하여 신청 관련 정보 발송
- 기숙사비 €200-240/월 수준', '- 보험가입 의무 : 수학기간 전체를 커버하는 자체 사보험 가입 필요', '2024-2', NULL, NULL), - (233, 43, 2, 2, 'ONE_OR_TWO_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 주전공 혹은 제2전공(혹은 연계전공)과 일치하는 학과로 지원 필요
- Business 계열 과목 이수 가능
- 학기당 수강학점 : 최대 30 ECTS credits', NULL, '- 어학성적 유효기간 내 인정', NULL, '- 숙소 제공 보장(Guaranteed)', '- 보험가입 의무 : 수학기간 전체를 커버하는 자체 사보험 가입 필요', '2024-2', NULL, NULL), - (234, 92, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 경영학계열
- 학기당 최소 15ECTS, 최대 35ECTS 수강해야 함
(Program에 따라 상이함)', '- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월15일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (235, 90, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 기계공학, 전기공학, 전자공학 전공 학생만 지원할 수 있음
- 교차수강 불가능
- 최대 30ECTS 수강', '- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월15일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (236, 91, 3, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 필요 없음
- 교차수강 가능
- 최소 15ECTS, 최대 33ECTS까지 수강', '- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월30일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공
- 외부숙소 배정에 대해 지원', NULL, '2024-2', 2.5, 4), - (237, 96, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차수강 불가능
- 최대 30ECTS 수강', NULL, NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (238, 101, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: IAE School of Management
- 소속전공과 지원전공이 일치하여야 함
- 교차수강 불가능
- 최대 36ECTS 수강', NULL, NULL, NULL, '- 제한된 기숙사 여석
- 1200EUR/1학기', NULL, '2024-2', NULL, NULL), - (239, 95, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 경영학 전공 학생만 지원할 수 있음
-지원 전, 경영학 중 어떤 세부 전공이 지원 불가한지는 지원 예정 학생이 수강 제공 과목 문서를 국제교류팀으로 문의 후 스스로 파악한 후 지원할 것
- 교차수강 불가능
- 최소 15ECTS, 최대 30ECTS까지 수강', '- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월15일까지 유효한 성적표여야 함)
- 최저 성적요건은 프로그램에 따라 상이함(지원전 별도 문의할 것)
', '* 학년에 따라 영어 성적이 상의하니 유의할 것
- 2-3학년: IBT 72/ TOEIC 750/ IELTS 5.5
- 4학년: IBT 83/ TOEIC 790/ IELTS 6.0', NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (240, 103, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-소속전공과 지원전공이 일치하여야 함
-서로 다른 프로그램, 언어, 학과 교차 수강 불가능
', '''- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년5월20일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (241, 86, 2, 7, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 컴퓨터공학 전공 학생만 지원할 수 있음
- 교차 수강 불가
- 학기당 최소 12ECTS, 최대 30ECTS 수강', '- 2024년 12월 31일까지 유효한 어학 성적표여야 함', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (242, 88, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 경영학(Marketing, Finance, Management) 전공 학생만 지원할 수 있음
- 교차 수강 불가
- 학기당 최소 15ECTS, 최대 34ECTS 수강', '- 2024년 12월 31일까지 유효한 어학 성적표여야 함', NULL, NULL, '- 미제공', '-강의는 온라인 및 오프라인 혼합형태로 이루어질 수 있음
-한 달 예측 일반주거비용은 800~2500 USD', '2024-2', 2.5, 4), - (243, 94, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 하고 교차 수강 불가
- 영어로 진행되는 전공수업: Diplomacy and International Relations
- 프랑스어로 진행되는 전공수업: International Relations and Political Science
- 최소 15ECTS, 최대 34ECTS 수강신청 가능', '- 2024년 12월 31일까지 유효한 어학 성적표여야 함', '-IBT minimum score: Reading 13, Writing 21, Listening 13, Speaking 18', NULL, '- 미제공', '-강의는 온라인 및 오프라인 혼합형태로 이루어질 수 있음
-한 달 예측 일반주거비용은 800~2500 USD', '2024-2', 2.5, 4), - (244, 105, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차수강 불가능
- 모든 교환학생은 해당 대학에서 교환학생시 최소 8 ECTS학점(프랑스어 5 ECTS학점, 프랑스문화 3ECTS학점) 이상 이수가 필수임
-2nd year of master law, International MBA 지원 불가 및 수강 불가', '- 2025년 2월 1일까지 유효한 어학 성적표여야 함. ', '- 영어점수는 아래 각 세부점수를 만족해야 함
- TOEFL IBT: 모든 영역 20점 이상
- TOEFL ITP: 모든 영역 50점 이상
- IELTS: 모든 영역에서 6 이상
', NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (245, 93, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
-지원가능전공 : 경영학(본교에서 경영학 관련 기초 과목을 이수한 사람만 지원 가능)
-교차 수강 불가
- 학기당 최소 20ECTS, 최대 30ECTS 수강', NULL, NULL, NULL, '- 기숙사는 미제공. 교외 숙소를 학생 스스로 구해야 함
- 교외숙소 비용은 한달에 대략 250~600€로 다양함', NULL, '2024-2', NULL, NULL), - (246, 102, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: 건축학부 재학생에 함함
- 발드센느대학에서 제공하는 건축학 수업은 이 대학의 석사레벨 (4-5학년) 학생들에게 제공하는 수업 레벨임을 참고할것
- 소속전공과 지원전공이 일치하여야 함
-교차수강불가
- 학기당 최대 30ECTS 수강', NULL, '- 프랑스어 성적 제출이 필수는 아니나 대부분의 과목이 프랑스어로 진행되므로 프랑스어를 사전에 공부할 것을 권고함', NULL, '- 미제공
', NULL, '2024-2', NULL, NULL), - (247, 159, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '-공학 및 과학 전공 학생만 지원 가능
-현재 3, 4학년 학생만 지원 가능
- 학기당 최소 5ECTS, 최대 30ECTS 수강
-소속 전공과 지원 전공이 일치하여야 하고 다른 학년 및 전공 수업 교차수강 불가
-공학 및 과학 계열 관련 기초 과목을 이수한 사람만 지원 가능
-지원 전, 공학 및 과학 중 어떤 세부 전공이 지원 불가한지는 지원 예정 학생이 factsheet 문서를 국제교류팀으로 문의 후 스스로 파악한 후 지원할 것', '- 2024년 10월 30일까지 유효한 어학 성적표여야 함', '-IBT minimum score: Reading 13, Writing 21, Listening 13, Speaking 18', NULL, '- 미제공
- 한 학기에 소요되는 대략적일반주거비용은 1000~2500 USD', NULL, '2024-2', NULL, NULL), - (248, 107, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차수강 불가능
-학기당 최소 5ECTS,최대 40ECTS 수강', '- 2024년 5월 31일까지 유효한 어학 성적표여야 함', NULL, NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (249, 108, 2, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 교차 수강 가능
- 학기당 최소 5ECTS, 최대 30ECTS 수강 가능', '-대부분의 정규과정이 프랑스어로만 진행되므로 해당학교에서 DELF B2성적을 가진 학생이 지원하기를 강력히 추천함', '-DELF B2 성적을 소유하고 있는 학생이 지원하기를 강력히 추천함', NULL, '- 기숙사 있으나 제한적이며 선착순 배정
- 1600-1800EUR/학기당', NULL, '2024-2', NULL, NULL), - (250, 160, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 교차 수강 가능
- 학기당 최소 5ECTS, 최대 30ECTS 수강 가능', '- 2024년 5월 31일까지 유효한 어학 성적표여야 함', '-DELF B2: score 50/100 (minimum)', NULL, '- 미제공', NULL, '2024-2', NULL, NULL), - (251, 99, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 교차수강 가능
- 지원불가능전공: Medicine, Midwifery, Nursing, Physiotherapy, Chiropody,Faculty of Law: 2nd year of Master (Master de Droit), Digital animations and Video gamrs / 2nd year of Master
- 최소 20ECTS 수강', NULL, NULL, NULL, '- 제한된 여석 기숙사, 선착순
- 2300~3000EUR/1학기', NULL, '2024-2', 2.75, 4), - (252, 106, 4, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치하여야 함
- 교차 수강 불가 (학생들은 B2 year 또는 B3 year중에서 수업을 골라야 함
- 3학기 이상 경영학 수업을 들은 사람만 지원 가능
- 학기당 최소 25ECTS 수강', '''- 어학성적표가 해당 외국대학 신청서 제출 시까지 유효하여야 함(2024년4월25일까지 유효한 성적표여야 함)', NULL, NULL, '- 미제공', NULL, '2024-2', 2.5, 4), - (253, 129, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, '※JLPT 성적 소지자만 지원 가능', '영어강의 미제공 ', NULL, '※해당교에서 지정한 보험 가입 필수 (약 2000엔)', '2024-2', NULL, NULL), - (254, 123, NULL, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※일본어 코스 이외에도 정규 코스 수강 가능 (수강 조건 부합시)
※한 학기당 최소 6과목 이상 수강, 최대 24학점까지 수강 가능
※정규 코스 대부분이 일본어수업, 약 30개 정도가 외국어로 강의됨
※정규 코스 신청시, 코디네이터 허가 필요

https://www.dokkyo.ac.jp/english/exchange/calendar/gradingsystem.html', '가을학기 지원 자격: N5, 봄학기 지원 자격: N4 ', '영어강의 미제공 ', '※기숙사 없으나 학교측에서 off-campus 보증
※2023년 기준 매달 58,000~70,000엔', '※2023학년 기준 보험가입 비용 한 학기: 5,400엔 / 1년 8,200엔
※3개월 이상 거주시 국민건강보험 가입 의무 (연간 1만엔)', '2024-2', NULL, NULL), - (255, 128, 1, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※대부분의 교환학생이 국제처 프로그램대로 수업 수강
※JLPT 성적이 높을시 몇몇 학부 수업 수강 가능', NULL, '※대부분 일본어수업으로 진행', '※교환학생들에게 기숙사 미제공', '※대부분의 아파트 계약이 1년 단위이므로, 교환학생일시 아파트 계약 어려움 ', '2024-2', NULL, NULL), - (256, 122, NULL, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※해당 학교 일정상 3월 초까지 서류 제출 필요
※하쿠산 캠퍼스에서만 수강 가능
※수강하고 싶은 과목 요구조건 일치 필요
※비자요건 충족요건으로 매주 최소 7과목 수강', 'TOEIC L&R + S&W = 1530', NULL, NULL, NULL, '2024-2', 2.5, 4), - (257, 124, 6, 1, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', '※해당 학교 일정상 3월 초까지 서류 제출 필요
※최소 6과목 이상 수강
※최대 24학점까지 이수 가능', '※JLPT 성적 소지자만 지원 가능
※학부별로 요구 어학성적기준 상이, 관련페이지 참조
※2021년 9월부터의 영어성적 제출', NULL, NULL, NULL, '2024-2', NULL, NULL), - (258, 125, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '※DEPARTMENT OF GLOBAL ENGLISH, DEPARTMENT OF JAPANESE CULTURE, DEPARTMENT OF MEDIA AND INFORMATION, DEPARTMENT OF PSYCHOLOGY 지원 가능', '※여학생들만 지원 가능', NULL, '※대부분 일본어수업으로 진행', '※기숙사 없음, 계약된 Off-campus 방 존재', NULL, '2024-2', 2.3, 4), - (259, 120, NULL, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '일반일본어강의 수강시 N1 혹은 N2 필요', '※2.30 points 이상 지원 가능
※니가타 대학 학점 환산표 참조', NULL, NULL, NULL, NULL, '2024-2', NULL, NULL), - (260, 127, NULL, 10, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, '※지원 학부/전공별로 자격요건 상이', NULL, NULL, NULL, '2024-2', NULL, NULL), - (261, 131, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'Professional Graduate Program (Law School, Business School)지원불가', '※최소/최대 수강학점은 없으나, 비자조건을 충족하려면 교환학생들은 6과목 이상을 수강하거나 연구시간이 주에 10시간 이상이어야 함 ', NULL, NULL, 'On-campus, Off-campus 둘 다 존재', '기숙사비용 한 학기 27만엔, 두 학기 59만엔 ', '2024-2', 2.5, 4), - (262, 119, NULL, 4, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'A코스 : https://docs.google.com/spreadsheets/d/1jbW2kI1igRHPVvC3d9QoNNIdERbyXtLYqm_RJJ509_I/edit#gid=1999179472

B코스 :
https://docs.google.com/spreadsheets/d/1TAyAhi78Eufz9nbnR7a6_5xvteThT0zC0rBYono3fG4/edit#gid=1847514433', '※A코스(영어)/B 코스(일본어)로 선택 가능
※해당 학교 일정상 3월 초까지 서류 제출 필요
※한 학기당 최소 7과목 수강', '※유효기간 2년 이내의 영어성적 제출
※A코스시, 영어성적이 없는 학생의 경우 추천서 필요
【Letter of Language Competence】
https://docs.google.com/document/d/1nq4mYDT0WN9O1ylpA4WG-sJjU_9MHk-j/edit?usp=drive_link', NULL, '※대학 측에서 지정한 숙소에서 묵어야 함
※입국 전, 한 학기분의 렌트비용 (240,000~300,000엔)을 내야하며, 신용카드로만 결제 가능', NULL, '2024-2', 2.5, 4), - (263, 136, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※12-18 ECTS 수강 가능', '※College of Science에 한해 지원 가능
※지원일까지 유효한 영어성적 제출', NULL, '※기숙사 할당량 제한 ', NULL, '2024-2', NULL, NULL), - (264, 135, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '※학부의 허락 하에 원하는 교과목 수강 가능', '※18-23 ECTS 수강 가능 (4-5 HKSYU courses)', '※2년간 유효한 IELTS 성적 제출', NULL, NULL, NULL, '2024-2', 2.5, 3), - (265, 161, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '※인하대에서의 전공과 같아야함 ', '※학기당 최소 9학점, 최대 15학점까지 수강 가능', '※2년간 유효한 영어성적 제출', NULL, NULL, NULL, '2024-2', NULL, NULL), - (266, 133, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※30학점까지 수강 가능', '※3년간 유효한 영어성적 제출', NULL, '※한 달에 약 25 $-120 $ ', NULL, '2024-2', 3, 4), - (267, 134, NULL, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '※학부 코디네이너의 허가 하에, 다른 학부 수업 수강 가능', '※정규코스는 30학점으로 구성되어있으며, 학기당 최대 36학점까지 수강 가능. 추가수강 희망시 해당교의 허가 필요', 'https://www.ozyegin.edu.tr/en/student-services/application-admission/language-proficiency-requirement', NULL, '※교환학생의 경우 기숙사 보증금 불필요', '※모든 교환학생들은 도착 후 residence permit 신청
※Immigration Office에서의 조건에 부합하는 유효한 건강보험과, 튀르키예에서 가입한 건강보험 둘 다 필요', '2024-2', NULL, NULL), - (268, 162, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '※지도교수의 허락 하에 다른 학부 수업 수강 가능', '※최소 학점 9학점, 최대 21학점 수강 가능
※최소 15학점 수강 권장
※1학기 (8월-12월), 2학기 (1월-5월)

https://www.rsuip.org/for-student/timetable/', NULL, NULL, NULL, '※출국 전 보험가입 필수', '2024-2', 2.5, 4), - (269, 163, 2, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※최소 9UCTS 수강', NULL, NULL, 'On-Campus (4인실): 학기당 1,500-2,000 USD
Off-Campus (1인실): 매달 150 USD', NULL, '2024-2', NULL, NULL), - (270, 114, NULL, 5, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', NULL, '※3월에 성적이 나오므로, 졸업 전 막학기 학생 지원 비권장
※최소 2, 최대 4 SMU course 수강 가능 ', '※영어어학성적 대신 Support letter 접수 가능
※COVID-19의 상황을 고려해, TOEFL iBT® Special Home Edition 혹은 IELTS Indicator 성적 또한 제출 가능
※지원 시점에서 2년간 유효한 영어성적 제출', NULL, 'SMU 할인이 적용된 방 옵션 리스트 제공', '※필수경비(학기당 약 SGD250)에 보험가입비 포함됨 ', '2024-2', NULL, NULL), - (271, 164, NULL, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', NULL, '※영어과목일시 자유롭게 수강 가능
※최소 15학점, 최대 30학점 수강 가능', '※2년간 유효한 영어성적 제출', NULL, '※1년 이하 체류 학생들에게 기숙사 제공 가능
※한 학기당 50-100 EUR', NULL, '2024-2', NULL, NULL), - (272, 115, 2, 3, NULL, 'HOME_UNIVERSITY_PAYMENT', '※전공과 다른 학부 지원 가능', '※ 대학원 학위과정 Master of Science in Computer Science and Data Analytics, Master of Science in Electrical and Power Engineering with George Washington University 수강 불가
※최대 30 ECTS 수강 가능', '※영어어학성적 대신 지도교수로부터 B2 이상의 영어성적을 가지고 있다는 Support letter 로도 대체 가능 ', NULL, '※기숙사를 보유하고 있지 않으나, 학교측에서 교환학생들이 숙소를 찾는 것에 직접적인 도움 제공
※숙소 예약은 학교 측에서 보증되며, 260-400 AZN정도 발생', NULL, '2024-2', 2, 4), - (273, 165, NULL, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '※English Language and Literature, Radio, TV and Cinema, Finance and Banking, Economics, Business Administration, Political Science and Public Administration, International Relations, International Trade and Finance, New Media, Computer Engineering, Industrial Engineering, Industrial Design, Interior Architecture, Civil Engineering, Mechanical Engineering, Architecture 수강 가능', '※최대 36학점까지 수강 가능', '※B2 수준의 영어 혹은 튀르키예어 어학성적 요구
※3년간 유효한 영어성적 제출 ', NULL, '※기숙사 보유, 선착순 신청
※업데이트 된 비용문의 erasmus@beykent.edu.tr', NULL, '2024-2', NULL, NULL), - (274, 139, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능', '- 영어강의의 대부분이 대학원 과정으로, 학부 3/4학년에게만 수강이 가능하도록 해둠. 따라서, 중국어는 잘 하지 못하나 영어에 자신이 있는 경우에는 학부 3/4학년에 지원하길 바람', ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 보유(https://in.ncu.edu.tw/~ncu7221/OSDS/index.php)
- 여실 여부 미확정으로, 여실이 없을 시 도보 5~15분 거리의 오프캠퍼스 숙소 이용 가능', '- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', '70 (경영대 - 80)', 100), - (275, 166, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능', '-', ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 보유 (NTD 10,000/1학기)', '- 보험가입의무(NTD 500/월)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', NULL, NULL), - (276, 167, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능', '-', '- 2022.05 이후 발급된 성적
- 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 보유 (4인실, NTD 18,000/1학기)
- 여실 여부 미확정', '- 보험가입의무(최소 5천만원 이상의 보장성을 지닌 한국여행자보험 가입 필수)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 3, 4.3), - (277, 168, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능
- 교환학생 지원 가능 학과/프로그램
(https://drive.google.com/file/d/1jCGkEjimtykwscl_ZLSzyGlEOtXzhZlq/view?usp=drive_link) _이 외 불가', '- 3, 4학년만 지원가능', '- 특정 어학요건은 없으나, 모든 강의는 영어 혹은 중국어로 이루어짐으로 영어를 우수하게 하는 학생들만 지원 요망
- 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 미보유, 관련내용 확인', '- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 2.44, 4.3), - (278, 138, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능
- 교환학생 지원불가 전공: International Business Bachelor Program, Master of Business Administration Program in International Business, Global Human Resource Management MBA Program, Si-Wan College.', NULL, ' - 본교 중국어 어학시험에 응시하여야 함', NULL, '-기숙사 보유, 홈페이지 하단부 확인
- 거의 기숙사는 보장되나, 확실히 보장된다고 말하기는 어려움. 지원시 기숙사 동시지원필요', '- 보험가입의무(NTD 326/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', NULL, NULL), - (279, 169, '4 - 학부생
1 - 대학원생', 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 교차수강 가능', '- 3, 4학년만 지원가능(학부)', '- 최소 2024년 말일까지 유효한 성적
- 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유
- 여실 여부 미확정으로, 여실이 없을 시 오프캠퍼스 숙소 이용가능', '- 보험가입의무(NTD 3500/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 3, 4.3), - (280, 170, '2 - 학부생
1 - 대학원생', 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차수강 가능', NULL, '- 중어과정의 경우
Science, Engineering, Economics, Management (HSK4, 180)
Humanities, Foreign languages (HSK5, 180)', NULL, '-기숙사 보유
3개월 이상 / 싱글(월 900RMB), 더블(월 600RMB)', '- 보험가입의무(RMB 400/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 75, 100), - (281, 171, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 반드시 일치할 필요는 없음
- 교차수강 불가능
- 북경외대 IBS만 신청가능', NULL, '- 최소 2024년 12월까지 유효한 성적
- 본교 중국어 어학시험에 응시하여야 함', NULL, '- 기숙사 보유(800 - 2,000USD/1학기)
- 선착순, 대략 95%의 신청자가 합격된다고 함.
(Youtube : @ibsbfsu_official)', '- 보험가입의무(USD 60/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', 2.8, 4), - (282, 145, '2 - 학부생
1 - 대학원생', 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치할 것
- 특히 예술대학의 경우 HSK4급 이상 필요
- 교차수강 불가능', NULL, NULL, NULL, '- 기숙사 보유(1인/1침대/1일 33위안)', '- 보험가입의무(RMB 400/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망
- 개인상황에 따라 다르나 한달 생활비 1천-2천 위안 예상', '2024-2', '70 (2.3)', '100 (4.0)'), - (283, 172, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 중국어 어학과정 외 타 과정 수강 불가
- 교환학생은 Chinese in College of Advanced Chinese Training에서 수강', '- Chinese Government Scholarship / Confucius Institute Scholarship 수혜자 지원불가', NULL, NULL, '- 기숙사 보유(더블룸:80RMB/일, 싱글룸:150RMB/일)
※ 지원간 기숙사 신청 미리 필요
- 중국 입국 24시간 내에 입주신고 필요', '- 보험가입의무(RMB 400/학기)
- 강의 리스트 중어/영어 진행여부 확인 요망', '2024-2', NULL, NULL), - (284, 149, NULL, 10, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', NULL, NULL, '- 어학요건이 대학별로 상이하므로 반드시 Program Guide 참고할 것', NULL, NULL, '- 지원 전 반드시 국제교류팀 담당자와 상담할 것', '2024-2', NULL, NULL); \ No newline at end of file +INSERT INTO university_info_for_apply(term, university_id, semester_requirement, student_capacity, + semester_available_for_dispatch, tuition_fee_type, details_for_major, + details_for_apply, details_for_language, details_for_english_course, + details_for_accommodation, details) +VALUES ('2024-1', 1, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, + '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), + ('2024-1', 2, 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, + '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend'), + ('2024-1', 3, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', + NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, + ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000
- 한 학기 등록금: 약 $7,500
- International Program and Service Fees $2,500'), + ('2024-1', 4, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', + NULL, + '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', + NULL, NULL, NULL), + ('2024-1', 5, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', + NULL, + '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', + NULL, NULL, '국제학생 등록금 적용 (학점당 $2,080)'), + ('2024-1', 6, 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', + '- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, + '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 각 영역 최소 5.5 이상
- 외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함 ', NULL, NULL, + '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람
- In-state 등록금 납부
(등록금 관련 정보 : https://www.unisq.edu.au/international/partnerships/study-abroad-exchange/fees-scholarships)'), + ('2024-1', 7, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + '타전공 지원 및 수강 가능
- MECO, CAEL, LAWS unit 수강 여석 제한 있음', + NULL, + '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기/듣기/말하기 17점, 쓰기 19점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', + NULL, NULL, 'OSHC(Overseas Student Health Cover) 국제학생 보험가입 의무 (2023년 기준 AUD 348/학기, 학기마다 비용 상이)'), + ('2024-1', 8, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + '타전공 지원 및 수강 가능
지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, + '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', + NULL, NULL, + '※ 24-1학기에 한하여 ''Destination Australia Cheung Kong Exchange Program Scholarship'' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), + ('2024-1', 9, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함
- 교환학생에게 제공되는 수업만 수강 가능
- Faculty of Engineering 내에서 2/3이상의 수업을 수강하여야 함
- 30 ECTS 수강', + '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 1일)', NULL, NULL, '- 교외 숙소', NULL), + ('2024-1', 10, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + '- 본교 기초과목 이수사항에 따라 지원이 제한될 수 있으나 소속전공과 정확하게 일치 하지 않아도 지원은 가능(연관 전공이어야 함)
- 최소 7.5 ECTS, 최대 30ECTS 수강 가능
- 교차 수강 가능(선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)', + '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', NULL, NULL, '- 제공(학교 운영 기숙사 아님)
- 선착순 배정', NULL), + ('2024-1', 11, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, + '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점
- TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점
외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', + NULL, NULL, NULL), + ('2024-1', 12, 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + '제한학과 많음. (Factsheet참조및Factsheet언급된 제한학과 외에도 학기마다 제한학과 발생가능성있음). 지원 전 권역 담당자랑 사전상담 요망. 학기당 30ECTS수강해야 LA승인남. 성적처리 늦은 편이라 8차 학기 수학자는 성적처리 늦은 거 감안하고 추가 이에 따른 불편함이 있음을 인지후 지원요망. ', + '지원 전 권역 담당자와 사전상담 요망', + '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 듣기 및 쓰기 18점, 읽기 18점, 말하기 20점, 쓰기 18점 이상
- IELTS : 모든 영역에서 6.0이상', + NULL, NULL, '영국 생활비 및 숙소비용 유럽권 지역 중 상대적으로 매우 높은편. 지원전 반드시 사전고려 요망'), + ('2024-1', 13, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', + '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '학교인근 외부 숙소는 있지만, 외부업체운영숙소라 대학관할아님', NULL), + ('2024-1', 14, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', + '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', + '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점 이상, 쓰기 17점 이상, 말하기 20점 이상, 듣기 17점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상
''- TOEIC의 경우 S/W 점수 합산 310점 이상 ', + NULL, + '자체기숙사는 없음. 교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있음(선착순 경우많음). 더블룸 기준약 한달에 € 340 per month (기숙사 종류게 따라 가격 차이 유) 예산잡으면됨.', + NULL), + ('2024-1', 15, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + '- 지원가능전공: History, Philosophy, Art History, theology
(영어과목 수가 그리 많지는 않으므로, 사전 확인필요)
''- 학기당 최소 15ECTS 수강신청해야 함', + '봄학기에는 영어과목이 극히 제한적으로 열린다고 함. 지원 전 권역 담당자와 사전상담 요망', NULL, NULL, '학교에서 몇가지 기숙사 옵션 합격시 연결예정.', NULL), + ('2024-1', 16, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', + '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '기숙사없음', NULL), + ('2024-1', 17, 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + '- 소속전공과 지원전공이 일치 또는 유사하여야 함 : 전공이 제한적이므로 반드시 홈페이지에서 지원 가능 전공을 확인할 것
- 최대 30ECTS 수강', + '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', NULL, NULL, '- 미제공', NULL), + ('2024-1', 18, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', + '*해당 학교 일정 상 10월초까지 서류제출 필요', '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, NULL), + ('2024-1', 19, 2, 1, 'IRRELEVANT', NULL, + '교환학생 지원가능 : Department of Global English, Department of Japanese culture, Department of Media and Information, Department of Psychology.', + '여학생만 신청가능', NULL, NULL, + '기숙사 없음, 계약된 외부 기숙사 사용-“Maison de Claire Ibaraki” 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)', NULL), + ('2024-1', 20, 2, 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, + '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월', NULL); \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java new file mode 100644 index 000000000..7d9849cbd --- /dev/null +++ b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.database; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2, replace = AutoConfigureTestDatabase.Replace.ANY) +@ActiveProfiles("test") +@DataJpaTest +class DatabaseConnectionTest { + + @Autowired + private JdbcTemplate jdbcTemplate; + + private DatabaseMetaData metaData; + + @DisplayName("데이터베이스 연결 및 테이블 존재 여부 테스트") + @Test + void connectDatabaseAndCheckTables() { + assertThatCode(() -> metaData = Objects.requireNonNull(jdbcTemplate.getDataSource()) + .getConnection() + .getMetaData()) + .doesNotThrowAnyException(); + + assertAll( + () -> assertThat(isTableExist("SITE_USER")).isTrue(), + () -> assertThat(isTableExist("COUNTRY")).isTrue(), + () -> assertThat(isTableExist("INTERESTED_COUNTRY")).isTrue(), + () -> assertThat(isTableExist("REGION")).isTrue(), + () -> assertThat(isTableExist("INTERESTED_REGION")).isTrue(), + () -> assertThat(isTableExist("LANGUAGE_REQUIREMENT")).isTrue(), + () -> assertThat(isTableExist("UNIVERSITY")).isTrue(), + () -> assertThat(isTableExist("LIKED_UNIVERSITY")).isTrue(), + () -> assertThat(isTableExist("UNIVERSITY_INFO_FOR_APPLY")).isTrue() + ); + } + + private boolean isTableExist(String tableName) throws SQLException { + return metaData.getTables(null, null, tableName, null).next(); + } +} diff --git a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java new file mode 100644 index 000000000..6a7637ed5 --- /dev/null +++ b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.database; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class RedisConnectionTest { + + @Autowired + private RedisTemplate redisTemplate; + + @DisplayName("레디스 연결 및 작동 테스트") + @Test + void connectRedis() { + String key = "test-key"; + String expectedValue = "test-value"; + + redisTemplate.opsForValue().set(key, expectedValue); + String actualValue = redisTemplate.opsForValue().get(key); + + assertThat(actualValue).isEqualTo(expectedValue); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java new file mode 100644 index 000000000..6ad77f58b --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -0,0 +1,187 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.dto.ApplicantResponse; +import com.example.solidconnection.application.dto.ApplicationsResponse; +import com.example.solidconnection.application.dto.UniversityApplicantsResponse; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.VerifyStatus; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static com.example.solidconnection.e2e.DynamicFixture.createDummyGpa; +import static com.example.solidconnection.e2e.DynamicFixture.createDummyLanguageTest; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지원자 조회 테스트") +class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { + + @Autowired + SiteUserRepository siteUserRepository; + + @Autowired + ApplicationRepository applicationRepository; + + @Autowired + TokenService tokenService; + + private String accessToken; + private Application 나의_지원정보; + private Application 사용자1_지원정보; + private Application 사용자2_지원정보; + private Application 사용자3_지원정보; + + @BeforeEach + public void setUpUserAndToken() { + // setUp - 회원 정보 저장 + String email = "email@email.com"; + SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + + // setUp - 사용자 정보 저장 + SiteUser 사용자1 = siteUserRepository.save(createSiteUserByEmail("email1")); + SiteUser 사용자2 = siteUserRepository.save(createSiteUserByEmail("email2")); + SiteUser 사용자3 = siteUserRepository.save(createSiteUserByEmail("email3")); + + // setUp - 지원 정보 저장 + Gpa gpa = createDummyGpa(); + LanguageTest languageTest = createDummyLanguageTest(); + 나의_지원정보 = new Application(siteUser, gpa, languageTest); + 사용자1_지원정보 = new Application(사용자1, gpa, languageTest); + 사용자2_지원정보 = new Application(사용자2, gpa, languageTest); + 사용자3_지원정보 = new Application(사용자3, gpa, languageTest); + 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "0"); + 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "1"); + 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, "2"); + 사용자3_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, "3"); + 나의_지원정보.setVerifyStatus(VerifyStatus.APPROVED); + 사용자1_지원정보.setVerifyStatus(VerifyStatus.APPROVED); + 사용자2_지원정보.setVerifyStatus(VerifyStatus.APPROVED); + 사용자3_지원정보.setVerifyStatus(VerifyStatus.APPROVED); + applicationRepository.saveAll(List.of(나의_지원정보, 사용자1_지원정보, 사용자2_지원정보, 사용자3_지원정보)); + } + + @Test + void 전체_지원자를_조회한다() { + ApplicationsResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().log().all() + .get("/application") + .then().log().all() + .statusCode(200) + .extract().as(ApplicationsResponse.class); + + List firstChoiceApplicants = response.firstChoice(); + List secondChoiceApplicants = response.secondChoice(); + + assertThat(firstChoiceApplicants).containsAnyElementsOf(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(사용자1_지원정보, false))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true))), + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(사용자2_지원정보, false))), + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(사용자3_지원정보, false))) + )); + assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(사용자1_지원정보, false))), + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(사용자3_지원정보, false))), + UniversityApplicantsResponse.of(그라츠대학_지원_정보, + List.of(ApplicantResponse.of(사용자2_지원정보, false))) + )); + } + + @Test + void 지역으로_필터링해서_지원자를_조회한다() { + ApplicationsResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().log().all() + .get("/application?region=" + 영미권.getCode()) + .then().log().all() + .statusCode(200) + .extract().as(ApplicationsResponse.class); + + List firstChoiceApplicants = response.firstChoice(); + List secondChoiceApplicants = response.secondChoice(); + + assertThat(firstChoiceApplicants).containsAnyElementsOf(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(사용자1_지원정보, false))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true))), + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(사용자3_지원정보, false))))); + assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(사용자1_지원정보, false))))); + } + + @Test + void 대학_국문_이름으로_필터링해서_지원자를_조회한다() { + ApplicationsResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().log().all() + .get("/application?keyword=라") + .then().log().all() + .statusCode(200) + .extract().as(ApplicationsResponse.class); + + List firstChoiceApplicants = response.firstChoice(); + List secondChoiceApplicants = response.secondChoice(); + + assertThat(firstChoiceApplicants).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(그라츠대학_지원_정보, List.of()), + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, List.of()), + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(사용자3_지원정보, false)))); + assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of( + UniversityApplicantsResponse.of(그라츠대학_지원_정보, + List.of(ApplicantResponse.of(사용자2_지원정보, false))), + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(사용자3_지원정보, false))), + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, List.of()))); + } + + @Test + void 국가_국문_이름으로_필터링해서_지원자를_조회한다() { + ApplicationsResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().log().all() + .get("/application?keyword=일본") + .then().log().all() + .statusCode(200) + .extract().as(ApplicationsResponse.class); + + List firstChoiceApplicants = response.firstChoice(); + List secondChoiceApplicants = response.secondChoice(); + + assertThat(firstChoiceApplicants).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(사용자2_지원정보, false)))); + assertThat(secondChoiceApplicants).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(메이지대학_지원_정보, List.of())); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java new file mode 100644 index 000000000..f5443f805 --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java @@ -0,0 +1,218 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.dto.ScoreRequest; +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.custom.response.ErrorResponse; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.VerifyStatus; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; +import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("지원 정보 제출 테스트") +class ApplicationSubmissionTest extends UniversityDataSetUpEndToEndTest { + + @Autowired + private ApplicationRepository applicationRepository; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private TokenService tokenService; + + private final String email = "email@email.com"; + private String accessToken; + private SiteUser siteUser; + + @BeforeEach + public void setUpUserAndToken() { + // setUp - 회원 정보 저장 + siteUser = siteUserRepository.save(createSiteUserByEmail(email)); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + } + + @Test + void 대학교_성적과_어학성적을_처음으로_제출한다() { + // request - body 생성 및 요청 + ScoreRequest request = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", + "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); + RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .log().all() + .post("/application/score") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + + Application application = applicationRepository.getApplicationBySiteUser(siteUser); + assertAll("대학교 성적과 어학 성적을 저장한다.", + () -> assertThat(application.getId()).isNotNull(), + () -> assertThat(application.getSiteUser().getId()).isEqualTo(siteUser.getId()), + () -> assertThat(application.getLanguageTest().getLanguageTestType()).isEqualTo(request.languageTestType()), + () -> assertThat(application.getLanguageTest().getLanguageTestScore()).isEqualTo(request.languageTestScore()), + () -> assertThat(application.getLanguageTest().getLanguageTestReportUrl()).isEqualTo(request.languageTestReportUrl()), + () -> assertThat(application.getGpa().getGpa()).isEqualTo(request.gpa()), + () -> assertThat(application.getGpa().getGpaReportUrl()).isEqualTo(request.gpaReportUrl()), + () -> assertThat(application.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), + () -> assertThat(application.getUpdateCount()).isZero()); + } + + @Test + void 대학교_성적과_어학성적을_다시_제출한다() { + // setUp - 성적 정보 저장 + ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", + "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); + applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())); + + // request - body 생성 및 요청 + ScoreRequest secondRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "90", + "languageTestReportUrl", 4.1, 4.5, "gpaReportUrl"); + RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(secondRequest) + .contentType("application/json") + .log().all() + .post("/application/score") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + + Application updatedApplication = applicationRepository.getApplicationBySiteUser(siteUser); + assertAll("대학교 성적과 어학 성적을 수정한다. 이때 수정 횟수는 증가하지 않고, 성적 승인 상태는 PENDING 으로 바뀐다.", + () -> assertThat(updatedApplication.getId()).isNotNull(), + () -> assertThat(updatedApplication.getSiteUser().getId()).isEqualTo(siteUser.getId()), + () -> assertThat(updatedApplication.getLanguageTest().getLanguageTestType()).isEqualTo(secondRequest.languageTestType()), + () -> assertThat(updatedApplication.getLanguageTest().getLanguageTestScore()).isEqualTo(secondRequest.languageTestScore()), + () -> assertThat(updatedApplication.getLanguageTest().getLanguageTestReportUrl()).isEqualTo(secondRequest.languageTestReportUrl()), + () -> assertThat(updatedApplication.getGpa().getGpa()).isEqualTo(secondRequest.gpa()), + () -> assertThat(updatedApplication.getGpa().getGpaReportUrl()).isEqualTo(secondRequest.gpaReportUrl()), + () -> assertThat(updatedApplication.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), + () -> assertThat(updatedApplication.getUpdateCount()).isZero()); + } + + @Test + void 성적_제출_후_지망_대학을_제출한다() { + // setUp - 성적 정보 저장 + ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", + "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); + applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())); + + // request - body 생성 및 요청 + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .log().all() + .post("/application/university") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + + Application application = applicationRepository.getApplicationBySiteUser(siteUser); + assertAll("지망 대학교를 저장한다.", + () -> assertThat(application.getId()).isNotNull(), + () -> assertThat(application.getSiteUser().getId()).isEqualTo(siteUser.getId()), + () -> assertThat(application.getFirstChoiceUniversity().getId()).isEqualTo(request.firstChoiceUniversityId()), + () -> assertThat(application.getSecondChoiceUniversity().getId()).isEqualTo(request.secondChoiceUniversityId()), + () -> assertThat(application.getNicknameForApply()).isNotNull(), + () -> assertThat(application.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), + () -> assertThat(application.getUpdateCount()).isZero()); + } + + @Test + void 지망_대학을_수정한다() { + // setUp - 성적 정보와 지망 대학 저장 + ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", + "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); + applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())) + .updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "nickname"); + Application initialApplication = applicationRepository.getApplicationBySiteUser(siteUser); + + // request - body 생성 및 요청 + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .log().all() + .post("/application/university") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + + Application updatedApplication = applicationRepository.getApplicationBySiteUser(siteUser); + assertAll("지망 대학교를 수정한다. 이때 수정 횟수는 증가하고, 성적 승인 상태는 바뀌지 않는다.", + () -> assertThat(updatedApplication.getId()).isNotNull(), + () -> assertThat(updatedApplication.getSiteUser().getId()).isEqualTo(siteUser.getId()), + () -> assertThat(updatedApplication.getFirstChoiceUniversity().getId()).isEqualTo(request.firstChoiceUniversityId()), + () -> assertThat(updatedApplication.getSecondChoiceUniversity().getId()).isEqualTo(request.secondChoiceUniversityId()), + () -> assertThat(updatedApplication.getNicknameForApply()).isNotNull(), + () -> assertThat(updatedApplication.getVerifyStatus()).isEqualTo(initialApplication.getVerifyStatus()), + () -> assertThat(updatedApplication.getUpdateCount()).isEqualTo(initialApplication.getUpdateCount())); + } + + @Test + void 지망_대학을_최대_수정_가능_횟수보다_더_수정하려고하면_예외_응답을_반환한다() { + // setUp - 성적 정보와 지망 대학 저장 + ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", + "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); + applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())); + Application initialApplication = applicationRepository.getApplicationBySiteUser(siteUser); + + // setUp - 지망 대학을 한계까지 수정 + for (int i = 0; i <= APPLICATION_UPDATE_COUNT_LIMIT; i++) { + initialApplication.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "nickname"); + applicationRepository.save(initialApplication); + } + + // request - body 생성 및 요청 + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + ErrorResponse errorResponse = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .post("/application/university") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()).isEqualTo(APPLY_UPDATE_LIMIT_EXCEED.getMessage()); + } + + @Test + void 일지망_대학과_이지망_대학이_같으면_예외_응답을_반환한다() { + // request - body 생성 및 요청 + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 그라츠대학_지원_정보.getId()); + ErrorResponse errorResponse = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .log().all() + .post("/application/university") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()).isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java b/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java new file mode 100644 index 000000000..9b23d230e --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.support.DatabaseClearExtension; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; + +@ExtendWith(DatabaseClearExtension.class) +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +abstract class BaseEndToEndTest { + + @LocalServerPort + private int port; + + @BeforeEach + public void setUp() { + RestAssured.port = port; + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java new file mode 100644 index 000000000..43247b1d9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java @@ -0,0 +1,91 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.type.SemesterAvailableForDispatch; +import com.example.solidconnection.type.TuitionFeeType; +import com.example.solidconnection.university.domain.LanguageRequirement; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; + +import java.util.Set; + +public class DynamicFixture { + + public static SiteUser createSiteUserByEmail(String email) { + return new SiteUser( + email, + "nickname", + "profileImage", + "2000-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE + ); + } + + public static SiteUser createSiteUserByNickName(String nickname) { + return new SiteUser( + "email@email.com", + nickname, + "profileImage", + "2000-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE + ); + } + + public static KakaoUserInfoDto createKakaoUserInfoDtoByEmail(String email) { + return new KakaoUserInfoDto( + new KakaoUserInfoDto.KakaoAccountDto( + new KakaoUserInfoDto.KakaoAccountDto.KakaoProfileDto( + "nickname", + "profileImageUrl" + ), + email + ) + ); + } + + public static UniversityInfoForApply createUniversityForApply( + String term, University university, Set languageRequirements) { + return new UniversityInfoForApply( + null, + term, + 1, + TuitionFeeType.HOME_UNIVERSITY_PAYMENT, + SemesterAvailableForDispatch.ONE_SEMESTER, + "1", + "detailsForLanguage", + "gpaRequirement", + "gpaRequirementCriteria", + "detailsForApply", + "detailsForMajor", + "detailsForAccommodation", + "detailsForEnglishCourse", + "details", + languageRequirements, + university); + } + + public static LikedUniversity createLikedUniversity( + SiteUser siteUser, UniversityInfoForApply universityInfoForApply) { + return new LikedUniversity(null, universityInfoForApply, siteUser); + } + + public static Gpa createDummyGpa() { + return new Gpa(3.5, 4.0, "gpaReportUrl"); + } + + public static LanguageTest createDummyLanguageTest() { + return new LanguageTest(LanguageTestType.TOEIC, "900", "toeicReportUrl"); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java new file mode 100644 index 000000000..953fddf8b --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -0,0 +1,57 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("마이페이지 테스트") +class MyPageTest extends BaseEndToEndTest { + + private final String email = "email@email.com"; + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private TokenService tokenService; + private String accessToken; + + @BeforeEach + public void setUpUserAndToken() { + // setUp - 회원 정보 저장 + siteUserRepository.save(createSiteUserByEmail(email)); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + } + + @Test + void 마이페이지_정보를_조회한다() { + // request - 요청 + MyPageResponse myPageResponse = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .get("/my-page") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(MyPageResponse.class); + + SiteUser savedSiteUser = siteUserRepository.getByEmail(email); + assertAll("불러온 마이 페이지 정보가 DB의 정보와 일치한다.", + () -> assertThat(myPageResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), + () -> assertThat(myPageResponse.birth()).isEqualTo(savedSiteUser.getBirth()), + () -> assertThat(myPageResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl())); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java new file mode 100644 index 000000000..fa515e4f2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -0,0 +1,138 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.custom.response.ErrorResponse; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageUpdateRequest; +import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static com.example.solidconnection.siteuser.service.SiteUserService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("마이페이지 수정 테스트") +class MyPageUpdateTest extends BaseEndToEndTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private TokenService tokenService; + + private String accessToken; + + private SiteUser siteUser; + + private final String email = "email@email.com"; + + @BeforeEach + public void setUpUserAndToken() { + // setUp - 회원 정보 저장 + siteUser = createSiteUserByEmail(email); + siteUserRepository.save(siteUser); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + } + + @Test + void 수정을_위해_수정_전_정보를_조회한다() { + // request - 요청 + MyPageUpdateResponse myPageUpdateResponse = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .get("/my-page/update") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(MyPageUpdateResponse.class); + + SiteUser savedSiteUser = siteUserRepository.getByEmail(email); + assertAll("불러온 마이 페이지 정보가 DB의 정보와 일치한다.", + () -> assertThat(myPageUpdateResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), + () -> assertThat(myPageUpdateResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl())); + } + + @Test + void 마이_페이지_정보를_수정한다() { + // request - body 생성 및 요청 + MyPageUpdateRequest myPageUpdateRequest = new MyPageUpdateRequest("newNickname", "newProfileImageUrl"); + MyPageUpdateResponse myPageUpdateResponse = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .body(myPageUpdateRequest) + .contentType("application/json") + .patch("/my-page/update") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(MyPageUpdateResponse.class); + + SiteUser savedSiteUser = siteUserRepository.getByEmail(email); + assertAll("마이 페이지 정보가 수정된다.", + () -> assertThat(myPageUpdateResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), + () -> assertThat(myPageUpdateResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl())); + } + + @Test + void 마이_페이지_정보를_수정할_때_닉네임이_중복된다면_예외_응답을_반환한다() { + // setUp - 같은 닉네임을 갖는 다른 회원 정보 저장 + SiteUser existUser = createSiteUserByEmail("existUser"); + String duplicateNickname = "duplicateNickname"; + existUser.setNickname(duplicateNickname); + siteUserRepository.save(existUser); + + // request - body 생성 및 요청 + MyPageUpdateRequest myPageUpdateRequest = new MyPageUpdateRequest(duplicateNickname, "newProfileImageUrl"); + ErrorResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .body(myPageUpdateRequest) + .contentType("application/json") + .patch("/my-page/update") + .then().log().all() + .statusCode(HttpStatus.CONFLICT.value()) + .extract().as(ErrorResponse.class); + + assertThat(response.message()) + .isEqualTo(NICKNAME_ALREADY_EXISTED.getMessage()); + } + + @Test + void 마이_페이지_정보를_수정할_때_닉네임_변경_가능_기한이_지나지않았다면_예외_응답을_반환한다() { + // setUp - 회원 정보 저장 (닉네임 변경 가능 시간이 되기 1분 전) + LocalDateTime nicknameModifiedAt = LocalDateTime.now() + .minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES) + .plusMinutes(1); + siteUser.setNicknameModifiedAt(nicknameModifiedAt); + siteUserRepository.save(siteUser); + + // request - body 생성 및 요청 + MyPageUpdateRequest myPageUpdateRequest = new MyPageUpdateRequest("newNickname", "newProfileImageUrl"); + ErrorResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .body(myPageUpdateRequest) + .contentType("application/json") + .patch("/my-page/update") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .extract().as(ErrorResponse.class); + + assertThat(response.message()) + .contains(CAN_NOT_CHANGE_NICKNAME_YET.getMessage()); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java new file mode 100644 index 000000000..8f1bd1018 --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -0,0 +1,135 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.auth.client.KakaoOAuthClient; +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; +import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; +import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; + +import java.time.LocalDate; + +import static com.example.solidconnection.e2e.DynamicFixture.createKakaoUserInfoDtoByEmail; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static com.example.solidconnection.scheduler.UserRemovalScheduler.ACCOUNT_RECOVER_DURATION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.BDDMockito.given; + +@DisplayName("카카오 로그인 테스트") +class SignInTest extends BaseEndToEndTest { + + @Autowired + SiteUserRepository siteUserRepository; + + @Autowired + RedisTemplate redisTemplate; + + @MockBean + KakaoOAuthClient kakaoOAuthClient; + + @Test + void 신규_회원이_카카오로_로그인한다() { + // stub - kakaoOAuthClient 가 정해진 사용자 프로필 정보를 반환하도록 + String kakaoCode = "kakaoCode"; + String email = "email@email.com"; + KakaoUserInfoDto kakaoUserInfoDto = createKakaoUserInfoDtoByEmail(email); + given(kakaoOAuthClient.processOauth(kakaoCode)) + .willReturn(kakaoUserInfoDto); + + // request - body 생성 및 요청 + KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); + FirstAccessResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(kakaoCodeRequest) + .when().post("/auth/kakao") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(FirstAccessResponse.class); + + KakaoUserInfoDto.KakaoAccountDto.KakaoProfileDto kakaoProfileDto = kakaoUserInfoDto.kakaoAccountDto().profile(); + assertAll("카카오톡 사용자 정보를 응답한다.", + () -> assertThat(response.isRegistered()).isFalse(), + () -> assertThat(response.email()).isEqualTo(email), + () -> assertThat(response.nickname()).isEqualTo(kakaoProfileDto.nickname()), + () -> assertThat(response.profileImageUrl()).isEqualTo(kakaoProfileDto.profileImageUrl()), + () -> assertThat(response.kakaoOauthToken()).isNotNull()); + assertThat(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email))) + .as("카카오 인증 토큰을 저장한다.") + .isEqualTo(response.kakaoOauthToken()); + } + + @Test + void 기존_회원이_카카오로_로그인한다() { + // stub - kakaoOAuthClient 가 정해진 사용자 프로필 정보를 반환하도록 + String kakaoCode = "kakaoCode"; + String email = "email@email.com"; + given(kakaoOAuthClient.processOauth(kakaoCode)) + .willReturn(createKakaoUserInfoDtoByEmail(email)); + + // setUp - 사용자 정보 저장 + siteUserRepository.save(createSiteUserByEmail(email)); + + // request - body 생성 및 요청 + KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); + SignInResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(kakaoCodeRequest) + .when().post("/auth/kakao") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(SignInResponse.class); + + assertAll("리프레스 토큰과 엑세스 토큰을 응답한다.", + () -> assertThat(response.isRegistered()).isTrue(), + () -> assertThat(response.accessToken()).isNotNull(), + () -> assertThat(response.refreshToken()).isNotNull()); + assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + .as("리프레시 토큰을 저장한다.") + .isEqualTo(response.refreshToken()); + } + + @Test + void 탈퇴한_회원이_계정_복구_기간_안에_다시_로그인하면_탈퇴가_무효화된다() { + // stub - kakaoOAuthClient 가 정해진 사용자 프로필 정보를 반환하도록 + String kakaoCode = "kakaoCode"; + String email = "email@email.com"; + given(kakaoOAuthClient.processOauth(kakaoCode)) + .willReturn(createKakaoUserInfoDtoByEmail(email)); + + // setUp - 계정 복구 기간이 되지 않은 사용자 저장 + SiteUser siteUserFixture = createSiteUserByEmail(email); + LocalDate justBeforeRemoval = LocalDate.now().minusDays(ACCOUNT_RECOVER_DURATION - 1); + siteUserFixture.setQuitedAt(justBeforeRemoval); + siteUserRepository.save(siteUserFixture); + + // request - body 생성 및 요청 + KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); + SignInResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(kakaoCodeRequest) + .when().post("/auth/kakao") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(SignInResponse.class); + + assertAll("리프레스 토큰과 엑세스 토큰을 응답하고, 탈퇴 날짜를 초기화한다.", + () -> assertThat(response.isRegistered()).isTrue(), + () -> assertThat(response.accessToken()).isNotNull(), + () -> assertThat(response.refreshToken()).isNotNull(), + () -> assertThat(siteUserRepository.getByEmail(email).getQuitedAt()).isNull()); + assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + .as("리프레시 토큰을 저장한다.") + .isEqualTo(response.refreshToken()); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java new file mode 100644 index 000000000..7472616f6 --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -0,0 +1,185 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.auth.dto.SignUpResponse; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.custom.response.ErrorResponse; +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.InterestedCountry; +import com.example.solidconnection.entity.InterestedRegion; +import com.example.solidconnection.entity.Region; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByNickName; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("회원가입 테스트") +class SignUpTest extends BaseEndToEndTest { + + @Autowired + SiteUserRepository siteUserRepository; + + @Autowired + CountryRepository countryRepository; + + @Autowired + RegionRepository regionRepository; + + @Autowired + InterestedRegionRepository interestedRegionRepository; + + @Autowired + InterestedCountyRepository interestedCountyRepository; + + @Autowired + TokenService tokenService; + + @Autowired + RedisTemplate redisTemplate; + + @Test + void 유효한_카카오_토큰으로_회원가입한다() { + // setup - 국가, 지역 정보 저장 + Region region = regionRepository.save(new Region("EROUPE", "유럽")); + List countries = countryRepository.saveAll(List.of( + new Country("FR", "프랑스", region), + new Country("DE", "독일", region))); + + // setup - 카카오 토큰 발급 + String email = "email@email.com"; + String generatedKakaoToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); + tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + + // request - body 생성 및 요청 + List interestedRegionNames = List.of("유럽"); + List interestedCountryNames = List.of("프랑스", "독일"); + SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, interestedRegionNames, interestedCountryNames, + PreparationStatus.CONSIDERING, "nickname", "profile", Gender.FEMALE, "2000-01-01"); + SignUpResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(signUpRequest) + .when().post("/auth/sign-up") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(SignUpResponse.class); + + SiteUser savedSiteUser = siteUserRepository.getByEmail(email); + assertAll( + "회원 정보를 저장한다.", + () -> assertThat(savedSiteUser.getId()).isNotNull(), + () -> assertThat(savedSiteUser.getEmail()).isEqualTo(email), + () -> assertThat(savedSiteUser.getBirth()).isEqualTo(signUpRequest.birth()), + () -> assertThat(savedSiteUser.getNickname()).isEqualTo(signUpRequest.nickname()), + () -> assertThat(savedSiteUser.getProfileImageUrl()).isEqualTo(signUpRequest.profileImageUrl()), + () -> assertThat(savedSiteUser.getGender()).isEqualTo(signUpRequest.gender()), + () -> assertThat(savedSiteUser.getPreparationStage()).isEqualTo(signUpRequest.preparationStatus())); + + List interestedRegions = interestedRegionRepository.findAllBySiteUser(savedSiteUser).stream() + .map(InterestedRegion::getRegion) + .toList(); + List interestedCountries = interestedCountyRepository.findAllBySiteUser(savedSiteUser).stream() + .map(InterestedCountry::getCountry) + .toList(); + assertAll( + "관심 지역과 나라 정보를 저장한다.", + () -> assertThat(interestedRegions).containsExactlyInAnyOrder(region), + () -> assertThat(interestedCountries).containsExactlyElementsOf(countries) + ); + + assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + .as("리프레시 토큰을 저장한다.") + .isEqualTo(response.refreshToken()); + } + + @Test + void 이미_있는_닉네임으로_회원가입하면_예외를_응답한다() { + // setup - 회원 정보 저장 + String alreadyExistNickname = "nickname"; + SiteUser alreadyExistUser = createSiteUserByNickName(alreadyExistNickname); + siteUserRepository.save(alreadyExistUser); + + // setup - 카카오 토큰 발급 + String email = "email@email.com"; + String generatedKakaoToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); + tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + + // request - body 생성 및 요청 + SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, + PreparationStatus.CONSIDERING, alreadyExistNickname, "profile", Gender.FEMALE, "2000-01-01"); + ErrorResponse errorResponse = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(signUpRequest) + .when().post("/auth/sign-up") + .then().log().all() + .statusCode(HttpStatus.CONFLICT.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()) + .isEqualTo(NICKNAME_ALREADY_EXISTED.getMessage()); + } + + @Test + void 이미_있는_이메일로_회원가입하면_예외를_응답한다() { + // setup - 회원 정보 저장 + String alreadyExistEmail = "email@email.com"; + SiteUser alreadyExistUser = createSiteUserByEmail(alreadyExistEmail); + siteUserRepository.save(alreadyExistUser); + + // setup - 카카오 토큰 발급 + String generatedKakaoToken = tokenService.generateToken(alreadyExistEmail, TokenType.KAKAO_OAUTH); + tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + + // request - body 생성 및 요청 + SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, + PreparationStatus.CONSIDERING, "nickname0", "profile", Gender.FEMALE, "2000-01-01"); + ErrorResponse errorResponse = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(signUpRequest) + .when().post("/auth/sign-up") + .then().log().all() + .statusCode(HttpStatus.CONFLICT.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()) + .isEqualTo(USER_ALREADY_EXISTED.getMessage()); + } + + @Test + void 유효하지_않은_카카오_토큰으로_회원가입을_하면_예외를_응답한다() { + SignUpRequest signUpRequest = new SignUpRequest("invalid", null, null, + PreparationStatus.CONSIDERING, "nickname", "profile", Gender.FEMALE, "2000-01-01"); + + ErrorResponse errorResponse = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(signUpRequest) + .when().post("/auth/sign-up") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()) + .contains(JWT_EXCEPTION.getMessage()); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java new file mode 100644 index 000000000..24380dd81 --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java @@ -0,0 +1,301 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.Region; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.support.DatabaseClearExtension; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.domain.LanguageRequirement; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.repository.LanguageRequirementRepository; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.UniversityRepository; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; + +import java.util.HashSet; + +import static com.example.solidconnection.type.SemesterAvailableForDispatch.ONE_SEMESTER; +import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; + +@ExtendWith(DatabaseClearExtension.class) +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +abstract class UniversityDataSetUpEndToEndTest { + + public static Region 영미권; + public static Region 유럽; + public static Region 아시아; + public static Country 미국; + public static Country 캐나다; + public static Country 덴마크; + public static Country 오스트리아; + public static Country 일본; + + public static University 영미권_미국_괌대학_A; + public static University 영미권_미국_괌대학_B; + public static University 영미권_미국_네바다주립대학_라스베이거스; + public static University 영미권_캐나다_메모리얼대학_세인트존스_A; + public static University 유럽_덴마크_서던덴마크대학교; + public static University 유럽_덴마크_코펜하겐IT대학; + public static University 유럽_오스트리아_그라츠대학; + public static University 유럽_오스트리아_그라츠공과대학; + public static University 유럽_오스트리아_린츠_카톨릭대학; + public static University 아시아_일본_메이지대학; + + public static UniversityInfoForApply 괌대학_A_지원_정보; + public static UniversityInfoForApply 괌대학_B_지원_정보; + public static UniversityInfoForApply 네바다주립대학_라스베이거스_지원_정보; + public static UniversityInfoForApply 메모리얼대학_세인트존스_A_지원_정보; + public static UniversityInfoForApply 서던덴마크대학교_지원_정보; + public static UniversityInfoForApply 코펜하겐IT대학_지원_정보; + public static UniversityInfoForApply 그라츠대학_지원_정보; + public static UniversityInfoForApply 그라츠공과대학_지원_정보; + public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; + public static UniversityInfoForApply 메이지대학_지원_정보; + + @Value("${university.term}") + public String term; + + @LocalServerPort + private int port; + + @Autowired + private RegionRepository regionRepository; + + @Autowired + private CountryRepository countryRepository; + + @Autowired + private UniversityRepository universityRepository; + + @Autowired + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Autowired + private LanguageRequirementRepository languageRequirementRepository; + + @BeforeEach + public void setUpBasicData() { + RestAssured.port = port; + + 영미권 = regionRepository.save(new Region("AMERICAS", "영미권")); + 유럽 = regionRepository.save(new Region("EUROPE", "유럽")); + 아시아 = regionRepository.save(new Region("ASIA", "아시아")); + + 미국 = countryRepository.save(new Country("US", "미국", 영미권)); + 캐나다 = countryRepository.save(new Country("CA", "캐나다", 영미권)); + 덴마크 = countryRepository.save(new Country("DK", "덴마크", 유럽)); + 오스트리아 = countryRepository.save(new Country("AT", "오스트리아", 유럽)); + 일본 = countryRepository.save(new Country("JP", "일본", 아시아)); + + 영미권_미국_괌대학_A = universityRepository.save(new University( + null, "괌대학(A형)", "University of Guam", "university_of_guam", + "https://www.uog.edu/admissions/international-students", + "https://www.uog.edu/admissions/course-schedule", + "https://www.uog.edu/life-at-uog/residence-halls/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png", + null, 미국, 영미권 + )); + + 영미권_미국_괌대학_B = universityRepository.save(new University( + null, "괌대학(B형)", "University of Guam", "university_of_guam", + "https://www.uog.edu/admissions/international-students", + "https://www.uog.edu/admissions/course-schedule/", + "https://www.uog.edu/life-at-uog/residence-halls/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png", + null, 미국, 영미권 + )); + + 영미권_미국_네바다주립대학_라스베이거스 = universityRepository.save(new University( + null, "네바다주립대학 라스베이거스(B형)", "University of Nevada, Las Vegas", "university_of_nevada_las_vegas", + "https://www.unlv.edu/engineering/eip", + "https://www.unlv.edu/engineering/academic-programs", + "https://www.unlv.edu/housing", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png", + null, 미국, 영미권 + )); + + 영미권_캐나다_메모리얼대학_세인트존스_A = universityRepository.save(new University( + null, "메모리얼 대학 세인트존스(A형)", "Memorial University of Newfoundland St. John's", "memorial_university_of_newfoundland_st_johns", + "https://mun.ca/goabroad/visiting-students-inbound/", + "https://www.unlv.edu/engineering/academic-programs", + "https://www.mun.ca/residences/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png", + null, 캐나다, 영미권 + )); + + 유럽_덴마크_서던덴마크대학교 = universityRepository.save(new University( + null, "서던덴마크대학교", "University of Southern Denmark", "university_of_southern_denmark", + "https://www.sdu.dk/en", + "https://www.sdu.dk/en", + "https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/1.png", + null, 덴마크, 유럽 + )); + + 유럽_덴마크_코펜하겐IT대학 = universityRepository.save(new University( + null, "코펜하겐 IT대학", "IT University of Copenhagen", "it_university_of_copenhagen", + "https://en.itu.dk/", null, + "https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/1.png", + null, 덴마크, 유럽 + )); + + 유럽_오스트리아_그라츠대학 = universityRepository.save(new University( + null, "그라츠 대학", "University of Graz", "university_of_graz", + "https://www.uni-graz.at/en/", + "https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf", + "https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/1.png", + null, 오스트리아, 유럽 + )); + + 유럽_오스트리아_그라츠공과대학 = universityRepository.save(new University( + null, "그라츠공과대학", "Graz University of Technology", "graz_university_of_technology", + "https://www.tugraz.at/en/home", null, + "https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/1.png", + null, 오스트리아, 유럽 + )); + + 유럽_오스트리아_린츠_카톨릭대학 = universityRepository.save(new University( + null, "린츠 카톨릭 대학교", "Catholic Private University Linz", "catholic_private_university_linz", + "https://ku-linz.at/en", null, + "https://ku-linz.at/en/ku_international/incomings/kulis", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/1.png", + null, 오스트리아, 유럽 + )); + + 아시아_일본_메이지대학 = universityRepository.save(new University( + null, "메이지대학", "Meiji University", "meiji_university", + "https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf", null, + "https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png", + null, 일본, 아시아 + )); + + 괌대학_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_괌대학_A + )); + + 괌대학_B_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_괌대학_B + )); + + 네바다주립대학_라스베이거스_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_네바다주립대학_라스베이거스 + )); + + 메모리얼대학_세인트존스_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_캐나다_메모리얼대학_세인트존스_A + )); + + 서던덴마크대학교_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_덴마크_서던덴마크대학교 + )); + + 코펜하겐IT대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_덴마크_코펜하겐IT대학 + )); + + 그라츠대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_그라츠대학 + )); + + 그라츠공과대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_그라츠공과대학 + )); + + 린츠_카톨릭대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_린츠_카톨릭대학 + )); + + 메이지대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 아시아_일본_메이지대학 + )); + + saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(괌대학_B_지원_정보, LanguageTestType.TOEFL_IBT, "70"); + saveLanguageTestRequirement(괌대학_B_지원_정보, LanguageTestType.TOEIC, "900"); + saveLanguageTestRequirement(네바다주립대학_라스베이거스_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(메모리얼대학_세인트존스_A_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(서던덴마크대학교_지원_정보, LanguageTestType.TOEFL_IBT, "70"); + saveLanguageTestRequirement(코펜하겐IT대학_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(그라츠대학_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(그라츠공과대학_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(린츠_카톨릭대학_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(메이지대학_지원_정보, LanguageTestType.JLPT, "N2"); + } + + private void saveLanguageTestRequirement( + UniversityInfoForApply universityInfoForApply, LanguageTestType testType, String minScore) { + LanguageRequirement languageRequirement = new LanguageRequirement( + null, + testType, + minScore, + universityInfoForApply); + universityInfoForApply.addLanguageRequirements(languageRequirement); + universityInfoForApplyRepository.save(universityInfoForApply); + languageRequirementRepository.save(languageRequirement); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java new file mode 100644 index 000000000..cd1ae99de --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -0,0 +1,88 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.dto.LanguageRequirementResponse; +import com.example.solidconnection.university.dto.UniversityDetailResponse; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("대학교 상세 조회 테스트") +class UniversityDetailTest extends UniversityDataSetUpEndToEndTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private TokenService tokenService; + + private String accessToken; + + @BeforeEach + public void setUpUserAndToken() { + // setUp - 회원 정보 저장 + String email = "email@email.com"; + SiteUser siteUser = createSiteUserByEmail(email); + siteUserRepository.save(siteUser); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + } + + @Test + void 대학교_정보를_조회한다() { + // request - 요청 + UniversityDetailResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .get("/university/detail/" + 메이지대학_지원_정보.getId()) + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(UniversityDetailResponse.class); + + // response - 응답 + Assertions.assertAll( + () -> assertThat(response.id()).isEqualTo(메이지대학_지원_정보.getId()), + () -> assertThat(response.koreanName()).isEqualTo(아시아_일본_메이지대학.getKoreanName()), + () -> assertThat(response.englishName()).isEqualTo(아시아_일본_메이지대학.getEnglishName()), + () -> assertThat(response.region()).isEqualTo(아시아_일본_메이지대학.getRegion().getKoreanName()), + () -> assertThat(response.country()).isEqualTo(아시아_일본_메이지대학.getCountry().getKoreanName()), + () -> assertThat(response.languageRequirements()).isEqualTo( + 메이지대학_지원_정보.getLanguageRequirements().stream() + .map(LanguageRequirementResponse::from) + .toList()), + () -> assertThat(response.term()).isEqualTo(메이지대학_지원_정보.getTerm()), + () -> assertThat(response.formatName()).isEqualTo(아시아_일본_메이지대학.getFormatName()), + () -> assertThat(response.homepageUrl()).isEqualTo(아시아_일본_메이지대학.getHomepageUrl()), + () -> assertThat(response.logoImageUrl()).isEqualTo(아시아_일본_메이지대학.getLogoImageUrl()), + () -> assertThat(response.backgroundImageUrl()).isEqualTo(아시아_일본_메이지대학.getBackgroundImageUrl()), + () -> assertThat(response.detailsForLocal()).isEqualTo(아시아_일본_메이지대학.getDetailsForLocal()), + () -> assertThat(response.studentCapacity()).isEqualTo(메이지대학_지원_정보.getStudentCapacity()), + () -> assertThat(response.tuitionFeeType()).isEqualTo(메이지대학_지원_정보.getTuitionFeeType().getKoreanName()), + () -> assertThat(response.semesterAvailableForDispatch()).isEqualTo(메이지대학_지원_정보.getSemesterAvailableForDispatch().getKoreanName()), + () -> assertThat(response.detailsForLanguage()).isEqualTo(메이지대학_지원_정보.getDetailsForLanguage()), + () -> assertThat(response.gpaRequirement()).isEqualTo(메이지대학_지원_정보.getGpaRequirement()), + () -> assertThat(response.gpaRequirementCriteria()).isEqualTo(메이지대학_지원_정보.getGpaRequirementCriteria()), + () -> assertThat(response.semesterRequirement()).isEqualTo(메이지대학_지원_정보.getSemesterRequirement()), + () -> assertThat(response.detailsForApply()).isEqualTo(메이지대학_지원_정보.getDetailsForApply()), + () -> assertThat(response.detailsForMajor()).isEqualTo(메이지대학_지원_정보.getDetailsForMajor()), + () -> assertThat(response.detailsForAccommodation()).isEqualTo(메이지대학_지원_정보.getDetailsForAccommodation()), + () -> assertThat(response.detailsForEnglishCourse()).isEqualTo(메이지대학_지원_정보.getDetailsForEnglishCourse()), + () -> assertThat(response.details()).isEqualTo(메이지대학_지원_정보.getDetails()), + () -> assertThat(response.accommodationUrl()).isEqualTo(아시아_일본_메이지대학.getAccommodationUrl()), + () -> assertThat(response.englishCourseUrl()).isEqualTo(아시아_일본_메이지대학.getEnglishCourseUrl()) + ); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java new file mode 100644 index 000000000..33216453a --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -0,0 +1,150 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static com.example.solidconnection.e2e.DynamicFixture.createLikedUniversity; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static com.example.solidconnection.e2e.DynamicFixture.createUniversityForApply; +import static com.example.solidconnection.university.service.UniversityService.LIKE_CANCELED_MESSAGE; +import static com.example.solidconnection.university.service.UniversityService.LIKE_SUCCESS_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("대학교 좋아요 테스트") +class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { + + private final String email = "email@email.com"; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Autowired + private LikedUniversityRepository likedUniversityRepository; + + @Autowired + private TokenService tokenService; + + private String accessToken; + private SiteUser siteUser; + + @BeforeEach + public void setUpUserAndToken() { + // setUp - 회원 정보 저장 + siteUser = createSiteUserByEmail(email); + siteUserRepository.save(siteUser); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + } + + @Test + void 좋아요를_한_대학을_조회한다() { + // setUp - 대학교 좋아요 저장 + UniversityInfoForApply differentTermUniversityInfoForApply = + createUniversityForApply(term + " 추가 지원", 영미권_미국_괌대학_A, null); + universityInfoForApplyRepository.save(differentTermUniversityInfoForApply); + likedUniversityRepository.saveAll(Set.of( + createLikedUniversity(siteUser, 괌대학_A_지원_정보), + createLikedUniversity(siteUser, differentTermUniversityInfoForApply) + )); + + // request - 요청 + List wishUniversities = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .get("/university/like") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); + + List wishUniversitiesId = wishUniversities.stream() + .map(UniversityInfoForApplyPreviewResponse::id) + .toList(); + assertThat(wishUniversitiesId) + .as("좋아요한 대학교를 지원 시기와 관계 없이 불러온다.") + .containsExactlyInAnyOrder(괌대학_A_지원_정보.getId(), differentTermUniversityInfoForApply.getId()); + } + + @Test + void 좋아요_하지_않은_대학교에_좋아요를_누른다() { + // request - 요청 + LikeResultResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .post("/university/" + 괌대학_A_지원_정보.getId() + "/like") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(LikeResultResponse.class); + + Optional likedUniversity + = likedUniversityRepository.findAllBySiteUser_Email(email).stream().findFirst(); + assertAll("좋아요 누른 대학교를 저장하고 좋아요 성공 응답을 반환한다.", + () -> assertThat(likedUniversity).isPresent(), + () -> assertThat(likedUniversity.get().getId()).isEqualTo(괌대학_A_지원_정보.getId()), + () -> assertThat(response.result()).isEqualTo(LIKE_SUCCESS_MESSAGE) + ); + } + + @Test + void 이미_좋아요한_대학교에_좋아요를_누른다() { + // setUp - 대학교 좋아요 저장 + likedUniversityRepository.save(createLikedUniversity(siteUser, 괌대학_A_지원_정보)); + + // request - 요청 + LikeResultResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .post("/university/" + 괌대학_A_지원_정보.getId() + "/like") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(LikeResultResponse.class); + + Optional likedUniversity + = likedUniversityRepository.findAllBySiteUser_Email(email).stream().findFirst(); + assertAll("좋아요 누른 대학교를 삭제하고, 좋아요 취소 응답을 반환한다.", + () -> assertThat(likedUniversity).isEmpty(), + () -> assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE) + ); + } + + @Test + void 대학의_좋아요_여부를_조회한다() { + // setUp - 대학교 좋아요 저장 + likedUniversityRepository.save(createLikedUniversity(siteUser, 괌대학_A_지원_정보)); + + // request - 요청 + IsLikeResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .get("/university/"+ 괌대학_A_지원_정보.getId() +"/like") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(IsLikeResponse.class); + + assertThat(response.isLike()).isTrue(); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java new file mode 100644 index 000000000..1187fb0ad --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -0,0 +1,178 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("대학교 검색 테스트") +class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { + + private final String email = "email@email.com"; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Autowired + private LikedUniversityRepository likedUniversityRepository; + + @Autowired + private TokenService tokenService; + + private String accessToken; + private SiteUser siteUser; + + @BeforeEach + public void setUpUserAndToken() { + // setUp - 회원 정보 저장 + siteUser = createSiteUserByEmail(email); + siteUserRepository.save(siteUser); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + } + + @Test + void 아무_필터링_없이_전체_대학을_조회한다() { + // request - 요청 + List response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/university/search") + .then().log().all() + .statusCode(200) + .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); + + assertThat(response).containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠공과대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(린츠_카톨릭대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보) + ); + } + + @Test + void 지역으로_필터링한_대학을_조회한다() { + // request - 요청 + List response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/university/search?region=" + 영미권.getCode()) + .then().log().all() + .statusCode(200) + .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); + + assertThat(response).containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보) + ); + } + + @Test + void 국가_국문명_또는_대학_국문명으로_필터링한_대학을_조회한다() { + // request - 요청 + List response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/university/search?keyword=라") + .then().log().all() + .statusCode(200) + .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); + + assertThat(response).containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠공과대학_지원_정보) + ); + } + + @Test + void 둘_이상의_국가_국문명_또는_대학_국문명으로_필터링한_대학을_조회한다() { + // request - 요청 + List response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/university/search?keyword=라&keyword=일본") + .then().log().all() + .statusCode(200) + .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); + + assertThat(response).containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠공과대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보) + ); + } + + @Test + void 어학시험_종류로_필터링한_대학을_조회한다() { + // request - 요청 + List response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/university/search?testType=TOEFL_IBT") + .then().log().all() + .statusCode(200) + .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); + + assertThat(response).containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠대학_지원_정보) + ); + } + + @Test + void 어학시험과_시험_성적으로_필터링한_대학을_조회한다() { + // request - 요청 + List response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/university/search?testType=TOEFL_IBT&testScore=70") + .then().log().all() + .statusCode(200) + .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); + + assertThat(response).containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보) + ); + } + + @Test + void 지역과_어학시험과_시험_성적으로_필터링한_대학을_조회한다() { + // request - 요청 + List response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/university/search?region=EUROPE&testType=TOEFL_IBT&testScore=70") + .then().log().all() + .statusCode(200) + .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); + + assertThat(response) + .containsExactlyInAnyOrder(UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보)); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java new file mode 100644 index 000000000..d0749f1f9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java @@ -0,0 +1,150 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.dto.VerifyStatusResponse; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.VerifyStatus; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.example.solidconnection.e2e.DynamicFixture.createDummyGpa; +import static com.example.solidconnection.e2e.DynamicFixture.createDummyLanguageTest; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("지원 상태 조회 테스트") +class VerifyStatusQueryTest extends UniversityDataSetUpEndToEndTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private TokenService tokenService; + + @Autowired + private ApplicationRepository applicationRepository; + + private String accessToken; + private SiteUser siteUser; + + @BeforeEach + public void setUpUserAndToken() { + // setUp - 회원 정보 저장 + String email = "email@email.com"; + siteUser = siteUserRepository.save(createSiteUserByEmail(email)); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + } + + @Test + void 아무것도_제출하지_않은_상태를_반환한다() { + // request - 요청 + VerifyStatusResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/application/status") + .then().log().all() + .statusCode(200) + .extract().as(VerifyStatusResponse.class); + + assertAll( + () -> assertThat(response.status()).isEqualTo("NOT_SUBMITTED"), + () -> assertThat(response.updateCount()).isZero() + ); + } + + @Test + void 성적만_제출한_상태를_반환한다() { + // setUp - 성적만 제출한 상태 + Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); + applicationRepository.save(application); + + // request - 요청 + VerifyStatusResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/application/status") + .then().log().all() + .statusCode(200) + .extract().as(VerifyStatusResponse.class); + + assertAll( + () -> assertThat(response.status()).isEqualTo("SCORE_SUBMITTED"), + () -> assertThat(response.updateCount()).isZero() + ); + } + + @Test + void 성적과_대학을_모두_제출하고_승인을_기대라는_상태를_반환한다() { + // setUp - 성적과 대학을 모두 제출한 상태 + Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + applicationRepository.save(application); + + // request - 요청 + VerifyStatusResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/application/status") + .then().log().all() + .statusCode(200) + .extract().as(VerifyStatusResponse.class); + + assertAll( + () -> assertThat(response.status()).isEqualTo("SUBMITTED_PENDING"), + () -> assertThat(response.updateCount()).isZero() + ); + } + + @Test + void 성적과_대학을_모두_제출했지만_승인이_반려된_상태를_반환한다() { + // setUp - 성적과 대학을 모두 제출했지만, 승인 거절 + Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + application.setVerifyStatus(VerifyStatus.REJECTED); + applicationRepository.save(application); + + // request - 요청 + VerifyStatusResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/application/status") + .then().log().all() + .statusCode(200) + .extract().as(VerifyStatusResponse.class); + + assertAll( + () -> assertThat(response.status()).isEqualTo("SUBMITTED_REJECTED"), + () -> assertThat(response.updateCount()).isZero() + ); + } + + @Test + void 성적과_대학을_모두_제출했으며_승인이_된_상태를_반환한다() { + // setUp - 성적과 대학을 모두 제출했으며, 승인이 된 상태 + Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + application.setVerifyStatus(VerifyStatus.APPROVED); + applicationRepository.save(application); + + // request - 요청 + VerifyStatusResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().get("/application/status") + .then().log().all() + .statusCode(200) + .extract().as(VerifyStatusResponse.class); + + assertAll( + () -> assertThat(response.status()).isEqualTo("SUBMITTED_APPROVED"), + () -> assertThat(response.updateCount()).isZero() + ); + } +} diff --git a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java new file mode 100644 index 000000000..098a22c18 --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java @@ -0,0 +1,50 @@ +package com.example.solidconnection.support; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; + +@ActiveProfiles("test") +@Component +public class DatabaseCleaner { + + @Autowired + private RedisTemplate redisTemplate; + + @PersistenceContext + private EntityManager em; + + @Transactional + public void clear() { + em.clear(); + truncate(); + Objects.requireNonNull(redisTemplate.getConnectionFactory()) + .getConnection() + .serverCommands() + .flushDb(); + } + + private void truncate() { + em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + getTruncateQueries().forEach(query -> em.createNativeQuery(query).executeUpdate()); + em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } + + @SuppressWarnings("unchecked") + private List getTruncateQueries() { + String sql = """ + SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ' RESTART IDENTITY', ';') AS q + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'PUBLIC' + """; + + return em.createNativeQuery(sql).getResultList(); + } +} diff --git a/src/test/java/com/example/solidconnection/support/DatabaseClearExtension.java b/src/test/java/com/example/solidconnection/support/DatabaseClearExtension.java new file mode 100644 index 000000000..32d7a8975 --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/DatabaseClearExtension.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.support; + +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +public class DatabaseClearExtension implements BeforeEachCallback { + + @Override + public void beforeEach(ExtensionContext context) { + DatabaseCleaner databaseCleaner = getDataCleaner(context); + databaseCleaner.clear(); + } + + private DatabaseCleaner getDataCleaner(ExtensionContext extensionContext) { + return SpringExtension.getApplicationContext(extensionContext) + .getBean(DatabaseCleaner.class); + } +} From d73004b4b530fd7f3549f5bbefed90a414dbe728 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:20:12 +0900 Subject: [PATCH 061/158] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=20=ED=99=98=EA=B2=BD=20=EB=B0=8F=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=B6=94=EA=B0=80=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 로컬 개발 환경용 docker-compose.local.yml 추가 * feat: 프로필 파일 추가 * chore: 프로필 관련 .gitignore 수정 * docs: README.md 수정 * ci: 자동 빌드-배포 Github Actions 워크플로 추가 * ci: 자동 배포 Github Actions 워크플로우에 application.yml 불러오기 추가 --- .github/workflows/release.yml | 82 +++++++++++++++++++++++ .gitignore | 4 +- README.md | Bin 950 -> 1154 bytes docker-compose.local.yml | 27 ++++++++ src/main/resources/application-local.yml | 30 +++++++++ src/main/resources/application-test.yml | 26 +++++++ src/main/resources/application.yml | 41 ++++++++++++ 7 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 docker-compose.local.yml create mode 100644 src/main/resources/application-local.yml create mode 100644 src/main/resources/application-test.yml create mode 100644 src/main/resources/application.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..f7de45642 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: Build Gradle and Deploy + +on: + push: + branches: [ "release" ] + +jobs: + build-gradle: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Create application-secret.yml + run: echo "${{ secrets.APPLICATION_SECRET }}" > src/main/resources/application-secret.yml + + - name: Create application-prod.yml + run: echo "${{ secrets.APPLICATION_PROD }}" > src/main/resources/application-prod.yml + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + + - name: Build with Gradle + run: ./gradlew bootJar -Dspring.profiles.active=prod + + - name: Copy jar file to remote + uses: appleboy/scp-action@v0.1.10 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.PRIVATE_KEY }} + source: "./build/libs/*.jar" + target: "/home/${{ secrets.USERNAME }}/solid-connect-server/build/libs" + + - name: Copy docker file to remote + uses: appleboy/scp-action@v0.1.10 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.PRIVATE_KEY }} + source: "./Dockerfile" + target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" + + - name: Copy docker compose file to remote + uses: appleboy/scp-action@v0.1.10 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.PRIVATE_KEY }} + source: "./docker-compose.yml" + target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" + + - name: Copy nginx configuration file to remote + uses: appleboy/scp-action@v0.1.10 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.PRIVATE_KEY }} + source: "./nginx.conf" + target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" + + - name: Run docker compose + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.PRIVATE_KEY }} + script_stop: true + script: | + cd /home/${{ secrets.USERNAME }}/solid-connect-server + docker-compose down + docker-compose up -d --build diff --git a/.gitignore b/.gitignore index aedec0c70..ffed9d77d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,5 @@ out/ .vscode/ ### YML ### -*.yml -!docker-compose.yml \ No newline at end of file +application-secret.yml +application-prod.yml \ No newline at end of file diff --git a/README.md b/README.md index ef24e835ce7010709de45f879e8dbfcc9bed17a1..acdcee3b78f7518548a07db083f5cd2070bac25a 100644 GIT binary patch delta 222 zcmdnS-o!b7z`QA8DOFabqWkA z4EYSn4A~5+3`Iay$v|E%LjjO128t*!JUP^P6{Jf62tnp0ps3e{sP{wCpv#a3REBIQ nR1sW_9zzb;%0wWo$508hE(fTi6etg}B84Ggqjfbi3&>IcHe@Uq delta 17 XcmZqT+{Qk^j8&Hb2sbk`Rx<+tCmjRO diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 000000000..2102f390b --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: solid-connection-local-mysql + environment: + MYSQL_ROOT_PASSWORD: solid_connection_local_root_password + MYSQL_DATABASE: solid_connection + MYSQL_USER: solid_connection_local_username + MYSQL_PASSWORD: solid_connection_local_password + ports: + - "3306:3306" +# volumes: +# - mysql_data_local:/var/lib/mysql + + redis: + image: redis:latest + container_name: solid-connection-local-redis + ports: + - "6379:6379" +# volumes: +# - redis_data_local:/data + +#volumes: +# mysql_data_local: +# redis_data_local: \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 000000000..fa7eebacb --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,30 @@ +spring: + config: + activate: + on-profile: local + + jpa: + hibernate: + ddl-auto: create + generate-ddl: true + show-sql: true + database: mysql + defer-datasource-initialization: true + + sql: + init: + mode: always + + datasource: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost/solid_connection?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: solid_connection_local_username + password: solid_connection_local_password + + data: + redis: + host: localhost + port: 6379 + +kakao: + redirect_uri: "http://localhost:8080/auth/kakao" diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 000000000..4dc996907 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,26 @@ +spring: + config: + activate: + on-profile: test + + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create + generate-ddl: true + show-sql: true + database: mysql + defer-datasource-initialization: true + + datasource: + url: jdbc:h2:mem:testdb + username: sa + password: + + data: + redis: + host: localhost + port: 6379 + +kakao: + redirect_uri: "http://localhost:8080/auth/kakao" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 000000000..55193a7b3 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,41 @@ +spring: + profiles: + group: + prod: + - prod + - secret + local: + - local + - secret + test: + - test + - secret + + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + + jpa: + hibernate: + ddl-auto: none + generate-ddl: false + show-sql: false + database: mysql + defer-datasource-initialization: true + + mvc: + path match: + matching-strategy: ANT_PATH_MATCHER + + sql: + init: + mode: never + +jwt: + secret: + aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee + +kakao: + token_url: "https://kauth.kakao.com/oauth/token" + user_info_url: "https://kapi.kakao.com/v2/user/me" \ No newline at end of file From 5586645503b223aaf88b3ebf5a6dd44c84b6dc0f Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:23:25 +0900 Subject: [PATCH 062/158] =?UTF-8?q?fix:=20release.yml=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7de45642..fa9da579f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: run: ./gradlew bootJar -Dspring.profiles.active=prod - name: Copy jar file to remote - uses: appleboy/scp-action@v0.1.10 + uses: appleboy/scp-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} @@ -43,7 +43,7 @@ jobs: target: "/home/${{ secrets.USERNAME }}/solid-connect-server/build/libs" - name: Copy docker file to remote - uses: appleboy/scp-action@v0.1.10 + uses: appleboy/scp-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} @@ -52,7 +52,7 @@ jobs: target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - name: Copy docker compose file to remote - uses: appleboy/scp-action@v0.1.10 + uses: appleboy/scp-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} @@ -61,7 +61,7 @@ jobs: target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - name: Copy nginx configuration file to remote - uses: appleboy/scp-action@v0.1.10 + uses: appleboy/scp-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} From d956dcf5bbc8653228162fb9af230b82b6b1b6df Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:25:47 +0900 Subject: [PATCH 063/158] =?UTF-8?q?fix:=20release.yml=20Gradle=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa9da579f..3830190ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,9 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + - name: Grant execute permission for Gradle wrapper(gradlew) + run: chmod +x ./gradlew + - name: Build with Gradle run: ./gradlew bootJar -Dspring.profiles.active=prod From 7a6d71966bbe03492b853eeb6025f96ea8c8bdda Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:33:26 +0900 Subject: [PATCH 064/158] =?UTF-8?q?fix:=20release.yml=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3830190ca..9ea15683c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: username: ${{ secrets.USERNAME }} key: ${{ secrets.PRIVATE_KEY }} source: "./build/libs/*.jar" - target: "/home/${{ secrets.USERNAME }}/solid-connect-server/build/libs" + target: "/home/${{ secrets.USERNAME }}/solid-connect-server/build/libs/" - name: Copy docker file to remote uses: appleboy/scp-action@master From e949ca7659f9472b310bd6ddc2d31f06ebb3c4f1 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:38:02 +0900 Subject: [PATCH 065/158] =?UTF-8?q?fix:=20release.yml=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ea15683c..ac375db23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: username: ${{ secrets.USERNAME }} key: ${{ secrets.PRIVATE_KEY }} source: "./build/libs/*.jar" - target: "/home/${{ secrets.USERNAME }}/solid-connect-server/build/libs/" + target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - name: Copy docker file to remote uses: appleboy/scp-action@master From 4771c9c5bfbb56188d825c9278b659dcdb55cf69 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:48:09 +0900 Subject: [PATCH 066/158] =?UTF-8?q?fix:=20Dockerfile=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 847788e44..4742ac87a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ARG JAR_FILE=./build/libs/solid-connection-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar # 시스템 진입점 정의 -ENTRYPOINT ["java","-jar","/app.jar"] +ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=prod"] # 볼륨 설정 -VOLUME /tmp \ No newline at end of file +VOLUME /tmp From aa7646769370808d0eac36c2ad7e7b675f3fea82 Mon Sep 17 00:00:00 2001 From: devMuromi Date: Wed, 17 Jul 2024 19:57:10 +0900 Subject: [PATCH 067/158] =?UTF-8?q?fix:=20gpa=5Fcreteria=20->=20gpa=5Fcrit?= =?UTF-8?q?eria=20=ED=95=84=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/solidconnection/application/domain/Gpa.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/application/domain/Gpa.java b/src/main/java/com/example/solidconnection/application/domain/Gpa.java index 0c737a7d2..82803dae9 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Gpa.java +++ b/src/main/java/com/example/solidconnection/application/domain/Gpa.java @@ -15,7 +15,7 @@ public class Gpa { @Column(nullable = false, name = "gpa") private Double gpa; - @Column(nullable = false, name = "gpa_creteria") + @Column(nullable = false, name = "gpa_criteria") private Double gpaCriteria; @Column(nullable = false, name = "gpa_report_url", length = 500) From 2e653af1cbd10f94b3ee548788334ddbc671dee4 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 23 Jul 2024 20:21:46 +0900 Subject: [PATCH 068/158] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EC=98=88=EC=99=B8=20=EB=A1=9C=EA=B7=B8=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 --- .../custom/exception/CustomExceptionHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java index 7394203cb..c0c610bce 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java @@ -66,11 +66,10 @@ public ResponseEntity handleJwtException(JwtException ex) { .body(errorResponse); } - @ExceptionHandler(Exception.class) public ResponseEntity handleOtherException(Exception ex) { String errorMessage = ex.getMessage(); - log.error("알 수 없는 예외 발생 : {}", errorMessage); + log.error("서버 내부 예외 발생 : {}", errorMessage); ErrorResponse errorResponse = new ErrorResponse(NOT_DEFINED_ERROR, errorMessage); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) From da56102318aa3f010da617fcb4ca68ea4cc4cd56 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 24 Jul 2024 00:42:55 +0900 Subject: [PATCH 069/158] =?UTF-8?q?refactor:=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EB=8C=80=ED=95=99=20=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 변수명 변경, 가독성을 위한 개행, 하드코딩 제거 --- .../service/UniversityRecommendService.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index cf58f801a..bb704faea 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -24,6 +24,7 @@ public class UniversityRecommendService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final GeneralRecommendUniversities generalRecommendUniversities; private final SiteUserRepository siteUserRepository; + @Value("${university.term}") public String term; @@ -39,16 +40,16 @@ public UniversityRecommendsResponse getPersonalRecommends(String email) { // 맞춤 추천 대학교를 불러온다. List personalRecommends = universityInfoForApplyRepository .findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(siteUser, term); - List shuffledList + List trimmedRecommendUniversities = personalRecommends.subList(0, Math.min(RECOMMEND_UNIVERSITY_NUM, personalRecommends.size())); - Collections.shuffle(personalRecommends); + Collections.shuffle(trimmedRecommendUniversities); // 맞춤 추천 대학교의 수가 6개보다 적다면, 일반 추천 대학교를 부족한 수 만큼 불러온다. - if (shuffledList.size() < 6) { - shuffledList.addAll(getGeneralRecommendsExcludingSelected(shuffledList)); + if (trimmedRecommendUniversities.size() < RECOMMEND_UNIVERSITY_NUM) { + trimmedRecommendUniversities.addAll(getGeneralRecommendsExcludingSelected(trimmedRecommendUniversities)); } - return new UniversityRecommendsResponse(shuffledList.stream() + return new UniversityRecommendsResponse(trimmedRecommendUniversities.stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList()); } From 95880bc6ddb9c5d3eeae65e82dd725bc15606568 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 24 Jul 2024 00:43:40 +0900 Subject: [PATCH 070/158] =?UTF-8?q?fix:=20LazyFetch=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../university/domain/UniversityInfoForApply.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java b/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java index bbb4dd015..903bc9320 100644 --- a/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java +++ b/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java @@ -14,6 +14,7 @@ import jakarta.persistence.OneToMany; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -21,6 +22,7 @@ import java.util.Set; @Getter +@EqualsAndHashCode(of = "id") @AllArgsConstructor(access = AccessLevel.PUBLIC) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @@ -71,10 +73,10 @@ public class UniversityInfoForApply { @Column(length = 500) private String details; - @OneToMany(mappedBy = "universityInfoForApply", fetch = FetchType.LAZY) + @OneToMany(mappedBy = "universityInfoForApply", fetch = FetchType.EAGER) private Set languageRequirements = new HashSet<>(); - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) private University university; public void addLanguageRequirements(LanguageRequirement languageRequirements) { From 664b0c6f029513611993da106424b3c26755b65d Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 24 Jul 2024 00:44:34 +0900 Subject: [PATCH 071/158] =?UTF-8?q?fix:=20403=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인하지 않아도 접근할 수 있는 페이지 목록 수정 --- .../solidconnection/config/security/SecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index e3415e9c4..1c63fe94a 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -50,7 +50,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/", "/index.html", "/favicon.ico", "/file/profile/pre", "/auth/kakao", "/auth/sign-up", "/auth/reissue", - "/university/detail/**", "/university/search/**", "/home", + "/university/detail/**", "/university/search/**", "/university/recommends", "/swagger-ui/**", "/v3/api-docs/**" ) .permitAll() From 138f404dc8c0fc53d2a6ab42eda19cba30fb3fa5 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 24 Jul 2024 00:48:17 +0900 Subject: [PATCH 072/158] =?UTF-8?q?test:=20=EC=B6=94=EC=B2=9C=20=EB=8C=80?= =?UTF-8?q?=ED=95=99=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/GeneralRecommendUniversities.java | 2 +- .../e2e/UniversityRecommendTest.java | 193 ++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java index f9441c61d..f37fac410 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java @@ -28,7 +28,7 @@ public class GeneralRecommendUniversities { "오스트라바 대학", "RMIT멜버른공과대학(A형)", "알브슈타트 지그마링엔 대학", "뉴저지시티대학(A형)", "도요대학", "템플대학(A형)", "빈 공과대학교", "리스본대학 공과대학", "바덴뷔르템베르크 산학협력대학", "긴다이대학", "네바다주립대학 라스베이거스(B형)", "릴 가톨릭 대학", - "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학" + "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교" ); @Value("${university.term}") diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java new file mode 100644 index 000000000..ee46733a1 --- /dev/null +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -0,0 +1,193 @@ +package com.example.solidconnection.e2e; + +import com.example.solidconnection.config.token.TokenService; +import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.entity.InterestedCountry; +import com.example.solidconnection.entity.InterestedRegion; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import com.example.solidconnection.university.service.GeneralRecommendUniversities; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import java.util.List; + +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; +import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("추천 대학 목록 조회 테스트") +class UniversityRecommendTest extends UniversityDataSetUpEndToEndTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private InterestedRegionRepository interestedRegionRepository; + + @Autowired + private InterestedCountyRepository interestedCountyRepository; + + @Autowired + private TokenService tokenService; + + @Autowired + private GeneralRecommendUniversities generalRecommendUniversities; + + private SiteUser siteUser; + private String accessToken; + + @BeforeEach + void setUp() { + // setUp - 회원 정보 저장 + String email = "email@email.com"; + siteUser = siteUserRepository.save(createSiteUserByEmail(email)); + generalRecommendUniversities.init(); + + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenService.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); + tokenService.saveToken(refreshToken, TokenType.REFRESH); + } + + @Test + void 관심_지역을_설정한_사용자의_추천_대학_목록을_조회한다() { + // setUp - 관심 지역 저장 + interestedRegionRepository.save(new InterestedRegion(siteUser, 영미권)); + + // request - 요청 + UniversityRecommendsResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .get("/university/recommends") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(UniversityRecommendsResponse.class); + + assertAll( + String.format("관심 지역에 해당하는 학교를 포함한 %d개의 대학 목록을 반환한다.", RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(response.recommendedUniversities()) + .containsOnlyOnceElementsOf(List.of( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보) + )) + ); + } + + @Test + void 관심_국가를_설정한_사용자의_추천_대학_목록을_조회한다() { + // setUp - 관심 국가 저장 + interestedCountyRepository.save(new InterestedCountry(siteUser, 덴마크)); + + // request - 요청 + UniversityRecommendsResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .get("/university/recommends") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(UniversityRecommendsResponse.class); + + assertAll( + String.format("관심 국가에 해당하는 학교를 포함한 %d개의 대학 목록을 반환한다.", RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(response.recommendedUniversities()) + .containsOnlyOnceElementsOf(List.of( + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + )) + ); + } + + @Test + void 관심_지역과_관심_국가를_설정한_사용자의_추천_대학_목록을_조회한다() { + // setUp - 관심 지역과 국가 저장 + interestedRegionRepository.save(new InterestedRegion(siteUser, 영미권)); + interestedCountyRepository.save(new InterestedCountry(siteUser, 덴마크)); + + // request - 요청 + UniversityRecommendsResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .get("/university/recommends") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(UniversityRecommendsResponse.class); + + assertAll( + String.format("관심 지역 또는 국가에 해당하는 학교를 포함한 %d개의 대학 목록을 반환한다.", RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(response.recommendedUniversities()) + .containsOnlyOnceElementsOf(List.of( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + )) + ); + } + + @Test + void 관심_지역_또는_관심_국가를_설정하지_않은_사용자의_추천_대학_목록을_조회한다() { + // request - 요청 + UniversityRecommendsResponse response = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .log().all() + .get("/university/recommends") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(UniversityRecommendsResponse.class); + + List generalRecommendUniversities + = this.generalRecommendUniversities.getRecommendUniversities().stream() + .map(UniversityInfoForApplyPreviewResponse::from) + .toList(); + assertAll( + String.format("일반 추천 대학 목록 %d개를 반환한다.", RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(generalRecommendUniversities) + .containsOnlyOnceElementsOf(response.recommendedUniversities()) + ); + } + + @Test + void 로그인하지_않은_방문객의_추천_대학_목록을_조회한다() { + // request - 요청 + UniversityRecommendsResponse response = RestAssured.given() + .log().all() + .get("/university/recommends") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(UniversityRecommendsResponse.class); + + List generalRecommendUniversities + = this.generalRecommendUniversities.getRecommendUniversities().stream() + .map(UniversityInfoForApplyPreviewResponse::from) + .toList(); + assertAll( + String.format("일반 추천 대학 목록 %d개를 반환한다.", RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM), + () -> assertThat(generalRecommendUniversities) + .containsOnlyOnceElementsOf(response.recommendedUniversities()) + ); + } +} From 419adef3bd9b5cfb0102a87b0705e25e697a1c3c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 24 Jul 2024 01:13:13 +0900 Subject: [PATCH 073/158] =?UTF-8?q?chore:=20language=5Frequirement=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B4=88=EA=B8=B0=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/data.sql | 43 ++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index c3fa34ce4..8c88643c6 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -221,4 +221,45 @@ VALUES ('2024-1', 1, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대 '여학생만 신청가능', NULL, NULL, '기숙사 없음, 계약된 외부 기숙사 사용-“Maison de Claire Ibaraki” 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)', NULL), ('2024-1', 20, 2, 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, - '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월', NULL); \ No newline at end of file + '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월', NULL); + +INSERT INTO language_requirement(language_test_type, min_score, university_info_for_apply_id) +VALUES + ('TOEFL_IBT', '80', 1), + ('IELTS', '6.5', 1), + ('TOEFL_IBT', '80', 2), + ('IELTS', '6.5', 2), + ('TOEFL_IBT', '79', 3), + ('IELTS', '6.0', 3), + ('TOEFL_IBT', '88', 4), + ('IELTS', '6.5', 4), + ('TOEFL_IBT', '88', 5), + ('IELTS', '6.5', 5), + ('TOEFL_IBT', '85', 6), + ('IELTS', '6.5', 6), + ('TOEFL_IBT', '85', 7), + ('IELTS', '6.5', 7), + ('TOEFL_IBT', '80', 8), + ('IELTS', '6.0', 8), + ('TOEFL_IBT', '83', 9), + ('IELTS', '6.5', 9), + ('TOEFL_IBT', '87', 10), + ('IELTS', '6.5', 10), + ('TOEFL_IBT', '90', 11), + ('IELTS', '6.5', 11), + ('TOEFL_IBT', '85', 12), + ('IELTS', '6.5', 12), + ('TOEFL_IBT', '82', 13), + ('IELTS', '6.0', 13), + ('TOEFL_IBT', '85', 14), + ('IELTS', '6.5', 14), + ('TOEFL_IBT', '90', 15), + ('IELTS', '7.0', 15), + ('TOEFL_IBT', '85', 16), + ('IELTS', '6.5', 16), + ('DELF', 'B2', 17), + ('DALF', 'C1', 17), + ('JLPT', 'N2', 18), + ('JLPT', 'N1', 19), + ('TOEFL_IBT', '85', 20), + ('IELTS', '6.5', 20); From 1958e42fce38bbddfc78ced8daaf9904a5a03e35 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Wed, 24 Jul 2024 01:13:31 +0900 Subject: [PATCH 074/158] =?UTF-8?q?refactor:=20=EC=96=B4=ED=95=99=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=EC=9D=B4=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EB=8C=80=ED=95=99=EB=8F=84=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/custom/UniversityFilterRepositoryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java index 994618a6b..943a2899d 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java @@ -82,7 +82,7 @@ public List findByRegionCodeAndKeywordsAndLanguageTestTy .join(universityInfoForApply.university, university) .join(university.country, country) .join(country.region, region) - .join(universityInfoForApply.languageRequirements, languageRequirement) + .leftJoin(universityInfoForApply.languageRequirements, languageRequirement) .where(regionCodeEq(region, regionCode) .and(countryOrUniversityContainsKeyword(country, university, keywords))) .fetch(); From 90a8873e63512c1636b061e6820da18d9c5a7c3e Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 25 Jul 2024 00:26:49 +0900 Subject: [PATCH 075/158] =?UTF-8?q?fix:=20=EC=9D=BC=EB=B0=98=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EB=8C=80=ED=95=99=20=EB=AA=A9=EB=A1=9D=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EC=8B=9C=EC=A0=90=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../university/service/GeneralRecommendUniversities.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java index f37fac410..6eb70f81b 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java @@ -1,11 +1,13 @@ package com.example.solidconnection.university.service; +import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.university.domain.UniversityInfoForApply; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.List; @@ -24,6 +26,7 @@ public class GeneralRecommendUniversities { @Getter private final List recommendUniversities; private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final CountryRepository countryRepository; private final List candidates = List.of( "오스트라바 대학", "RMIT멜버른공과대학(A형)", "알브슈타트 지그마링엔 대학", "뉴저지시티대학(A형)", "도요대학", "템플대학(A형)", "빈 공과대학교", @@ -34,7 +37,7 @@ public class GeneralRecommendUniversities { @Value("${university.term}") public String term; - @PostConstruct + @EventListener(ApplicationReadyEvent.class) public void init() { int i = 0; while (recommendUniversities.size() < RECOMMEND_UNIVERSITY_NUM && i < candidates.size()) { From d660cd3d799e4de0e2af862d189b3e61649e389e Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 25 Jul 2024 00:30:47 +0900 Subject: [PATCH 076/158] =?UTF-8?q?chore:=20api=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=98=88=EC=8B=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/solidconnection/auth/dto/SignUpRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java index 60b2c6cc1..e77cbd31b 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java @@ -16,10 +16,10 @@ public record SignUpRequest( @Schema(description = "카카오 인증 토큰", example = "kakaoToken123") String kakaoOauthToken, - @ArraySchema(schema = @Schema(description = "관심 지역 목록", example = "[\"아시아\", \"유럽\"]")) + @ArraySchema(schema = @Schema(description = "관심 지역 목록", example = "아시아권")) List interestedRegions, - @ArraySchema(schema = @Schema(description = "관심 국가 목록", example = "[\"일본\", \"독일\"]")) + @ArraySchema(schema = @Schema(description = "관심 국가 목록", example = "일본")) List interestedCountries, @Schema(description = "지원 준비 단계", example = "CONSIDERING") From 2bbce914df48644bbafdbd072ef56fca3f8934bf Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 25 Jul 2024 00:54:50 +0900 Subject: [PATCH 077/158] =?UTF-8?q?refactor:=20=ED=95=99=EA=B5=90=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ApplicationQueryService.java | 1 + .../custom/UniversityFilterRepository.java | 4 ++-- .../custom/UniversityFilterRepositoryImpl.java | 14 ++++++-------- .../university/service/UniversityService.java | 3 ++- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index e4f314225..104114c5c 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -39,6 +39,7 @@ public class ApplicationQueryService { * 다른 지원자들의 성적을 조회한다. * - 유저가 다른 지원자들을 볼 수 있는지 검증한다. * - 지역과 키워드를 통해 대학을 필터링한다. + * - 지역은 영어 대문자로 받는다 e.g. ASIA * - 1지망, 2지망 지원자들을 조회한다. * */ @Transactional(readOnly = true) diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepository.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepository.java index bc2306737..009496be7 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepository.java @@ -10,6 +10,6 @@ public interface UniversityFilterRepository { List findByRegionCodeAndKeywords(String regionCode, List keywords); - List findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScore( - String regionCode, List keywords, LanguageTestType testType, String testScore); + List findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + String regionCode, List keywords, LanguageTestType testType, String testScore, String term); } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java index 943a2899d..0beac6763 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java @@ -3,7 +3,6 @@ import com.example.solidconnection.entity.QCountry; import com.example.solidconnection.entity.QRegion; import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.university.domain.QLanguageRequirement; import com.example.solidconnection.university.domain.QUniversity; import com.example.solidconnection.university.domain.QUniversityInfoForApply; import com.example.solidconnection.university.domain.University; @@ -68,26 +67,25 @@ private BooleanExpression createKeywordCondition(StringPath namePath, List findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScore( - String regionCode, List keywords, LanguageTestType testType, String testScore) { + public List findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + String regionCode, List keywords, LanguageTestType testType, String testScore, String term) { QUniversity university = QUniversity.university; QCountry country = QCountry.country; QRegion region = QRegion.region; QUniversityInfoForApply universityInfoForApply = QUniversityInfoForApply.universityInfoForApply; - QLanguageRequirement languageRequirement = QLanguageRequirement.languageRequirement; List filteredUniversityInfoForApply = queryFactory .selectFrom(universityInfoForApply) .join(universityInfoForApply.university, university) .join(university.country, country) - .join(country.region, region) - .leftJoin(universityInfoForApply.languageRequirements, languageRequirement) + .join(university.region, region) .where(regionCodeEq(region, regionCode) - .and(countryOrUniversityContainsKeyword(country, university, keywords))) + .and(countryOrUniversityContainsKeyword(country, university, keywords)) + .and(universityInfoForApply.term.eq(term))) .fetch(); - if(testType == null || testScore == null || testScore.isEmpty()) { + if(testScore == null || testScore.isEmpty()) { if(testType != null) { return filteredUniversityInfoForApply.stream() .filter(uifa -> uifa.getLanguageRequirements().stream() diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index cbc97e4f6..94e8bba83 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -52,6 +52,7 @@ public UniversityDetailResponse getUniversityDetail(Long universityInfoForApplyI /* * 대학교 검색 결과를 불러온다. * - 권역, 키워드, 언어 시험 종류, 언어 시험 점수를 조건으로 검색하여 결과를 반환한다. + * - 권역은 영어 대문자로 받는다 e.g. ASIA * - 키워드는 국가명 또는 대학명에 포함되는 것이 조건이다. * - 언어 시험 점수는 합격 최소 점수보다 높은 것이 조건이다. * */ @@ -59,7 +60,7 @@ public UniversityDetailResponse getUniversityDetail(Long universityInfoForApplyI public List searchUniversity( String regionCode, List keywords, LanguageTestType testType, String testScore) { return universityFilterRepository - .findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScore(regionCode, keywords, testType, testScore) + .findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm(regionCode, keywords, testType, testScore, term) .stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList(); From 6895c2e37e5dd391c11a429f777e56de7eb41bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Sun, 28 Jul 2024 12:08:08 +0900 Subject: [PATCH 078/158] =?UTF-8?q?docs:=20Feature=20request=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..1b6ed9260 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +## 어떤 기능인가요? + +> 추가하려는 기능에 대해 간결하게 설명해주세요 + +## 작업 상세 내용 + +- [ ] TODO +- [ ] TODO +- [ ] TODO + +## 참고할만한 자료(선택) From bc40872812bd282d87e230043f65136df013f253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Wed, 31 Jul 2024 22:30:09 +0900 Subject: [PATCH 079/158] =?UTF-8?q?docs:=20PR=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pull_request_template.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..f039ae72b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## 관련 이슈 + +- resolves: #이슈 번호 + +## 작업 내용 + + + + + +## 특이 사항 + + + +## 리뷰 요구사항 (선택) + + + + + + From 9b7c41691f9dff8b6b8d9ea2a52f37a1bb02a921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:12:42 +0900 Subject: [PATCH 080/158] =?UTF-8?q?=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A0=95=EC=9D=98=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 커뮤니티 관련 엔티티 정의 * test: 커뮤니티 관련 테이블 존재 여부 테스트 생성 * refactor: @DynamicUpdate, @DynamicInsert 어노테이션 BaseEntity로 이동 * style: 파일 끝에 개행 추가 --- .../example/solidconnection/entity/Board.java | 29 ++++++++++ .../solidconnection/entity/Comment.java | 42 +++++++++++++++ .../example/solidconnection/entity/Post.java | 54 +++++++++++++++++++ .../solidconnection/entity/PostImage.java | 25 +++++++++ .../entity/common/BaseEntity.java | 26 +++++++++ .../entity/mapping/PostLike.java | 28 ++++++++++ .../siteuser/domain/SiteUser.java | 29 ++++++---- .../solidconnection/type/PostCategory.java | 5 ++ .../database/DatabaseConnectionTest.java | 7 ++- 9 files changed, 233 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/entity/Board.java create mode 100644 src/main/java/com/example/solidconnection/entity/Comment.java create mode 100644 src/main/java/com/example/solidconnection/entity/Post.java create mode 100644 src/main/java/com/example/solidconnection/entity/PostImage.java create mode 100644 src/main/java/com/example/solidconnection/entity/common/BaseEntity.java create mode 100644 src/main/java/com/example/solidconnection/entity/mapping/PostLike.java create mode 100644 src/main/java/com/example/solidconnection/type/PostCategory.java diff --git a/src/main/java/com/example/solidconnection/entity/Board.java b/src/main/java/com/example/solidconnection/entity/Board.java new file mode 100644 index 000000000..adef5b0bb --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/Board.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Board { + + @Id + @Column(length = 20) + private String code; + + @Column(nullable = false, length = 20) + private String koreanName; + + @OneToMany(mappedBy = "board", cascade = CascadeType.ALL) + private List postList = new ArrayList<>(); +} + diff --git a/src/main/java/com/example/solidconnection/entity/Comment.java b/src/main/java/com/example/solidconnection/entity/Comment.java new file mode 100644 index 000000000..dd178fb9c --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/Comment.java @@ -0,0 +1,42 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Comment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 255) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Comment parentComment; + + @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) + private List commentList = new ArrayList<>(); +} diff --git a/src/main/java/com/example/solidconnection/entity/Post.java b/src/main/java/com/example/solidconnection/entity/Post.java new file mode 100644 index 000000000..9a5e60069 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/Post.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.entity; + +import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.entity.mapping.PostLike; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.PostCategory; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class Post extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 255) + private String title; + + @Column(length = 1000) + private String content; + + private Boolean isQuestion; + + private Long likeCount; + + @Enumerated(EnumType.STRING) + private PostCategory category; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_code") + private Board board; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + private List commentList = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postImageList = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + private List postLikeList = new ArrayList<>(); +} diff --git a/src/main/java/com/example/solidconnection/entity/PostImage.java b/src/main/java/com/example/solidconnection/entity/PostImage.java new file mode 100644 index 000000000..ddd866e38 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/PostImage.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PostImage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 500) + private String url; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; +} diff --git a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java b/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java new file mode 100644 index 000000000..5f1283c64 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.entity.common; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +@DynamicUpdate +@DynamicInsert +public abstract class BaseEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java b/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java new file mode 100644 index 000000000..feba376b8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java @@ -0,0 +1,28 @@ +package com.example.solidconnection.entity.mapping; + +import com.example.solidconnection.entity.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PostLike { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; +} diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index b13a26e9a..7ebb828e1 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -1,26 +1,24 @@ package com.example.solidconnection.siteuser.domain; +import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.entity.Post; +import com.example.solidconnection.entity.mapping.PostLike; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import jakarta.persistence.*; +import lombok.*; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity +@Builder +@AllArgsConstructor public class SiteUser { @Id @@ -59,6 +57,15 @@ public class SiteUser { @Setter private LocalDate quitedAt; + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) + private List postList = new ArrayList<>(); + + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) + private List commentList = new ArrayList<>(); + + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) + private List postLikeList = new ArrayList<>(); + public SiteUser( String email, String nickname, diff --git a/src/main/java/com/example/solidconnection/type/PostCategory.java b/src/main/java/com/example/solidconnection/type/PostCategory.java new file mode 100644 index 000000000..b42b94f95 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/PostCategory.java @@ -0,0 +1,5 @@ +package com.example.solidconnection.type; + +public enum PostCategory { + 전체, 자유, 질문 +} diff --git a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java index 7d9849cbd..a9d80afcc 100644 --- a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java @@ -44,7 +44,12 @@ void connectDatabaseAndCheckTables() { () -> assertThat(isTableExist("LANGUAGE_REQUIREMENT")).isTrue(), () -> assertThat(isTableExist("UNIVERSITY")).isTrue(), () -> assertThat(isTableExist("LIKED_UNIVERSITY")).isTrue(), - () -> assertThat(isTableExist("UNIVERSITY_INFO_FOR_APPLY")).isTrue() + () -> assertThat(isTableExist("UNIVERSITY_INFO_FOR_APPLY")).isTrue(), + () -> assertThat(isTableExist("BOARD")).isTrue(), + () -> assertThat(isTableExist("COMMENT")).isTrue(), + () -> assertThat(isTableExist("POST")).isTrue(), + () -> assertThat(isTableExist("POST_IMAGE")).isTrue(), + () -> assertThat(isTableExist("POST_LIKE")).isTrue() ); } From 142abc174f04059d7e60949d946ce562cec5206f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:21:38 +0900 Subject: [PATCH 081/158] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 엔티티 클래스에서 빌더패턴 대신 생성자 사용 * feat: 연관관계 편의 메소드 작성 * feat: 커뮤니티 관련 DTO 정의 * feat: 커뮤니티 레포지토리 정의 * feat: 커뮤니티 서비스 로직 추가 * feat: 재귀 쿼리를 위한 Transient 필드 추가 및 댓글 서비스 로직 추가 * feat: 커뮤니티 컨트롤러 로직 추가 * feat: 커뮤니티 예외 추가 * feat: 게시글 조회수 동시성 처리 로직 추가 * feat: 게시판 데이터 추가 * feat: Mockito 의존성 추가 * test: 커뮤니티 레포지토리 테스트 추가 * test: 커뮤니티 서비스 테스트 추가 * test: 게시글 조회수 동시성 처리 테스트 추가 * feat: BaseEntity 동작을 위해 @EnableScheduling 어노테이션 추가 * feat: 로컬 환경에서 도커 컨테이너 빌드 자동화하는 쉘 스크립트 작성 * refactor: BoardCode 불일치 문제 해결 * refactor: 함수명 컨벤션에 따라 수정 * refactor: 불필요한 Getter 삭제 * feat: 스케줄링 작업 비동기로 전환 * feat: 조회수 갱신 작업 비동기로 전환 * feat: 조회수 조작 막는 코드 추가 * test: 조회수 조작 막는 테스트코드 추가 * refactor: 재사용성을 고려하여 RedisService 함수 리팩토링 * refactor: 중복코드 RedisUtils 함수화 --- .gitignore | 6 +- build.gradle | 3 +- docker-compose.local.yml | 14 +- local_compose_down.sh | 12 + local_compose_up.sh | 23 + .../SolidConnectionApplication.java | 2 + .../board/controller/BoardController.java | 45 ++ .../{entity => board/domain}/Board.java | 18 +- .../board/dto/PostFindBoardResponse.java | 15 + .../board/repository/BoardRepository.java | 30 + .../board/service/BoardService.java | 59 ++ .../comment/dto/PostFindCommentResponse.java | 36 ++ .../comment/repository/CommentRepository.java | 33 ++ .../comment/service/CommentService.java | 29 + .../config/redis/RedisConfig.java | 9 + .../config/scheduler/SchedulerConfig.java | 24 + .../config/sync/AsyncConfig.java | 23 + .../custom/exception/ErrorCode.java | 8 + .../dto/PostFindPostImageResponse.java | 24 + .../solidconnection/entity/Comment.java | 12 +- .../example/solidconnection/entity/Post.java | 54 -- .../solidconnection/entity/PostImage.java | 17 +- .../entity/mapping/PostLike.java | 6 +- .../post/controller/PostController.java | 79 +++ .../solidconnection/post/domain/Post.java | 106 ++++ .../post/dto/BoardFindPostResponse.java | 52 ++ .../post/dto/PostCreateRequest.java | 27 + .../post/dto/PostCreateResponse.java | 14 + .../post/dto/PostDeleteResponse.java | 6 + .../post/dto/PostFindResponse.java | 53 ++ .../post/dto/PostUpdateRequest.java | 8 + .../post/dto/PostUpdateResponse.java | 13 + .../post/repository/PostRepository.java | 28 + .../post/service/PostService.java | 172 ++++++ .../repositories/PostImageRepository.java | 9 + .../example/solidconnection/s3/S3Service.java | 49 +- .../scheduler/UpdateViewCountScheduler.java | 40 ++ .../solidconnection/service/RedisService.java | 43 ++ .../service/UpdateViewCountService.java | 32 ++ .../siteuser/domain/SiteUser.java | 9 +- .../dto/PostFindSiteUserResponse.java | 17 + .../solidconnection/type/BoardCode.java | 5 + .../example/solidconnection/type/ImgType.java | 2 +- .../solidconnection/type/RedisConstants.java | 18 + .../solidconnection/util/RedisUtils.java | 52 ++ src/main/resources/application-local.yml | 3 + src/main/resources/application-test.yml | 3 + src/main/resources/application.yml | 11 +- src/main/resources/data.sql | 83 +-- src/main/resources/scripts/incrViewCount.lua | 14 + .../PostViewCountConcurrencyTest.java | 171 ++++++ .../unit/repository/BoardRepositoryTest.java | 141 +++++ .../unit/repository/PostRepositoryTest.java | 139 +++++ .../unit/service/BoardServiceTest.java | 152 +++++ .../unit/service/PostServiceTest.java | 541 ++++++++++++++++++ 55 files changed, 2456 insertions(+), 138 deletions(-) create mode 100755 local_compose_down.sh create mode 100755 local_compose_up.sh create mode 100644 src/main/java/com/example/solidconnection/board/controller/BoardController.java rename src/main/java/com/example/solidconnection/{entity => board/domain}/Board.java (50%) create mode 100644 src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java create mode 100644 src/main/java/com/example/solidconnection/board/repository/BoardRepository.java create mode 100644 src/main/java/com/example/solidconnection/board/service/BoardService.java create mode 100644 src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java create mode 100644 src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java create mode 100644 src/main/java/com/example/solidconnection/comment/service/CommentService.java create mode 100644 src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java create mode 100644 src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java create mode 100644 src/main/java/com/example/solidconnection/dto/PostFindPostImageResponse.java delete mode 100644 src/main/java/com/example/solidconnection/entity/Post.java create mode 100644 src/main/java/com/example/solidconnection/post/controller/PostController.java create mode 100644 src/main/java/com/example/solidconnection/post/domain/Post.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java create mode 100644 src/main/java/com/example/solidconnection/post/repository/PostRepository.java create mode 100644 src/main/java/com/example/solidconnection/post/service/PostService.java create mode 100644 src/main/java/com/example/solidconnection/repositories/PostImageRepository.java create mode 100644 src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java create mode 100644 src/main/java/com/example/solidconnection/service/RedisService.java create mode 100644 src/main/java/com/example/solidconnection/service/UpdateViewCountService.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/PostFindSiteUserResponse.java create mode 100644 src/main/java/com/example/solidconnection/type/BoardCode.java create mode 100644 src/main/java/com/example/solidconnection/type/RedisConstants.java create mode 100644 src/main/java/com/example/solidconnection/util/RedisUtils.java create mode 100644 src/main/resources/scripts/incrViewCount.lua create mode 100644 src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java diff --git a/.gitignore b/.gitignore index ffed9d77d..9f59fa8d9 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,8 @@ out/ ### YML ### application-secret.yml -application-prod.yml \ No newline at end of file +application-prod.yml + +### docker volumes ### +mysql_data_local +redis_data_local diff --git a/build.gradle b/build.gradle index b3e68dd00..45902c5f4 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거 implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + testImplementation "org.mockito:mockito-core:3.3.3" compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' @@ -62,4 +63,4 @@ sourceSets { compileJava { options.annotationProcessorGeneratedSourcesDirectory = file('build/generated/sources/annotationProcessor/java/main') -} \ No newline at end of file +} diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 2102f390b..07fd75023 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: mysql: image: mysql:8.0 @@ -11,17 +9,13 @@ services: MYSQL_PASSWORD: solid_connection_local_password ports: - "3306:3306" -# volumes: -# - mysql_data_local:/var/lib/mysql + volumes: + - ./mysql_data_local:/var/lib/mysql redis: image: redis:latest container_name: solid-connection-local-redis ports: - "6379:6379" -# volumes: -# - redis_data_local:/data - -#volumes: -# mysql_data_local: -# redis_data_local: \ No newline at end of file + volumes: + - ./redis_data_local:/data diff --git a/local_compose_down.sh b/local_compose_down.sh new file mode 100755 index 000000000..e45cda18f --- /dev/null +++ b/local_compose_down.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +echo "Starting all docker containers..." +docker-compose -f docker-compose.local.yml down + +echo "Pruning unused Docker images..." +docker image prune -f + +echo "Containers are up and running." +docker-compose ps -a diff --git a/local_compose_up.sh b/local_compose_up.sh new file mode 100755 index 000000000..67ef9d0ba --- /dev/null +++ b/local_compose_up.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# 명령이 0이 아닌 종료값을 가질때 즉시 종료 +set -e + +if [ ! -d "mysql_data_local" ]; then + echo "mysql_data_local 디렉토리가 없습니다. 디렉토리를 생성합니다." + mkdir -p mysql_data_local +fi + +if [ ! -d "redis_data_local" ]; then + echo "redis_data_local 디렉토리가 없습니다. 디렉토리를 생성합니다." + mkdir -p redis_data_local +fi + +echo "Starting all docker containers..." +docker-compose -f docker-compose.local.yml up -d + +echo "Pruning unused Docker images..." +docker image prune -f + +echo "Containers are up and running." +docker-compose ps -a diff --git a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java index 81e27f96b..6727bdece 100644 --- a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java +++ b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java @@ -2,9 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling +@EnableJpaAuditing @SpringBootApplication public class SolidConnectionApplication { diff --git a/src/main/java/com/example/solidconnection/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/board/controller/BoardController.java new file mode 100644 index 000000000..29cfc249a --- /dev/null +++ b/src/main/java/com/example/solidconnection/board/controller/BoardController.java @@ -0,0 +1,45 @@ +package com.example.solidconnection.board.controller; + +import com.example.solidconnection.board.service.BoardService; +import com.example.solidconnection.post.dto.BoardFindPostResponse; +import com.example.solidconnection.type.BoardCode; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/communities") +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public class BoardController { + + private final BoardService boardService; + + // todo: 회원별로 접근 가능한 게시판 목록 조회 기능 개발 + @GetMapping() + public ResponseEntity findAccessibleCodes() { + List accessibleCodeList = new ArrayList<>(); + for (BoardCode boardCode : BoardCode.values()) { + accessibleCodeList.add(String.valueOf(boardCode)); + } + return ResponseEntity.ok().body(accessibleCodeList); + } + + @GetMapping("/{code}") + public ResponseEntity findPostsByCodeAndCategory( + @PathVariable(value = "code") String code, + @RequestParam(value = "category", defaultValue = "전체") String category) { + + List postsByCodeAndPostCategory = boardService + .findPostsByCodeAndPostCategory(code, category); + return ResponseEntity.ok().body(postsByCodeAndPostCategory); + } +} diff --git a/src/main/java/com/example/solidconnection/entity/Board.java b/src/main/java/com/example/solidconnection/board/domain/Board.java similarity index 50% rename from src/main/java/com/example/solidconnection/entity/Board.java rename to src/main/java/com/example/solidconnection/board/domain/Board.java index adef5b0bb..007553367 100644 --- a/src/main/java/com/example/solidconnection/entity/Board.java +++ b/src/main/java/com/example/solidconnection/board/domain/Board.java @@ -1,19 +1,15 @@ -package com.example.solidconnection.entity; +package com.example.solidconnection.board.domain; +import com.example.solidconnection.post.domain.Post; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.util.ArrayList; import java.util.List; @Entity @Getter -@Builder @NoArgsConstructor -@AllArgsConstructor public class Board { @Id @@ -23,7 +19,11 @@ public class Board { @Column(nullable = false, length = 20) private String koreanName; - @OneToMany(mappedBy = "board", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "board", cascade = CascadeType.ALL, orphanRemoval = true) private List postList = new ArrayList<>(); -} + public Board(String code, String koreanName) { + this.code = code; + this.koreanName = koreanName; + } +} diff --git a/src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java b/src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java new file mode 100644 index 000000000..b06baa305 --- /dev/null +++ b/src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.board.dto; + +import com.example.solidconnection.board.domain.Board; + +public record PostFindBoardResponse( + String code, + String koreanName +) { + public static PostFindBoardResponse from(Board board) { + return new PostFindBoardResponse( + board.getCode(), + board.getKoreanName() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/board/repository/BoardRepository.java b/src/main/java/com/example/solidconnection/board/repository/BoardRepository.java new file mode 100644 index 000000000..5c4538279 --- /dev/null +++ b/src/main/java/com/example/solidconnection/board/repository/BoardRepository.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.board.repository; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; + +@Repository +public interface BoardRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"postList"}) + Optional findBoardByCode(@Param("code") String code); + + default Board getByCodeUsingEntityGraph(String code) { + return findBoardByCode(code) + .orElseThrow(() -> new CustomException(ErrorCode.INVALID_BOARD_CODE)); + } + + default Board getByCode(String code) { + return findById(code) + .orElseThrow(() -> new CustomException(INVALID_BOARD_CODE)); + } +} diff --git a/src/main/java/com/example/solidconnection/board/service/BoardService.java b/src/main/java/com/example/solidconnection/board/service/BoardService.java new file mode 100644 index 000000000..3a74b919c --- /dev/null +++ b/src/main/java/com/example/solidconnection/board/service/BoardService.java @@ -0,0 +1,59 @@ +package com.example.solidconnection.board.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.BoardFindPostResponse; +import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.type.PostCategory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BoardService { + private final BoardRepository boardRepository; + + private String validateCode(String code) { + try { + return String.valueOf(BoardCode.valueOf(code)); + } catch (IllegalArgumentException ex) { + throw new CustomException(ErrorCode.INVALID_BOARD_CODE); + } + } + + private PostCategory validatePostCategory(String postCategory) { + try { + return PostCategory.valueOf(postCategory); + } catch (IllegalArgumentException ex) { + throw new CustomException(ErrorCode.INVALID_POST_CATEGORY); + } + } + + @Transactional(readOnly = true) + public List findPostsByCodeAndPostCategory(String code, String category) { + + String boardCode = validateCode(code); + PostCategory postCategory = validatePostCategory(category); + + Board board = boardRepository.getByCodeUsingEntityGraph(boardCode); + List postList = getPostListByPostCategory(board.getPostList(), postCategory); + + return BoardFindPostResponse.from(postList); + } + + private List getPostListByPostCategory(List postList, PostCategory postCategory) { + if (postCategory.equals(PostCategory.전체)) { + return postList; + } + return postList.stream() + .filter(post -> post.getCategory().equals(postCategory)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java new file mode 100644 index 000000000..75414f943 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java @@ -0,0 +1,36 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; + +import java.time.LocalDateTime; + +public record PostFindCommentResponse( + Long id, + Long parentId, + String content, + Boolean isOwner, + LocalDateTime createdAt, + LocalDateTime updatedAt, + PostFindSiteUserResponse postFindSiteUserResponse + +) { + public static PostFindCommentResponse from(Boolean isOwner, Comment comment) { + return new PostFindCommentResponse( + comment.getId(), + getParentCommentId(comment), + comment.getContent(), + isOwner, + comment.getCreatedAt(), + comment.getUpdatedAt(), + PostFindSiteUserResponse.from(comment.getSiteUser()) + ); + } + + private static Long getParentCommentId(Comment comment) { + if (comment.getParentComment() != null) { + return comment.getParentComment().getId(); + } + return null; + } +} diff --git a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java new file mode 100644 index 000000000..0b0d7152c --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java @@ -0,0 +1,33 @@ +package com.example.solidconnection.comment.repository; + +import com.example.solidconnection.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + + @Query(value = """ + WITH RECURSIVE CommentTree AS ( + SELECT + id, parent_id, post_id, site_user_id, content, + created_at, updated_at, + 0 AS level, CAST(id AS CHAR(255)) AS path + FROM comment + WHERE post_id = :postId AND parent_id IS NULL + UNION ALL + SELECT + c.id, c.parent_id, c.post_id, c.site_user_id, c.content, + c.created_at, c.updated_at, + ct.level + 1, CONCAT(ct.path, '->', c.id) + FROM comment c + INNER JOIN CommentTree ct ON c.parent_id = ct.id + ) + SELECT * FROM CommentTree + ORDER BY path + """, nativeQuery = true) + List findCommentTreeByPostId(@Param("postId") Long postId); + +} diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java new file mode 100644 index 000000000..9e32e4d32 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.comment.service; + +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.entity.Comment; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CommentService { + + private final CommentRepository commentRepository; + + private Boolean isOwner(Comment comment, String email) { + return comment.getSiteUser().getEmail().equals(email); + } + + + public List findCommentsByPostId(String email, Long postId) { + return commentRepository.findCommentTreeByPostId(postId) + .stream() + .map(comment -> PostFindCommentResponse.from(isOwner(comment, email), comment)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java index 9c416de9a..1aa671dcf 100644 --- a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java +++ b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java @@ -3,9 +3,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -36,4 +39,10 @@ public RedisTemplate redisTemplate() { redisTemplate.setConnectionFactory(redisConnectionFactory()); return redisTemplate; } + + @Bean(name = "incrViewCountScript") + public RedisScript incrViewCountLuaScript() { + Resource scriptSource = new ClassPathResource("scripts/incrViewCount.lua"); + return RedisScript.of(scriptSource, Long.class); + } } diff --git a/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java b/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java new file mode 100644 index 000000000..a52bf281a --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.config.scheduler; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +@Configuration +public class SchedulerConfig implements SchedulingConfigurer { + private final int POOL_SIZE = 5; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + + final ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(POOL_SIZE); + threadPoolTaskScheduler.setThreadNamePrefix("Scheduler-"); + threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true); + threadPoolTaskScheduler.setAwaitTerminationSeconds(10); + threadPoolTaskScheduler.initialize(); + + taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); + } +} diff --git a/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java b/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java new file mode 100644 index 000000000..738d26e04 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.config.sync; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +public class AsyncConfig { + + @Bean(name = "asyncExecutor") + public ThreadPoolTaskExecutor asyncExecutor() { + + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("Async-"); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(10); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index ea1507b04..6caf9edc2 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -51,6 +51,14 @@ public enum ErrorCode { CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2지망에 동일한 대학교를 입력할 수 없습니다."), CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 " + MIN_DAYS_BETWEEN_NICKNAME_CHANGES + "일이 지나지 않았습니다."), + // community + INVALID_POST_CATEGORY(HttpStatus.BAD_REQUEST.value(),"잘못된 카테고리명입니다."), + INVALID_BOARD_CODE(HttpStatus.BAD_REQUEST.value(), "잘못된 게시판 코드입니다."), + INVALID_POST_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글입니다."), + INVALID_POST_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 게시글만 제어할 수 있습니다."), + CAN_NOT_DELETE_OR_UPDATE_QUESTION(HttpStatus.BAD_REQUEST.value(), "질문글은 수정이나 삭제할 수 없습니다."), + CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES(HttpStatus.BAD_REQUEST.value(), "5개 이상의 파일을 업로드할 수 없습니다."), + // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/dto/PostFindPostImageResponse.java b/src/main/java/com/example/solidconnection/dto/PostFindPostImageResponse.java new file mode 100644 index 000000000..22a6a4af0 --- /dev/null +++ b/src/main/java/com/example/solidconnection/dto/PostFindPostImageResponse.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.dto; + +import com.example.solidconnection.entity.PostImage; + +import java.util.List; +import java.util.stream.Collectors; + +public record PostFindPostImageResponse( + Long id, + String url +) { + public static PostFindPostImageResponse from(PostImage postImage) { + return new PostFindPostImageResponse( + postImage.getId(), + postImage.getUrl() + ); + } + + public static List from(List postImageList) { + return postImageList.stream() + .map(PostFindPostImageResponse::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/example/solidconnection/entity/Comment.java b/src/main/java/com/example/solidconnection/entity/Comment.java index dd178fb9c..7b4ad87d8 100644 --- a/src/main/java/com/example/solidconnection/entity/Comment.java +++ b/src/main/java/com/example/solidconnection/entity/Comment.java @@ -1,10 +1,9 @@ package com.example.solidconnection.entity; import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,11 +12,16 @@ @Entity @Getter -@Builder @NoArgsConstructor -@AllArgsConstructor public class Comment extends BaseEntity { + // for recursive query + @Transient + private int level; + + @Transient + private String path; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/example/solidconnection/entity/Post.java b/src/main/java/com/example/solidconnection/entity/Post.java deleted file mode 100644 index 9a5e60069..000000000 --- a/src/main/java/com/example/solidconnection/entity/Post.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.example.solidconnection.entity; - -import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.entity.mapping.PostLike; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.type.PostCategory; -import jakarta.persistence.*; -import lombok.*; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -@ToString -public class Post extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(length = 255) - private String title; - - @Column(length = 1000) - private String content; - - private Boolean isQuestion; - - private Long likeCount; - - @Enumerated(EnumType.STRING) - private PostCategory category; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "board_code") - private Board board; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "site_user_id") - private SiteUser siteUser; - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) - private List commentList = new ArrayList<>(); - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) - private List postImageList = new ArrayList<>(); - - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) - private List postLikeList = new ArrayList<>(); -} diff --git a/src/main/java/com/example/solidconnection/entity/PostImage.java b/src/main/java/com/example/solidconnection/entity/PostImage.java index ddd866e38..9ab87853c 100644 --- a/src/main/java/com/example/solidconnection/entity/PostImage.java +++ b/src/main/java/com/example/solidconnection/entity/PostImage.java @@ -1,16 +1,13 @@ package com.example.solidconnection.entity; +import com.example.solidconnection.post.domain.Post; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@Builder @NoArgsConstructor -@AllArgsConstructor public class PostImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -22,4 +19,16 @@ public class PostImage { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; + + public PostImage(String url) { + this.url = url; + } + + public void setPost(Post post) { + if (this.post != null) { + this.post.getPostImageList().remove(this); + } + this.post = post; + post.getPostImageList().add(this); + } } diff --git a/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java b/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java index feba376b8..074806838 100644 --- a/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java +++ b/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java @@ -1,18 +1,14 @@ package com.example.solidconnection.entity.mapping; -import com.example.solidconnection.entity.Post; +import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@Builder @NoArgsConstructor -@AllArgsConstructor public class PostLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java new file mode 100644 index 000000000..022ca8b61 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -0,0 +1,79 @@ +package com.example.solidconnection.post.controller; + +import com.example.solidconnection.post.dto.*; +import com.example.solidconnection.post.service.PostService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.security.Principal; +import java.util.Collections; +import java.util.List; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/communities") +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public class PostController { + + private final PostService postService; + + @PostMapping(value = "/{code}/posts") + public ResponseEntity createPost( + Principal principal, + @PathVariable("code") String code, + @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, + @RequestParam(value = "file", required = false) List imageFile) { + + if (imageFile == null) { + imageFile = Collections.emptyList(); + } + PostCreateResponse post = postService + .createPost(principal.getName(), code, postCreateRequest, imageFile); + return ResponseEntity.ok().body(post); + } + + @PatchMapping(value = "/{code}/posts/{post_id}") + public ResponseEntity updatePost( + Principal principal, + @PathVariable("code") String code, + @PathVariable("post_id") Long postId, + @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, + @RequestParam(value = "file", required = false) List imageFile) { + + if (imageFile == null) { + imageFile = Collections.emptyList(); + } + PostUpdateResponse postUpdateResponse = postService + .updatePost(principal.getName(), code, postId, postUpdateRequest, imageFile); + return ResponseEntity.ok().body(postUpdateResponse); + } + + + @GetMapping("/{code}/posts/{post_id}") + public ResponseEntity findPostById( + Principal principal, + @PathVariable("code") String code, + @PathVariable("post_id") Long postId) { + + PostFindResponse postFindResponse = postService + .findPostById(principal.getName(), code, postId); + return ResponseEntity.ok().body(postFindResponse); + } + + @DeleteMapping(value = "/{code}/posts/{post_id}") + public ResponseEntity deletePostById( + Principal principal, + @PathVariable("code") String code, + @PathVariable("post_id") Long postId) { + + PostDeleteResponse postDeleteResponse = postService.deletePostById(principal.getName(), code, postId); + return ResponseEntity.ok().body(postDeleteResponse); + } +} diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/post/domain/Post.java new file mode 100644 index 000000000..646ac3995 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/domain/Post.java @@ -0,0 +1,106 @@ +package com.example.solidconnection.post.domain; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.entity.mapping.PostLike; +import com.example.solidconnection.post.dto.PostUpdateRequest; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.PostCategory; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.BatchSize; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class Post extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 255) + private String title; + + @Column(length = 1000) + private String content; + + private Boolean isQuestion; + + private Long likeCount; + + private Long viewCount; + + @Enumerated(EnumType.STRING) + private PostCategory category; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_code") + private Board board; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + @BatchSize(size = 20) + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List commentList = new ArrayList<>(); + + @BatchSize(size = 5) + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postImageList = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postLikeList = new ArrayList<>(); + + public Post(String title, String content, Boolean isQuestion, Long likeCount, Long viewCount, PostCategory category) { + this.title = title; + this.content = content; + this.isQuestion = isQuestion; + this.likeCount = likeCount; + this.viewCount = viewCount; + this.category = category; + } + + public void setBoardAndSiteUser(Board board, SiteUser siteUser) { + if (this.board != null) { + this.board.getPostList().remove(this); + } + this.board = board; + board.getPostList().add(this); + + if (this.siteUser != null) { + this.siteUser.getPostList().remove(this); + } + this.siteUser = siteUser; + siteUser.getPostList().add(this); + } + + public void resetBoardAndSiteUser() { + if (this.board != null) { + this.board.getPostList().remove(this); + this.board = null; + } + if (this.siteUser != null) { + this.siteUser.getPostList().remove(this); + this.siteUser = null; + } + } + + public void update(PostUpdateRequest postUpdateRequest) { + this.title = postUpdateRequest.title(); + this.content = postUpdateRequest.content(); + this.category = PostCategory.valueOf(postUpdateRequest.postCategory()); + } + + public void increaseViewCount(Long updateViewCount) { + this.viewCount += updateViewCount; + } + +} diff --git a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java b/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java new file mode 100644 index 000000000..89c931925 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java @@ -0,0 +1,52 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +public record BoardFindPostResponse( + Long id, + String title, + String content, + Long likeCount, + Integer commentCount, + LocalDateTime createdAt, + LocalDateTime updatedAt, + String postCategory, + String url +) { + + public static BoardFindPostResponse from(Post post) { + return new BoardFindPostResponse( + post.getId(), + post.getTitle(), + post.getContent(), + post.getLikeCount(), + getCommentCount(post), + post.getCreatedAt(), + post.getUpdatedAt(), + String.valueOf(post.getCategory()), + getFirstImageUrl(post) + ); + } + + private static int getCommentCount(Post post) { + return post.getCommentList().size(); + } + + private static String getFirstImageUrl(Post post) { + return post.getPostImageList().stream() + .findFirst() + .map(PostImage::getUrl) + .orElse(null); + } + + public static List from(List postList) { + return postList.stream() + .map(BoardFindPostResponse::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java new file mode 100644 index 000000000..13cd6469b --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.PostCategory; + +public record PostCreateRequest( + String postCategory, + String title, + String content, + Boolean isQuestion +) { + + public Post toEntity(SiteUser siteUser, Board board) { + Post post = new Post( + this.title, + this.content, + this.isQuestion, + 0L, + 0L, + PostCategory.valueOf(this.postCategory) + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java new file mode 100644 index 000000000..a514ffca6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.post.domain.Post; + +public record PostCreateResponse( + Long id +) { + + public static PostCreateResponse from(Post post) { + return new PostCreateResponse( + post.getId() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java new file mode 100644 index 000000000..23c67670d --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.post.dto; + +public record PostDeleteResponse( + Long id +) { +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java new file mode 100644 index 000000000..bbde1ba91 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.board.dto.PostFindBoardResponse; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.dto.*; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; + +import java.time.LocalDateTime; +import java.util.List; + +public record PostFindResponse( + Long id, + String title, + String content, + Boolean isQuestion, + Long likeCount, + Long viewCount, + Integer commentCount, + String postCategory, + Boolean isOwner, + LocalDateTime createdAt, + LocalDateTime updatedAt, + PostFindBoardResponse postFindBoardResponse, + PostFindSiteUserResponse postFindSiteUserResponse, + List postFindCommentResponses, + List postFindPostImageResponses +) { + + public static PostFindResponse from(Post post, Boolean isOwner, PostFindBoardResponse postFindBoardResponse, + PostFindSiteUserResponse postFindSiteUserResponse, + List postFindCommentResponses, + List postFindPostImageResponses + ) { + return new PostFindResponse( + post.getId(), + post.getTitle(), + post.getContent(), + post.getIsQuestion(), + post.getLikeCount(), + post.getViewCount(), + postFindCommentResponses.size(), + String.valueOf(post.getCategory()), + isOwner, + post.getCreatedAt(), + post.getUpdatedAt(), + postFindBoardResponse, + postFindSiteUserResponse, + postFindCommentResponses, + postFindPostImageResponses + ); + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java new file mode 100644 index 000000000..9394932d7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.post.dto; + +public record PostUpdateRequest( + String postCategory, + String title, + String content +) { +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java new file mode 100644 index 000000000..70d656766 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.post.domain.Post; + +public record PostUpdateResponse( + Long id +) { + public static PostUpdateResponse from(Post post) { + return new PostUpdateResponse( + post.getId() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java new file mode 100644 index 000000000..fda9cb166 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java @@ -0,0 +1,28 @@ +package com.example.solidconnection.post.repository; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; + +@Repository +public interface PostRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"postImageList", "board", "siteUser"}) + Optional findPostById(Long id); + + default Post getByIdUsingEntityGraph(Long id) { + return findPostById(id) + .orElseThrow(() -> new CustomException(INVALID_POST_ID)); + } + + default Post getById(Long id) { + return findById(id) + .orElseThrow(() -> new CustomException(INVALID_POST_ID)); + } +} diff --git a/src/main/java/com/example/solidconnection/post/service/PostService.java b/src/main/java/com/example/solidconnection/post/service/PostService.java new file mode 100644 index 000000000..52bd22310 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/service/PostService.java @@ -0,0 +1,172 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.dto.PostFindBoardResponse; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.dto.*; +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.*; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.util.RedisUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; + +@Service +@RequiredArgsConstructor +public class PostService { + private final PostRepository postRepository; + private final SiteUserRepository siteUserRepository; + private final BoardRepository boardRepository; + private final S3Service s3Service; + private final CommentService commentService; + private final RedisService redisService; + private final RedisUtils redisUtils; + + private String validateCode(String code) { + try { + return String.valueOf(BoardCode.valueOf(code)); + } catch (IllegalArgumentException ex) { + throw new CustomException(INVALID_BOARD_CODE); + } + } + + private void validateOwnership(Post post, String email) { + if (!post.getSiteUser().getEmail().equals(email)) { + throw new CustomException(INVALID_POST_ACCESS); + } + } + + private void validateFileSize(List imageFile) { + if (imageFile.isEmpty()) { + return; + } + if (imageFile.size() > 5) { + throw new CustomException(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES); + } + } + + private void validateQuestion(Post post) { + if (post.getIsQuestion()) { + throw new CustomException(CAN_NOT_DELETE_OR_UPDATE_QUESTION); + } + } + + private Boolean getIsOwner(Post post, String email) { + return post.getSiteUser().getEmail().equals(email); + } + + @Transactional + public PostCreateResponse createPost(String email, String code, PostCreateRequest postCreateRequest, + List imageFile) { + + // 유효성 검증 + String boardCode = validateCode(code); + validateFileSize(imageFile); + + // 객체 생성 + SiteUser siteUser = siteUserRepository.getByEmail(email); + Board board = boardRepository.getByCode(boardCode); + Post post = postCreateRequest.toEntity(siteUser, board); + // 이미지 처리 + savePostImages(imageFile, post); + Post createdPost = postRepository.save(post); + + return PostCreateResponse.from(createdPost); + } + + @Transactional + public PostUpdateResponse updatePost(String email, String code, Long postId, PostUpdateRequest postUpdateRequest, + List imageFile) { + + // 유효성 검증 + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + validateOwnership(post, email); + validateQuestion(post); + validateFileSize(imageFile); + + // 기존 사진 모두 삭제 + removePostImages(post); + // 새로운 이미지 등록 + savePostImages(imageFile, post); + // 게시글 내용 수정 + post.update(postUpdateRequest); + + return PostUpdateResponse.from(post); + } + + private void savePostImages(List imageFile, Post post) { + if (imageFile.isEmpty()) { + return; + } + List uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, ImgType.COMMUNITY); + for (UploadedFileUrlResponse uploadedFileUrlResponse : uploadedFileUrlResponseList) { + PostImage postImage = new PostImage(uploadedFileUrlResponse.fileUrl()); + postImage.setPost(post); + } + } + + private void removePostImages(Post post) { + for (PostImage postImage : post.getPostImageList()) { + s3Service.deletePostImage(postImage.getUrl()); + } + post.getPostImageList().clear(); + } + + @Transactional(readOnly = true) + public PostFindResponse findPostById(String email, String code, Long postId) { + + String boardCode = validateCode(code); + + Post post = postRepository.getByIdUsingEntityGraph(postId); + Boolean isOwner = getIsOwner(post, email); + + PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); + PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); + List postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList()); + List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); + + // caching && 어뷰징 방지 + if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email,postId))) { + redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(postId)); + } + + return PostFindResponse.from( + post, isOwner, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); + } + + @Transactional + public PostDeleteResponse deletePostById(String email, String code, Long postId) { + + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + validateOwnership(post, email); + validateQuestion(post); + + removePostImages(post); + post.resetBoardAndSiteUser(); + // cache out + redisService.deleteKey(redisUtils.getPostViewCountRedisKey(postId)); + postRepository.deleteById(post.getId()); + + return new PostDeleteResponse(postId); + } +} diff --git a/src/main/java/com/example/solidconnection/repositories/PostImageRepository.java b/src/main/java/com/example/solidconnection/repositories/PostImageRepository.java new file mode 100644 index 000000000..0ae776877 --- /dev/null +++ b/src/main/java/com/example/solidconnection/repositories/PostImageRepository.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.repositories; + +import com.example.solidconnection.entity.PostImage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostImageRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 49e515872..4bee94932 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -19,10 +19,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.UUID; +import java.util.*; import static com.example.solidconnection.custom.exception.ErrorCode.FILE_NOT_EXIST; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_FILE_EXTENSIONS; @@ -76,6 +73,40 @@ public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType i return new UploadedFileUrlResponse(amazonS3.getUrl(bucket, fileName).toString()); } + public List uploadFiles(List multipartFile, ImgType imageFile) { + + List uploadedFileUrlResponseList = new ArrayList<>(); + + for (MultipartFile file : multipartFile) { + // 파일 검증 + validateImgFile(file); + + // 메타데이터 생성 + String contentType = file.getContentType(); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(contentType); + metadata.setContentLength(file.getSize()); + + // 파일 이름 생성 + UUID randomUUID = UUID.randomUUID(); + String fileName = imageFile.getType() + "/" + randomUUID; + + try { + amazonS3.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), metadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (AmazonServiceException e) { + log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage()); + throw new CustomException(S3_SERVICE_EXCEPTION); + } catch (SdkClientException | IOException e) { + log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); + throw new CustomException(S3_CLIENT_EXCEPTION); + } + uploadedFileUrlResponseList.add(new UploadedFileUrlResponse(amazonS3.getUrl(bucket, fileName).toString())); + } + + return uploadedFileUrlResponseList; + } + private void validateImgFile(MultipartFile file) { if (file == null || file.isEmpty()) { throw new CustomException(FILE_NOT_EXIST); @@ -108,6 +139,11 @@ public void deleteExProfile(String email) { deleteFile(key); } + public void deletePostImage(String url) { + String key = getPostImageUrl(url); + deleteFile(key); + } + private void deleteFile(String fileName) { try { amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); @@ -126,4 +162,9 @@ private String getExProfileImageUrl(String email) { int domainStartIndex = fileName.indexOf(".com"); return fileName.substring(domainStartIndex + 5); } + + private String getPostImageUrl(String url) { + int domainStartIndex = url.indexOf(".com"); + return url.substring(domainStartIndex + 5); + } } diff --git a/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java b/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java new file mode 100644 index 000000000..41ba66398 --- /dev/null +++ b/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java @@ -0,0 +1,40 @@ +package com.example.solidconnection.scheduler; + +import com.example.solidconnection.service.UpdateViewCountService; +import com.example.solidconnection.util.RedisUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static com.example.solidconnection.type.RedisConstants.*; + +@RequiredArgsConstructor +@Component +@EnableScheduling +@EnableAsync +@Slf4j +public class UpdateViewCountScheduler { + + private final RedisUtils redisUtils; + private final ThreadPoolTaskExecutor asyncExecutor; + private final UpdateViewCountService updateViewCountService; + + @Async + @Scheduled(fixedDelayString = "${view.count.scheduling.delay}") + public void updateViewCount() { + + log.info("updateViewCount thread: {}", Thread.currentThread().getName()); + List itemViewCountKeys = redisUtils.getKeysOrderByExpiration(VIEW_COUNT_KEY_PATTERN.getValue()); + + itemViewCountKeys.forEach(key -> asyncExecutor.submit(() -> { + updateViewCountService.updateViewCount(key); + })); + } +} diff --git a/src/main/java/com/example/solidconnection/service/RedisService.java b/src/main/java/com/example/solidconnection/service/RedisService.java new file mode 100644 index 000000000..4776f1692 --- /dev/null +++ b/src/main/java/com/example/solidconnection/service/RedisService.java @@ -0,0 +1,43 @@ +package com.example.solidconnection.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import static com.example.solidconnection.type.RedisConstants.*; + +@Service +public class RedisService { + private final RedisTemplate redisTemplate; + private final RedisScript incrViewCountLuaScript; + + @Autowired + public RedisService(RedisTemplate redisTemplate, + @Qualifier("incrViewCountScript") RedisScript incrViewCountLuaScript) { + this.redisTemplate = redisTemplate; + this.incrViewCountLuaScript = incrViewCountLuaScript; + } + + // incr & set ttl -> lua + public void increaseViewCountSync(String key) { + redisTemplate.execute(incrViewCountLuaScript, Collections.singletonList(key), VIEW_COUNT_TTL.getValue()); + } + + public void deleteKey(String key){ + redisTemplate.opsForValue().getAndDelete(key); + } + + public Long getAndDelete(String key) { + return Long.valueOf(redisTemplate.opsForValue().getAndDelete(key)); + } + + public boolean isPresent(String key) { + return Boolean.TRUE.equals(redisTemplate.opsForValue() + .setIfAbsent(key, "1", Long.parseLong(VALIDATE_VIEW_COUNT_TTL.getValue()), TimeUnit.SECONDS)); + } +} diff --git a/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java b/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java new file mode 100644 index 000000000..46954fff6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java @@ -0,0 +1,32 @@ +package com.example.solidconnection.service; + +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.util.RedisUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +@EnableAsync +@Slf4j +public class UpdateViewCountService { + + private final PostRepository postRepository; + private final RedisService redisService; + private final RedisUtils redisUtils; + + @Transactional + @Async + public void updateViewCount(String key) { + log.info("updateViewCount Processing key: {} in thread: {}", key, Thread.currentThread().getName()); + Long postId = redisUtils.getPostIdFromPostViewCountRedisKey(key); + Post post = postRepository.getById(postId); + post.increaseViewCount(redisService.getAndDelete(key)); + log.info("updateViewCount Updated post id: {} with view count from key: {}", postId, key); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 7ebb828e1..2cd7dc4ff 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -1,7 +1,7 @@ package com.example.solidconnection.siteuser.domain; import com.example.solidconnection.entity.Comment; -import com.example.solidconnection.entity.Post; +import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.entity.mapping.PostLike; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; @@ -17,7 +17,6 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -@Builder @AllArgsConstructor public class SiteUser { @@ -57,13 +56,13 @@ public class SiteUser { @Setter private LocalDate quitedAt; - @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List postList = new ArrayList<>(); - @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List commentList = new ArrayList<>(); - @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List postLikeList = new ArrayList<>(); public SiteUser( diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/PostFindSiteUserResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/PostFindSiteUserResponse.java new file mode 100644 index 000000000..85d649631 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/PostFindSiteUserResponse.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.siteuser.dto; + +import com.example.solidconnection.siteuser.domain.SiteUser; + +public record PostFindSiteUserResponse( + Long id, + String nickname, + String profileImageUrl +) { + public static PostFindSiteUserResponse from(SiteUser siteUser) { + return new PostFindSiteUserResponse( + siteUser.getId(), + siteUser.getNickname(), + siteUser.getProfileImageUrl() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/type/BoardCode.java b/src/main/java/com/example/solidconnection/type/BoardCode.java new file mode 100644 index 000000000..0d161e941 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/BoardCode.java @@ -0,0 +1,5 @@ +package com.example.solidconnection.type; + +public enum BoardCode { + EUROPE, AMERICAS, ASIA, FREE; +} diff --git a/src/main/java/com/example/solidconnection/type/ImgType.java b/src/main/java/com/example/solidconnection/type/ImgType.java index 4cba0787c..45eb516bb 100644 --- a/src/main/java/com/example/solidconnection/type/ImgType.java +++ b/src/main/java/com/example/solidconnection/type/ImgType.java @@ -4,7 +4,7 @@ @Getter public enum ImgType { - PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"); + PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"), COMMUNITY("community"); private final String type; diff --git a/src/main/java/com/example/solidconnection/type/RedisConstants.java b/src/main/java/com/example/solidconnection/type/RedisConstants.java new file mode 100644 index 000000000..22d7762b1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/type/RedisConstants.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.type; + +import lombok.Getter; + +@Getter +public enum RedisConstants { + VIEW_COUNT_TTL("60"), + VALIDATE_VIEW_COUNT_TTL("1"), + VALIDATE_VIEW_COUNT_KEY_PREFIX("validate:post:view:"), + VIEW_COUNT_KEY_PREFIX("post:view:"), + VIEW_COUNT_KEY_PATTERN("post:view:*"); + + private final String value; + + RedisConstants(String value) { + this.value = value; + } +} diff --git a/src/main/java/com/example/solidconnection/util/RedisUtils.java b/src/main/java/com/example/solidconnection/util/RedisUtils.java new file mode 100644 index 000000000..7df79418e --- /dev/null +++ b/src/main/java/com/example/solidconnection/util/RedisUtils.java @@ -0,0 +1,52 @@ +package com.example.solidconnection.util; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.example.solidconnection.type.RedisConstants.VALIDATE_VIEW_COUNT_KEY_PREFIX; +import static com.example.solidconnection.type.RedisConstants.VIEW_COUNT_KEY_PREFIX; + +@Component +public class RedisUtils { + + private final RedisTemplate redisTemplate; + + @Autowired + public RedisUtils(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public List getKeysOrderByExpiration(String pattern) { + Set keys = redisTemplate.keys(pattern); + if (keys == null || keys.isEmpty()) { + return Collections.emptyList(); + } + return keys.stream() + .sorted(Comparator.comparingLong(this::getExpirationTime)) + .collect(Collectors.toList()); + } + + public Long getExpirationTime(String key) { + return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + } + + public String getPostViewCountRedisKey(Long postId) { + return VIEW_COUNT_KEY_PREFIX.getValue() + postId; + } + + public String getValidatePostViewCountRedisKey(String email, Long postId) { + return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + email; + } + + public Long getPostIdFromPostViewCountRedisKey(String key) { + return Long.parseLong(key.substring(VIEW_COUNT_KEY_PREFIX.getValue().length())); + } +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index fa7eebacb..f0baec690 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -10,6 +10,9 @@ spring: show-sql: true database: mysql defer-datasource-initialization: true + properties: + hibernate: + format_sql: true sql: init: diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 4dc996907..bf25bb62f 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -11,6 +11,9 @@ spring: show-sql: true database: mysql defer-datasource-initialization: true + properties: + hibernate: + format_sql: true datasource: url: jdbc:h2:mem:testdb diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 55193a7b3..22cc4368e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,6 +11,10 @@ spring: - test - secret + tomcat: + threads: + min-spare: 20 # default 10 + servlet: multipart: max-file-size: 10MB @@ -38,4 +42,9 @@ jwt: kakao: token_url: "https://kauth.kakao.com/oauth/token" - user_info_url: "https://kapi.kakao.com/v2/user/me" \ No newline at end of file + user_info_url: "https://kapi.kakao.com/v2/user/me" + +view: + count: + scheduling: + delay: 3000 diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 8c88643c6..5d2ce9a04 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -224,42 +224,47 @@ VALUES ('2024-1', 1, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대 '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월', NULL); INSERT INTO language_requirement(language_test_type, min_score, university_info_for_apply_id) -VALUES - ('TOEFL_IBT', '80', 1), - ('IELTS', '6.5', 1), - ('TOEFL_IBT', '80', 2), - ('IELTS', '6.5', 2), - ('TOEFL_IBT', '79', 3), - ('IELTS', '6.0', 3), - ('TOEFL_IBT', '88', 4), - ('IELTS', '6.5', 4), - ('TOEFL_IBT', '88', 5), - ('IELTS', '6.5', 5), - ('TOEFL_IBT', '85', 6), - ('IELTS', '6.5', 6), - ('TOEFL_IBT', '85', 7), - ('IELTS', '6.5', 7), - ('TOEFL_IBT', '80', 8), - ('IELTS', '6.0', 8), - ('TOEFL_IBT', '83', 9), - ('IELTS', '6.5', 9), - ('TOEFL_IBT', '87', 10), - ('IELTS', '6.5', 10), - ('TOEFL_IBT', '90', 11), - ('IELTS', '6.5', 11), - ('TOEFL_IBT', '85', 12), - ('IELTS', '6.5', 12), - ('TOEFL_IBT', '82', 13), - ('IELTS', '6.0', 13), - ('TOEFL_IBT', '85', 14), - ('IELTS', '6.5', 14), - ('TOEFL_IBT', '90', 15), - ('IELTS', '7.0', 15), - ('TOEFL_IBT', '85', 16), - ('IELTS', '6.5', 16), - ('DELF', 'B2', 17), - ('DALF', 'C1', 17), - ('JLPT', 'N2', 18), - ('JLPT', 'N1', 19), - ('TOEFL_IBT', '85', 20), - ('IELTS', '6.5', 20); +VALUES ('TOEFL_IBT', '80', 1), + ('IELTS', '6.5', 1), + ('TOEFL_IBT', '80', 2), + ('IELTS', '6.5', 2), + ('TOEFL_IBT', '79', 3), + ('IELTS', '6.0', 3), + ('TOEFL_IBT', '88', 4), + ('IELTS', '6.5', 4), + ('TOEFL_IBT', '88', 5), + ('IELTS', '6.5', 5), + ('TOEFL_IBT', '85', 6), + ('IELTS', '6.5', 6), + ('TOEFL_IBT', '85', 7), + ('IELTS', '6.5', 7), + ('TOEFL_IBT', '80', 8), + ('IELTS', '6.0', 8), + ('TOEFL_IBT', '83', 9), + ('IELTS', '6.5', 9), + ('TOEFL_IBT', '87', 10), + ('IELTS', '6.5', 10), + ('TOEFL_IBT', '90', 11), + ('IELTS', '6.5', 11), + ('TOEFL_IBT', '85', 12), + ('IELTS', '6.5', 12), + ('TOEFL_IBT', '82', 13), + ('IELTS', '6.0', 13), + ('TOEFL_IBT', '85', 14), + ('IELTS', '6.5', 14), + ('TOEFL_IBT', '90', 15), + ('IELTS', '7.0', 15), + ('TOEFL_IBT', '85', 16), + ('IELTS', '6.5', 16), + ('DELF', 'B2', 17), + ('DALF', 'C1', 17), + ('JLPT', 'N2', 18), + ('JLPT', 'N1', 19), + ('TOEFL_IBT', '85', 20), + ('IELTS', '6.5', 20); + +INSERT INTO board (code, korean_name) +VALUES ('EUROPE', '유럽권'), + ('AMERICAS', '미주권'), + ('ASIA', '아시아권'), + ('FREE', '자유게시판'); diff --git a/src/main/resources/scripts/incrViewCount.lua b/src/main/resources/scripts/incrViewCount.lua new file mode 100644 index 000000000..9421c0739 --- /dev/null +++ b/src/main/resources/scripts/incrViewCount.lua @@ -0,0 +1,14 @@ +-- Define the key and TTL (Time To Live) from KEYS and ARGV +local key = KEYS[1] +local ttl = tonumber(ARGV[1]) + +-- Attempt to set the key with value 1 if it does not already exist, and set TTL +local result = redis.call('SET', key, 1, 'NX', 'EX', ttl) + +-- If the key was set, it means it did not exist and we set it +if result then + return 1 +else + -- If the key was not set, it means it already existed, so increment the value + return redis.call('INCR', key) +end diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java new file mode 100644 index 000000000..9eb936301 --- /dev/null +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -0,0 +1,171 @@ +package com.example.solidconnection.concurrency; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.util.RedisUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static com.example.solidconnection.type.RedisConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@ActiveProfiles("test") +@DisplayName("게시글 조회수 동시성 테스트") +public class PostViewCountConcurrencyTest { + + @Autowired + private RedisService redisService; + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private RedisUtils redisUtils; + + @Value("${view.count.scheduling.delay}") + private int SCHEDULING_DELAY_MS; + private int THREAD_NUMS = 1000; + private int THREAD_POOL_SIZE = 200; + private int TIMEOUT_SECONDS = 10; + + private Post post; + private Board board; + private SiteUser siteUser; + + @BeforeEach + public void setUp() { + board = createBoard(); + boardRepository.save(board); + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + post = createPost(board, siteUser); + postRepository.save(post); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + + return post; + } + + @Test + public void 게시글을_조회할_때_조회수_동시성_문제를_해결한다() throws InterruptedException { + + ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); + CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); + + for (int i = 0; i < THREAD_NUMS; i++) { + executorService.submit(() -> { + try { + redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + } finally { + doneSignal.countDown(); + } + }); + } + + doneSignal.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!terminated) { + System.err.println("ExecutorService did not terminate in the expected time."); + } + + Thread.sleep(SCHEDULING_DELAY_MS); + + assertEquals(THREAD_NUMS, postRepository.getById(post.getId()).getViewCount()); + } + + @Test + public void 게시글을_조회할_때_조회수_조작_문제를_해결한다() throws InterruptedException { + + redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + + ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); + CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); + + for (int i = 0; i < THREAD_NUMS; i++) { + executorService.submit(() -> { + try { + boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + if (isFirstTime) { + redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + } + } finally { + doneSignal.countDown(); + } + }); + } + Thread.sleep(Long.parseLong(VALIDATE_VIEW_COUNT_TTL.getValue()) * 1000); + for (int i = 0; i < THREAD_NUMS; i++) { + executorService.submit(() -> { + try { + boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + if (isFirstTime) { + redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + } + } finally { + doneSignal.countDown(); + } + }); + } + + doneSignal.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!terminated) { + System.err.println("ExecutorService did not terminate in the expected time."); + } + + Thread.sleep(SCHEDULING_DELAY_MS); + + assertEquals(2L, postRepository.getById(post.getId()).getViewCount()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java new file mode 100644 index 000000000..c540954b1 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java @@ -0,0 +1,141 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@ActiveProfiles("test") +@DisplayName("게시판 레포지토리 테스트") +public class BoardRepositoryTest { + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private EntityManager entityManager; + + private Board board; + private SiteUser siteUser; + private Post post; + + @BeforeEach + public void setUp() { + board = createBoard(); + boardRepository.save(board); + + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + + post = createPost(board, siteUser); + post = postRepository.save(post); + + entityManager.flush(); + entityManager.clear(); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + @Test + @Transactional + public void 게시판을_조회할_때_게시글은_즉시_로딩한다() { + // when + Board foundBoard = boardRepository.getByCodeUsingEntityGraph(board.getCode()); + foundBoard.getPostList().size(); // 추가쿼리 발생하지 않는다. + + // then + assertThat(foundBoard.getCode()).isEqualTo(board.getCode()); + } + + @Test + @Transactional + public void 게시판을_조회할_때_게시글은_즉시_로딩한다_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // given + String invalidCode = "INVALID_CODE"; + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + boardRepository.getByCodeUsingEntityGraph(invalidCode); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_BOARD_CODE.getCode()); + } + + @Test + @Transactional + public void 게시판을_조회한다() { + // when + Board foundBoard = boardRepository.getByCode(board.getCode()); + + // then + assertEquals(board.getCode(), foundBoard.getCode()); + } + + @Test + @Transactional + public void 게시판을_조회할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // given + String invalidCode = "INVALID_CODE"; + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + boardRepository.getByCode(invalidCode); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_BOARD_CODE.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java new file mode 100644 index 000000000..b81ee952c --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -0,0 +1,139 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@ActiveProfiles("test") +@DisplayName("게시글 레포지토리 테스트") +public class PostRepositoryTest { + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + + private Post post; + private Board board; + private SiteUser siteUser; + + @BeforeEach + public void setUp() { + board = createBoard(); + boardRepository.save(board); + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + post = createPostWithImages(board, siteUser); + post = postRepository.save(post); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPostWithImages(Board board, SiteUser siteUser) { + Post postWithImages = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + postWithImages.setBoardAndSiteUser(board, siteUser); + + List postImageList = new ArrayList<>(); + postImageList.add(new PostImage("https://s3.example.com/test1.png")); + postImageList.add(new PostImage("https://s3.example.com/test2.png")); + for (PostImage postImage : postImageList) { + postImage.setPost(postWithImages); + } + return postWithImages; + } + + @Test + @Transactional + public void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { + Post foundPost = postRepository.getByIdUsingEntityGraph(post.getId()); + foundPost.getPostImageList().size(); // 추가쿼리 발생하지 않는다. + + assertThat(foundPost).isEqualTo(post); + } + + @Test + @Transactional + public void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // given + Long invalidId = -1L; + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + postRepository.getByIdUsingEntityGraph(invalidId); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + @Transactional + public void 게시글을_조회한다() { + Post foundPost = postRepository.getById(post.getId()); + + assertEquals(post, foundPost); + } + + @Test + @Transactional + public void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + Long invalidId = -1L; + + CustomException exception = assertThrows(CustomException.class, () -> { + postRepository.getById(invalidId); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java new file mode 100644 index 000000000..710546c9b --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java @@ -0,0 +1,152 @@ +package com.example.solidconnection.unit.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.board.service.BoardService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.BoardFindPostResponse; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +@DisplayName("게시판 서비스 테스트") +public class BoardServiceTest { + @InjectMocks + BoardService boardService; + @Mock + BoardRepository boardRepository; + + private SiteUser siteUser; + private Board board; + private List postList = new ArrayList<>(); + private List freePostList = new ArrayList<>(); + private List questionPostList = new ArrayList<>(); + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + board = createBoard("FREE", "자유게시판"); + + Post post_question_1 = createPost("질문", board, siteUser); + Post post_free_1 = createPost("자유", board, siteUser); + Post post_free_2 = createPost("자유", board, siteUser); + + postList.add(post_question_1); + postList.add(post_free_1); + postList.add(post_free_2); + questionPostList.add(post_question_1); + freePostList.add(post_free_1); + freePostList.add(post_free_2); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard(String code, String koreanName) { + return new Board(code, koreanName); + } + + private Post createPost(String postCategory, Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf(postCategory) + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + @Test + void 게시판을_조회할_때_게시판_코드와_게시글_카테고리에_따라서_조회한다() { + // Given + String category = "자유"; + when(boardRepository.getByCodeUsingEntityGraph(board.getCode())).thenReturn(board); + + // When + List responses = boardService.findPostsByCodeAndPostCategory(board.getCode(), category); + + // Then + List expectedResponses = freePostList.stream() + .map(BoardFindPostResponse::from) + .toList(); + assertIterableEquals(expectedResponses, responses); + verify(boardRepository, times(1)).getByCodeUsingEntityGraph(board.getCode()); + } + + @Test + void 게시판을_조회할_때_카테고리가_전체라면_해당_게시판의_모든_게시글을_조회한다() { + // Given + String category = "전체"; + when(boardRepository.getByCodeUsingEntityGraph(board.getCode())).thenReturn(board); + + // When + List responses = boardService.findPostsByCodeAndPostCategory(board.getCode(), category); + + // Then + List expectedResponses = postList.stream() + .map(BoardFindPostResponse::from) + .toList(); + assertIterableEquals(expectedResponses, responses); + verify(boardRepository, times(1)).getByCodeUsingEntityGraph(board.getCode()); + } + + @Test + void 게시판을_조회할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + String invalidCode = "INVALID_CODE"; + String category = "자유"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> { + boardService.findPostsByCodeAndPostCategory(invalidCode, category); + }); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시판을_조회할_때_유효한_카테고리가_아니라면_예외_응답을_반환한다() { + // Given + String invalidCategory = "INVALID_CATEGORY"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> { + boardService.findPostsByCodeAndPostCategory(board.getCode(), invalidCategory); + }); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_POST_CATEGORY.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_POST_CATEGORY.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java new file mode 100644 index 000000000..c04c1485a --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java @@ -0,0 +1,541 @@ +package com.example.solidconnection.unit.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.dto.PostFindBoardResponse; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.dto.PostFindPostImageResponse; +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.*; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.*; +import com.example.solidconnection.util.RedisUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.util.*; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; + + +@ExtendWith(MockitoExtension.class) +@DisplayName("게시글 서비스 테스트") +public class PostServiceTest { + @InjectMocks + PostService postService; + @Mock + PostRepository postRepository; + @Mock + SiteUserRepository siteUserRepository; + @Mock + BoardRepository boardRepository; + @Mock + S3Service s3Service; + @Mock + CommentService commentService; + @Mock + RedisService redisService; + @Mock + RedisUtils redisUtils; + + private SiteUser siteUser; + private Board board; + private Post post; + private Post postWithImages; + private Post questionPost; + private List imageFiles; + private List imageFilesWithMoreThanFiveFiles; + private List uploadedFileUrlResponseList; + + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + board = createBoard(); + imageFiles = createMockImageFiles(); + imageFilesWithMoreThanFiveFiles = createMockImageFilesWithMoreThanFiveFiles(); + uploadedFileUrlResponseList = createUploadedFileUrlResponses(); + post = createPost(board, siteUser); + postWithImages = createPostWithImages(board, siteUser); + questionPost = createQuestionPost(board, siteUser); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + + return post; + } + + private Post createPostWithImages(Board board, SiteUser siteUser) { + Post postWithImages = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + postWithImages.setBoardAndSiteUser(board, siteUser); + + List postImageList = new ArrayList<>(); + postImageList.add(new PostImage("https://s3.example.com/test1.png")); + postImageList.add(new PostImage("https://s3.example.com/test2.png")); + for (PostImage postImage : postImageList) { + postImage.setPost(postWithImages); + } + return postWithImages; + } + + private Post createQuestionPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + true, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private List createMockImageFiles() { + List multipartFileList = new ArrayList<>(); + multipartFileList.add(new MockMultipartFile("file1", "test1.png", + "image/png", "test image content 1".getBytes())); + multipartFileList.add(new MockMultipartFile("file2", "test1.png", + "image/png", "test image content 1".getBytes())); + return multipartFileList; + } + + private List createUploadedFileUrlResponses() { + return Arrays.asList( + new UploadedFileUrlResponse("https://s3.example.com/test1.png"), + new UploadedFileUrlResponse("https://s3.example.com/test2.png") + ); + } + + private List createMockImageFilesWithMoreThanFiveFiles() { + List multipartFileList = new ArrayList<>(); + multipartFileList.add(new MockMultipartFile("file1", "test1.png", + "image/png", "test image content 1".getBytes())); + multipartFileList.add(new MockMultipartFile("file2", "test1.png", + "image/png", "test image content 1".getBytes())); + multipartFileList.add(new MockMultipartFile("file3", "test1.png", + "image/png", "test image content 1".getBytes())); + multipartFileList.add(new MockMultipartFile("file4", "test1.png", + "image/png", "test image content 1".getBytes())); + multipartFileList.add(new MockMultipartFile("file5", "test1.png", + "image/png", "test image content 1".getBytes())); + multipartFileList.add(new MockMultipartFile("file6", "test1.png", + "image/png", "test image content 1".getBytes())); + return multipartFileList; + } + + /** + * 게시글 등록 + */ + @Test + void 게시글을_등록한다_이미지_있음() { + // Given + PostCreateRequest postCreateRequest = new PostCreateRequest( + "자유", "title", "content", false); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(boardRepository.getByCode(board.getCode())).thenReturn(board); + when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); + when(postRepository.save(any(Post.class))).thenReturn(postWithImages); + + // When + PostCreateResponse postCreateResponse = postService.createPost( + siteUser.getEmail(), board.getCode(), postCreateRequest, imageFiles); + + // Then + assertEquals(postCreateResponse, PostCreateResponse.from(postWithImages)); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(boardRepository, times(1)).getByCode(board.getCode()); + verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); + verify(postRepository, times(1)).save(any(Post.class)); + } + + @Test + void 게시글을_등록한다_이미지_없음() { + // Given + PostCreateRequest postCreateRequest = new PostCreateRequest( + "자유", "title", "content", false); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(boardRepository.getByCode(board.getCode())).thenReturn(board); + when(postRepository.save(postCreateRequest.toEntity(siteUser, board))).thenReturn(post); + + // When + PostCreateResponse postCreateResponse = postService.createPost( + siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList()); + + // Then + assertEquals(postCreateResponse, PostCreateResponse.from(post)); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(boardRepository, times(1)).getByCode(board.getCode()); + verify(postRepository, times(1)).save(postCreateRequest.toEntity(siteUser, board)); + } + + @Test + void 게시글을_등록할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + String invalidBoardCode = "INVALID_CODE"; + PostCreateRequest postCreateRequest = new PostCreateRequest( + "자유", "title", "content", false); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> postService + .createPost(siteUser.getEmail(), invalidBoardCode, postCreateRequest, Collections.emptyList())); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시글을_등록할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { + // Given + PostCreateRequest postCreateRequest = new PostCreateRequest( + "자유", "title", "content", false); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> postService + .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, imageFilesWithMoreThanFiveFiles)); + assertThat(exception.getMessage()) + .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getCode()); + } + + /** + * 게시글 수정 + */ + @Test + void 게시글을_수정한다_기존_사진_없음_수정_사진_없음() { + // Given + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("질문", "updateTitle", "updateContent"); + when(postRepository.getById(post.getId())).thenReturn(post); + + // When + PostUpdateResponse response = postService.updatePost( + siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, Collections.emptyList()); + + // Then + assertEquals(response, PostUpdateResponse.from(post)); + verify(postRepository, times(1)).getById(post.getId()); + verify(s3Service, times(0)).deletePostImage(any(String.class)); + verify(s3Service, times(0)).uploadFiles(anyList(), any(ImgType.class)); + } + + @Test + void 게시글을_수정한다_기존_사진_있음_수정_사진_없음() { + // Given + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); + when(postRepository.getById(postWithImages.getId())).thenReturn(postWithImages); + + // When + PostUpdateResponse response = postService.updatePost( + siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, Collections.emptyList()); + + // Then + assertEquals(response, PostUpdateResponse.from(postWithImages)); + verify(postRepository, times(1)).getById(postWithImages.getId()); + verify(s3Service, times(imageFiles.size())).deletePostImage(any(String.class)); + verify(s3Service, times(0)).uploadFiles(anyList(), any(ImgType.class)); + } + + @Test + void 게시글을_수정한다_기존_사진_없음_수정_사진_있음() { + // Given + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); + when(postRepository.getById(post.getId())).thenReturn(post); + when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); + + // When + PostUpdateResponse response = postService.updatePost( + siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFiles); + + // Then + assertEquals(response, PostUpdateResponse.from(post)); + verify(postRepository, times(1)).getById(post.getId()); + verify(s3Service, times(0)).deletePostImage(any(String.class)); + verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); + } + + @Test + void 게시글을_수정한다_기존_사진_있음_수정_사진_있음() { + // Given + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); + when(postRepository.getById(postWithImages.getId())).thenReturn(postWithImages); + when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); + + // When + PostUpdateResponse response = postService.updatePost( + siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, imageFiles); + + // Then + assertEquals(response, PostUpdateResponse.from(postWithImages)); + verify(postRepository, times(1)).getById(postWithImages.getId()); + verify(s3Service, times(imageFiles.size())).deletePostImage(any(String.class)); + verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); + } + + @Test + void 게시글을_수정할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); + String invalidBoardCode = "INVALID_CODE"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.updatePost(siteUser.getEmail(), invalidBoardCode, post.getId(), postUpdateRequest, imageFiles)); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시글을_수정할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.updatePost(siteUser.getEmail(), board.getCode(), invalidPostId, postUpdateRequest, imageFiles)); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 게시글을_수정할_때_본인의_게시글이_아니라면_예외_응답을_반환한다() { + // Given + String invalidEmail = "invalidEmail@example.com"; + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); + when(postRepository.getById(post.getId())).thenReturn(post); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.updatePost(invalidEmail, board.getCode(), post.getId(), postUpdateRequest, imageFiles)); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ACCESS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ACCESS.getCode()); + } + + @Test + void 게시글을_수정할_때_질문글_이라면_예외_응답을_반환한다() { + // Given + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); + when(postRepository.getById(questionPost.getId())).thenReturn(questionPost); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.updatePost(siteUser.getEmail(), board.getCode(), questionPost.getId(), postUpdateRequest, imageFiles)); + assertThat(exception.getMessage()) + .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); + } + + + @Test + void 게시글을_수정할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { + // Given + PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); + when(postRepository.getById(post.getId())).thenReturn(post); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.updatePost(siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFilesWithMoreThanFiveFiles)); + assertThat(exception.getMessage()) + .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getCode()); + } + + /** + * 게시글 조회 + */ + @Test + void 게시글을_찾는다() { + // Given + List commentFindResultDTOList = new ArrayList<>(); + when(postRepository.getByIdUsingEntityGraph(post.getId())).thenReturn(post); + when(commentService.findCommentsByPostId(siteUser.getEmail(), post.getId())).thenReturn(commentFindResultDTOList); + + // When + PostFindResponse response = postService.findPostById(siteUser.getEmail(), board.getCode(), post.getId()); + + // Then + PostFindResponse expectedResponse = PostFindResponse.from( + post, + true, + PostFindBoardResponse.from(post.getBoard()), + PostFindSiteUserResponse.from(post.getSiteUser()), + commentFindResultDTOList, + PostFindPostImageResponse.from(post.getPostImageList()) + ); + assertEquals(expectedResponse, response); + verify(postRepository, times(1)).getByIdUsingEntityGraph(post.getId()); + verify(commentService, times(1)).findCommentsByPostId(siteUser.getEmail(), post.getId()); + } + + @Test + void 게시글을_찾을_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + String invalidBoardCode = "INVALID_CODE"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.findPostById(siteUser.getEmail(), invalidBoardCode, post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시글을_찾을_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(postRepository.getByIdUsingEntityGraph(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.findPostById(siteUser.getEmail(), board.getCode(), invalidPostId)); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + /** + * 게시글 삭제 + */ + @Test + void 게시글을_삭제한다() { + // Give + when(postRepository.getById(post.getId())).thenReturn(post); + + // When + PostDeleteResponse postDeleteResponse = postService.deletePostById(siteUser.getEmail(), board.getCode(), post.getId()); + + // Then + assertEquals(postDeleteResponse.id(), post.getId()); + verify(postRepository, times(1)).getById(post.getId()); + verify(redisService, times(1)).deleteKey(redisUtils.getPostViewCountRedisKey(post.getId())); + verify(postRepository, times(1)).deleteById(post.getId()); + } + + @Test + void 게시글을_삭제할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + String invalidBoardCode = "INVALID_CODE"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.deletePostById(siteUser.getEmail(), invalidBoardCode, post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.deletePostById(siteUser.getEmail(), board.getCode(), invalidPostId)); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_POST_ID.getCode()); + } + + @Test + void 게시글을_삭제할_때_자신의_게시글이_아니라면_예외_응답을_반환한다() { + // Given + String invalidEmail = "invalidEmail@example.com"; + when(postRepository.getById(post.getId())).thenReturn(post); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.deletePostById(invalidEmail, board.getCode(), post.getId()) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ACCESS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ACCESS.getCode()); + } + + @Test + void 게시글을_삭제할_때_질문글_이라면_예외_응답을_반환한다() { + when(postRepository.getById(questionPost.getId())).thenReturn(questionPost); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.deletePostById(siteUser.getEmail(), board.getCode(), questionPost.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); + } +} From f96c06ca3122440d4e87a2c66f07d53b3cce59b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:54:06 +0900 Subject: [PATCH 082/158] =?UTF-8?q?=EB=8C=93=EA=B8=80=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 댓글 엔티티 정의 * refactor: 댓글 엔티티 폴더 이동 * feat: 댓글 레포지토리 로직 추가 * feat: 댓글 서비스 로직 추가 * feat: 댓글 컨트롤러 로직 추가 * feat: 댓글 관련 DTO 정의 * feat: 댓글 관련 예외 정의 * test: 댓글 레포지토리 테스트 로직 추가 * test: 댓글 서비스 테스트 로직 추가 * feat: 게시글 API 파라미터 검증 로직 추가 * refactor: 조회수 갱신 함수 함수명 수정 * refactor: 테스트 클래스 접근제한자 수정 * refactor: 댓글 생성시 parentId 파라미터 null 허용을 위해 record를 class로 수정 --- .../board/service/BoardService.java | 12 +- .../comment/controller/CommentController.java | 62 +++ .../comment/domain/Comment.java | 111 +++++ .../comment/dto/CommentCreateRequest.java | 39 ++ .../comment/dto/CommentCreateResponse.java | 14 + .../comment/dto/CommentDeleteResponse.java | 6 + .../comment/dto/CommentUpdateRequest.java | 12 + .../comment/dto/CommentUpdateResponse.java | 14 + .../comment/dto/PostFindCommentResponse.java | 2 +- .../comment/repository/CommentRepository.java | 8 +- .../comment/service/CommentService.java | 75 +++- .../custom/exception/ErrorCode.java | 3 + .../solidconnection/entity/Comment.java | 46 -- .../post/controller/PostController.java | 5 +- .../solidconnection/post/domain/Post.java | 2 +- .../post/dto/PostCreateRequest.java | 9 + .../post/dto/PostUpdateRequest.java | 9 + .../post/service/PostService.java | 11 +- .../solidconnection/service/RedisService.java | 2 +- .../siteuser/domain/SiteUser.java | 4 +- .../PostViewCountConcurrencyTest.java | 12 +- .../unit/repository/BoardRepositoryTest.java | 2 +- .../repository/CommentRepositoryTest.java | 156 +++++++ .../unit/repository/PostRepositoryTest.java | 2 +- .../unit/service/BoardServiceTest.java | 2 +- .../unit/service/CommentServiceTest.java | 423 ++++++++++++++++++ .../unit/service/PostServiceTest.java | 18 +- 27 files changed, 990 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/comment/controller/CommentController.java create mode 100644 src/main/java/com/example/solidconnection/comment/domain/Comment.java create mode 100644 src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java create mode 100644 src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java create mode 100644 src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java create mode 100644 src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java create mode 100644 src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java delete mode 100644 src/main/java/com/example/solidconnection/entity/Comment.java create mode 100644 src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java diff --git a/src/main/java/com/example/solidconnection/board/service/BoardService.java b/src/main/java/com/example/solidconnection/board/service/BoardService.java index 3a74b919c..1ec5ac8b0 100644 --- a/src/main/java/com/example/solidconnection/board/service/BoardService.java +++ b/src/main/java/com/example/solidconnection/board/service/BoardService.java @@ -9,12 +9,15 @@ import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.PostCategory; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; + @Service @RequiredArgsConstructor public class BoardService { @@ -28,12 +31,11 @@ private String validateCode(String code) { } } - private PostCategory validatePostCategory(String postCategory) { - try { - return PostCategory.valueOf(postCategory); - } catch (IllegalArgumentException ex) { - throw new CustomException(ErrorCode.INVALID_POST_CATEGORY); + private PostCategory validatePostCategory(String category){ + if(!EnumUtils.isValidEnum(PostCategory.class, category)){ + throw new CustomException(INVALID_POST_CATEGORY); } + return PostCategory.valueOf(category); } @Transactional(readOnly = true) diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java new file mode 100644 index 000000000..61bae1036 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.comment.controller; + +import com.example.solidconnection.comment.dto.*; +import com.example.solidconnection.comment.service.CommentService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/posts") +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public class CommentController { + + private final CommentService commentService; + + @PostMapping("/{post_id}/comments") + public ResponseEntity createComment( + Principal principal, + @PathVariable("post_id") Long postId, + @Valid @RequestBody CommentCreateRequest commentCreateRequest + ) { + + CommentCreateResponse commentCreateResponse = commentService.createComment( + principal.getName(), postId, commentCreateRequest); + return ResponseEntity.ok().body(commentCreateResponse); + } + + @PatchMapping("/{post_id}/comments/{comment_id}") + public ResponseEntity updateComment( + Principal principal, + @PathVariable("post_id") Long postId, + @PathVariable("comment_id") Long commentId, + @Valid @RequestBody CommentUpdateRequest commentUpdateRequest + ) { + + CommentUpdateResponse commentUpdateResponse = commentService.updateComment( + principal.getName(), postId, commentId, commentUpdateRequest + ); + return ResponseEntity.ok().body(commentUpdateResponse); + } + + @DeleteMapping("/{post_id}/comments/{comment_id}") + public ResponseEntity deleteCommentById( + Principal principal, + @PathVariable("post_id") Long postId, + @PathVariable("comment_id") Long commentId + ) { + + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById(principal.getName(), postId, commentId); + return ResponseEntity.ok().body(commentDeleteResponse); + } + +} diff --git a/src/main/java/com/example/solidconnection/comment/domain/Comment.java b/src/main/java/com/example/solidconnection/comment/domain/Comment.java new file mode 100644 index 000000000..774c01123 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/domain/Comment.java @@ -0,0 +1,111 @@ +package com.example.solidconnection.comment.domain; + +import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class Comment extends BaseEntity { + + // for recursive query + @Transient + private int level; + + @Transient + private String path; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 255) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Comment parentComment; + + @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) + private List commentList = new ArrayList<>(); + + public Comment(String content) { + this.content = content; + } + + public void setParentCommentAndPostAndSiteUser(Comment parentComment, Post post, SiteUser siteUser) { + + if (this.parentComment != null) { + this.parentComment.getCommentList().remove(this); + } + this.parentComment = parentComment; + parentComment.getCommentList().add(this); + + if (this.post != null) { + this.post.getCommentList().remove(this); + } + this.post = post; + post.getCommentList().add(this); + + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + } + this.siteUser = siteUser; + siteUser.getCommentList().add(this); + } + + public void setPostAndSiteUser(Post post, SiteUser siteUser) { + + if (this.post != null) { + this.post.getCommentList().remove(this); + } + this.post = post; + post.getCommentList().add(this); + + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + } + this.siteUser = siteUser; + siteUser.getCommentList().add(this); + } + + public void resetPostAndSiteUserAndParentComment() { + if (this.post != null) { + this.post.getCommentList().remove(this); + this.post = null; + } + if (this.siteUser != null) { + this.siteUser.getCommentList().remove(this); + this.siteUser = null; + } + if (this.parentComment != null) { + this.parentComment.getCommentList().remove(this); + this.parentComment = null; + } + } + + public void updateContent(String content) { + this.content = content; + } + + public void deprecateComment() { + this.content = null; + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java new file mode 100644 index 000000000..c16c77323 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java @@ -0,0 +1,39 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +@Getter +public class CommentCreateRequest { + + @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") + String content; + + @Nullable + Long parentId; + + public CommentCreateRequest(String content, @Nullable Long parentId) { + this.content = content; + this.parentId = parentId; + } + + public Comment toEntity(SiteUser siteUser, Post post, Comment parentComment) { + + Comment comment = new Comment( + this.content + ); + + if (parentComment == null) { + comment.setPostAndSiteUser(post, siteUser); + } else { + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); + } + return comment; + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java new file mode 100644 index 000000000..60d7529c2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; + +public record CommentCreateResponse( + Long id +) { + + public static CommentCreateResponse from(Comment comment) { + return new CommentCreateResponse( + comment.getId() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java new file mode 100644 index 000000000..393e4fe8b --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.comment.dto; + +public record CommentDeleteResponse( + Long id +) { +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java new file mode 100644 index 000000000..23ae16118 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CommentUpdateRequest( + @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") + String content +) { + +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java new file mode 100644 index 000000000..b621ab111 --- /dev/null +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.comment.dto; + +import com.example.solidconnection.comment.domain.Comment; + +public record CommentUpdateResponse( + Long id +) { + + public static CommentUpdateResponse from(Comment comment) { + return new CommentUpdateResponse( + comment.getId() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java index 75414f943..8524eb95a 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java +++ b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java @@ -1,6 +1,6 @@ package com.example.solidconnection.comment.dto; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java index 0b0d7152c..b78011903 100644 --- a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java @@ -1,12 +1,14 @@ package com.example.solidconnection.comment.repository; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.custom.exception.CustomException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; public interface CommentRepository extends JpaRepository { @Query(value = """ @@ -30,4 +32,8 @@ WITH RECURSIVE CommentTree AS ( """, nativeQuery = true) List findCommentTreeByPostId(@Param("postId") Long postId); + default Comment getById(Long id) { + return findById(id) + .orElseThrow(() -> new CustomException(INVALID_COMMENT_ID)); + } } diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 9e32e4d32..4fde0c1cf 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -1,29 +1,100 @@ package com.example.solidconnection.comment.service; +import com.example.solidconnection.comment.dto.*; import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; + @Service @RequiredArgsConstructor public class CommentService { private final CommentRepository commentRepository; + private final SiteUserRepository siteUserRepository; + private final PostRepository postRepository; private Boolean isOwner(Comment comment, String email) { return comment.getSiteUser().getEmail().equals(email); } + private void validateOwnership(Comment comment, String email) { + if (!comment.getSiteUser().getEmail().equals(email)) { + throw new CustomException(INVALID_POST_ACCESS); + } + } + private void validateDeprecated(Comment comment) { + if (comment.getContent() == null) { + throw new CustomException(CAN_NOT_UPDATE_DEPRECATED_COMMENT); + } + } + + @Transactional(readOnly = true) public List findCommentsByPostId(String email, Long postId) { return commentRepository.findCommentTreeByPostId(postId) .stream() .map(comment -> PostFindCommentResponse.from(isOwner(comment, email), comment)) .collect(Collectors.toList()); } + + @Transactional + public CommentCreateResponse createComment(String email, Long postId, CommentCreateRequest commentCreateRequest) { + + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + + Comment parentComment = Optional.ofNullable(commentCreateRequest.getParentId()) + .map(commentRepository::getById) + .orElse(null); + Comment createdComment = commentRepository.save(commentCreateRequest.toEntity(siteUser, post, parentComment)); + + return CommentCreateResponse.from(createdComment); + } + + @Transactional + public CommentUpdateResponse updateComment(String email, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { + + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + Comment comment = commentRepository.getById(commentId); + validateDeprecated(comment); + validateOwnership(comment, email); + + comment.updateContent(commentUpdateRequest.content()); + + return CommentUpdateResponse.from(comment); + } + + @Transactional + public CommentDeleteResponse deleteCommentById(String email, Long postId, Long commentId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + Post post = postRepository.getById(postId); + Comment comment = commentRepository.getById(commentId); + validateOwnership(comment, email); + + if (comment.getCommentList().isEmpty()) { + // 하위 댓글이 없다면 삭제한다. + comment.resetPostAndSiteUserAndParentComment(); + commentRepository.deleteById(commentId); + } else { + // 하위 댓글 있으면 value만 null로 수정한다. + comment.deprecateComment(); + } + + return new CommentDeleteResponse(commentId); + } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 6caf9edc2..0a9df0b61 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -58,6 +58,9 @@ public enum ErrorCode { INVALID_POST_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 게시글만 제어할 수 있습니다."), CAN_NOT_DELETE_OR_UPDATE_QUESTION(HttpStatus.BAD_REQUEST.value(), "질문글은 수정이나 삭제할 수 없습니다."), CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES(HttpStatus.BAD_REQUEST.value(), "5개 이상의 파일을 업로드할 수 없습니다."), + INVALID_COMMENT_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 댓글입니다."), + INVALID_COMMENT_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 댓글만 제어할 수 있습니다."), + CAN_NOT_UPDATE_DEPRECATED_COMMENT(HttpStatus.BAD_REQUEST.value(),"이미 삭제된 댓글을 수정할 수 없습니다."), // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/entity/Comment.java b/src/main/java/com/example/solidconnection/entity/Comment.java deleted file mode 100644 index 7b4ad87d8..000000000 --- a/src/main/java/com/example/solidconnection/entity/Comment.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.example.solidconnection.entity; - -import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@NoArgsConstructor -public class Comment extends BaseEntity { - - // for recursive query - @Transient - private int level; - - @Transient - private String path; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(length = 255) - private String content; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "site_user_id") - private SiteUser siteUser; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parent_id") - private Comment parentComment; - - @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL) - private List commentList = new ArrayList<>(); -} diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index 022ca8b61..fd452af8a 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -4,6 +4,7 @@ import com.example.solidconnection.post.service.PostService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -28,7 +29,7 @@ public class PostController { public ResponseEntity createPost( Principal principal, @PathVariable("code") String code, - @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, + @Valid @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, @RequestParam(value = "file", required = false) List imageFile) { if (imageFile == null) { @@ -44,7 +45,7 @@ public ResponseEntity updatePost( Principal principal, @PathVariable("code") String code, @PathVariable("post_id") Long postId, - @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, + @Valid @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, @RequestParam(value = "file", required = false) List imageFile) { if (imageFile == null) { diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/post/domain/Post.java index 646ac3995..12e43195f 100644 --- a/src/main/java/com/example/solidconnection/post/domain/Post.java +++ b/src/main/java/com/example/solidconnection/post/domain/Post.java @@ -1,7 +1,7 @@ package com.example.solidconnection.post.domain; import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.entity.common.BaseEntity; import com.example.solidconnection.entity.mapping.PostLike; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java index 13cd6469b..03ab79686 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java @@ -4,11 +4,20 @@ import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; public record PostCreateRequest( + @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, + @NotBlank(message = "게시글 제목은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String title, + @NotBlank(message = "게시글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 1000, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content, + @NotNull(message = "게시글 질문여부를 설정해주세요.") Boolean isQuestion ) { diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java index 9394932d7..b82b73685 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java @@ -1,8 +1,17 @@ package com.example.solidconnection.post.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + public record PostUpdateRequest( + @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, + @NotBlank(message = "게시글 제목은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String title, + @NotBlank(message = "게시글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 1000, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content ) { } diff --git a/src/main/java/com/example/solidconnection/post/service/PostService.java b/src/main/java/com/example/solidconnection/post/service/PostService.java index 52bd22310..d4ed3c081 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostService.java @@ -19,8 +19,10 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -69,6 +71,12 @@ private void validateQuestion(Post post) { } } + private void validatePostCategory(String category){ + if(!EnumUtils.isValidEnum(PostCategory.class, category)){ + throw new CustomException(INVALID_POST_CATEGORY); + } + } + private Boolean getIsOwner(Post post, String email) { return post.getSiteUser().getEmail().equals(email); } @@ -79,6 +87,7 @@ public PostCreateResponse createPost(String email, String code, PostCreateReques // 유효성 검증 String boardCode = validateCode(code); + validatePostCategory(postCreateRequest.postCategory()); validateFileSize(imageFile); // 객체 생성 @@ -146,7 +155,7 @@ public PostFindResponse findPostById(String email, String code, Long postId) { // caching && 어뷰징 방지 if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email,postId))) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(postId)); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); } return PostFindResponse.from( diff --git a/src/main/java/com/example/solidconnection/service/RedisService.java b/src/main/java/com/example/solidconnection/service/RedisService.java index 4776f1692..9816a264e 100644 --- a/src/main/java/com/example/solidconnection/service/RedisService.java +++ b/src/main/java/com/example/solidconnection/service/RedisService.java @@ -24,7 +24,7 @@ public RedisService(RedisTemplate redisTemplate, } // incr & set ttl -> lua - public void increaseViewCountSync(String key) { + public void increaseViewCount(String key) { redisTemplate.execute(incrViewCountLuaScript, Collections.singletonList(key), VIEW_COUNT_TTL.getValue()); } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 2cd7dc4ff..300c69492 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -1,6 +1,6 @@ package com.example.solidconnection.siteuser.domain; -import com.example.solidconnection.entity.Comment; +import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.entity.mapping.PostLike; import com.example.solidconnection.type.Gender; @@ -59,7 +59,7 @@ public class SiteUser { @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List postList = new ArrayList<>(); - @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL) private List commentList = new ArrayList<>(); @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index 9eb936301..c2213993d 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -98,13 +98,15 @@ private Post createPost(Board board, SiteUser siteUser) { @Test public void 게시글을_조회할_때_조회수_동시성_문제를_해결한다() throws InterruptedException { + redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } finally { doneSignal.countDown(); } @@ -118,7 +120,7 @@ private Post createPost(Board board, SiteUser siteUser) { System.err.println("ExecutorService did not terminate in the expected time."); } - Thread.sleep(SCHEDULING_DELAY_MS); + Thread.sleep(SCHEDULING_DELAY_MS+1000); assertEquals(THREAD_NUMS, postRepository.getById(post.getId()).getViewCount()); } @@ -136,7 +138,7 @@ private Post createPost(Board board, SiteUser siteUser) { try { boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); if (isFirstTime) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } } finally { doneSignal.countDown(); @@ -149,7 +151,7 @@ private Post createPost(Board board, SiteUser siteUser) { try { boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); if (isFirstTime) { - redisService.increaseViewCountSync(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } } finally { doneSignal.countDown(); @@ -164,7 +166,7 @@ private Post createPost(Board board, SiteUser siteUser) { System.err.println("ExecutorService did not terminate in the expected time."); } - Thread.sleep(SCHEDULING_DELAY_MS); + Thread.sleep(SCHEDULING_DELAY_MS+1000); assertEquals(2L, postRepository.getById(post.getId()).getViewCount()); } diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java index c540954b1..9ea7ee0d9 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java @@ -27,7 +27,7 @@ @DataJpaTest @ActiveProfiles("test") @DisplayName("게시판 레포지토리 테스트") -public class BoardRepositoryTest { +class BoardRepositoryTest { @Autowired private PostRepository postRepository; @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java new file mode 100644 index 000000000..7029dead9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java @@ -0,0 +1,156 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +@SpringBootTest +@ActiveProfiles("local") +@DisplayName("댓글 레포지토리 테스트") +class CommentRepositoryTest { + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private EntityManager entityManager; + @Autowired + private CommentRepository commentRepository; + + private Board board; + private SiteUser siteUser; + private Post post; + private Comment parentComment; + private Comment childComment; + + @BeforeEach + public void setUp() { + board = createBoard(); + boardRepository.save(board); + + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + + post = createPost(board, siteUser); + post = postRepository.save(post); + + parentComment = createParentComment(); + childComment = createChildComment(); + commentRepository.save(parentComment); + commentRepository.save(childComment); + + entityManager.flush(); + entityManager.clear(); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private Comment createParentComment() { + Comment comment = new Comment( + "parent" + ); + comment.setPostAndSiteUser(post, siteUser); + return comment; + } + + private Comment createChildComment() { + Comment comment = new Comment( + "child" + ); + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); + return comment; + } + + @Test + @Transactional + public void 재귀쿼리로_댓글트리를_조회한다() { + // when + List commentTreeByPostId = commentRepository.findCommentTreeByPostId(post.getId()); + + // then + List expectedResponse = List.of(parentComment, childComment); + assertEquals(commentTreeByPostId, expectedResponse); + } + + @Test + @Transactional + public void 댓글을_조회한다() { + // when + Comment foundComment = commentRepository.getById(parentComment.getId()); + + // then + assertEquals(parentComment, foundComment); + } + + @Test + @Transactional + public void 댓글을_조회할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // given + Long invalidId = -1L; + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + commentRepository.getById(invalidId); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index b81ee952c..5fa873d06 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -31,7 +31,7 @@ @DataJpaTest @ActiveProfiles("test") @DisplayName("게시글 레포지토리 테스트") -public class PostRepositoryTest { +class PostRepositoryTest { @Autowired private PostRepository postRepository; @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java index 710546c9b..18c37b807 100644 --- a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java @@ -27,7 +27,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("게시판 서비스 테스트") -public class BoardServiceTest { +class BoardServiceTest { @InjectMocks BoardService boardService; @Mock diff --git a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java new file mode 100644 index 000000000..8a90b275b --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java @@ -0,0 +1,423 @@ +package com.example.solidconnection.unit.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.dto.*; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("댓글 서비스 테스트") +class CommentServiceTest { + @InjectMocks + CommentService commentService; + @Mock + PostRepository postRepository; + @Mock + SiteUserRepository siteUserRepository; + @Mock + CommentRepository commentRepository; + + private SiteUser siteUser; + private Board board; + private Post post; + private Comment parentComment_1; + private Comment parentComment_2; + private Comment p1s_childComment; + + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + board = createBoard(); + post = createPost(board, siteUser); + parentComment_1 = createParentComment(); + parentComment_2 = createParentComment(); + p1s_childComment = createChildComment(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private Comment createParentComment() { + Comment comment = new Comment( + "parent" + ); + comment.setPostAndSiteUser(post, siteUser); + return comment; + } + + private Comment createChildComment() { + Comment comment = new Comment( + "child" + ); + comment.setParentCommentAndPostAndSiteUser(parentComment_1, post, siteUser); + return comment; + } + + /** + * 댓글 조회 + */ + + @Test + void 특정_게시글의_댓글들을_조회한다() { + // Given + List commentList = List.of(parentComment_1, p1s_childComment, parentComment_2); + when(commentRepository.findCommentTreeByPostId(post.getId())).thenReturn(commentList); + + // When + List postFindCommentResponses = commentService.findCommentsByPostId( + siteUser.getEmail(), post.getId()); + + // Then + List expectedResponse = commentList.stream() + .map(comment -> PostFindCommentResponse.from(isOwner(comment, siteUser.getEmail()), comment)) + .collect(Collectors.toList()); + assertEquals(postFindCommentResponses, expectedResponse); + } + + private Boolean isOwner(Comment comment, String email) { + return comment.getSiteUser().getEmail().equals(email); + } + + /** + * 댓글 등록 + */ + @Test + void 부모_댓글을_등록한다() { + // Given + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "parent", null + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.save(any(Comment.class))).thenReturn(parentComment_1); + + // When + CommentCreateResponse commentCreateResponse = commentService.createComment( + siteUser.getEmail(), post.getId(), commentCreateRequest); + + // Then + assertEquals(commentCreateResponse, CommentCreateResponse.from(parentComment_1)); + verify(commentRepository, times(0)) + .getById(any(Long.class)); + verify(commentRepository, times(1)) + .save(commentCreateRequest.toEntity(siteUser, post, parentComment_1)); + } + + @Test + void 자식_댓글을_등록한다() { + // Given + Long parentCommentId = 1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", parentCommentId + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(parentCommentId)).thenReturn(parentComment_1); + when(commentRepository.save(any(Comment.class))).thenReturn(p1s_childComment); + + // When + CommentCreateResponse commentCreateResponse = commentService.createComment( + siteUser.getEmail(), post.getId(), commentCreateRequest); + + // Then + assertEquals(commentCreateResponse, CommentCreateResponse.from(p1s_childComment)); + verify(commentRepository, times(1)) + .getById(parentCommentId); + verify(commentRepository, times(1)) + .save(commentCreateRequest.toEntity(siteUser, post, parentComment_1)); + } + + + @Test + void 댓글을_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", null + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.createComment(siteUser.getEmail(), invalidPostId, commentCreateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + verify(commentRepository, times(0)) + .save(any(Comment.class)); + } + + @Test + void 댓글을_등록할_때_유효한_부모_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidParentCommentId = -1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child", invalidParentCommentId + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidParentCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.createComment(siteUser.getEmail(), post.getId(), commentCreateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + verify(commentRepository, times(0)) + .save(any(Comment.class)); + } + + /** + * 댓글 수정 + */ + @Test + void 댓글을_수정한다() { + // Given + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When + CommentUpdateResponse commentUpdateResponse = commentService.updateComment( + siteUser.getEmail(), post.getId(), parentComment_1.getId(), commentUpdateRequest); + + // Then + assertEquals(commentUpdateResponse.id(), parentComment_1.getId()); + } + + @Test + void 댓글을_수정할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), invalidPostId, parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 댓글을_수정할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidCommentId = -1L; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), post.getId(), invalidCommentId, commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } + + @Test + void 댓글을_수정할_때_이미_삭제된_댓글이라면_예외_응답을_반환한다() { + // Given + parentComment_1.deprecateComment(); + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(siteUser.getEmail(), post.getId(), parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getCode()); + } + + @Test + void 댓글을_수정할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { + // Given + String invalidEmail = "invalidEmail@test.com"; + CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( + "update" + ); + when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.updateComment(invalidEmail, post.getId(), parentComment_1.getId(), commentUpdateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ACCESS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ACCESS.getCode()); + } + + /** + * 댓글 삭제 + */ + + @Test + void 댓글을_삭제한다_자식댓글_있음() { + // Given + Long parentCommentId = 1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( + siteUser.getEmail(), post.getId(), parentCommentId); + + // Then + assertEquals(parentComment_1.getContent(), null); + assertEquals(commentDeleteResponse.id(), parentCommentId); + verify(commentRepository, times(0)).deleteById(parentCommentId); + } + + @Test + void 댓글을_삭제한다_자식댓글_없음() { + // Given + Long childCommentId = 1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(p1s_childComment); + + // When + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( + siteUser.getEmail(), post.getId(), childCommentId); + + // Then + assertEquals(commentDeleteResponse.id(), childCommentId); + verify(commentRepository, times(1)).deleteById(childCommentId); + } + + @Test + void 댓글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(siteUser.getEmail(), invalidPostId, parentComment_1.getId()) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 댓글을_삭제할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidCommentId = -1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(siteUser.getEmail(), post.getId(), invalidCommentId) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_ID.getCode()); + } + + @Test + void 댓글을_삭제할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { + // Given + String invalidEmail = "invalidEmail@test.com"; + when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(parentComment_1); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.deleteCommentById(invalidEmail, post.getId(), parentComment_1.getId()) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ACCESS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ACCESS.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java index c04c1485a..7f19707b7 100644 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java @@ -43,7 +43,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("게시글 서비스 테스트") -public class PostServiceTest { +class PostServiceTest { @InjectMocks PostService postService; @Mock @@ -241,6 +241,22 @@ private List createMockImageFilesWithMoreThanFiveFiles() { .isEqualTo(INVALID_BOARD_CODE.getCode()); } + @Test + void 게시글을_등록할_때_유효한_카테고리가_아니라면_예외_응답을_반환한다() { + // Given + String invalidPostCategory = "invalidPostCategory"; + PostCreateRequest postCreateRequest = new PostCreateRequest( + invalidPostCategory, "title", "content", false); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> postService + .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList())); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_CATEGORY.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_CATEGORY.getCode()); + } + @Test void 게시글을_등록할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { // Given From 04c0eed730672b400f90e73e06c95e82bd55d485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:26:06 +0900 Subject: [PATCH 083/158] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20API=20=EA=B5=AC=ED=98=84=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 게시글 좋아요 엔티티 정의 * feat: 게시글 좋아요 레포지토리 정의 * feat: 좋아요 등록,삭제 레포지토리 메소드 작성 * feat: 게시글 좋아요 서비스 로직 추가 * feat: 게시글 좋아요 컨트롤러 로직 추가 * feat: 게시글 좋아요 관련 DTO 정의 * feat: 게시글 좋아요 관련 예외 정의 * feat: 게시글 좋아요 레포지토리 테스트 로직 추가 * feat: 게시글 좋아요 서비스 테스트 로직 추가 * feat: 게시글 좋아요 레포지토리 테스트 로직 추가 * feat: 게시글 좋아요 동시성 테스트 로직 추가 * refactor: 특정 게시글 조회시 좋아요 여부도 반환 * refactor: 게시글 좋아요 삭제 경로 수정 * refactor: CommentCreateRequest record 타입으로 변경 및 Long 타입 활용하여 null 값 허용 * refactor: 게시글 작성시 category 전체 허용하지 않도록 수정 --- .../comment/dto/CommentCreateRequest.java | 23 +-- .../comment/service/CommentService.java | 7 +- .../custom/exception/ErrorCode.java | 2 + .../entity/mapping/PostLike.java | 24 --- .../post/controller/PostController.java | 22 +++ .../solidconnection/post/domain/Post.java | 1 - .../solidconnection/post/domain/PostLike.java | 53 +++++++ .../post/dto/PostDislikeResponse.java | 15 ++ .../post/dto/PostFindResponse.java | 4 +- .../post/dto/PostLikeResponse.java | 17 +++ .../post/repository/PostLikeRepository.java | 23 +++ .../post/repository/PostRepository.java | 13 ++ .../post/service/PostService.java | 56 ++++++- .../siteuser/domain/SiteUser.java | 2 +- .../PostLikeCountConcurrencyTest.java | 141 ++++++++++++++++++ .../repository/PostLikeRepositoryTest.java | 122 +++++++++++++++ .../unit/repository/PostRepositoryTest.java | 39 ++++- .../unit/service/PostServiceTest.java | 138 +++++++++++++++++ 18 files changed, 646 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/entity/mapping/PostLike.java create mode 100644 src/main/java/com/example/solidconnection/post/domain/PostLike.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java create mode 100644 src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java create mode 100644 src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java create mode 100644 src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java index c16c77323..8cf57e360 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java @@ -3,26 +3,15 @@ import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lombok.Getter; - -@Getter -public class CommentCreateRequest { - - @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") - @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") - String content; - - @Nullable - Long parentId; - - public CommentCreateRequest(String content, @Nullable Long parentId) { - this.content = content; - this.parentId = parentId; - } +public record CommentCreateRequest( + @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") + @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") + String content, + Long parentId +) { public Comment toEntity(SiteUser siteUser, Post post, Comment parentComment) { Comment comment = new Comment( diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 4fde0c1cf..8003ab26b 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -57,9 +57,10 @@ public CommentCreateResponse createComment(String email, Long postId, CommentCre SiteUser siteUser = siteUserRepository.getByEmail(email); Post post = postRepository.getById(postId); - Comment parentComment = Optional.ofNullable(commentCreateRequest.getParentId()) - .map(commentRepository::getById) - .orElse(null); + Comment parentComment = null; + if (commentCreateRequest.parentId() != null) { + parentComment = commentRepository.getById(commentCreateRequest.parentId()); + } Comment createdComment = commentRepository.save(commentCreateRequest.toEntity(siteUser, post, parentComment)); return CommentCreateResponse.from(createdComment); diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 0a9df0b61..418d7c8b9 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -61,6 +61,8 @@ public enum ErrorCode { INVALID_COMMENT_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 댓글입니다."), INVALID_COMMENT_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 댓글만 제어할 수 있습니다."), CAN_NOT_UPDATE_DEPRECATED_COMMENT(HttpStatus.BAD_REQUEST.value(),"이미 삭제된 댓글을 수정할 수 없습니다."), + INVALID_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글 좋아요입니다."), + DUPLICATE_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "이미 좋아요한 게시글입니다."), // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java b/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java deleted file mode 100644 index 074806838..000000000 --- a/src/main/java/com/example/solidconnection/entity/mapping/PostLike.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.solidconnection.entity.mapping; - -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor -public class PostLike { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "post_id") - private Post post; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "site_user_id") - private SiteUser siteUser; -} diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index fd452af8a..c5b974082 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -77,4 +77,26 @@ public ResponseEntity deletePostById( PostDeleteResponse postDeleteResponse = postService.deletePostById(principal.getName(), code, postId); return ResponseEntity.ok().body(postDeleteResponse); } + + @PostMapping(value = "/{code}/posts/{post_id}/like") + public ResponseEntity likePost( + Principal principal, + @PathVariable("code") String code, + @PathVariable("post_id") Long postId + ) { + + PostLikeResponse postLikeResponse = postService.likePost(principal.getName(), code, postId); + return ResponseEntity.ok().body(postLikeResponse); + } + + @DeleteMapping(value = "/{code}/posts/{post_id}/like") + public ResponseEntity dislikePost( + Principal principal, + @PathVariable("code") String code, + @PathVariable("post_id") Long postId + ) { + + PostDislikeResponse postDislikeResponse = postService.dislikePost(principal.getName(), code, postId); + return ResponseEntity.ok().body(postDislikeResponse); + } } diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/post/domain/Post.java index 12e43195f..287b255a8 100644 --- a/src/main/java/com/example/solidconnection/post/domain/Post.java +++ b/src/main/java/com/example/solidconnection/post/domain/Post.java @@ -4,7 +4,6 @@ import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.entity.mapping.PostLike; import com.example.solidconnection.post.dto.PostUpdateRequest; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; diff --git a/src/main/java/com/example/solidconnection/post/domain/PostLike.java b/src/main/java/com/example/solidconnection/post/domain/PostLike.java new file mode 100644 index 000000000..0af621370 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/domain/PostLike.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.post.domain; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@EqualsAndHashCode(of = "id") +public class PostLike { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_user_id") + private SiteUser siteUser; + + public void setPostAndSiteUser(Post post, SiteUser siteUser) { + + if (this.post != null) { + this.post.getPostLikeList().remove(this); + } + this.post = post; + post.getPostLikeList().add(this); + + if (this.siteUser != null) { + this.siteUser.getPostLikeList().remove(this); + } + this.siteUser = siteUser; + siteUser.getPostLikeList().add(this); + } + + public void resetPostAndSiteUser() { + + if (this.post != null) { + this.post.getPostLikeList().remove(this); + } + this.post = null; + + if (this.siteUser != null) { + this.siteUser.getPostLikeList().remove(this); + } + this.siteUser = null; + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java new file mode 100644 index 000000000..14de9987d --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.post.domain.Post; + +public record PostDislikeResponse( + Long likeCount, + Boolean isLiked +) { + public static PostDislikeResponse from(Post post) { + return new PostDislikeResponse( + post.getLikeCount(), + false + ); + } +} diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java index bbde1ba91..7f5f703af 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java @@ -19,6 +19,7 @@ public record PostFindResponse( Integer commentCount, String postCategory, Boolean isOwner, + Boolean isLiked, LocalDateTime createdAt, LocalDateTime updatedAt, PostFindBoardResponse postFindBoardResponse, @@ -27,7 +28,7 @@ public record PostFindResponse( List postFindPostImageResponses ) { - public static PostFindResponse from(Post post, Boolean isOwner, PostFindBoardResponse postFindBoardResponse, + public static PostFindResponse from(Post post, Boolean isOwner, Boolean isLiked, PostFindBoardResponse postFindBoardResponse, PostFindSiteUserResponse postFindSiteUserResponse, List postFindCommentResponses, List postFindPostImageResponses @@ -42,6 +43,7 @@ public static PostFindResponse from(Post post, Boolean isOwner, PostFindBoardRes postFindCommentResponses.size(), String.valueOf(post.getCategory()), isOwner, + isLiked, post.getCreatedAt(), post.getUpdatedAt(), postFindBoardResponse, diff --git a/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java new file mode 100644 index 000000000..0ce14b175 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.post.dto; + +import com.example.solidconnection.post.domain.Post; + +public record PostLikeResponse( + Long likeCount, + Boolean isLiked + + +) { + public static PostLikeResponse from(Post post) { + return new PostLikeResponse( + post.getLikeCount(), + true + ); + } +} diff --git a/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java b/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java new file mode 100644 index 000000000..398157c73 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.post.repository; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.siteuser.domain.SiteUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; + +@Repository +public interface PostLikeRepository extends JpaRepository { + + Optional findPostLikeByPostAndSiteUser(Post post, SiteUser siteUser); + + default PostLike getByPostAndSiteUser(Post post, SiteUser siteUser) { + return findPostLikeByPostAndSiteUser(post, siteUser) + .orElseThrow(() -> new CustomException(INVALID_POST_LIKE)); + } +} diff --git a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java index fda9cb166..f5c10875c 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java +++ b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java @@ -4,6 +4,9 @@ import com.example.solidconnection.post.domain.Post; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -25,4 +28,14 @@ default Post getById(Long id) { return findById(id) .orElseThrow(() -> new CustomException(INVALID_POST_ID)); } + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Post p SET p.likeCount = p.likeCount - 1 " + + "WHERE p.id = :postId AND p.likeCount > 0") + void decreaseLikeCount(@Param("postId") Long postId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 " + + "WHERE p.id = :postId") + void increaseLikeCount(@Param("postId") Long postId); } diff --git a/src/main/java/com/example/solidconnection/post/service/PostService.java b/src/main/java/com/example/solidconnection/post/service/PostService.java index d4ed3c081..4f9f77a73 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostService.java @@ -8,6 +8,8 @@ import com.example.solidconnection.dto.*; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.repository.PostLikeRepository; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.dto.*; import com.example.solidconnection.post.repository.PostRepository; @@ -24,6 +26,7 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -41,6 +44,7 @@ public class PostService { private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; + private final PostLikeRepository postLikeRepository; private String validateCode(String code) { try { @@ -71,8 +75,8 @@ private void validateQuestion(Post post) { } } - private void validatePostCategory(String category){ - if(!EnumUtils.isValidEnum(PostCategory.class, category)){ + private void validatePostCategory(String category) { + if (!EnumUtils.isValidEnum(PostCategory.class, category) || category.equals(PostCategory.전체.toString())) { throw new CustomException(INVALID_POST_CATEGORY); } } @@ -81,6 +85,11 @@ private Boolean getIsOwner(Post post, String email) { return post.getSiteUser().getEmail().equals(email); } + private Boolean getIsLiked(Post post, SiteUser siteUser) { + return postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser) + .isPresent(); + } + @Transactional public PostCreateResponse createPost(String email, String code, PostCreateRequest postCreateRequest, List imageFile) { @@ -146,7 +155,9 @@ public PostFindResponse findPostById(String email, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getByIdUsingEntityGraph(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); Boolean isOwner = getIsOwner(post, email); + Boolean isLiked = getIsLiked(post, siteUser); PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); @@ -154,12 +165,12 @@ public PostFindResponse findPostById(String email, String code, Long postId) { List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); // caching && 어뷰징 방지 - if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email,postId))) { + if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email, postId))) { redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); } return PostFindResponse.from( - post, isOwner, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); + post, isOwner, isLiked, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); } @Transactional @@ -178,4 +189,41 @@ public PostDeleteResponse deletePostById(String email, String code, Long postId) return new PostDeleteResponse(postId); } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public PostLikeResponse likePost(String email, String code, Long postId) { + + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + validateDuplicatePostLike(post, siteUser); + + PostLike postLike = new PostLike(); + postLike.setPostAndSiteUser(post, siteUser); + postLikeRepository.save(postLike); + postRepository.increaseLikeCount(post.getId()); + + return PostLikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + } + + private void validateDuplicatePostLike(Post post, SiteUser siteUser) { + if (postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser).isPresent()) { + throw new CustomException(DUPLICATE_POST_LIKE); + } + } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public PostDislikeResponse dislikePost(String email, String code, Long postId) { + + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + + PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); + postLike.resetPostAndSiteUser(); + postLikeRepository.deleteById(postLike.getId()); + postRepository.decreaseLikeCount(post.getId()); + + return PostDislikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 300c69492..f3c870ceb 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -2,7 +2,7 @@ import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.entity.mapping.PostLike; +import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java new file mode 100644 index 000000000..f07b6821c --- /dev/null +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -0,0 +1,141 @@ +package com.example.solidconnection.concurrency; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@ActiveProfiles("test") +@DisplayName("게시글 좋아요 동시성 테스트") +class PostLikeCountConcurrencyTest { + + @Autowired + private PostService postService; + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + + @Value("${view.count.scheduling.delay}") + private int SCHEDULING_DELAY_MS; + private int THREAD_NUMS = 1000; + private int THREAD_POOL_SIZE = 200; + private int TIMEOUT_SECONDS = 10; + + private Post post; + private Board board; + private SiteUser siteUser; + + @BeforeEach + void setUp() { + board = createBoard(); + boardRepository.save(board); + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + post = createPost(board, siteUser); + postRepository.save(post); + createSiteUsers(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private void createSiteUsers() { + for (int i = 0; i < 1000; i++) { + + SiteUser siteUser = new SiteUser( + "email" + i, + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + siteUserRepository.save(siteUser); + } + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + + return post; + } + + @Test + void 게시글_좋아요_동시성_문제를_해결한다() throws InterruptedException { + + ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); + CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); + + Long likeCount = postRepository.getById(post.getId()).getLikeCount(); + + for (int i = 0; i < THREAD_NUMS; i++) { + String email = "email" + i; + executorService.submit(() -> { + try { + postService.likePost(email, board.getCode(), post.getId()); + postService.dislikePost(email, board.getCode(), post.getId()); + } finally { + doneSignal.countDown(); + } + }); + } + + doneSignal.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!terminated) { + System.err.println("ExecutorService did not terminate in the expected time."); + } + + assertEquals(likeCount, postRepository.getById(post.getId()).getLikeCount()); + } + +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java new file mode 100644 index 000000000..c39e28497 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java @@ -0,0 +1,122 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.repository.PostLikeRepository; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DataJpaTest +@ActiveProfiles("test") +@DisplayName("게시글 좋아요 레포지토리 테스트") +class PostLikeRepositoryTest { + @Autowired + private PostRepository postRepository; + @Autowired + private BoardRepository boardRepository; + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private PostLikeRepository postLikeRepository; + + private Post post; + private Board board; + private SiteUser siteUser; + private PostLike postLike; + + + @BeforeEach + void setUp() { + board = createBoard(); + boardRepository.save(board); + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + post = createPost(board, siteUser); + post = postRepository.save(post); + postLike = createPostLike(post, siteUser); + postLikeRepository.save(postLike); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Board createBoard() { + return new Board( + "FREE", "자유게시판"); + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "title", + "content", + false, + 0L, + 0L, + PostCategory.valueOf("자유") + ); + post.setBoardAndSiteUser(board, siteUser); + return post; + } + + private PostLike createPostLike(Post post, SiteUser siteUser) { + PostLike postLike = new PostLike(); + postLike.setPostAndSiteUser(post, siteUser); + return postLike; + } + + @Test + @Transactional + void 게시글_좋아요를_조회한다() { + // when + PostLike foundPostLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); + + // then + assertEquals(foundPostLike, postLike); + } + + @Test + @Transactional + void 게시글_좋아요를_조회할_때_유효한_좋아요가_아니라면_예외_응답을_반환한다() { + // given + postLike.resetPostAndSiteUser(); + postLikeRepository.delete(postLike); + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + postLikeRepository.getByPostAndSiteUser(post, siteUser); + }); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_LIKE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_LIKE.getCode()); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index 5fa873d06..ecc2c4f6d 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -44,7 +44,7 @@ class PostRepositoryTest { private SiteUser siteUser; @BeforeEach - public void setUp() { + void setUp() { board = createBoard(); boardRepository.save(board); siteUser = createSiteUser(); @@ -92,7 +92,7 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { @Test @Transactional - public void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { + void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { Post foundPost = postRepository.getByIdUsingEntityGraph(post.getId()); foundPost.getPostImageList().size(); // 추가쿼리 발생하지 않는다. @@ -101,7 +101,7 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { @Test @Transactional - public void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { + void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { // given Long invalidId = -1L; @@ -117,7 +117,7 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { @Test @Transactional - public void 게시글을_조회한다() { + void 게시글을_조회한다() { Post foundPost = postRepository.getById(post.getId()); assertEquals(post, foundPost); @@ -125,7 +125,7 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { @Test @Transactional - public void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { Long invalidId = -1L; CustomException exception = assertThrows(CustomException.class, () -> { @@ -136,4 +136,33 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { assertThat(exception.getCode()) .isEqualTo(INVALID_POST_ID.getCode()); } + + @Test + @Transactional + void 게시글_좋아요를_등록한다() { + // given + Long likeCount = post.getLikeCount(); + + // when + postRepository.increaseLikeCount(post.getId()); + + // then + Post response = postRepository.getById(post.getId()); + assertEquals(response.getLikeCount(), likeCount + 1); + } + + @Test + @Transactional + void 게시글_좋아요를_삭제한다() { + // given + Long likeCount = post.getLikeCount(); + postRepository.increaseLikeCount(post.getId()); + + // when + postRepository.decreaseLikeCount(post.getId()); + + // then + Post response = postRepository.getById(post.getId()); + assertEquals(response.getLikeCount(), likeCount); + } } diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java index 7f19707b7..917d073f5 100644 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java @@ -9,6 +9,8 @@ import com.example.solidconnection.custom.exception.ErrorCode; import com.example.solidconnection.dto.PostFindPostImageResponse; import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.repository.PostLikeRepository; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.dto.*; import com.example.solidconnection.post.repository.PostRepository; @@ -53,6 +55,8 @@ class PostServiceTest { @Mock BoardRepository boardRepository; @Mock + PostLikeRepository postLikeRepository; + @Mock S3Service s3Service; @Mock CommentService commentService; @@ -66,6 +70,7 @@ class PostServiceTest { private Post post; private Post postWithImages; private Post questionPost; + private PostLike postLike; private List imageFiles; private List imageFilesWithMoreThanFiveFiles; private List uploadedFileUrlResponseList; @@ -81,6 +86,7 @@ void setUp() { post = createPost(board, siteUser); postWithImages = createPostWithImages(board, siteUser); questionPost = createQuestionPost(board, siteUser); + postLike = createPostLike(post, siteUser); } private SiteUser createSiteUser() { @@ -147,6 +153,12 @@ private Post createQuestionPost(Board board, SiteUser siteUser) { return post; } + private PostLike createPostLike(Post post, SiteUser siteUser) { + PostLike postLike = new PostLike(); + postLike.setPostAndSiteUser(post, siteUser); + return postLike; + } + private List createMockImageFiles() { List multipartFileList = new ArrayList<>(); multipartFileList.add(new MockMultipartFile("file1", "test1.png", @@ -431,6 +443,8 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // Given List commentFindResultDTOList = new ArrayList<>(); when(postRepository.getByIdUsingEntityGraph(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser)).thenReturn(Optional.empty()); when(commentService.findCommentsByPostId(siteUser.getEmail(), post.getId())).thenReturn(commentFindResultDTOList); // When @@ -440,6 +454,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { PostFindResponse expectedResponse = PostFindResponse.from( post, true, + false, PostFindBoardResponse.from(post.getBoard()), PostFindSiteUserResponse.from(post.getSiteUser()), commentFindResultDTOList, @@ -447,6 +462,8 @@ private List createMockImageFilesWithMoreThanFiveFiles() { ); assertEquals(expectedResponse, response); verify(postRepository, times(1)).getByIdUsingEntityGraph(post.getId()); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(postLikeRepository, times(1)).findPostLikeByPostAndSiteUser(post, siteUser); verify(commentService, times(1)).findCommentsByPostId(siteUser.getEmail(), post.getId()); } @@ -554,4 +571,125 @@ private List createMockImageFilesWithMoreThanFiveFiles() { assertThat(exception.getCode()) .isEqualTo(ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); } + + /** + * 게시글 좋아요 + */ + @Test + void 게시글_좋아요를_등록한다() { + // Given + when(postRepository.getById(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + + // When + PostLikeResponse postLikeResponse = postService.likePost(siteUser.getEmail(), board.getCode(), post.getId()); + + // Then + assertEquals(postLikeResponse, PostLikeResponse.from(post)); + verify(postLikeRepository, times(1)).save(any(PostLike.class)); + } + + @Test + void 게시글_좋아요를_등록할_때_중복된_좋아요라면_예외_응답을_반환한다() { + when(postRepository.getById(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser)).thenReturn(Optional.of(postLike)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.likePost(siteUser.getEmail(), board.getCode(), post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getCode()); + } + + @Test + void 게시글_좋아요를_등록할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + String invalidBoardCode = "INVALID_CODE"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.likePost(siteUser.getEmail(), invalidBoardCode, post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시글_좋아요를_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.likePost(siteUser.getEmail(), board.getCode(), invalidPostId)); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } + + @Test + void 게시글_좋아요를_삭제한다() { + // Given + Long likeCount = post.getLikeCount(); + when(postRepository.getById(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenReturn(postLike); + + // When + PostDislikeResponse postDislikeResponse = postService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId()); + + // Then + assertEquals(postDislikeResponse, PostDislikeResponse.from(post)); + verify(postLikeRepository, times(1)).deleteById(post.getId()); + } + + @Test + void 게시글_좋아요를_삭제할_때_존재하지_않는_좋아요라면_예외_응답을_반환한다() { + when(postRepository.getById(post.getId())).thenReturn(post); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenThrow(new CustomException(INVALID_POST_LIKE)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_POST_LIKE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_POST_LIKE.getCode()); + } + + @Test + void 게시글_좋아요를_삭제할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { + // Given + String invalidBoardCode = "INVALID_CODE"; + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.dislikePost(siteUser.getEmail(), invalidBoardCode, post.getId())); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + } + + @Test + void 게시글_좋아요를_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { + // Given + Long invalidPostId = -1L; + when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + postService.dislikePost(siteUser.getEmail(), board.getCode(), invalidPostId)); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_POST_ID.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_POST_ID.getCode()); + } } From 61bd3932543da1433d66af4032a48f57176f943a Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+devMuromi@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:25:13 +0900 Subject: [PATCH 084/158] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=EC=84=9C=203?= =?UTF-8?q?=EC=B0=A8=20=EC=A7=80=EB=A7=9D=20=EB=8C=80=ED=95=99=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 3차 지망 대학 추가 * feat: 1, 2, 3지망 중복 지망 검사 추가 * docs: 주석 수정 --- .../application/domain/Application.java | 5 ++ .../application/dto/ApplicationsResponse.java | 5 +- .../dto/UniversityChoiceRequest.java | 6 ++- .../repository/ApplicationRepository.java | 2 + .../service/ApplicationQueryService.java | 11 ++++- .../service/ApplicationSubmissionService.java | 22 ++++++--- .../custom/exception/ErrorCode.java | 2 +- .../e2e/ApplicantsQueryTest.java | 19 ++++++-- .../e2e/ApplicationSubmissionTest.java | 48 ++++++++++++++++--- .../e2e/VerifyStatusQueryTest.java | 6 +-- 10 files changed, 101 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java index b16e6433f..44dcc52f1 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Application.java +++ b/src/main/java/com/example/solidconnection/application/domain/Application.java @@ -54,6 +54,9 @@ public class Application { @ManyToOne private UniversityInfoForApply secondChoiceUniversity; + @ManyToOne + private UniversityInfoForApply thirdChoiceUniversity; + @ManyToOne private SiteUser siteUser; @@ -77,12 +80,14 @@ public void updateGpaAndLanguageTest( public void updateUniversityChoice( UniversityInfoForApply firstChoiceUniversity, UniversityInfoForApply secondChoiceUniversity, + UniversityInfoForApply thirdChoiceUniversity, String nicknameForApply) { if (this.firstChoiceUniversity != null) { this.updateCount++; } this.firstChoiceUniversity = firstChoiceUniversity; this.secondChoiceUniversity = secondChoiceUniversity; + this.thirdChoiceUniversity = thirdChoiceUniversity; this.nicknameForApply = nicknameForApply; } } diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java index fb93b7ff5..2e3025137 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java @@ -12,5 +12,8 @@ public record ApplicationsResponse( List firstChoice, @ArraySchema(arraySchema = @Schema(description = "2지망 대학에 지원한 지원자 목록")) - List secondChoice) { + List secondChoice, + + @ArraySchema(arraySchema = @Schema(description = "3지망 대학에 지원한 지원자 목록")) + List thirdChoice) { } diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java index b179404c8..a76799571 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java @@ -11,5 +11,7 @@ public record UniversityChoiceRequest( Long firstChoiceUniversityId, @Schema(description = "2지망 대학교의 지원 정보 ID (선택사항)", example = "2", nullable = true) - Long secondChoiceUniversityId) { -} + Long secondChoiceUniversityId, + + @Schema(description = "3지망 대학교의 지원 정보 ID (선택사항)", example = "3", nullable = true) + Long thirdChoiceUniversityId) {} diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java index 3cbf7c88f..34b714438 100644 --- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -26,6 +26,8 @@ public interface ApplicationRepository extends JpaRepository List findAllBySecondChoiceUniversityAndVerifyStatus(UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus); + List findAllByThirdChoiceUniversityAndVerifyStatus(UniversityInfoForApply thirdChoiceUniversity, VerifyStatus verifyStatus); + default Application getApplicationBySiteUser(SiteUser siteUser) { return findBySiteUser(siteUser) .orElseThrow(() -> new CustomException(APPLICATION_NOT_FOUND)); diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 104114c5c..793cf28ca 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -55,7 +55,8 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin // 1지망, 2지망 지원자들을 조회한다. List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser); List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser); - return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants); + List thirdChoiceApplicants = getThirdChoiceApplicants(universities, siteUser); + return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants); } private void validateSiteUserCanViewApplicants(SiteUser siteUser) { @@ -81,6 +82,14 @@ private List getSecondChoiceApplicants(List getThirdChoiceApplicants(List universities, SiteUser siteUser) { + return getApplicantsByChoice( + universities, + siteUser, + uia -> applicationRepository.findAllByThirdChoiceUniversityAndVerifyStatus(uia, VerifyStatus.APPROVED) + ); + } + private List getApplicantsByChoice( List searchedUniversities, SiteUser siteUser, diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index dc56e6da3..beec76a05 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -16,7 +16,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; @@ -61,7 +63,7 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) { /* * 지망 대학교를 제출한다. - * - 첫번째 지망과 두번째 지망이 같은지 검증한다. + * - 지망 대학중 중복된 대학교가 있는지 검증한다. * - 지원 정보 제출 내역이 없다면, 지금의 프로세스(성적 제출 후 지망대학 제출)에 벗어나는 요청이므로 예외를 응답한다. * - 기존에 제출한 적이 있다면, 수정한다. * - 수정 횟수 제한을 초과하지 않았는지 검증한다. @@ -70,7 +72,7 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) { * */ @Transactional public boolean submitUniversityChoice(String email, UniversityChoiceRequest universityChoiceRequest) { - validateFirstAndSecondChoiceIdDifferent(universityChoiceRequest); + validateNoDuplicateUniversityChoices(universityChoiceRequest); Application application = applicationRepository.findBySiteUser_Email(email) .orElseThrow(() -> new CustomException(SCORE_SHOULD_SUBMITTED_FIRST)); @@ -78,9 +80,11 @@ public boolean submitUniversityChoice(String email, UniversityChoiceRequest univ .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term); UniversityInfoForApply secondChoiceUniversity = universityInfoForApplyRepository .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.secondChoiceUniversityId(), term); + UniversityInfoForApply thirdChoiceUniversity = universityInfoForApplyRepository + .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.thirdChoiceUniversityId(), term); validateUpdateLimitNotExceed(application); - application.updateUniversityChoice(firstChoiceUniversity, secondChoiceUniversity, getRandomNickname()); + application.updateUniversityChoice(firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); return true; } @@ -98,10 +102,14 @@ private void validateUpdateLimitNotExceed(Application application) { } } - private void validateFirstAndSecondChoiceIdDifferent(UniversityChoiceRequest universityChoiceRequest) { - if (Objects.equals( - universityChoiceRequest.firstChoiceUniversityId(), - universityChoiceRequest.secondChoiceUniversityId())) { + private void validateNoDuplicateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) { + Set uniqueUniversityIds = new HashSet<>(); + + uniqueUniversityIds.add(universityChoiceRequest.firstChoiceUniversityId()); + uniqueUniversityIds.add(universityChoiceRequest.secondChoiceUniversityId()); + uniqueUniversityIds.add(universityChoiceRequest.thirdChoiceUniversityId()); + + if (uniqueUniversityIds.size() < 3) { throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 418d7c8b9..c8bc38acc 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -48,7 +48,7 @@ public enum ErrorCode { INVALID_TEST_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않은 어학 시험 종류입니다."), APPLICATION_NOT_APPROVED(HttpStatus.BAD_REQUEST.value(), "성적표가 인증되지 않았습니다."), APPLY_UPDATE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST.value(), "지원 정보 수정은 " + APPLICATION_UPDATE_COUNT_LIMIT + "회까지만 가능합니다."), - CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2지망에 동일한 대학교를 입력할 수 없습니다."), + CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2, 3지망에 동일한 대학교를 입력할 수 없습니다."), CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 " + MIN_DAYS_BETWEEN_NICKNAME_CHANGES + "일이 지나지 않았습니다."), // community diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 6ad77f58b..d609aa121 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -66,10 +66,10 @@ public void setUpUserAndToken() { 사용자1_지원정보 = new Application(사용자1, gpa, languageTest); 사용자2_지원정보 = new Application(사용자2, gpa, languageTest); 사용자3_지원정보 = new Application(사용자3, gpa, languageTest); - 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "0"); - 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "1"); - 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, "2"); - 사용자3_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, "3"); + 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 린츠_카톨릭대학_지원_정보, "0"); + 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 그라츠공과대학_지원_정보, "1"); + 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, 서던덴마크대학교_지원_정보, "2"); + 사용자3_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "3"); 나의_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자1_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자2_지원정보.setVerifyStatus(VerifyStatus.APPROVED); @@ -89,6 +89,7 @@ public void setUpUserAndToken() { List firstChoiceApplicants = response.firstChoice(); List secondChoiceApplicants = response.secondChoice(); + List thirdChoiceApplicants = response.thirdChoice(); assertThat(firstChoiceApplicants).containsAnyElementsOf(List.of( UniversityApplicantsResponse.of(괌대학_A_지원_정보, @@ -110,6 +111,16 @@ public void setUpUserAndToken() { UniversityApplicantsResponse.of(그라츠대학_지원_정보, List.of(ApplicantResponse.of(사용자2_지원정보, false))) )); + assertThat(thirdChoiceApplicants).containsAnyElementsOf(List.of( + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true))), + UniversityApplicantsResponse.of(서던덴마크대학교_지원_정보, + List.of(ApplicantResponse.of(사용자2_지원정보, false))), + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(사용자1_지원정보, false))), + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(사용자3_지원정보, false))) + )); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java index f5443f805..0cbe37f35 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java @@ -119,7 +119,7 @@ public void setUpUserAndToken() { applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())); // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); RestAssured.given() .header("Authorization", "Bearer " + accessToken) .body(request) @@ -135,6 +135,7 @@ public void setUpUserAndToken() { () -> assertThat(application.getSiteUser().getId()).isEqualTo(siteUser.getId()), () -> assertThat(application.getFirstChoiceUniversity().getId()).isEqualTo(request.firstChoiceUniversityId()), () -> assertThat(application.getSecondChoiceUniversity().getId()).isEqualTo(request.secondChoiceUniversityId()), + () -> assertThat(application.getThirdChoiceUniversity().getId()).isEqualTo(request.thirdChoiceUniversityId()), () -> assertThat(application.getNicknameForApply()).isNotNull(), () -> assertThat(application.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), () -> assertThat(application.getUpdateCount()).isZero()); @@ -146,11 +147,11 @@ public void setUpUserAndToken() { ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())) - .updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "nickname"); + .updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "nickname"); Application initialApplication = applicationRepository.getApplicationBySiteUser(siteUser); // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); RestAssured.given() .header("Authorization", "Bearer " + accessToken) .body(request) @@ -166,6 +167,7 @@ public void setUpUserAndToken() { () -> assertThat(updatedApplication.getSiteUser().getId()).isEqualTo(siteUser.getId()), () -> assertThat(updatedApplication.getFirstChoiceUniversity().getId()).isEqualTo(request.firstChoiceUniversityId()), () -> assertThat(updatedApplication.getSecondChoiceUniversity().getId()).isEqualTo(request.secondChoiceUniversityId()), + () -> assertThat(updatedApplication.getThirdChoiceUniversity().getId()).isEqualTo(request.thirdChoiceUniversityId()), () -> assertThat(updatedApplication.getNicknameForApply()).isNotNull(), () -> assertThat(updatedApplication.getVerifyStatus()).isEqualTo(initialApplication.getVerifyStatus()), () -> assertThat(updatedApplication.getUpdateCount()).isEqualTo(initialApplication.getUpdateCount())); @@ -181,12 +183,12 @@ public void setUpUserAndToken() { // setUp - 지망 대학을 한계까지 수정 for (int i = 0; i <= APPLICATION_UPDATE_COUNT_LIMIT; i++) { - initialApplication.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, "nickname"); + initialApplication.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "nickname"); applicationRepository.save(initialApplication); } // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); ErrorResponse errorResponse = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .body(request) @@ -202,7 +204,41 @@ public void setUpUserAndToken() { @Test void 일지망_대학과_이지망_대학이_같으면_예외_응답을_반환한다() { // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 그라츠대학_지원_정보.getId()); + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 그라츠대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); + ErrorResponse errorResponse = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .log().all() + .post("/application/university") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()).isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + } + + @Test + void 일지망_대학과_삼지망_대학이_같으면_예외_응답을_반환한다() { + // request - body 생성 및 요청 + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 그라츠대학_지원_정보.getId()); + ErrorResponse errorResponse = RestAssured.given() + .header("Authorization", "Bearer " + accessToken) + .body(request) + .contentType("application/json") + .log().all() + .post("/application/university") + .then().log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()) + .extract().as(ErrorResponse.class); + + assertThat(errorResponse.message()).isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + } + + @Test + void 이지망_대학과_삼지망_대학이_같으면_예외_응답을_반환한다() { + // request - body 생성 및 요청 + UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); ErrorResponse errorResponse = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .body(request) diff --git a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java index d0749f1f9..b4447247a 100644 --- a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java @@ -87,7 +87,7 @@ public void setUpUserAndToken() { void 성적과_대학을_모두_제출하고_승인을_기대라는_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출한 상태 Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "닉네임"); applicationRepository.save(application); // request - 요청 @@ -108,7 +108,7 @@ public void setUpUserAndToken() { void 성적과_대학을_모두_제출했지만_승인이_반려된_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출했지만, 승인 거절 Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보,"닉네임"); application.setVerifyStatus(VerifyStatus.REJECTED); applicationRepository.save(application); @@ -130,7 +130,7 @@ public void setUpUserAndToken() { void 성적과_대학을_모두_제출했으며_승인이_된_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출했으며, 승인이 된 상태 Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, "닉네임"); + application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "닉네임"); application.setVerifyStatus(VerifyStatus.APPROVED); applicationRepository.save(application); From 7cf69cd631ef119d77060499b000b0843103497f Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:04:42 +0900 Subject: [PATCH 085/158] =?UTF-8?q?feat:=20universityInfoForApply=EC=97=90?= =?UTF-8?q?=20koreanName=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 신규 국가 쿼리 추가 * feat: UniversityInforForApply에 koreanName 필드 추가 * feat: university 등록금 납부별로 나누어진 데이터 통합 * fix: 학교 detail 반환 시 university id가 아닌 applyInfoForApply id 반환하게 변경 UniversityDetailTest:57 테스트가 의도한 것으로 보고 applyInfoForApply id를 반환하게 변경 --- .../domain/UniversityInfoForApply.java | 3 + .../dto/UniversityDetailResponse.java | 4 +- ...UniversityInfoForApplyPreviewResponse.java | 2 +- .../UniversityInfoForApplyRepository.java | 2 +- .../service/GeneralRecommendUniversities.java | 2 +- src/main/resources/data.sql | 97 +++++++++---------- .../solidconnection/e2e/DynamicFixture.java | 1 + .../e2e/UniversityDataSetUpEndToEndTest.java | 51 ++++------ .../e2e/UniversityDetailTest.java | 2 +- .../e2e/UniversityLikeTest.java | 2 +- 10 files changed, 76 insertions(+), 90 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java b/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java index 903bc9320..6a1cdf4dd 100644 --- a/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java +++ b/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java @@ -35,6 +35,9 @@ public class UniversityInfoForApply { @Column(length = 50, nullable = false) private String term; + @Column(nullable = false, length = 100) + private String koreanName; + @Column private Integer studentCapacity; diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java index d5db9d74a..118bfdaa6 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java @@ -92,9 +92,9 @@ public static UniversityDetailResponse of( University university, UniversityInfoForApply universityInfoForApply) { return new UniversityDetailResponse( - university.getId(), + universityInfoForApply.getId(), universityInfoForApply.getTerm(), - university.getKoreanName(), + universityInfoForApply.getKoreanName(), university.getEnglishName(), university.getFormatName(), university.getRegion().getKoreanName(), diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java index f0a0c73cb..fe5713c67 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java @@ -43,7 +43,7 @@ public static UniversityInfoForApplyPreviewResponse from(UniversityInfoForApply return new UniversityInfoForApplyPreviewResponse( universityInfoForApply.getId(), universityInfoForApply.getTerm(), - universityInfoForApply.getUniversity().getKoreanName(), + universityInfoForApply.getKoreanName(), universityInfoForApply.getUniversity().getRegion().getKoreanName(), universityInfoForApply.getUniversity().getCountry().getKoreanName(), universityInfoForApply.getUniversity().getLogoImageUrl(), diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index 6c9120fab..2f56cce3a 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -20,7 +20,7 @@ public interface UniversityInfoForApplyRepository extends JpaRepository findByIdAndTerm(Long id, String term); - Optional findByUniversity_KoreanNameAndTerm(String koreanName, String term); + Optional findByKoreanNameAndTerm(String koreanName, String term); @Query("SELECT c FROM UniversityInfoForApply c WHERE c.university IN :universities AND c.term = :term") List findByUniversitiesAndTerm(@Param("universities") List universities, @Param("term") String term); diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java index 6eb70f81b..cf213c6af 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java @@ -41,7 +41,7 @@ public class GeneralRecommendUniversities { public void init() { int i = 0; while (recommendUniversities.size() < RECOMMEND_UNIVERSITY_NUM && i < candidates.size()) { - universityInfoForApplyRepository.findByUniversity_KoreanNameAndTerm(candidates.get(i), term) + universityInfoForApplyRepository.findByKoreanNameAndTerm(candidates.get(i), term) .ifPresent(recommendUniversities::add); i++; } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 5d2ce9a04..477fc03f5 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -35,192 +35,185 @@ VALUES ('BN', '브루나이', 'ASIA'), ('HU', '헝가리', 'EUROPE'), ('LT', '리투아니아', 'EUROPE'), ('TH', '태국', 'ASIA'), - ('UZ', '우즈베키스탄', 'ASIA'); + ('UZ', '우즈베키스탄', 'ASIA'), + ('KZ', '카자흐스탄', 'ASIA'), + ('IL', '이스라엘', 'ASIA'), + ('MY', '말레이시아', 'ASIA'), + ('RU', '러시아', 'EUROPE'); INSERT INTO university(id, country_code, region_code, english_name, format_name, korean_name, accommodation_url, english_course_url, homepage_url, details_for_local, logo_image_url, background_image_url) -VALUES (1, 'US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학(A형)', +VALUES (1, 'US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학', 'https://www.uog.edu/life-at-uog/residence-halls/', 'https://www.uog.edu/admissions/course-schedule', 'https://www.uog.edu/admissions/international-students', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png'), - (2, 'US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학(B형)', - 'https://www.uog.edu/life-at-uog/residence-halls/', 'https://www.uog.edu/admissions/course-schedule', - 'https://www.uog.edu/admissions/international-students', NULL, - 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png', - 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png'), - (3, 'US', 'AMERICAS', 'University of Nevada, Las Vegas', 'university_of_nevada_las_vegas', '네바다주립대학 라스베이거스(B형)', + (2, 'US', 'AMERICAS', 'University of Nevada, Las Vegas', 'university_of_nevada_las_vegas', '네바다주립대학 라스베이거스', 'https://www.unlv.edu/housing', 'https://www.unlv.edu/engineering/academic-programs', 'https://www.unlv.edu/engineering/eip', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png'), - (4, 'CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', - 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스(A형)', 'https://www.mun.ca/residences/', - 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', - 'https://mun.ca/goabroad/visiting-students-inbound/', NULL, - 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png', - 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png'), - (5, 'CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', - 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스(B형)', 'www.mun.ca/residences', + (3, 'CA', 'AMERICAS', 'Memorial University of Newfoundland St. John''s', + 'memorial_university_of_newfoundland_st_johns', '메모리얼 대학 세인트존스', 'https://www.mun.ca/residences/', 'https://www.mun.ca/regoff/registration-and-final-exams/course-offerings/', 'https://mun.ca/goabroad/visiting-students-inbound/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png'), - (6, 'AU', 'AMERICAS', 'University of Southern Queensland', 'university_of_southern_queensland', '서던퀸스랜드대학(B형)', + (4, 'AU', 'AMERICAS', 'University of Southern Queensland', 'university_of_southern_queensland', '서던퀸스랜드대학', 'https://www.unisq.edu.au/current-students/support/accommodation', 'https://www.unisq.edu.au/course/specification/current/', 'https://www.unisq.edu.au/international/partnerships/study-abroad-exchange', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_queensland/1.png'), - (7, 'AU', 'AMERICAS', 'University of Sydney', 'university_of_sydney', '시드니대학', + (5, 'AU', 'AMERICAS', 'University of Sydney', 'university_of_sydney', '시드니대학', 'https://www.sydney.edu.au/study/accommodation.html', 'www.sydney.edu.au/sydney-abroad-units', 'https://www.sydney.edu.au/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_sydney/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_sydney/1.png'), - (8, 'AU', 'AMERICAS', 'Curtin University', 'curtin_university', '커틴대학(A형)', + (6, 'AU', 'AMERICAS', 'Curtin University', 'curtin_university', '커틴대학', 'https://www.curtin.edu.au/study/campus-life/accommodation/#perth', 'https://handbook.curtin.edu.au/', 'https://www.curtin.edu.au/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/curtin_university/1.png'), - (9, 'DK', 'EUROPE', 'University of Southern Denmark', 'university_of_southern_denmark', '서던덴마크대학교', + (7, 'DK', 'EUROPE', 'University of Southern Denmark', 'university_of_southern_denmark', '서던덴마크대학교', 'https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing', 'https://www.sdu.dk/en/uddannelse/exchange_programmes', 'https://www.sdu.dk/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/1.png'), - (10, 'DK', 'EUROPE', 'IT University of Copenhagen', 'it_university_of_copenhagen', '코펜하겐 IT대학', + (8, 'DK', 'EUROPE', 'IT University of Copenhagen', 'it_university_of_copenhagen', '코펜하겐 IT대학', 'https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students', 'https://en.itu.dk/Programmes/Exchange-students/Become-an-exchange-student-at-ITU', 'https://en.itu.dk/programmes/exchange-students/become-an-exchange-student-at-itu', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/1.png'), - (11, 'DE', 'EUROPE', 'Neu-Ulm University of Applied Sciences', 'neu-ulm_university_of_applied_sciences', + (9, 'DE', 'EUROPE', 'Neu-Ulm University of Applied Sciences', 'neu-ulm_university_of_applied_sciences', '노이울름 대학', 'https://www.hnu.de/fileadmin/user_upload/5_Internationales/International_Incomings/Bewerbung/Housing_Broschure.pdf', 'https://www.hnu.de/en/international/international-exchange-students/courses-taught-in-english', 'https://www.hnu.de/en/international', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neu-ulm_university_of_applied_sciences/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/neu-ulm_university_of_applied_sciences/1.png'), - (12, 'GB', 'EUROPE', 'University of Hull', 'university_of_hull', '헐대학', + (10, 'GB', 'EUROPE', 'University of Hull', 'university_of_hull', '헐대학', 'https://www.hull.ac.uk/Choose-Hull/Student-life/Accommodation/accommodation.aspx', 'https://universityofhull.app.box.com/s/mpvulz3yz0uijdt68rybce19nek0d8eh', 'https://www.hull.ac.uk/choose-hull/study-at-hull/need-to-know/key-dates', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hull/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_hull/1.png'), - (13, 'AT', 'EUROPE', 'University of Graz', 'university_of_graz', '그라츠 대학', + (11, 'AT', 'EUROPE', 'University of Graz', 'university_of_graz', '그라츠 대학', 'https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/', 'https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf', 'https://www.uni-graz.at/en/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/1.png'), - (14, 'AT', 'EUROPE', 'Graz University of Technology', 'graz_university_of_technology', '그라츠공과대학', + (12, 'AT', 'EUROPE', 'Graz University of Technology', 'graz_university_of_technology', '그라츠공과대학', 'https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033', 'https://tugraz.at/go/search-courses', 'https://www.tugraz.at/en/home', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/1.png'), - (15, 'AT', 'EUROPE', 'Catholic Private University Linz', 'catholic_private_university_linz', '린츠 카톨릭 대학교', NULL, + (13, 'AT', 'EUROPE', 'Catholic Private University Linz', 'catholic_private_university_linz', '린츠 카톨릭 대학교', NULL, 'https://ku-linz.at/en/ku_international/incomings/kulis', 'https://ku-linz.at/en', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/1.png'), - (16, 'AT', 'EUROPE', 'University of Applied Sciences Technikum Wien', + (14, 'AT', 'EUROPE', 'University of Applied Sciences Technikum Wien', 'university_of_applied_sciences_technikum_wien', '빈 공과대학교', NULL, 'https://www.technikum-wien.at/en/international/student-mobility/', 'https://www.technikum-wien.at/international/studierendenmobilitaet-2/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_technikum_wien/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_applied_sciences_technikum_wien/1.png'), - (17, 'FR', 'EUROPE', 'IPSA', 'ipsa', 'IPSA', 'https://www.ipsa.fr/en/student-life/pratical-information/', NULL, + (15, 'FR', 'EUROPE', 'IPSA', 'ipsa', 'IPSA', 'https://www.ipsa.fr/en/student-life/pratical-information/', NULL, 'https://www.ipsa.fr/en/engineering-school/aeronautical-space', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ipsa/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/ipsa/1.png'), - (18, 'JP', 'ASIA', 'Meiji University', 'meiji_university', '메이지대학', + (16, 'JP', 'ASIA', 'Meiji University', 'meiji_university', '메이지대학', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png'), - (19, 'JP', 'ASIA', 'BAIKA Women''s University', 'baika_womens_university', '바이카여자대학', + (17, 'JP', 'ASIA', 'BAIKA Women''s University', 'baika_womens_university', '바이카여자대학', 'https://dormy-ac.com/page/baika/', NULL, 'https://www.baika.ac.jp/english/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/baika_womens_university/1.png'), - (20, 'JP', 'ASIA', 'Bunkyo Gakuin University', 'bunkyo_gakuin_university', '분쿄가쿠인대학', NULL, NULL, + (18, 'JP', 'ASIA', 'Bunkyo Gakuin University', 'bunkyo_gakuin_university', '분쿄가쿠인대학', NULL, NULL, 'https://www.bgu.ac.jp/', NULL, 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/1.png'); -INSERT INTO university_info_for_apply(term, university_id, semester_requirement, student_capacity, +INSERT INTO university_info_for_apply(term, university_id, korean_name, semester_requirement, student_capacity, semester_available_for_dispatch, tuition_fee_type, details_for_major, details_for_apply, details_for_language, details_for_english_course, details_for_accommodation, details) -VALUES ('2024-1', 1, 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, +VALUES ('2024-1', 1, '괌대학(A형)', 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), - ('2024-1', 2, 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, + ('2024-1', 1, '괌대학(B형)', 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend'), - ('2024-1', 3, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + ('2024-1', 2, '네바다주립대학 라스베이거스(B형)', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000
- 한 학기 등록금: 약 $7,500
- International Program and Service Fees $2,500'), - ('2024-1', 4, 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 3, '메모리얼 대학 세인트존스(A형)', 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', NULL, NULL, NULL), - ('2024-1', 5, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + ('2024-1', 3, '메모리얼 대학 세인트존스(B형)', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', NULL, NULL, '국제학생 등록금 적용 (학점당 $2,080)'), - ('2024-1', 6, 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', + ('2024-1', 4, '서던퀸스랜드대학(B형)', 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 각 영역 최소 5.5 이상
- 외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함 ', NULL, NULL, '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람
- In-state 등록금 납부
(등록금 관련 정보 : https://www.unisq.edu.au/international/partnerships/study-abroad-exchange/fees-scholarships)'), - ('2024-1', 7, 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + ('2024-1', 5, '시드니대학', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- MECO, CAEL, LAWS unit 수강 여석 제한 있음', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기/듣기/말하기 17점, 쓰기 19점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, 'OSHC(Overseas Student Health Cover) 국제학생 보험가입 의무 (2023년 기준 AUD 348/학기, 학기마다 비용 상이)'), - ('2024-1', 8, 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 6, '커틴대학(A형)', 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, '※ 24-1학기에 한하여 ''Destination Australia Cheung Kong Exchange Program Scholarship'' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), - ('2024-1', 9, 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 7, '서던덴마크대학교', 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함
- 교환학생에게 제공되는 수업만 수강 가능
- Faculty of Engineering 내에서 2/3이상의 수업을 수강하여야 함
- 30 ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 1일)', NULL, NULL, '- 교외 숙소', NULL), - ('2024-1', 10, 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 8, '코펜하겐 IT대학', 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 본교 기초과목 이수사항에 따라 지원이 제한될 수 있으나 소속전공과 정확하게 일치 하지 않아도 지원은 가능(연관 전공이어야 함)
- 최소 7.5 ECTS, 최대 30ECTS 수강 가능
- 교차 수강 가능(선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', NULL, NULL, '- 제공(학교 운영 기숙사 아님)
- 선착순 배정', NULL), - ('2024-1', 11, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, + ('2024-1', 9, '노이울름 대학', 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점
- TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점
외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, NULL), - ('2024-1', 12, 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 10, '헐대학', 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '제한학과 많음. (Factsheet참조및Factsheet언급된 제한학과 외에도 학기마다 제한학과 발생가능성있음). 지원 전 권역 담당자랑 사전상담 요망. 학기당 30ECTS수강해야 LA승인남. 성적처리 늦은 편이라 8차 학기 수학자는 성적처리 늦은 거 감안하고 추가 이에 따른 불편함이 있음을 인지후 지원요망. ', '지원 전 권역 담당자와 사전상담 요망', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 듣기 및 쓰기 18점, 읽기 18점, 말하기 20점, 쓰기 18점 이상
- IELTS : 모든 영역에서 6.0이상', NULL, NULL, '영국 생활비 및 숙소비용 유럽권 지역 중 상대적으로 매우 높은편. 지원전 반드시 사전고려 요망'), - ('2024-1', 13, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', + ('2024-1', 11, '그라츠 대학', 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '학교인근 외부 숙소는 있지만, 외부업체운영숙소라 대학관할아님', NULL), - ('2024-1', 14, 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', + ('2024-1', 12, '그라츠공과대학', 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점 이상, 쓰기 17점 이상, 말하기 20점 이상, 듣기 17점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상
''- TOEIC의 경우 S/W 점수 합산 310점 이상 ', NULL, '자체기숙사는 없음. 교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있음(선착순 경우많음). 더블룸 기준약 한달에 € 340 per month (기숙사 종류게 따라 가격 차이 유) 예산잡으면됨.', NULL), - ('2024-1', 15, 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 13, '린츠 카톨릭 대학교', 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: History, Philosophy, Art History, theology
(영어과목 수가 그리 많지는 않으므로, 사전 확인필요)
''- 학기당 최소 15ECTS 수강신청해야 함', '봄학기에는 영어과목이 극히 제한적으로 열린다고 함. 지원 전 권역 담당자와 사전상담 요망', NULL, NULL, '학교에서 몇가지 기숙사 옵션 합격시 연결예정.', NULL), - ('2024-1', 16, 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 14, '빈 공과대학교', 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '기숙사없음', NULL), - ('2024-1', 17, 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 15, 'IPSA', 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치 또는 유사하여야 함 : 전공이 제한적이므로 반드시 홈페이지에서 지원 가능 전공을 확인할 것
- 최대 30ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', NULL, NULL, '- 미제공', NULL), - ('2024-1', 18, 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + ('2024-1', 16, '메이지대학', 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', '*해당 학교 일정 상 10월초까지 서류제출 필요', '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, NULL), - ('2024-1', 19, 2, 1, 'IRRELEVANT', NULL, + ('2024-1', 17, '바이카여자대학', 2, 1, 'IRRELEVANT', NULL, '교환학생 지원가능 : Department of Global English, Department of Japanese culture, Department of Media and Information, Department of Psychology.', '여학생만 신청가능', NULL, NULL, '기숙사 없음, 계약된 외부 기숙사 사용-“Maison de Claire Ibaraki” 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)', NULL), - ('2024-1', 20, 2, 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, + ('2024-1', 18, '분쿄가쿠인대학', 2, 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월', NULL); INSERT INTO language_requirement(language_test_type, min_score, university_info_for_apply_id) diff --git a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java index 43247b1d9..a549b62a2 100644 --- a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java +++ b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java @@ -60,6 +60,7 @@ public static UniversityInfoForApply createUniversityForApply( return new UniversityInfoForApply( null, term, + "koreanName", 1, TuitionFeeType.HOME_UNIVERSITY_PAYMENT, SemesterAvailableForDispatch.ONE_SEMESTER, diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java index 24380dd81..9afecbbfd 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java @@ -40,10 +40,9 @@ abstract class UniversityDataSetUpEndToEndTest { public static Country 오스트리아; public static Country 일본; - public static University 영미권_미국_괌대학_A; - public static University 영미권_미국_괌대학_B; + public static University 영미권_미국_괌대학; public static University 영미권_미국_네바다주립대학_라스베이거스; - public static University 영미권_캐나다_메모리얼대학_세인트존스_A; + public static University 영미권_캐나다_메모리얼대학_세인트존스; public static University 유럽_덴마크_서던덴마크대학교; public static University 유럽_덴마크_코펜하겐IT대학; public static University 유럽_오스트리아_그라츠대학; @@ -97,8 +96,8 @@ public void setUpBasicData() { 오스트리아 = countryRepository.save(new Country("AT", "오스트리아", 유럽)); 일본 = countryRepository.save(new Country("JP", "일본", 아시아)); - 영미권_미국_괌대학_A = universityRepository.save(new University( - null, "괌대학(A형)", "University of Guam", "university_of_guam", + 영미권_미국_괌대학 = universityRepository.save(new University( + null, "괌대학", "University of Guam", "university_of_guam", "https://www.uog.edu/admissions/international-students", "https://www.uog.edu/admissions/course-schedule", "https://www.uog.edu/life-at-uog/residence-halls/", @@ -107,18 +106,8 @@ public void setUpBasicData() { null, 미국, 영미권 )); - 영미권_미국_괌대학_B = universityRepository.save(new University( - null, "괌대학(B형)", "University of Guam", "university_of_guam", - "https://www.uog.edu/admissions/international-students", - "https://www.uog.edu/admissions/course-schedule/", - "https://www.uog.edu/life-at-uog/residence-halls/", - "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png", - "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png", - null, 미국, 영미권 - )); - 영미권_미국_네바다주립대학_라스베이거스 = universityRepository.save(new University( - null, "네바다주립대학 라스베이거스(B형)", "University of Nevada, Las Vegas", "university_of_nevada_las_vegas", + null, "네바다주립대학 라스베이거스", "University of Nevada, Las Vegas", "university_of_nevada_las_vegas", "https://www.unlv.edu/engineering/eip", "https://www.unlv.edu/engineering/academic-programs", "https://www.unlv.edu/housing", @@ -127,8 +116,8 @@ public void setUpBasicData() { null, 미국, 영미권 )); - 영미권_캐나다_메모리얼대학_세인트존스_A = universityRepository.save(new University( - null, "메모리얼 대학 세인트존스(A형)", "Memorial University of Newfoundland St. John's", "memorial_university_of_newfoundland_st_johns", + 영미권_캐나다_메모리얼대학_세인트존스 = universityRepository.save(new University( + null, "메모리얼 대학 세인트존스", "Memorial University of Newfoundland St. John's", "memorial_university_of_newfoundland_st_johns", "https://mun.ca/goabroad/visiting-students-inbound/", "https://www.unlv.edu/engineering/academic-programs", "https://www.mun.ca/residences/", @@ -194,23 +183,23 @@ public void setUpBasicData() { )); 괌대학_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "괌대학(A형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", - new HashSet<>(), 영미권_미국_괌대학_A + new HashSet<>(), 영미권_미국_괌대학 )); 괌대학_B_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "괌대학(B형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", - new HashSet<>(), 영미권_미국_괌대학_B + new HashSet<>(), 영미권_미국_괌대학 )); 네바다주립대학_라스베이거스_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "네바다주립대학 라스베이거스(B형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", @@ -218,15 +207,15 @@ public void setUpBasicData() { )); 메모리얼대학_세인트존스_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "메모리얼 대학 세인트존스(A형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", - new HashSet<>(), 영미권_캐나다_메모리얼대학_세인트존스_A + new HashSet<>(), 영미권_캐나다_메모리얼대학_세인트존스 )); 서던덴마크대학교_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "서던덴마크대학교", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", @@ -234,7 +223,7 @@ public void setUpBasicData() { )); 코펜하겐IT대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "코펜하겐 IT대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", @@ -242,7 +231,7 @@ public void setUpBasicData() { )); 그라츠대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "그라츠 대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", @@ -250,7 +239,7 @@ public void setUpBasicData() { )); 그라츠공과대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "그라츠공과대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", @@ -258,7 +247,7 @@ public void setUpBasicData() { )); 린츠_카톨릭대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "린츠 카톨릭 대학교", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", @@ -266,7 +255,7 @@ public void setUpBasicData() { )); 메이지대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( - null, term, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, term, "메이지대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index cd1ae99de..dc8401700 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -55,7 +55,7 @@ public void setUpUserAndToken() { // response - 응답 Assertions.assertAll( () -> assertThat(response.id()).isEqualTo(메이지대학_지원_정보.getId()), - () -> assertThat(response.koreanName()).isEqualTo(아시아_일본_메이지대학.getKoreanName()), + () -> assertThat(response.koreanName()).isEqualTo(메이지대학_지원_정보.getKoreanName()), () -> assertThat(response.englishName()).isEqualTo(아시아_일본_메이지대학.getEnglishName()), () -> assertThat(response.region()).isEqualTo(아시아_일본_메이지대학.getRegion().getKoreanName()), () -> assertThat(response.country()).isEqualTo(아시아_일본_메이지대학.getCountry().getKoreanName()), diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index 33216453a..37f922e4e 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -66,7 +66,7 @@ public void setUpUserAndToken() { void 좋아요를_한_대학을_조회한다() { // setUp - 대학교 좋아요 저장 UniversityInfoForApply differentTermUniversityInfoForApply = - createUniversityForApply(term + " 추가 지원", 영미권_미국_괌대학_A, null); + createUniversityForApply(term + " 추가 지원", 영미권_미국_괌대학, null); universityInfoForApplyRepository.save(differentTermUniversityInfoForApply); likedUniversityRepository.saveAll(Set.of( createLikedUniversity(siteUser, 괌대학_A_지원_정보), From ea82ebd2501714b4e68bd0007dbd93b20f4492de Mon Sep 17 00:00:00 2001 From: sewon Date: Fri, 23 Aug 2024 13:37:03 +0900 Subject: [PATCH 086/158] =?UTF-8?q?refactor:=20=ED=95=B4=EC=99=B8=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=9D=84=20=EA=B3=A0=EB=A0=A4=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EC=8B=9C=EA=B0=84,=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20UTC=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/dto/PostFindCommentResponse.java | 6 ++--- .../entity/common/BaseEntity.java | 22 +++++++++++++------ .../post/dto/BoardFindPostResponse.java | 6 ++--- .../post/dto/PostFindResponse.java | 6 ++--- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java index 8524eb95a..2335b68ad 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java +++ b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java @@ -3,15 +3,15 @@ import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; public record PostFindCommentResponse( Long id, Long parentId, String content, Boolean isOwner, - LocalDateTime createdAt, - LocalDateTime updatedAt, + ZonedDateTime createdAt, + ZonedDateTime updatedAt, PostFindSiteUserResponse postFindSiteUserResponse ) { diff --git a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java b/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java index 5f1283c64..387561463 100644 --- a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java +++ b/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java @@ -2,14 +2,14 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreUpdate; import lombok.Getter; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @@ -18,9 +18,17 @@ @DynamicInsert public abstract class BaseEntity { - @CreatedDate - private LocalDateTime createdAt; + private ZonedDateTime createdAt; + private ZonedDateTime updatedAt; - @LastModifiedDate - private LocalDateTime updatedAt; + @PrePersist + public void onPrePersist() { + this.createdAt = ZonedDateTime.now(); + this.updatedAt = ZonedDateTime.now(); + } + + @PreUpdate + public void onPreUpdate() { + this.updatedAt = ZonedDateTime.now(); + } } diff --git a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java b/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java index 89c931925..8e6d3202a 100644 --- a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java +++ b/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java @@ -3,7 +3,7 @@ import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.post.domain.Post; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.List; import java.util.stream.Collectors; @@ -13,8 +13,8 @@ public record BoardFindPostResponse( String content, Long likeCount, Integer commentCount, - LocalDateTime createdAt, - LocalDateTime updatedAt, + ZonedDateTime createdAt, + ZonedDateTime updatedAt, String postCategory, String url ) { diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java index 7f5f703af..45e4e5dc7 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java @@ -6,7 +6,7 @@ import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.List; public record PostFindResponse( @@ -20,8 +20,8 @@ public record PostFindResponse( String postCategory, Boolean isOwner, Boolean isLiked, - LocalDateTime createdAt, - LocalDateTime updatedAt, + ZonedDateTime createdAt, + ZonedDateTime updatedAt, PostFindBoardResponse postFindBoardResponse, PostFindSiteUserResponse postFindSiteUserResponse, List postFindCommentResponses, From 2678bc47d5db89946752e0256f466e1b2c6f1246 Mon Sep 17 00:00:00 2001 From: sewon Date: Fri, 23 Aug 2024 13:39:23 +0900 Subject: [PATCH 087/158] =?UTF-8?q?refactor:=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=EC=8B=9C=20=EB=B6=80=EB=AA=A8=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EC=9D=B4=20=EB=AC=B4=EC=9D=98=EB=AF=B8=ED=95=B4?= =?UTF-8?q?=EC=A7=84=EB=8B=A4=EB=A9=B4=20=EB=B6=80=EB=AA=A8=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EB=8F=84=20=EC=82=AD=EC=A0=9C=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/service/CommentService.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 8003ab26b..56f7ea626 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -13,11 +13,9 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static com.example.solidconnection.custom.exception.ErrorCode.*; @Service @RequiredArgsConstructor @@ -87,15 +85,28 @@ public CommentDeleteResponse deleteCommentById(String email, Long postId, Long c Comment comment = commentRepository.getById(commentId); validateOwnership(comment, email); - if (comment.getCommentList().isEmpty()) { - // 하위 댓글이 없다면 삭제한다. + if (comment.getParentComment() != null) { + // 대댓글인 경우 + Comment parentComment = comment.getParentComment(); + // 대댓글을 삭제합니다. comment.resetPostAndSiteUserAndParentComment(); commentRepository.deleteById(commentId); + // 대댓글 삭제 이후, 부모댓글이 무의미하다면 이 역시 삭제합니다. + if (parentComment.getCommentList().isEmpty() && parentComment.getContent() == null) { + parentComment.resetPostAndSiteUserAndParentComment(); + commentRepository.deleteById(parentComment.getId()); + } } else { - // 하위 댓글 있으면 value만 null로 수정한다. - comment.deprecateComment(); + // 댓글인 경우 + if (comment.getCommentList().isEmpty()) { + // 대댓글이 없는 경우 + comment.resetPostAndSiteUserAndParentComment(); + commentRepository.deleteById(commentId); + } else { + // 대댓글이 있는 경우 + comment.deprecateComment(); + } } - return new CommentDeleteResponse(commentId); } } From d97ffcbb84ddca91eaf1350fa820b2ce02b06ce6 Mon Sep 17 00:00:00 2001 From: sewon Date: Fri, 23 Aug 2024 13:40:15 +0900 Subject: [PATCH 088/158] =?UTF-8?q?refactor:=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=EB=A7=8C=20=ED=97=88=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/service/CommentService.java | 10 +++++++++- .../solidconnection/custom/exception/ErrorCode.java | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 56f7ea626..8c0b0458f 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -41,6 +41,13 @@ private void validateDeprecated(Comment comment) { } } + // 대대댓글부터 허용하지 않음 + private void validateCommentDepth(Comment parentComment) { + if (parentComment.getParentComment() != null) { + throw new CustomException(INVALID_COMMENT_LEVEL); + } + } + @Transactional(readOnly = true) public List findCommentsByPostId(String email, Long postId) { return commentRepository.findCommentTreeByPostId(postId) @@ -58,6 +65,7 @@ public CommentCreateResponse createComment(String email, Long postId, CommentCre Comment parentComment = null; if (commentCreateRequest.parentId() != null) { parentComment = commentRepository.getById(commentCreateRequest.parentId()); + validateCommentDepth(parentComment); } Comment createdComment = commentRepository.save(commentCreateRequest.toEntity(siteUser, post, parentComment)); @@ -91,7 +99,7 @@ public CommentDeleteResponse deleteCommentById(String email, Long postId, Long c // 대댓글을 삭제합니다. comment.resetPostAndSiteUserAndParentComment(); commentRepository.deleteById(commentId); - // 대댓글 삭제 이후, 부모댓글이 무의미하다면 이 역시 삭제합니다. + // 대댓글 삭제 이후, 부모댓글이 무의미하다면 이역시 삭제합니다. if (parentComment.getCommentList().isEmpty() && parentComment.getContent() == null) { parentComment.resetPostAndSiteUserAndParentComment(); commentRepository.deleteById(parentComment.getId()); diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index c8bc38acc..b04c08b35 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -59,6 +59,7 @@ public enum ErrorCode { CAN_NOT_DELETE_OR_UPDATE_QUESTION(HttpStatus.BAD_REQUEST.value(), "질문글은 수정이나 삭제할 수 없습니다."), CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES(HttpStatus.BAD_REQUEST.value(), "5개 이상의 파일을 업로드할 수 없습니다."), INVALID_COMMENT_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 댓글입니다."), + INVALID_COMMENT_LEVEL(HttpStatus.BAD_REQUEST.value(), "최대 대댓글까지만 작성할 수 있습니다."), INVALID_COMMENT_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 댓글만 제어할 수 있습니다."), CAN_NOT_UPDATE_DEPRECATED_COMMENT(HttpStatus.BAD_REQUEST.value(),"이미 삭제된 댓글을 수정할 수 없습니다."), INVALID_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글 좋아요입니다."), From 4a86c99c803370622b11225d738ca915727d3c4b Mon Sep 17 00:00:00 2001 From: sewon Date: Fri, 23 Aug 2024 13:41:32 +0900 Subject: [PATCH 089/158] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20api=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/service/CommentServiceTest.java | 126 +++++++++++++----- 1 file changed, 93 insertions(+), 33 deletions(-) diff --git a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java index 8a90b275b..9ced8bcd8 100644 --- a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java @@ -44,9 +44,10 @@ class CommentServiceTest { private SiteUser siteUser; private Board board; private Post post; - private Comment parentComment_1; - private Comment parentComment_2; - private Comment p1s_childComment; + private Comment parentComment; + private Comment parentCommentWithNullContent; + private Comment childComment; + private Comment childCommentOfNullContentParent; @BeforeEach @@ -54,9 +55,10 @@ void setUp() { siteUser = createSiteUser(); board = createBoard(); post = createPost(board, siteUser); - parentComment_1 = createParentComment(); - parentComment_2 = createParentComment(); - p1s_childComment = createChildComment(); + parentComment = createParentComment("parent"); + parentCommentWithNullContent = createParentComment(null); + childComment = createChildComment(parentComment); + childCommentOfNullContentParent = createChildComment(parentCommentWithNullContent); } private SiteUser createSiteUser() { @@ -89,19 +91,19 @@ private Post createPost(Board board, SiteUser siteUser) { return post; } - private Comment createParentComment() { + private Comment createParentComment(String content) { Comment comment = new Comment( - "parent" + content ); comment.setPostAndSiteUser(post, siteUser); return comment; } - private Comment createChildComment() { + private Comment createChildComment(Comment parentComment) { Comment comment = new Comment( "child" ); - comment.setParentCommentAndPostAndSiteUser(parentComment_1, post, siteUser); + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); return comment; } @@ -112,7 +114,7 @@ private Comment createChildComment() { @Test void 특정_게시글의_댓글들을_조회한다() { // Given - List commentList = List.of(parentComment_1, p1s_childComment, parentComment_2); + List commentList = List.of(parentComment, childComment, parentCommentWithNullContent); when(commentRepository.findCommentTreeByPostId(post.getId())).thenReturn(commentList); // When @@ -141,18 +143,18 @@ private Boolean isOwner(Comment comment, String email) { ); when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.save(any(Comment.class))).thenReturn(parentComment_1); + when(commentRepository.save(any(Comment.class))).thenReturn(parentComment); // When CommentCreateResponse commentCreateResponse = commentService.createComment( siteUser.getEmail(), post.getId(), commentCreateRequest); // Then - assertEquals(commentCreateResponse, CommentCreateResponse.from(parentComment_1)); + assertEquals(commentCreateResponse, CommentCreateResponse.from(parentComment)); verify(commentRepository, times(0)) .getById(any(Long.class)); verify(commentRepository, times(1)) - .save(commentCreateRequest.toEntity(siteUser, post, parentComment_1)); + .save(commentCreateRequest.toEntity(siteUser, post, parentComment)); } @Test @@ -164,19 +166,19 @@ private Boolean isOwner(Comment comment, String email) { ); when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(parentCommentId)).thenReturn(parentComment_1); - when(commentRepository.save(any(Comment.class))).thenReturn(p1s_childComment); + when(commentRepository.getById(parentCommentId)).thenReturn(parentComment); + when(commentRepository.save(any(Comment.class))).thenReturn(childComment); // When CommentCreateResponse commentCreateResponse = commentService.createComment( siteUser.getEmail(), post.getId(), commentCreateRequest); // Then - assertEquals(commentCreateResponse, CommentCreateResponse.from(p1s_childComment)); + assertEquals(commentCreateResponse, CommentCreateResponse.from(childComment)); verify(commentRepository, times(1)) .getById(parentCommentId); verify(commentRepository, times(1)) - .save(commentCreateRequest.toEntity(siteUser, post, parentComment_1)); + .save(commentCreateRequest.toEntity(siteUser, post, parentComment)); } @@ -225,6 +227,29 @@ private Boolean isOwner(Comment comment, String email) { .save(any(Comment.class)); } + @Test + void 댓글을_등록할_때_대대댓글_부터는_예외_응답을_반환한다() { + // Given + Long childCommentId = 1L; + CommentCreateRequest commentCreateRequest = new CommentCreateRequest( + "child's child", childCommentId + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(childCommentId)).thenReturn(childComment); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + commentService.createComment(siteUser.getEmail(), post.getId(), commentCreateRequest) + ); + assertThat(exception.getMessage()) + .isEqualTo(INVALID_COMMENT_LEVEL.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(INVALID_COMMENT_LEVEL.getCode()); + verify(commentRepository, times(0)) + .save(any(Comment.class)); + } + /** * 댓글 수정 */ @@ -236,14 +261,14 @@ private Boolean isOwner(Comment comment, String email) { ); when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment_1); + when(commentRepository.getById(any())).thenReturn(parentComment); // When CommentUpdateResponse commentUpdateResponse = commentService.updateComment( - siteUser.getEmail(), post.getId(), parentComment_1.getId(), commentUpdateRequest); + siteUser.getEmail(), post.getId(), parentComment.getId(), commentUpdateRequest); // Then - assertEquals(commentUpdateResponse.id(), parentComment_1.getId()); + assertEquals(commentUpdateResponse.id(), parentComment.getId()); } @Test @@ -258,7 +283,7 @@ private Boolean isOwner(Comment comment, String email) { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(siteUser.getEmail(), invalidPostId, parentComment_1.getId(), commentUpdateRequest) + commentService.updateComment(siteUser.getEmail(), invalidPostId, parentComment.getId(), commentUpdateRequest) ); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); @@ -290,17 +315,17 @@ private Boolean isOwner(Comment comment, String email) { @Test void 댓글을_수정할_때_이미_삭제된_댓글이라면_예외_응답을_반환한다() { // Given - parentComment_1.deprecateComment(); + parentComment.deprecateComment(); CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( "update" ); when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment_1); + when(commentRepository.getById(any())).thenReturn(parentComment); // When & Then CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(siteUser.getEmail(), post.getId(), parentComment_1.getId(), commentUpdateRequest) + commentService.updateComment(siteUser.getEmail(), post.getId(), parentComment.getId(), commentUpdateRequest) ); assertThat(exception.getMessage()) .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getMessage()); @@ -317,11 +342,11 @@ private Boolean isOwner(Comment comment, String email) { ); when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment_1); + when(commentRepository.getById(any())).thenReturn(parentComment); // When & Then CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(invalidEmail, post.getId(), parentComment_1.getId(), commentUpdateRequest) + commentService.updateComment(invalidEmail, post.getId(), parentComment.getId(), commentUpdateRequest) ); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ACCESS.getMessage()); @@ -339,14 +364,14 @@ private Boolean isOwner(Comment comment, String email) { Long parentCommentId = 1L; when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment_1); + when(commentRepository.getById(any())).thenReturn(parentComment); // When CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( siteUser.getEmail(), post.getId(), parentCommentId); // Then - assertEquals(parentComment_1.getContent(), null); + assertEquals(parentComment.getContent(), null); assertEquals(commentDeleteResponse.id(), parentCommentId); verify(commentRepository, times(0)).deleteById(parentCommentId); } @@ -357,7 +382,24 @@ private Boolean isOwner(Comment comment, String email) { Long childCommentId = 1L; when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(p1s_childComment); + when(commentRepository.getById(any())).thenReturn(childComment); + + // When + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( + siteUser.getEmail(), post.getId(), childCommentId); + + // Then + assertEquals(commentDeleteResponse.id(), childCommentId); + verify(commentRepository, times(1)).deleteById(childCommentId); + } + + @Test + void 대댓글을_삭제한다_부모댓글_유효() { + // Given + Long childCommentId = 1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(childComment); // When CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( @@ -368,6 +410,24 @@ private Boolean isOwner(Comment comment, String email) { verify(commentRepository, times(1)).deleteById(childCommentId); } + @Test + void 대댓글을_삭제한다_부모댓글_유효하지_않음() { + // Given + + Long childCommentId = 1L; + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(postRepository.getById(post.getId())).thenReturn(post); + when(commentRepository.getById(any())).thenReturn(childCommentOfNullContentParent); + + // When + CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( + siteUser.getEmail(), post.getId(), childCommentId); + + // Then + assertEquals(commentDeleteResponse.id(), childCommentId); + verify(commentRepository, times(2)).deleteById(any()); + } + @Test void 댓글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { // Given @@ -377,7 +437,7 @@ private Boolean isOwner(Comment comment, String email) { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - commentService.deleteCommentById(siteUser.getEmail(), invalidPostId, parentComment_1.getId()) + commentService.deleteCommentById(siteUser.getEmail(), invalidPostId, parentComment.getId()) ); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); @@ -409,11 +469,11 @@ private Boolean isOwner(Comment comment, String email) { String invalidEmail = "invalidEmail@test.com"; when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment_1); + when(commentRepository.getById(any())).thenReturn(parentComment); // When & Then CustomException exception = assertThrows(CustomException.class, () -> - commentService.deleteCommentById(invalidEmail, post.getId(), parentComment_1.getId()) + commentService.deleteCommentById(invalidEmail, post.getId(), parentComment.getId()) ); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ACCESS.getMessage()); From 16821b9fc108468c2032495778f0c5915e8bb4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:28:16 +0900 Subject: [PATCH 090/158] =?UTF-8?q?=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EA=B5=AC=EC=B6=95=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Actuator,Prometheus 의존성 추가 * chore: Actuator 설정 정보 추가 * feat: /actuator/** 경로 허용 --- build.gradle | 2 ++ .../config/security/SecurityConfiguration.java | 3 ++- src/main/resources/application.yml | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 45902c5f4..3799f64bc 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,8 @@ dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거 implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' testImplementation "org.mockito:mockito-core:3.3.3" + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-registry-prometheus' compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 1c63fe94a..934eaf9f8 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -51,7 +51,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/file/profile/pre", "/auth/kakao", "/auth/sign-up", "/auth/reissue", "/university/detail/**", "/university/search/**", "/university/recommends", - "/swagger-ui/**", "/v3/api-docs/**" + "/swagger-ui/**", "/v3/api-docs/**", + "/actuator/**" ) .permitAll() .anyRequest().authenticated()) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 22cc4368e..b114af84e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -48,3 +48,9 @@ view: count: scheduling: delay: 3000 + +management: + endpoints: + web: + exposure: + include: prometheus From db787ea7ab71c6f4ba9be434b7127664f1b2342f Mon Sep 17 00:00:00 2001 From: sewon Date: Sat, 24 Aug 2024 20:32:39 +0900 Subject: [PATCH 091/158] =?UTF-8?q?refactor:=20=EC=8B=9C=EA=B0=84=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20UTC=20=ED=83=80=EC=9E=84=EC=A1=B4=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=A7=80=EC=A0=95=ED=95=98=EC=97=AC=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/entity/common/BaseEntity.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java b/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java index 387561463..27493f1be 100644 --- a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java +++ b/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java @@ -9,6 +9,7 @@ import org.hibernate.annotations.DynamicUpdate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.ZoneId; import java.time.ZonedDateTime; @MappedSuperclass @@ -23,12 +24,12 @@ public abstract class BaseEntity { @PrePersist public void onPrePersist() { - this.createdAt = ZonedDateTime.now(); - this.updatedAt = ZonedDateTime.now(); + this.createdAt = ZonedDateTime.now(ZoneId.of("UTC")); + this.updatedAt = this.createdAt; } @PreUpdate public void onPreUpdate() { - this.updatedAt = ZonedDateTime.now(); + this.updatedAt = ZonedDateTime.now(ZoneId.of("UTC")); } } From bf27f612442a1f08e87164f2a7549c46dda38115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Sun, 25 Aug 2024 20:02:22 +0900 Subject: [PATCH 092/158] =?UTF-8?q?=EC=A7=80=EC=9B=90=EC=84=9C=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=ED=95=99=EA=B8=B0=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Application에 term 필드 추가 * feat: 지원시 term 저장하는 로직 추가 * refactor: 성적 공유 목록 확인시 현재 term만 조회하도록 수정 * refactor: UniversityApplicantsResponse 반환값 universityInfoForApply.getKoreanName()로 수정한다 * refactor: 학기별로 유저가 application을 가질 수 있도록 수정 * refactor: 유저,학기 쌍으로 유일한 Application객체 찾도록 수정 * refactor: 다른 지원자 성적 조회 가능 기준을 금학기 지원이력 유무로 수정 * test: ApplicationService 테스트 로직 추가 * test: ApplicationSubmissionService 생성에 필요한 Mock 추가 --- .../application/domain/Application.java | 9 +- .../dto/UniversityApplicantsResponse.java | 2 +- .../repository/ApplicationRepository.java | 14 +- .../service/ApplicationQueryService.java | 24 +-- .../service/ApplicationSubmissionService.java | 39 ++-- .../service/VerifyStatusQueryService.java | 7 +- .../service/UniversityRecommendService.java | 2 +- .../e2e/ApplicantsQueryTest.java | 49 ++++- .../e2e/ApplicationSubmissionTest.java | 20 +- .../e2e/VerifyStatusQueryTest.java | 8 +- .../unit/service/ApplicationServiceTest.java | 174 ++++++++++++++++++ 11 files changed, 291 insertions(+), 57 deletions(-) create mode 100644 src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java index 44dcc52f1..085141f22 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Application.java +++ b/src/main/java/com/example/solidconnection/application/domain/Application.java @@ -48,6 +48,9 @@ public class Application { @Column(columnDefinition = "int not null default 0") private Integer updateCount; + @Column(length = 50, nullable = false) + private String term; + @ManyToOne private UniversityInfoForApply firstChoiceUniversity; @@ -63,10 +66,14 @@ public class Application { public Application( SiteUser siteUser, Gpa gpa, - LanguageTest languageTest) { + LanguageTest languageTest, + String term) { this.siteUser = siteUser; this.gpa = gpa; this.languageTest = languageTest; + this.term = term; + this.updateCount = 0; + this.verifyStatus = PENDING; } public void updateGpaAndLanguageTest( diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java index a78c98c20..84751786b 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java @@ -25,7 +25,7 @@ public record UniversityApplicantsResponse( public static UniversityApplicantsResponse of(UniversityInfoForApply universityInfoForApply, List applicant) { return new UniversityApplicantsResponse( - universityInfoForApply.getUniversity().getKoreanName(), + universityInfoForApply.getKoreanName(), universityInfoForApply.getStudentCapacity(), universityInfoForApply.getUniversity().getRegion().getKoreanName(), universityInfoForApply.getUniversity().getCountry().getKoreanName(), diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java index 34b714438..8f30c196c 100644 --- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -18,18 +18,18 @@ public interface ApplicationRepository extends JpaRepository boolean existsByNicknameForApply(String nicknameForApply); - Optional findBySiteUser_Email(String email); + Optional findTop1BySiteUser_EmailOrderByTermDesc(String email); - Optional findBySiteUser(SiteUser siteUser); + Optional findBySiteUserAndTerm(SiteUser siteUser, String term); - List findAllByFirstChoiceUniversityAndVerifyStatus(UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus); + List findAllByFirstChoiceUniversityAndVerifyStatusAndTerm(UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus, String term); - List findAllBySecondChoiceUniversityAndVerifyStatus(UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus); + List findAllBySecondChoiceUniversityAndVerifyStatusAndTerm(UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus, String term); - List findAllByThirdChoiceUniversityAndVerifyStatus(UniversityInfoForApply thirdChoiceUniversity, VerifyStatus verifyStatus); + List findAllByThirdChoiceUniversityAndVerifyStatusAndTerm(UniversityInfoForApply thirdChoiceUniversity, VerifyStatus verifyStatus, String term); - default Application getApplicationBySiteUser(SiteUser siteUser) { - return findBySiteUser(siteUser) + default Application getApplicationBySiteUserAndTerm(SiteUser siteUser, String term) { + return findBySiteUserAndTerm(siteUser, term) .orElseThrow(() -> new CustomException(APPLICATION_NOT_FOUND)); } } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 793cf28ca..2e481023d 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -52,41 +52,43 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin List universities = universityFilterRepository.findByRegionCodeAndKeywords(regionCode, List.of(keyword)); - // 1지망, 2지망 지원자들을 조회한다. - List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser); - List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser); - List thirdChoiceApplicants = getThirdChoiceApplicants(universities, siteUser); + // 1지망, 2지망, 3지망 지원자들을 조회한다. + List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser, term); + List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser, term); + List thirdChoiceApplicants = getThirdChoiceApplicants(universities, siteUser, term); return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants); } + // 학기별로 상태가 관리된다. + // 금학기에 지원이력이 있는 사용자만 지원정보를 확인할 수 있도록 한다. private void validateSiteUserCanViewApplicants(SiteUser siteUser) { - VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUser(siteUser).getVerifyStatus(); + VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term).getVerifyStatus(); if (verifyStatus != VerifyStatus.APPROVED) { throw new CustomException(APPLICATION_NOT_APPROVED); } } - private List getFirstChoiceApplicants(List universities, SiteUser siteUser) { + private List getFirstChoiceApplicants(List universities, SiteUser siteUser, String term) { return getApplicantsByChoice( universities, siteUser, - uia -> applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatus(uia, VerifyStatus.APPROVED) + uia -> applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatusAndTerm(uia, VerifyStatus.APPROVED, term) ); } - private List getSecondChoiceApplicants(List universities, SiteUser siteUser) { + private List getSecondChoiceApplicants(List universities, SiteUser siteUser, String term) { return getApplicantsByChoice( universities, siteUser, - uia -> applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatus(uia, VerifyStatus.APPROVED) + uia -> applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatusAndTerm(uia, VerifyStatus.APPROVED, term) ); } - private List getThirdChoiceApplicants(List universities, SiteUser siteUser) { + private List getThirdChoiceApplicants(List universities, SiteUser siteUser, String term) { return getApplicantsByChoice( universities, siteUser, - uia -> applicationRepository.findAllByThirdChoiceUniversityAndVerifyStatus(uia, VerifyStatus.APPROVED) + uia -> applicationRepository.findAllByThirdChoiceUniversityAndVerifyStatusAndTerm(uia, VerifyStatus.APPROVED, term) ); } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index beec76a05..753bc89cc 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -16,13 +16,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; -import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; -import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; -import static com.example.solidconnection.custom.exception.ErrorCode.SCORE_SHOULD_SUBMITTED_FIRST; +import static com.example.solidconnection.custom.exception.ErrorCode.*; @RequiredArgsConstructor @Service @@ -35,11 +31,12 @@ public class ApplicationSubmissionService { private final SiteUserRepository siteUserRepository; @Value("${university.term}") - public String term; + private String term; /* * 학점과 영어 성적을 제출한다. - * - 기존에 제출한 적이 있다면, 수정한다. + * - 금학기에 제출한 적이 있다면, 수정한다. + * - 성적을 제출한적이 한번도 없거나 제출한적이 있지만 금학기에 제출한 적이 없다면 새로 등록한다. * - 수정을 하고 나면, 성적 승인 상태(verifyStatus)를 PENDING 상태로 변경한다. * */ @Transactional @@ -48,15 +45,14 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) { Gpa gpa = scoreRequest.toGpa(); LanguageTest languageTest = scoreRequest.toLanguageTest(); - applicationRepository.findBySiteUser_Email(email) + applicationRepository.findBySiteUserAndTerm(siteUser, term) .ifPresentOrElse( - // 수정 + // 금학기에 성적 제출 이력이 있는 경우 application -> application.updateGpaAndLanguageTest(gpa, languageTest), - - // 최초 등록 - () -> applicationRepository.save( - new Application(siteUser, gpa, languageTest) - ) + () -> { + // 성적 제출한적이 한번도 없는 경우 && 성적 제출한적이 있지만 금학기에 없는 경우 + applicationRepository.save(new Application(siteUser, gpa, languageTest, term)); + } ); return true; } @@ -73,9 +69,20 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) { @Transactional public boolean submitUniversityChoice(String email, UniversityChoiceRequest universityChoiceRequest) { validateNoDuplicateUniversityChoices(universityChoiceRequest); - Application application = applicationRepository.findBySiteUser_Email(email) + + // 성적 제출한 적이 한번도 없는 경우 + Application existingApplication = applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(email) .orElseThrow(() -> new CustomException(SCORE_SHOULD_SUBMITTED_FIRST)); + Application application = Optional.of(existingApplication) + .filter(app -> !app.getTerm().equals(term)) + .map(app -> { + // 성적 제출한 적이 있지만 금학기에 없는 경우, 이전 성적으로 새 Application 객체를 등록 + SiteUser siteUser = siteUserRepository.getByEmail(email); + return applicationRepository.save(new Application(siteUser, app.getGpa(), app.getLanguageTest(), term)); + }) + .orElse(existingApplication); // 금학기에 이미 성적 제출한 경우 기존 객체 사용 + UniversityInfoForApply firstChoiceUniversity = universityInfoForApplyRepository .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term); UniversityInfoForApply secondChoiceUniversity = universityInfoForApplyRepository diff --git a/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java b/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java index a5f7bf752..33bb340d9 100644 --- a/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java @@ -7,6 +7,7 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,13 +26,17 @@ public class VerifyStatusQueryService { private final ApplicationRepository applicationRepository; private final SiteUserRepository siteUserRepository; + @Value("${university.term}") + private String term; + /* * 지원 상태를 조회한다. + * 학기별로 상태가 관리된다. * */ @Transactional(readOnly = true) public VerifyStatusResponse getVerifyStatus(String email) { SiteUser siteUser = siteUserRepository.getByEmail(email); - Optional application = applicationRepository.findBySiteUser_Email(siteUser.getEmail()); + Optional application = applicationRepository.findBySiteUserAndTerm(siteUser,term); // 아무것도 제출 안함 if (application.isEmpty()) { diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index bb704faea..88a7a222f 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -26,7 +26,7 @@ public class UniversityRecommendService { private final SiteUserRepository siteUserRepository; @Value("${university.term}") - public String term; + private String term; /* * 사용자 맞춤 추천 대학교를 불러온다. diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index d609aa121..cb5fbfe52 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import java.util.List; @@ -42,6 +43,11 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { private Application 사용자1_지원정보; private Application 사용자2_지원정보; private Application 사용자3_지원정보; + private Application 사용자4_이전학기_지원정보; + + @Value("${university.term}") + private String term; + private String beforeTerm = "1988-1"; @BeforeEach public void setUpUserAndToken() { @@ -58,23 +64,27 @@ public void setUpUserAndToken() { SiteUser 사용자1 = siteUserRepository.save(createSiteUserByEmail("email1")); SiteUser 사용자2 = siteUserRepository.save(createSiteUserByEmail("email2")); SiteUser 사용자3 = siteUserRepository.save(createSiteUserByEmail("email3")); + SiteUser 사용자4_이전학기_지원자 = siteUserRepository.save(createSiteUserByEmail("email4")); // setUp - 지원 정보 저장 Gpa gpa = createDummyGpa(); LanguageTest languageTest = createDummyLanguageTest(); - 나의_지원정보 = new Application(siteUser, gpa, languageTest); - 사용자1_지원정보 = new Application(사용자1, gpa, languageTest); - 사용자2_지원정보 = new Application(사용자2, gpa, languageTest); - 사용자3_지원정보 = new Application(사용자3, gpa, languageTest); + 나의_지원정보 = new Application(siteUser, gpa, languageTest, term); + 사용자1_지원정보 = new Application(사용자1, gpa, languageTest, term); + 사용자2_지원정보 = new Application(사용자2, gpa, languageTest, term); + 사용자3_지원정보 = new Application(사용자3, gpa, languageTest, term); + 사용자4_이전학기_지원정보 = new Application(사용자4_이전학기_지원자, gpa, languageTest, beforeTerm); 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 린츠_카톨릭대학_지원_정보, "0"); 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 그라츠공과대학_지원_정보, "1"); 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, 서던덴마크대학교_지원_정보, "2"); 사용자3_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "3"); + 사용자4_이전학기_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "4"); 나의_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자1_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자2_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자3_지원정보.setVerifyStatus(VerifyStatus.APPROVED); - applicationRepository.saveAll(List.of(나의_지원정보, 사용자1_지원정보, 사용자2_지원정보, 사용자3_지원정보)); + 사용자4_이전학기_지원정보.setVerifyStatus(VerifyStatus.APPROVED); + applicationRepository.saveAll(List.of(나의_지원정보, 사용자1_지원정보, 사용자2_지원정보, 사용자3_지원정보, 사용자4_이전학기_지원정보)); } @Test @@ -195,4 +205,33 @@ public void setUpUserAndToken() { assertThat(secondChoiceApplicants).containsExactlyInAnyOrder( UniversityApplicantsResponse.of(메이지대학_지원_정보, List.of())); } + + @Test + void 지원자를_조회할_때_이전학기_지원자는_조회되지_않는다() { + ApplicationsResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().log().all() + .get("/application") + .then().log().all() + .statusCode(200) + .extract().as(ApplicationsResponse.class); + + List firstChoiceApplicants = response.firstChoice(); + List secondChoiceApplicants = response.secondChoice(); + List thirdChoiceApplicants = response.thirdChoice(); + + + assertThat(firstChoiceApplicants).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(사용자4_이전학기_지원정보, false))) + )); + assertThat(secondChoiceApplicants).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(사용자4_이전학기_지원정보, false))) + )); + assertThat(thirdChoiceApplicants).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(사용자4_이전학기_지원정보, false))) + )); + } } diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java index 0cbe37f35..a12612071 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java @@ -66,7 +66,7 @@ public void setUpUserAndToken() { .then().log().all() .statusCode(HttpStatus.OK.value()); - Application application = applicationRepository.getApplicationBySiteUser(siteUser); + Application application = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); assertAll("대학교 성적과 어학 성적을 저장한다.", () -> assertThat(application.getId()).isNotNull(), () -> assertThat(application.getSiteUser().getId()).isEqualTo(siteUser.getId()), @@ -84,7 +84,7 @@ public void setUpUserAndToken() { // setUp - 성적 정보 저장 ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())); + applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest(),term)); // request - body 생성 및 요청 ScoreRequest secondRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "90", @@ -98,7 +98,7 @@ public void setUpUserAndToken() { .then().log().all() .statusCode(HttpStatus.OK.value()); - Application updatedApplication = applicationRepository.getApplicationBySiteUser(siteUser); + Application updatedApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); assertAll("대학교 성적과 어학 성적을 수정한다. 이때 수정 횟수는 증가하지 않고, 성적 승인 상태는 PENDING 으로 바뀐다.", () -> assertThat(updatedApplication.getId()).isNotNull(), () -> assertThat(updatedApplication.getSiteUser().getId()).isEqualTo(siteUser.getId()), @@ -116,7 +116,7 @@ public void setUpUserAndToken() { // setUp - 성적 정보 저장 ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())); + applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest(),term)); // request - body 생성 및 요청 UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); @@ -129,7 +129,7 @@ public void setUpUserAndToken() { .then().log().all() .statusCode(HttpStatus.OK.value()); - Application application = applicationRepository.getApplicationBySiteUser(siteUser); + Application application = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); assertAll("지망 대학교를 저장한다.", () -> assertThat(application.getId()).isNotNull(), () -> assertThat(application.getSiteUser().getId()).isEqualTo(siteUser.getId()), @@ -146,9 +146,9 @@ public void setUpUserAndToken() { // setUp - 성적 정보와 지망 대학 저장 ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())) + applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest(),term)) .updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "nickname"); - Application initialApplication = applicationRepository.getApplicationBySiteUser(siteUser); + Application initialApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); // request - body 생성 및 요청 UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); @@ -161,7 +161,7 @@ public void setUpUserAndToken() { .then().log().all() .statusCode(HttpStatus.OK.value()); - Application updatedApplication = applicationRepository.getApplicationBySiteUser(siteUser); + Application updatedApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); assertAll("지망 대학교를 수정한다. 이때 수정 횟수는 증가하고, 성적 승인 상태는 바뀌지 않는다.", () -> assertThat(updatedApplication.getId()).isNotNull(), () -> assertThat(updatedApplication.getSiteUser().getId()).isEqualTo(siteUser.getId()), @@ -178,8 +178,8 @@ public void setUpUserAndToken() { // setUp - 성적 정보와 지망 대학 저장 ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest())); - Application initialApplication = applicationRepository.getApplicationBySiteUser(siteUser); + applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest(),term)); + Application initialApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); // setUp - 지망 대학을 한계까지 수정 for (int i = 0; i <= APPLICATION_UPDATE_COUNT_LIMIT; i++) { diff --git a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java index b4447247a..856c4cdca 100644 --- a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java @@ -66,7 +66,7 @@ public void setUpUserAndToken() { @Test void 성적만_제출한_상태를_반환한다() { // setUp - 성적만 제출한 상태 - Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); + Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest(),term); applicationRepository.save(application); // request - 요청 @@ -86,7 +86,7 @@ public void setUpUserAndToken() { @Test void 성적과_대학을_모두_제출하고_승인을_기대라는_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출한 상태 - Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); + Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest(),term); application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "닉네임"); applicationRepository.save(application); @@ -107,7 +107,7 @@ public void setUpUserAndToken() { @Test void 성적과_대학을_모두_제출했지만_승인이_반려된_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출했지만, 승인 거절 - Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); + Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest(),term); application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보,"닉네임"); application.setVerifyStatus(VerifyStatus.REJECTED); applicationRepository.save(application); @@ -129,7 +129,7 @@ public void setUpUserAndToken() { @Test void 성적과_대학을_모두_제출했으며_승인이_된_상태를_반환한다() { // setUp - 성적과 대학을 모두 제출했으며, 승인이 된 상태 - Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest()); + Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest(),term); application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "닉네임"); application.setVerifyStatus(VerifyStatus.APPROVED); applicationRepository.save(application); diff --git a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java new file mode 100644 index 000000000..d967c90bf --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java @@ -0,0 +1,174 @@ +package com.example.solidconnection.unit.service; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.dto.ScoreRequest; +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.application.service.ApplicationSubmissionService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.*; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.SCORE_SHOULD_SUBMITTED_FIRST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("지원 서비스 테스트") +public class ApplicationServiceTest { + @InjectMocks + ApplicationSubmissionService applicationSubmissionService; + @Mock + ApplicationRepository applicationRepository; + @Mock + SiteUserRepository siteUserRepository; + @Mock + UniversityInfoForApplyRepository universityInfoForApplyRepository; + + private SiteUser siteUser; + private Application application; + private Application applicationBeforeTerm; + + private String term = "2024-1"; + private String beforeTerm = "1999-1"; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(applicationSubmissionService, "term", term); // 테스트시 @value값 주입위함 + siteUser = createSiteUser(); + application = createApplication(term); + applicationBeforeTerm = createApplication(beforeTerm); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private Application createApplication(String term) { + return new Application( + siteUser, + new Gpa(4.0, 4.5, "url"), + new LanguageTest(LanguageTestType.TOEIC, "900", "url"), + term + ); + } + + @Test + void 성적을_제출한다_금학기_제출이력_없음() { + // Given + ScoreRequest scoreRequest = new ScoreRequest( + LanguageTestType.TOEIC, "990", "url", 4.5, 4.5, "url" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.empty()); + + // When + applicationSubmissionService.submitScore(siteUser.getEmail(), scoreRequest); + + // Then + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(applicationRepository, times(1)).findBySiteUserAndTerm(siteUser, term); + verify(applicationRepository, times(1)).save(any(Application.class)); + } + + @Test + void 성적을_제출한다_금학기_제출이력_있음() { + // Given + ScoreRequest scoreRequest = new ScoreRequest( + LanguageTestType.TOEIC, "990", "url", 4.5, 4.5, "url" + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.of(application)); + + // When + applicationSubmissionService.submitScore(siteUser.getEmail(), scoreRequest); + + // Then + assertEquals(application.getGpa().getGpa(), scoreRequest.gpa()); + assertEquals(application.getLanguageTest().getLanguageTestScore(), scoreRequest.languageTestScore()); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(applicationRepository, times(1)).findBySiteUserAndTerm(siteUser, term); + verify(applicationRepository, times(0)).save(any(Application.class)); + } + + // 예외테스트 + @Test + void 지망대학_제출할_때_성적_제출이력이_없다면_예외_응답을_반환한다() { + // given + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 1L, 2L, 3L + ); + when(applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail())) + .thenReturn(Optional.empty()); + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + }); + assertThat(exception.getMessage()) + .isEqualTo(SCORE_SHOULD_SUBMITTED_FIRST.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(SCORE_SHOULD_SUBMITTED_FIRST.getCode()); + } + + @Test + void 지망대학_제출한다_이전학기_성적_제출이력_있음() { + // Given + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 1L, 2L, 3L + ); + when(applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail())) + .thenReturn(Optional.of(applicationBeforeTerm)); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + + // When + applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + + // Then + verify(applicationRepository, times(1)).findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail()); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(applicationRepository, times(1)).save(any(Application.class)); + } + + @Test + void 지망대학_제출한다_금학기_성적_제출이력_있음() { + // Given + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 1L, 2L, 3L + ); + when(applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail())) + .thenReturn(Optional.of(application)); + + // When + applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + + // Then + verify(applicationRepository, times(1)).findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail()); + verify(siteUserRepository, times(0)).getByEmail(siteUser.getEmail()); + verify(applicationRepository, times(0)).save(any(Application.class)); + } +} From 9062f816b0daf1b396e851511ccd9793ef206fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Sun, 25 Aug 2024 20:16:04 +0900 Subject: [PATCH 093/158] =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95,=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=88=98=EC=A0=95=20API=20=EB=B6=84=EB=A6=AC=20(#7?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 프로필 이미지 수정 서비스 로직 추가 * feat: 닉네임 수정 서비스 로직 추가 * feat: 프로필 이미지 수정 컨트롤러 로직 추가 * feat: 닉네임 수정 컨트롤러 로직 추가 * feat: 닉네임 수정 관련 DTO 추가 * feat: 프로필 이미지 수정 관련 DTO 추가 * feat: 프로필 이미지 수정 관련 예외 추가 * test: 프로필 이미지, 닉네임 수정 관련 테스트 로직 추가 * chore: 마이페이지 정보 수정 API 삭제 --- .../custom/exception/ErrorCode.java | 1 + .../controller/SiteUserController.java | 30 ++-- .../controller/SiteUserControllerSwagger.java | 23 --- .../siteuser/dto/NicknameUpdateRequest.java | 11 ++ .../siteuser/dto/NicknameUpdateResponse.java | 15 ++ .../dto/ProfileImageUpdateResponse.java | 15 ++ .../siteuser/service/SiteUserService.java | 74 +++++--- .../solidconnection/e2e/MyPageUpdateTest.java | 34 ++-- .../unit/service/SiteUserServiceTest.java | 162 ++++++++++++++++++ 9 files changed, 285 insertions(+), 80 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateRequest.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateResponse.java create mode 100644 src/main/java/com/example/solidconnection/siteuser/dto/ProfileImageUpdateResponse.java create mode 100644 src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index b04c08b35..f9e1e45b1 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -50,6 +50,7 @@ public enum ErrorCode { APPLY_UPDATE_LIMIT_EXCEED(HttpStatus.BAD_REQUEST.value(), "지원 정보 수정은 " + APPLICATION_UPDATE_COUNT_LIMIT + "회까지만 가능합니다."), CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2, 3지망에 동일한 대학교를 입력할 수 없습니다."), CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 " + MIN_DAYS_BETWEEN_NICKNAME_CHANGES + "일이 지나지 않았습니다."), + PROFILE_IMAGE_NEEDED(HttpStatus.BAD_REQUEST.value(), "프로필 이미지가 필요합니다."), // community INVALID_POST_CATEGORY(HttpStatus.BAD_REQUEST.value(),"잘못된 카테고리명입니다."), diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java index d7fe1a1fc..443404def 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -1,17 +1,12 @@ package com.example.solidconnection.siteuser.controller; -import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.dto.MyPageUpdateRequest; -import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.dto.*; import com.example.solidconnection.siteuser.service.SiteUserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.security.Principal; @@ -36,12 +31,19 @@ public ResponseEntity getMyPageInfoToUpdate(Principal prin .ok(myPageUpdateDto); } - @PatchMapping("/update") - public ResponseEntity updateMyPageInfo( + @PatchMapping("/update/profileImage") + public ResponseEntity updateProfileImage( Principal principal, - @Valid @RequestBody MyPageUpdateRequest myPageUpdateDto) { - MyPageUpdateResponse myPageUpdateResponse = siteUserService.update(principal.getName(), myPageUpdateDto); - return ResponseEntity - .ok(myPageUpdateResponse); + @RequestParam(value = "file", required = false) MultipartFile imageFile) { + ProfileImageUpdateResponse profileImageUpdateResponse = siteUserService.updateProfileImage(principal.getName(), imageFile); + return ResponseEntity.ok().body(profileImageUpdateResponse); + } + + @PatchMapping("/update/nickname") + public ResponseEntity updateNickname( + Principal principal, + @Valid @RequestBody NicknameUpdateRequest nicknameUpdateRequest) { + NicknameUpdateResponse nicknameUpdateResponse = siteUserService.updateNickname(principal.getName(), nicknameUpdateRequest); + return ResponseEntity.ok().body(nicknameUpdateResponse); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java index e17b41c3e..6f479f67e 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java @@ -52,27 +52,4 @@ public interface SiteUserControllerSwagger { } ) ResponseEntity getMyPageInfoToUpdate(Principal principal); - - @Operation( - summary = "마이 페이지 정보 수정", - requestBody = @RequestBody( - description = "업데이트할 정보", - required = true, - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = MyPageUpdateRequest.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "마이 페이지 정보 수정 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = MyPageUpdateResponse.class) - ) - ) - } - ) - ResponseEntity updateMyPageInfo(Principal principal, @Valid @RequestBody MyPageUpdateRequest myPageUpdateDto); } diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateRequest.java b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateRequest.java new file mode 100644 index 000000000..4627a7451 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateRequest.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.siteuser.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record NicknameUpdateRequest( + @NotBlank(message = "닉네임을 입력해주세요.") + @Schema(description = "변경할 닉네임", example = "NewNickname") + String nickname +) { +} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateResponse.java new file mode 100644 index 000000000..5a967fa6e --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateResponse.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.siteuser.dto; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import io.swagger.v3.oas.annotations.media.Schema; + +public record NicknameUpdateResponse( + @Schema(description = "업데이트된 사용자 닉네임", example = "UpdatedNickname") + String nickname +) { + public static NicknameUpdateResponse from(SiteUser siteUser) { + return new NicknameUpdateResponse( + siteUser.getNickname() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/ProfileImageUpdateResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/ProfileImageUpdateResponse.java new file mode 100644 index 000000000..127578e2f --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/dto/ProfileImageUpdateResponse.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.siteuser.dto; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import io.swagger.v3.oas.annotations.media.Schema; + +public record ProfileImageUpdateResponse( + @Schema(description = "업데이트된 프로필 이미지 URL", example = "http://example.com/updated-profile.jpg") + String profileImageUrl +) { + public static ProfileImageUpdateResponse from(SiteUser siteUser) { + return new ProfileImageUpdateResponse( + siteUser.getProfileImageUrl() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index e44af3a3e..71436ff87 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -1,34 +1,36 @@ package com.example.solidconnection.siteuser.service; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.dto.MyPageUpdateRequest; -import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.dto.*; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.ImgType; import com.example.solidconnection.university.domain.LikedUniversity; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; -import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.*; @RequiredArgsConstructor @Service public class SiteUserService { - public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 30; + public static final int MIN_DAYS_BETWEEN_NICKNAME_CHANGES = 7; public static final DateTimeFormatter NICKNAME_LAST_CHANGE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private final SiteUserRepository siteUserRepository; private final LikedUniversityRepository likedUniversityRepository; + private final S3Service s3Service; /* * 마이페이지 정보를 조회한다. @@ -49,31 +51,11 @@ public MyPageUpdateResponse getMyPageInfoToUpdate(String email) { return MyPageUpdateResponse.from(siteUser); } - /* - * 마이페이지 정보를 수정한다. - * - 닉네임 중복을 검증한다. - * - '닉네임 변경 최소 기간'이 지나지 않았는데 변경하려 하는지 검증한다. - * */ - @Transactional - public MyPageUpdateResponse update(String email, MyPageUpdateRequest pageUpdateRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - - validateNicknameDuplicated(pageUpdateRequest.nickname()); - validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); - - siteUser.setNickname(pageUpdateRequest.nickname()); - siteUser.setProfileImageUrl(pageUpdateRequest.profileImageUrl()); - siteUser.setNicknameModifiedAt(LocalDateTime.now()); - siteUserRepository.save(siteUser); - return MyPageUpdateResponse.from(siteUser); - } - private void validateNicknameDuplicated(String nickname) { if (siteUserRepository.existsByNickname(nickname)) { throw new CustomException(NICKNAME_ALREADY_EXISTED); } } - private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { if (lastModifiedAt == null) { return; @@ -96,4 +78,44 @@ public List getWishUniversity(String emai .map(likedUniversity -> UniversityInfoForApplyPreviewResponse.from(likedUniversity.getUniversityInfoForApply())) .toList(); } + + + /* + * 프로필 이미지를 수정한다. + * */ + @Transactional + public ProfileImageUpdateResponse updateProfileImage(String email, MultipartFile imageFile) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + validateProfileImage(imageFile); + + s3Service.deleteExProfile(email); + UploadedFileUrlResponse uploadedFileUrlResponse = s3Service.uploadFile(imageFile, ImgType.PROFILE); + siteUser.setProfileImageUrl(uploadedFileUrlResponse.fileUrl()); + siteUserRepository.save(siteUser); + + return ProfileImageUpdateResponse.from(siteUser); + } + + private void validateProfileImage(MultipartFile imageFile) { + if (imageFile == null || imageFile.isEmpty()) { + throw new CustomException(PROFILE_IMAGE_NEEDED); + } + } + + /* + * 닉네임을 수정한다. + * */ + @Transactional + public NicknameUpdateResponse updateNickname(String email, NicknameUpdateRequest nicknameUpdateRequest) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + + validateNicknameDuplicated(nicknameUpdateRequest.nickname()); + validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); + + siteUser.setNickname(nicknameUpdateRequest.nickname()); + siteUser.setNicknameModifiedAt(LocalDateTime.now()); + siteUserRepository.save(siteUser); + + return NicknameUpdateResponse.from(siteUser); + } } diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java index fa515e4f2..cb058fe3a 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -4,8 +4,9 @@ import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.MyPageUpdateRequest; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; +import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; @@ -68,27 +69,26 @@ public void setUpUserAndToken() { } @Test - void 마이_페이지_정보를_수정한다() { + void 닉네임을_수정한다() { // request - body 생성 및 요청 - MyPageUpdateRequest myPageUpdateRequest = new MyPageUpdateRequest("newNickname", "newProfileImageUrl"); - MyPageUpdateResponse myPageUpdateResponse = RestAssured.given() + NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); + NicknameUpdateResponse nicknameUpdateResponse = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .body(myPageUpdateRequest) + .body(nicknameUpdateRequest) .contentType("application/json") - .patch("/my-page/update") + .patch("/my-page/update/nickname") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(MyPageUpdateResponse.class); + .extract().as(NicknameUpdateResponse.class); SiteUser savedSiteUser = siteUserRepository.getByEmail(email); assertAll("마이 페이지 정보가 수정된다.", - () -> assertThat(myPageUpdateResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), - () -> assertThat(myPageUpdateResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl())); + () -> assertThat(nicknameUpdateResponse.nickname()).isEqualTo(savedSiteUser.getNickname())); } @Test - void 마이_페이지_정보를_수정할_때_닉네임이_중복된다면_예외_응답을_반환한다() { + void 닉네임을_수정할_때_닉네임이_중복된다면_예외_응답을_반환한다() { // setUp - 같은 닉네임을 갖는 다른 회원 정보 저장 SiteUser existUser = createSiteUserByEmail("existUser"); String duplicateNickname = "duplicateNickname"; @@ -96,13 +96,13 @@ public void setUpUserAndToken() { siteUserRepository.save(existUser); // request - body 생성 및 요청 - MyPageUpdateRequest myPageUpdateRequest = new MyPageUpdateRequest(duplicateNickname, "newProfileImageUrl"); + NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("duplicateNickname"); ErrorResponse response = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .body(myPageUpdateRequest) + .body(nicknameUpdateRequest) .contentType("application/json") - .patch("/my-page/update") + .patch("/my-page/update/nickname") .then().log().all() .statusCode(HttpStatus.CONFLICT.value()) .extract().as(ErrorResponse.class); @@ -112,7 +112,7 @@ public void setUpUserAndToken() { } @Test - void 마이_페이지_정보를_수정할_때_닉네임_변경_가능_기한이_지나지않았다면_예외_응답을_반환한다() { + void 닉네임을_수정할_때_닉네임_변경_가능_기한이_지나지않았다면_예외_응답을_반환한다() { // setUp - 회원 정보 저장 (닉네임 변경 가능 시간이 되기 1분 전) LocalDateTime nicknameModifiedAt = LocalDateTime.now() .minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES) @@ -121,13 +121,13 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // request - body 생성 및 요청 - MyPageUpdateRequest myPageUpdateRequest = new MyPageUpdateRequest("newNickname", "newProfileImageUrl"); + NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); ErrorResponse response = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .body(myPageUpdateRequest) + .body(nicknameUpdateRequest) .contentType("application/json") - .patch("/my-page/update") + .patch("/my-page/update/nickname") .then().log().all() .statusCode(HttpStatus.BAD_REQUEST.value()) .extract().as(ErrorResponse.class); diff --git a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java new file mode 100644 index 000000000..f6a330348 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java @@ -0,0 +1,162 @@ +package com.example.solidconnection.unit.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; +import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; +import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.siteuser.service.SiteUserService; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; + +import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.siteuser.service.SiteUserService.NICKNAME_LAST_CHANGE_DATE_FORMAT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("유저 서비스 테스트") +public class SiteUserServiceTest { + @InjectMocks + SiteUserService siteUserService; + @Mock + SiteUserRepository siteUserRepository; + @Mock + LikedUniversityRepository likedUniversityRepository; + @Mock + S3Service s3Service; + + private SiteUser siteUser; + private MultipartFile imageFile; + private UploadedFileUrlResponse uploadedFileUrlResponse; + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + imageFile = createMockImageFile(); + uploadedFileUrlResponse = createUploadedFileUrlResponse(); + + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "http://example.com/profile.jpg", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private MultipartFile createMockImageFile() { + return new MockMultipartFile("file1", "test1.png", + "image/png", "test image content 1".getBytes()); + + } + + private UploadedFileUrlResponse createUploadedFileUrlResponse() { + return new UploadedFileUrlResponse("https://s3.example.com/test1.png"); + } + + @Test + void 프로필_이미지를_수정한다() { + // Given + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); + + // When + ProfileImageUpdateResponse profileImageUpdateResponse = + siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); + // Then + assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(s3Service, times(1)).deleteExProfile(siteUser.getEmail()); + verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); + verify(siteUserRepository, times(1)).save(any(SiteUser.class)); + } + + @Test + void 프로필_이미지를_수정할_때_이미지가_없다면_예외_응답을_반환한다() { + // Given + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + siteUserService.updateProfileImage(siteUser.getEmail(), null)); + assertThat(exception.getMessage()) + .isEqualTo(PROFILE_IMAGE_NEEDED.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(PROFILE_IMAGE_NEEDED.getCode()); + } + + @Test + void 닉네임을_수정한다() { + // Given + NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + + // When + NicknameUpdateResponse nicknameUpdateResponse + = siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest); + // Then + assertEquals( nicknameUpdateResponse, NicknameUpdateResponse.from(siteUser)); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(siteUserRepository, times(1)).save(any(SiteUser.class)); + } + + @Test + void 닉네임을_수정할_때_중복된_닉네임이라면_예외_응답을_반환한다() { + // Given + NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(siteUserRepository.existsByNickname(nicknameUpdateRequest.nickname())).thenReturn(true); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest)); + assertThat(exception.getMessage()) + .isEqualTo(NICKNAME_ALREADY_EXISTED.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(NICKNAME_ALREADY_EXISTED.getCode()); + } + + @Test + void 닉네임을_수정할_때_변경_가능_기한이_지나지_않았다면_예외_응답을_반환한다() { + // Given + NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); + siteUser.setNicknameModifiedAt(LocalDateTime.now()); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + + // When & Then + CustomException exception = assertThrows(CustomException.class, () -> + siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest)); + + String formatLastModifiedAt + = String.format("(마지막 수정 시간 : %s)", NICKNAME_LAST_CHANGE_DATE_FORMAT.format(siteUser.getNicknameModifiedAt())); + CustomException expectedException = new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); + assertThat(exception.getMessage()).isEqualTo(expectedException.getMessage()); + assertThat(exception.getCode()).isEqualTo(expectedException.getCode()); + } +} From 0b800b963654a24cbdd0c4d492f23ecc1d9691b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:17:34 +0900 Subject: [PATCH 094/158] =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=82=AC=EC=A7=84=20=EC=88=98=EC=A0=95=EC=8B=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 초기 프로필 이미지를 수정의 경우 파일 삭제하지 않도록 수정 * test: 초기 프로필 이미지 수정 테스트코드 추가 * fix: 초기 프로필 이미지 경로가 null이 될 수 있음을 고려하여 수정 * test: 초기 프로필 이미지 경로가 null일때 테스트코드 추가 --- .../siteuser/service/SiteUserService.java | 10 ++++- .../unit/service/SiteUserServiceTest.java | 37 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index 71436ff87..0ab6aa0d2 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -88,7 +88,10 @@ public ProfileImageUpdateResponse updateProfileImage(String email, MultipartFile SiteUser siteUser = siteUserRepository.getByEmail(email); validateProfileImage(imageFile); - s3Service.deleteExProfile(email); + // 프로필 이미지를 처음 수정하는 경우에는 deleteExProfile 수행하지 않음 + if(!isDefaultProfileImage(siteUser.getProfileImageUrl())){ + s3Service.deleteExProfile(email); + } UploadedFileUrlResponse uploadedFileUrlResponse = s3Service.uploadFile(imageFile, ImgType.PROFILE); siteUser.setProfileImageUrl(uploadedFileUrlResponse.fileUrl()); siteUserRepository.save(siteUser); @@ -102,6 +105,11 @@ private void validateProfileImage(MultipartFile imageFile) { } } + private boolean isDefaultProfileImage(String profileImageUrl) { + String prefix = "https://solid-connection-uploaded.s3.ap-northeast-2.amazonaws.com/profile/"; + return profileImageUrl == null || !profileImageUrl.startsWith(prefix); + } + /* * 닉네임을 수정한다. * */ diff --git a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java index f6a330348..7cfeb94c6 100644 --- a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java @@ -49,6 +49,7 @@ public class SiteUserServiceTest { private SiteUser siteUser; private MultipartFile imageFile; private UploadedFileUrlResponse uploadedFileUrlResponse; + private final String defaultProfileImageUrl = "http://k.kakaocdn.net/dn/o2c5A/btsASaNh2Lr/Xum5kRyuErD8LIuLQEWfC0/img_640x640.jpg"; @BeforeEach void setUp() { @@ -62,7 +63,7 @@ private SiteUser createSiteUser() { return new SiteUser( "test@example.com", "nickname", - "http://example.com/profile.jpg", + "https://solid-connection-uploaded.s3.ap-northeast-2.amazonaws.com/profile/abcd", "1999-01-01", PreparationStatus.CONSIDERING, Role.MENTEE, @@ -80,6 +81,40 @@ private UploadedFileUrlResponse createUploadedFileUrlResponse() { return new UploadedFileUrlResponse("https://s3.example.com/test1.png"); } + @Test + void 초기_프로필_이미지를_수정한다_kakao() { + siteUser.setProfileImageUrl(defaultProfileImageUrl); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); + + // When + ProfileImageUpdateResponse profileImageUpdateResponse = + siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); + // Then + assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(s3Service, times(0)).deleteExProfile(siteUser.getEmail()); + verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); + verify(siteUserRepository, times(1)).save(any(SiteUser.class)); + } + + @Test + void 초기_프로필_이미지를_수정한다_null() { + siteUser.setProfileImageUrl(null); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); + + // When + ProfileImageUpdateResponse profileImageUpdateResponse = + siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); + // Then + assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(s3Service, times(0)).deleteExProfile(siteUser.getEmail()); + verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); + verify(siteUserRepository, times(1)).save(any(SiteUser.class)); + } + @Test void 프로필_이미지를_수정한다() { // Given From 1071f6f32f691642b3fb61e547d4def7d3b6e570 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:36:08 +0900 Subject: [PATCH 095/158] =?UTF-8?q?chore:=20nginx=20=EC=B5=9C=EB=8C=80=20b?= =?UTF-8?q?ody=20size=20=EC=A1=B0=EC=A0=95=20->=2010mb=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: nginx 최대 body size 조정 -> 10mb * remove: 쿼리 기록 파일 삭제 --- nginx.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/nginx.conf b/nginx.conf index 0be40fd00..e94acb4e3 100644 --- a/nginx.conf +++ b/nginx.conf @@ -20,6 +20,7 @@ server { ssl_certificate /etc/letsencrypt/live/api.solid-connect.net/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.solid-connect.net/privkey.pem; + client_max_body_size 10M; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; # 클라이언트 보다 서버의 암호화 알고리즘을 우선하도록 설정 From e3e13b29da403aee8e4b94be6dce0d960babdcc7 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Tue, 27 Aug 2024 23:45:38 +0900 Subject: [PATCH 096/158] =?UTF-8?q?docs:=20Bug=20report=20=EA=B9=83?= =?UTF-8?q?=ED=97=99=20=EC=9D=B4=EC=8A=88=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..2b503786a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +## 어떤 버그인가요 + +> 문제가 되는 부분에 대해 설명해주세요 + +## 재현 방법(선택) +버그를 재현할 수 있는 과정을 설명해주세요(필요하다면 사진을 첨부해주세요) +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## 참고할만한 자료(선택) From 26057de4bfbbddaec6469c1ddf31c92f26691bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:30:52 +0900 Subject: [PATCH 097/158] =?UTF-8?q?=EC=A7=80=EC=9B=90=EC=84=9C=202,=203?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EC=A7=80=20=EB=B9=84=ED=95=84=EC=88=98=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(#81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 지원서의 2,3 선택지 필수적으로 선택하지 않아도 되도록 수정 * test: 지원서 입력값 유효성 검증 테스트 추가 --- .../service/ApplicationSubmissionService.java | 33 +++++++----- .../unit/service/ApplicationServiceTest.java | 51 ++++++++++++++++++- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index 753bc89cc..0f22be85f 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -68,7 +68,7 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) { * */ @Transactional public boolean submitUniversityChoice(String email, UniversityChoiceRequest universityChoiceRequest) { - validateNoDuplicateUniversityChoices(universityChoiceRequest); + validateUniversityChoices(universityChoiceRequest); // 성적 제출한 적이 한번도 없는 경우 Application existingApplication = applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(email) @@ -83,14 +83,16 @@ public boolean submitUniversityChoice(String email, UniversityChoiceRequest univ }) .orElse(existingApplication); // 금학기에 이미 성적 제출한 경우 기존 객체 사용 + validateUpdateLimitNotExceed(application); + UniversityInfoForApply firstChoiceUniversity = universityInfoForApplyRepository .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term); - UniversityInfoForApply secondChoiceUniversity = universityInfoForApplyRepository - .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.secondChoiceUniversityId(), term); - UniversityInfoForApply thirdChoiceUniversity = universityInfoForApplyRepository - .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.thirdChoiceUniversityId(), term); - - validateUpdateLimitNotExceed(application); + UniversityInfoForApply secondChoiceUniversity = Optional.ofNullable(universityChoiceRequest.secondChoiceUniversityId()) + .map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term)) + .orElse(null); + UniversityInfoForApply thirdChoiceUniversity = Optional.ofNullable(universityChoiceRequest.thirdChoiceUniversityId()) + .map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term)) + .orElse(null); application.updateUniversityChoice(firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); return true; } @@ -109,14 +111,21 @@ private void validateUpdateLimitNotExceed(Application application) { } } - private void validateNoDuplicateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) { + // 입력값 유효성 검증 + private void validateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) { Set uniqueUniversityIds = new HashSet<>(); - uniqueUniversityIds.add(universityChoiceRequest.firstChoiceUniversityId()); - uniqueUniversityIds.add(universityChoiceRequest.secondChoiceUniversityId()); - uniqueUniversityIds.add(universityChoiceRequest.thirdChoiceUniversityId()); + if (universityChoiceRequest.secondChoiceUniversityId() != null) { + addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.secondChoiceUniversityId()); + } + if (universityChoiceRequest.thirdChoiceUniversityId() != null) { + addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.thirdChoiceUniversityId()); + } + } - if (uniqueUniversityIds.size() < 3) { + private void addUniversityChoice(Set uniqueUniversityIds, Long universityId) { + boolean notAdded = !uniqueUniversityIds.add(universityId); + if (notAdded) { throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); } } diff --git a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java index d967c90bf..7688478fc 100644 --- a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java @@ -16,6 +16,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -23,6 +25,7 @@ import java.util.Optional; +import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; import static com.example.solidconnection.custom.exception.ErrorCode.SCORE_SHOULD_SUBMITTED_FIRST; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -116,7 +119,9 @@ private Application createApplication(String term) { verify(applicationRepository, times(0)).save(any(Application.class)); } - // 예외테스트 + /** + * 지망대학 제출 + */ @Test void 지망대학_제출할_때_성적_제출이력이_없다면_예외_응답을_반환한다() { // given @@ -171,4 +176,48 @@ private Application createApplication(String term) { verify(siteUserRepository, times(0)).getByEmail(siteUser.getEmail()); verify(applicationRepository, times(0)).save(any(Application.class)); } + + @ParameterizedTest + @CsvSource({ + "1, 2, 3", + "1, , 3", + "1, 2, ", + "1, , " + }) + void 지망대학_제출할_때_2지망과_3지망은_NULL_허용한다(Long firstChoice, Long secondChoice, Long thirdChoice) { + // Given + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest(firstChoice, secondChoice, thirdChoice); + when(applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail())) + .thenReturn(Optional.of(application)); + + // When + applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + + // Then + verify(applicationRepository, times(1)).findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail()); + verify(siteUserRepository, times(0)).getByEmail(siteUser.getEmail()); + verify(applicationRepository, times(0)).save(any(Application.class)); + } + + @ParameterizedTest + @CsvSource({ + "1, 1, 1", + "1, 2, 1", + "1, 1, 2", + "1, , 1", + "1, 1, " + }) + void 지망대학_제출할_때_선택지가_중복된다면_예외_응답을_반환한다(Long firstChoice, Long secondChoice, Long thirdChoice) { + // given + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest(firstChoice, secondChoice, thirdChoice); + + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + }); + assertThat(exception.getMessage()) + .isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getCode()); + } } From e25f011a6d08122281572e4fabb8984a4f7bd038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Wed, 4 Sep 2024 21:32:11 +0900 Subject: [PATCH 098/158] =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=EB=8F=84=EC=9E=85?= =?UTF-8?q?=20(#83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Redis 및 AOP 활용한 커스텀 캐싱 기능 개발 * feat: Thundering Herd 문제를 해결하는 캐싱 기능 개발 * feat: Redis pub/sub 기능 도입 * refactor: 캐싱 도입에 따라서 유효성 검증 로직 분리 * refactor: 캐싱된 list 데이터 역직렬화 문제를 해결하기 위해 Wrapping DTO 생성 * feat: 지원자 조회 기능에 캐싱 적용 * feat: 기존 지원자 성적 수정 또는 새로운 지원자 등록 시 캐시아웃 적용 * feat: 공통 추천학교 조회 기능에 캐싱 적용 * feat: 대학정보 조회 기능에 캐싱 적용 * test: Thundering Herd 테스트 코드 추가 --- .../SolidConnectionApplication.java | 2 + .../controller/ApplicationController.java | 1 + .../service/ApplicationQueryService.java | 9 +- .../service/ApplicationSubmissionService.java | 3 + .../cache/CacheUpdateListener.java | 22 +++ .../solidconnection/cache/CachingAspect.java | 56 +++++++ .../cache/CompletableFutureManager.java | 23 +++ .../cache/ThunderingHerdCachingAspect.java | 150 ++++++++++++++++++ .../cache/annotation/DefaultCacheOut.java | 14 ++ .../cache/annotation/DefaultCaching.java | 14 ++ .../annotation/ThunderingHerdCaching.java | 14 ++ .../cache/manager/CacheManager.java | 8 + .../cache/manager/CustomCacheManager.java | 41 +++++ .../config/redis/RedisConfig.java | 24 +++ .../solidconnection/type/RedisConstants.java | 9 +- .../controller/UniversityController.java | 3 +- ...niversityInfoForApplyPreviewResponses.java | 8 + .../service/UniversityRecommendService.java | 2 + .../university/service/UniversityService.java | 15 +- .../solidconnection/util/RedisUtils.java | 27 +++- .../concurrency/ThunderingHerdTest.java | 90 +++++++++++ 21 files changed, 521 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java create mode 100644 src/main/java/com/example/solidconnection/cache/CachingAspect.java create mode 100644 src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java create mode 100644 src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java create mode 100644 src/main/java/com/example/solidconnection/cache/annotation/DefaultCacheOut.java create mode 100644 src/main/java/com/example/solidconnection/cache/annotation/DefaultCaching.java create mode 100644 src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java create mode 100644 src/main/java/com/example/solidconnection/cache/manager/CacheManager.java create mode 100644 src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java create mode 100644 src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponses.java create mode 100644 src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java diff --git a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java index 6727bdece..670a3f0f7 100644 --- a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java +++ b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java @@ -2,11 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling @EnableJpaAuditing +@EnableCaching @SpringBootApplication public class SolidConnectionApplication { diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 3234e45b4..baf5159a1 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -55,6 +55,7 @@ public ResponseEntity getApplicants( Principal principal, @RequestParam(required = false, defaultValue = "") String region, @RequestParam(required = false, defaultValue = "") String keyword) { + applicationQueryService.validateSiteUserCanViewApplicants(principal.getName()); ApplicationsResponse result = applicationQueryService.getApplicants(principal.getName(), region, keyword); return ResponseEntity .ok(result); diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 2e481023d..aee3ad25e 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -5,6 +5,7 @@ import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -43,10 +44,10 @@ public class ApplicationQueryService { * - 1지망, 2지망 지원자들을 조회한다. * */ @Transactional(readOnly = true) + @ThunderingHerdCaching(key = "application:query:{1}:{2}", cacheManager = "customCacheManager", ttlSec = 86400) public ApplicationsResponse getApplicants(String email, String regionCode, String keyword) { // 유저가 다른 지원자들을 볼 수 있는지 검증 SiteUser siteUser = siteUserRepository.getByEmail(email); - validateSiteUserCanViewApplicants(siteUser); // 국가와 키워드와 지역을 통해 대학을 필터링한다. List universities @@ -61,8 +62,10 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin // 학기별로 상태가 관리된다. // 금학기에 지원이력이 있는 사용자만 지원정보를 확인할 수 있도록 한다. - private void validateSiteUserCanViewApplicants(SiteUser siteUser) { - VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term).getVerifyStatus(); + @Transactional(readOnly = true) + public void validateSiteUserCanViewApplicants(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term).getVerifyStatus(); if (verifyStatus != VerifyStatus.APPROVED) { throw new CustomException(APPLICATION_NOT_APPROVED); } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index 0f22be85f..b23b876c7 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -6,6 +6,7 @@ import com.example.solidconnection.application.dto.ScoreRequest; import com.example.solidconnection.application.dto.UniversityChoiceRequest; import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.cache.annotation.DefaultCacheOut; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -40,6 +41,7 @@ public class ApplicationSubmissionService { * - 수정을 하고 나면, 성적 승인 상태(verifyStatus)를 PENDING 상태로 변경한다. * */ @Transactional + @DefaultCacheOut(key = "application:query", cacheManager = "customCacheManager", prefix = true) public boolean submitScore(String email, ScoreRequest scoreRequest) { SiteUser siteUser = siteUserRepository.getByEmail(email); Gpa gpa = scoreRequest.toGpa(); @@ -67,6 +69,7 @@ public boolean submitScore(String email, ScoreRequest scoreRequest) { * - 성적 승인 상태(verifyStatus) 는 변경하지 않는다. * */ @Transactional + @DefaultCacheOut(key = "application:query", cacheManager = "customCacheManager", prefix = true) public boolean submitUniversityChoice(String email, UniversityChoiceRequest universityChoiceRequest) { validateUniversityChoices(universityChoiceRequest); diff --git a/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java b/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java new file mode 100644 index 000000000..34e2752b3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.cache; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CacheUpdateListener implements MessageListener { + + private final CompletableFutureManager futureManager; + @Override + public void onMessage(Message message, byte[] pattern) { + String messageBody = new String(message.getBody(), StandardCharsets.UTF_8).replaceAll("^\"|\"$", ""); + futureManager.completeFuture(messageBody); + } +} diff --git a/src/main/java/com/example/solidconnection/cache/CachingAspect.java b/src/main/java/com/example/solidconnection/cache/CachingAspect.java new file mode 100644 index 000000000..29c355372 --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/CachingAspect.java @@ -0,0 +1,56 @@ +package com.example.solidconnection.cache; + +import com.example.solidconnection.cache.annotation.DefaultCacheOut; +import com.example.solidconnection.cache.annotation.DefaultCaching; +import com.example.solidconnection.cache.manager.CacheManager; +import com.example.solidconnection.util.RedisUtils; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@RequiredArgsConstructor +public class CachingAspect { + private final ApplicationContext applicationContext; + private final RedisUtils redisUtils; + + @Around("@annotation(defaultCaching)") + public Object cache(ProceedingJoinPoint joinPoint, DefaultCaching defaultCaching) throws Throwable { + + CacheManager cacheManager = (CacheManager) applicationContext.getBean(defaultCaching.cacheManager()); + String key = redisUtils.generateCacheKey(defaultCaching.key(), joinPoint.getArgs()); + Long ttl = defaultCaching.ttlSec(); + + // 1. 캐시에 있으면 반환 + Object cachedValue = cacheManager.get(key); + if (cachedValue != null) { + return cachedValue; + } + // 2. 캐시에 없으면 캐싱 후 반환 + Object result = joinPoint.proceed(); + cacheManager.put(key, result, ttl); + return result; + } + + @Around("@annotation(defaultCacheOut)") + public Object cacheEvict(ProceedingJoinPoint joinPoint, DefaultCacheOut defaultCacheOut) throws Throwable { + + CacheManager cacheManager = (CacheManager) applicationContext.getBean(defaultCacheOut.cacheManager()); + + for (String key : defaultCacheOut.key()) { + String cacheKey = redisUtils.generateCacheKey(key, joinPoint.getArgs()); + boolean usingPrefix = defaultCacheOut.prefix(); + + if (usingPrefix) { + cacheManager.evictUsingPrefix(cacheKey); + }else{ + cacheManager.evict(cacheKey); + } + } + return joinPoint.proceed(); + } +} diff --git a/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java b/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java new file mode 100644 index 000000000..6bcf01e03 --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java @@ -0,0 +1,23 @@ +package com.example.solidconnection.cache; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +@Component +public class CompletableFutureManager { + private final Map> waitingRequests = new ConcurrentHashMap<>(); + + public CompletableFuture getOrCreateFuture(String key) { + return waitingRequests.computeIfAbsent(key, k -> new CompletableFuture<>()); + } + + public void completeFuture(String key) { + CompletableFuture future = waitingRequests.remove(key); + if (future != null) { + future.complete(null); + } + } +} diff --git a/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java b/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java new file mode 100644 index 000000000..8dc1694db --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java @@ -0,0 +1,150 @@ +package com.example.solidconnection.cache; + +import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; +import com.example.solidconnection.cache.manager.CacheManager; +import com.example.solidconnection.util.RedisUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.*; + +import static com.example.solidconnection.type.RedisConstants.*; + +@Aspect +@Component +@Slf4j +public class ThunderingHerdCachingAspect { + private final ApplicationContext applicationContext; + private final RedisTemplate redisTemplate; + private final CompletableFutureManager futureManager; + private final RedisUtils redisUtils; + + @Autowired + public ThunderingHerdCachingAspect(ApplicationContext applicationContext, RedisTemplate redisTemplate, + CompletableFutureManager futureManager, RedisUtils redisUtils) { + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + this.redisTemplate = redisTemplate; + this.applicationContext = applicationContext; + this.futureManager = futureManager; + this.redisUtils = redisUtils; + } + + @Around("@annotation(thunderingHerdCaching)") + public Object cache(ProceedingJoinPoint joinPoint, ThunderingHerdCaching thunderingHerdCaching) { + + CacheManager cacheManager = (CacheManager) applicationContext.getBean(thunderingHerdCaching.cacheManager()); + String key = redisUtils.generateCacheKey(thunderingHerdCaching.key(), joinPoint.getArgs()); + Long ttl = thunderingHerdCaching.ttlSec(); + + Object cachedValue = cacheManager.get(key); + if (cachedValue == null) { + log.info("Cache miss. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return createCache(joinPoint, cacheManager, ttl, key); + } + + if (redisUtils.isCacheExpiringSoon(key, ttl, Double.valueOf(REFRESH_LIMIT_PERCENT.getValue()))) { + log.info("Cache hit, but TTL is expiring soon. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return refreshCache(cachedValue, ttl, key); + } + + log.info("Cache hit. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return cachedValue; + } + + private Object createCache(ProceedingJoinPoint joinPoint, CacheManager cacheManager, Long ttl, String key) { + return executeWithLock( + redisUtils.getCreateLockKey(key), + () -> { + log.info("생성락 흭득하였습니다. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + Object result = proceedJoinPoint(joinPoint); + cacheManager.put(key, result, ttl); + redisTemplate.convertAndSend(CREATE_CHANNEL.getValue(), key); + log.info("캐시 생성 후 채널에 pub 진행합니다. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return result; + }, + () -> { + log.info("생성락 흭득에 실패하여 대기하러 갑니다. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return waitForCacheToUpdate(joinPoint, key); + } + ); + } + + private Object refreshCache(Object cachedValue, Long ttl, String key) { + return executeWithLock( + redisUtils.getRefreshLockKey(key), + () -> { + log.info("갱신락 흭득하였습니다. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + redisTemplate.opsForValue().getAndExpire(key, Duration.ofSeconds(ttl)); + log.info("TTL 갱신을 마쳤습니다. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return cachedValue; + }, + () -> { + log.info("갱신락 흭득에 실패하였습니다. 캐시의 값을 바로 반환합니다. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return cachedValue; + } + ); + } + + private Object executeWithLock(String lockKey, Callable onLockAcquired, Callable onLockFailed) { + String lockValue = UUID.randomUUID().toString(); + boolean lockAcquired = false; + + try { + lockAcquired = tryAcquireLock(lockKey, lockValue); + if (lockAcquired) { + return onLockAcquired.call(); + } else { + return onLockFailed.call(); + } + } catch (Exception e) { + throw new RuntimeException("Error during executeWithLock", e); + } finally { + releaseLock(lockKey, lockValue, lockAcquired); + } + } + + private boolean tryAcquireLock(String lockKey, String lockValue) { + return redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofMillis(Long.parseLong(LOCK_TIMEOUT_MS.getValue()))); + } + + private void releaseLock(String lockKey, String lockValue, boolean lockAcquired) { + if (lockAcquired && lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { + redisTemplate.delete(lockKey); + log.info("락 반환합니다. Key: {}", lockKey); + } + } + + private Object waitForCacheToUpdate(ProceedingJoinPoint joinPoint, String key) { + CompletableFuture future = futureManager.getOrCreateFuture(key); + try { + future.get(Long.parseLong(MAX_WAIT_TIME_MS.getValue()), TimeUnit.MILLISECONDS); + log.info("대기에서 빠져나와 생성된 캐시값을 가져옵니다. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return redisTemplate.opsForValue().get(key); + } catch (TimeoutException e) { + log.warn("대기중 타임아웃 발생하여 DB 접근하여 반환합니다. Key: {}, Thread: {}", key, Thread.currentThread().getName()); + return proceedJoinPoint(joinPoint); + } catch (InterruptedException | ExecutionException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Error during waitForCacheToUpdate", e); + } + } + + private Object proceedJoinPoint(ProceedingJoinPoint joinPoint) { + try { + return joinPoint.proceed(); + } catch (Throwable e) { + throw new RuntimeException("Error during proceedJoinPoint", e); + } + } +} diff --git a/src/main/java/com/example/solidconnection/cache/annotation/DefaultCacheOut.java b/src/main/java/com/example/solidconnection/cache/annotation/DefaultCacheOut.java new file mode 100644 index 000000000..bb1d5b518 --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/annotation/DefaultCacheOut.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.cache.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DefaultCacheOut { + String[] key(); + String cacheManager(); + boolean prefix() default false; +} diff --git a/src/main/java/com/example/solidconnection/cache/annotation/DefaultCaching.java b/src/main/java/com/example/solidconnection/cache/annotation/DefaultCaching.java new file mode 100644 index 000000000..36c45a616 --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/annotation/DefaultCaching.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.cache.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DefaultCaching { + String key(); + String cacheManager(); + long ttlSec(); +} diff --git a/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java b/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java new file mode 100644 index 000000000..6772a52e7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.cache.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ThunderingHerdCaching { + String key(); + String cacheManager(); + long ttlSec(); +} diff --git a/src/main/java/com/example/solidconnection/cache/manager/CacheManager.java b/src/main/java/com/example/solidconnection/cache/manager/CacheManager.java new file mode 100644 index 000000000..8c46324e1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/manager/CacheManager.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.cache.manager; + +public interface CacheManager { + void put(String key, Object value, Long ttl); + Object get(String key); + void evict(String key); + void evictUsingPrefix(String key); +} diff --git a/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java b/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java new file mode 100644 index 000000000..833ed00f7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java @@ -0,0 +1,41 @@ +package com.example.solidconnection.cache.manager; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.Set; + +@Component("customCacheManager") +public class CustomCacheManager implements CacheManager { + private final RedisTemplate redisTemplate; + + @Autowired + public CustomCacheManager(RedisTemplate redisTemplate) { + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + this.redisTemplate = redisTemplate; + } + + public void put(String key, Object object, Long ttl) { + redisTemplate.opsForValue().set(key, object, Duration.ofSeconds(ttl)); + } + + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + public void evict(String key) { + redisTemplate.delete(key); + } + + public void evictUsingPrefix(String key) { + Set keys = redisTemplate.keys(key+"*"); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + } + } +} diff --git a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java index 1aa671dcf..282c36e8c 100644 --- a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java +++ b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.redis; +import com.example.solidconnection.cache.CacheUpdateListener; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -9,9 +10,14 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import static com.example.solidconnection.type.RedisConstants.CREATE_CHANNEL; + @Configuration @EnableRedisRepositories public class RedisConfig { @@ -40,6 +46,24 @@ public RedisTemplate redisTemplate() { return redisTemplate; } + @Bean + public RedisTemplate objectRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } + + @Bean + RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, + CacheUpdateListener listener) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(listener, new PatternTopic(CREATE_CHANNEL.getValue())); + return container; + } + @Bean(name = "incrViewCountScript") public RedisScript incrViewCountLuaScript() { Resource scriptSource = new ClassPathResource("scripts/incrViewCount.lua"); diff --git a/src/main/java/com/example/solidconnection/type/RedisConstants.java b/src/main/java/com/example/solidconnection/type/RedisConstants.java index 22d7762b1..7d4c7f2c9 100644 --- a/src/main/java/com/example/solidconnection/type/RedisConstants.java +++ b/src/main/java/com/example/solidconnection/type/RedisConstants.java @@ -8,7 +8,14 @@ public enum RedisConstants { VALIDATE_VIEW_COUNT_TTL("1"), VALIDATE_VIEW_COUNT_KEY_PREFIX("validate:post:view:"), VIEW_COUNT_KEY_PREFIX("post:view:"), - VIEW_COUNT_KEY_PATTERN("post:view:*"); + VIEW_COUNT_KEY_PATTERN("post:view:*"), + + REFRESH_LIMIT_PERCENT("10.0"), + CREATE_LOCK_PREFIX("create_lock:"), + REFRESH_LOCK_PREFIX("refresh_lock:"), + LOCK_TIMEOUT_MS("10000"), + MAX_WAIT_TIME_MS("3000"), + CREATE_CHANNEL("create_channel"); private final String value; diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 3b19c683e..4d0609549 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -72,6 +72,7 @@ public ResponseEntity getUniversityDetails( return ResponseEntity.ok(universityDetailResponse); } + // todo return타입 UniversityInfoForApplyPreviewResponses로 추후 수정 필요 @GetMapping("/search") public ResponseEntity> searchUniversity( @RequestParam(required = false, defaultValue = "") String region, @@ -79,7 +80,7 @@ public ResponseEntity> searchUnivers @RequestParam(required = false, defaultValue = "") LanguageTestType testType, @RequestParam(required = false, defaultValue = "") String testScore) { List universityInfoForApplyPreviewResponse - = universityService.searchUniversity(region, keyword, testType, testScore); + = universityService.searchUniversity(region, keyword, testType, testScore).universityInfoForApplyPreviewResponses(); return ResponseEntity.ok(universityInfoForApplyPreviewResponse); } } diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponses.java b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponses.java new file mode 100644 index 000000000..3c8a00df4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponses.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.university.dto; + +import java.util.List; + +public record UniversityInfoForApplyPreviewResponses( + List universityInfoForApplyPreviewResponses +) { +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index 88a7a222f..cf9c112f8 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -1,5 +1,6 @@ package com.example.solidconnection.university.service; +import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.domain.UniversityInfoForApply; @@ -65,6 +66,7 @@ private List getGeneralRecommendsExcludingSelected(List< * 공통 추천 대학교를 불러온다. * */ @Transactional(readOnly = true) + @ThunderingHerdCaching(key = "university:recommend:general", cacheManager = "customCacheManager", ttlSec = 86400) public UniversityRecommendsResponse getGeneralRecommends() { List generalRecommends = new ArrayList<>(generalRecommendUniversities.getRecommendUniversities()); Collections.shuffle(generalRecommends); diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index 94e8bba83..c0cbe2c05 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -1,5 +1,6 @@ package com.example.solidconnection.university.service; +import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -7,10 +8,7 @@ import com.example.solidconnection.university.domain.LikedUniversity; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import com.example.solidconnection.university.dto.IsLikeResponse; -import com.example.solidconnection.university.dto.LikeResultResponse; -import com.example.solidconnection.university.dto.UniversityDetailResponse; -import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.*; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.custom.UniversityFilterRepositoryImpl; import lombok.RequiredArgsConstructor; @@ -41,6 +39,7 @@ public class UniversityService { * - 대학교(University) 정보와 대학 지원 정보(UniversityInfoForApply) 정보를 조합하여 반환한다. * */ @Transactional(readOnly = true) + @ThunderingHerdCaching(key = "university:{0}", cacheManager = "customCacheManager", ttlSec = 86400) public UniversityDetailResponse getUniversityDetail(Long universityInfoForApplyId) { UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); @@ -57,13 +56,15 @@ public UniversityDetailResponse getUniversityDetail(Long universityInfoForApplyI * - 언어 시험 점수는 합격 최소 점수보다 높은 것이 조건이다. * */ @Transactional(readOnly = true) - public List searchUniversity( + @ThunderingHerdCaching(key = "university:{0}:{1}:{2}:{3}", cacheManager = "customCacheManager", ttlSec = 86400) + public UniversityInfoForApplyPreviewResponses searchUniversity( String regionCode, List keywords, LanguageTestType testType, String testScore) { - return universityFilterRepository + + return new UniversityInfoForApplyPreviewResponses(universityFilterRepository .findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm(regionCode, keywords, testType, testScore, term) .stream() .map(UniversityInfoForApplyPreviewResponse::from) - .toList(); + .toList()); } /* diff --git a/src/main/java/com/example/solidconnection/util/RedisUtils.java b/src/main/java/com/example/solidconnection/util/RedisUtils.java index 7df79418e..ef91dfc3d 100644 --- a/src/main/java/com/example/solidconnection/util/RedisUtils.java +++ b/src/main/java/com/example/solidconnection/util/RedisUtils.java @@ -11,8 +11,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static com.example.solidconnection.type.RedisConstants.VALIDATE_VIEW_COUNT_KEY_PREFIX; -import static com.example.solidconnection.type.RedisConstants.VIEW_COUNT_KEY_PREFIX; +import static com.example.solidconnection.type.RedisConstants.*; @Component public class RedisUtils { @@ -49,4 +48,28 @@ public String getValidatePostViewCountRedisKey(String email, Long postId) { public Long getPostIdFromPostViewCountRedisKey(String key) { return Long.parseLong(key.substring(VIEW_COUNT_KEY_PREFIX.getValue().length())); } + + public String generateCacheKey(String keyPattern, Object[] args) { + for (int i = 0; i < args.length; i++) { + // 키 패턴에 {i}가 포함된 경우에만 해당 인덱스의 파라미터를 삽입 + if (keyPattern.contains("{" + i + "}")) { + String replacement = (args[i] != null) ? args[i].toString() : "null"; + keyPattern = keyPattern.replace("{" + i + "}", replacement); + } + } + return keyPattern; + } + + public String getCreateLockKey(String key) { + return CREATE_LOCK_PREFIX.getValue() + key; + } + + public String getRefreshLockKey(String key) { + return REFRESH_LOCK_PREFIX.getValue() + key; + } + + public boolean isCacheExpiringSoon(String key, Long defaultTtl, Double percent) { + Long leftTtl = redisTemplate.getExpire(key); + return defaultTtl != null && ((double) leftTtl /defaultTtl)*100 < percent; + } } diff --git a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java new file mode 100644 index 000000000..7ec6a511e --- /dev/null +++ b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java @@ -0,0 +1,90 @@ +package com.example.solidconnection.concurrency; + +import com.example.solidconnection.application.service.ApplicationQueryService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@SpringBootTest +@ActiveProfiles("test") +@DisplayName("ThunderingHerd 테스트") +public class ThunderingHerdTest { + @Autowired + private ApplicationQueryService applicationQueryService; + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private SiteUserRepository siteUserRepository; + private int THREAD_NUMS = 1000; + private int THREAD_POOL_SIZE = 200; + private int TIMEOUT_SECONDS = 10; + private SiteUser siteUser; + + @BeforeEach + public void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + @Test + public void ThunderingHerd_문제를_해결한다() throws InterruptedException { + redisTemplate.opsForValue().getAndDelete("application::"); + redisTemplate.opsForValue().getAndDelete("application:ASIA:"); + redisTemplate.opsForValue().getAndDelete("application::추오"); + + ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); + CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); + + for (int i = 0; i < THREAD_NUMS; i++) { + executorService.submit(() -> { + try { + List tasks = Arrays.asList( + () -> applicationQueryService.getApplicants(siteUser.getEmail(), "", ""), + () -> applicationQueryService.getApplicants(siteUser.getEmail(), "ASIA", ""), + () -> applicationQueryService.getApplicants(siteUser.getEmail(), "", "추오") + ); + Collections.shuffle(tasks); + tasks.forEach(Runnable::run); + } finally { + doneSignal.countDown(); + } + }); + } + + doneSignal.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + executorService.shutdown(); + boolean terminated = executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!terminated) { + System.err.println("ExecutorService did not terminate in the expected time."); + } + } +} From 75d02f0a3185616c1b000b8acd34592e2fa3aee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:50:45 +0900 Subject: [PATCH 099/158] =?UTF-8?q?chore:=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20redis-exporter=20=EC=B6=94=EA=B0=80=20(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 86e7e3a1e..e7358d2b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,16 @@ services: ports: - "6379:6379" + redis-exporter: + image: oliver006/redis_exporter + container_name: redis-exporter + ports: + - "9121:9121" + environment: + REDIS_ADDR: "redis:6379" + depends_on: + - redis + solid-connect-server: build: context: . From bebc855ae66dbc51b5c3a7dc2696b4c659e33508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:10:10 +0900 Subject: [PATCH 100/158] =?UTF-8?q?fix:=20=EC=A1=B0=ED=9A=8C=EC=88=98=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20=EC=8B=9C=20=EB=B3=80=EA=B2=BD=EA=B0=90?= =?UTF-8?q?=EC=A7=80=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B3=A0=20=EC=A7=81=EC=A0=91=20update=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/solidconnection/post/domain/Post.java | 5 ----- .../solidconnection/post/repository/PostRepository.java | 5 +++++ .../solidconnection/service/UpdateViewCountService.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/post/domain/Post.java index 287b255a8..203feb5a9 100644 --- a/src/main/java/com/example/solidconnection/post/domain/Post.java +++ b/src/main/java/com/example/solidconnection/post/domain/Post.java @@ -97,9 +97,4 @@ public void update(PostUpdateRequest postUpdateRequest) { this.content = postUpdateRequest.content(); this.category = PostCategory.valueOf(postUpdateRequest.postCategory()); } - - public void increaseViewCount(Long updateViewCount) { - this.viewCount += updateViewCount; - } - } diff --git a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java index f5c10875c..b819cc45a 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java +++ b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java @@ -38,4 +38,9 @@ default Post getById(Long id) { @Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 " + "WHERE p.id = :postId") void increaseLikeCount(@Param("postId") Long postId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Post p SET p.viewCount = p.viewCount + :count " + + "WHERE p.id = :postId") + void increaseViewCount(@Param("postId") Long postId, @Param("count") Long count); } diff --git a/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java b/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java index 46954fff6..55d4d9eba 100644 --- a/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java +++ b/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java @@ -26,7 +26,7 @@ public void updateViewCount(String key) { log.info("updateViewCount Processing key: {} in thread: {}", key, Thread.currentThread().getName()); Long postId = redisUtils.getPostIdFromPostViewCountRedisKey(key); Post post = postRepository.getById(postId); - post.increaseViewCount(redisService.getAndDelete(key)); + postRepository.increaseViewCount(postId, redisService.getAndDelete(key)); log.info("updateViewCount Updated post id: {} with view count from key: {}", postId, key); } } From c1cfe1a341c09791ec24097c37e8b6f40994f008 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Sat, 7 Sep 2024 19:55:30 +0900 Subject: [PATCH 101/158] =?UTF-8?q?feat:=20=EB=82=B4=EA=B0=80=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=ED=95=9C=20=EB=8C=80=ED=95=99=EC=9D=98=20=EC=84=B1?= =?UTF-8?q?=EC=A0=81=20=EC=A7=80=EC=9B=90=20=ED=98=84=ED=99=A9=20api=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 내가 지원한 대학의 지원서만 볼 수 있는 api endpoint 추가 * test: /application/competitors e2e 테스트 추가 --- .../controller/ApplicationController.java | 9 +++++ .../service/ApplicationQueryService.java | 19 ++++++++- .../e2e/ApplicantsQueryTest.java | 40 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index baf5159a1..d6695df28 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -61,6 +61,15 @@ public ResponseEntity getApplicants( .ok(result); } + @GetMapping("/competitors") + public ResponseEntity getApplicantsForUserCompetitors( + Principal principal) { + applicationQueryService.validateSiteUserCanViewApplicants(principal.getName()); + ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(principal.getName()); + return ResponseEntity + .ok(result); + } + @GetMapping("/status") public ResponseEntity getApplicationVerifyStatus(Principal principal) { VerifyStatusResponse result = verifyStatusQueryService.getVerifyStatus(principal.getName()); diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index aee3ad25e..fff928f05 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -46,7 +46,6 @@ public class ApplicationQueryService { @Transactional(readOnly = true) @ThunderingHerdCaching(key = "application:query:{1}:{2}", cacheManager = "customCacheManager", ttlSec = 86400) public ApplicationsResponse getApplicants(String email, String regionCode, String keyword) { - // 유저가 다른 지원자들을 볼 수 있는지 검증 SiteUser siteUser = siteUserRepository.getByEmail(email); // 국가와 키워드와 지역을 통해 대학을 필터링한다. @@ -60,6 +59,24 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants); } + @Transactional(readOnly = true) + public ApplicationsResponse getApplicantsByUserApplications(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + + Application userLatestApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term); + List userAppliedUniversities = List.of( + userLatestApplication.getFirstChoiceUniversity().getUniversity(), + userLatestApplication.getSecondChoiceUniversity().getUniversity(), + userLatestApplication.getThirdChoiceUniversity().getUniversity() + ); + + + List firstChoiceApplicants = getFirstChoiceApplicants(userAppliedUniversities, siteUser, term); + List secondChoiceApplicants = getSecondChoiceApplicants(userAppliedUniversities, siteUser, term); + List thirdChoiceApplicants = getThirdChoiceApplicants(userAppliedUniversities, siteUser, term); + return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants); + } + // 학기별로 상태가 관리된다. // 금학기에 지원이력이 있는 사용자만 지원정보를 확인할 수 있도록 한다. @Transactional(readOnly = true) diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index cb5fbfe52..7dd8b63f9 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -234,4 +234,44 @@ public void setUpUserAndToken() { List.of(ApplicantResponse.of(사용자4_이전학기_지원정보, false))) )); } + + @Test + void 내가_지원한_대학의_지원자를_조회한다() { + ApplicationsResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + accessToken) + .when().log().all() + .get("/application/competitors") + .then().log().all() + .statusCode(200) + .extract().as(ApplicationsResponse.class); + + List firstChoiceApplicants = response.firstChoice(); + List secondChoiceApplicants = response.secondChoice(); + List thirdChoiceApplicants = response.thirdChoice(); + + assertThat(firstChoiceApplicants).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(사용자1_지원정보, false))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true))), + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, List.of())); + assertThat(secondChoiceApplicants).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(사용자1_지원정보, false))), + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of())); + assertThat(thirdChoiceApplicants).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of()), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of()), + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of(ApplicantResponse.of(나의_지원정보, true)))); + + assertThat(firstChoiceApplicants.size()).isEqualTo(3); + assertThat(secondChoiceApplicants.size()).isEqualTo(3); + assertThat(thirdChoiceApplicants.size()).isEqualTo(3); + } } From 57cdb4ba4bc644724bdcdb59e328ac8991287328 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Sun, 8 Sep 2024 12:21:50 +0900 Subject: [PATCH 102/158] =?UTF-8?q?fix:=20application/competitor=20API=205?= =?UTF-8?q?00=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: application 테스트시 관리자 사용자 추가, 관리자가_선택한_대학의_지원자는_조회되지_않는다() 추가 * fix: application의 choiceUniversity가 null인 경우 고려 * test: /application/competitors 관련 테스트명 '경쟁자~'로 변경, 지원대학중 일부만 null일때 테스트 추가 테스트 사용자6 추가 경쟁자를_조회한다() 이름 변경 지원_대학중_미선택이_있을_떄_경쟁자를_조회한다() 추가 지원_대학이_모두_미선택일_때_경쟁자를_조회한다() 이름 변경 --- .../service/ApplicationQueryService.java | 22 ++++-- .../e2e/ApplicantsQueryTest.java | 76 +++++++++++++++++-- 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index fff928f05..66ae84918 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -19,9 +19,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Function; +import java.util.stream.Collectors; import static com.example.solidconnection.custom.exception.ErrorCode.APPLICATION_NOT_APPROVED; @@ -64,12 +67,19 @@ public ApplicationsResponse getApplicantsByUserApplications(String email) { SiteUser siteUser = siteUserRepository.getByEmail(email); Application userLatestApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term); - List userAppliedUniversities = List.of( - userLatestApplication.getFirstChoiceUniversity().getUniversity(), - userLatestApplication.getSecondChoiceUniversity().getUniversity(), - userLatestApplication.getThirdChoiceUniversity().getUniversity() - ); - + List userAppliedUniversities = Arrays.asList( + Optional.ofNullable(userLatestApplication.getFirstChoiceUniversity()) + .map(UniversityInfoForApply::getUniversity) + .orElse(null), + Optional.ofNullable(userLatestApplication.getSecondChoiceUniversity()) + .map(UniversityInfoForApply::getUniversity) + .orElse(null), + Optional.ofNullable(userLatestApplication.getThirdChoiceUniversity()) + .map(UniversityInfoForApply::getUniversity) + .orElse(null) + ).stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); List firstChoiceApplicants = getFirstChoiceApplicants(userAppliedUniversities, siteUser, term); List secondChoiceApplicants = getSecondChoiceApplicants(userAppliedUniversities, siteUser, term); diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 7dd8b63f9..2f69d6cf7 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -39,11 +39,15 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { TokenService tokenService; private String accessToken; + private String adminAccessToken; + private String user6AccessToken; private Application 나의_지원정보; private Application 사용자1_지원정보; private Application 사용자2_지원정보; private Application 사용자3_지원정보; private Application 사용자4_이전학기_지원정보; + private Application 사용자5_관리자_지원정보; + private Application 사용자6_지원정보; @Value("${university.term}") private String term; @@ -60,11 +64,21 @@ public void setUpUserAndToken() { String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); tokenService.saveToken(refreshToken, TokenType.REFRESH); + adminAccessToken = tokenService.generateToken("email5", TokenType.ACCESS); + String adminRefreshToken = tokenService.generateToken("email5", TokenType.REFRESH); + tokenService.saveToken(adminRefreshToken, TokenType.REFRESH); + + user6AccessToken = tokenService.generateToken("email6", TokenType.ACCESS); + String user6RefreshToken = tokenService.generateToken("email6", TokenType.REFRESH); + tokenService.saveToken(user6RefreshToken, TokenType.REFRESH); + // setUp - 사용자 정보 저장 SiteUser 사용자1 = siteUserRepository.save(createSiteUserByEmail("email1")); SiteUser 사용자2 = siteUserRepository.save(createSiteUserByEmail("email2")); SiteUser 사용자3 = siteUserRepository.save(createSiteUserByEmail("email3")); SiteUser 사용자4_이전학기_지원자 = siteUserRepository.save(createSiteUserByEmail("email4")); + SiteUser 사용자5_관리자 = siteUserRepository.save(createSiteUserByEmail("email5")); + SiteUser 사용자6 = siteUserRepository.save(createSiteUserByEmail("email6")); // setUp - 지원 정보 저장 Gpa gpa = createDummyGpa(); @@ -74,17 +88,22 @@ public void setUpUserAndToken() { 사용자2_지원정보 = new Application(사용자2, gpa, languageTest, term); 사용자3_지원정보 = new Application(사용자3, gpa, languageTest, term); 사용자4_이전학기_지원정보 = new Application(사용자4_이전학기_지원자, gpa, languageTest, beforeTerm); + 사용자5_관리자_지원정보 = new Application(사용자5_관리자, gpa, languageTest, term); + 사용자6_지원정보 = new Application(사용자6, gpa, languageTest, term); 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 린츠_카톨릭대학_지원_정보, "0"); 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 그라츠공과대학_지원_정보, "1"); 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, 서던덴마크대학교_지원_정보, "2"); 사용자3_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "3"); 사용자4_이전학기_지원정보.updateUniversityChoice(네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "4"); + 사용자6_지원정보.updateUniversityChoice(코펜하겐IT대학_지원_정보, null, null, "6"); 나의_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자1_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자2_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자3_지원정보.setVerifyStatus(VerifyStatus.APPROVED); 사용자4_이전학기_지원정보.setVerifyStatus(VerifyStatus.APPROVED); - applicationRepository.saveAll(List.of(나의_지원정보, 사용자1_지원정보, 사용자2_지원정보, 사용자3_지원정보, 사용자4_이전학기_지원정보)); + 사용자5_관리자_지원정보.setVerifyStatus(VerifyStatus.APPROVED); + 사용자6_지원정보.setVerifyStatus(VerifyStatus.APPROVED); + applicationRepository.saveAll(List.of(나의_지원정보, 사용자1_지원정보, 사용자2_지원정보, 사용자3_지원정보, 사용자4_이전학기_지원정보, 사용자5_관리자_지원정보, 사용자6_지원정보)); } @Test @@ -236,7 +255,7 @@ public void setUpUserAndToken() { } @Test - void 내가_지원한_대학의_지원자를_조회한다() { + void 경쟁자를_조회한다() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() @@ -245,6 +264,8 @@ public void setUpUserAndToken() { .statusCode(200) .extract().as(ApplicationsResponse.class); + Integer choicedUniversityCount = 3; + List firstChoiceApplicants = response.firstChoice(); List secondChoiceApplicants = response.secondChoice(); List thirdChoiceApplicants = response.thirdChoice(); @@ -269,9 +290,52 @@ public void setUpUserAndToken() { List.of()), UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, List.of(ApplicantResponse.of(나의_지원정보, true)))); - - assertThat(firstChoiceApplicants.size()).isEqualTo(3); - assertThat(secondChoiceApplicants.size()).isEqualTo(3); - assertThat(thirdChoiceApplicants.size()).isEqualTo(3); + + assertThat(firstChoiceApplicants.size()).isEqualTo(choicedUniversityCount); + assertThat(secondChoiceApplicants.size()).isEqualTo(choicedUniversityCount); + assertThat(thirdChoiceApplicants.size()).isEqualTo(choicedUniversityCount); } + + @Test + void 지원_대학중_미선택이_있을_떄_경쟁자를_조회한다() { + ApplicationsResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + user6AccessToken) + .when().log().all() + .get("/application/competitors") + .then().log().all() + .statusCode(200) + .extract().as(ApplicationsResponse.class); + + Integer choicedUniversityCount = 1; + + List firstChoiceApplicants = response.firstChoice(); + List secondChoiceApplicants = response.secondChoice(); + List thirdChoiceApplicants = response.thirdChoice(); + + assertThat(firstChoiceApplicants.size()).isEqualTo(choicedUniversityCount); + assertThat(secondChoiceApplicants.size()).isEqualTo(choicedUniversityCount); + assertThat(thirdChoiceApplicants.size()).isEqualTo(choicedUniversityCount); + } + + @Test + void 지원_대학이_모두_미선택일_때_경쟁자를_조회한다() { + ApplicationsResponse response = RestAssured.given().log().all() + .header("Authorization", "Bearer " + adminAccessToken) + .when().log().all() + .get("/application/competitors") + .then().log().all() + .statusCode(200) + .extract().as(ApplicationsResponse.class); + + Integer choicedUniversityCount = 0; + + List firstChoiceApplicants = response.firstChoice(); + List secondChoiceApplicants = response.secondChoice(); + List thirdChoiceApplicants = response.thirdChoice(); + + assertThat(firstChoiceApplicants.size()).isEqualTo(choicedUniversityCount); + assertThat(secondChoiceApplicants.size()).isEqualTo(choicedUniversityCount); + assertThat(thirdChoiceApplicants.size()).isEqualTo(choicedUniversityCount); + } + } From eeef801198b3aaa81c172ac33f262a2c51e9d16d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:03:00 +0900 Subject: [PATCH 103/158] =?UTF-8?q?S3=20=ED=8C=8C=EC=9D=BC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=97=90=20=EA=B4=80=ED=95=9C=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20CloudFront=20=EB=8F=84=EC=9E=85=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Url을 제외한 path만 저장하도록 리팩토링 * feat: 저장된 파일의 url prefix 제공하는 api 생성 --- .../solidconnection/s3/S3Controller.java | 19 +++++++++++++++---- .../s3/S3ControllerSwagger.java | 17 +++++++++++++++++ .../example/solidconnection/s3/S3Service.java | 17 ++++------------- .../solidconnection/s3/urlPrefixResponse.java | 9 +++++++++ .../siteuser/service/SiteUserService.java | 2 +- .../unit/service/SiteUserServiceTest.java | 4 ++-- 6 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/s3/urlPrefixResponse.java diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java index 8da2984ea..7a1c1fc7c 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -2,11 +2,9 @@ import com.example.solidconnection.type.ImgType; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.security.Principal; @@ -17,6 +15,14 @@ public class S3Controller implements S3ControllerSwagger { private final S3Service s3Service; + @Value("${cloud.aws.s3.url.default}") + private String s3Default; + @Value("${cloud.aws.s3.url.uploaded}") + private String s3Uploaded; + @Value("${cloud.aws.cloudFront.url.default}") + private String cloudFrontDefault; + @Value("${cloud.aws.cloudFront.url.uploaded}") + private String cloudFrontUploaded; @PostMapping("/profile/pre") public ResponseEntity uploadPreProfileImage( @@ -46,4 +52,9 @@ public ResponseEntity uploadLanguageImage( UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST); return ResponseEntity.ok(profileImageUrl); } + + @GetMapping("/s3-url-prefix") + public ResponseEntity getS3UrlPrefix() { + return ResponseEntity.ok(new urlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded)); + } } diff --git a/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java b/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java index aa432b195..bce6bfe15 100644 --- a/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java +++ b/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java @@ -116,4 +116,21 @@ public interface S3ControllerSwagger { } ) ResponseEntity uploadLanguageImage(@RequestParam("file") MultipartFile imageFile); + + @SecurityRequirements + @SecurityRequirement(name = ACCESS_TOKEN) + @Operation( + summary = "S3 url prefix 확인", + responses = { + @ApiResponse( + responseCode = "200", + description = "S3 url prefix 반환", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = urlPrefixResponse.class) + ) + ) + } + ) + ResponseEntity getS3UrlPrefix(); } diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 4bee94932..553367973 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -69,8 +69,7 @@ public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType i log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); throw new CustomException(S3_CLIENT_EXCEPTION); } - - return new UploadedFileUrlResponse(amazonS3.getUrl(bucket, fileName).toString()); + return new UploadedFileUrlResponse(fileName); } public List uploadFiles(List multipartFile, ImgType imageFile) { @@ -101,7 +100,7 @@ public List uploadFiles(List multipartFi log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); throw new CustomException(S3_CLIENT_EXCEPTION); } - uploadedFileUrlResponseList.add(new UploadedFileUrlResponse(amazonS3.getUrl(bucket, fileName).toString())); + uploadedFileUrlResponseList.add(new UploadedFileUrlResponse(fileName)); } return uploadedFileUrlResponseList; @@ -140,8 +139,7 @@ public void deleteExProfile(String email) { } public void deletePostImage(String url) { - String key = getPostImageUrl(url); - deleteFile(key); + deleteFile(url); } private void deleteFile(String fileName) { @@ -158,13 +156,6 @@ private void deleteFile(String fileName) { private String getExProfileImageUrl(String email) { SiteUser siteUser = siteUserRepository.getByEmail(email); - String fileName = siteUser.getProfileImageUrl(); - int domainStartIndex = fileName.indexOf(".com"); - return fileName.substring(domainStartIndex + 5); - } - - private String getPostImageUrl(String url) { - int domainStartIndex = url.indexOf(".com"); - return url.substring(domainStartIndex + 5); + return siteUser.getProfileImageUrl(); } } diff --git a/src/main/java/com/example/solidconnection/s3/urlPrefixResponse.java b/src/main/java/com/example/solidconnection/s3/urlPrefixResponse.java new file mode 100644 index 000000000..59eac23ca --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/urlPrefixResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.s3; + +public record urlPrefixResponse( + String s3Default, + String s3Uploaded, + String cloudFrontDefault, + String cloudFrontUploaded +) { +} diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index 0ab6aa0d2..793627adf 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -106,7 +106,7 @@ private void validateProfileImage(MultipartFile imageFile) { } private boolean isDefaultProfileImage(String profileImageUrl) { - String prefix = "https://solid-connection-uploaded.s3.ap-northeast-2.amazonaws.com/profile/"; + String prefix = "profile/"; return profileImageUrl == null || !profileImageUrl.startsWith(prefix); } diff --git a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java index 7cfeb94c6..860f76e11 100644 --- a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java @@ -63,7 +63,7 @@ private SiteUser createSiteUser() { return new SiteUser( "test@example.com", "nickname", - "https://solid-connection-uploaded.s3.ap-northeast-2.amazonaws.com/profile/abcd", + "profile/fajwoiejoiewjfoi", "1999-01-01", PreparationStatus.CONSIDERING, Role.MENTEE, @@ -78,7 +78,7 @@ private MultipartFile createMockImageFile() { } private UploadedFileUrlResponse createUploadedFileUrlResponse() { - return new UploadedFileUrlResponse("https://s3.example.com/test1.png"); + return new UploadedFileUrlResponse("profile/fajwoiejoiewjfoi"); } @Test From a5286fa2a338c69a5c78df9efdcae71ef2dc55a4 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:29:53 +0900 Subject: [PATCH 104/158] =?UTF-8?q?feat:=20=EB=82=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=EC=97=90=20email=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/controller/AuthController.java | 9 +++------ .../solidconnection/siteuser/dto/MyPageResponse.java | 4 ++++ .../java/com/example/solidconnection/e2e/MyPageTest.java | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index c126da674..b73812a2a 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -31,15 +31,13 @@ public class AuthController implements AuthControllerSwagger { @PostMapping("/kakao") public ResponseEntity processKakaoOauth(@RequestBody KakaoCodeRequest kakaoCodeRequest) { KakaoOauthResponse kakaoOauthResponse = signInService.signIn(kakaoCodeRequest); - return ResponseEntity - .ok(kakaoOauthResponse); + return ResponseEntity.ok(kakaoOauthResponse); } @PostMapping("/sign-up") public ResponseEntity signUp(@Valid @RequestBody SignUpRequest signUpRequest) { SignUpResponse signUpResponseDto = signUpService.signUp(signUpRequest); - return ResponseEntity - .ok(signUpResponseDto); + return ResponseEntity.ok(signUpResponseDto); } @PostMapping("/sign-out") @@ -57,7 +55,6 @@ public ResponseEntity quit(Principal principal) { @PostMapping("/reissue") public ResponseEntity reissueToken(Principal principal) { ReissueResponse reissueResponse = authService.reissue(principal.getName()); - return ResponseEntity - .ok(reissueResponse); + return ResponseEntity.ok(reissueResponse); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java index 0f035c905..892094394 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java @@ -19,6 +19,9 @@ public record MyPageResponse( @Schema(description = "생년월일", example = "1990-01-01") String birth, + @Schema(description = "이메일", example = "example@solid-conenct.net") + String email, + @Schema(description = "좋아요 누른 게시물 수", example = "0") int likedPostCount, @@ -34,6 +37,7 @@ public static MyPageResponse of(SiteUser siteUser, int likedUniversityCount) { siteUser.getProfileImageUrl(), siteUser.getRole(), siteUser.getBirth(), + siteUser.getEmail(), 0, // TODO: 커뮤니티 기능 생기면 업데이트 필요 0, // TODO: 멘토 기능 생기면 업데이트 필요 likedUniversityCount diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index 953fddf8b..059e00cde 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -52,6 +52,7 @@ public void setUpUserAndToken() { assertAll("불러온 마이 페이지 정보가 DB의 정보와 일치한다.", () -> assertThat(myPageResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), () -> assertThat(myPageResponse.birth()).isEqualTo(savedSiteUser.getBirth()), - () -> assertThat(myPageResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl())); + () -> assertThat(myPageResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl()), + () -> assertThat(myPageResponse.email()).isEqualTo(savedSiteUser.getEmail())); } } From c1ba148bbdf960179fcca7976703de756e8c77b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:32:02 +0900 Subject: [PATCH 105/158] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B9=84=EB=8F=99=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/s3/FileUploadService.java | 51 +++++++++++++++ .../example/solidconnection/s3/S3Service.java | 64 +++++-------------- 2 files changed, 68 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/s3/FileUploadService.java diff --git a/src/main/java/com/example/solidconnection/s3/FileUploadService.java b/src/main/java/com/example/solidconnection/s3/FileUploadService.java new file mode 100644 index 000000000..5e31c5475 --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/FileUploadService.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.s3; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.SdkClientException; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.example.solidconnection.custom.exception.CustomException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +import static com.example.solidconnection.custom.exception.ErrorCode.S3_CLIENT_EXCEPTION; +import static com.example.solidconnection.custom.exception.ErrorCode.S3_SERVICE_EXCEPTION; + +@Component +@EnableAsync +@Slf4j +public class FileUploadService { + private final AmazonS3Client amazonS3; + + public FileUploadService(AmazonS3Client amazonS3) { + this.amazonS3 = amazonS3; + } + + @Async + public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) { + // 메타데이터 생성 + String contentType = multipartFile.getContentType(); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(contentType); + metadata.setContentLength(multipartFile.getSize()); + + try { + amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + log.info("이미지 업로드 정상적 완료 thread: {}", Thread.currentThread().getName()); + } catch (AmazonServiceException e) { + log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage()); + throw new CustomException(S3_SERVICE_EXCEPTION); + } catch (SdkClientException | IOException e) { + log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); + throw new CustomException(S3_CLIENT_EXCEPTION); + } + } +} diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 553367973..534c4a935 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -3,10 +3,7 @@ import com.amazonaws.AmazonServiceException; import com.amazonaws.SdkClientException; import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.DeleteObjectRequest; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -15,10 +12,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; import java.util.*; import static com.example.solidconnection.custom.exception.ErrorCode.FILE_NOT_EXIST; @@ -34,8 +31,11 @@ public class S3Service { private static final Logger log = LoggerFactory.getLogger(S3Service.class); private final AmazonS3Client amazonS3; private final SiteUserRepository siteUserRepository; + private final FileUploadService fileUploadService; + private final ThreadPoolTaskExecutor asyncExecutor; @Value("${cloud.aws.s3.bucket}") private String bucket; + private final long MAX_FILE_SIZE_MB = 1024 * 1024 * 3; /* * 파일을 S3에 업로드한다. @@ -44,30 +44,24 @@ public class S3Service { * - 파일에 대한 메타 데이터를 생성한다. * - 임의의 랜덤한 문자열로 파일 이름을 생성한다. * - S3에 파일을 업로드한다. + * - 3mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다. + * - 3mb 미만의 파일은 바로 업로드한다. * */ public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType imageFile) { // 파일 검증 validateImgFile(multipartFile); - - // 메타데이터 생성 - String contentType = multipartFile.getContentType(); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(contentType); - metadata.setContentLength(multipartFile.getSize()); - // 파일 이름 생성 UUID randomUUID = UUID.randomUUID(); String fileName = imageFile.getType() + "/" + randomUUID; - - try { - amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata) - .withCannedAcl(CannedAccessControlList.PublicRead)); - } catch (AmazonServiceException e) { - log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage()); - throw new CustomException(S3_SERVICE_EXCEPTION); - } catch (SdkClientException | IOException e) { - log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); - throw new CustomException(S3_CLIENT_EXCEPTION); + // 파일업로드 비동기로 진행 + if (multipartFile.getSize() >= MAX_FILE_SIZE_MB) { + asyncExecutor.submit(() -> { + fileUploadService.uploadFile(bucket, "origin/" + fileName, multipartFile); + }); + } else { + asyncExecutor.submit(() -> { + fileUploadService.uploadFile(bucket, fileName, multipartFile); + }); } return new UploadedFileUrlResponse(fileName); } @@ -75,34 +69,10 @@ public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType i public List uploadFiles(List multipartFile, ImgType imageFile) { List uploadedFileUrlResponseList = new ArrayList<>(); - for (MultipartFile file : multipartFile) { - // 파일 검증 - validateImgFile(file); - - // 메타데이터 생성 - String contentType = file.getContentType(); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(contentType); - metadata.setContentLength(file.getSize()); - - // 파일 이름 생성 - UUID randomUUID = UUID.randomUUID(); - String fileName = imageFile.getType() + "/" + randomUUID; - - try { - amazonS3.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), metadata) - .withCannedAcl(CannedAccessControlList.PublicRead)); - } catch (AmazonServiceException e) { - log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage()); - throw new CustomException(S3_SERVICE_EXCEPTION); - } catch (SdkClientException | IOException e) { - log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); - throw new CustomException(S3_CLIENT_EXCEPTION); - } - uploadedFileUrlResponseList.add(new UploadedFileUrlResponse(fileName)); + UploadedFileUrlResponse uploadedFileUrlResponse = uploadFile(file, imageFile); + uploadedFileUrlResponseList.add(uploadedFileUrlResponse); } - return uploadedFileUrlResponseList; } From ed2b5f17f7f68e8948420fbced36ef385be007bc Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:12:48 +0900 Subject: [PATCH 106/158] =?UTF-8?q?chore:=20release=20github=20action=20?= =?UTF-8?q?=EC=9E=84=EC=9D=98=20=EC=8B=A4=ED=96=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac375db23..7bf1a0ddd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ name: Build Gradle and Deploy on: push: branches: [ "release" ] + workflow_dispatch: jobs: build-gradle: From d908841da42e15d3290d5cf8d969cd89bb818b46 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:18:17 +0900 Subject: [PATCH 107/158] =?UTF-8?q?fix:=20GeneralRecommendUniversities.ini?= =?UTF-8?q?t=20=EC=8B=9C=20=EC=BF=BC=EB=A6=AC=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findByKoreanNameAndTerm -> findFirstByKoreanNameAndTerm 로 전환. semester에 따라서 중복이 나오는 것이 불가피 하기에 무작위 하나로 설정 --- .../university/repository/UniversityInfoForApplyRepository.java | 2 +- .../university/service/GeneralRecommendUniversities.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index 2f56cce3a..4adc0d718 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -20,7 +20,7 @@ public interface UniversityInfoForApplyRepository extends JpaRepository findByIdAndTerm(Long id, String term); - Optional findByKoreanNameAndTerm(String koreanName, String term); + Optional findFirstByKoreanNameAndTerm(String koreanName, String term); @Query("SELECT c FROM UniversityInfoForApply c WHERE c.university IN :universities AND c.term = :term") List findByUniversitiesAndTerm(@Param("universities") List universities, @Param("term") String term); diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java index cf213c6af..5c1c2e787 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java @@ -41,7 +41,7 @@ public class GeneralRecommendUniversities { public void init() { int i = 0; while (recommendUniversities.size() < RECOMMEND_UNIVERSITY_NUM && i < candidates.size()) { - universityInfoForApplyRepository.findByKoreanNameAndTerm(candidates.get(i), term) + universityInfoForApplyRepository.findFirstByKoreanNameAndTerm(candidates.get(i), term) .ifPresent(recommendUniversities::add); i++; } From 64a1455a6c20ac1e42bc3d3c258d755d64465d5f Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:47:06 +0900 Subject: [PATCH 108/158] chore: Update release.yml --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac375db23..7bf1a0ddd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ name: Build Gradle and Deploy on: push: branches: [ "release" ] + workflow_dispatch: jobs: build-gradle: From ff6f7b7bdde1669bd487909d86edefc7ac689a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=9B=90?= <107756067+leesewon00@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:15:28 +0900 Subject: [PATCH 109/158] =?UTF-8?q?=EC=A7=80=EC=9B=90=EC=84=9C(application?= =?UTF-8?q?)=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=EC=84=9C=20=ED=95=99?= =?UTF-8?q?=EC=A0=90/=EC=96=B4=ED=95=99=20=EC=9D=B8=EC=A6=9D=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=A7=80=EC=9B=90=20=EC=A0=88=EC=B0=A8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 학점, 어학 엔티티 정의 * feat: 학점, 어학 레포지토리 정의 * feat: 학점, 어학 서비스 로직 추가 * feat: 학점, 어학 컨트롤러 로직 추가 * feat: 학점, 어학 관련 DTO 정의 * feat: 학점, 어학 관련 예외 정의 * feat: 학점, 어학 레포지토리 테스트 로직 추가 * feat: 학점, 어학 서비스 테스트 로직 추가 * refactor: 수정안에 따른 지원 절차 수정 * refactor: 수정안에 따른 지원 절차 테스트 로직 수정 * feat: api naming 통일을 위한 경로 수정 * refactor: 조회 목적에 맞게 함수명 수정 * refactor: 지원서 제출에서 기존이력을 soft delete 방식으로 처리 * refactor: 예상하지 못한 쿼리 실행을 방지하기 위해 즉시로딩 지연로딩으로 수정 * refactor: GpaScore, LanguageTestScore 이력 수정 대신 새로운 객체 생성하여 처리 * fix: 추천 대학 목록 조회 테스트 실패로 즉시로딩으로 수정 --- .../controller/ApplicationController.java | 34 +-- .../ApplicationControllerSwagger.java | 68 ----- .../application/domain/Application.java | 64 ++++- .../application/domain/Gpa.java | 2 + .../application/domain/LanguageTest.java | 2 + .../application/dto/ApplyRequest.java | 18 ++ .../repository/ApplicationRepository.java | 7 +- .../service/ApplicationSubmissionService.java | 107 ++++---- .../service/VerifyStatusQueryService.java | 75 ----- .../custom/exception/ErrorCode.java | 7 + .../score/controller/ScoreController.java | 56 ++++ .../score/domain/GpaScore.java | 53 ++++ .../score/domain/LanguageTestScore.java | 52 ++++ .../score/dto/GpaScoreRequest.java | 34 +++ .../score/dto/GpaScoreStatus.java | 25 ++ .../score/dto/GpaScoreStatusResponse.java | 8 + .../score/dto/LanguageTestScoreRequest.java | 37 +++ .../score/dto/LanguageTestScoreStatus.java | 25 ++ .../dto/LanguageTestScoreStatusResponse.java | 9 + .../score/repository/GpaScoreRepository.java | 15 + .../LanguageTestScoreRepository.java | 17 ++ .../score/service/ScoreService.java | 73 +++++ .../siteuser/domain/SiteUser.java | 9 + .../university/domain/University.java | 7 +- .../e2e/ApplicationSubmissionTest.java | 254 ----------------- .../e2e/VerifyStatusQueryTest.java | 150 ---------- .../repository/GpaScoreRepositoryTest.java | 93 +++++++ .../LanguageTestScoreRepositoryTest.java | 98 +++++++ .../unit/service/ApplicationServiceTest.java | 256 ++++++++++-------- .../unit/service/ScoreServiceTest.java | 201 ++++++++++++++ 30 files changed, 1093 insertions(+), 763 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java delete mode 100644 src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java create mode 100644 src/main/java/com/example/solidconnection/score/controller/ScoreController.java create mode 100644 src/main/java/com/example/solidconnection/score/domain/GpaScore.java create mode 100644 src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusResponse.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java create mode 100644 src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java create mode 100644 src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java create mode 100644 src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java create mode 100644 src/main/java/com/example/solidconnection/score/service/ScoreService.java delete mode 100644 src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java delete mode 100644 src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java create mode 100644 src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index d6695df28..8baf7255f 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -1,13 +1,8 @@ package com.example.solidconnection.application.controller; -import com.example.solidconnection.application.dto.ApplicationSubmissionResponse; -import com.example.solidconnection.application.dto.ApplicationsResponse; -import com.example.solidconnection.application.dto.ScoreRequest; -import com.example.solidconnection.application.dto.UniversityChoiceRequest; -import com.example.solidconnection.application.dto.VerifyStatusResponse; +import com.example.solidconnection.application.dto.*; import com.example.solidconnection.application.service.ApplicationQueryService; import com.example.solidconnection.application.service.ApplicationSubmissionService; -import com.example.solidconnection.application.service.VerifyStatusQueryService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -28,28 +23,18 @@ public class ApplicationController implements ApplicationControllerSwagger { private final ApplicationSubmissionService applicationSubmissionService; private final ApplicationQueryService applicationQueryService; - private final VerifyStatusQueryService verifyStatusQueryService; - @PostMapping("/score") - public ResponseEntity submitScore( + // 지원서 제출하기 api + @PostMapping() + public ResponseEntity apply( Principal principal, - @Valid @RequestBody ScoreRequest scoreRequest) { - boolean result = applicationSubmissionService.submitScore(principal.getName(), scoreRequest); + @Valid @RequestBody ApplyRequest applyRequest) { + boolean result = applicationSubmissionService.apply(principal.getName(), applyRequest); return ResponseEntity .status(HttpStatus.OK) .body(new ApplicationSubmissionResponse(result)); } - @PostMapping("/university") - public ResponseEntity submitUniversityChoice( - Principal principal, - @Valid @RequestBody UniversityChoiceRequest universityChoiceRequest) { - boolean result = applicationSubmissionService.submitUniversityChoice(principal.getName(), universityChoiceRequest); - return ResponseEntity - .status(HttpStatus.OK) - .body(new ApplicationSubmissionResponse(result)); - } - @GetMapping public ResponseEntity getApplicants( Principal principal, @@ -69,11 +54,4 @@ public ResponseEntity getApplicantsForUserCompetitors( return ResponseEntity .ok(result); } - - @GetMapping("/status") - public ResponseEntity getApplicationVerifyStatus(Principal principal) { - VerifyStatusResponse result = verifyStatusQueryService.getVerifyStatus(principal.getName()); - return ResponseEntity - .ok(result); - } } diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java index f531923ac..3b93dea4c 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java @@ -1,10 +1,6 @@ package com.example.solidconnection.application.controller; -import com.example.solidconnection.application.dto.ApplicationSubmissionResponse; import com.example.solidconnection.application.dto.ApplicationsResponse; -import com.example.solidconnection.application.dto.ScoreRequest; -import com.example.solidconnection.application.dto.UniversityChoiceRequest; -import com.example.solidconnection.application.dto.VerifyStatusResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -12,10 +8,8 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestParam; -import io.swagger.v3.oas.annotations.parameters.RequestBody; import java.security.Principal; @@ -25,53 +19,6 @@ @SecurityRequirements @SecurityRequirement(name = ACCESS_TOKEN) public interface ApplicationControllerSwagger { - - @Operation( - summary = "대학 성적과 어학 성적 제출", - requestBody = @RequestBody( - description = "대학 성적과 어학 성적", - required = true, - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ScoreRequest.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "대학 성적과 어학 성적 제출 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ApplicationSubmissionResponse.class) - ) - ) - } - ) - ResponseEntity submitScore(Principal principal, @Valid @RequestBody ScoreRequest scoreRequest); - - @Operation( - summary = "지망 대학 제출", - requestBody = @RequestBody( - description = "지망 대학", - required = true, - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UniversityChoiceRequest.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "지망 대학 제출 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ApplicationSubmissionResponse.class) - ) - ) - } - ) - ResponseEntity submitUniversityChoice(Principal principal, @Valid @RequestBody UniversityChoiceRequest universityChoiceRequest); - @Operation( summary = "지원자 목록 조회", responses = { @@ -86,19 +33,4 @@ public interface ApplicationControllerSwagger { } ) ResponseEntity getApplicants(Principal principal, @RequestParam(required = false) String region, @RequestParam(required = false) String keyword); - - @Operation( - summary = "성적 승인 상태 확인", - responses = { - @ApiResponse( - responseCode = "200", - description = "성적 승인 상태 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = VerifyStatusResponse.class) - ) - ) - } - ) - ResponseEntity getApplicationVerifyStatus(Principal principal); } diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java index 085141f22..0c56fd7f5 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Application.java +++ b/src/main/java/com/example/solidconnection/application/domain/Application.java @@ -3,15 +3,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import jakarta.persistence.Column; -import jakarta.persistence.Embedded; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -51,16 +43,19 @@ public class Application { @Column(length = 50, nullable = false) private String term; - @ManyToOne + @Column(columnDefinition = "TINYINT(1) NOT NULL DEFAULT 0") + private Boolean isDelete; + + @ManyToOne(fetch = FetchType.LAZY) private UniversityInfoForApply firstChoiceUniversity; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private UniversityInfoForApply secondChoiceUniversity; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private UniversityInfoForApply thirdChoiceUniversity; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private SiteUser siteUser; public Application( @@ -76,14 +71,53 @@ public Application( this.verifyStatus = PENDING; } - public void updateGpaAndLanguageTest( + public Application( + SiteUser siteUser, + Gpa gpa, + LanguageTest languageTest, + String term, + Integer updateCount, + UniversityInfoForApply firstChoiceUniversity, + UniversityInfoForApply secondChoiceUniversity, + UniversityInfoForApply thirdChoiceUniversity, + String nicknameForApply) { + this.siteUser = siteUser; + this.gpa = gpa; + this.languageTest = languageTest; + this.term = term; + this.updateCount = updateCount; + this.firstChoiceUniversity = firstChoiceUniversity; + this.secondChoiceUniversity = secondChoiceUniversity; + this.thirdChoiceUniversity = thirdChoiceUniversity; + this.nicknameForApply = nicknameForApply; + this.verifyStatus = PENDING; + } + + public Application( + SiteUser siteUser, Gpa gpa, - LanguageTest languageTest) { + LanguageTest languageTest, + String term, + UniversityInfoForApply firstChoiceUniversity, + UniversityInfoForApply secondChoiceUniversity, + UniversityInfoForApply thirdChoiceUniversity, + String nicknameForApply) { + this.siteUser = siteUser; this.gpa = gpa; this.languageTest = languageTest; + this.term = term; + this.updateCount = 0; + this.firstChoiceUniversity = firstChoiceUniversity; + this.secondChoiceUniversity = secondChoiceUniversity; + this.thirdChoiceUniversity = thirdChoiceUniversity; + this.nicknameForApply = nicknameForApply; this.verifyStatus = PENDING; } + public void setIsDeleteTrue() { + this.isDelete = true; + } + public void updateUniversityChoice( UniversityInfoForApply firstChoiceUniversity, UniversityInfoForApply secondChoiceUniversity, diff --git a/src/main/java/com/example/solidconnection/application/domain/Gpa.java b/src/main/java/com/example/solidconnection/application/domain/Gpa.java index 82803dae9..85b12d047 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Gpa.java +++ b/src/main/java/com/example/solidconnection/application/domain/Gpa.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,6 +11,7 @@ @AllArgsConstructor @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @Embeddable +@EqualsAndHashCode(of = {"gpa", "gpaCriteria", "gpaReportUrl"}) public class Gpa { @Column(nullable = false, name = "gpa") diff --git a/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java b/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java index a1e579ad8..4295372d4 100644 --- a/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java +++ b/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java @@ -6,6 +6,7 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,6 +14,7 @@ @AllArgsConstructor @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @Embeddable +@EqualsAndHashCode(of = {"languageTestType", "languageTestScore", "languageTestReportUrl"}) public class LanguageTest { @Column(nullable = false, name = "language_test_type", length = 10) diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java new file mode 100644 index 000000000..3a5d7f2f5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "지원서 제출") +public record ApplyRequest( + @NotNull(message = "gpa score id를 입력해주세요.") + @Schema(description = "지원하는 유저의 gpa score id", example = "1") + Long gpaScoreId, + + @NotNull(message = "language test score id를 입력해주세요.") + @Schema(description = "지원하는 유저의 language test score id", example = "1") + Long languageTestScoreId, + + UniversityChoiceRequest universityChoiceRequest +) { +} diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java index 8f30c196c..a3bca9dc2 100644 --- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -6,6 +6,8 @@ import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.UniversityInfoForApply; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -18,9 +20,8 @@ public interface ApplicationRepository extends JpaRepository boolean existsByNicknameForApply(String nicknameForApply); - Optional findTop1BySiteUser_EmailOrderByTermDesc(String email); - - Optional findBySiteUserAndTerm(SiteUser siteUser, String term); + @Query("SELECT a FROM Application a WHERE a.siteUser = :siteUser AND a.term = :term AND a.isDelete = false") + Optional findBySiteUserAndTerm(@Param("siteUser") SiteUser siteUser, @Param("term") String term); List findAllByFirstChoiceUniversityAndVerifyStatusAndTerm(UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus, String term); diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index b23b876c7..6b263996e 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -1,15 +1,17 @@ package com.example.solidconnection.application.service; -import com.example.solidconnection.application.domain.Application; -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.application.dto.ScoreRequest; +import com.example.solidconnection.application.domain.*; +import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.dto.UniversityChoiceRequest; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.cache.annotation.DefaultCacheOut; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.UniversityInfoForApply; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import lombok.RequiredArgsConstructor; @@ -30,63 +32,26 @@ public class ApplicationSubmissionService { private final ApplicationRepository applicationRepository; private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final SiteUserRepository siteUserRepository; + private final GpaScoreRepository gpaScoreRepository; + private final LanguageTestScoreRepository languageTestScoreRepository; @Value("${university.term}") private String term; - /* - * 학점과 영어 성적을 제출한다. - * - 금학기에 제출한 적이 있다면, 수정한다. - * - 성적을 제출한적이 한번도 없거나 제출한적이 있지만 금학기에 제출한 적이 없다면 새로 등록한다. - * - 수정을 하고 나면, 성적 승인 상태(verifyStatus)를 PENDING 상태로 변경한다. - * */ + // 학점 및 어학성적이 모두 유효한 경우에만 지원서 등록이 가능하다. + // 기존에 있던 status field 우선 APRROVED로 입력시킨다. @Transactional - @DefaultCacheOut(key = "application:query", cacheManager = "customCacheManager", prefix = true) - public boolean submitScore(String email, ScoreRequest scoreRequest) { + public boolean apply(String email, ApplyRequest applyRequest) { SiteUser siteUser = siteUserRepository.getByEmail(email); - Gpa gpa = scoreRequest.toGpa(); - LanguageTest languageTest = scoreRequest.toLanguageTest(); - - applicationRepository.findBySiteUserAndTerm(siteUser, term) - .ifPresentOrElse( - // 금학기에 성적 제출 이력이 있는 경우 - application -> application.updateGpaAndLanguageTest(gpa, languageTest), - () -> { - // 성적 제출한적이 한번도 없는 경우 && 성적 제출한적이 있지만 금학기에 없는 경우 - applicationRepository.save(new Application(siteUser, gpa, languageTest, term)); - } - ); - return true; - } - - /* - * 지망 대학교를 제출한다. - * - 지망 대학중 중복된 대학교가 있는지 검증한다. - * - 지원 정보 제출 내역이 없다면, 지금의 프로세스(성적 제출 후 지망대학 제출)에 벗어나는 요청이므로 예외를 응답한다. - * - 기존에 제출한 적이 있다면, 수정한다. - * - 수정 횟수 제한을 초과하지 않았는지 검증한다. - * - 새로운 '제출 닉네임'을 부여한다. (악의적으로 타인의 변경 기록을 추적하는 것을 막기 위해) - * - 성적 승인 상태(verifyStatus) 는 변경하지 않는다. - * */ - @Transactional - @DefaultCacheOut(key = "application:query", cacheManager = "customCacheManager", prefix = true) - public boolean submitUniversityChoice(String email, UniversityChoiceRequest universityChoiceRequest) { + UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest(); validateUniversityChoices(universityChoiceRequest); - // 성적 제출한 적이 한번도 없는 경우 - Application existingApplication = applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(email) - .orElseThrow(() -> new CustomException(SCORE_SHOULD_SUBMITTED_FIRST)); + Long gpaScoreId = applyRequest.gpaScoreId(); + Long languageTestScoreId = applyRequest.languageTestScoreId(); + GpaScore gpaScore = getValidGpaScore(siteUser, gpaScoreId); + LanguageTestScore languageTestScore = getValidLanguageTestScore(siteUser, languageTestScoreId); - Application application = Optional.of(existingApplication) - .filter(app -> !app.getTerm().equals(term)) - .map(app -> { - // 성적 제출한 적이 있지만 금학기에 없는 경우, 이전 성적으로 새 Application 객체를 등록 - SiteUser siteUser = siteUserRepository.getByEmail(email); - return applicationRepository.save(new Application(siteUser, app.getGpa(), app.getLanguageTest(), term)); - }) - .orElse(existingApplication); // 금학기에 이미 성적 제출한 경우 기존 객체 사용 - - validateUpdateLimitNotExceed(application); + Optional application = applicationRepository.findBySiteUserAndTerm(siteUser, term); UniversityInfoForApply firstChoiceUniversity = universityInfoForApplyRepository .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term); @@ -96,10 +61,44 @@ public boolean submitUniversityChoice(String email, UniversityChoiceRequest univ UniversityInfoForApply thirdChoiceUniversity = Optional.ofNullable(universityChoiceRequest.thirdChoiceUniversityId()) .map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term)) .orElse(null); - application.updateUniversityChoice(firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); + + if (application.isEmpty()) { + Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(), + term, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); + newApplication.setVerifyStatus(VerifyStatus.APPROVED); + applicationRepository.save(newApplication); + } else { + Application before = application.get(); + validateUpdateLimitNotExceed(before); + before.setIsDeleteTrue(); // 기존 이력 soft delete 수행한다. + + Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(), + term, before.getUpdateCount() + 1, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname()); + newApplication.setVerifyStatus(VerifyStatus.APPROVED); + applicationRepository.save(newApplication); + } return true; } + private GpaScore getValidGpaScore(SiteUser siteUser, Long gpaScoreId) { + GpaScore gpaScore = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId) + .orElseThrow(() -> new CustomException(INVALID_GPA_SCORE)); + if (gpaScore.getVerifyStatus() != VerifyStatus.APPROVED) { + throw new CustomException(INVALID_GPA_SCORE_STATUS); + } + return gpaScore; + } + + private LanguageTestScore getValidLanguageTestScore(SiteUser siteUser, Long languageTestScoreId) { + LanguageTestScore languageTestScore = languageTestScoreRepository + .findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId) + .orElseThrow(() -> new CustomException(INVALID_LANGUAGE_TEST_SCORE)); + if (languageTestScore.getVerifyStatus() != VerifyStatus.APPROVED) { + throw new CustomException(INVALID_LANGUAGE_TEST_SCORE_STATUS); + } + return languageTestScore; + } + private String getRandomNickname() { String randomNickname = NicknameCreator.createRandomNickname(); while (applicationRepository.existsByNicknameForApply(randomNickname)) { diff --git a/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java b/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java deleted file mode 100644 index 33bb340d9..000000000 --- a/src/main/java/com/example/solidconnection/application/service/VerifyStatusQueryService.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.example.solidconnection.application.service; - -import com.example.solidconnection.application.domain.Application; -import com.example.solidconnection.application.dto.VerifyStatusResponse; -import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.VerifyStatus; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.NOT_SUBMITTED; -import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.SCORE_SUBMITTED; -import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.SUBMITTED_APPROVED; -import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.SUBMITTED_PENDING; -import static com.example.solidconnection.application.service.VerifyStatusQueryService.ApplicationStatusResponse.SUBMITTED_REJECTED; - -@RequiredArgsConstructor -@Service -public class VerifyStatusQueryService { - - private final ApplicationRepository applicationRepository; - private final SiteUserRepository siteUserRepository; - - @Value("${university.term}") - private String term; - - /* - * 지원 상태를 조회한다. - * 학기별로 상태가 관리된다. - * */ - @Transactional(readOnly = true) - public VerifyStatusResponse getVerifyStatus(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - Optional application = applicationRepository.findBySiteUserAndTerm(siteUser,term); - - // 아무것도 제출 안함 - if (application.isEmpty()) { - return new VerifyStatusResponse(NOT_SUBMITTED.name(), 0); - } - - int updateCount = application.get().getUpdateCount(); - - // 제출한 상태 - if (application.get().getVerifyStatus() == VerifyStatus.PENDING) { - // 성적만 제출 - if (application.get().getFirstChoiceUniversity() == null) { - return new VerifyStatusResponse(SCORE_SUBMITTED.name(), 0); - } - // 성적 승인 대기 중 - return new VerifyStatusResponse(SUBMITTED_PENDING.name(), updateCount); - } - - // 성적 승인 반려 - if (application.get().getVerifyStatus() == VerifyStatus.REJECTED) { - return new VerifyStatusResponse(SUBMITTED_REJECTED.name(), updateCount); - } - - // 성적 승인 완료 - return new VerifyStatusResponse(SUBMITTED_APPROVED.name(), updateCount); - } - - public enum ApplicationStatusResponse { - NOT_SUBMITTED, // 어떤 것도 제출하지 않음 - COLLEGE_SUBMITTED, // 지망 대학만 제출 - SCORE_SUBMITTED, // 성적만 제출 - SUBMITTED_PENDING, // 성적 인증 대기 중 - SUBMITTED_REJECTED, // 성적 인증 승인 완료 - SUBMITTED_APPROVED // 성적 인증 반려 - } -} diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index f9e1e45b1..1a3aa8df7 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -66,6 +66,13 @@ public enum ErrorCode { INVALID_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글 좋아요입니다."), DUPLICATE_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "이미 좋아요한 게시글입니다."), + // score + INVALID_GPA_SCORE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 학점입니다."), + INVALID_GPA_SCORE_STATUS(HttpStatus.BAD_REQUEST.value(), "학점이 승인되지 않았습니다."), + INVALID_LANGUAGE_TEST_SCORE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 어학성적입니다."), + INVALID_LANGUAGE_TEST_SCORE_STATUS(HttpStatus.BAD_REQUEST.value(), "어학성적이 승인되지 않았습니다."), + USER_DO_NOT_HAVE_GPA(HttpStatus.BAD_REQUEST.value(), "해당 유저의 학점을 찾을 수 없음"), + // general JSON_PARSING_FAILED(HttpStatus.BAD_REQUEST.value(), "JSON 파싱을 할 수 없습니다."), JWT_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "JWT 토큰을 처리할 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java new file mode 100644 index 000000000..7bd0edaf2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java @@ -0,0 +1,56 @@ +package com.example.solidconnection.score.controller; + +import com.example.solidconnection.score.dto.*; +import com.example.solidconnection.score.service.ScoreService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; + +import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; + +@RestController +@RequestMapping("/score") +@RequiredArgsConstructor +@SecurityRequirements +@SecurityRequirement(name = ACCESS_TOKEN) +public class ScoreController { + + private final ScoreService scoreService; + + // 학점을 등록하는 api + @PostMapping("/gpa") + public ResponseEntity submitGpaScore( + Principal principal, + @Valid @RequestBody GpaScoreRequest gpaScoreRequest) { + Long id = scoreService.submitGpaScore(principal.getName(), gpaScoreRequest); + return ResponseEntity.ok(id); + } + + // 어학성적을 등록하는 api + @PostMapping("/languageTest") + public ResponseEntity submitLanguageTestScore( + Principal principal, + @Valid @RequestBody LanguageTestScoreRequest languageTestScoreRequest) { + Long id = scoreService.submitLanguageTestScore(principal.getName(), languageTestScoreRequest); + return ResponseEntity.ok(id); + } + + // 학점 상태를 확인하는 api + @GetMapping("/gpa") + public ResponseEntity getGpaScoreStatus(Principal principal) { + GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(principal.getName()); + return ResponseEntity.ok(gpaScoreStatus); + } + + // 어학 성적 상태를 확인하는 api + @GetMapping("/languageTest") + public ResponseEntity getLanguageTestScoreStatus(Principal principal) { + LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(principal.getName()); + return ResponseEntity.ok(languageTestScoreStatus); + } +} diff --git a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java new file mode 100644 index 000000000..2747f8c88 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.score.domain; + +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.VerifyStatus; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; + +@Getter +@Entity +@NoArgsConstructor +@EqualsAndHashCode +public class GpaScore extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Embedded + private Gpa gpa; + + private LocalDate issueDate; + + @Setter + @Column(columnDefinition = "varchar(50) not null default 'PENDING'") + @Enumerated(EnumType.STRING) + private VerifyStatus verifyStatus; + + private String rejectedReason; + + @ManyToOne + private SiteUser siteUser; + + public GpaScore(Gpa gpa, SiteUser siteUser, LocalDate issueDate) { + this.gpa = gpa; + this.siteUser = siteUser; + this.issueDate = issueDate; + this.verifyStatus = VerifyStatus.PENDING; + this.rejectedReason = null; + } + + public void setSiteUser(SiteUser siteUser) { + if (this.siteUser != null) { + this.siteUser.getGpaScoreList().remove(this); + } + this.siteUser = siteUser; + siteUser.getGpaScoreList().add(this); + } +} diff --git a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java new file mode 100644 index 000000000..bc16bc4e4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java @@ -0,0 +1,52 @@ +package com.example.solidconnection.score.domain; + +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.entity.common.BaseEntity; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.VerifyStatus; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDate; + +@Getter +@Entity +@NoArgsConstructor +@AllArgsConstructor +public class LanguageTestScore extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Embedded + private LanguageTest languageTest; + + private LocalDate issueDate; + + @Setter + @Column(columnDefinition = "varchar(50) not null default 'PENDING'") + @Enumerated(EnumType.STRING) + private VerifyStatus verifyStatus; + + private String rejectedReason; + + @ManyToOne + private SiteUser siteUser; + + public LanguageTestScore(LanguageTest languageTest, LocalDate issueDate, SiteUser siteUser) { + this.languageTest = languageTest; + this.issueDate = issueDate; + this.verifyStatus = VerifyStatus.PENDING; + this.siteUser = siteUser; + } + + public void setSiteUser(SiteUser siteUser) { + if (this.siteUser != null) { + this.siteUser.getLanguageTestScoreList().remove(this); + } + this.siteUser = siteUser; + siteUser.getLanguageTestScoreList().add(this); + } +} diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java new file mode 100644 index 000000000..b655f6143 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.score.dto; + +import com.example.solidconnection.application.domain.Gpa; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; + +@Schema(description = "대학 성적과 어학 시험 성적") +public record GpaScoreRequest( + @NotNull(message = "학점을 입력해주세요.") + @Schema(description = "GPA", example = "3.5", required = true) + Double gpa, + + @NotNull(message = "학점 기준을 입력해주세요.") + @Schema(description = "GPA 계산 기준", example = "4.0", required = true) + Double gpaCriteria, + + @NotNull(message = "발급일자를 입력해주세요.") + @Schema(description = "발급일자", example = "2024-10-06", required = true) + LocalDate issueDate, + + @NotBlank(message = "대학 성적 증명서를 첨부해주세요.") + @Schema(description = "대학 성적 증명서 URL", example = "http://example.com/gpa-report.pdf", required = true) + String gpaReportUrl) { + + public Gpa toGpa() { + return new Gpa( + this.gpa, + this.gpaCriteria, + this.gpaReportUrl); + } +} diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java new file mode 100644 index 000000000..0361cf0e7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.score.dto; + +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.type.VerifyStatus; + +import java.time.LocalDate; + +public record GpaScoreStatus( + Long id, + Gpa gpa, + LocalDate issueDate, + VerifyStatus verifyStatus, + String rejectedReason +) { + public static GpaScoreStatus from(GpaScore gpaScore) { + return new GpaScoreStatus( + gpaScore.getId(), + gpaScore.getGpa(), + gpaScore.getIssueDate(), + gpaScore.getVerifyStatus(), + gpaScore.getRejectedReason() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusResponse.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusResponse.java new file mode 100644 index 000000000..06fdba0d3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatusResponse.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.score.dto; + +import java.util.List; + +public record GpaScoreStatusResponse( + List gpaScoreStatusList +) { +} diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java new file mode 100644 index 000000000..36687b6c2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java @@ -0,0 +1,37 @@ +package com.example.solidconnection.score.dto; + + +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.type.LanguageTestType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; + +@Schema(description = "대학 성적과 어학 시험 성적") +public record LanguageTestScoreRequest( + @NotNull(message = "어학 종류를 입력해주세요.") + @Schema(description = "어학 시험 종류", example = "TOEFL", required = true) + LanguageTestType languageTestType, + + @NotBlank(message = "어학 점수를 입력해주세요.") + @Schema(description = "어학 시험 점수", example = "115", required = true) + String languageTestScore, + + @NotNull(message = "발급일자를 입력해주세요.") + @Schema(description = "발급일자", example = "2024-10-06", required = true) + LocalDate issueDate, + + @NotBlank(message = "어학 증명서를 첨부해주세요.") + @Schema(description = "어학 증명서 URL", example = "http://example.com/test-report.pdf", required = true) + String languageTestReportUrl) { + + public LanguageTest toLanguageTest() { + return new LanguageTest( + this.languageTestType, + this.languageTestScore, + this.languageTestReportUrl + ); + } +} diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java new file mode 100644 index 000000000..2d1d8fcb1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.score.dto; + +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.type.VerifyStatus; + +import java.time.LocalDate; + +public record LanguageTestScoreStatus( + Long id, + LanguageTest languageTest, + LocalDate issueDate, + VerifyStatus verifyStatus, + String rejectedReason +) { + public static LanguageTestScoreStatus from(LanguageTestScore languageTestScore) { + return new LanguageTestScoreStatus( + languageTestScore.getId(), + languageTestScore.getLanguageTest(), + languageTestScore.getIssueDate(), + languageTestScore.getVerifyStatus(), + languageTestScore.getRejectedReason() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java new file mode 100644 index 000000000..3d4f74894 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.score.dto; + + +import java.util.List; + +public record LanguageTestScoreStatusResponse( + List languageTestScoreStatusList +) { +} diff --git a/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java b/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java new file mode 100644 index 000000000..c5fbb2847 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.score.repository; + +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.siteuser.domain.SiteUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface GpaScoreRepository extends JpaRepository { + Optional findGpaScoreBySiteUser(SiteUser siteUser); + + Optional findGpaScoreBySiteUserAndId(SiteUser siteUser, Long id); +} diff --git a/src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java b/src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java new file mode 100644 index 000000000..8e6ca3967 --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.score.repository; + +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.LanguageTestType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface LanguageTestScoreRepository extends JpaRepository { + + Optional findLanguageTestScoreBySiteUserAndLanguageTest_LanguageTestType(SiteUser siteUser, LanguageTestType languageTestType); + + Optional findLanguageTestScoreBySiteUserAndId(SiteUser siteUser, Long id); +} diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java new file mode 100644 index 000000000..b2d9ad29e --- /dev/null +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -0,0 +1,73 @@ +package com.example.solidconnection.score.service; + +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.dto.*; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ScoreService { + + private final GpaScoreRepository gpaScoreRepository; + private final LanguageTestScoreRepository languageTestScoreRepository; + private final SiteUserRepository siteUserRepository; + + @Transactional + public Long submitGpaScore(String email, GpaScoreRequest gpaScoreRequest) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + + GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser, gpaScoreRequest.issueDate()); + newGpaScore.setSiteUser(siteUser); + GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); // 저장 후 반환된 객체 + return savedNewGpaScore.getId(); // 저장된 GPA Score의 ID 반환 + } + + @Transactional + public Long submitLanguageTestScore(String email, LanguageTestScoreRequest languageTestScoreRequest) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); + + LanguageTestScore newScore = new LanguageTestScore( + languageTest, languageTestScoreRequest.issueDate(), siteUser); + newScore.setSiteUser(siteUser); + LanguageTestScore savedNewScore = languageTestScoreRepository.save(newScore); // 새로 저장한 객체 + return savedNewScore.getId(); // 저장된 객체의 ID 반환 + } + + @Transactional(readOnly = true) + public GpaScoreStatusResponse getGpaScoreStatus(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + List gpaScoreStatusList = + Optional.ofNullable(siteUser.getGpaScoreList()) + .map(scores -> scores.stream() + .map(GpaScoreStatus::from) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()); + return new GpaScoreStatusResponse(gpaScoreStatusList); + } + + @Transactional(readOnly = true) + public LanguageTestScoreStatusResponse getLanguageTestScoreStatus(String email) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + List languageTestScoreStatusList = + Optional.ofNullable(siteUser.getLanguageTestScoreList()) + .map(scores -> scores.stream() + .map(LanguageTestScoreStatus::from) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()); + return new LanguageTestScoreStatusResponse(languageTestScoreStatusList); + } +} diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index f3c870ceb..47a071b04 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -3,6 +3,8 @@ import com.example.solidconnection.comment.domain.Comment; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -65,6 +67,13 @@ public class SiteUser { @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List postLikeList = new ArrayList<>(); + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) + private List languageTestScoreList = new ArrayList<>(); + + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) + private List gpaScoreList = new ArrayList<>(); + + public SiteUser( String email, String nickname, diff --git a/src/main/java/com/example/solidconnection/university/domain/University.java b/src/main/java/com/example/solidconnection/university/domain/University.java index c3021385e..8e31dfa4a 100644 --- a/src/main/java/com/example/solidconnection/university/domain/University.java +++ b/src/main/java/com/example/solidconnection/university/domain/University.java @@ -2,12 +2,7 @@ import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.Region; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java deleted file mode 100644 index a12612071..000000000 --- a/src/test/java/com/example/solidconnection/e2e/ApplicationSubmissionTest.java +++ /dev/null @@ -1,254 +0,0 @@ -package com.example.solidconnection.e2e; - -import com.example.solidconnection.application.domain.Application; -import com.example.solidconnection.application.dto.ScoreRequest; -import com.example.solidconnection.application.dto.UniversityChoiceRequest; -import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; -import com.example.solidconnection.custom.response.ErrorResponse; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.type.VerifyStatus; -import io.restassured.RestAssured; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; - -import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; -import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; -import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; -import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -@DisplayName("지원 정보 제출 테스트") -class ApplicationSubmissionTest extends UniversityDataSetUpEndToEndTest { - - @Autowired - private ApplicationRepository applicationRepository; - - @Autowired - private SiteUserRepository siteUserRepository; - - @Autowired - private TokenService tokenService; - - private final String email = "email@email.com"; - private String accessToken; - private SiteUser siteUser; - - @BeforeEach - public void setUpUserAndToken() { - // setUp - 회원 정보 저장 - siteUser = siteUserRepository.save(createSiteUserByEmail(email)); - - // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); - } - - @Test - void 대학교_성적과_어학성적을_처음으로_제출한다() { - // request - body 생성 및 요청 - ScoreRequest request = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", - "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .body(request) - .contentType("application/json") - .log().all() - .post("/application/score") - .then().log().all() - .statusCode(HttpStatus.OK.value()); - - Application application = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); - assertAll("대학교 성적과 어학 성적을 저장한다.", - () -> assertThat(application.getId()).isNotNull(), - () -> assertThat(application.getSiteUser().getId()).isEqualTo(siteUser.getId()), - () -> assertThat(application.getLanguageTest().getLanguageTestType()).isEqualTo(request.languageTestType()), - () -> assertThat(application.getLanguageTest().getLanguageTestScore()).isEqualTo(request.languageTestScore()), - () -> assertThat(application.getLanguageTest().getLanguageTestReportUrl()).isEqualTo(request.languageTestReportUrl()), - () -> assertThat(application.getGpa().getGpa()).isEqualTo(request.gpa()), - () -> assertThat(application.getGpa().getGpaReportUrl()).isEqualTo(request.gpaReportUrl()), - () -> assertThat(application.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), - () -> assertThat(application.getUpdateCount()).isZero()); - } - - @Test - void 대학교_성적과_어학성적을_다시_제출한다() { - // setUp - 성적 정보 저장 - ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", - "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest(),term)); - - // request - body 생성 및 요청 - ScoreRequest secondRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "90", - "languageTestReportUrl", 4.1, 4.5, "gpaReportUrl"); - RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .body(secondRequest) - .contentType("application/json") - .log().all() - .post("/application/score") - .then().log().all() - .statusCode(HttpStatus.OK.value()); - - Application updatedApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); - assertAll("대학교 성적과 어학 성적을 수정한다. 이때 수정 횟수는 증가하지 않고, 성적 승인 상태는 PENDING 으로 바뀐다.", - () -> assertThat(updatedApplication.getId()).isNotNull(), - () -> assertThat(updatedApplication.getSiteUser().getId()).isEqualTo(siteUser.getId()), - () -> assertThat(updatedApplication.getLanguageTest().getLanguageTestType()).isEqualTo(secondRequest.languageTestType()), - () -> assertThat(updatedApplication.getLanguageTest().getLanguageTestScore()).isEqualTo(secondRequest.languageTestScore()), - () -> assertThat(updatedApplication.getLanguageTest().getLanguageTestReportUrl()).isEqualTo(secondRequest.languageTestReportUrl()), - () -> assertThat(updatedApplication.getGpa().getGpa()).isEqualTo(secondRequest.gpa()), - () -> assertThat(updatedApplication.getGpa().getGpaReportUrl()).isEqualTo(secondRequest.gpaReportUrl()), - () -> assertThat(updatedApplication.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), - () -> assertThat(updatedApplication.getUpdateCount()).isZero()); - } - - @Test - void 성적_제출_후_지망_대학을_제출한다() { - // setUp - 성적 정보 저장 - ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", - "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest(),term)); - - // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); - RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .body(request) - .contentType("application/json") - .log().all() - .post("/application/university") - .then().log().all() - .statusCode(HttpStatus.OK.value()); - - Application application = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); - assertAll("지망 대학교를 저장한다.", - () -> assertThat(application.getId()).isNotNull(), - () -> assertThat(application.getSiteUser().getId()).isEqualTo(siteUser.getId()), - () -> assertThat(application.getFirstChoiceUniversity().getId()).isEqualTo(request.firstChoiceUniversityId()), - () -> assertThat(application.getSecondChoiceUniversity().getId()).isEqualTo(request.secondChoiceUniversityId()), - () -> assertThat(application.getThirdChoiceUniversity().getId()).isEqualTo(request.thirdChoiceUniversityId()), - () -> assertThat(application.getNicknameForApply()).isNotNull(), - () -> assertThat(application.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), - () -> assertThat(application.getUpdateCount()).isZero()); - } - - @Test - void 지망_대학을_수정한다() { - // setUp - 성적 정보와 지망 대학 저장 - ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", - "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest(),term)) - .updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "nickname"); - Application initialApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); - - // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); - RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .body(request) - .contentType("application/json") - .log().all() - .post("/application/university") - .then().log().all() - .statusCode(HttpStatus.OK.value()); - - Application updatedApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); - assertAll("지망 대학교를 수정한다. 이때 수정 횟수는 증가하고, 성적 승인 상태는 바뀌지 않는다.", - () -> assertThat(updatedApplication.getId()).isNotNull(), - () -> assertThat(updatedApplication.getSiteUser().getId()).isEqualTo(siteUser.getId()), - () -> assertThat(updatedApplication.getFirstChoiceUniversity().getId()).isEqualTo(request.firstChoiceUniversityId()), - () -> assertThat(updatedApplication.getSecondChoiceUniversity().getId()).isEqualTo(request.secondChoiceUniversityId()), - () -> assertThat(updatedApplication.getThirdChoiceUniversity().getId()).isEqualTo(request.thirdChoiceUniversityId()), - () -> assertThat(updatedApplication.getNicknameForApply()).isNotNull(), - () -> assertThat(updatedApplication.getVerifyStatus()).isEqualTo(initialApplication.getVerifyStatus()), - () -> assertThat(updatedApplication.getUpdateCount()).isEqualTo(initialApplication.getUpdateCount())); - } - - @Test - void 지망_대학을_최대_수정_가능_횟수보다_더_수정하려고하면_예외_응답을_반환한다() { - // setUp - 성적 정보와 지망 대학 저장 - ScoreRequest firstRequest = new ScoreRequest(LanguageTestType.TOEFL_IBT, "80", - "languageTestReportUrl", 4.0, 4.5, "gpaReportUrl"); - applicationRepository.save(new Application(siteUser, firstRequest.toGpa(), firstRequest.toLanguageTest(),term)); - Application initialApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser,term); - - // setUp - 지망 대학을 한계까지 수정 - for (int i = 0; i <= APPLICATION_UPDATE_COUNT_LIMIT; i++) { - initialApplication.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "nickname"); - applicationRepository.save(initialApplication); - } - - // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); - ErrorResponse errorResponse = RestAssured.given().log().all() - .header("Authorization", "Bearer " + accessToken) - .body(request) - .contentType("application/json") - .post("/application/university") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract().as(ErrorResponse.class); - - assertThat(errorResponse.message()).isEqualTo(APPLY_UPDATE_LIMIT_EXCEED.getMessage()); - } - - @Test - void 일지망_대학과_이지망_대학이_같으면_예외_응답을_반환한다() { - // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 그라츠대학_지원_정보.getId(), 메이지대학_지원_정보.getId()); - ErrorResponse errorResponse = RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .body(request) - .contentType("application/json") - .log().all() - .post("/application/university") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract().as(ErrorResponse.class); - - assertThat(errorResponse.message()).isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); - } - - @Test - void 일지망_대학과_삼지망_대학이_같으면_예외_응답을_반환한다() { - // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 그라츠대학_지원_정보.getId()); - ErrorResponse errorResponse = RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .body(request) - .contentType("application/json") - .log().all() - .post("/application/university") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract().as(ErrorResponse.class); - - assertThat(errorResponse.message()).isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); - } - - @Test - void 이지망_대학과_삼지망_대학이_같으면_예외_응답을_반환한다() { - // request - body 생성 및 요청 - UniversityChoiceRequest request = new UniversityChoiceRequest(그라츠대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId(), 코펜하겐IT대학_지원_정보.getId()); - ErrorResponse errorResponse = RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .body(request) - .contentType("application/json") - .log().all() - .post("/application/university") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract().as(ErrorResponse.class); - - assertThat(errorResponse.message()).isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); - } -} diff --git a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java b/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java deleted file mode 100644 index 856c4cdca..000000000 --- a/src/test/java/com/example/solidconnection/e2e/VerifyStatusQueryTest.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.example.solidconnection.e2e; - -import com.example.solidconnection.application.domain.Application; -import com.example.solidconnection.application.dto.VerifyStatusResponse; -import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.VerifyStatus; -import io.restassured.RestAssured; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import static com.example.solidconnection.e2e.DynamicFixture.createDummyGpa; -import static com.example.solidconnection.e2e.DynamicFixture.createDummyLanguageTest; -import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -@DisplayName("지원 상태 조회 테스트") -class VerifyStatusQueryTest extends UniversityDataSetUpEndToEndTest { - - @Autowired - private SiteUserRepository siteUserRepository; - - @Autowired - private TokenService tokenService; - - @Autowired - private ApplicationRepository applicationRepository; - - private String accessToken; - private SiteUser siteUser; - - @BeforeEach - public void setUpUserAndToken() { - // setUp - 회원 정보 저장 - String email = "email@email.com"; - siteUser = siteUserRepository.save(createSiteUserByEmail(email)); - - // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); - } - - @Test - void 아무것도_제출하지_않은_상태를_반환한다() { - // request - 요청 - VerifyStatusResponse response = RestAssured.given().log().all() - .header("Authorization", "Bearer " + accessToken) - .when().get("/application/status") - .then().log().all() - .statusCode(200) - .extract().as(VerifyStatusResponse.class); - - assertAll( - () -> assertThat(response.status()).isEqualTo("NOT_SUBMITTED"), - () -> assertThat(response.updateCount()).isZero() - ); - } - - @Test - void 성적만_제출한_상태를_반환한다() { - // setUp - 성적만 제출한 상태 - Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest(),term); - applicationRepository.save(application); - - // request - 요청 - VerifyStatusResponse response = RestAssured.given().log().all() - .header("Authorization", "Bearer " + accessToken) - .when().get("/application/status") - .then().log().all() - .statusCode(200) - .extract().as(VerifyStatusResponse.class); - - assertAll( - () -> assertThat(response.status()).isEqualTo("SCORE_SUBMITTED"), - () -> assertThat(response.updateCount()).isZero() - ); - } - - @Test - void 성적과_대학을_모두_제출하고_승인을_기대라는_상태를_반환한다() { - // setUp - 성적과 대학을 모두 제출한 상태 - Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest(),term); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "닉네임"); - applicationRepository.save(application); - - // request - 요청 - VerifyStatusResponse response = RestAssured.given().log().all() - .header("Authorization", "Bearer " + accessToken) - .when().get("/application/status") - .then().log().all() - .statusCode(200) - .extract().as(VerifyStatusResponse.class); - - assertAll( - () -> assertThat(response.status()).isEqualTo("SUBMITTED_PENDING"), - () -> assertThat(response.updateCount()).isZero() - ); - } - - @Test - void 성적과_대학을_모두_제출했지만_승인이_반려된_상태를_반환한다() { - // setUp - 성적과 대학을 모두 제출했지만, 승인 거절 - Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest(),term); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보,"닉네임"); - application.setVerifyStatus(VerifyStatus.REJECTED); - applicationRepository.save(application); - - // request - 요청 - VerifyStatusResponse response = RestAssured.given().log().all() - .header("Authorization", "Bearer " + accessToken) - .when().get("/application/status") - .then().log().all() - .statusCode(200) - .extract().as(VerifyStatusResponse.class); - - assertAll( - () -> assertThat(response.status()).isEqualTo("SUBMITTED_REJECTED"), - () -> assertThat(response.updateCount()).isZero() - ); - } - - @Test - void 성적과_대학을_모두_제출했으며_승인이_된_상태를_반환한다() { - // setUp - 성적과 대학을 모두 제출했으며, 승인이 된 상태 - Application application = new Application(siteUser, createDummyGpa(), createDummyLanguageTest(),term); - application.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 네바다주립대학_라스베이거스_지원_정보, "닉네임"); - application.setVerifyStatus(VerifyStatus.APPROVED); - applicationRepository.save(application); - - // request - 요청 - VerifyStatusResponse response = RestAssured.given().log().all() - .header("Authorization", "Bearer " + accessToken) - .when().get("/application/status") - .then().log().all() - .statusCode(200) - .extract().as(VerifyStatusResponse.class); - - assertAll( - () -> assertThat(response.status()).isEqualTo("SUBMITTED_APPROVED"), - () -> assertThat(response.updateCount()).isZero() - ); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java new file mode 100644 index 000000000..e3fa680c2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java @@ -0,0 +1,93 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ActiveProfiles("test") +@DisplayName("학점 레포지토리 테스트") +@Transactional +public class GpaScoreRepositoryTest { + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private GpaScoreRepository gpaScoreRepository; + + private SiteUser siteUser; + + @BeforeEach + public void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + @Test + public void 사용자의_학점을_조회한다_기존이력_없을_때() { + Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUser(siteUser); + assertThat(gpaScoreBySiteUser).isEqualTo(Optional.empty()); + } + + @Test + public void 사용자의_학점을_조회한다_기존이력_있을_때() { + GpaScore gpaScore = new GpaScore( + new Gpa(4.5, 4.5, "http://example.com/gpa-report.pdf"), + siteUser, + LocalDate.of(2024, 10, 10) + ); + gpaScore.setSiteUser(siteUser); + gpaScoreRepository.save(gpaScore); + + Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUser(siteUser); + assertThat(gpaScoreBySiteUser).isEqualTo(Optional.of(gpaScore)); + } + + @Test + public void 아이디와_사용자정보로_사용자의_학점을_조회한다_기존이력_없을_때() { + Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, 1L); + assertThat(gpaScoreBySiteUser).isEqualTo(Optional.empty()); + } + + @Test + public void 아이디와_사용자정보로_사용자의_학점을_조회한다_기존이력_있을_때() { + GpaScore gpaScore = new GpaScore( + new Gpa(4.5, 4.5, "http://example.com/gpa-report.pdf"), + siteUser, + LocalDate.of(2024, 10, 10) + ); + gpaScore.setSiteUser(siteUser); + gpaScoreRepository.save(gpaScore); + + Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScore.getId()); + assertThat(gpaScoreBySiteUser).isEqualTo(Optional.of(gpaScore)); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java new file mode 100644 index 000000000..7369f20fa --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java @@ -0,0 +1,98 @@ +package com.example.solidconnection.unit.repository; + +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ActiveProfiles("test") +@DisplayName("어학성적 레포지토리 테스트") +@Transactional +public class LanguageTestScoreRepositoryTest { + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + + private SiteUser siteUser; + + @BeforeEach + public void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + @Test + public void 사용자의_어학성적을_조회한다_기존이력_없을_때() { + Optional languageTestScore = languageTestScoreRepository + .findLanguageTestScoreBySiteUserAndLanguageTest_LanguageTestType(siteUser, LanguageTestType.TOEIC); + assertThat(languageTestScore).isEqualTo(Optional.empty()); + } + + @Test + public void 사용자의_어학성적을_조회한다_기존이력_있을_때() { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "990", "http://example.com/gpa-report.pdf"), + LocalDate.of(2024, 10, 10), + siteUser + ); + languageTestScore.setSiteUser(siteUser); + languageTestScoreRepository.save(languageTestScore); + + Optional languageTestScore1 = languageTestScoreRepository + .findLanguageTestScoreBySiteUserAndLanguageTest_LanguageTestType(siteUser, LanguageTestType.TOEIC); + assertThat(languageTestScore1).isEqualTo(Optional.of(languageTestScore)); + } + + @Test + public void 아이디와_사용자정보로_사용자의_어학성적을_조회한다_기존이력_없을_때() { + Optional languageTestScore = languageTestScoreRepository + .findLanguageTestScoreBySiteUserAndId(siteUser, 1L); + assertThat(languageTestScore).isEqualTo(Optional.empty()); + } + + @Test + public void 아이디와_사용자정보로_사용자의_어학성적을_조회한다_기존이력_있을_때() { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "990", "http://example.com/gpa-report.pdf"), + LocalDate.of(2024, 10, 10), + siteUser + ); + languageTestScore.setSiteUser(siteUser); + languageTestScoreRepository.save(languageTestScore); + + Optional languageTestScore1 = languageTestScoreRepository + .findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScore.getId()); + assertThat(languageTestScore1).isEqualTo(Optional.of(languageTestScore)); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java index 7688478fc..dd87a383f 100644 --- a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java @@ -3,11 +3,16 @@ import com.example.solidconnection.application.domain.Application; import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.application.dto.ScoreRequest; +import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.dto.UniversityChoiceRequest; import com.example.solidconnection.application.repository.ApplicationRepository; import com.example.solidconnection.application.service.ApplicationSubmissionService; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.*; @@ -16,19 +21,15 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.beans.factory.annotation.Value; +import java.time.LocalDate; import java.util.Optional; -import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; -import static com.example.solidconnection.custom.exception.ErrorCode.SCORE_SHOULD_SUBMITTED_FIRST; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -41,27 +42,28 @@ public class ApplicationServiceTest { @Mock ApplicationRepository applicationRepository; @Mock + UniversityInfoForApplyRepository universityInfoForApplyRepository; + @Mock SiteUserRepository siteUserRepository; @Mock - UniversityInfoForApplyRepository universityInfoForApplyRepository; + GpaScoreRepository gpaScoreRepository; + @Mock + LanguageTestScoreRepository languageTestScoreRepository; + @Value("${university.term}") + private String term; private SiteUser siteUser; - private Application application; - private Application applicationBeforeTerm; - - private String term = "2024-1"; - private String beforeTerm = "1999-1"; + private GpaScore gpaScore; + private LanguageTestScore languageTestScore; + private final long gpaScoreId = 1L; + private final long languageTestScoreId = 1L; + private final long firstChoiceUniversityId = 1L; + private final long secondChoiceUniversityId = 2L; + private final long thirdChoiceUniversityId = 3L; @BeforeEach void setUp() { - ReflectionTestUtils.setField(applicationSubmissionService, "term", term); // 테스트시 @value값 주입위함 - siteUser = createSiteUser(); - application = createApplication(term); - applicationBeforeTerm = createApplication(beforeTerm); - } - - private SiteUser createSiteUser() { - return new SiteUser( + siteUser = new SiteUser( "test@example.com", "nickname", "profileImageUrl", @@ -70,154 +72,188 @@ private SiteUser createSiteUser() { Role.MENTEE, Gender.MALE ); - } - - private Application createApplication(String term) { - return new Application( + gpaScore = new GpaScore( + new Gpa(4.3, 4.5, "gpaScoreUrl"), siteUser, - new Gpa(4.0, 4.5, "url"), - new LanguageTest(LanguageTestType.TOEIC, "900", "url"), - term + LocalDate.of(2024, 10, 30) + ); + languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "990", "languageTestScoreUrl"), + LocalDate.of(2024, 10, 30), + siteUser ); } @Test - void 성적을_제출한다_금학기_제출이력_없음() { + void 지원한다_기존_이력_없음() { // Given - ScoreRequest scoreRequest = new ScoreRequest( - LanguageTestType.TOEIC, "990", "url", 4.5, 4.5, "url" + ApplyRequest applyRequest = new ApplyRequest( + gpaScoreId, + languageTestScoreId, + new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) ); when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); + languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); + when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.of(languageTestScore)); when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.empty()); // When - applicationSubmissionService.submitScore(siteUser.getEmail(), scoreRequest); + boolean result = applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); // Then + assertThat(result).isEqualTo(true); verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(applicationRepository, times(1)).findBySiteUserAndTerm(siteUser, term); + verify(gpaScoreRepository, times(1)).findGpaScoreBySiteUserAndId(siteUser, gpaScoreId); + verify(languageTestScoreRepository, times(1)).findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId); verify(applicationRepository, times(1)).save(any(Application.class)); } @Test - void 성적을_제출한다_금학기_제출이력_있음() { + void 지원한다_기존_이력_있음() { // Given - ScoreRequest scoreRequest = new ScoreRequest( - LanguageTestType.TOEIC, "990", "url", 4.5, 4.5, "url" + Application beforeApplication = new Application( + siteUser, + new Gpa(4.5, 4.5, "beforeGpaScoreUrl"), + new LanguageTest(LanguageTestType.TOEIC, "900", "beforeLanguageTestUrl"), + term + ); + beforeApplication.setVerifyStatus(VerifyStatus.APPROVED); + ApplyRequest applyRequest = new ApplyRequest( + gpaScoreId, + languageTestScoreId, + new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.of(application)); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, 1L)).thenReturn(Optional.of(gpaScore)); + languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); + when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, 1L)).thenReturn(Optional.of(languageTestScore)); + when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.of(beforeApplication)); // When - applicationSubmissionService.submitScore(siteUser.getEmail(), scoreRequest); + boolean result = applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); // Then - assertEquals(application.getGpa().getGpa(), scoreRequest.gpa()); - assertEquals(application.getLanguageTest().getLanguageTestScore(), scoreRequest.languageTestScore()); + assertThat(result).isEqualTo(true); verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(gpaScoreRepository, times(1)).findGpaScoreBySiteUserAndId(siteUser, gpaScoreId); + verify(languageTestScoreRepository, times(1)).findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId); verify(applicationRepository, times(1)).findBySiteUserAndTerm(siteUser, term); - verify(applicationRepository, times(0)).save(any(Application.class)); + verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(firstChoiceUniversityId, term); + verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(secondChoiceUniversityId, term); + verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(thirdChoiceUniversityId, term); + verify(applicationRepository, times(1)).save(any(Application.class)); } - /** - * 지망대학 제출 - */ @Test - void 지망대학_제출할_때_성적_제출이력이_없다면_예외_응답을_반환한다() { + void 지원할_때_존재하지_않는_학점이라면_예외_응답을_반환한다() { // given - UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( - 1L, 2L, 3L + ApplyRequest applyRequest = new ApplyRequest( + gpaScoreId, + languageTestScoreId, + new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) ); - when(applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail())) - .thenReturn(Optional.empty()); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.empty()); // when, then CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); }); assertThat(exception.getMessage()) - .isEqualTo(SCORE_SHOULD_SUBMITTED_FIRST.getMessage()); + .isEqualTo(ErrorCode.INVALID_GPA_SCORE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(SCORE_SHOULD_SUBMITTED_FIRST.getCode()); + .isEqualTo(ErrorCode.INVALID_GPA_SCORE.getCode()); } @Test - void 지망대학_제출한다_이전학기_성적_제출이력_있음() { - // Given - UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( - 1L, 2L, 3L + void 지원할_때_승인되지_않은_학점이라면_예외_응답을_반환한다() { + // given + ApplyRequest applyRequest = new ApplyRequest( + gpaScoreId, + languageTestScoreId, + new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) ); - when(applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail())) - .thenReturn(Optional.of(applicationBeforeTerm)); when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + gpaScore.setVerifyStatus(VerifyStatus.REJECTED); + when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - // When - applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); - - // Then - verify(applicationRepository, times(1)).findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail()); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(applicationRepository, times(1)).save(any(Application.class)); + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); + }); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_GPA_SCORE_STATUS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_GPA_SCORE_STATUS.getCode()); } @Test - void 지망대학_제출한다_금학기_성적_제출이력_있음() { - // Given - UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( - 1L, 2L, 3L + void 지원할_때_존재하지_않는_어학성적이라면_예외_응답을_반환한다() { + // given + ApplyRequest applyRequest = new ApplyRequest( + gpaScoreId, + languageTestScoreId, + new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) ); - when(applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail())) - .thenReturn(Optional.of(application)); - - // When - applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); + when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.empty()); - // Then - verify(applicationRepository, times(1)).findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail()); - verify(siteUserRepository, times(0)).getByEmail(siteUser.getEmail()); - verify(applicationRepository, times(0)).save(any(Application.class)); + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); + }); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE.getCode()); } - @ParameterizedTest - @CsvSource({ - "1, 2, 3", - "1, , 3", - "1, 2, ", - "1, , " - }) - void 지망대학_제출할_때_2지망과_3지망은_NULL_허용한다(Long firstChoice, Long secondChoice, Long thirdChoice) { - // Given - UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest(firstChoice, secondChoice, thirdChoice); - when(applicationRepository.findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail())) - .thenReturn(Optional.of(application)); - - // When - applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + @Test + void 지원할_때_승인되지_않은_어학성적이라면_예외_응답을_반환한다() { + // given + ApplyRequest applyRequest = new ApplyRequest( + gpaScoreId, + languageTestScoreId, + new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); + languageTestScore.setVerifyStatus(VerifyStatus.REJECTED); + when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.of(languageTestScore)); - // Then - verify(applicationRepository, times(1)).findTop1BySiteUser_EmailOrderByTermDesc(siteUser.getEmail()); - verify(siteUserRepository, times(0)).getByEmail(siteUser.getEmail()); - verify(applicationRepository, times(0)).save(any(Application.class)); + // when, then + CustomException exception = assertThrows(CustomException.class, () -> { + applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); + }); + assertThat(exception.getMessage()) + .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); + assertThat(exception.getCode()) + .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS.getCode()); } - @ParameterizedTest - @CsvSource({ - "1, 1, 1", - "1, 2, 1", - "1, 1, 2", - "1, , 1", - "1, 1, " - }) - void 지망대학_제출할_때_선택지가_중복된다면_예외_응답을_반환한다(Long firstChoice, Long secondChoice, Long thirdChoice) { + @Test + void 지원할_때_학교_선택이_중복되면_예외_응답을_반환한다() { // given - UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest(firstChoice, secondChoice, thirdChoice); + ApplyRequest applyRequest = new ApplyRequest( + gpaScoreId, + languageTestScoreId, + new UniversityChoiceRequest(firstChoiceUniversityId, firstChoiceUniversityId, firstChoiceUniversityId) + ); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); // when, then CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.submitUniversityChoice(siteUser.getEmail(), universityChoiceRequest); + applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); }); assertThat(exception.getMessage()) - .isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + .isEqualTo(ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); assertThat(exception.getCode()) - .isEqualTo(CANT_APPLY_FOR_SAME_UNIVERSITY.getCode()); + .isEqualTo(ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY.getCode()); } } diff --git a/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java new file mode 100644 index 000000000..39deadb54 --- /dev/null +++ b/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java @@ -0,0 +1,201 @@ +package com.example.solidconnection.unit.service; + +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.dto.*; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +import com.example.solidconnection.score.service.ScoreService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("점수 서비스 테스트") +public class ScoreServiceTest { + @InjectMocks + ScoreService scoreService; + @Mock + GpaScoreRepository gpaScoreRepository; + @Mock + LanguageTestScoreRepository languageTestScoreRepository; + @Mock + SiteUserRepository siteUserRepository; + + private SiteUser siteUser; + private GpaScore beforeGpaScore; + private GpaScore beforeGpaScore2; + private LanguageTestScore beforeLanguageTestScore; + private LanguageTestScore beforeLanguageTestScore2; + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + beforeGpaScore = createBeforeGpaScore(siteUser, 4.5); + beforeGpaScore2 = createBeforeGpaScore(siteUser, 4.3); + beforeLanguageTestScore = createBeforeLanguageTestScore(siteUser); + beforeLanguageTestScore2 = createBeforeLanguageTestScore2(siteUser); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private GpaScore createBeforeGpaScore(SiteUser siteUser, Double gpa) { + return new GpaScore( + new Gpa(gpa, 4.5, "http://example.com/gpa-report.pdf"), + siteUser, + LocalDate.of(2024, 10, 20) + ); + } + + private LanguageTestScore createBeforeLanguageTestScore(SiteUser siteUser) { + return new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "900", "http://example.com/gpa-report.pdf"), + LocalDate.of(2024, 10, 30), + siteUser + ); + } + + private LanguageTestScore createBeforeLanguageTestScore2(SiteUser siteUser) { + return new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEFL_IBT, "100", "http://example.com/gpa-report.pdf"), + LocalDate.of(2024, 10, 30), + siteUser + ); + } + + @Test + void 학점을_등록한다_기존이력이_없을_때() { + // Given + GpaScoreRequest gpaScoreRequest = new GpaScoreRequest( + 4.5, 4.5, LocalDate.of(2024, 10, 20), "http://example.com/gpa-report.pdf" + ); + GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser, gpaScoreRequest.issueDate()); + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(gpaScoreRepository.save(newGpaScore)).thenReturn(newGpaScore); + + // 새로운 gpa 저장하게된다. + scoreService.submitGpaScore(siteUser.getEmail(), gpaScoreRequest); + + // Then + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(gpaScoreRepository, times(1)).save(any(GpaScore.class)); + } + + @Test + void 어학성적을_등록한다_기존이력이_없을_때() { + // Given + LanguageTestScoreRequest languageTestScoreRequest = new LanguageTestScoreRequest( + LanguageTestType.TOEIC, "900", + LocalDate.of(2024, 10, 30), "http://example.com/gpa-report.pdf" + ); + LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); + LanguageTestScore languageTestScore = new LanguageTestScore(languageTest, LocalDate.of(2024, 10, 30), siteUser); + + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + when(languageTestScoreRepository.save(any(LanguageTestScore.class))).thenReturn(languageTestScore); + + //when + scoreService.submitLanguageTestScore(siteUser.getEmail(), languageTestScoreRequest); + + // Then + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + verify(languageTestScoreRepository, times(1)).save(any(LanguageTestScore.class)); + } + + @Test + void 학점이력을_조회한다_제출이력이_있을_때() { + // Given + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + beforeGpaScore.setSiteUser(siteUser); + beforeGpaScore2.setSiteUser(siteUser); + + // when + GpaScoreStatusResponse gpaScoreStatusResponse = scoreService.getGpaScoreStatus(siteUser.getEmail()); + + // Then + List expectedStatusList = List.of( + GpaScoreStatus.from(beforeGpaScore), + GpaScoreStatus.from(beforeGpaScore2) + ); + assertThat(gpaScoreStatusResponse.gpaScoreStatusList()) + .hasSize(2) + .containsExactlyElementsOf(expectedStatusList); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + } + + @Test + void 학점이력을_조회한다_제출이력이_없을_때() { + // Given + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + + // when + GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser.getEmail()); + + // Then + assertThat(gpaScoreStatus.gpaScoreStatusList()).isEmpty(); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + } + + + @Test + void 어학이력을_조회한다_제출이력이_있을_때() { + // Given + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + beforeLanguageTestScore.setSiteUser(siteUser); + beforeLanguageTestScore2.setSiteUser(siteUser); + + // when + LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser.getEmail()); + + // Then + List expectedStatusList = List.of( + LanguageTestScoreStatus.from(beforeLanguageTestScore), + LanguageTestScoreStatus.from(beforeLanguageTestScore2) + ); + assertThat(languageTestScoreStatus.languageTestScoreStatusList()) + .hasSize(2) + .containsExactlyElementsOf(expectedStatusList); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + } + + @Test + void 어학이력을_조회한다_제출이력이_없을_때() { + // Given + when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); + + // when + LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser.getEmail()); + + // Then + assertThat(languageTestScoreStatus.languageTestScoreStatusList()).isEmpty(); + verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); + } +} From cfcda24ae8ced8d7e3617bcf68d2fa5d748cb9bb Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 10 Dec 2024 00:13:43 +0900 Subject: [PATCH 110/158] =?UTF-8?q?style:=20=EC=BB=A8=EB=B2=A4=EC=85=98=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20(#110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style: dto 개행 통일 * refactor: dto 패키지 변경 - post 패키지에서만 쓰이는 dto를 post.dto 안으로 이동 * refactor: 안쓰는 코드 삭제 * style: 레포지토리 컨벤션 통일 - 주석 삭제 - 함수 나열 순서 통일 - 개행 통일 * style: 컨벤션 통일 - asterisk(*) 제거 - 개행 통일 * style: 개행 컨벤션 통일 * style: 사용하지 않는 코드 삭제 * style: 함수 선언 위치 통일 --- .../controller/ApplicationController.java | 4 +- .../application/domain/Application.java | 11 +++- .../application/dto/ApplicantResponse.java | 2 - .../dto/ApplicationSubmissionResponse.java | 1 - .../application/dto/ApplicationsResponse.java | 3 +- .../application/dto/ScoreRequest.java | 51 ---------------- .../dto/UniversityChoiceRequest.java | 4 +- .../application/dto/VerifyStatusResponse.java | 13 ---- .../repository/ApplicationRepository.java | 18 ++++-- .../service/ApplicationQueryService.java | 1 + .../service/ApplicationSubmissionService.java | 17 ++++-- .../application/service/NicknameCreator.java | 22 +++---- .../auth/client/KakaoOAuthClient.java | 4 ++ .../auth/dto/SignInResponse.java | 1 - .../auth/dto/kakao/FirstAccessResponse.java | 1 - .../board/controller/BoardController.java | 6 +- .../solidconnection/board/domain/Board.java | 9 ++- .../board/service/BoardService.java | 28 ++++----- .../cache/CacheUpdateListener.java | 1 + .../solidconnection/cache/CachingAspect.java | 3 +- .../cache/CompletableFutureManager.java | 3 +- .../cache/ThunderingHerdCachingAspect.java | 12 +++- .../cache/annotation/DefaultCacheOut.java | 3 + .../cache/annotation/DefaultCaching.java | 3 + .../annotation/ThunderingHerdCaching.java | 2 + .../cache/manager/CacheManager.java | 4 ++ .../cache/manager/CustomCacheManager.java | 3 +- .../comment/controller/CommentController.java | 18 ++++-- .../comment/domain/Comment.java | 12 +++- .../comment/dto/CommentCreateRequest.java | 1 + .../comment/dto/CommentUpdateRequest.java | 1 - .../comment/dto/PostFindCommentResponse.java | 2 +- .../comment/repository/CommentRepository.java | 1 + .../comment/service/CommentService.java | 59 +++++++++++-------- .../config/redis/RedisConfig.java | 1 - .../config/scheduler/SchedulerConfig.java | 1 + .../security/JwtAuthenticationFilter.java | 1 + .../config/swagger/SwaggerConfig.java | 2 +- .../config/sync/AsyncConfig.java | 1 - .../config/token/TokenValidator.java | 42 ++++++------- .../custom/exception/CustomException.java | 1 + .../custom/exception/ErrorCode.java | 4 +- .../solidconnection/entity/PostImage.java | 10 +++- .../post/controller/PostController.java | 19 +++++- .../solidconnection/post/domain/Post.java | 17 +++++- .../solidconnection/post/domain/PostLike.java | 11 +++- .../post/dto/BoardFindPostResponse.java | 12 ++-- .../post/dto/PostCreateRequest.java | 3 + .../dto/PostFindPostImageResponse.java | 2 +- .../post/dto/PostFindResponse.java | 1 - .../post/dto/PostLikeResponse.java | 2 - .../post/dto/PostUpdateRequest.java | 2 + .../post/repository/PostLikeRepository.java | 2 +- .../post/repository/PostRepository.java | 36 ++++++----- .../post/service/PostService.java | 23 ++++++-- .../repositories/CountryRepository.java | 11 ---- .../repositories/RegionRepository.java | 8 --- .../solidconnection/s3/FileUploadService.java | 1 + .../solidconnection/s3/S3Controller.java | 10 +++- .../example/solidconnection/s3/S3Service.java | 10 +++- .../scheduler/UpdateViewCountScheduler.java | 2 +- .../score/controller/ScoreController.java | 11 +++- .../score/domain/GpaScore.java | 12 +++- .../score/domain/LanguageTestScore.java | 12 +++- .../score/repository/GpaScoreRepository.java | 1 + .../score/service/ScoreService.java | 7 ++- .../solidconnection/service/RedisService.java | 6 +- .../controller/SiteUserController.java | 13 +++- .../controller/SiteUserControllerSwagger.java | 3 - .../siteuser/domain/SiteUser.java | 16 ++++- .../siteuser/dto/MyPageResponse.java | 1 - .../siteuser/dto/MyPageUpdateResponse.java | 12 ++-- .../siteuser/service/SiteUserService.java | 47 ++++++++------- .../university/domain/University.java | 7 ++- .../dto/UniversityDetailResponse.java | 1 - ...UniversityInfoForApplyPreviewResponse.java | 4 +- .../UniversityFilterRepositoryImpl.java | 4 +- .../university/service/UniversityService.java | 6 +- .../solidconnection/util/RedisUtils.java | 7 ++- src/main/resources/static/index.html | 3 - .../unit/service/PostServiceTest.java | 2 +- 81 files changed, 439 insertions(+), 295 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/application/dto/ScoreRequest.java delete mode 100644 src/main/java/com/example/solidconnection/application/dto/VerifyStatusResponse.java rename src/main/java/com/example/solidconnection/{ => post}/dto/PostFindPostImageResponse.java (93%) delete mode 100644 src/main/resources/static/index.html diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 8baf7255f..6242180aa 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -1,6 +1,8 @@ package com.example.solidconnection.application.controller; -import com.example.solidconnection.application.dto.*; +import com.example.solidconnection.application.dto.ApplicationSubmissionResponse; +import com.example.solidconnection.application.dto.ApplicationsResponse; +import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.service.ApplicationQueryService; import com.example.solidconnection.application.service.ApplicationSubmissionService; import jakarta.validation.Valid; diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java index 0c56fd7f5..7faf77e6e 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Application.java +++ b/src/main/java/com/example/solidconnection/application/domain/Application.java @@ -3,7 +3,16 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java index d03f1c9a3..b6b5ee477 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java @@ -2,12 +2,10 @@ import com.example.solidconnection.application.domain.Application; import com.example.solidconnection.type.LanguageTestType; - import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "지원자") public record ApplicantResponse( - @Schema(description = "닉네임", example = "행복한 개발자") String nicknameForApply, diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java index 279f2b150..fe112a8d0 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java @@ -4,7 +4,6 @@ @Schema(description = "지원 정보 제출 성공 여부") public record ApplicationSubmissionResponse( - @Schema(description = "제출 성공 여부", example = "true") boolean isSuccess) { } diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java index 2e3025137..3d5e2ca88 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java @@ -5,9 +5,8 @@ import java.util.List; -@Schema(description = "1지망과 2지망 대학과 그 대학에 지원한 지원자 정보") +@Schema(description = "지망별 지원자 목록") public record ApplicationsResponse( - @ArraySchema(arraySchema = @Schema(description = "1지망 대학에 지원한 지원자 목록")) List firstChoice, diff --git a/src/main/java/com/example/solidconnection/application/dto/ScoreRequest.java b/src/main/java/com/example/solidconnection/application/dto/ScoreRequest.java deleted file mode 100644 index 1f17be430..000000000 --- a/src/main/java/com/example/solidconnection/application/dto/ScoreRequest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.example.solidconnection.application.dto; - - -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.type.LanguageTestType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "대학 성적과 어학 시험 성적") -public record ScoreRequest( - @NotNull(message = "어학 종류를 입력해주세요.") - @Schema(description = "어학 시험 종류", example = "TOEFL", required = true) - LanguageTestType languageTestType, - - @NotBlank(message = "어학 점수를 입력해주세요.") - @Schema(description = "어학 시험 점수", example = "115", required = true) - String languageTestScore, - - @NotBlank(message = "어학 증명서를 첨부해주세요.") - @Schema(description = "어학 증명서 URL", example = "http://example.com/test-report.pdf", required = true) - String languageTestReportUrl, - - @NotNull(message = "학점을 입력해주세요.") - @Schema(description = "GPA", example = "3.5", required = true) - Double gpa, - - @NotNull(message = "학점 기준을 입력해주세요.") - @Schema(description = "GPA 계산 기준", example = "4.0", required = true) - Double gpaCriteria, - - @NotBlank(message = "대학 성적 증명서를 첨부해주세요.") - @Schema(description = "대학 성적 증명서 URL", example = "http://example.com/gpa-report.pdf", required = true) - String gpaReportUrl) { - - public Gpa toGpa() { - return new Gpa( - this.gpa, - this.gpaCriteria, - this.gpaReportUrl); - } - - public LanguageTest toLanguageTest() { - return new LanguageTest( - this.languageTestType, - this.languageTestScore, - this.languageTestReportUrl - ); - } -} diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java index a76799571..033de880a 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java @@ -5,7 +5,6 @@ @Schema(description = "지망 대학") public record UniversityChoiceRequest( - @NotNull(message = "1지망 대학교를 입력해주세요.") @Schema(description = "1지망 대학교의 지원 정보 ID", example = "1") Long firstChoiceUniversityId, @@ -14,4 +13,5 @@ public record UniversityChoiceRequest( Long secondChoiceUniversityId, @Schema(description = "3지망 대학교의 지원 정보 ID (선택사항)", example = "3", nullable = true) - Long thirdChoiceUniversityId) {} + Long thirdChoiceUniversityId) { +} diff --git a/src/main/java/com/example/solidconnection/application/dto/VerifyStatusResponse.java b/src/main/java/com/example/solidconnection/application/dto/VerifyStatusResponse.java deleted file mode 100644 index 8019e9f8e..000000000 --- a/src/main/java/com/example/solidconnection/application/dto/VerifyStatusResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.solidconnection.application.dto; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "지원 상태와 지망 대학 변경 횟수") -public record VerifyStatusResponse( - - @Schema(description = "지원 상태", example = "SUBMITTED_PENDING") - String status, - - @Schema(description = "지망 대학 변경 횟수", example = "1") - int updateCount) { -} diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java index a3bca9dc2..1a06ec321 100644 --- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java @@ -20,14 +20,22 @@ public interface ApplicationRepository extends JpaRepository boolean existsByNicknameForApply(String nicknameForApply); - @Query("SELECT a FROM Application a WHERE a.siteUser = :siteUser AND a.term = :term AND a.isDelete = false") - Optional findBySiteUserAndTerm(@Param("siteUser") SiteUser siteUser, @Param("term") String term); + List findAllByFirstChoiceUniversityAndVerifyStatusAndTerm( + UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus, String term); - List findAllByFirstChoiceUniversityAndVerifyStatusAndTerm(UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus, String term); + List findAllBySecondChoiceUniversityAndVerifyStatusAndTerm( + UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus, String term); - List findAllBySecondChoiceUniversityAndVerifyStatusAndTerm(UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus, String term); + List findAllByThirdChoiceUniversityAndVerifyStatusAndTerm( + UniversityInfoForApply thirdChoiceUniversity, VerifyStatus verifyStatus, String term); - List findAllByThirdChoiceUniversityAndVerifyStatusAndTerm(UniversityInfoForApply thirdChoiceUniversity, VerifyStatus verifyStatus, String term); + @Query(""" + SELECT a FROM Application a + WHERE a.siteUser = :siteUser + AND a.term = :term + AND a.isDelete = false + """) + Optional findBySiteUserAndTerm(@Param("siteUser") SiteUser siteUser, @Param("term") String term); default Application getApplicationBySiteUserAndTerm(SiteUser siteUser, String term) { return findBySiteUserAndTerm(siteUser, term) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 66ae84918..68cf9c0aa 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -36,6 +36,7 @@ public class ApplicationQueryService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final SiteUserRepository siteUserRepository; private final UniversityFilterRepositoryImpl universityFilterRepository; + @Value("${university.term}") public String term; diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index 6b263996e..f82e9ad76 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -1,14 +1,14 @@ package com.example.solidconnection.application.service; -import com.example.solidconnection.application.domain.*; +import com.example.solidconnection.application.domain.Application; import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.dto.UniversityChoiceRequest; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.score.repository.GpaScoreRepository; -import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; @@ -19,9 +19,16 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.*; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; +import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE_STATUS; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS; @RequiredArgsConstructor @Service diff --git a/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java b/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java index 21a36dfab..d9243ce39 100644 --- a/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java +++ b/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java @@ -9,18 +9,20 @@ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) class NicknameCreator { - public static final List ADJECTIVES = List.copyOf( - Set.of("기쁜", "행복한", "즐거운", "밝은", "따뜻한", "시원한", "고고한", "예쁜", "신선한", "풍부한", "깨끗한", - "귀한", "눈부신", "멋진", "고귀한", "화려한", "상큼한", "활기찬", "유쾌한", "똘똘한", "친절한", "좋은", - "영리한", "용감한", "정직한", "성실한", "강인한", "귀여운", "순수한", "희망찬", "발랄한", "나른한", "후한", "빛나는", - "따스한", "안락한", "편안한", "성공한", "재미난", "청량한", "찬란한", "소중한", "특별한", "단순한", "반가운", "그리운") + public static final List ADJECTIVES = List.copyOf(Set.of( + "기쁜", "행복한", "즐거운", "밝은", "따뜻한", "시원한", "고고한", "예쁜", "신선한", "풍부한", "깨끗한", + "귀한", "눈부신", "멋진", "고귀한", "화려한", "상큼한", "활기찬", "유쾌한", "똘똘한", "친절한", "좋은", + "영리한", "용감한", "정직한", "성실한", "강인한", "귀여운", "순수한", "희망찬", "발랄한", "나른한", "후한", "빛나는", + "따스한", "안락한", "편안한", "성공한", "재미난", "청량한", "찬란한", "소중한", "특별한", "단순한", "반가운", "그리운") ); - public static final List NOUNS = List.copyOf( - Set.of("청춘", "토끼", "기사", "곰", "사슴", "여우", "팬더", "이슬", "새싹", "햇빛", "나비", "별", "달", "구름", - "사탕", "젤리", "마법", "풍선", "캔디", "초코", "인형", "쿠키", "요정", "장미", "마녀", "보물", "꽃", "보석", - "달빛", "오리", "날개", "여행", "편지", "불꽃", "파도", "별빛", "구슬", "노래", "음표", "선율", "미소", "가방", "거울", - "씨앗", "열매", "바다", "약속", "구두", "공기", "등불", "촛불", "진주", "꿀벌", "예감", "바람", "오전", "오후", "아침", "점심", "저녁") + public static final List NOUNS = List.copyOf(Set.of( + "청춘", "토끼", "기사", "곰", "사슴", "여우", "팬더", "이슬", "새싹", "햇빛", "나비", "별", "달", "구름", + "사탕", "젤리", "마법", "풍선", "캔디", "초코", "인형", "쿠키", "요정", "장미", "마녀", "보물", "꽃", "보석", + "달빛", "오리", "날개", "여행", "편지", "불꽃", "파도", "별빛", "구슬", "노래", "음표", "선율", "미소", "가방", + "거울", "씨앗", "열매", "바다", "약속", "구두", "공기", "등불", "촛불", "진주", "꿀벌", "예감", "바람", + "오전", "오후", "아침", "점심", "저녁") ); + private static final Random RANDOM = new Random(); public static String createRandomNickname() { diff --git a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java index fa9d1f265..9862d0074 100644 --- a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java +++ b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java @@ -25,12 +25,16 @@ public class KakaoOAuthClient { private final RestTemplate restTemplate; + @Value("${kakao.redirect_uri}") public String redirectUri; + @Value("${kakao.client_id}") private String clientId; + @Value("${kakao.token_url}") private String tokenUrl; + @Value("${kakao.user_info_url}") private String userInfoUrl; diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java index ec89bbf16..41d9425cb 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java @@ -1,7 +1,6 @@ package com.example.solidconnection.auth.dto; import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; - import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "로그인 응답 데이터") diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java index 2f777c3b7..d766099ed 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java @@ -4,7 +4,6 @@ @Schema(description = "등록되지 않은 사용자의 최초 접속 시 응답 데이터") public record FirstAccessResponse( - @Schema(description = "사용자 등록 여부", example = "false") boolean isRegistered, diff --git a/src/main/java/com/example/solidconnection/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/board/controller/BoardController.java index 29cfc249a..1777603cd 100644 --- a/src/main/java/com/example/solidconnection/board/controller/BoardController.java +++ b/src/main/java/com/example/solidconnection/board/controller/BoardController.java @@ -7,7 +7,11 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/example/solidconnection/board/domain/Board.java b/src/main/java/com/example/solidconnection/board/domain/Board.java index 007553367..77d0aada8 100644 --- a/src/main/java/com/example/solidconnection/board/domain/Board.java +++ b/src/main/java/com/example/solidconnection/board/domain/Board.java @@ -1,8 +1,13 @@ package com.example.solidconnection.board.domain; import com.example.solidconnection.post.domain.Post; -import jakarta.persistence.*; -import lombok.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/example/solidconnection/board/service/BoardService.java b/src/main/java/com/example/solidconnection/board/service/BoardService.java index 1ec5ac8b0..2513e0903 100644 --- a/src/main/java/com/example/solidconnection/board/service/BoardService.java +++ b/src/main/java/com/example/solidconnection/board/service/BoardService.java @@ -23,6 +23,18 @@ public class BoardService { private final BoardRepository boardRepository; + @Transactional(readOnly = true) + public List findPostsByCodeAndPostCategory(String code, String category) { + + String boardCode = validateCode(code); + PostCategory postCategory = validatePostCategory(category); + + Board board = boardRepository.getByCodeUsingEntityGraph(boardCode); + List postList = getPostListByPostCategory(board.getPostList(), postCategory); + + return BoardFindPostResponse.from(postList); + } + private String validateCode(String code) { try { return String.valueOf(BoardCode.valueOf(code)); @@ -31,25 +43,13 @@ private String validateCode(String code) { } } - private PostCategory validatePostCategory(String category){ - if(!EnumUtils.isValidEnum(PostCategory.class, category)){ + private PostCategory validatePostCategory(String category) { + if (!EnumUtils.isValidEnum(PostCategory.class, category)) { throw new CustomException(INVALID_POST_CATEGORY); } return PostCategory.valueOf(category); } - @Transactional(readOnly = true) - public List findPostsByCodeAndPostCategory(String code, String category) { - - String boardCode = validateCode(code); - PostCategory postCategory = validatePostCategory(category); - - Board board = boardRepository.getByCodeUsingEntityGraph(boardCode); - List postList = getPostListByPostCategory(board.getPostList(), postCategory); - - return BoardFindPostResponse.from(postList); - } - private List getPostListByPostCategory(List postList, PostCategory postCategory) { if (postCategory.equals(PostCategory.전체)) { return postList; diff --git a/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java b/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java index 34e2752b3..c785168b3 100644 --- a/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java +++ b/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java @@ -14,6 +14,7 @@ public class CacheUpdateListener implements MessageListener { private final CompletableFutureManager futureManager; + @Override public void onMessage(Message message, byte[] pattern) { String messageBody = new String(message.getBody(), StandardCharsets.UTF_8).replaceAll("^\"|\"$", ""); diff --git a/src/main/java/com/example/solidconnection/cache/CachingAspect.java b/src/main/java/com/example/solidconnection/cache/CachingAspect.java index 29c355372..816532022 100644 --- a/src/main/java/com/example/solidconnection/cache/CachingAspect.java +++ b/src/main/java/com/example/solidconnection/cache/CachingAspect.java @@ -15,6 +15,7 @@ @Component @RequiredArgsConstructor public class CachingAspect { + private final ApplicationContext applicationContext; private final RedisUtils redisUtils; @@ -47,7 +48,7 @@ public Object cacheEvict(ProceedingJoinPoint joinPoint, DefaultCacheOut defaultC if (usingPrefix) { cacheManager.evictUsingPrefix(cacheKey); - }else{ + } else { cacheManager.evict(cacheKey); } } diff --git a/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java b/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java index 6bcf01e03..48c36b28c 100644 --- a/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java +++ b/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java @@ -2,12 +2,13 @@ import org.springframework.stereotype.Component; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.Map; @Component public class CompletableFutureManager { + private final Map> waitingRequests = new ConcurrentHashMap<>(); public CompletableFuture getOrCreateFuture(String key) { diff --git a/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java b/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java index 8dc1694db..a37e80f51 100644 --- a/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java +++ b/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java @@ -16,14 +16,22 @@ import java.time.Duration; import java.util.UUID; -import java.util.concurrent.*; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; -import static com.example.solidconnection.type.RedisConstants.*; +import static com.example.solidconnection.type.RedisConstants.CREATE_CHANNEL; +import static com.example.solidconnection.type.RedisConstants.LOCK_TIMEOUT_MS; +import static com.example.solidconnection.type.RedisConstants.MAX_WAIT_TIME_MS; +import static com.example.solidconnection.type.RedisConstants.REFRESH_LIMIT_PERCENT; @Aspect @Component @Slf4j public class ThunderingHerdCachingAspect { + private final ApplicationContext applicationContext; private final RedisTemplate redisTemplate; private final CompletableFutureManager futureManager; diff --git a/src/main/java/com/example/solidconnection/cache/annotation/DefaultCacheOut.java b/src/main/java/com/example/solidconnection/cache/annotation/DefaultCacheOut.java index bb1d5b518..2b5c8aada 100644 --- a/src/main/java/com/example/solidconnection/cache/annotation/DefaultCacheOut.java +++ b/src/main/java/com/example/solidconnection/cache/annotation/DefaultCacheOut.java @@ -8,7 +8,10 @@ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DefaultCacheOut { + String[] key(); + String cacheManager(); + boolean prefix() default false; } diff --git a/src/main/java/com/example/solidconnection/cache/annotation/DefaultCaching.java b/src/main/java/com/example/solidconnection/cache/annotation/DefaultCaching.java index 36c45a616..316daab0f 100644 --- a/src/main/java/com/example/solidconnection/cache/annotation/DefaultCaching.java +++ b/src/main/java/com/example/solidconnection/cache/annotation/DefaultCaching.java @@ -8,7 +8,10 @@ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DefaultCaching { + String key(); + String cacheManager(); + long ttlSec(); } diff --git a/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java b/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java index 6772a52e7..c5a9e0e9b 100644 --- a/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java +++ b/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java @@ -9,6 +9,8 @@ @Retention(RetentionPolicy.RUNTIME) public @interface ThunderingHerdCaching { String key(); + String cacheManager(); + long ttlSec(); } diff --git a/src/main/java/com/example/solidconnection/cache/manager/CacheManager.java b/src/main/java/com/example/solidconnection/cache/manager/CacheManager.java index 8c46324e1..3373d1563 100644 --- a/src/main/java/com/example/solidconnection/cache/manager/CacheManager.java +++ b/src/main/java/com/example/solidconnection/cache/manager/CacheManager.java @@ -1,8 +1,12 @@ package com.example.solidconnection.cache.manager; public interface CacheManager { + void put(String key, Object value, Long ttl); + Object get(String key); + void evict(String key); + void evictUsingPrefix(String key); } diff --git a/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java b/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java index 833ed00f7..2e489567c 100644 --- a/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java +++ b/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java @@ -11,6 +11,7 @@ @Component("customCacheManager") public class CustomCacheManager implements CacheManager { + private final RedisTemplate redisTemplate; @Autowired @@ -33,7 +34,7 @@ public void evict(String key) { } public void evictUsingPrefix(String key) { - Set keys = redisTemplate.keys(key+"*"); + Set keys = redisTemplate.keys(key + "*"); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java index 61bae1036..bcb50715c 100644 --- a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java +++ b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java @@ -1,13 +1,23 @@ package com.example.solidconnection.comment.controller; -import com.example.solidconnection.comment.dto.*; +import com.example.solidconnection.comment.dto.CommentCreateRequest; +import com.example.solidconnection.comment.dto.CommentCreateResponse; +import com.example.solidconnection.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.comment.dto.CommentUpdateResponse; import com.example.solidconnection.comment.service.CommentService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; import java.security.Principal; @@ -28,7 +38,6 @@ public ResponseEntity createComment( @PathVariable("post_id") Long postId, @Valid @RequestBody CommentCreateRequest commentCreateRequest ) { - CommentCreateResponse commentCreateResponse = commentService.createComment( principal.getName(), postId, commentCreateRequest); return ResponseEntity.ok().body(commentCreateResponse); @@ -41,7 +50,6 @@ public ResponseEntity updateComment( @PathVariable("comment_id") Long commentId, @Valid @RequestBody CommentUpdateRequest commentUpdateRequest ) { - CommentUpdateResponse commentUpdateResponse = commentService.updateComment( principal.getName(), postId, commentId, commentUpdateRequest ); @@ -54,9 +62,7 @@ public ResponseEntity deleteCommentById( @PathVariable("post_id") Long postId, @PathVariable("comment_id") Long commentId ) { - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById(principal.getName(), postId, commentId); return ResponseEntity.ok().body(commentDeleteResponse); } - } diff --git a/src/main/java/com/example/solidconnection/comment/domain/Comment.java b/src/main/java/com/example/solidconnection/comment/domain/Comment.java index 774c01123..a4d147a61 100644 --- a/src/main/java/com/example/solidconnection/comment/domain/Comment.java +++ b/src/main/java/com/example/solidconnection/comment/domain/Comment.java @@ -3,7 +3,17 @@ import com.example.solidconnection.entity.common.BaseEntity; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.persistence.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Transient; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java index 8cf57e360..c2065685b 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java @@ -10,6 +10,7 @@ public record CommentCreateRequest( @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content, + Long parentId ) { public Comment toEntity(SiteUser siteUser, Post post, Comment parentComment) { diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java index 23ae16118..d99429931 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java @@ -8,5 +8,4 @@ public record CommentUpdateRequest( @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content ) { - } diff --git a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java index 2335b68ad..a0d68066a 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java +++ b/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java @@ -13,8 +13,8 @@ public record PostFindCommentResponse( ZonedDateTime createdAt, ZonedDateTime updatedAt, PostFindSiteUserResponse postFindSiteUserResponse - ) { + public static PostFindCommentResponse from(Boolean isOwner, Comment comment) { return new PostFindCommentResponse( comment.getId(), diff --git a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java index b78011903..ce37c42a1 100644 --- a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java @@ -9,6 +9,7 @@ import java.util.List; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; + public interface CommentRepository extends JpaRepository { @Query(value = """ diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 8c0b0458f..7d25ee5f6 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -1,8 +1,13 @@ package com.example.solidconnection.comment.service; -import com.example.solidconnection.comment.dto.*; -import com.example.solidconnection.comment.repository.CommentRepository; import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.dto.CommentCreateRequest; +import com.example.solidconnection.comment.dto.CommentCreateResponse; +import com.example.solidconnection.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.repository.CommentRepository; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; @@ -15,7 +20,9 @@ import java.util.List; import java.util.stream.Collectors; -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_LEVEL; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; @Service @RequiredArgsConstructor @@ -25,29 +32,6 @@ public class CommentService { private final SiteUserRepository siteUserRepository; private final PostRepository postRepository; - private Boolean isOwner(Comment comment, String email) { - return comment.getSiteUser().getEmail().equals(email); - } - - private void validateOwnership(Comment comment, String email) { - if (!comment.getSiteUser().getEmail().equals(email)) { - throw new CustomException(INVALID_POST_ACCESS); - } - } - - private void validateDeprecated(Comment comment) { - if (comment.getContent() == null) { - throw new CustomException(CAN_NOT_UPDATE_DEPRECATED_COMMENT); - } - } - - // 대대댓글부터 허용하지 않음 - private void validateCommentDepth(Comment parentComment) { - if (parentComment.getParentComment() != null) { - throw new CustomException(INVALID_COMMENT_LEVEL); - } - } - @Transactional(readOnly = true) public List findCommentsByPostId(String email, Long postId) { return commentRepository.findCommentTreeByPostId(postId) @@ -56,6 +40,10 @@ public List findCommentsByPostId(String email, Long pos .collect(Collectors.toList()); } + private Boolean isOwner(Comment comment, String email) { + return comment.getSiteUser().getEmail().equals(email); + } + @Transactional public CommentCreateResponse createComment(String email, Long postId, CommentCreateRequest commentCreateRequest) { @@ -72,6 +60,13 @@ public CommentCreateResponse createComment(String email, Long postId, CommentCre return CommentCreateResponse.from(createdComment); } + // 대대댓글부터 허용하지 않음 + private void validateCommentDepth(Comment parentComment) { + if (parentComment.getParentComment() != null) { + throw new CustomException(INVALID_COMMENT_LEVEL); + } + } + @Transactional public CommentUpdateResponse updateComment(String email, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { @@ -86,6 +81,12 @@ public CommentUpdateResponse updateComment(String email, Long postId, Long comme return CommentUpdateResponse.from(comment); } + private void validateDeprecated(Comment comment) { + if (comment.getContent() == null) { + throw new CustomException(CAN_NOT_UPDATE_DEPRECATED_COMMENT); + } + } + @Transactional public CommentDeleteResponse deleteCommentById(String email, Long postId, Long commentId) { SiteUser siteUser = siteUserRepository.getByEmail(email); @@ -117,4 +118,10 @@ public CommentDeleteResponse deleteCommentById(String email, Long postId, Long c } return new CommentDeleteResponse(commentId); } + + private void validateOwnership(Comment comment, String email) { + if (!comment.getSiteUser().getEmail().equals(email)) { + throw new CustomException(INVALID_POST_ACCESS); + } + } } diff --git a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java index 282c36e8c..22847dc6d 100644 --- a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java +++ b/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java @@ -23,7 +23,6 @@ public class RedisConfig { private final String redisHost; - private final int redisPort; public RedisConfig(@Value("${spring.data.redis.host}") final String redisHost, diff --git a/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java b/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java index a52bf281a..2a2cfa6a5 100644 --- a/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java +++ b/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java @@ -7,6 +7,7 @@ @Configuration public class SchedulerConfig implements SchedulingConfigurer { + private final int POOL_SIZE = 5; @Override diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 7e103f911..6062c640e 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -29,6 +29,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; + private final TokenService tokenService; private final TokenValidator tokenValidator; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; diff --git a/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java b/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java index f74a57fb9..82902d32b 100644 --- a/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java +++ b/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java @@ -22,7 +22,7 @@ ) public class SwaggerConfig { - public static final String ACCESS_TOKEN = "access_token"; + public static final String ACCESS_TOKEN = "access_token"; @Bean public OpenAPI customOpenAPI() { diff --git a/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java b/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java index 738d26e04..417b040b3 100644 --- a/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java +++ b/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java @@ -9,7 +9,6 @@ public class AsyncConfig { @Bean(name = "asyncExecutor") public ThreadPoolTaskExecutor asyncExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java index a95a504ed..9a63a21f5 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/config/token/TokenValidator.java @@ -36,30 +36,15 @@ public void validateAccessToken(String token) { validateRefreshToken(token); } - private void validateRefreshToken(String token) { - String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)) == null) { - throw new CustomException(REFRESH_TOKEN_EXPIRED); - } - } - - private void validateNotSignOut(String token) { - String email = getClaim(token).getSubject(); - if (SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)))) { - throw new CustomException(USER_ALREADY_SIGN_OUT); - } - } - public void validateKakaoToken(String token) { validateTokenNotEmpty(token); validateTokenNotExpired(token, TokenType.KAKAO_OAUTH); validateKakaoTokenNotUsed(token); } - private void validateKakaoTokenNotUsed(String token) { - String email = getClaim(token).getSubject(); - if (!Objects.equals(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email)), token)) { - throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); + private void validateTokenNotEmpty(String token) { + if (!StringUtils.hasText(token)) { + throw new CustomException(INVALID_TOKEN); } } @@ -76,9 +61,24 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { } } - private void validateTokenNotEmpty(String token) { - if (!StringUtils.hasText(token)) { - throw new CustomException(INVALID_TOKEN); + private void validateNotSignOut(String token) { + String email = getClaim(token).getSubject(); + if (SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)))) { + throw new CustomException(USER_ALREADY_SIGN_OUT); + } + } + + private void validateRefreshToken(String token) { + String email = getClaim(token).getSubject(); + if (redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)) == null) { + throw new CustomException(REFRESH_TOKEN_EXPIRED); + } + } + + private void validateKakaoTokenNotUsed(String token) { + String email = getClaim(token).getSubject(); + if (!Objects.equals(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email)), token)) { + throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java index 367d704eb..2f1962fbf 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomException.java @@ -4,6 +4,7 @@ @Getter public class CustomException extends RuntimeException { + private final int code; private final String message; diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 1a3aa8df7..765013303 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -53,7 +53,7 @@ public enum ErrorCode { PROFILE_IMAGE_NEEDED(HttpStatus.BAD_REQUEST.value(), "프로필 이미지가 필요합니다."), // community - INVALID_POST_CATEGORY(HttpStatus.BAD_REQUEST.value(),"잘못된 카테고리명입니다."), + INVALID_POST_CATEGORY(HttpStatus.BAD_REQUEST.value(), "잘못된 카테고리명입니다."), INVALID_BOARD_CODE(HttpStatus.BAD_REQUEST.value(), "잘못된 게시판 코드입니다."), INVALID_POST_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글입니다."), INVALID_POST_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 게시글만 제어할 수 있습니다."), @@ -62,7 +62,7 @@ public enum ErrorCode { INVALID_COMMENT_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 댓글입니다."), INVALID_COMMENT_LEVEL(HttpStatus.BAD_REQUEST.value(), "최대 대댓글까지만 작성할 수 있습니다."), INVALID_COMMENT_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 댓글만 제어할 수 있습니다."), - CAN_NOT_UPDATE_DEPRECATED_COMMENT(HttpStatus.BAD_REQUEST.value(),"이미 삭제된 댓글을 수정할 수 없습니다."), + CAN_NOT_UPDATE_DEPRECATED_COMMENT(HttpStatus.BAD_REQUEST.value(), "이미 삭제된 댓글을 수정할 수 없습니다."), INVALID_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글 좋아요입니다."), DUPLICATE_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "이미 좋아요한 게시글입니다."), diff --git a/src/main/java/com/example/solidconnection/entity/PostImage.java b/src/main/java/com/example/solidconnection/entity/PostImage.java index 9ab87853c..653beecc4 100644 --- a/src/main/java/com/example/solidconnection/entity/PostImage.java +++ b/src/main/java/com/example/solidconnection/entity/PostImage.java @@ -1,7 +1,14 @@ package com.example.solidconnection.entity; import com.example.solidconnection.post.domain.Post; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,6 +16,7 @@ @Getter @NoArgsConstructor public class PostImage { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index c5b974082..cd287809b 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -1,13 +1,28 @@ package com.example.solidconnection.post.controller; -import com.example.solidconnection.post.dto.*; +import com.example.solidconnection.post.dto.PostCreateRequest; +import com.example.solidconnection.post.dto.PostCreateResponse; +import com.example.solidconnection.post.dto.PostDeleteResponse; +import com.example.solidconnection.post.dto.PostDislikeResponse; +import com.example.solidconnection.post.dto.PostFindResponse; +import com.example.solidconnection.post.dto.PostLikeResponse; +import com.example.solidconnection.post.dto.PostUpdateRequest; +import com.example.solidconnection.post.dto.PostUpdateResponse; import com.example.solidconnection.post.service.PostService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.security.Principal; diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/post/domain/Post.java index 203feb5a9..31125f8bd 100644 --- a/src/main/java/com/example/solidconnection/post/domain/Post.java +++ b/src/main/java/com/example/solidconnection/post/domain/Post.java @@ -7,8 +7,21 @@ import com.example.solidconnection.post.dto.PostUpdateRequest; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; -import jakarta.persistence.*; -import lombok.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; import org.hibernate.annotations.BatchSize; import java.util.ArrayList; diff --git a/src/main/java/com/example/solidconnection/post/domain/PostLike.java b/src/main/java/com/example/solidconnection/post/domain/PostLike.java index 0af621370..9edf4052e 100644 --- a/src/main/java/com/example/solidconnection/post/domain/PostLike.java +++ b/src/main/java/com/example/solidconnection/post/domain/PostLike.java @@ -1,7 +1,13 @@ package com.example.solidconnection.post.domain; import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,6 +17,7 @@ @NoArgsConstructor @EqualsAndHashCode(of = "id") public class PostLike { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -24,7 +31,6 @@ public class PostLike { private SiteUser siteUser; public void setPostAndSiteUser(Post post, SiteUser siteUser) { - if (this.post != null) { this.post.getPostLikeList().remove(this); } @@ -39,7 +45,6 @@ public void setPostAndSiteUser(Post post, SiteUser siteUser) { } public void resetPostAndSiteUser() { - if (this.post != null) { this.post.getPostLikeList().remove(this); } diff --git a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java b/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java index 8e6d3202a..4f475824c 100644 --- a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java +++ b/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java @@ -33,6 +33,12 @@ public static BoardFindPostResponse from(Post post) { ); } + public static List from(List postList) { + return postList.stream() + .map(BoardFindPostResponse::from) + .collect(Collectors.toList()); + } + private static int getCommentCount(Post post) { return post.getCommentList().size(); } @@ -43,10 +49,4 @@ private static String getFirstImageUrl(Post post) { .map(PostImage::getUrl) .orElse(null); } - - public static List from(List postList) { - return postList.stream() - .map(BoardFindPostResponse::from) - .collect(Collectors.toList()); - } } diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java index 03ab79686..a1ba1c696 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java @@ -11,12 +11,15 @@ public record PostCreateRequest( @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, + @NotBlank(message = "게시글 제목은 빈 값일 수 없습니다.") @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String title, + @NotBlank(message = "게시글 내용은 빈 값일 수 없습니다.") @Size(min = 1, max = 1000, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content, + @NotNull(message = "게시글 질문여부를 설정해주세요.") Boolean isQuestion ) { diff --git a/src/main/java/com/example/solidconnection/dto/PostFindPostImageResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java similarity index 93% rename from src/main/java/com/example/solidconnection/dto/PostFindPostImageResponse.java rename to src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java index 22a6a4af0..63adf0020 100644 --- a/src/main/java/com/example/solidconnection/dto/PostFindPostImageResponse.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.dto; +package com.example.solidconnection.post.dto; import com.example.solidconnection.entity.PostImage; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java index 45e4e5dc7..1562dd5bc 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java @@ -2,7 +2,6 @@ import com.example.solidconnection.board.dto.PostFindBoardResponse; import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.dto.*; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java b/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java index 0ce14b175..35d7d58c9 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java @@ -5,8 +5,6 @@ public record PostLikeResponse( Long likeCount, Boolean isLiked - - ) { public static PostLikeResponse from(Post post) { return new PostLikeResponse( diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java index b82b73685..b9bdc6f54 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java @@ -7,9 +7,11 @@ public record PostUpdateRequest( @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, + @NotBlank(message = "게시글 제목은 빈 값일 수 없습니다.") @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String title, + @NotBlank(message = "게시글 내용은 빈 값일 수 없습니다.") @Size(min = 1, max = 1000, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content diff --git a/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java b/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java index 398157c73..bebde7a92 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java +++ b/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java @@ -1,8 +1,8 @@ package com.example.solidconnection.post.repository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java index b819cc45a..e96881147 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java +++ b/src/main/java/com/example/solidconnection/post/repository/PostRepository.java @@ -19,6 +19,27 @@ public interface PostRepository extends JpaRepository { @EntityGraph(attributePaths = {"postImageList", "board", "siteUser"}) Optional findPostById(Long id); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + UPDATE Post p SET p.likeCount = p.likeCount - 1 + WHERE p.id = :postId AND p.likeCount > 0 + """) + void decreaseLikeCount(@Param("postId") Long postId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + UPDATE Post p SET p.likeCount = p.likeCount + 1 + WHERE p.id = :postId + """) + void increaseLikeCount(@Param("postId") Long postId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + UPDATE Post p SET p.viewCount = p.viewCount + :count + WHERE p.id = :postId + """) + void increaseViewCount(@Param("postId") Long postId, @Param("count") Long count); + default Post getByIdUsingEntityGraph(Long id) { return findPostById(id) .orElseThrow(() -> new CustomException(INVALID_POST_ID)); @@ -28,19 +49,4 @@ default Post getById(Long id) { return findById(id) .orElseThrow(() -> new CustomException(INVALID_POST_ID)); } - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE Post p SET p.likeCount = p.likeCount - 1 " + - "WHERE p.id = :postId AND p.likeCount > 0") - void decreaseLikeCount(@Param("postId") Long postId); - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE Post p SET p.likeCount = p.likeCount + 1 " + - "WHERE p.id = :postId") - void increaseLikeCount(@Param("postId") Long postId); - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE Post p SET p.viewCount = p.viewCount + :count " + - "WHERE p.id = :postId") - void increaseViewCount(@Param("postId") Long postId, @Param("count") Long count); } diff --git a/src/main/java/com/example/solidconnection/post/service/PostService.java b/src/main/java/com/example/solidconnection/post/service/PostService.java index 4f9f77a73..d31cfb97a 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostService.java @@ -1,17 +1,24 @@ package com.example.solidconnection.post.service; +import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.dto.PostFindBoardResponse; import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.comment.dto.PostFindCommentResponse; import com.example.solidconnection.comment.service.CommentService; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.dto.*; -import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.dto.PostCreateRequest; +import com.example.solidconnection.post.dto.PostCreateResponse; +import com.example.solidconnection.post.dto.PostDeleteResponse; +import com.example.solidconnection.post.dto.PostDislikeResponse; +import com.example.solidconnection.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.post.dto.PostFindResponse; +import com.example.solidconnection.post.dto.PostLikeResponse; +import com.example.solidconnection.post.dto.PostUpdateRequest; +import com.example.solidconnection.post.dto.PostUpdateResponse; import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.*; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; @@ -32,11 +39,17 @@ import java.util.List; -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_POST_LIKE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; @Service @RequiredArgsConstructor public class PostService { + private final PostRepository postRepository; private final SiteUserRepository siteUserRepository; private final BoardRepository boardRepository; diff --git a/src/main/java/com/example/solidconnection/repositories/CountryRepository.java b/src/main/java/com/example/solidconnection/repositories/CountryRepository.java index aff9470b4..d9ba75555 100644 --- a/src/main/java/com/example/solidconnection/repositories/CountryRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/CountryRepository.java @@ -7,21 +7,10 @@ import org.springframework.stereotype.Repository; import java.util.List; -import java.util.Optional; @Repository public interface CountryRepository extends JpaRepository { - Optional findByKoreanName(String koreanName); - -/* default Country getByKoreanName(String koreanName) { - return findByKoreanName(koreanName) - .orElseThrow(() -> new CustomException(COUNTRY_NOT_FOUND_BY_KOREAN_NAME)); - }*/ - @Query("SELECT c FROM Country c WHERE c.koreanName IN :names") List findByKoreanNames(@Param(value = "names") List names); - - @Query("SELECT c FROM Country c WHERE c.koreanName LIKE %:keyword%") - List findByKoreanNameContaining(@Param("keyword") String keyword); } diff --git a/src/main/java/com/example/solidconnection/repositories/RegionRepository.java b/src/main/java/com/example/solidconnection/repositories/RegionRepository.java index 9a26233bc..0dc99fb08 100644 --- a/src/main/java/com/example/solidconnection/repositories/RegionRepository.java +++ b/src/main/java/com/example/solidconnection/repositories/RegionRepository.java @@ -7,18 +7,10 @@ import org.springframework.stereotype.Repository; import java.util.List; -import java.util.Optional; @Repository public interface RegionRepository extends JpaRepository { - Optional findByKoreanName(String koreanName); - -/* default Region getByKoreanName(String koreanName) { - return findByKoreanName(koreanName) - .orElseThrow(() -> new CustomException(REGION_NOT_FOUND_BY_KOREAN_NAME)); - }*/ - @Query("SELECT r FROM Region r WHERE r.koreanName IN :names") List findByKoreanNames(@Param(value = "names") List names); } diff --git a/src/main/java/com/example/solidconnection/s3/FileUploadService.java b/src/main/java/com/example/solidconnection/s3/FileUploadService.java index 5e31c5475..71d9f9c7a 100644 --- a/src/main/java/com/example/solidconnection/s3/FileUploadService.java +++ b/src/main/java/com/example/solidconnection/s3/FileUploadService.java @@ -22,6 +22,7 @@ @EnableAsync @Slf4j public class FileUploadService { + private final AmazonS3Client amazonS3; public FileUploadService(AmazonS3Client amazonS3) { diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java index 7a1c1fc7c..2c513cb8c 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -4,7 +4,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.security.Principal; @@ -15,12 +19,16 @@ public class S3Controller implements S3ControllerSwagger { private final S3Service s3Service; + @Value("${cloud.aws.s3.url.default}") private String s3Default; + @Value("${cloud.aws.s3.url.uploaded}") private String s3Uploaded; + @Value("${cloud.aws.cloudFront.url.default}") private String cloudFrontDefault; + @Value("${cloud.aws.cloudFront.url.uploaded}") private String cloudFrontUploaded; diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 534c4a935..049be9fa3 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -16,7 +16,11 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; import static com.example.solidconnection.custom.exception.ErrorCode.FILE_NOT_EXIST; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_FILE_EXTENSIONS; @@ -29,13 +33,15 @@ public class S3Service { private static final Logger log = LoggerFactory.getLogger(S3Service.class); + private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 3; + private final AmazonS3Client amazonS3; private final SiteUserRepository siteUserRepository; private final FileUploadService fileUploadService; private final ThreadPoolTaskExecutor asyncExecutor; + @Value("${cloud.aws.s3.bucket}") private String bucket; - private final long MAX_FILE_SIZE_MB = 1024 * 1024 * 3; /* * 파일을 S3에 업로드한다. diff --git a/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java b/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java index 41ba66398..8da1fe1ca 100644 --- a/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java +++ b/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java @@ -13,7 +13,7 @@ import java.util.List; -import static com.example.solidconnection.type.RedisConstants.*; +import static com.example.solidconnection.type.RedisConstants.VIEW_COUNT_KEY_PATTERN; @RequiredArgsConstructor @Component diff --git a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java index 7bd0edaf2..7550102aa 100644 --- a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java +++ b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java @@ -1,13 +1,20 @@ package com.example.solidconnection.score.controller; -import com.example.solidconnection.score.dto.*; +import com.example.solidconnection.score.dto.GpaScoreRequest; +import com.example.solidconnection.score.dto.GpaScoreStatusResponse; +import com.example.solidconnection.score.dto.LanguageTestScoreRequest; +import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; import com.example.solidconnection.score.service.ScoreService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +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; import java.security.Principal; diff --git a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java index 2747f8c88..17b5cca48 100644 --- a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java +++ b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java @@ -4,7 +4,15 @@ import com.example.solidconnection.entity.common.BaseEntity; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.VerifyStatus; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,9 +25,11 @@ @NoArgsConstructor @EqualsAndHashCode public class GpaScore extends BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Embedded private Gpa gpa; diff --git a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java index bc16bc4e4..88501f686 100644 --- a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java +++ b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java @@ -4,7 +4,15 @@ import com.example.solidconnection.entity.common.BaseEntity; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.VerifyStatus; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,9 +25,11 @@ @NoArgsConstructor @AllArgsConstructor public class LanguageTestScore extends BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Embedded private LanguageTest languageTest; diff --git a/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java b/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java index c5fbb2847..e3c26665b 100644 --- a/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java +++ b/src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java @@ -9,6 +9,7 @@ @Repository public interface GpaScoreRepository extends JpaRepository { + Optional findGpaScoreBySiteUser(SiteUser siteUser); Optional findGpaScoreBySiteUserAndId(SiteUser siteUser, Long id); diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index b2d9ad29e..d09038fa5 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -3,7 +3,12 @@ import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; -import com.example.solidconnection.score.dto.*; +import com.example.solidconnection.score.dto.GpaScoreRequest; +import com.example.solidconnection.score.dto.GpaScoreStatus; +import com.example.solidconnection.score.dto.GpaScoreStatusResponse; +import com.example.solidconnection.score.dto.LanguageTestScoreRequest; +import com.example.solidconnection.score.dto.LanguageTestScoreStatus; +import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; diff --git a/src/main/java/com/example/solidconnection/service/RedisService.java b/src/main/java/com/example/solidconnection/service/RedisService.java index 9816a264e..93a9de74f 100644 --- a/src/main/java/com/example/solidconnection/service/RedisService.java +++ b/src/main/java/com/example/solidconnection/service/RedisService.java @@ -9,10 +9,12 @@ import java.util.Collections; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.type.RedisConstants.*; +import static com.example.solidconnection.type.RedisConstants.VALIDATE_VIEW_COUNT_TTL; +import static com.example.solidconnection.type.RedisConstants.VIEW_COUNT_TTL; @Service public class RedisService { + private final RedisTemplate redisTemplate; private final RedisScript incrViewCountLuaScript; @@ -28,7 +30,7 @@ public void increaseViewCount(String key) { redisTemplate.execute(incrViewCountLuaScript, Collections.singletonList(key), VIEW_COUNT_TTL.getValue()); } - public void deleteKey(String key){ + public void deleteKey(String key) { redisTemplate.opsForValue().getAndDelete(key); } diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java index 443404def..d03c69fa8 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -1,11 +1,20 @@ package com.example.solidconnection.siteuser.controller; -import com.example.solidconnection.siteuser.dto.*; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; +import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; +import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; import com.example.solidconnection.siteuser.service.SiteUserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.security.Principal; diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java index 6f479f67e..260610436 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java @@ -1,17 +1,14 @@ package com.example.solidconnection.siteuser.controller; import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.dto.MyPageUpdateRequest; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import java.security.Principal; diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 47a071b04..d548ff852 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -8,8 +8,20 @@ import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; -import jakarta.persistence.*; -import lombok.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java index 892094394..88396f5b9 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java @@ -2,7 +2,6 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.Role; - import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "마이페이지 페이지 정보 응답") diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java index d186b08af..5572d5a2d 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java @@ -11,10 +11,10 @@ public record MyPageUpdateResponse( @Schema(description = "업데이트된 프로필 이미지 URL", example = "http://example.com/updated-profile.jpg") String profileImageUrl) { - public static MyPageUpdateResponse from(SiteUser siteUser) { - return new MyPageUpdateResponse( - siteUser.getNickname(), - siteUser.getProfileImageUrl() - ); - } + public static MyPageUpdateResponse from(SiteUser siteUser) { + return new MyPageUpdateResponse( + siteUser.getNickname(), + siteUser.getProfileImageUrl() + ); + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index 793627adf..a7a2e5d71 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -4,7 +4,11 @@ import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.*; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; +import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; +import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.ImgType; @@ -19,7 +23,9 @@ import java.time.format.DateTimeFormatter; import java.util.List; -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.PROFILE_IMAGE_NEEDED; @RequiredArgsConstructor @Service @@ -51,22 +57,6 @@ public MyPageUpdateResponse getMyPageInfoToUpdate(String email) { return MyPageUpdateResponse.from(siteUser); } - private void validateNicknameDuplicated(String nickname) { - if (siteUserRepository.existsByNickname(nickname)) { - throw new CustomException(NICKNAME_ALREADY_EXISTED); - } - } - private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { - if (lastModifiedAt == null) { - return; - } - if (LocalDateTime.now().isBefore(lastModifiedAt.plusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES))) { - String formatLastModifiedAt - = String.format("(마지막 수정 시간 : %s)", NICKNAME_LAST_CHANGE_DATE_FORMAT.format(lastModifiedAt)); - throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); - } - } - /* * 관심 대학교 목록을 조회한다. * */ @@ -79,7 +69,6 @@ public List getWishUniversity(String emai .toList(); } - /* * 프로필 이미지를 수정한다. * */ @@ -89,7 +78,7 @@ public ProfileImageUpdateResponse updateProfileImage(String email, MultipartFile validateProfileImage(imageFile); // 프로필 이미지를 처음 수정하는 경우에는 deleteExProfile 수행하지 않음 - if(!isDefaultProfileImage(siteUser.getProfileImageUrl())){ + if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { s3Service.deleteExProfile(email); } UploadedFileUrlResponse uploadedFileUrlResponse = s3Service.uploadFile(imageFile, ImgType.PROFILE); @@ -104,7 +93,6 @@ private void validateProfileImage(MultipartFile imageFile) { throw new CustomException(PROFILE_IMAGE_NEEDED); } } - private boolean isDefaultProfileImage(String profileImageUrl) { String prefix = "profile/"; return profileImageUrl == null || !profileImageUrl.startsWith(prefix); @@ -126,4 +114,21 @@ public NicknameUpdateResponse updateNickname(String email, NicknameUpdateRequest return NicknameUpdateResponse.from(siteUser); } + + private void validateNicknameDuplicated(String nickname) { + if (siteUserRepository.existsByNickname(nickname)) { + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + + private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { + if (lastModifiedAt == null) { + return; + } + if (LocalDateTime.now().isBefore(lastModifiedAt.plusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES))) { + String formatLastModifiedAt + = String.format("(마지막 수정 시간 : %s)", NICKNAME_LAST_CHANGE_DATE_FORMAT.format(lastModifiedAt)); + throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); + } + } } diff --git a/src/main/java/com/example/solidconnection/university/domain/University.java b/src/main/java/com/example/solidconnection/university/domain/University.java index 8e31dfa4a..c3021385e 100644 --- a/src/main/java/com/example/solidconnection/university/domain/University.java +++ b/src/main/java/com/example/solidconnection/university/domain/University.java @@ -2,7 +2,12 @@ import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.Region; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java index 118bfdaa6..ed4c6109b 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java @@ -9,7 +9,6 @@ @Schema(description = "대학 세부 사항 응답 데이터") public record UniversityDetailResponse( - @Schema(description = "대학 지원을 위한 정보 id", example = "1") long id, diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java index fe5713c67..de0987eaf 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java @@ -36,8 +36,8 @@ public record UniversityInfoForApplyPreviewResponse( public static UniversityInfoForApplyPreviewResponse from(UniversityInfoForApply universityInfoForApply) { List languageRequirementResponses = new java.util.ArrayList<>( universityInfoForApply.getLanguageRequirements().stream() - .map(LanguageRequirementResponse::from) - .toList()); + .map(LanguageRequirementResponse::from) + .toList()); Collections.sort(languageRequirementResponses); return new UniversityInfoForApplyPreviewResponse( diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java index 0beac6763..dd84cfbf5 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UniversityFilterRepositoryImpl.java @@ -85,8 +85,8 @@ public List findByRegionCodeAndKeywordsAndLanguageTestTy .and(universityInfoForApply.term.eq(term))) .fetch(); - if(testScore == null || testScore.isEmpty()) { - if(testType != null) { + if (testScore == null || testScore.isEmpty()) { + if (testType != null) { return filteredUniversityInfoForApply.stream() .filter(uifa -> uifa.getLanguageRequirements().stream() .anyMatch(lr -> lr.getLanguageTestType().equals(testType))) diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityService.java index c0cbe2c05..708374e96 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityService.java @@ -8,7 +8,11 @@ import com.example.solidconnection.university.domain.LikedUniversity; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import com.example.solidconnection.university.dto.*; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import com.example.solidconnection.university.dto.UniversityDetailResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponses; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.custom.UniversityFilterRepositoryImpl; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/solidconnection/util/RedisUtils.java b/src/main/java/com/example/solidconnection/util/RedisUtils.java index ef91dfc3d..6c56fa73f 100644 --- a/src/main/java/com/example/solidconnection/util/RedisUtils.java +++ b/src/main/java/com/example/solidconnection/util/RedisUtils.java @@ -11,7 +11,10 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static com.example.solidconnection.type.RedisConstants.*; +import static com.example.solidconnection.type.RedisConstants.CREATE_LOCK_PREFIX; +import static com.example.solidconnection.type.RedisConstants.REFRESH_LOCK_PREFIX; +import static com.example.solidconnection.type.RedisConstants.VALIDATE_VIEW_COUNT_KEY_PREFIX; +import static com.example.solidconnection.type.RedisConstants.VIEW_COUNT_KEY_PREFIX; @Component public class RedisUtils { @@ -70,6 +73,6 @@ public String getRefreshLockKey(String key) { public boolean isCacheExpiringSoon(String key, Long defaultTtl, Double percent) { Long leftTtl = redisTemplate.getExpire(key); - return defaultTtl != null && ((double) leftTtl /defaultTtl)*100 < percent; + return defaultTtl != null && ((double) leftTtl / defaultTtl) * 100 < percent; } } diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html deleted file mode 100644 index c3dca01d5..000000000 --- a/src/main/resources/static/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -

Solid Connection Backend Page

-이 페이지가 보이면 백엔드 배포 서버가 잘 작동하고 있다는 것입니다. \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java index 917d073f5..57c5916a9 100644 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java @@ -7,7 +7,7 @@ import com.example.solidconnection.comment.service.CommentService; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.dto.PostFindPostImageResponse; +import com.example.solidconnection.post.dto.PostFindPostImageResponse; import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.post.repository.PostLikeRepository; From 74bbc491147e71af44de84cfaa0337321134f0ac Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 10 Dec 2024 00:14:08 +0900 Subject: [PATCH 111/158] =?UTF-8?q?refactor:=20=EA=B4=80=EC=8B=AC=EC=82=AC?= =?UTF-8?q?=20=EB=B3=84=20yml=20=EB=B6=84=EB=A6=AC=20(#113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yml | 33 ------------------- src/main/resources/application-test.yml | 29 ----------------- src/main/resources/application.yml | 41 +++--------------------- 3 files changed, 5 insertions(+), 98 deletions(-) delete mode 100644 src/main/resources/application-local.yml delete mode 100644 src/main/resources/application-test.yml diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml deleted file mode 100644 index f0baec690..000000000 --- a/src/main/resources/application-local.yml +++ /dev/null @@ -1,33 +0,0 @@ -spring: - config: - activate: - on-profile: local - - jpa: - hibernate: - ddl-auto: create - generate-ddl: true - show-sql: true - database: mysql - defer-datasource-initialization: true - properties: - hibernate: - format_sql: true - - sql: - init: - mode: always - - datasource: - driverClassName: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost/solid_connection?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: solid_connection_local_username - password: solid_connection_local_password - - data: - redis: - host: localhost - port: 6379 - -kakao: - redirect_uri: "http://localhost:8080/auth/kakao" diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml deleted file mode 100644 index bf25bb62f..000000000 --- a/src/main/resources/application-test.yml +++ /dev/null @@ -1,29 +0,0 @@ -spring: - config: - activate: - on-profile: test - - jpa: - database-platform: org.hibernate.dialect.H2Dialect - hibernate: - ddl-auto: create - generate-ddl: true - show-sql: true - database: mysql - defer-datasource-initialization: true - properties: - hibernate: - format_sql: true - - datasource: - url: jdbc:h2:mem:testdb - username: sa - password: - - data: - redis: - host: localhost - port: 6379 - -kakao: - redirect_uri: "http://localhost:8080/auth/kakao" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b114af84e..a644c7e9f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,15 +1,9 @@ spring: - profiles: - group: - prod: - - prod - - secret - local: - - local - - secret - test: - - test - - secret + config: + import: + - classpath:/secret/application-cloud.yml + - classpath:/secret/application-db.yml + - classpath:/secret/application-variable.yml tomcat: threads: @@ -20,35 +14,10 @@ spring: max-file-size: 10MB max-request-size: 10MB - jpa: - hibernate: - ddl-auto: none - generate-ddl: false - show-sql: false - database: mysql - defer-datasource-initialization: true - mvc: path match: matching-strategy: ANT_PATH_MATCHER - sql: - init: - mode: never - -jwt: - secret: - aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee - -kakao: - token_url: "https://kauth.kakao.com/oauth/token" - user_info_url: "https://kapi.kakao.com/v2/user/me" - -view: - count: - scheduling: - delay: 3000 - management: endpoints: web: From 928192487062e1f02d33b9b252cbe6f5fc65f8f1 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Wed, 11 Dec 2024 18:07:33 +0900 Subject: [PATCH 112/158] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=20(#115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: swagger 관련 코드 제거 * refactor: 스웨거 의존성 제거 * refactor: 스웨거 엔드포인트 제거 * fix: 테스트 코드 컴파일 에러 해결 --- build.gradle | 4 +- .../controller/ApplicationController.java | 2 +- .../ApplicationControllerSwagger.java | 36 ----- .../application/dto/ApplicantResponse.java | 11 -- .../dto/ApplicationSubmissionResponse.java | 4 - .../application/dto/ApplicationsResponse.java | 9 -- .../application/dto/ApplyRequest.java | 4 - .../dto/UniversityApplicantsResponse.java | 12 -- .../dto/UniversityChoiceRequest.java | 6 - .../auth/controller/AuthController.java | 2 +- .../controller/AuthControllerSwagger.java | 116 --------------- .../auth/dto/ReissueResponse.java | 4 - .../auth/dto/SignInResponse.java | 7 - .../auth/dto/SignUpRequest.java | 20 +-- .../auth/dto/SignUpResponse.java | 6 - .../auth/dto/kakao/FirstAccessResponse.java | 12 -- .../auth/dto/kakao/KakaoCodeRequest.java | 4 - .../board/controller/BoardController.java | 6 - .../comment/controller/CommentController.java | 6 - .../security/JwtAuthenticationFilter.java | 4 - .../security/SecurityConfiguration.java | 1 - .../config/swagger/SwaggerConfig.java | 40 ------ .../post/controller/PostController.java | 6 - .../solidconnection/s3/S3Controller.java | 2 +- .../s3/S3ControllerSwagger.java | 136 ------------------ .../s3/UploadedFileUrlResponse.java | 4 - .../score/controller/ScoreController.java | 6 - .../score/dto/GpaScoreRequest.java | 6 - .../score/dto/LanguageTestScoreRequest.java | 6 - .../controller/SiteUserController.java | 2 +- .../controller/SiteUserControllerSwagger.java | 52 ------- .../siteuser/dto/MyPageResponse.java | 17 --- .../siteuser/dto/MyPageUpdateRequest.java | 4 - .../siteuser/dto/MyPageUpdateResponse.java | 5 - .../siteuser/dto/NicknameUpdateRequest.java | 2 - .../siteuser/dto/NicknameUpdateResponse.java | 2 - .../dto/ProfileImageUpdateResponse.java | 2 - .../controller/UniversityController.java | 2 +- .../UniversityControllerSwagger.java | 124 ---------------- .../university/dto/IsLikeResponse.java | 4 - .../dto/LanguageRequirementResponse.java | 5 - .../university/dto/LikeResultResponse.java | 4 - .../dto/UniversityDetailResponse.java | 54 ------- ...UniversityInfoForApplyPreviewResponse.java | 18 --- .../dto/UniversityRecommendsResponse.java | 5 - .../solidconnection/e2e/SignUpTest.java | 9 +- 46 files changed, 13 insertions(+), 780 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java delete mode 100644 src/main/java/com/example/solidconnection/auth/controller/AuthControllerSwagger.java delete mode 100644 src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java delete mode 100644 src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java delete mode 100644 src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java delete mode 100644 src/main/java/com/example/solidconnection/university/controller/UniversityControllerSwagger.java diff --git a/build.gradle b/build.gradle index 3799f64bc..5157e8f6c 100644 --- a/build.gradle +++ b/build.gradle @@ -36,8 +36,8 @@ dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거 implementation 'com.amazonaws:aws-java-sdk-s3:1.12.470' implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' - testImplementation "org.mockito:mockito-core:3.3.3" + implementation 'org.apache.commons:commons-lang3:3.12.0' + testImplementation 'org.mockito:mockito-core:3.3.3' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 6242180aa..dce62235f 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -21,7 +21,7 @@ @RequiredArgsConstructor @RequestMapping("/application") @RestController -public class ApplicationController implements ApplicationControllerSwagger { +public class ApplicationController { private final ApplicationSubmissionService applicationSubmissionService; private final ApplicationQueryService applicationQueryService; diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java deleted file mode 100644 index 3b93dea4c..000000000 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationControllerSwagger.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.solidconnection.application.controller; - -import com.example.solidconnection.application.dto.ApplicationsResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestParam; - -import java.security.Principal; - -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - -@Tag(name = "Application", description = "지원 정보 API") -@SecurityRequirements -@SecurityRequirement(name = ACCESS_TOKEN) -public interface ApplicationControllerSwagger { - @Operation( - summary = "지원자 목록 조회", - responses = { - @ApiResponse( - responseCode = "200", - description = "지원자 목록 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ApplicationsResponse.class) - ) - ) - } - ) - ResponseEntity getApplicants(Principal principal, @RequestParam(required = false) String region, @RequestParam(required = false) String keyword); -} diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java index b6b5ee477..9835491b1 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java @@ -2,23 +2,12 @@ import com.example.solidconnection.application.domain.Application; import com.example.solidconnection.type.LanguageTestType; -import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "지원자") public record ApplicantResponse( - @Schema(description = "닉네임", example = "행복한 개발자") String nicknameForApply, - - @Schema(description = "GPA", example = "3.85") double gpa, - - @Schema(description = "어학 시험 유형", example = "TOEFL_IBT") LanguageTestType testType, - - @Schema(description = "어학 시험 점수", example = "110") String testScore, - - @Schema(description = "현재 사용자가 해당 지원지인지", example = "true") boolean isMine) { public static ApplicantResponse of(Application application, boolean isMine) { diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java index fe112a8d0..4f353733b 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java @@ -1,9 +1,5 @@ package com.example.solidconnection.application.dto; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "지원 정보 제출 성공 여부") public record ApplicationSubmissionResponse( - @Schema(description = "제출 성공 여부", example = "true") boolean isSuccess) { } diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java index 3d5e2ca88..a3429c1ef 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java @@ -1,18 +1,9 @@ package com.example.solidconnection.application.dto; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; - import java.util.List; -@Schema(description = "지망별 지원자 목록") public record ApplicationsResponse( - @ArraySchema(arraySchema = @Schema(description = "1지망 대학에 지원한 지원자 목록")) List firstChoice, - - @ArraySchema(arraySchema = @Schema(description = "2지망 대학에 지원한 지원자 목록")) List secondChoice, - - @ArraySchema(arraySchema = @Schema(description = "3지망 대학에 지원한 지원자 목록")) List thirdChoice) { } diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java index 3a5d7f2f5..49c4b01ce 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java @@ -1,16 +1,12 @@ package com.example.solidconnection.application.dto; -import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -@Schema(description = "지원서 제출") public record ApplyRequest( @NotNull(message = "gpa score id를 입력해주세요.") - @Schema(description = "지원하는 유저의 gpa score id", example = "1") Long gpaScoreId, @NotNull(message = "language test score id를 입력해주세요.") - @Schema(description = "지원하는 유저의 language test score id", example = "1") Long languageTestScoreId, UniversityChoiceRequest universityChoiceRequest diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java index 84751786b..1d3415003 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java @@ -1,26 +1,14 @@ package com.example.solidconnection.application.dto; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; -@Schema(description = "대학과 그 대학에 지원한 지원자 정보") public record UniversityApplicantsResponse( - @Schema(description = "대학의 한국어 이름", example = "괌대학") String koreanName, - - @Schema(description = "선발 인원", example = "4") int studentCapacity, - - @Schema(description = "지역", example = "영미권") String region, - - @Schema(description = "국가", example = "미국") String country, - - @ArraySchema(schema = @Schema(description = "지원자 목록", implementation = ApplicantResponse.class)) List applicants) { public static UniversityApplicantsResponse of(UniversityInfoForApply universityInfoForApply, List applicant) { diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java index 033de880a..2d05cfe5b 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java @@ -1,17 +1,11 @@ package com.example.solidconnection.application.dto; -import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -@Schema(description = "지망 대학") public record UniversityChoiceRequest( @NotNull(message = "1지망 대학교를 입력해주세요.") - @Schema(description = "1지망 대학교의 지원 정보 ID", example = "1") Long firstChoiceUniversityId, - @Schema(description = "2지망 대학교의 지원 정보 ID (선택사항)", example = "2", nullable = true) Long secondChoiceUniversityId, - - @Schema(description = "3지망 대학교의 지원 정보 ID (선택사항)", example = "3", nullable = true) Long thirdChoiceUniversityId) { } diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index b73812a2a..5f48124dc 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -22,7 +22,7 @@ @RequiredArgsConstructor @RequestMapping("/auth") @RestController -public class AuthController implements AuthControllerSwagger { +public class AuthController { private final AuthService authService; private final SignUpService signUpService; diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthControllerSwagger.java b/src/main/java/com/example/solidconnection/auth/controller/AuthControllerSwagger.java deleted file mode 100644 index 28ce96f69..000000000 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthControllerSwagger.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.example.solidconnection.auth.controller; - -import com.example.solidconnection.auth.dto.ReissueResponse; -import com.example.solidconnection.auth.dto.SignInResponse; -import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.ResponseEntity; - -import java.security.Principal; - -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - -@Tag(name = "Auth", description = "인증 API") -public interface AuthControllerSwagger { - - @Operation( - summary = "카카오 OAuth 처리", - requestBody = @RequestBody( - description = "클라이언트가 받아온 카카오 인증 코드", - required = true, - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = KakaoCodeRequest.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "로그인 성공 또는 회원가입을 위한 사용자 정보 불러오기 성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(oneOf = {SignInResponse.class, FirstAccessResponse.class}) - ) - ) - } - ) - ResponseEntity processKakaoOauth(@RequestBody KakaoCodeRequest kakaoCodeRequest); - - @Operation( - summary = "회원가입", - requestBody = @RequestBody( - description = "회원가입 요청 정보", - required = true, - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = SignUpRequest.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "회원가입 성공, 엑세스 토큰과 리프레시 토큰 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = SignUpResponse.class) - ) - ) - } - ) - ResponseEntity signUp(@Valid @RequestBody SignUpRequest signUpRequest); - - @SecurityRequirements - @SecurityRequirement(name = ACCESS_TOKEN) - @Operation( - summary = "로그아웃", - responses = { - @ApiResponse( - responseCode = "204", - description = "로그아웃 성공" - ) - } - ) - ResponseEntity signOut(Principal principal); - - @SecurityRequirements - @SecurityRequirement(name = ACCESS_TOKEN) - @Operation( - summary = "회원 탈퇴", - responses = { - @ApiResponse( - responseCode = "200", - description = "회원 탈퇴 성공" - ) - } - ) - ResponseEntity quit(Principal principal); - - @SecurityRequirements - @SecurityRequirement(name = ACCESS_TOKEN) - @Operation( - summary = "토큰 재발급", - responses = { - @ApiResponse( - responseCode = "200", - description = "토큰 재발급 성공, 새 토큰 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = ReissueResponse.class) - ) - ) - } - ) - ResponseEntity reissueToken(Principal principal); -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java index 5e6097d38..48b55e6cb 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java @@ -1,9 +1,5 @@ package com.example.solidconnection.auth.dto; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "토큰 재발급 응답") public record ReissueResponse( - @Schema(description = "새로 발급된 액세스 토큰", example = "newAccessToken123") String accessToken) { } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java index 41d9425cb..400491b42 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java @@ -1,16 +1,9 @@ package com.example.solidconnection.auth.dto; import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; -import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "로그인 응답 데이터") public record SignInResponse( - @Schema(description = "사용자 등록 여부", example = "true") boolean isRegistered, - - @Schema(description = "발급된 액세스 토큰", example = "accessTokenExample123") String accessToken, - - @Schema(description = "발급된 리프레시 토큰", example = "refreshTokenExample123") String refreshToken) implements KakaoOauthResponse { } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java index e77cbd31b..fcb68cad1 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java @@ -5,38 +5,22 @@ import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; import com.fasterxml.jackson.annotation.JsonFormat; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import java.util.List; -@Schema(description = "회원가입 요청 데이터") public record SignUpRequest( - @Schema(description = "카카오 인증 토큰", example = "kakaoToken123") String kakaoOauthToken, - - @ArraySchema(schema = @Schema(description = "관심 지역 목록", example = "아시아권")) List interestedRegions, - - @ArraySchema(schema = @Schema(description = "관심 국가 목록", example = "일본")) List interestedCountries, - - @Schema(description = "지원 준비 단계", example = "CONSIDERING") PreparationStatus preparationStatus, + String profileImageUrl, + Gender gender, @NotBlank(message = "닉네임을 입력해주세요.") - @Schema(description = "닉네임", example = "nickname123") String nickname, - @Schema(description = "프로필 이미지 URL", example = "http://example.com/profile.jpg") - String profileImageUrl, - - @Schema(description = "성별", example = "MALE") - Gender gender, - @JsonFormat(pattern = "yyyy-MM-dd") - @Schema(description = "생년월일", example = "1999-01-01") String birth) { public SiteUser toSiteUser(String email, Role role) { diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java index b43eb95e8..2d74610cc 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java @@ -1,12 +1,6 @@ package com.example.solidconnection.auth.dto; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "회원가입 후 응답 데이터") public record SignUpResponse( - @Schema(description = "액세스 토큰", example = "accessTokenSignup123") String accessToken, - - @Schema(description = "리프레시 토큰", example = "refreshTokenSignup123") String refreshToken) { } diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java index d766099ed..6d7130bf0 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java @@ -1,22 +1,10 @@ package com.example.solidconnection.auth.dto.kakao; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "등록되지 않은 사용자의 최초 접속 시 응답 데이터") public record FirstAccessResponse( - @Schema(description = "사용자 등록 여부", example = "false") boolean isRegistered, - - @Schema(description = "카카오 닉네임", example = "홍길동") String nickname, - - @Schema(description = "이메일", example = "user@example.com") String email, - - @Schema(description = "카카오 프로필 이미지 URL", example = "http://example.com/image.jpg") String profileImageUrl, - - @Schema(description = "우리 서비스에사 발급한 카카오 인증 토큰", example = "abc123xyz") String kakaoOauthToken) implements KakaoOauthResponse { public static FirstAccessResponse of(KakaoUserInfoDto kakaoUserInfoDto, String kakaoOauthToken) { diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java index fcdbefb40..4fcfc5576 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java +++ b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java @@ -1,9 +1,5 @@ package com.example.solidconnection.auth.dto.kakao; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "클라이언트에서 받은 카카오 코드") public record KakaoCodeRequest( - @Schema(description = "카카오 코드", example = "ABCD1234") String code) { } diff --git a/src/main/java/com/example/solidconnection/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/board/controller/BoardController.java index 1777603cd..f6ebb27d0 100644 --- a/src/main/java/com/example/solidconnection/board/controller/BoardController.java +++ b/src/main/java/com/example/solidconnection/board/controller/BoardController.java @@ -3,8 +3,6 @@ import com.example.solidconnection.board.service.BoardService; import com.example.solidconnection.post.dto.BoardFindPostResponse; import com.example.solidconnection.type.BoardCode; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -16,13 +14,9 @@ import java.util.ArrayList; import java.util.List; -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - @RestController @RequiredArgsConstructor @RequestMapping("/communities") -@SecurityRequirements -@SecurityRequirement(name = ACCESS_TOKEN) public class BoardController { private final BoardService boardService; diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java index bcb50715c..a7eaab252 100644 --- a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java +++ b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java @@ -6,8 +6,6 @@ import com.example.solidconnection.comment.dto.CommentUpdateRequest; import com.example.solidconnection.comment.dto.CommentUpdateResponse; import com.example.solidconnection.comment.service.CommentService; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -21,13 +19,9 @@ import java.security.Principal; -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - @RestController @RequiredArgsConstructor @RequestMapping("/posts") -@SecurityRequirements -@SecurityRequirement(name = ACCESS_TOKEN) public class CommentController { private final CommentService commentService; diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index 6062c640e..a618bec04 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -108,10 +108,6 @@ private HashSet getPermitAllEndpoints() { // 대학교 정보 permitAllEndpoints.add("/university/search/**"); - // API 문서 - permitAllEndpoints.add("/swagger-ui/**"); - permitAllEndpoints.add("/v3/api-docs/**"); - return permitAllEndpoints; } } diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 934eaf9f8..449bdd35c 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -51,7 +51,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/file/profile/pre", "/auth/kakao", "/auth/sign-up", "/auth/reissue", "/university/detail/**", "/university/search/**", "/university/recommends", - "/swagger-ui/**", "/v3/api-docs/**", "/actuator/**" ) .permitAll() diff --git a/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java b/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java deleted file mode 100644 index 82902d32b..000000000 --- a/src/main/java/com/example/solidconnection/config/swagger/SwaggerConfig.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.solidconnection.config.swagger; - -import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; -import io.swagger.v3.oas.annotations.security.SecurityScheme; -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static io.swagger.v3.oas.annotations.enums.SecuritySchemeType.HTTP; - -@Configuration -@SecurityScheme( - name = "access_token", - type = HTTP, - in = SecuritySchemeIn.HEADER, - scheme = "bearer", - bearerFormat = "JWT", - paramName = "Authorization", - description = "엑세스 토큰을 입력하세요. (Bearer 포함 X)" -) -public class SwaggerConfig { - - public static final String ACCESS_TOKEN = "access_token"; - - @Bean - public OpenAPI customOpenAPI() { - return new OpenAPI() - .components(new Components()) - .info(apiInfo()); - } - - private Info apiInfo() { - return new Info() - .title("솔리드 커넥션 API 문서✈️") - .description("솔리드 커넥션의 API 문서입니다. \n\"Authorize\" 버튼을 눌러 인증을 하면 인증이 필요한 API를 호출할 수 있습니다.") - .version("1.0.0"); - } -} diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index cd287809b..05cdfc574 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -9,8 +9,6 @@ import com.example.solidconnection.post.dto.PostUpdateRequest; import com.example.solidconnection.post.dto.PostUpdateResponse; import com.example.solidconnection.post.service.PostService; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -29,13 +27,9 @@ import java.util.Collections; import java.util.List; -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - @RestController @RequiredArgsConstructor @RequestMapping("/communities") -@SecurityRequirements -@SecurityRequirement(name = ACCESS_TOKEN) public class PostController { private final PostService postService; diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java index 2c513cb8c..0f32a4ab6 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -16,7 +16,7 @@ @RequiredArgsConstructor @RequestMapping("/file") @RestController -public class S3Controller implements S3ControllerSwagger { +public class S3Controller { private final S3Service s3Service; diff --git a/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java b/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java deleted file mode 100644 index bce6bfe15..000000000 --- a/src/main/java/com/example/solidconnection/s3/S3ControllerSwagger.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.example.solidconnection.s3; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.multipart.MultipartFile; - -import java.security.Principal; - -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - -@Tag(name = "ImageUpload", description = "S3 파일 업로드 API") -public interface S3ControllerSwagger { - - @Operation( - summary = "회원가입 전 프로필 이미지 업로드 - 프로필 이미지 설정", - requestBody = @RequestBody( - description = "업로드할 프로필 이미지 파일", - required = true, - content = @Content( - mediaType = "multipart/form-data", - schema = @Schema(implementation = MultipartFile.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "프로필 이미지 업로드 성공, URL 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UploadedFileUrlResponse.class) - ) - ) - } - ) - ResponseEntity uploadPreProfileImage(@RequestParam("file") MultipartFile imageFile); - - @SecurityRequirements - @SecurityRequirement(name = ACCESS_TOKEN) - @Operation( - summary = "회원가입 후 프로필 이미지 업로드 - 프로필 이미지 수정", - requestBody = @RequestBody( - description = "업로드할 프로필 이미지 파일", - required = true, - content = @Content( - mediaType = "multipart/form-data", - schema = @Schema(implementation = MultipartFile.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "프로필 이미지 업로드 성공 후 기존 이미지 삭제, URL 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UploadedFileUrlResponse.class) - ) - ) - } - ) - ResponseEntity uploadPostProfileImage(@RequestParam("file") MultipartFile imageFile, Principal principal); - - @SecurityRequirements - @SecurityRequirement(name = ACCESS_TOKEN) - @Operation( - summary = "GPA 증명서 이미지 업로드", - requestBody = @RequestBody( - description = "업로드할 GPA 증명서 이미지 파일", - required = true, - content = @Content( - mediaType = "multipart/form-data", - schema = @Schema(implementation = MultipartFile.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "GPA 증명서 이미지 업로드 성공, URL 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UploadedFileUrlResponse.class) - ) - ) - } - ) - ResponseEntity uploadGpaImage(@RequestParam("file") MultipartFile imageFile); - - @SecurityRequirements - @SecurityRequirement(name = ACCESS_TOKEN) - @Operation( - summary = "어학 시험 증명서 이미지 업로드", - requestBody = @RequestBody( - description = "업로드할 어학 시험 증명서 이미지 파일", - required = true, - content = @Content( - mediaType = "multipart/form-data", - schema = @Schema(implementation = MultipartFile.class) - ) - ), - responses = { - @ApiResponse( - responseCode = "200", - description = "어학 시험 증명서 이미지 업로드 성공, URL 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UploadedFileUrlResponse.class) - ) - ) - } - ) - ResponseEntity uploadLanguageImage(@RequestParam("file") MultipartFile imageFile); - - @SecurityRequirements - @SecurityRequirement(name = ACCESS_TOKEN) - @Operation( - summary = "S3 url prefix 확인", - responses = { - @ApiResponse( - responseCode = "200", - description = "S3 url prefix 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = urlPrefixResponse.class) - ) - ) - } - ) - ResponseEntity getS3UrlPrefix(); -} diff --git a/src/main/java/com/example/solidconnection/s3/UploadedFileUrlResponse.java b/src/main/java/com/example/solidconnection/s3/UploadedFileUrlResponse.java index 090f206f7..6d9b690fa 100644 --- a/src/main/java/com/example/solidconnection/s3/UploadedFileUrlResponse.java +++ b/src/main/java/com/example/solidconnection/s3/UploadedFileUrlResponse.java @@ -1,9 +1,5 @@ package com.example.solidconnection.s3; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "업로드된 파일의 URL 응답") public record UploadedFileUrlResponse( - @Schema(description = "파일 URL", example = "http://example.com/uploads/profile.jpg") String fileUrl) { } diff --git a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java index 7550102aa..42ee7b009 100644 --- a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java +++ b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java @@ -5,8 +5,6 @@ import com.example.solidconnection.score.dto.LanguageTestScoreRequest; import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; import com.example.solidconnection.score.service.ScoreService; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -18,13 +16,9 @@ import java.security.Principal; -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - @RestController @RequestMapping("/score") @RequiredArgsConstructor -@SecurityRequirements -@SecurityRequirement(name = ACCESS_TOKEN) public class ScoreController { private final ScoreService scoreService; diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java index b655f6143..5227ba9ed 100644 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java @@ -1,28 +1,22 @@ package com.example.solidconnection.score.dto; import com.example.solidconnection.application.domain.Gpa; -import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -@Schema(description = "대학 성적과 어학 시험 성적") public record GpaScoreRequest( @NotNull(message = "학점을 입력해주세요.") - @Schema(description = "GPA", example = "3.5", required = true) Double gpa, @NotNull(message = "학점 기준을 입력해주세요.") - @Schema(description = "GPA 계산 기준", example = "4.0", required = true) Double gpaCriteria, @NotNull(message = "발급일자를 입력해주세요.") - @Schema(description = "발급일자", example = "2024-10-06", required = true) LocalDate issueDate, @NotBlank(message = "대학 성적 증명서를 첨부해주세요.") - @Schema(description = "대학 성적 증명서 URL", example = "http://example.com/gpa-report.pdf", required = true) String gpaReportUrl) { public Gpa toGpa() { diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java index 36687b6c2..c39e5fcb9 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java @@ -3,28 +3,22 @@ import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.type.LanguageTestType; -import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -@Schema(description = "대학 성적과 어학 시험 성적") public record LanguageTestScoreRequest( @NotNull(message = "어학 종류를 입력해주세요.") - @Schema(description = "어학 시험 종류", example = "TOEFL", required = true) LanguageTestType languageTestType, @NotBlank(message = "어학 점수를 입력해주세요.") - @Schema(description = "어학 시험 점수", example = "115", required = true) String languageTestScore, @NotNull(message = "발급일자를 입력해주세요.") - @Schema(description = "발급일자", example = "2024-10-06", required = true) LocalDate issueDate, @NotBlank(message = "어학 증명서를 첨부해주세요.") - @Schema(description = "어학 증명서 URL", example = "http://example.com/test-report.pdf", required = true) String languageTestReportUrl) { public LanguageTest toLanguageTest() { diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java index d03c69fa8..c0d58356f 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -22,7 +22,7 @@ @RequiredArgsConstructor @RequestMapping("/my-page") @RestController -class SiteUserController implements SiteUserControllerSwagger { +class SiteUserController { private final SiteUserService siteUserService; diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java deleted file mode 100644 index 260610436..000000000 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserControllerSwagger.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.solidconnection.siteuser.controller; - -import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; - -import java.security.Principal; - -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - -@Tag(name = "SiteUser", description = "사용자 API") -@SecurityRequirements -@SecurityRequirement(name = ACCESS_TOKEN) -public interface SiteUserControllerSwagger { - - @Operation( - summary = "마이 페이지 페이지 정보 조회", - responses = { - @ApiResponse( - responseCode = "200", - description = "마이 페이지 정보 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = MyPageResponse.class) - ) - ) - } - ) - ResponseEntity getMyPageInfo(Principal principal); - - @Operation( - summary = "마이 페이지 정보 수정을 위한 데이터 조회", - responses = { - @ApiResponse( - responseCode = "200", - description = "수정 가능한 정보 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = MyPageUpdateResponse.class) - ) - ) - } - ) - ResponseEntity getMyPageInfoToUpdate(Principal principal); -} diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java index 88396f5b9..66a7dbef2 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java @@ -2,32 +2,15 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.Role; -import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "마이페이지 페이지 정보 응답") public record MyPageResponse( - @Schema(description = "닉네임", example = "nickname") String nickname, - - @Schema(description = "프로필 이미지 URL", example = "http://example.com/profile.jpg") String profileImageUrl, - - @Schema(description = "역할", example = "MENTEE") Role role, - - @Schema(description = "생년월일", example = "1990-01-01") String birth, - - @Schema(description = "이메일", example = "example@solid-conenct.net") String email, - - @Schema(description = "좋아요 누른 게시물 수", example = "0") int likedPostCount, - - @Schema(description = "좋아요 누른 멘토 수", example = "0") int likedMentorCount, - - @Schema(description = "좋아요 누른 대학 수", example = "3") int likedUniversityCount) { public static MyPageResponse of(SiteUser siteUser, int likedUniversityCount) { diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateRequest.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateRequest.java index bb40e075d..11584d163 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateRequest.java @@ -1,16 +1,12 @@ package com.example.solidconnection.siteuser.dto; import com.example.solidconnection.siteuser.domain.SiteUser; -import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -@Schema(description = "마이 페이지 정보 수정 요청") public record MyPageUpdateRequest( @NotBlank(message = "닉네임을 입력해주세요.") - @Schema(description = "변경할 닉네임", example = "NewNickname") String nickname, - @Schema(description = "변경할 프로필 이미지 URL", example = "http://example.com/new-profile.jpg", nullable = true) String profileImageUrl) { public static MyPageUpdateRequest from(SiteUser siteUser) { diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java index 5572d5a2d..78583405b 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageUpdateResponse.java @@ -1,14 +1,9 @@ package com.example.solidconnection.siteuser.dto; import com.example.solidconnection.siteuser.domain.SiteUser; -import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "마이 페이지 정보 수정 응답") public record MyPageUpdateResponse( - @Schema(description = "업데이트된 사용자 닉네임", example = "UpdatedNickname") String nickname, - - @Schema(description = "업데이트된 프로필 이미지 URL", example = "http://example.com/updated-profile.jpg") String profileImageUrl) { public static MyPageUpdateResponse from(SiteUser siteUser) { diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateRequest.java b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateRequest.java index 4627a7451..9b83969b4 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateRequest.java @@ -1,11 +1,9 @@ package com.example.solidconnection.siteuser.dto; -import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; public record NicknameUpdateRequest( @NotBlank(message = "닉네임을 입력해주세요.") - @Schema(description = "변경할 닉네임", example = "NewNickname") String nickname ) { } diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateResponse.java index 5a967fa6e..a59e71824 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateResponse.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/NicknameUpdateResponse.java @@ -1,10 +1,8 @@ package com.example.solidconnection.siteuser.dto; import com.example.solidconnection.siteuser.domain.SiteUser; -import io.swagger.v3.oas.annotations.media.Schema; public record NicknameUpdateResponse( - @Schema(description = "업데이트된 사용자 닉네임", example = "UpdatedNickname") String nickname ) { public static NicknameUpdateResponse from(SiteUser siteUser) { diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/ProfileImageUpdateResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/ProfileImageUpdateResponse.java index 127578e2f..d806fde20 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/ProfileImageUpdateResponse.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/ProfileImageUpdateResponse.java @@ -1,10 +1,8 @@ package com.example.solidconnection.siteuser.dto; import com.example.solidconnection.siteuser.domain.SiteUser; -import io.swagger.v3.oas.annotations.media.Schema; public record ProfileImageUpdateResponse( - @Schema(description = "업데이트된 프로필 이미지 URL", example = "http://example.com/updated-profile.jpg") String profileImageUrl ) { public static ProfileImageUpdateResponse from(SiteUser siteUser) { diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 4d0609549..2bab9da1a 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -24,7 +24,7 @@ @RequiredArgsConstructor @RequestMapping("/university") @RestController -public class UniversityController implements UniversityControllerSwagger { +public class UniversityController { private final UniversityService universityService; private final UniversityRecommendService universityRecommendService; diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityControllerSwagger.java b/src/main/java/com/example/solidconnection/university/controller/UniversityControllerSwagger.java deleted file mode 100644 index e140917f2..000000000 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityControllerSwagger.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.example.solidconnection.university.controller; - -import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.university.dto.IsLikeResponse; -import com.example.solidconnection.university.dto.LikeResultResponse; -import com.example.solidconnection.university.dto.UniversityDetailResponse; -import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; -import com.example.solidconnection.university.dto.UniversityRecommendsResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityRequirements; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; - -import java.security.Principal; -import java.util.List; - -import static com.example.solidconnection.config.swagger.SwaggerConfig.ACCESS_TOKEN; - -@Tag(name = "University", description = "대학 및 대학 지원을 위한 정보 API") -@SecurityRequirements -@SecurityRequirement(name = ACCESS_TOKEN) -public interface UniversityControllerSwagger { - - @Operation( - summary = "대학 추천 목록 조회", - responses = { - @ApiResponse( - responseCode = "200", - description = "사용자별 개인화 된 대학 추천 목록 또는 일반 추천 목록 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UniversityRecommendsResponse.class) - ) - ) - } - ) - ResponseEntity getUniversityRecommends(Principal principal); - - @Operation( - summary = "좋아요한 대학 목록 조회", - responses = { - @ApiResponse( - responseCode = "200", - description = "사용자가 좋아요한 대학 목록 반환", - content = @Content( - mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = UniversityInfoForApplyPreviewResponse.class)) - ) - ) - } - ) - ResponseEntity> getMyWishUniversity(Principal principal); - - @Operation( - summary = "대학 좋아요 여부 확인", - responses = { - @ApiResponse( - responseCode = "200", - description = "대학 좋아요 여부 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = IsLikeResponse.class) - ) - ) - } - ) - ResponseEntity getIsLiked(Principal principal, @PathVariable Long universityInfoForApplyId); - - @Operation( - summary = "대학 좋아요 하기", - responses = { - @ApiResponse( - responseCode = "200", - description = "대학 좋아요 결과 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = LikeResultResponse.class) - ) - ) - } - ) - ResponseEntity addWishUniversity(Principal principal, @PathVariable Long universityInfoForApplyId); - - @Operation( - summary = "대학 상세 정보 조회", - responses = { - @ApiResponse( - responseCode = "200", - description = "대학 상세 정보 반환", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = UniversityDetailResponse.class) - ) - ) - } - ) - ResponseEntity getUniversityDetails(@PathVariable Long universityInfoForApplyId); - - @Operation( - summary = "대학 검색", - responses = { - @ApiResponse( - responseCode = "200", - description = "검색 조건에 맞는 대학 목록 반환", - content = @Content( - mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = UniversityInfoForApplyPreviewResponse.class)) - ) - ) - } - ) - ResponseEntity> searchUniversity( - @RequestParam(required = false, defaultValue = "") String region, - @RequestParam(required = false, defaultValue = "") List keyword, - @RequestParam(required = false, defaultValue = "") LanguageTestType testType, - @RequestParam(required = false, defaultValue = "") String testScore); -} diff --git a/src/main/java/com/example/solidconnection/university/dto/IsLikeResponse.java b/src/main/java/com/example/solidconnection/university/dto/IsLikeResponse.java index bd62505bd..7d4aebbf9 100644 --- a/src/main/java/com/example/solidconnection/university/dto/IsLikeResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/IsLikeResponse.java @@ -1,9 +1,5 @@ package com.example.solidconnection.university.dto; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "대학교 '좋아요' 여부") public record IsLikeResponse( - @Schema(description = "대학교 '좋아요' 여부", example = "true") boolean isLike) { } diff --git a/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementResponse.java b/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementResponse.java index 7b02108a2..8cc7b9733 100644 --- a/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/LanguageRequirementResponse.java @@ -2,14 +2,9 @@ import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.domain.LanguageRequirement; -import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "어학 성적 요구사항 응답") public record LanguageRequirementResponse( - @Schema(description = "어학 시험 유형", example = "TOEFL_IBT") LanguageTestType languageTestType, - - @Schema(description = "최소 점수 요구사항", example = "100") String minScore) implements Comparable { public static LanguageRequirementResponse from(LanguageRequirement languageRequirement) { diff --git a/src/main/java/com/example/solidconnection/university/dto/LikeResultResponse.java b/src/main/java/com/example/solidconnection/university/dto/LikeResultResponse.java index 29e81bc95..c67f2e408 100644 --- a/src/main/java/com/example/solidconnection/university/dto/LikeResultResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/LikeResultResponse.java @@ -1,9 +1,5 @@ package com.example.solidconnection.university.dto; -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "좋아요 결과 응답 데이터") public record LikeResultResponse( - @Schema(description = "좋아요 결과", example = "LIKE_SUCCESS") String result) { } diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java index ed4c6109b..4121654a3 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityDetailResponse.java @@ -2,89 +2,35 @@ import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; -@Schema(description = "대학 세부 사항 응답 데이터") public record UniversityDetailResponse( - @Schema(description = "대학 지원을 위한 정보 id", example = "1") long id, - - @Schema(description = "모집 시기", example = "2024-2") String term, - - @Schema(description = "국문 이름", example = "그라츠 대학") String koreanName, - - @Schema(description = "영문 이름", example = "University of Graz") String englishName, - - @Schema(description = "서브스에서 사용되는 이름", example = "university_of_graz") String formatName, - - @Schema(description = "지역", example = "유럽") String region, - - @Schema(description = "국가", example = "오스트리아") String country, - - @Schema(description = "대학 홈페이지 URL", example = "http://www.graz.ac.kr") String homepageUrl, - - @Schema(description = "대학 로고 이미지 URL", example = "http://example.com/logo.jpg") String logoImageUrl, - - @Schema(description = "대학 배경 이미지 URL", example = "http://example.com/background.jpg") String backgroundImageUrl, - - @Schema(description = "현지에 대한 세부 사항", example = "Detailed information about local conditions.") String detailsForLocal, - - @Schema(description = "모집 인원", example = "2") int studentCapacity, - - @Schema(description = "등록금 유형", example = "본교납부형") String tuitionFeeType, - - @Schema(description = "파견 가능 학기", example = "1") String semesterAvailableForDispatch, - - @ArraySchema(arraySchema = @Schema(description = "어학 성적 요구사항")) List languageRequirements, - - @Schema(description = "어학 성적 세부 사항", example = "Minimum TOEFL score required is 80.") String detailsForLanguage, - - @Schema(description = "GPA", example = "3.5") String gpaRequirement, - - @Schema(description = "GPA 계산 기준", example = "4.0") String gpaRequirementCriteria, - - @Schema(description = "필요 학기", example = "2") String semesterRequirement, - - @Schema(description = "지원에 대한 세부 사항", example = "Application process detailed here.") String detailsForApply, - - @Schema(description = "전공에 대한 세부 사항", example = "Major requirements detailed here.") String detailsForMajor, - - @Schema(description = "숙박에 대한 세부 사항", example = "Accommodation details provided.") String detailsForAccommodation, - - @Schema(description = "영어 과정 세부 사항", example = "English courses available for international students.") String detailsForEnglishCourse, - - @Schema(description = "기타 세부 사항", example = "Additional university details.") String details, - - @Schema(description = "숙박 시설 URL", example = "http://example.com/accommodation") String accommodationUrl, - - @Schema(description = "영어 수업 정보 URL", example = "http://example.com/englishCourses") String englishCourseUrl) { public static UniversityDetailResponse of( diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java index de0987eaf..93214b056 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java @@ -1,36 +1,18 @@ package com.example.solidconnection.university.dto; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; import java.util.Collections; import java.util.List; -@Schema(description = "대학 미리보기 응답") public record UniversityInfoForApplyPreviewResponse( - @Schema(description = "대학 지원을 위한 정보 id", example = "1") long id, - - @Schema(description = "모집 시기", example = "2024-2") String term, - - @Schema(description = "국문 이름", example = "그라츠대학") String koreanName, - - @Schema(description = "지역", example = "유럽") String region, - - @Schema(description = "국가", example = "오스트리아") String country, - - @Schema(description = "대학 로고 이미지 URL", example = "http://example.com/logo.jpg") String logoImageUrl, - - @Schema(description = "모집 인원", example = "2") int studentCapacity, - - @ArraySchema(arraySchema = @Schema(description = "어학시험 요구사항")) List languageRequirements) { public static UniversityInfoForApplyPreviewResponse from(UniversityInfoForApply universityInfoForApply) { diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityRecommendsResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityRecommendsResponse.java index 947a7e78c..057061f3e 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityRecommendsResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityRecommendsResponse.java @@ -1,12 +1,7 @@ package com.example.solidconnection.university.dto; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; - import java.util.List; -@Schema(description = "추천 대학 목록 응답") public record UniversityRecommendsResponse( - @ArraySchema(arraySchema = @Schema(description = "추천된 대학 목록", implementation = UniversityInfoForApplyPreviewResponse.class)) List recommendedUniversities) { } diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 7472616f6..62fae2731 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -76,7 +76,7 @@ class SignUpTest extends BaseEndToEndTest { List interestedRegionNames = List.of("유럽"); List interestedCountryNames = List.of("프랑스", "독일"); SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, interestedRegionNames, interestedCountryNames, - PreparationStatus.CONSIDERING, "nickname", "profile", Gender.FEMALE, "2000-01-01"); + PreparationStatus.CONSIDERING, "nickname", Gender.FEMALE, "profile", "2000-01-01"); SignUpResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) @@ -127,7 +127,7 @@ class SignUpTest extends BaseEndToEndTest { // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, - PreparationStatus.CONSIDERING, alreadyExistNickname, "profile", Gender.FEMALE, "2000-01-01"); + PreparationStatus.CONSIDERING, alreadyExistNickname, Gender.FEMALE, "profile", "2000-01-01"); ErrorResponse errorResponse = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) @@ -153,7 +153,7 @@ class SignUpTest extends BaseEndToEndTest { // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, - PreparationStatus.CONSIDERING, "nickname0", "profile", Gender.FEMALE, "2000-01-01"); + PreparationStatus.CONSIDERING, "nickname0", Gender.FEMALE, "profile", "2000-01-01"); ErrorResponse errorResponse = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) @@ -169,8 +169,7 @@ class SignUpTest extends BaseEndToEndTest { @Test void 유효하지_않은_카카오_토큰으로_회원가입을_하면_예외를_응답한다() { SignUpRequest signUpRequest = new SignUpRequest("invalid", null, null, - PreparationStatus.CONSIDERING, "nickname", "profile", Gender.FEMALE, "2000-01-01"); - + PreparationStatus.CONSIDERING, "nickname", Gender.FEMALE, "profile", "2000-01-01"); ErrorResponse errorResponse = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) From d335cdebc2c230804281904f74393c4fac5e5534 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 12 Dec 2024 18:05:44 +0900 Subject: [PATCH 113/158] =?UTF-8?q?chore:=20=EC=84=9C=EB=B8=8C=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=A0=81=EC=9A=A9=20(#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 3 +++ src/main/resources/secret | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 src/main/resources/secret diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..a7033a673 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/main/resources/secret"] + path = src/main/resources/secret + url = https://github.com/solid-connection/solid-connect-secret diff --git a/src/main/resources/secret b/src/main/resources/secret new file mode 160000 index 000000000..82f7ec98b --- /dev/null +++ b/src/main/resources/secret @@ -0,0 +1 @@ +Subproject commit 82f7ec98b47aa3da3874d0ac2d94f91784078b13 From c0426c93df476197103ebc0cf4280516c8adba18 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:52:02 +0900 Subject: [PATCH 114/158] =?UTF-8?q?chore:=20=ED=98=B8=EC=8A=A4=ED=8C=85=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=B3=80=EA=B2=BD=EA=B3=BC=20ngi?= =?UTF-8?q?nx=20=EC=A0=9C=EA=B1=B0=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(#118)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: nginx docker-compose에서 제거 * chore: cross-origin 허용 url 전환 * chore: nginx 제거와 docker-compose 명령 변경에 따른 workflow 수정 * feat: CorsPropertiesConfig 추가, cors allowed origins 정보를 yml로 분리한다 * style: 주석 삭제 --- .github/workflows/release.yml | 13 ++----------- docker-compose.yml | 18 +++--------------- nginx.conf => docs/nginx.conf | 12 ++++-------- .../config/cors/CorsPropertiesConfig.java | 17 +++++++++++++++++ .../solidconnection/config/cors/WebConfig.java | 6 +++++- .../config/security/SecurityConfiguration.java | 4 +++- 6 files changed, 34 insertions(+), 36 deletions(-) rename nginx.conf => docs/nginx.conf (75%) create mode 100644 src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bf1a0ddd..0dbdd061d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,15 +63,6 @@ jobs: key: ${{ secrets.PRIVATE_KEY }} source: "./docker-compose.yml" target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - - - name: Copy nginx configuration file to remote - uses: appleboy/scp-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.PRIVATE_KEY }} - source: "./nginx.conf" - target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - name: Run docker compose uses: appleboy/ssh-action@master @@ -82,5 +73,5 @@ jobs: script_stop: true script: | cd /home/${{ secrets.USERNAME }}/solid-connect-server - docker-compose down - docker-compose up -d --build + docker compose down + docker compose up -d --build diff --git a/docker-compose.yml b/docker-compose.yml index e7358d2b2..8813a4e58 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,27 +17,15 @@ services: depends_on: - redis - solid-connect-server: + solid-connection-server: build: context: . dockerfile: Dockerfile - container_name: solid-connect-server + container_name: solid-connection-server ports: - "8080:8080" environment: - SPRING_DATA_REDIS_HOST=redis - SPRING_DATA_REDIS_PORT=6379 depends_on: - - redis - - nginx: - image: nginx:latest - container_name: nginx - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/conf.d/default.conf - - /etc/letsencrypt:/etc/letsencrypt - depends_on: - - solid-connect-server + - redis \ No newline at end of file diff --git a/nginx.conf b/docs/nginx.conf similarity index 75% rename from nginx.conf rename to docs/nginx.conf index e94acb4e3..303463bce 100644 --- a/nginx.conf +++ b/docs/nginx.conf @@ -3,7 +3,7 @@ server { # http를 사용하는 경우 주석 해제 # location / { -# proxy_pass http://solid-connect-server:8080; +# proxy_pass http://solid-connection-server:8080; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -18,8 +18,8 @@ server { server { listen 443 ssl; - ssl_certificate /etc/letsencrypt/live/api.solid-connect.net/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.solid-connect.net/privkey.pem; + ssl_certificate /etc/letsencrypt/live/api.solid-connection.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.solid-connection.com/privkey.pem; client_max_body_size 10M; ssl_protocols TLSv1.2 TLSv1.3; @@ -31,14 +31,10 @@ server { ssl_stapling_verify on; location / { - proxy_pass http://solid-connect-server:8080; + proxy_pass http://solid-connection-server:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - - location ~ /.well-known/acme-challenge { # 인증서 갱신에 필요한 경로 설정 - allow all; - } } \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java b/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java new file mode 100644 index 000000000..55e47bd90 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.config.cors; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Getter +@Setter +@ConfigurationProperties(prefix = "cors") +@Configuration +public class CorsPropertiesConfig { + + private List allowedOrigins; +} \ No newline at end of file diff --git a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java index 9143d6558..00f3cf411 100644 --- a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java +++ b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java @@ -1,16 +1,20 @@ package com.example.solidconnection.config.cors; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration +@RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + private final CorsPropertiesConfig corsProperties; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:8080", "http://localhost:3000", "https://www.solid-connect.net") + .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) .allowedMethods("*") .allowedHeaders("*") .allowCredentials(true); diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 449bdd35c..70bcf6c37 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.config.cors.CorsPropertiesConfig; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -23,11 +24,12 @@ public class SecurityConfiguration { private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CorsPropertiesConfig corsProperties; @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("https://www.solid-connect.net", "http://localhost:8080", "https://www.api.solid-connect.net", "http://localhost:3000")); + configuration.setAllowedOrigins(corsProperties.getAllowedOrigins()); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); From 838ebb702a7a1006b0a2ebd91d8e35e215f92b81 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:15:22 +0900 Subject: [PATCH 115/158] =?UTF-8?q?chore:=20workflows/release=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수동으로 환경변수에 지정한 yml을 불러오는 것이 아닌, 서브모듈 불러오게 수정 #54 가 적용되었다 생각했는데, closed 되어있어 착각했습니다.. --- .github/workflows/release.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0dbdd061d..e7262baa7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,13 +13,11 @@ jobs: steps: - uses: actions/checkout@v4 + with: + - name: Set secret files + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} + submodules: true - - name: Create application-secret.yml - run: echo "${{ secrets.APPLICATION_SECRET }}" > src/main/resources/application-secret.yml - - - name: Create application-prod.yml - run: echo "${{ secrets.APPLICATION_PROD }}" > src/main/resources/application-prod.yml - - name: Set up JDK 17 uses: actions/setup-java@v4 with: From d004bd6206f79a04c2e2acdcc8b7b652a354d787 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:26:29 +0900 Subject: [PATCH 116/158] chore: fix workflow/release (#124) --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7262baa7..43c2e6e80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,9 +12,9 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - name: Checkout the code + uses: actions/checkout@v4 with: - - name: Set secret files token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} submodules: true From 5384a678465d9b77894df6fd0c7ab9de950b800c Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:12:53 +0900 Subject: [PATCH 117/158] chore: update submodule solid-connect-secret to the latest commit (#125) --- src/main/resources/secret | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/secret b/src/main/resources/secret index 82f7ec98b..907aff6fb 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 82f7ec98b47aa3da3874d0ac2d94f91784078b13 +Subproject commit 907aff6fb175a1a783d2235761276f49f40ab739 From d91902e985599767066275d6b4b263f967194567 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:17:37 +0900 Subject: [PATCH 118/158] =?UTF-8?q?chore:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20uri=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/secret | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/secret b/src/main/resources/secret index 907aff6fb..5c716274e 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 907aff6fb175a1a783d2235761276f49f40ab739 +Subproject commit 5c716274e096bd349d71d3ced1644bfb08ad0912 From 874bdc391f46e93eb576dafebc0b3fba0194e84c Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 17 Dec 2024 00:30:42 +0900 Subject: [PATCH 119/158] =?UTF-8?q?fix:=20=EC=A0=84=EC=B2=B4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=ED=86=B5=EA=B3=BC?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: tiny int 제거 * test: local -> dev 로 명칭 변경된 것 적용 * test: 불필요한 코드 제거 * test: dto 생성자의 인자 순서를 올바르게 변경 --- .../solidconnection/application/domain/Application.java | 4 ++-- .../java/com/example/solidconnection/e2e/SignUpTest.java | 8 ++++---- .../unit/repository/CommentRepositoryTest.java | 8 +------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java index 7faf77e6e..61dc5159e 100644 --- a/src/main/java/com/example/solidconnection/application/domain/Application.java +++ b/src/main/java/com/example/solidconnection/application/domain/Application.java @@ -52,8 +52,8 @@ public class Application { @Column(length = 50, nullable = false) private String term; - @Column(columnDefinition = "TINYINT(1) NOT NULL DEFAULT 0") - private Boolean isDelete; + @Column + private boolean isDelete = false; @ManyToOne(fetch = FetchType.LAZY) private UniversityInfoForApply firstChoiceUniversity; diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 62fae2731..2da99def8 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -76,7 +76,7 @@ class SignUpTest extends BaseEndToEndTest { List interestedRegionNames = List.of("유럽"); List interestedCountryNames = List.of("프랑스", "독일"); SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, interestedRegionNames, interestedCountryNames, - PreparationStatus.CONSIDERING, "nickname", Gender.FEMALE, "profile", "2000-01-01"); + PreparationStatus.CONSIDERING, "profile", Gender.FEMALE, "nickname", "2000-01-01"); SignUpResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) @@ -127,7 +127,7 @@ class SignUpTest extends BaseEndToEndTest { // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, - PreparationStatus.CONSIDERING, alreadyExistNickname, Gender.FEMALE, "profile", "2000-01-01"); + PreparationStatus.CONSIDERING, "profile", Gender.FEMALE, alreadyExistNickname, "2000-01-01"); ErrorResponse errorResponse = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) @@ -153,7 +153,7 @@ class SignUpTest extends BaseEndToEndTest { // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, - PreparationStatus.CONSIDERING, "nickname0", Gender.FEMALE, "profile", "2000-01-01"); + PreparationStatus.CONSIDERING, "profile", Gender.FEMALE, "nickname0", "2000-01-01"); ErrorResponse errorResponse = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) @@ -169,7 +169,7 @@ class SignUpTest extends BaseEndToEndTest { @Test void 유효하지_않은_카카오_토큰으로_회원가입을_하면_예외를_응답한다() { SignUpRequest signUpRequest = new SignUpRequest("invalid", null, null, - PreparationStatus.CONSIDERING, "nickname", Gender.FEMALE, "profile", "2000-01-01"); + PreparationStatus.CONSIDERING, "profile", Gender.FEMALE, "nickname", "2000-01-01"); ErrorResponse errorResponse = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java index 7029dead9..a53037346 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java @@ -13,7 +13,6 @@ import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; -import jakarta.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -31,7 +30,7 @@ @SpringBootTest -@ActiveProfiles("local") +@ActiveProfiles("dev") @DisplayName("댓글 레포지토리 테스트") class CommentRepositoryTest { @Autowired @@ -41,8 +40,6 @@ class CommentRepositoryTest { @Autowired private SiteUserRepository siteUserRepository; @Autowired - private EntityManager entityManager; - @Autowired private CommentRepository commentRepository; private Board board; @@ -66,9 +63,6 @@ public void setUp() { childComment = createChildComment(); commentRepository.save(parentComment); commentRepository.save(childComment); - - entityManager.flush(); - entityManager.clear(); } private Board createBoard() { From 60d9333798aa73f42951066d0645123c747420d6 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Fri, 27 Dec 2024 21:04:22 +0900 Subject: [PATCH 120/158] =?UTF-8?q?chore:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EA=B0=9C=ED=96=89=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style: 개행 수정 - 불필요한 개행 제거 - EOF 개행 추가 * chore: 오타 수정 - down 하는 곳에서 Starting 이라 에코하고 있던 것을 수정 * refactor: docker-compose 를 docker compose 로 --- README.md | Bin 1154 -> 1154 bytes local_compose_down.sh | 8 ++++---- local_compose_up.sh | 4 ++-- .../config/cors/CorsPropertiesConfig.java | 2 +- .../siteuser/domain/SiteUser.java | 1 - 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index acdcee3b78f7518548a07db083f5cd2070bac25a..a07f6d77b95a903a0b2ef7d7e620278d8a540a83 100644 GIT binary patch delta 13 UcmZqTY~q}-ZepF##si0#0VPuftN;K2 delta 12 TcmZqTY~q}-j!}2x`pe7!9FGLK diff --git a/local_compose_down.sh b/local_compose_down.sh index e45cda18f..32792e490 100755 --- a/local_compose_down.sh +++ b/local_compose_down.sh @@ -2,11 +2,11 @@ set -e -echo "Starting all docker containers..." -docker-compose -f docker-compose.local.yml down +echo "Stopping all docker containers..." +docker compose -f docker-compose.local.yml down echo "Pruning unused Docker images..." docker image prune -f -echo "Containers are up and running." -docker-compose ps -a +echo "Containers are down and not running." +docker compose ps -a diff --git a/local_compose_up.sh b/local_compose_up.sh index 67ef9d0ba..400861e57 100755 --- a/local_compose_up.sh +++ b/local_compose_up.sh @@ -14,10 +14,10 @@ if [ ! -d "redis_data_local" ]; then fi echo "Starting all docker containers..." -docker-compose -f docker-compose.local.yml up -d +docker compose -f docker-compose.local.yml up -d echo "Pruning unused Docker images..." docker image prune -f echo "Containers are up and running." -docker-compose ps -a +docker compose ps -a diff --git a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java b/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java index 55e47bd90..68144d733 100644 --- a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java +++ b/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java @@ -14,4 +14,4 @@ public class CorsPropertiesConfig { private List allowedOrigins; -} \ No newline at end of file +} diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index d548ff852..e518a5efb 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -85,7 +85,6 @@ public class SiteUser { @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List gpaScoreList = new ArrayList<>(); - public SiteUser( String email, String nickname, From 972c47ccd2d4e2b5fc3dc1cbdc011404c11f0d42 Mon Sep 17 00:00:00 2001 From: Wibaek Park <34394229+wibaek@users.noreply.github.com> Date: Sun, 5 Jan 2025 20:19:42 +0900 Subject: [PATCH 121/158] =?UTF-8?q?chore:=20flyway=20=EB=8F=84=EC=9E=85=20?= =?UTF-8?q?(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: flyway 추가 * chore: v2 마이그레이션 파일 추가 - gpa_score, language_test_score 테이블 추가 - 이름이 맞지 않는 FK 이름 수정 - 누락되었던 FK 추가 - application.is_delete 추가 * chore: flyway 환경설정 추가 * fix: 오류 설정 수정 * chore: flyway-mysql 추가 * fix: 마이그레이션 sql에서 FK 명 오타 수정, 현재 존재하는 데이터 정합성과 맞지 않은 쿼리 하나 비활성화 * style: 파일 마지막줄 개행 추가 * chore: V1__init.sql 추가 * fix: enum 누락 수정 --- build.gradle | 3 + src/main/resources/db/migration/V1__init.sql | 259 ++++++++++++++++++ ..._add_gpa_score_and_language_test_score.sql | 95 +++++++ src/main/resources/secret | 2 +- 4 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V1__init.sql create mode 100644 src/main/resources/db/migration/V2__add_gpa_score_and_language_test_score.sql diff --git a/build.gradle b/build.gradle index 5157e8f6c..ceba38c4b 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,9 @@ dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거 'jakarta.persistence:jakarta.persistence-api:3.1.0', 'jakarta.annotation:jakarta.annotation-api:2.1.1' ) + + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' } tasks.named('test') { diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 000000000..8910ad629 --- /dev/null +++ b/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,259 @@ +CREATE TABLE IF NOT EXISTS application +( + id BIGINT AUTO_INCREMENT NOT NULL, + term VARCHAR(50) NOT NULL, + site_user_id BIGINT NULL, + nickname_for_apply VARCHAR(100) NULL, + update_count INT DEFAULT 0 NOT NULL, + verify_status VARCHAR(50) DEFAULT 'PENDING' NOT NULL, + gpa DOUBLE NOT NULL, + gpa_criteria DOUBLE NOT NULL, + language_test_type ENUM ('CEFR','DALF','DELF','DUOLINGO','IELTS','JLPT','NEW_HSK','TCF','TEF','TOEFL_IBT','TOEFL_ITP','TOEIC') NOT NULL, + language_test_score VARCHAR(255) NOT NULL, + gpa_report_url VARCHAR(500) NOT NULL, + language_test_report_url VARCHAR(500) NOT NULL, + first_choice_university_id BIGINT NULL, + second_choice_university_id BIGINT NULL, + third_choice_university_id BIGINT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS board +( + code VARCHAR(20) NOT NULL, + korean_name VARCHAR(20) NOT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (code) +); + +CREATE TABLE IF NOT EXISTS comment +( + created_at datetime NULL, + id BIGINT AUTO_INCREMENT NOT NULL, + parent_id BIGINT NULL, + post_id BIGINT NULL, + site_user_id BIGINT NULL, + updated_at datetime NULL, + content VARCHAR(255) NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS country +( + code VARCHAR(2) NOT NULL, + region_code VARCHAR(10) NULL, + korean_name VARCHAR(100) NOT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (code) +); + +CREATE TABLE IF NOT EXISTS interested_country +( + country_code VARCHAR(2) NULL, + id BIGINT AUTO_INCREMENT NOT NULL, + site_user_id BIGINT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS interested_region +( + id BIGINT AUTO_INCREMENT NOT NULL, + site_user_id BIGINT NULL, + region_code VARCHAR(10) NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS language_requirement +( + id BIGINT AUTO_INCREMENT NOT NULL, + university_info_for_apply_id BIGINT NULL, + language_test_type ENUM ('CEFR','DALF','DELF','DUOLINGO','IELTS','JLPT','NEW_HSK','TCF','TEF','TOEFL_IBT','TOEFL_ITP','TOEIC') NOT NULL, + min_score VARCHAR(255) NOT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS liked_university +( + id BIGINT AUTO_INCREMENT NOT NULL, + site_user_id BIGINT NULL, + university_info_for_apply_id BIGINT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS post +( + is_question BIT(1) NULL, + created_at datetime NULL, + id BIGINT AUTO_INCREMENT NOT NULL, + like_count BIGINT NULL, + site_user_id BIGINT NULL, + updated_at datetime NULL, + view_count BIGINT NULL, + board_code VARCHAR(20) NULL, + content VARCHAR(1000) NULL, + category ENUM ('자유','전체','질문') NULL, + title VARCHAR(255) NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS post_image +( + id BIGINT AUTO_INCREMENT NOT NULL, + post_id BIGINT NULL, + url VARCHAR(500) NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS post_like +( + id BIGINT AUTO_INCREMENT NOT NULL, + post_id BIGINT NULL, + site_user_id BIGINT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS region +( + code VARCHAR(10) NOT NULL, + korean_name VARCHAR(100) NOT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (code) +); + +CREATE TABLE IF NOT EXISTS site_user +( + quited_at date NULL, + id BIGINT AUTO_INCREMENT NOT NULL, + nickname_modified_at datetime NULL, + birth VARCHAR(20) NOT NULL, + email VARCHAR(100) NOT NULL, + nickname VARCHAR(100) NOT NULL, + profile_image_url VARCHAR(500) NULL, + gender ENUM ('FEMALE','MALE','PREFER_NOT_TO_SAY') NOT NULL, + preparation_stage ENUM ('AFTER_EXCHANGE','CONSIDERING','PREPARING_FOR_DEPARTURE','STUDYING_ABROAD') NOT NULL, + `role` ENUM ('MENTEE','MENTOR') NOT NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS university +( + id BIGINT AUTO_INCREMENT NOT NULL, + region_code VARCHAR(10) NULL, + country_code VARCHAR(2) NULL, + format_name VARCHAR(100) NOT NULL, + english_name VARCHAR(100) NOT NULL, + korean_name VARCHAR(100) NOT NULL, + background_image_url VARCHAR(500) NOT NULL, + logo_image_url VARCHAR(500) NOT NULL, + details_for_local VARCHAR(1000) NULL, + homepage_url VARCHAR(500) NULL, + english_course_url VARCHAR(500) NULL, + accommodation_url VARCHAR(500) NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS university_info_for_apply +( + id BIGINT AUTO_INCREMENT NOT NULL, + term VARCHAR(50) NOT NULL, + university_id BIGINT NULL, + korean_name VARCHAR(100) NOT NULL, + student_capacity INT NULL, + tuition_fee_type ENUM ('HOME_UNIVERSITY_PAYMENT','MIXED_PAYMENT','OVERSEAS_UNIVERSITY_PAYMENT') NULL, + semester_available_for_dispatch ENUM ('FOUR_SEMESTER','IRRELEVANT','NO_DATA','ONE_OR_TWO_SEMESTER','ONE_SEMESTER','ONE_YEAR') NULL, + details_for_language VARCHAR(1000) NULL, + gpa_requirement VARCHAR(100) NULL, + gpa_requirement_criteria VARCHAR(100) NULL, + semester_requirement VARCHAR(100) NULL, + details_for_apply VARCHAR(1000) NULL, + details_for_major VARCHAR(1000) NULL, + details_for_english_course VARCHAR(1000) NULL, + details_for_accommodation VARCHAR(1000) NULL, + details VARCHAR(500) NULL, + CONSTRAINT `PRIMARY` PRIMARY KEY (id) +); + +ALTER TABLE site_user + ADD CONSTRAINT site_user_email_unique UNIQUE (email); + +ALTER TABLE comment + ADD CONSTRAINT FK11tfff2an5hdv747cktxbdi6t FOREIGN KEY (site_user_id) REFERENCES site_user (id) ON DELETE NO ACTION; + +CREATE INDEX FK11tfff2an5hdv747cktxbdi6t ON comment (site_user_id); + +ALTER TABLE interested_country + ADD CONSTRAINT FK26u5am55jefclcd7r5smk8ai7 FOREIGN KEY (site_user_id) REFERENCES site_user (id) ON DELETE NO ACTION; + +CREATE INDEX FK26u5am55jefclcd7r5smk8ai7 ON interested_country (site_user_id); + +ALTER TABLE interested_region + ADD CONSTRAINT FK7h2182pqkavi9d8o2pku6gidi FOREIGN KEY (region_code) REFERENCES region (code) ON DELETE NO ACTION; + +CREATE INDEX FK7h2182pqkavi9d8o2pku6gidi ON interested_region (region_code); + +ALTER TABLE interested_country + ADD CONSTRAINT FK7x4ad24lblkq2ss0920uqfd6s FOREIGN KEY (country_code) REFERENCES country (code) ON DELETE NO ACTION; + +CREATE INDEX FK7x4ad24lblkq2ss0920uqfd6s ON interested_country (country_code); + +ALTER TABLE university_info_for_apply + ADD CONSTRAINT FKd0257hco6uy2utd1xccjh3fal FOREIGN KEY (university_id) REFERENCES university (id) ON DELETE NO ACTION; + +CREATE INDEX FKd0257hco6uy2utd1xccjh3fal ON university_info_for_apply (university_id); + +ALTER TABLE post + ADD CONSTRAINT FKfu9q9o3mlqkd58wg45ykgu8ni FOREIGN KEY (site_user_id) REFERENCES site_user (id) ON DELETE NO ACTION; + +CREATE INDEX FKfu9q9o3mlqkd58wg45ykgu8ni ON post (site_user_id); + +ALTER TABLE post_like + ADD CONSTRAINT FKgx1v0whinnoqveopoh6tb4ykb FOREIGN KEY (site_user_id) REFERENCES site_user (id) ON DELETE NO ACTION; + +CREATE INDEX FKgx1v0whinnoqveopoh6tb4ykb ON post_like (site_user_id); + +ALTER TABLE liked_university + ADD CONSTRAINT FKhj3gn3mqmfeiiw9jt83g7t3rk FOREIGN KEY (university_info_for_apply_id) REFERENCES university_info_for_apply (id) ON DELETE NO ACTION; + +CREATE INDEX FKhj3gn3mqmfeiiw9jt83g7t3rk_idx ON liked_university (university_info_for_apply_id); + +ALTER TABLE interested_region + ADD CONSTRAINT FKia6h0pbisqhgm3lkeya6vqo4w FOREIGN KEY (site_user_id) REFERENCES site_user (id) ON DELETE NO ACTION; + +CREATE INDEX FKia6h0pbisqhgm3lkeya6vqo4w ON interested_region (site_user_id); + +ALTER TABLE country + ADD CONSTRAINT FKife035f2scmgcutdtv6bfd6g8 FOREIGN KEY (region_code) REFERENCES region (code) ON DELETE NO ACTION; + +CREATE INDEX FKife035f2scmgcutdtv6bfd6g8 ON country (region_code); + +ALTER TABLE university + ADD CONSTRAINT FKksoyt17h0te1ra588y4a3208r FOREIGN KEY (country_code) REFERENCES country (code) ON DELETE NO ACTION; + +CREATE INDEX FKksoyt17h0te1ra588y4a3208r ON university (country_code); + +ALTER TABLE university + ADD CONSTRAINT FKpwr8ocev54r8d22wdyj4a37bc FOREIGN KEY (region_code) REFERENCES region (code) ON DELETE NO ACTION; + +CREATE INDEX FKpwr8ocev54r8d22wdyj4a37bc ON university (region_code); + +ALTER TABLE language_requirement + ADD CONSTRAINT FKr75pgslwfbrvjkfau6dwtlg8l FOREIGN KEY (university_info_for_apply_id) REFERENCES university_info_for_apply (id) ON DELETE NO ACTION; + +CREATE INDEX FKr75pgslwfbrvjkfau6dwtlg8l ON language_requirement (university_info_for_apply_id); + +ALTER TABLE liked_university + ADD CONSTRAINT FKrrhud921brslcukx6fyuh0th3 FOREIGN KEY (site_user_id) REFERENCES site_user (id) ON DELETE NO ACTION; + +CREATE INDEX FKrrhud921brslcukx6fyuh0th3 ON liked_university (site_user_id); + +ALTER TABLE application + ADD CONSTRAINT FKs4s3hebtn7vwd0b4xt8msxsis FOREIGN KEY (site_user_id) REFERENCES site_user (id) ON DELETE NO ACTION; + +CREATE INDEX FKs4s3hebtn7vwd0b4xt8msxsis ON application (site_user_id); + +ALTER TABLE application + ADD CONSTRAINT fk_university_info_for_apply_id_1 FOREIGN KEY (first_choice_university_id) REFERENCES university_info_for_apply (id) ON DELETE NO ACTION; + +CREATE INDEX fk_university_info_for_apply_id_1 ON application (first_choice_university_id); + +ALTER TABLE application + ADD CONSTRAINT fk_university_info_for_apply_id_2 FOREIGN KEY (second_choice_university_id) REFERENCES university_info_for_apply (id) ON DELETE NO ACTION; + +CREATE INDEX fk_university_info_for_apply_id_2 ON application (second_choice_university_id); \ No newline at end of file diff --git a/src/main/resources/db/migration/V2__add_gpa_score_and_language_test_score.sql b/src/main/resources/db/migration/V2__add_gpa_score_and_language_test_score.sql new file mode 100644 index 000000000..ad3ab10b0 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_gpa_score_and_language_test_score.sql @@ -0,0 +1,95 @@ +create table gpa_score ( + gpa float(53) not null, + gpa_criteria float(53) not null, + issue_date date, + created_at datetime(6), + id bigint not null auto_increment, + site_user_id bigint, + updated_at datetime(6), + gpa_report_url varchar(500) not null, + rejected_reason varchar(255), + verify_status varchar(50) not null default 'PENDING', + primary key (id) +) engine=InnoDB; + +alter table gpa_score + add constraint FK2k65qncfxvol5j4l4hb7d6iv1 + foreign key (site_user_id) + references site_user (id); + +create table language_test_score ( + issue_date date, + created_at datetime(6), + id bigint not null auto_increment, + site_user_id bigint, + updated_at datetime(6), + language_test_type enum ('CEFR', 'DALF', 'DELF', 'DUOLINGO', 'IELTS', 'JLPT', 'NEW_HSK', 'TCF', 'TEF', 'TOEFL_IBT', 'TOEFL_ITP', 'TOEIC') not null, + language_test_report_url varchar(500) not null, + language_test_score varchar(255) not null, + rejected_reason varchar(255), + verify_status varchar(50) not null default 'PENDING', + primary key (id) +) engine=InnoDB; + +alter table language_test_score + add constraint FKt2uevj2r4iuxumblj5ofbgmqn + foreign key (site_user_id) + references site_user (id); + +alter table application add column is_delete bit; + +alter table application drop foreign key fk_university_info_for_apply_id_1; +alter table application drop foreign key fk_university_info_for_apply_id_2; + +alter table application + add constraint FKi822ljuirbu9o0lnd9jt7l7qg + foreign key (first_choice_university_id) + references university_info_for_apply (id); + +alter table application + add constraint FKepp2by7frnkt1o1w3v4t4lgtu + foreign key (second_choice_university_id) + references university_info_for_apply (id); + +alter table application + add constraint FKeajojvwgn069mfxhbq5ja1sws + foreign key (third_choice_university_id) + references university_info_for_apply (id); + +alter table comment + add constraint FKde3rfu96lep00br5ov0mdieyt + foreign key (parent_id) + references comment (id); + +alter table comment + add constraint FKs1slvnkuemjsq2kj4h3vhx7i1 + foreign key (post_id) + references post (id); + +alter table liked_university drop foreign key FKhj3gn3mqmfeiiw9jt83g7t3rk; +alter table liked_university drop foreign key FKrrhud921brslcukx6fyuh0th3; + +alter table liked_university + add constraint FKkuqxb64dnfrl7har8t5ionw83 + foreign key (site_user_id) + references site_user (id); + +alter table liked_university + add constraint FKo317gq6apc3a091w32qhidtjt + foreign key (university_info_for_apply_id) + references university_info_for_apply (id); + +alter table post + add constraint FKlpnkhhbfb3gg3tfreh2a7qh8b + foreign key (board_code) + references board (code); + +-- alter table post_image +-- add constraint FKsip7qv57jw2fw50g97t16nrjr +-- foreign key (post_id) +-- references post (id); + +alter table post_like + add constraint FKj7iy0k7n3d0vkh8o7ibjna884 + foreign key (post_id) + references post (id); diff --git a/src/main/resources/secret b/src/main/resources/secret index 5c716274e..b4f88d141 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 5c716274e096bd349d71d3ced1644bfb08ad0912 +Subproject commit b4f88d14185e2009e0793dfd16d22c2c3b9257ae From f25968c1456978a39ed4bc24778b593d09a5dd7b Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Wed, 8 Jan 2025 04:43:51 +0900 Subject: [PATCH 122/158] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: testcontainers 의존성 추가 * test: MySQL, Redis 테스트 컨테이너 추가 * test: 커스텀 어노테이션 생성 - 어노테이션으로 테스트 컨테이너를 사용할 수 있도록 * test: TestContainer 어노테이션 적용 * test: DatabaseCleaner에 MySQL 문법 적용 * test: 깨지는 테스트 수정 * test: 불필요한 테스트 disabled * chore: redis test container 의존성 제거 --- build.gradle | 13 +++++-- src/main/resources/secret | 2 +- .../PostLikeCountConcurrencyTest.java | 6 ++-- .../PostViewCountConcurrencyTest.java | 6 ++-- .../concurrency/ThunderingHerdTest.java | 6 ++-- .../database/DatabaseConnectionTest.java | 2 ++ .../database/RedisConnectionTest.java | 2 ++ .../solidconnection/e2e/BaseEndToEndTest.java | 6 ++-- .../solidconnection/e2e/SignUpTest.java | 2 +- .../e2e/UniversityDataSetUpEndToEndTest.java | 6 ++-- .../support/DatabaseCleaner.java | 9 ++--- .../support/MySQLTestContainer.java | 34 +++++++++++++++++++ .../support/RedisTestContainer.java | 28 +++++++++++++++ .../support/TestContainerDataJpaTest.java | 22 ++++++++++++ .../support/TestContainerSpringBootTest.java | 22 ++++++++++++ .../unit/repository/BoardRepositoryTest.java | 6 ++-- .../repository/CommentRepositoryTest.java | 7 ++-- .../repository/GpaScoreRepositoryTest.java | 6 ++-- .../LanguageTestScoreRepositoryTest.java | 6 ++-- .../repository/PostLikeRepositoryTest.java | 8 ++--- .../unit/repository/PostRepositoryTest.java | 10 +++--- 21 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 src/test/java/com/example/solidconnection/support/MySQLTestContainer.java create mode 100644 src/test/java/com/example/solidconnection/support/RedisTestContainer.java create mode 100644 src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java create mode 100644 src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java diff --git a/build.gradle b/build.gradle index ceba38c4b..24f5e41c4 100644 --- a/build.gradle +++ b/build.gradle @@ -37,17 +37,24 @@ dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거 implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' implementation 'org.apache.commons:commons-lang3:3.12.0' - testImplementation 'org.mockito:mockito-core:3.3.3' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + // Lombok compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' + + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'com.h2database:h2:2.2.224' + testImplementation 'org.mockito:mockito-core:3.3.3' testImplementation 'io.rest-assured:rest-assured:5.4.0' - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + // Testcontainers + testImplementation 'org.testcontainers:testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:mysql' + annotationProcessor( 'com.querydsl:querydsl-apt:5.0.0:jakarta', 'jakarta.persistence:jakarta.persistence-api:3.1.0', diff --git a/src/main/resources/secret b/src/main/resources/secret index b4f88d141..80a569b4c 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit b4f88d14185e2009e0793dfd16d22c2c3b9257ae +Subproject commit 80a569b4c023225c77874e140521c703010414eb diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index f07b6821c..e553eb4bb 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -7,6 +7,7 @@ import com.example.solidconnection.post.service.PostService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -16,8 +17,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -26,8 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -@SpringBootTest -@ActiveProfiles("test") +@TestContainerSpringBootTest @DisplayName("게시글 좋아요 동시성 테스트") class PostLikeCountConcurrencyTest { diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index c2213993d..dcd423168 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -7,6 +7,7 @@ import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -17,8 +18,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -28,8 +27,7 @@ import static com.example.solidconnection.type.RedisConstants.*; import static org.junit.jupiter.api.Assertions.assertEquals; -@SpringBootTest -@ActiveProfiles("test") +@TestContainerSpringBootTest @DisplayName("게시글 조회수 동시성 테스트") public class PostViewCountConcurrencyTest { diff --git a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java index 7ec6a511e..dce720610 100644 --- a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java @@ -3,6 +3,7 @@ import com.example.solidconnection.application.service.ApplicationQueryService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -10,9 +11,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.test.context.ActiveProfiles; import java.util.Arrays; import java.util.Collections; @@ -22,8 +21,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -@SpringBootTest -@ActiveProfiles("test") +@TestContainerSpringBootTest @DisplayName("ThunderingHerd 테스트") public class ThunderingHerdTest { @Autowired diff --git a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java index a9d80afcc..d156cf485 100644 --- a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.database; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -17,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; +@Disabled @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2, replace = AutoConfigureTestDatabase.Replace.ANY) @ActiveProfiles("test") @DataJpaTest diff --git a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java index 6a7637ed5..69fcedaef 100644 --- a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.database; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,6 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@Disabled @ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class RedisConnectionTest { diff --git a/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java b/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java index 9b23d230e..0b3ac3524 100644 --- a/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java +++ b/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java @@ -1,16 +1,14 @@ package com.example.solidconnection.e2e; import com.example.solidconnection.support.DatabaseClearExtension; +import com.example.solidconnection.support.TestContainerSpringBootTest; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.ActiveProfiles; +@TestContainerSpringBootTest @ExtendWith(DatabaseClearExtension.class) -@ActiveProfiles("test") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) abstract class BaseEndToEndTest { @LocalServerPort diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 2da99def8..eff3e54b5 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -105,7 +105,7 @@ class SignUpTest extends BaseEndToEndTest { assertAll( "관심 지역과 나라 정보를 저장한다.", () -> assertThat(interestedRegions).containsExactlyInAnyOrder(region), - () -> assertThat(interestedCountries).containsExactlyElementsOf(countries) + () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java index 9afecbbfd..20a0bbc6b 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java @@ -5,6 +5,7 @@ import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.support.DatabaseClearExtension; +import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.domain.LanguageRequirement; import com.example.solidconnection.university.domain.University; @@ -17,9 +18,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.ActiveProfiles; import java.util.HashSet; @@ -27,8 +26,7 @@ import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; @ExtendWith(DatabaseClearExtension.class) -@ActiveProfiles("test") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestContainerSpringBootTest abstract class UniversityDataSetUpEndToEndTest { public static Region 영미권; diff --git a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java index 098a22c18..bb77f82f2 100644 --- a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java +++ b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java @@ -32,17 +32,18 @@ public void clear() { } private void truncate() { - em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + em.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); getTruncateQueries().forEach(query -> em.createNativeQuery(query).executeUpdate()); - em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + em.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); } @SuppressWarnings("unchecked") private List getTruncateQueries() { String sql = """ - SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ' RESTART IDENTITY', ';') AS q + SELECT CONCAT('TRUNCATE TABLE ', TABLE_NAME, ';') AS q FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'PUBLIC' + WHERE TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_TYPE = 'BASE TABLE' """; return em.createNativeQuery(sql).getResultList(); diff --git a/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java b/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java new file mode 100644 index 000000000..0256fec13 --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.support; + +import jakarta.annotation.PostConstruct; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; + +import javax.sql.DataSource; + +@TestConfiguration +public class MySQLTestContainer { + + @Container + private static final MySQLContainer CONTAINER = new MySQLContainer<>("mysql:8.0"); + + @Bean + public DataSource dataSource() { + return DataSourceBuilder.create() + .url(CONTAINER.getJdbcUrl()) + .username(CONTAINER.getUsername()) + .password(CONTAINER.getPassword()) + .driverClassName(CONTAINER.getDriverClassName()) + .build(); + } + + @PostConstruct + void startContainer() { + if (!CONTAINER.isRunning()) { + CONTAINER.start(); + } + } +} diff --git a/src/test/java/com/example/solidconnection/support/RedisTestContainer.java b/src/test/java/com/example/solidconnection/support/RedisTestContainer.java new file mode 100644 index 000000000..39f35c2d5 --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/RedisTestContainer.java @@ -0,0 +1,28 @@ +package com.example.solidconnection.support; + +import jakarta.annotation.PostConstruct; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; + +@TestConfiguration +public class RedisTestContainer { + + @Container + private static final GenericContainer CONTAINER = new GenericContainer<>("redis:7.0"); + + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.redis.host", CONTAINER::getHost); + registry.add("spring.redis.port", CONTAINER::getFirstMappedPort); + } + + @PostConstruct + void startContainer() { + if (!CONTAINER.isRunning()) { + CONTAINER.start(); + } + } +} diff --git a/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java b/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java new file mode 100644 index 000000000..339672e60 --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.support; + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("test") +@Testcontainers +@Import(MySQLTestContainer.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestContainerDataJpaTest { +} diff --git a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java new file mode 100644 index 000000000..bcb110c6b --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.support; + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("test") +@Testcontainers +@Import({MySQLTestContainer.class, RedisTestContainer.class}) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestContainerSpringBootTest { +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java index 9ea7ee0d9..17e74d140 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java @@ -7,6 +7,7 @@ import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -16,16 +17,13 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("게시판 레포지토리 테스트") class BoardRepositoryTest { @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java index a53037346..b57288725 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java @@ -9,6 +9,7 @@ import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -17,8 +18,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -28,9 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -@ActiveProfiles("dev") +@TestContainerDataJpaTest @DisplayName("댓글 레포지토리 테스트") class CommentRepositoryTest { @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java index e3fa680c2..3ec59a5c2 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java @@ -5,6 +5,7 @@ import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -12,8 +13,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -21,8 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("학점 레포지토리 테스트") @Transactional public class GpaScoreRepositoryTest { diff --git a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java index 7369f20fa..0090088c1 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java @@ -5,6 +5,7 @@ import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.type.PreparationStatus; @@ -13,8 +14,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -22,8 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("어학성적 레포지토리 테스트") @Transactional public class LanguageTestScoreRepositoryTest { diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java index c39e28497..43ac210cb 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java @@ -3,12 +3,13 @@ import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -17,8 +18,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; @@ -26,8 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("게시글 좋아요 레포지토리 테스트") class PostLikeRepositoryTest { @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index ecc2c4f6d..42da9de22 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -8,6 +8,7 @@ import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -16,20 +17,17 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("게시글 레포지토리 테스트") class PostRepositoryTest { @Autowired From 6464568f6a93efb02393d0add0081f3f193dba96 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 16 Jan 2025 02:37:46 +0900 Subject: [PATCH 123/158] =?UTF-8?q?chore:=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=9D=B4=EC=8A=88=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/refactor_request.md | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/refactor_request.md diff --git a/.github/ISSUE_TEMPLATE/refactor_request.md b/.github/ISSUE_TEMPLATE/refactor_request.md new file mode 100644 index 000000000..38aa8ef03 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor_request.md @@ -0,0 +1,28 @@ +--- +name: Refactor request +about: Suggest an refactor for this project +title: '' +labels: refactor +assignees: '' + +--- + +## 어떤 부분을 리팩터링하려 하나요? + +> 리팩터링하려는 부분에 대해 간결하게 설명해주세요 + +### AS-IS +- as-is +- as-is + +### TO-BE +- to-be +- to-be + +## 작업 상세 내용 + +- [ ] TODO +- [ ] TODO +- [ ] TODO + +## 참고할만한 자료(선택) From dc1bdcd5c404e60fbeb2202d63edde0f75178fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:28:03 +0900 Subject: [PATCH 124/158] =?UTF-8?q?feat:=20=EC=B6=94=EC=B2=9C=20=EB=8C=80?= =?UTF-8?q?=ED=95=99=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B3=A0=20=EB=BF=90?= =?UTF-8?q?=EB=A7=8C=20=EC=95=84=EB=8B=88=EB=9D=BC=20background=20image?= =?UTF-8?q?=EB=8F=84=20=ED=8F=AC=ED=95=A8=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../university/dto/UniversityInfoForApplyPreviewResponse.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java index 93214b056..f6c2b4969 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java @@ -12,6 +12,7 @@ public record UniversityInfoForApplyPreviewResponse( String region, String country, String logoImageUrl, + String backgroundImageUrl, int studentCapacity, List languageRequirements) { @@ -29,6 +30,7 @@ public static UniversityInfoForApplyPreviewResponse from(UniversityInfoForApply universityInfoForApply.getUniversity().getRegion().getKoreanName(), universityInfoForApply.getUniversity().getCountry().getKoreanName(), universityInfoForApply.getUniversity().getLogoImageUrl(), + universityInfoForApply.getUniversity().getBackgroundImageUrl(), universityInfoForApply.getStudentCapacity(), languageRequirementResponses ); From 428e72aac3cbe1c1b7c0f9892c6762f59928162b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:08:53 +0900 Subject: [PATCH 125/158] =?UTF-8?q?test:=20=EB=8C=80=ED=95=99=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 대학교 상세조회 관련 통합테스트 코드 추가 * feat: 대학 통합테스트를 위한 데이터 셋업 추가 * refactor: 셋업된 데이터로 대학 조회 테스트하도록 변경 * feat: 대학교 검색 관련 통합테스트 코드 추가 * feat: 대학교 좋아요하기 관련 통합테스트 코드 추가 * refactor: 존재하지 않는 대학 상세정보 조회 시 id 변수명 수정 * refactor: *에서 구체적인 import 문으로 변경 * feat: 대학교 추천 관련 통합테스트 코드 추가 * refactor: 대학교 추천 서비스에 DisplayName 추가 * chore: 개행 컨벤션에 맞게 수정 * test: univseritySerivice에서 존재하지 않는 사용자에 대한 예외 검증 삭제 * refactor: UniversityService를 조회/좋아요 관련된 기능으로 분리 * test: 조회 및 좋아요 기능 테스트 코드 분리 * test: 대학교 조회 캐시 적용 테스트 개선 - CacheManager를 직접 검증하는 방식에서 SpyBean을 사용한 레포지토리 호출 횟수 검증으로 변경 * test: 예외처리 검증 테스트 한줄로 검증 * refactor: 좋아요 관련 응답 import *에서 구체적인 import 문으로 변경 * refactor: 대학 관련 서비스 dto *에서 구체적인 import 문으로 변경 * refactor: BDD Mockito 형식으로 변경 --- .../controller/UniversityController.java | 14 +- .../service/UniversityLikeService.java | 65 ++++ ...rvice.java => UniversityQueryService.java} | 48 +-- .../e2e/UniversityLikeTest.java | 4 +- .../UniversityDataSetUpIntegrationTest.java | 288 ++++++++++++++++++ .../service/UniversityLikeServiceTest.java | 136 +++++++++ .../service/UniversityQueryServiceTest.java | 205 +++++++++++++ .../UniversityRecommendServiceTest.java | 153 ++++++++++ 8 files changed, 858 insertions(+), 55 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java rename src/main/java/com/example/solidconnection/university/service/{UniversityService.java => UniversityQueryService.java} (55%) create mode 100644 src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java create mode 100644 src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 2bab9da1a..1acfcb931 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -7,8 +7,9 @@ import com.example.solidconnection.university.dto.UniversityDetailResponse; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import com.example.solidconnection.university.service.UniversityLikeService; +import com.example.solidconnection.university.service.UniversityQueryService; import com.example.solidconnection.university.service.UniversityRecommendService; -import com.example.solidconnection.university.service.UniversityService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -26,7 +27,8 @@ @RestController public class UniversityController { - private final UniversityService universityService; + private final UniversityQueryService universityQueryService; + private final UniversityLikeService universityLikeService; private final UniversityRecommendService universityRecommendService; private final SiteUserService siteUserService; @@ -52,7 +54,7 @@ public ResponseEntity> getMyWishUniv public ResponseEntity getIsLiked( Principal principal, @PathVariable Long universityInfoForApplyId) { - IsLikeResponse isLiked = universityService.getIsLiked(principal.getName(), universityInfoForApplyId); + IsLikeResponse isLiked = universityLikeService.getIsLiked(principal.getName(), universityInfoForApplyId); return ResponseEntity.ok(isLiked); } @@ -60,7 +62,7 @@ public ResponseEntity getIsLiked( public ResponseEntity addWishUniversity( Principal principal, @PathVariable Long universityInfoForApplyId) { - LikeResultResponse likeResultResponse = universityService.likeUniversity(principal.getName(), universityInfoForApplyId); + LikeResultResponse likeResultResponse = universityLikeService.likeUniversity(principal.getName(), universityInfoForApplyId); return ResponseEntity .ok(likeResultResponse); } @@ -68,7 +70,7 @@ public ResponseEntity addWishUniversity( @GetMapping("/detail/{universityInfoForApplyId}") public ResponseEntity getUniversityDetails( @PathVariable Long universityInfoForApplyId) { - UniversityDetailResponse universityDetailResponse = universityService.getUniversityDetail(universityInfoForApplyId); + UniversityDetailResponse universityDetailResponse = universityQueryService.getUniversityDetail(universityInfoForApplyId); return ResponseEntity.ok(universityDetailResponse); } @@ -80,7 +82,7 @@ public ResponseEntity> searchUnivers @RequestParam(required = false, defaultValue = "") LanguageTestType testType, @RequestParam(required = false, defaultValue = "") String testScore) { List universityInfoForApplyPreviewResponse - = universityService.searchUniversity(region, keyword, testType, testScore).universityInfoForApplyPreviewResponses(); + = universityQueryService.searchUniversity(region, keyword, testType, testScore).universityInfoForApplyPreviewResponses(); return ResponseEntity.ok(universityInfoForApplyPreviewResponse); } } diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java new file mode 100644 index 000000000..4b15e5b8d --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java @@ -0,0 +1,65 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class UniversityLikeService { + + public static final String LIKE_SUCCESS_MESSAGE = "LIKE_SUCCESS"; + public static final String LIKE_CANCELED_MESSAGE = "LIKE_CANCELED"; + + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final LikedUniversityRepository likedUniversityRepository; + private final SiteUserRepository siteUserRepository; + + @Value("${university.term}") + public String term; + + /* + * 대학교를 '좋아요' 한다. + * - 이미 좋아요가 눌러져있다면, 좋아요를 취소한다. + * */ + @Transactional + public LikeResultResponse likeUniversity(String email, Long universityInfoForApplyId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); + + Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); + if (alreadyLikedUniversity.isPresent()) { + likedUniversityRepository.delete(alreadyLikedUniversity.get()); + return new LikeResultResponse(LIKE_CANCELED_MESSAGE); + } + + LikedUniversity likedUniversity = LikedUniversity.builder() + .universityInfoForApply(universityInfoForApply) + .siteUser(siteUser) + .build(); + likedUniversityRepository.save(likedUniversity); + return new LikeResultResponse(LIKE_SUCCESS_MESSAGE); + } + + /* + * '좋아요'한 대학교인지 확인한다. + * */ + @Transactional(readOnly = true) + public IsLikeResponse getIsLiked(String email, Long universityInfoForApplyId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); + boolean isLike = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply).isPresent(); + return new IsLikeResponse(isLike); + } +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityQueryService.java similarity index 55% rename from src/main/java/com/example/solidconnection/university/service/UniversityService.java rename to src/main/java/com/example/solidconnection/university/service/UniversityQueryService.java index 708374e96..f93f3ffae 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityQueryService.java @@ -1,15 +1,9 @@ package com.example.solidconnection.university.service; import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.university.domain.LikedUniversity; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import com.example.solidconnection.university.dto.IsLikeResponse; -import com.example.solidconnection.university.dto.LikeResultResponse; import com.example.solidconnection.university.dto.UniversityDetailResponse; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponses; @@ -21,19 +15,13 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Optional; @RequiredArgsConstructor @Service -public class UniversityService { - - public static final String LIKE_SUCCESS_MESSAGE = "LIKE_SUCCESS"; - public static final String LIKE_CANCELED_MESSAGE = "LIKE_CANCELED"; +public class UniversityQueryService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final LikedUniversityRepository likedUniversityRepository; private final UniversityFilterRepositoryImpl universityFilterRepository; - private final SiteUserRepository siteUserRepository; @Value("${university.term}") public String term; @@ -70,38 +58,4 @@ public UniversityInfoForApplyPreviewResponses searchUniversity( .map(UniversityInfoForApplyPreviewResponse::from) .toList()); } - - /* - * 대학교를 '좋아요' 한다. - * - 이미 좋아요가 눌러져있다면, 좋아요를 취소한다. - * */ - @Transactional - public LikeResultResponse likeUniversity(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); - - Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); - if (alreadyLikedUniversity.isPresent()) { - likedUniversityRepository.delete(alreadyLikedUniversity.get()); - return new LikeResultResponse(LIKE_CANCELED_MESSAGE); - } - - LikedUniversity likedUniversity = LikedUniversity.builder() - .universityInfoForApply(universityInfoForApply) - .siteUser(siteUser) - .build(); - likedUniversityRepository.save(likedUniversity); - return new LikeResultResponse(LIKE_SUCCESS_MESSAGE); - } - - /* - * '좋아요'한 대학교인지 확인한다. - * */ - @Transactional(readOnly = true) - public IsLikeResponse getIsLiked(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); - boolean isLike = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply).isPresent(); - return new IsLikeResponse(isLike); - } } diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index 37f922e4e..fb78cdffb 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -25,8 +25,8 @@ import static com.example.solidconnection.e2e.DynamicFixture.createLikedUniversity; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createUniversityForApply; -import static com.example.solidconnection.university.service.UniversityService.LIKE_CANCELED_MESSAGE; -import static com.example.solidconnection.university.service.UniversityService.LIKE_SUCCESS_MESSAGE; +import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_CANCELED_MESSAGE; +import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_SUCCESS_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java new file mode 100644 index 000000000..91518a09e --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java @@ -0,0 +1,288 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.Region; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.support.DatabaseClearExtension; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.domain.LanguageRequirement; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.repository.LanguageRequirementRepository; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.UniversityRepository; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.web.server.LocalServerPort; + +import java.util.HashSet; + +import static com.example.solidconnection.type.SemesterAvailableForDispatch.ONE_SEMESTER; +import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; + +@ExtendWith(DatabaseClearExtension.class) +@TestContainerSpringBootTest +abstract class UniversityDataSetUpIntegrationTest { + + public static Region 영미권; + public static Region 유럽; + public static Region 아시아; + public static Country 미국; + public static Country 캐나다; + public static Country 덴마크; + public static Country 오스트리아; + public static Country 일본; + + public static University 영미권_미국_괌대학; + public static University 영미권_미국_네바다주립대학_라스베이거스; + public static University 영미권_캐나다_메모리얼대학_세인트존스; + public static University 유럽_덴마크_서던덴마크대학교; + public static University 유럽_덴마크_코펜하겐IT대학; + public static University 유럽_오스트리아_그라츠대학; + public static University 유럽_오스트리아_그라츠공과대학; + public static University 유럽_오스트리아_린츠_카톨릭대학; + public static University 아시아_일본_메이지대학; + + public static UniversityInfoForApply 괌대학_A_지원_정보; + public static UniversityInfoForApply 괌대학_B_지원_정보; + public static UniversityInfoForApply 네바다주립대학_라스베이거스_지원_정보; + public static UniversityInfoForApply 메모리얼대학_세인트존스_A_지원_정보; + public static UniversityInfoForApply 서던덴마크대학교_지원_정보; + public static UniversityInfoForApply 코펜하겐IT대학_지원_정보; + public static UniversityInfoForApply 그라츠대학_지원_정보; + public static UniversityInfoForApply 그라츠공과대학_지원_정보; + public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; + public static UniversityInfoForApply 메이지대학_지원_정보; + + @Value("${university.term}") + public String term; + + @LocalServerPort + private int port; + + @Autowired + private RegionRepository regionRepository; + + @Autowired + private CountryRepository countryRepository; + + @Autowired + private UniversityRepository universityRepository; + + @Autowired + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Autowired + private LanguageRequirementRepository languageRequirementRepository; + + @BeforeEach + public void setUpBasicData() { + RestAssured.port = port; + + 영미권 = regionRepository.save(new Region("AMERICAS", "영미권")); + 유럽 = regionRepository.save(new Region("EUROPE", "유럽")); + 아시아 = regionRepository.save(new Region("ASIA", "아시아")); + + 미국 = countryRepository.save(new Country("US", "미국", 영미권)); + 캐나다 = countryRepository.save(new Country("CA", "캐나다", 영미권)); + 덴마크 = countryRepository.save(new Country("DK", "덴마크", 유럽)); + 오스트리아 = countryRepository.save(new Country("AT", "오스트리아", 유럽)); + 일본 = countryRepository.save(new Country("JP", "일본", 아시아)); + + 영미권_미국_괌대학 = universityRepository.save(new University( + null, "괌대학", "University of Guam", "university_of_guam", + "https://www.uog.edu/admissions/international-students", + "https://www.uog.edu/admissions/course-schedule", + "https://www.uog.edu/life-at-uog/residence-halls/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png", + null, 미국, 영미권 + )); + + 영미권_미국_네바다주립대학_라스베이거스 = universityRepository.save(new University( + null, "네바다주립대학 라스베이거스", "University of Nevada, Las Vegas", "university_of_nevada_las_vegas", + "https://www.unlv.edu/engineering/eip", + "https://www.unlv.edu/engineering/academic-programs", + "https://www.unlv.edu/housing", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png", + null, 미국, 영미권 + )); + + 영미권_캐나다_메모리얼대학_세인트존스 = universityRepository.save(new University( + null, "메모리얼 대학 세인트존스", "Memorial University of Newfoundland St. John's", "memorial_university_of_newfoundland_st_johns", + "https://mun.ca/goabroad/visiting-students-inbound/", + "https://www.unlv.edu/engineering/academic-programs", + "https://www.mun.ca/residences/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png", + null, 캐나다, 영미권 + )); + + 유럽_덴마크_서던덴마크대학교 = universityRepository.save(new University( + null, "서던덴마크대학교", "University of Southern Denmark", "university_of_southern_denmark", + "https://www.sdu.dk/en", + "https://www.sdu.dk/en", + "https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/1.png", + null, 덴마크, 유럽 + )); + + 유럽_덴마크_코펜하겐IT대학 = universityRepository.save(new University( + null, "코펜하겐 IT대학", "IT University of Copenhagen", "it_university_of_copenhagen", + "https://en.itu.dk/", null, + "https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/1.png", + null, 덴마크, 유럽 + )); + + 유럽_오스트리아_그라츠대학 = universityRepository.save(new University( + null, "그라츠 대학", "University of Graz", "university_of_graz", + "https://www.uni-graz.at/en/", + "https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf", + "https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/1.png", + null, 오스트리아, 유럽 + )); + + 유럽_오스트리아_그라츠공과대학 = universityRepository.save(new University( + null, "그라츠공과대학", "Graz University of Technology", "graz_university_of_technology", + "https://www.tugraz.at/en/home", null, + "https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/1.png", + null, 오스트리아, 유럽 + )); + + 유럽_오스트리아_린츠_카톨릭대학 = universityRepository.save(new University( + null, "린츠 카톨릭 대학교", "Catholic Private University Linz", "catholic_private_university_linz", + "https://ku-linz.at/en", null, + "https://ku-linz.at/en/ku_international/incomings/kulis", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/1.png", + null, 오스트리아, 유럽 + )); + + 아시아_일본_메이지대학 = universityRepository.save(new University( + null, "메이지대학", "Meiji University", "meiji_university", + "https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf", null, + "https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png", + null, 일본, 아시아 + )); + + 괌대학_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "괌대학(A형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_괌대학 + )); + + 괌대학_B_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "괌대학(B형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_괌대학 + )); + + 네바다주립대학_라스베이거스_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "네바다주립대학 라스베이거스(B형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_네바다주립대학_라스베이거스 + )); + + 메모리얼대학_세인트존스_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "메모리얼 대학 세인트존스(A형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_캐나다_메모리얼대학_세인트존스 + )); + + 서던덴마크대학교_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "서던덴마크대학교", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_덴마크_서던덴마크대학교 + )); + + 코펜하겐IT대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "코펜하겐 IT대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_덴마크_코펜하겐IT대학 + )); + + 그라츠대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "그라츠 대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_그라츠대학 + )); + + 그라츠공과대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "그라츠공과대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_그라츠공과대학 + )); + + 린츠_카톨릭대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "린츠 카톨릭 대학교", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_린츠_카톨릭대학 + )); + + 메이지대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "메이지대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 아시아_일본_메이지대학 + )); + + saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(괌대학_B_지원_정보, LanguageTestType.TOEFL_IBT, "70"); + saveLanguageTestRequirement(괌대학_B_지원_정보, LanguageTestType.TOEIC, "900"); + saveLanguageTestRequirement(네바다주립대학_라스베이거스_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(메모리얼대학_세인트존스_A_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(서던덴마크대학교_지원_정보, LanguageTestType.TOEFL_IBT, "70"); + saveLanguageTestRequirement(코펜하겐IT대학_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(그라츠대학_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(그라츠공과대학_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(린츠_카톨릭대학_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(메이지대학_지원_정보, LanguageTestType.JLPT, "N2"); + } + + private void saveLanguageTestRequirement( + UniversityInfoForApply universityInfoForApply, LanguageTestType testType, String minScore) { + LanguageRequirement languageRequirement = new LanguageRequirement( + null, + testType, + minScore, + universityInfoForApply); + universityInfoForApply.addLanguageRequirements(languageRequirement); + universityInfoForApplyRepository.save(universityInfoForApply); + languageRequirementRepository.save(languageRequirement); + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java new file mode 100644 index 000000000..50adf6839 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java @@ -0,0 +1,136 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; +import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_CANCELED_MESSAGE; +import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_SUCCESS_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; + +@DisplayName("대학교 좋아요 서비스 테스트") +class UniversityLikeServiceTest extends UniversityDataSetUpIntegrationTest { + + @Autowired + private UniversityLikeService universityLikeService; + + @Autowired + private LikedUniversityRepository likedUniversityRepository; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Test + void 대학_좋아요를_등록한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + LikeResultResponse response = universityLikeService.likeUniversity( + testUser.getEmail(), 괌대학_A_지원_정보.getId()); + + // then + assertThat(response.result()).isEqualTo(LIKE_SUCCESS_MESSAGE); + assertThat(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply( + testUser, 괌대학_A_지원_정보)).isPresent(); + } + + @Test + void 대학_좋아요를_취소한다() { + // given + SiteUser testUser = createSiteUser(); + saveLikedUniversity(testUser, 괌대학_A_지원_정보); + + // when + LikeResultResponse response = universityLikeService.likeUniversity( + testUser.getEmail(), 괌대학_A_지원_정보.getId()); + + // then + assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE); + assertThat(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply( + testUser, 괌대학_A_지원_정보)).isEmpty(); + } + + @Test + void 존재하지_않는_대학_좋아요_시도하면_예외를_반환한다() { + // given + SiteUser testUser = createSiteUser(); + Long invalidUniversityId = 9999L; + + // when & then + assertThatCode(() -> universityLikeService.likeUniversity(testUser.getEmail(), invalidUniversityId)) + .isInstanceOf(CustomException.class) + .hasMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); + } + + @Test + void 좋아요한_대학인지_확인한다() { + // given + SiteUser testUser = createSiteUser(); + saveLikedUniversity(testUser, 괌대학_A_지원_정보); + + // when + IsLikeResponse response = universityLikeService.getIsLiked(testUser.getEmail(), 괌대학_A_지원_정보.getId()); + + // then + assertThat(response.isLike()).isTrue(); + } + + @Test + void 좋아요하지_않은_대학인지_확인한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + IsLikeResponse response = universityLikeService.getIsLiked(testUser.getEmail(), 괌대학_A_지원_정보.getId()); + + // then + assertThat(response.isLike()).isFalse(); + } + + @Test + void 존재하지_않는_대학의_좋아요_여부_조회시_예외를_반환한다() { + // given + SiteUser testUser = createSiteUser(); + Long invalidUniversityId = 9999L; + + // when & then + assertThatCode(() -> universityLikeService.getIsLiked(testUser.getEmail(), invalidUniversityId)) + .isInstanceOf(CustomException.class) + .hasMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private void saveLikedUniversity(SiteUser siteUser, UniversityInfoForApply universityInfoForApply) { + LikedUniversity likedUniversity = LikedUniversity.builder() + .siteUser(siteUser) + .universityInfoForApply(universityInfoForApply) + .build(); + likedUniversityRepository.save(likedUniversity); + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java new file mode 100644 index 000000000..54c235452 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java @@ -0,0 +1,205 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.dto.UniversityDetailResponse; +import com.example.solidconnection.university.dto.LanguageRequirementResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponses; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.custom.UniversityFilterRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +@DisplayName("대학교 조회 서비스 테스트") +class UniversityQueryServiceTest extends UniversityDataSetUpIntegrationTest { + + @Autowired + private UniversityQueryService universityQueryService; + + @SpyBean + private UniversityFilterRepository universityFilterRepository; + + @SpyBean + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Test + void 대학_상세정보를_정상_조회한다() { + // given + Long universityId = 괌대학_A_지원_정보.getId(); + + // when + UniversityDetailResponse response = universityQueryService.getUniversityDetail(universityId); + + // then + Assertions.assertAll( + () -> assertThat(response.id()).isEqualTo(괌대학_A_지원_정보.getId()), + () -> assertThat(response.term()).isEqualTo(괌대학_A_지원_정보.getTerm()), + () -> assertThat(response.koreanName()).isEqualTo(괌대학_A_지원_정보.getKoreanName()), + () -> assertThat(response.englishName()).isEqualTo(영미권_미국_괌대학.getEnglishName()), + () -> assertThat(response.formatName()).isEqualTo(영미권_미국_괌대학.getFormatName()), + () -> assertThat(response.region()).isEqualTo(영미권.getKoreanName()), + () -> assertThat(response.country()).isEqualTo(미국.getKoreanName()), + () -> assertThat(response.homepageUrl()).isEqualTo(영미권_미국_괌대학.getHomepageUrl()), + () -> assertThat(response.logoImageUrl()).isEqualTo(영미권_미국_괌대학.getLogoImageUrl()), + () -> assertThat(response.backgroundImageUrl()).isEqualTo(영미권_미국_괌대학.getBackgroundImageUrl()), + () -> assertThat(response.detailsForLocal()).isEqualTo(영미권_미국_괌대학.getDetailsForLocal()), + () -> assertThat(response.studentCapacity()).isEqualTo(괌대학_A_지원_정보.getStudentCapacity()), + () -> assertThat(response.tuitionFeeType()).isEqualTo(괌대학_A_지원_정보.getTuitionFeeType().getKoreanName()), + () -> assertThat(response.semesterAvailableForDispatch()).isEqualTo(괌대학_A_지원_정보.getSemesterAvailableForDispatch().getKoreanName()), + () -> assertThat(response.languageRequirements()).containsOnlyOnceElementsOf( + 괌대학_A_지원_정보.getLanguageRequirements().stream() + .map(LanguageRequirementResponse::from) + .toList()), + () -> assertThat(response.detailsForLanguage()).isEqualTo(괌대학_A_지원_정보.getDetailsForLanguage()), + () -> assertThat(response.gpaRequirement()).isEqualTo(괌대학_A_지원_정보.getGpaRequirement()), + () -> assertThat(response.gpaRequirementCriteria()).isEqualTo(괌대학_A_지원_정보.getGpaRequirementCriteria()), + () -> assertThat(response.semesterRequirement()).isEqualTo(괌대학_A_지원_정보.getSemesterRequirement()), + () -> assertThat(response.detailsForApply()).isEqualTo(괌대학_A_지원_정보.getDetailsForApply()), + () -> assertThat(response.detailsForMajor()).isEqualTo(괌대학_A_지원_정보.getDetailsForMajor()), + () -> assertThat(response.detailsForAccommodation()).isEqualTo(괌대학_A_지원_정보.getDetailsForAccommodation()), + () -> assertThat(response.detailsForEnglishCourse()).isEqualTo(괌대학_A_지원_정보.getDetailsForEnglishCourse()), + () -> assertThat(response.details()).isEqualTo(괌대학_A_지원_정보.getDetails()), + () -> assertThat(response.accommodationUrl()).isEqualTo(괌대학_A_지원_정보.getUniversity().getAccommodationUrl()), + () -> assertThat(response.englishCourseUrl()).isEqualTo(괌대학_A_지원_정보.getUniversity().getEnglishCourseUrl()) + ); + } + + @Test + void 대학_상세정보_조회시_캐시가_적용된다() { + // given + Long universityId = 괌대학_A_지원_정보.getId(); + + // when + UniversityDetailResponse firstResponse = universityQueryService.getUniversityDetail(universityId); + UniversityDetailResponse secondResponse = universityQueryService.getUniversityDetail(universityId); + + // then + assertThat(firstResponse).isEqualTo(secondResponse); + then(universityInfoForApplyRepository).should(times(1)).getUniversityInfoForApplyById(universityId); + } + + @Test + void 존재하지_않는_대학_상세정보_조회시_예외_응답을_반환한다() { + // given + Long invalidUniversityInfoForApplyId = 9999L; + + // when & then + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> universityQueryService.getUniversityDetail(invalidUniversityInfoForApplyId)) + .havingRootCause() + .isInstanceOf(CustomException.class) + .withMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); + } + + @Test + void 전체_대학을_조회한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + null, List.of(), null, null); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠공과대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(린츠_카톨릭대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보) + ); + } + + @Test + void 대학_조회시_캐시가_적용된다() { + // given + String regionCode = 영미권.getCode(); + List keywords = List.of("괌"); + LanguageTestType testType = LanguageTestType.TOEFL_IBT; + String testScore = "70"; + String term = "2024-1"; + + // when + UniversityInfoForApplyPreviewResponses firstResponse = + universityQueryService.searchUniversity(regionCode, keywords, testType, testScore); + UniversityInfoForApplyPreviewResponses secondResponse = + universityQueryService.searchUniversity(regionCode, keywords, testType, testScore); + + // then + assertThat(firstResponse).isEqualTo(secondResponse); + then(universityFilterRepository).should(times(1)) + .findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + regionCode, keywords, testType, testScore, term); + } + + @Test + void 지역으로_대학을_필터링한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + 영미권.getCode(), List.of(), null, null); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보) + ); + } + + @Test + void 키워드로_대학을_필터링한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + null, List.of("라", "일본"), null, null); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠공과대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보) + ); + } + + @Test + void 어학시험_조건으로_대학을_필터링한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + null, List.of(), LanguageTestType.TOEFL_IBT, "70"); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보) + ); + } + + @Test + void 모든_조건으로_대학을_필터링한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + "EUROPE", List.of(), LanguageTestType.TOEFL_IBT, "70"); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()).containsExactly(UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보)); + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java new file mode 100644 index 000000000..1fee99033 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java @@ -0,0 +1,153 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.entity.InterestedCountry; +import com.example.solidconnection.entity.InterestedRegion; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("대학교 추천 서비스 테스트") +class UniversityRecommendServiceTest extends UniversityDataSetUpIntegrationTest { + + @Autowired + private UniversityRecommendService universityRecommendService; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private InterestedRegionRepository interestedRegionRepository; + + @Autowired + private InterestedCountyRepository interestedCountyRepository; + + @Autowired + private GeneralRecommendUniversities generalRecommendUniversities; + + @BeforeEach + void setUp() { + generalRecommendUniversities.init(); + } + + @Test + void 관심_지역_설정한_사용자의_맞춤_추천_대학을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + interestedRegionRepository.save(new InterestedRegion(testUser, 영미권)); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsAll(List.of( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보) + )); + } + + @Test + void 관심_국가_설정한_사용자의_맞춤_추천_대학을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + interestedCountyRepository.save(new InterestedCountry(testUser, 덴마크)); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsAll(List.of( + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + )); + } + + @Test + void 관심_지역과_국가_모두_설정한_사용자의_맞춤_추천_대학을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + interestedRegionRepository.save(new InterestedRegion(testUser, 영미권)); + interestedCountyRepository.save(new InterestedCountry(testUser, 덴마크)); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + ); + } + + @Test + void 관심사_미설정_사용자는_일반_추천_대학을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsExactlyInAnyOrderElementsOf( + generalRecommendUniversities.getRecommendUniversities().stream() + .map(UniversityInfoForApplyPreviewResponse::from) + .toList() + ); + } + + @Test + void 일반_추천_대학을_조회한다() { + // when + UniversityRecommendsResponse response = universityRecommendService.getGeneralRecommends(); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsExactlyInAnyOrderElementsOf( + generalRecommendUniversities.getRecommendUniversities().stream() + .map(UniversityInfoForApplyPreviewResponse::from) + .toList() + ); + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } +} From f7dd92deadc90a3aa9cddd84a781a78fd8f7ba75 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 23 Jan 2025 02:12:40 +0900 Subject: [PATCH 126/158] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=94=84=EB=A7=81?= =?UTF-8?q?=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 토큰 기능 제공 클래스 이름 변경 - TokenService > TokenProvider - 비지니스 로직이 아니라 기능만 제공하는 것이라 Provider가 더 적절하다고 판단함 * refactor: 토큰 접두사 추가 함수 이름 변경, static import 적용 * feat: subject 추출 함수 추가 * test: TokenProvider 테스트 작성 - 기존에 작성되지 않았던 것들도 작성함 * refactor: 예외 응답 함수 추출 * feat: 로그아웃 체크 필터 생성 - AS-IS: 액세스 토큰을 검증하면서 로그아웃했는지를 검증하고 있다. 이는 액세스 토큰의 검증부에 들어갈 것이 아니라, 더 이전 단계에서 처리되어야 한다. - TO-BE: 로그아웃 토큰을 필터에서 처리한다. 이전보다 더 빠르게 예외를 응답할 수 있다. * test: 로그아웃 체크 필터 테스트 작성 * refactor: 중복 코드 함수로 추출 * refactor: JWT 인증 필터 수정 - 가독성 개선 - 다른 객체들에 책임 분산 - permitAllEndpoint에 대한 설정 제거 * refactor: 사용하지 않는 코드,클래스 제거 * test: JWT 인증 필터 테스트 작성 * refactor: 스프링 시큐리티 설정 클래스 수정 - 가독성 향상 - 필터 추가 - 시큐리티 단에서 관리하는 인증 필요없는 uri 제거 * refactor: 중복 선언된 cors 설정 제거 * refactor: cors 관련 설정 ConfigurationProperties로 변경 * refactor: TokenType 패키지 이동 * refactor: TokenProvider, TokenValidator 패키지 이동 * refactor: JwtProperties 분리 * refactor: JwtUtils 분리 * refactor: ConfigurationPropertiesScan 적용 * refactor: 인스턴스화 방지 * test: 테스트 메서드 이름에 컨벤션 적용 --- .../SolidConnectionApplication.java | 2 + .../token => auth/domain}/TokenType.java | 10 +- .../auth/service/AuthService.java | 19 +-- .../auth/service/SignInService.java | 15 +-- .../auth/service/SignUpService.java | 14 +- .../auth/service/TokenProvider.java | 51 +++++++ .../service}/TokenValidator.java | 33 ++--- .../config/cors/CorsPropertiesConfig.java | 17 --- .../config/cors/WebConfig.java | 22 --- .../config/security/CorsProperties.java | 9 ++ .../config/security/JwtAuthentication.java | 29 ++++ .../security/JwtAuthenticationEntryPoint.java | 23 ++-- .../security/JwtAuthenticationFilter.java | 101 ++++---------- .../config/security/JwtProperties.java | 7 + .../security/JwtUserDetails.java} | 17 +-- .../security/SecurityConfiguration.java | 45 +++---- .../config/security/SignOutCheckFilter.java | 48 +++++++ .../config/token/TokenService.java | 70 ---------- .../custom/exception/ErrorCode.java | 3 +- .../userdetails/CustomUserDetailsService.java | 25 ---- .../solidconnection/util/JwtUtils.java | 51 +++++++ .../security/JwtAuthenticationFilterTest.java | 127 ++++++++++++++++++ .../security/SignOutCheckFilterTest.java | 106 +++++++++++++++ .../config/token/TokenProviderTest.java | 96 +++++++++++++ .../e2e/ApplicantsQueryTest.java | 24 ++-- .../solidconnection/e2e/MyPageTest.java | 12 +- .../solidconnection/e2e/MyPageUpdateTest.java | 12 +- .../solidconnection/e2e/SignInTest.java | 9 +- .../solidconnection/e2e/SignUpTest.java | 21 +-- .../e2e/UniversityDetailTest.java | 12 +- .../e2e/UniversityLikeTest.java | 12 +- .../e2e/UniversityRecommendTest.java | 12 +- .../e2e/UniversitySearchTest.java | 12 +- .../solidconnection/util/JwtUtilsTest.java | 120 +++++++++++++++++ 34 files changed, 808 insertions(+), 378 deletions(-) rename src/main/java/com/example/solidconnection/{config/token => auth/domain}/TokenType.java (52%) create mode 100644 src/main/java/com/example/solidconnection/auth/service/TokenProvider.java rename src/main/java/com/example/solidconnection/{config/token => auth/service}/TokenValidator.java (68%) delete mode 100644 src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java delete mode 100644 src/main/java/com/example/solidconnection/config/cors/WebConfig.java create mode 100644 src/main/java/com/example/solidconnection/config/security/CorsProperties.java create mode 100644 src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java create mode 100644 src/main/java/com/example/solidconnection/config/security/JwtProperties.java rename src/main/java/com/example/solidconnection/{custom/userdetails/CustomUserDetails.java => config/security/JwtUserDetails.java} (60%) create mode 100644 src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java delete mode 100644 src/main/java/com/example/solidconnection/config/token/TokenService.java delete mode 100644 src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java create mode 100644 src/main/java/com/example/solidconnection/util/JwtUtils.java create mode 100644 src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java create mode 100644 src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java create mode 100644 src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/util/JwtUtilsTest.java diff --git a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java index 670a3f0f7..a7f0554a3 100644 --- a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java +++ b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; +@ConfigurationPropertiesScan @EnableScheduling @EnableJpaAuditing @EnableCaching diff --git a/src/main/java/com/example/solidconnection/config/token/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java similarity index 52% rename from src/main/java/com/example/solidconnection/config/token/TokenType.java rename to src/main/java/com/example/solidconnection/auth/domain/TokenType.java index d5fc1717f..7fa6045f7 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -1,13 +1,13 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.domain; import lombok.Getter; @Getter public enum TokenType { - ACCESS("", 1000 * 60 * 60), - REFRESH("refresh:", 1000 * 60 * 60 * 24 * 7), - KAKAO_OAUTH("kakao:", 1000 * 60 * 60); + ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour + REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days + KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60); // 1hour private final String prefix; private final int expireTime; @@ -17,7 +17,7 @@ public enum TokenType { this.expireTime = expireTime; } - public String addTokenPrefixToSubject(String subject) { + public String addPrefixToSubject(String subject) { return prefix + subject; } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index caf78074d..29fb1b347 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -2,8 +2,6 @@ import com.example.solidconnection.auth.dto.ReissueResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -16,15 +14,18 @@ import java.time.LocalDate; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.config.token.TokenValidator.SIGN_OUT_VALUE; +import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @RequiredArgsConstructor @Service public class AuthService { + public static final String SIGN_OUT_VALUE = "signOut"; + private final RedisTemplate redisTemplate; - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; /* @@ -36,9 +37,9 @@ public class AuthService { * */ public void signOut(String email) { redisTemplate.opsForValue().set( - TokenType.REFRESH.addTokenPrefixToSubject(email), + REFRESH.addPrefixToSubject(email), SIGN_OUT_VALUE, - TokenType.REFRESH.getExpireTime(), + REFRESH.getExpireTime(), TimeUnit.MILLISECONDS ); } @@ -62,14 +63,14 @@ public void quit(String email) { * */ public ReissueResponse reissue(String email) { // 리프레시 토큰 만료 확인 - String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(email); + String refreshTokenKey = REFRESH.addPrefixToSubject(email); String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); if (ObjectUtils.isEmpty(refreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = tokenService.generateToken(email, TokenType.ACCESS); - tokenService.saveToken(newAccessToken, TokenType.ACCESS); + String newAccessToken = tokenProvider.generateToken(email, ACCESS); + tokenProvider.saveToken(newAccessToken, ACCESS); return new ReissueResponse(newAccessToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index f6adda20d..2cd356d73 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -6,8 +6,7 @@ import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; @@ -18,7 +17,7 @@ @Service public class SignInService { - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; private final KakaoOAuthClient kakaoOAuthClient; @@ -58,15 +57,15 @@ private void resetQuitedAt(String email) { } private SignInResponse getSignInInfo(String email) { - String accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignInResponse(true, accessToken, refreshToken); } private FirstAccessResponse getFirstAccessInfo(KakaoUserInfoDto kakaoUserInfoDto) { - String kakaoOauthToken = tokenService.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); - tokenService.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); + String kakaoOauthToken = tokenProvider.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); + tokenProvider.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); return FirstAccessResponse.of(kakaoUserInfoDto, kakaoOauthToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java index f10f40dbd..5cbd781eb 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -2,9 +2,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; -import com.example.solidconnection.config.token.TokenValidator; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; @@ -29,7 +27,7 @@ public class SignUpService { private final TokenValidator tokenValidator; - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -51,7 +49,7 @@ public class SignUpService { public SignUpResponse signUp(SignUpRequest signUpRequest) { // 검증 tokenValidator.validateKakaoToken(signUpRequest.kakaoOauthToken()); - String email = tokenService.getEmail(signUpRequest.kakaoOauthToken()); + String email = tokenProvider.getEmail(signUpRequest.kakaoOauthToken()); validateNicknameDuplicated(signUpRequest.nickname()); validateUserNotDuplicated(email); @@ -64,9 +62,9 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { saveInterestedCountry(signUpRequest, savedSiteUser); // 토큰 발급 - String accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignUpResponse(accessToken, refreshToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java new file mode 100644 index 000000000..693a968ea --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; + +@RequiredArgsConstructor +@Component +public class TokenProvider { + + private final RedisTemplate redisTemplate; + private final JwtProperties jwtProperties; + + public String generateToken(String email, TokenType tokenType) { + Claims claims = Jwts.claims().setSubject(email); + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) + .compact(); + } + + public String saveToken(String token, TokenType tokenType) { + String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + redisTemplate.opsForValue().set( + tokenType.addPrefixToSubject(subject), + token, + tokenType.getExpireTime(), + TimeUnit.MILLISECONDS + ); + return token; + } + + public String getEmail(String token) { + return parseSubject(token, jwtProperties.secret()); + } +} diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java similarity index 68% rename from src/main/java/com/example/solidconnection/config/token/TokenValidator.java rename to src/main/java/com/example/solidconnection/auth/service/TokenValidator.java index 9a63a21f5..8c17ad00c 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java @@ -1,5 +1,6 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.service; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.exception.CustomException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -12,18 +13,18 @@ import java.util.Date; import java.util.Objects; +import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.EMPTY_TOKEN; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; -import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; @Component @RequiredArgsConstructor public class TokenValidator { - public static final String SIGN_OUT_VALUE = "signOut"; - private final RedisTemplate redisTemplate; @Value("${jwt.secret}") @@ -31,20 +32,19 @@ public class TokenValidator { public void validateAccessToken(String token) { validateTokenNotEmpty(token); - validateTokenNotExpired(token, TokenType.ACCESS); - validateNotSignOut(token); + validateTokenNotExpired(token, ACCESS); validateRefreshToken(token); } public void validateKakaoToken(String token) { validateTokenNotEmpty(token); - validateTokenNotExpired(token, TokenType.KAKAO_OAUTH); + validateTokenNotExpired(token, KAKAO_OAUTH); validateKakaoTokenNotUsed(token); } private void validateTokenNotEmpty(String token) { if (!StringUtils.hasText(token)) { - throw new CustomException(INVALID_TOKEN); + throw new CustomException(EMPTY_TOKEN); } } @@ -52,32 +52,25 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { Date expiration = getClaim(token).getExpiration(); long now = new Date().getTime(); if ((expiration.getTime() - now) < 0) { - if (tokenType.equals(TokenType.ACCESS)) { + if (tokenType.equals(ACCESS)) { throw new CustomException(ACCESS_TOKEN_EXPIRED); } - if (token.equals(TokenType.KAKAO_OAUTH)) { + if (token.equals(KAKAO_OAUTH)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } } - private void validateNotSignOut(String token) { - String email = getClaim(token).getSubject(); - if (SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)))) { - throw new CustomException(USER_ALREADY_SIGN_OUT); - } - } - private void validateRefreshToken(String token) { String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)) == null) { + if (redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email)) == null) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } } private void validateKakaoTokenNotUsed(String token) { String email = getClaim(token).getSubject(); - if (!Objects.equals(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email)), token)) { + if (!Objects.equals(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email)), token)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } diff --git a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java b/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java deleted file mode 100644 index 68144d733..000000000 --- a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.solidconnection.config.cors; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -@Getter -@Setter -@ConfigurationProperties(prefix = "cors") -@Configuration -public class CorsPropertiesConfig { - - private List allowedOrigins; -} diff --git a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java deleted file mode 100644 index 00f3cf411..000000000 --- a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.solidconnection.config.cors; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@RequiredArgsConstructor -public class WebConfig implements WebMvcConfigurer { - - private final CorsPropertiesConfig corsProperties; - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) - .allowedMethods("*") - .allowedHeaders("*") - .allowCredentials(true); - } -} diff --git a/src/main/java/com/example/solidconnection/config/security/CorsProperties.java b/src/main/java/com/example/solidconnection/config/security/CorsProperties.java new file mode 100644 index 000000000..f851692c6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/CorsProperties.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.config.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@ConfigurationProperties(prefix = "cors") +public record CorsProperties(List allowedOrigins) { +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java new file mode 100644 index 000000000..84692709a --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.config.security; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class JwtAuthentication extends AbstractAuthenticationToken { + + private final String token; + private final Object principal; + + public JwtAuthentication(Object principal, String token, Collection authorities) { + super(authorities); + this.token = token; + this.principal = principal; + setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return this.token; + } + + @Override + public Object getPrincipal() { + return this.principal; + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 69f5a2f2d..7487858be 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -12,7 +12,6 @@ import java.io.IOException; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; @Component @@ -25,24 +24,20 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + writeResponse(response, errorResponse); } - public void expiredCommence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(new CustomException(ACCESS_TOKEN_EXPIRED)); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { + ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); + writeResponse(response, errorResponse); } - public void customCommence(HttpServletRequest request, HttpServletResponse response, - CustomException customException) throws IOException { + public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(customException); + writeResponse(response, errorResponse); + } + + private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index a618bec04..e01009be1 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -1,113 +1,60 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenValidator; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.JwtExpiredTokenException; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -import org.springframework.util.AntPathMatcher; -import org.springframework.util.ObjectUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.HashSet; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { - public static final String TOKEN_HEADER = "Authorization"; - public static final String TOKEN_PREFIX = "Bearer "; + private static final String REISSUE_URI = "/auth/reissue"; + private static final String REISSUE_METHOD = "post"; - private final TokenService tokenService; - private final TokenValidator tokenValidator; + private final JwtProperties jwtProperties; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - - // 인증 정보를 저장할 필요 없는 url - AntPathMatcher pathMatcher = new AntPathMatcher(); - for (String endpoint : getPermitAllEndpoints()) { - if (pathMatcher.match(endpoint, request.getRequestURI())) { - filterChain.doFilter(request, response); - return; - } + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseTokenFromRequest(request); + if (token == null || isReissueRequest(request)) { + filterChain.doFilter(request, response); + return; } - // 토큰 검증 try { - String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 - if (token != null) { // 토큰이 있어야 검증 - 토큰 유무에 대한 다른 처리를 컨트롤러에서 할 수 있음 - try { - String requestURI = request.getRequestURI(); - if (requestURI.equals("/auth/reissue")) { - Authentication auth = this.tokenService.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - return; - } - tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 - } catch (ExpiredJwtException e) { - throw new JwtExpiredTokenException(ACCESS_TOKEN_EXPIRED.getMessage()); - } - Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 - SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 - } - } catch (JwtExpiredTokenException e) { - SecurityContextHolder.clearContext(); - jwtAuthenticationEntryPoint.expiredCommence(request, response, e); - return; + String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + UserDetails userDetails = new JwtUserDetails(subject); + Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + filterChain.doFilter(request, response); } catch (AuthenticationException e) { - SecurityContextHolder.clearContext(); jwtAuthenticationEntryPoint.commence(request, response, e); - return; } catch (CustomException e) { - jwtAuthenticationEntryPoint.customCommence(request, response, e); - return; - } - filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 - } - - private String resolveAccessTokenFromRequest(HttpServletRequest request) { - String token = request.getHeader(TOKEN_HEADER); - - if (!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) { // 토큰이 비어 있지 않고, Bearer로 시작한다면 - return token.substring(TOKEN_PREFIX.length()); // Bearer 제외한 실제 토큰 부분 반환 + jwtAuthenticationEntryPoint.customCommence(response, e); + } catch (Exception e) { + jwtAuthenticationEntryPoint.generalCommence(response, e); } - return null; } - private HashSet getPermitAllEndpoints() { - var permitAllEndpoints = new HashSet(); - - // 서버 정상 작동 확인 - permitAllEndpoints.add("/"); - permitAllEndpoints.add("/index.html"); - permitAllEndpoints.add("/favicon.ico"); - - // 이미지 업로드 - permitAllEndpoints.add("/file/profile/pre"); - - // 토큰이 필요하지 않은 인증 - permitAllEndpoints.add("/auth/kakao"); - permitAllEndpoints.add("/auth/sign-up"); - - // 대학교 정보 - permitAllEndpoints.add("/university/search/**"); - - return permitAllEndpoints; + private boolean isReissueRequest(HttpServletRequest request) { + return REISSUE_URI.equals(request.getRequestURI()) && REISSUE_METHOD.equals(request.getMethod()); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtProperties.java b/src/main/java/com/example/solidconnection/config/security/JwtProperties.java new file mode 100644 index 000000000..e0c63da46 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/JwtProperties.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.config.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "jwt") +public record JwtProperties(String secret) { +} diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java b/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java similarity index 60% rename from src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java rename to src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java index 5d992adaf..b3bbda5fa 100644 --- a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java @@ -1,26 +1,21 @@ -package com.example.solidconnection.custom.userdetails; +package com.example.solidconnection.config.security; -import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; -public class CustomUserDetails implements UserDetails {//todo: principal 을 썼을 때 바로 SiteUser를 반환하게 하면 안되나?? +public class JwtUserDetails implements UserDetails { - private final SiteUser siteUser; + private final String userName; - public CustomUserDetails(SiteUser siteUser) { - this.siteUser = siteUser; - } - - public String getEmail() { - return siteUser.getEmail(); + public JwtUserDetails(String userName) { + this.userName = userName; } @Override public String getUsername() { - return siteUser.getEmail(); + return this.userName; } @Override diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 70bcf6c37..d28d883ca 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -1,65 +1,52 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.config.cors.CorsPropertiesConfig; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; - @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) @RequiredArgsConstructor public class SecurityConfiguration { + private final CorsProperties corsProperties; + private final SignOutCheckFilter signOutCheckFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; - private final CorsPropertiesConfig corsProperties; @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(corsProperties.getAllowedOrigins()); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowedOrigins(corsProperties.allowedOrigins()); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); + return source; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) + return http .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) - .sessionManagement((session) -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(authorizeRequest - -> authorizeRequest - .requestMatchers( - "/", "/index.html", "/favicon.ico", - "/file/profile/pre", - "/auth/kakao", "/auth/sign-up", "/auth/reissue", - "/university/detail/**", "/university/search/**", "/university/recommends", - "/actuator/**" - ) - .permitAll() - .anyRequest().authenticated()) - .addFilterBefore(this.jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .formLogin(AbstractHttpConfigurer::disable); - - return http.build(); + .formLogin(AbstractHttpConfigurer::disable) + .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .addFilterBefore(this.jwtAuthenticationFilter, BasicAuthenticationFilter.class) + .addFilterBefore(this.signOutCheckFilter, JwtAuthenticationFilter.class) + .build(); } } diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java new file mode 100644 index 000000000..3c1218d13 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java @@ -0,0 +1,48 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.exception.CustomException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.service.AuthService.SIGN_OUT_VALUE; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; + +@Component +@RequiredArgsConstructor +public class SignOutCheckFilter extends OncePerRequestFilter { + + private final RedisTemplate redisTemplate; + private final JwtProperties jwtProperties; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseTokenFromRequest(request); + if (token == null || !isSignOut(token)) { + filterChain.doFilter(request, response); + return; + } + + jwtAuthenticationEntryPoint.customCommence(response, new CustomException(USER_ALREADY_SIGN_OUT)); + } + + private boolean isSignOut(String accessToken) { + String subject = parseSubject(accessToken, jwtProperties.secret()); + String refreshToken = REFRESH.addPrefixToSubject(subject); + return SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(refreshToken)); + } +} diff --git a/src/main/java/com/example/solidconnection/config/token/TokenService.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java deleted file mode 100644 index fc9ccea31..000000000 --- a/src/main/java/com/example/solidconnection/config/token/TokenService.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.solidconnection.config.token; - -import com.example.solidconnection.custom.userdetails.CustomUserDetailsService; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -import java.util.Date; -import java.util.concurrent.TimeUnit; - -@RequiredArgsConstructor -@Component -public class TokenService { - - private final RedisTemplate redisTemplate; - private final CustomUserDetailsService customUserDetailsService; - - @Value("${jwt.secret}") - private String secretKey; - - public String generateToken(String email, TokenType tokenType) { - Claims claims = Jwts.claims().setSubject(email); - Date now = new Date(); - Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(expiredDate) - .signWith(SignatureAlgorithm.HS512, this.secretKey) - .compact(); - } - - public void saveToken(String token, TokenType tokenType) { - redisTemplate.opsForValue().set( - tokenType.addTokenPrefixToSubject(getClaim(token).getSubject()), - token, - tokenType.getExpireTime(), - TimeUnit.MILLISECONDS - ); - } - - public Authentication getAuthentication(String token) { - String email = getClaim(token).getSubject(); - UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); - return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); - } - - public String getEmail(String token) { - return getClaim(token).getSubject(); - } - - private Claims getClaim(String token) { - try { - return Jwts.parser() - .setSigningKey(secretKey) - .parseClaimsJws(token) - .getBody(); - } catch (ExpiredJwtException e) { - return e.getClaims(); - } - } -} diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 765013303..8c3032284 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -29,7 +29,8 @@ public enum ErrorCode { // auth USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), - INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), + EMPTY_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java deleted file mode 100644 index c9f1b1606..000000000 --- a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.solidconnection.custom.userdetails; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.stereotype.Service; - -import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; - -@Service -@RequiredArgsConstructor -public class CustomUserDetailsService implements UserDetailsService { - - private final SiteUserRepository siteUserRepository; - - @Override - public UserDetails loadUserByUsername(String username) { - SiteUser siteUser = siteUserRepository.findByEmail(username) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND, username)); - return new CustomUserDetails(siteUser); - } -} diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java new file mode 100644 index 000000000..a3775365d --- /dev/null +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.util; + +import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; + +@Component +public class JwtUtils { + + private static final String TOKEN_HEADER = "Authorization"; + private static final String TOKEN_PREFIX = "Bearer "; + + private JwtUtils() { + } + + public static String parseTokenFromRequest(HttpServletRequest request) { + String token = request.getHeader(TOKEN_HEADER); + if (token == null || token.isBlank() || !token.startsWith(TOKEN_PREFIX)) { + return null; + } + return token.substring(TOKEN_PREFIX.length()); + } + + public static String parseSubject(String token, String secretKey) { + try { + return extractSubject(token, secretKey); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); + } + } + + public static String parseSubjectOrElseThrow(String token, String secretKey) { + try { + return extractSubject(token, secretKey); + } catch (ExpiredJwtException e) { + throw new CustomException(INVALID_TOKEN); + } + } + + private static String extractSubject(String token, String secretKey) throws ExpiredJwtException { + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java new file mode 100644 index 000000000..c0256f75a --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java @@ -0,0 +1,127 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +@DisplayName("토큰 인증 필터 테스트") +class JwtAuthenticationFilterTest { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + private JwtProperties jwtProperties; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + @BeforeEach + void setUp() { + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + SecurityContextHolder.clearContext(); + } + + @Test + public void 유효한_토큰에_대한_인증_정보를_저장한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()) + .isExactlyInstanceOf(JwtAuthentication.class); + then(filterChain).should().doFilter(request, response); + } + + @Test + public void 토큰이_없으면_다음_필터로_진행한다() throws Exception { + // given + request = new MockHttpServletRequest(); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + then(filterChain).should().doFilter(request, response); + } + + @Nested + class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { + + @Test + public void 만료된_토큰으로_인증하면_예외를_응답한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + then(filterChain).shouldHaveNoMoreInteractions(); + } + + @Test + public void 서명하지_않은_토큰으로_인증하면_예외를_응답한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, "wrongSecretKey") + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + then(filterChain).shouldHaveNoMoreInteractions(); + } + } + + private HttpServletRequest createRequestWithToken(String token) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + token); + return request; + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java new file mode 100644 index 000000000..13544152b --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java @@ -0,0 +1,106 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import java.util.Date; +import java.util.Objects; + +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +@DisplayName("로그아웃 체크 필터 테스트") +class SignOutCheckFilterTest { + + @Autowired + private SignOutCheckFilter signOutCheckFilter; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + private final String subject = "subject"; + + @BeforeEach + void setUp() { + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + Objects.requireNonNull(redisTemplate.getConnectionFactory()) + .getConnection() + .serverCommands() + .flushDb(); + } + + @Test + void 로그아웃한_토큰이면_예외를_응답한다() throws Exception { + // given + request = createTokenRequest(subject); + String refreshTokenKey = REFRESH.addPrefixToSubject(subject); + redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(USER_ALREADY_SIGN_OUT.getCode()); + then(filterChain).shouldHaveNoMoreInteractions(); + } + + @Test + void 토큰이_없으면_다음_필터로_전달한다() throws Exception { + // given + request = new MockHttpServletRequest(); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + @Test + void 로그아웃하지_않은_토큰이면_다음_필터로_전달한다() throws Exception { + // given + request = createTokenRequest(subject); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + private HttpServletRequest createTokenRequest(String subject) { + String token = Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + token); + return request; + } +} diff --git a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java b/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java new file mode 100644 index 000000000..d3992a33a --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java @@ -0,0 +1,96 @@ +package com.example.solidconnection.config.token; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@TestContainerSpringBootTest +@DisplayName("TokenProvider 테스트") +class TokenProviderTest { + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 토큰을_생성한다() { + // when + String subject = "subject123"; + String token = tokenProvider.generateToken(subject, TokenType.ACCESS); + + // then + String extractedSubject = Jwts.parser() + .setSigningKey(jwtProperties.secret()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + assertThat(subject).isEqualTo(extractedSubject); + } + + @Nested + class 토큰을_저장한다 { + + @Test + void 토큰이_유효하면_저장한다() { + // given + String subject = "subject321"; + String token = createValidToken(subject); + + // when + tokenProvider.saveToken(token, TokenType.ACCESS); + + // then + String savedToken = redisTemplate.opsForValue().get(TokenType.ACCESS.addPrefixToSubject(subject)); + assertThat(savedToken).isEqualTo(token); + } + + @Test + void 토큰이_유효하지않으면_예외가_발생한다() { + // given + String token = createInvalidToken(); + + // when & then + assertThatCode(() -> tokenProvider.saveToken(token, TokenType.REFRESH)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + private String createValidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private String createInvalidToken() { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 2f69d6cf7..6b739248b 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -7,8 +7,8 @@ import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; @@ -36,7 +36,7 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { ApplicationRepository applicationRepository; @Autowired - TokenService tokenService; + TokenProvider tokenProvider; private String accessToken; private String adminAccessToken; @@ -60,17 +60,17 @@ public void setUpUserAndToken() { SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); - adminAccessToken = tokenService.generateToken("email5", TokenType.ACCESS); - String adminRefreshToken = tokenService.generateToken("email5", TokenType.REFRESH); - tokenService.saveToken(adminRefreshToken, TokenType.REFRESH); + adminAccessToken = tokenProvider.generateToken("email5", TokenType.ACCESS); + String adminRefreshToken = tokenProvider.generateToken("email5", TokenType.REFRESH); + tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); - user6AccessToken = tokenService.generateToken("email6", TokenType.ACCESS); - String user6RefreshToken = tokenService.generateToken("email6", TokenType.REFRESH); - tokenService.saveToken(user6RefreshToken, TokenType.REFRESH); + user6AccessToken = tokenProvider.generateToken("email6", TokenType.ACCESS); + String user6RefreshToken = tokenProvider.generateToken("email6", TokenType.REFRESH); + tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); // setUp - 사용자 정보 저장 SiteUser 사용자1 = siteUserRepository.save(createSiteUserByEmail("email1")); diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index 059e00cde..fb42216c9 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -23,7 +23,7 @@ class MyPageTest extends BaseEndToEndTest { @Autowired private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @BeforeEach @@ -32,9 +32,9 @@ public void setUpUserAndToken() { siteUserRepository.save(createSiteUserByEmail(email)); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java index cb058fe3a..6d7f52032 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; @@ -31,7 +31,7 @@ class MyPageUpdateTest extends BaseEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @@ -46,9 +46,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index 8f1bd1018..efd5ad1d7 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -5,7 +5,6 @@ import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.restassured.RestAssured; @@ -19,6 +18,8 @@ import java.time.LocalDate; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.e2e.DynamicFixture.createKakaoUserInfoDtoByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.scheduler.UserRemovalScheduler.ACCOUNT_RECOVER_DURATION; @@ -64,7 +65,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.nickname()).isEqualTo(kakaoProfileDto.nickname()), () -> assertThat(response.profileImageUrl()).isEqualTo(kakaoProfileDto.profileImageUrl()), () -> assertThat(response.kakaoOauthToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email))) .as("카카오 인증 토큰을 저장한다.") .isEqualTo(response.kakaoOauthToken()); } @@ -94,7 +95,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -128,7 +129,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull(), () -> assertThat(siteUserRepository.getByEmail(email).getQuitedAt()).isNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index eff3e54b5..07dafb539 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -2,8 +2,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -27,6 +26,8 @@ import java.util.List; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; @@ -54,7 +55,7 @@ class SignUpTest extends BaseEndToEndTest { InterestedCountyRepository interestedCountyRepository; @Autowired - TokenService tokenService; + TokenProvider tokenProvider; @Autowired RedisTemplate redisTemplate; @@ -69,8 +70,8 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); @@ -108,7 +109,7 @@ class SignUpTest extends BaseEndToEndTest { () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -122,8 +123,8 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -148,8 +149,8 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = tokenService.generateToken(alreadyExistEmail, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(alreadyExistEmail, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index dc8401700..947f44fd0 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.LanguageRequirementResponse; @@ -24,7 +24,7 @@ class UniversityDetailTest extends UniversityDataSetUpEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @@ -36,9 +36,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index fb78cdffb..dccd1092f 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -45,7 +45,7 @@ class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { private LikedUniversityRepository likedUniversityRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; private SiteUser siteUser; @@ -57,9 +57,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index ee46733a1..00afbc8e3 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; import com.example.solidconnection.repositories.InterestedCountyRepository; @@ -38,7 +38,7 @@ class UniversityRecommendTest extends UniversityDataSetUpEndToEndTest { private InterestedCountyRepository interestedCountyRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; @Autowired private GeneralRecommendUniversities generalRecommendUniversities; @@ -54,9 +54,9 @@ void setUp() { generalRecommendUniversities.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java index 1187fb0ad..4859f9fe2 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -33,7 +33,7 @@ class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { private LikedUniversityRepository likedUniversityRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; private SiteUser siteUser; @@ -45,9 +45,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java new file mode 100644 index 000000000..4dfc11540 --- /dev/null +++ b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java @@ -0,0 +1,120 @@ +package com.example.solidconnection.util; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.util.Date; + +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("JwtUtils 테스트") +class JwtUtilsTest { + + private final String jwtSecretKey = "jwt-secret-key"; + + @Nested + class 요청으로부터_토큰을_추출한다 { + + @Test + void 토큰이_있으면_토큰을_반환한다() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + String token = "token"; + request.addHeader("Authorization", "Bearer " + token); + + // when + String extractedToken = parseTokenFromRequest(request); + + // then + assertThat(extractedToken).isEqualTo(token); + } + + @Test + void 토큰이_없으면_null_을_반환한다() { + // given + MockHttpServletRequest noHeader = new MockHttpServletRequest(); + MockHttpServletRequest wrongPrefix = new MockHttpServletRequest(); + wrongPrefix.addHeader("Authorization", "Wrong token"); + MockHttpServletRequest emptyToken = new MockHttpServletRequest(); + wrongPrefix.addHeader("Authorization", "Bearer "); + + // when & then + assertAll( + () -> assertThat(parseTokenFromRequest(noHeader)).isNull(), + () -> assertThat(parseTokenFromRequest(wrongPrefix)).isNull(), + () -> assertThat(parseTokenFromRequest(emptyToken)).isNull() + ); + } + } + + @Nested + class 토큰으로부터_subject_를_추출한다 { + + @Test + void 유효한_토큰의_subject_를_추출한다() { + // given + String subject = "subject000"; + String token = createValidToken(subject); + + // when + String extractedSubject = parseSubject(token, jwtSecretKey); + + // then + assertThat(extractedSubject).isEqualTo(subject); + } + + @Test + void 유효하지_않은_토큰의_subject_를_추출한다() { + // given + String subject = "subject999"; + String token = createInvalidToken(subject); + + // when + String extractedSubject = parseSubject(token, jwtSecretKey); + + // then + assertThat(extractedSubject).isEqualTo(subject); + } + + @Test + void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { + // given + String subject = "subject123"; + String token = createInvalidToken(subject); + + // when + assertThatCode(() -> parseSubjectOrElseThrow(token, jwtSecretKey)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + private String createValidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } + + private String createInvalidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } +} From 35bc2ace6ee752ba04b7f05801e02ea44832a723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 23 Jan 2025 10:57:18 +0900 Subject: [PATCH 127/158] =?UTF-8?q?test:=20=EC=9C=A0=EC=A0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20(#156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 통합 테스트 구조 개선 - BaseIntegrationTest 추가로 테스트 설정 공통화 - TestDataSetUpHelper 도입으로 테스트 데이터 관리 개선 * refactor: 대학교 통합 테스트 BaseIntegrationTest를 사용하는 것으로 변경 * test: 마이페이지 조회 관련 통합테스트 코드 추가 * test: 정보 수정을 위한 마이페이지 조회 관련 통합테스트 코드 추가 * test: 관심 대학교 목록 조회 관련 통합테스트 코드 추가 * test: 프로필 이미지를 수정 관련 통합테스트 코드 추가 * test: 닉네임 수정 관련 통합테스트 코드 추가 * chore: 예외 응답 테스트명 "~면_예외_응답을_반환한다"로 통일 * refactor: TestDataSetUpHelper 생성자 주입에서 필드 주입으로 변경 * style: 카멜케이스에 맞게 수정 * refactor: @Nested를 사용하여 프로필 이미지, 닉네임 수정 테스트 그룹화 * refactor: TestDataSetUpHelper를 BaseIntegrationTest로 통합 --- .../siteuser/service/SiteUserServiceTest.java | 309 ++++++++++++++++++ .../integration/BaseIntegrationTest.java} | 40 ++- .../service/UniversityLikeServiceTest.java | 7 +- .../service/UniversityQueryServiceTest.java | 5 +- .../UniversityRecommendServiceTest.java | 3 +- 5 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java rename src/test/java/com/example/solidconnection/{university/service/UniversityDataSetUpIntegrationTest.java => support/integration/BaseIntegrationTest.java} (96%) diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java new file mode 100644 index 000000000..8fdae031e --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java @@ -0,0 +1,309 @@ +package com.example.solidconnection.siteuser.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; +import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; +import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.PROFILE_IMAGE_NEEDED; +import static com.example.solidconnection.siteuser.service.SiteUserService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; +import static com.example.solidconnection.siteuser.service.SiteUserService.NICKNAME_LAST_CHANGE_DATE_FORMAT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.eq; + +@DisplayName("유저 서비스 테스트") +class SiteUserServiceTest extends BaseIntegrationTest { + + @Autowired + private SiteUserService siteUserService; + + @MockBean + private S3Service s3Service; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private LikedUniversityRepository likedUniversityRepository; + + @Test + void 마이페이지_정보를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + int likedUniversityCount = createLikedUniversities(testUser); + + // when + MyPageResponse response = siteUserService.getMyPageInfo(testUser.getEmail()); + + // then + Assertions.assertAll( + () -> assertThat(response.nickname()).isEqualTo(testUser.getNickname()), + () -> assertThat(response.profileImageUrl()).isEqualTo(testUser.getProfileImageUrl()), + () -> assertThat(response.role()).isEqualTo(testUser.getRole()), + () -> assertThat(response.birth()).isEqualTo(testUser.getBirth()), + () -> assertThat(response.email()).isEqualTo(testUser.getEmail()), + () -> assertThat(response.likedPostCount()).isEqualTo(testUser.getPostLikeList().size()), + () -> assertThat(response.likedUniversityCount()).isEqualTo(likedUniversityCount) + ); + } + + @Test + void 내_정보를_수정하기_위한_마이페이지_정보를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + MyPageUpdateResponse response = siteUserService.getMyPageInfoToUpdate(testUser.getEmail()); + + // then + Assertions.assertAll( + () -> assertThat(response.nickname()).isEqualTo(testUser.getNickname()), + () -> assertThat(response.profileImageUrl()).isEqualTo(testUser.getProfileImageUrl()) + ); + } + + @Test + void 관심_대학교_목록을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + int likedUniversityCount = createLikedUniversities(testUser); + + // when + List response = siteUserService.getWishUniversity(testUser.getEmail()); + + // then + assertThat(response) + .hasSize(likedUniversityCount) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("id") + .containsAll(List.of( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + )); + } + + @Nested + class 프로필_이미지_수정_테스트 { + + @Test + void 새로운_이미지로_성공적으로_업데이트한다() { + // given + SiteUser testUser = createSiteUser(); + String expectedUrl = "newProfileImageUrl"; + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse(expectedUrl)); + + // when + ProfileImageUpdateResponse response = siteUserService.updateProfileImage( + testUser.getEmail(), + imageFile + ); + + // then + assertThat(response.profileImageUrl()).isEqualTo(expectedUrl); + } + + @Test + void 프로필을_처음_수정하는_것이면_이전_이미지를_삭제하지_않는다() { + // given + SiteUser testUser = createSiteUser(); + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); + + // when + siteUserService.updateProfileImage(testUser.getEmail(), imageFile); + + // then + then(s3Service).should(never()).deleteExProfile(any()); + } + + @Test + void 프로필을_처음_수정하는_것이_아니라면_이전_이미지를_삭제한다() { + // given + SiteUser testUser = createSiteUserWithCustomProfile(); + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); + + // when + siteUserService.updateProfileImage(testUser.getEmail(), imageFile); + + // then + then(s3Service).should().deleteExProfile(testUser.getEmail()); + } + + @Test + void 빈_이미지_파일로_프로필을_수정하면_예외_응답을_반환한다() { + // given + SiteUser testUser = createSiteUser(); + MockMultipartFile emptyFile = createEmptyImageFile(); + + // when & then + assertThatCode(() -> siteUserService.updateProfileImage(testUser.getEmail(), emptyFile)) + .isInstanceOf(CustomException.class) + .hasMessage(PROFILE_IMAGE_NEEDED.getMessage()); + } + } + + @Nested + class 닉네임_수정_테스트 { + + @Test + void 닉네임을_성공적으로_수정한다() { + // given + SiteUser testUser = createSiteUser(); + String newNickname = "newNickname"; + NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); + + // when + NicknameUpdateResponse response = siteUserService.updateNickname( + testUser.getEmail(), + request + ); + + // then + SiteUser updatedUser = siteUserRepository.getByEmail(testUser.getEmail()); + assertThat(updatedUser.getNicknameModifiedAt()).isNotNull(); + assertThat(response.nickname()).isEqualTo(newNickname); + } + + @Test + void 중복된_닉네임으로_변경하면_예외_응답을_반환한다() { + // given + createDuplicatedSiteUser(); + SiteUser testUser = createSiteUser(); + NicknameUpdateRequest request = new NicknameUpdateRequest("duplicatedNickname"); + + // when & then + assertThatCode(() -> siteUserService.updateNickname(testUser.getEmail(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(NICKNAME_ALREADY_EXISTED.getMessage()); + } + + @Test + void 최소_대기기간이_지나지_않은_상태에서_변경하면_예외_응답을_반환한다() { + // given + SiteUser testUser = createSiteUser(); + LocalDateTime modifiedAt = LocalDateTime.now().minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES - 1); + testUser.setNicknameModifiedAt(modifiedAt); + siteUserRepository.save(testUser); + + NicknameUpdateRequest request = new NicknameUpdateRequest("newNickname"); + + // when & then + assertThatCode(() -> + siteUserService.updateNickname(testUser.getEmail(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(createExpectedErrorMessage(modifiedAt)); + } + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private SiteUser createSiteUserWithCustomProfile() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profile/profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private void createDuplicatedSiteUser() { + SiteUser siteUser = new SiteUser( + "duplicated@example.com", + "duplicatedNickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + siteUserRepository.save(siteUser); + } + + private int createLikedUniversities(SiteUser testUser) { + LikedUniversity likedUniversity1 = new LikedUniversity(null, 괌대학_A_지원_정보, testUser); + LikedUniversity likedUniversity2 = new LikedUniversity(null, 메이지대학_지원_정보, testUser); + LikedUniversity likedUniversity3 = new LikedUniversity(null, 코펜하겐IT대학_지원_정보, testUser); + + likedUniversityRepository.save(likedUniversity1); + likedUniversityRepository.save(likedUniversity2); + likedUniversityRepository.save(likedUniversity3); + return likedUniversityRepository.countBySiteUser_Email(testUser.getEmail()); + } + + private MockMultipartFile createValidImageFile() { + return new MockMultipartFile( + "image", + "test.jpg", + "image/jpeg", + "test image content".getBytes() + ); + } + + private MockMultipartFile createEmptyImageFile() { + return new MockMultipartFile( + "image", + "empty.jpg", + "image/jpeg", + new byte[0] + ); + } + + private String createExpectedErrorMessage(LocalDateTime modifiedAt) { + String formatLastModifiedAt = String.format( + "(마지막 수정 시간 : %s)", + NICKNAME_LAST_CHANGE_DATE_FORMAT.format(modifiedAt) + ); + return CAN_NOT_CHANGE_NICKNAME_YET.getMessage() + " : " + formatLastModifiedAt; + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java similarity index 96% rename from src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java rename to src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index 91518a09e..b1f7d9203 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.university.service; +package com.example.solidconnection.support.integration; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.Region; @@ -13,21 +13,19 @@ import com.example.solidconnection.university.repository.LanguageRequirementRepository; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.UniversityRepository; -import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.web.server.LocalServerPort; import java.util.HashSet; import static com.example.solidconnection.type.SemesterAvailableForDispatch.ONE_SEMESTER; import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; -@ExtendWith(DatabaseClearExtension.class) @TestContainerSpringBootTest -abstract class UniversityDataSetUpIntegrationTest { +@ExtendWith(DatabaseClearExtension.class) +public abstract class BaseIntegrationTest { public static Region 영미권; public static Region 유럽; @@ -59,12 +57,6 @@ abstract class UniversityDataSetUpIntegrationTest { public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; public static UniversityInfoForApply 메이지대학_지원_정보; - @Value("${university.term}") - public String term; - - @LocalServerPort - private int port; - @Autowired private RegionRepository regionRepository; @@ -80,20 +72,33 @@ abstract class UniversityDataSetUpIntegrationTest { @Autowired private LanguageRequirementRepository languageRequirementRepository; + @Value("${university.term}") + public String term; + @BeforeEach - public void setUpBasicData() { - RestAssured.port = port; + public void setUpBaseData() { + setUpRegions(); + setUpCountries(); + setUpUniversities(); + setUpUniversityInfos(); + setUpLanguageRequirements(); + } + private void setUpRegions() { 영미권 = regionRepository.save(new Region("AMERICAS", "영미권")); 유럽 = regionRepository.save(new Region("EUROPE", "유럽")); 아시아 = regionRepository.save(new Region("ASIA", "아시아")); + } + private void setUpCountries() { 미국 = countryRepository.save(new Country("US", "미국", 영미권)); 캐나다 = countryRepository.save(new Country("CA", "캐나다", 영미권)); 덴마크 = countryRepository.save(new Country("DK", "덴마크", 유럽)); 오스트리아 = countryRepository.save(new Country("AT", "오스트리아", 유럽)); 일본 = countryRepository.save(new Country("JP", "일본", 아시아)); + } + private void setUpUniversities() { 영미권_미국_괌대학 = universityRepository.save(new University( null, "괌대학", "University of Guam", "university_of_guam", "https://www.uog.edu/admissions/international-students", @@ -179,7 +184,9 @@ public void setUpBasicData() { "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png", null, 일본, 아시아 )); + } + private void setUpUniversityInfos() { 괌대학_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( null, term, "괌대학(A형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", @@ -259,7 +266,9 @@ public void setUpBasicData() { "detailsForAccommodation", "detailsForEnglishCourse", "details", new HashSet<>(), 아시아_일본_메이지대학 )); + } + private void setUpLanguageRequirements() { saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEFL_IBT, "80"); saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEIC, "800"); saveLanguageTestRequirement(괌대학_B_지원_정보, LanguageTestType.TOEFL_IBT, "70"); @@ -275,7 +284,10 @@ public void setUpBasicData() { } private void saveLanguageTestRequirement( - UniversityInfoForApply universityInfoForApply, LanguageTestType testType, String minScore) { + UniversityInfoForApply universityInfoForApply, + LanguageTestType testType, + String minScore + ) { LanguageRequirement languageRequirement = new LanguageRequirement( null, testType, diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java index 50adf6839..14371486c 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java @@ -4,6 +4,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -22,7 +23,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; @DisplayName("대학교 좋아요 서비스 테스트") -class UniversityLikeServiceTest extends UniversityDataSetUpIntegrationTest { +class UniversityLikeServiceTest extends BaseIntegrationTest { @Autowired private UniversityLikeService universityLikeService; @@ -65,7 +66,7 @@ class UniversityLikeServiceTest extends UniversityDataSetUpIntegrationTest { } @Test - void 존재하지_않는_대학_좋아요_시도하면_예외를_반환한다() { + void 존재하지_않는_대학_좋아요_시도하면_예외_응답을_반환한다() { // given SiteUser testUser = createSiteUser(); Long invalidUniversityId = 9999L; @@ -102,7 +103,7 @@ class UniversityLikeServiceTest extends UniversityDataSetUpIntegrationTest { } @Test - void 존재하지_않는_대학의_좋아요_여부_조회시_예외를_반환한다() { + void 존재하지_않는_대학의_좋아요_여부를_조회하면_예외_응답을_반환한다() { // given SiteUser testUser = createSiteUser(); Long invalidUniversityId = 9999L; diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java index 54c235452..1cd0d755f 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java @@ -1,6 +1,7 @@ package com.example.solidconnection.university.service; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.dto.UniversityDetailResponse; import com.example.solidconnection.university.dto.LanguageRequirementResponse; @@ -23,7 +24,7 @@ import static org.mockito.Mockito.times; @DisplayName("대학교 조회 서비스 테스트") -class UniversityQueryServiceTest extends UniversityDataSetUpIntegrationTest { +class UniversityQueryServiceTest extends BaseIntegrationTest { @Autowired private UniversityQueryService universityQueryService; @@ -91,7 +92,7 @@ class UniversityQueryServiceTest extends UniversityDataSetUpIntegrationTest { } @Test - void 존재하지_않는_대학_상세정보_조회시_예외_응답을_반환한다() { + void 존재하지_않는_대학_상세정보를_조회하면_예외_응답을_반환한다() { // given Long invalidUniversityInfoForApplyId = 9999L; diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java index 1fee99033..cadd45aaf 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java @@ -6,6 +6,7 @@ import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -22,7 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; @DisplayName("대학교 추천 서비스 테스트") -class UniversityRecommendServiceTest extends UniversityDataSetUpIntegrationTest { +class UniversityRecommendServiceTest extends BaseIntegrationTest { @Autowired private UniversityRecommendService universityRecommendService; From 2af7b77ff8bb48bb42889f8bd695c1002a80e39f Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 28 Jan 2025 00:24:54 +0900 Subject: [PATCH 128/158] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=8C=80=ED=95=99=20=ED=9B=84=EB=B3=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../university/service/GeneralRecommendUniversities.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java index 5c1c2e787..92054eee6 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java @@ -31,7 +31,8 @@ public class GeneralRecommendUniversities { "오스트라바 대학", "RMIT멜버른공과대학(A형)", "알브슈타트 지그마링엔 대학", "뉴저지시티대학(A형)", "도요대학", "템플대학(A형)", "빈 공과대학교", "리스본대학 공과대학", "바덴뷔르템베르크 산학협력대학", "긴다이대학", "네바다주립대학 라스베이거스(B형)", "릴 가톨릭 대학", - "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교" + "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교", + "밀라노공과대학", "장물랭리옹3세대학교", "시드니대학", "아우크스부르크대학", "쳄니츠 공과대학", "북경외국어대학교 IBS" ); @Value("${university.term}") From d4e57c0076de24516b579a10e283a368d8240f00 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 28 Jan 2025 00:25:28 +0900 Subject: [PATCH 129/158] =?UTF-8?q?hotfix:=20=EB=8C=80=ED=95=99=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EA=B8=B0=EB=8A=A5=20=EC=A0=95=EC=83=81?= =?UTF-8?q?=ED=99=94=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 기본 추천 대학 후보 추가 * chore: 기능 설명 주석 보충 --- .../university/service/GeneralRecommendUniversities.java | 3 ++- .../university/service/UniversityRecommendService.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java index 5c1c2e787..92054eee6 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java @@ -31,7 +31,8 @@ public class GeneralRecommendUniversities { "오스트라바 대학", "RMIT멜버른공과대학(A형)", "알브슈타트 지그마링엔 대학", "뉴저지시티대학(A형)", "도요대학", "템플대학(A형)", "빈 공과대학교", "리스본대학 공과대학", "바덴뷔르템베르크 산학협력대학", "긴다이대학", "네바다주립대학 라스베이거스(B형)", "릴 가톨릭 대학", - "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교" + "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교", + "밀라노공과대학", "장물랭리옹3세대학교", "시드니대학", "아우크스부르크대학", "쳄니츠 공과대학", "북경외국어대학교 IBS" ); @Value("${university.term}") diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index cf9c112f8..6a6a43fbf 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -33,7 +33,7 @@ public class UniversityRecommendService { * 사용자 맞춤 추천 대학교를 불러온다. * - 회원가입 시 선택한 관심 지역과 관심 국가에 해당하는 대학 중, 이번 term 에 열리는 학교들을 불러온다. * - 불러온 맞춤 추천 대학교의 순서를 무작위로 섞는다. - * - 맞춤 추천 대학교의 수가 6개보다 적다면, 공통 추천 대학교를 부족한 수 만큼 불러온다. + * - 맞춤 추천 대학교의 수가 6개보다 적다면, 공통 추천 대학교 후보에서 이번 term 에 열리는 학교들을 부족한 수 만큼 불러온다. * */ @Transactional(readOnly = true) public UniversityRecommendsResponse getPersonalRecommends(String email) { From f26e75ccefd268acc180f0b9d82b6e5f151be312 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 28 Jan 2025 03:11:34 +0900 Subject: [PATCH 130/158] =?UTF-8?q?refactor:=20auth=20type=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 사용자가 다양한 인증 유형을 가지도록 수정 * refactor: 함수 이름 변경 - 더 의미를 전달하도록 * test: 테스트 코드 패키지 이동 * refactor: EntryPoint 말고 필터로 필터에서 발생하는 모든 예외 처리 * feat: 토큰 만료 검사 함수 추가 * test: 깨지는 테스트 해결 * refactor: 로그아웃 로직 수정 * test: 로그아웃 필터 테스트 수정 --- .../auth/domain/TokenType.java | 4 +- .../auth/service/AuthService.java | 20 ++-- .../auth/service/TokenProvider.java | 6 +- ...Point.java => ExceptionHandlerFilter.java} | 33 ++++--- .../security/JwtAuthenticationFilter.java | 23 ++--- .../security/SecurityConfiguration.java | 6 +- .../config/security/SignOutCheckFilter.java | 20 ++-- .../siteuser/domain/AuthType.java | 9 ++ .../siteuser/domain/SiteUser.java | 34 ++++++- .../solidconnection/util/JwtUtils.java | 23 ++++- ...3__add_auth_type_column_and_unique_key.sql | 13 +++ .../service}/TokenProviderTest.java | 3 +- .../security/ExceptionHandlerFilterTest.java | 91 +++++++++++++++++++ .../security/JwtAuthenticationFilterTest.java | 23 +++-- .../security/SignOutCheckFilterTest.java | 38 ++++---- .../repository/SiteUserRepositoryTest.java | 62 +++++++++++++ .../support/TestContainerSpringBootTest.java | 2 + .../unit/repository/PostRepositoryTest.java | 10 +- .../solidconnection/util/JwtUtilsTest.java | 85 +++++++++++++++-- 19 files changed, 395 insertions(+), 110 deletions(-) rename src/main/java/com/example/solidconnection/config/security/{JwtAuthenticationEntryPoint.java => ExceptionHandlerFilter.java} (63%) create mode 100644 src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java create mode 100644 src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql rename src/test/java/com/example/solidconnection/{config/token => auth/service}/TokenProviderTest.java (96%) create mode 100644 src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java create mode 100644 src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java index 7fa6045f7..ad5607a27 100644 --- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -7,7 +7,9 @@ public enum TokenType { ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days - KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60); // 1hour + KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60), // 1hour + BLACKLIST("BLACKLIST:", ACCESS.expireTime) + ; private final String prefix; private final int expireTime; diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 29fb1b347..e16044e97 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -15,6 +15,7 @@ import java.util.concurrent.TimeUnit; import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @@ -30,16 +31,13 @@ public class AuthService { /* * 로그아웃 한다. - * - 리프레시 토큰을 무효화하기 위해 리프레시 토큰의 value 를 변경한다. - * - 어떤 사용자가 엑세스 토큰으로 인증이 필요한 기능을 사용하려 할 때, 로그아웃 검증이 진행되는데, - * - 이때 리프레시 토큰의 value 가 SIGN_OUT_VALUE 이면 예외 응답이 반환된다. - * - (TokenValidator.validateNotSignOut() 참고) + * - 엑세스 토큰을 블랙리스트에 추가한다. * */ - public void signOut(String email) { + public void signOut(String accessToken) { redisTemplate.opsForValue().set( - REFRESH.addPrefixToSubject(email), - SIGN_OUT_VALUE, - REFRESH.getExpireTime(), + BLACKLIST.addPrefixToSubject(accessToken), + accessToken, + BLACKLIST.getExpireTime(), TimeUnit.MILLISECONDS ); } @@ -61,15 +59,15 @@ public void quit(String email) { * - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다. * - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다. * */ - public ReissueResponse reissue(String email) { + public ReissueResponse reissue(String subject) { // 리프레시 토큰 만료 확인 - String refreshTokenKey = REFRESH.addPrefixToSubject(email); + String refreshTokenKey = REFRESH.addPrefixToSubject(subject); String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); if (ObjectUtils.isEmpty(refreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = tokenProvider.generateToken(email, ACCESS); + String newAccessToken = tokenProvider.generateToken(subject, ACCESS); tokenProvider.saveToken(newAccessToken, ACCESS); return new ReissueResponse(newAccessToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 693a968ea..9cba77c36 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -12,8 +12,8 @@ import java.util.Date; import java.util.concurrent.TimeUnit; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; @RequiredArgsConstructor @Component @@ -35,7 +35,7 @@ public String generateToken(String email, TokenType tokenType) { } public String saveToken(String token, TokenType tokenType) { - String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + String subject = parseSubject(token, jwtProperties.secret()); redisTemplate.opsForValue().set( tokenType.addPrefixToSubject(subject), token, @@ -46,6 +46,6 @@ public String saveToken(String token, TokenType tokenType) { } public String getEmail(String token) { - return parseSubject(token, jwtProperties.secret()); + return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java similarity index 63% rename from src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java rename to src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java index 7487858be..59022c198 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java @@ -3,12 +3,15 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -16,24 +19,32 @@ @Component @RequiredArgsConstructor -public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { +public class ExceptionHandlerFilter extends OncePerRequestFilter { private final ObjectMapper objectMapper; @Override - public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); - writeResponse(response, errorResponse); + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (CustomException e) { + customCommence(response, e); + } catch (Exception e) { + generalCommence(response, e); + } } - public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); + public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { + SecurityContextHolder.clearContext(); + ErrorResponse errorResponse = new ErrorResponse(customException); writeResponse(response, errorResponse); } - public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(customException); + public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { + SecurityContextHolder.clearContext(); + ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); writeResponse(response, errorResponse); } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index e01009be1..5c7ab9f97 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -1,6 +1,5 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -8,7 +7,6 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; @@ -16,7 +14,7 @@ import java.io.IOException; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseSubject; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @@ -27,7 +25,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final String REISSUE_METHOD = "post"; private final JwtProperties jwtProperties; - private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @@ -39,19 +36,11 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, return; } - try { - String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); - UserDetails userDetails = new JwtUserDetails(subject); - Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - } catch (AuthenticationException e) { - jwtAuthenticationEntryPoint.commence(request, response, e); - } catch (CustomException e) { - jwtAuthenticationEntryPoint.customCommence(response, e); - } catch (Exception e) { - jwtAuthenticationEntryPoint.generalCommence(response, e); - } + String subject = parseSubject(token, jwtProperties.secret()); + UserDetails userDetails = new JwtUserDetails(subject); + Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + filterChain.doFilter(request, response); } private boolean isReissueRequest(HttpServletRequest request) { diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index d28d883ca..3f6307f8f 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -19,6 +19,7 @@ public class SecurityConfiguration { private final CorsProperties corsProperties; + private final ExceptionHandlerFilter exceptionHandlerFilter; private final SignOutCheckFilter signOutCheckFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; @@ -45,8 +46,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) - .addFilterBefore(this.jwtAuthenticationFilter, BasicAuthenticationFilter.class) - .addFilterBefore(this.signOutCheckFilter, JwtAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) + .addFilterBefore(signOutCheckFilter, JwtAuthenticationFilter.class) + .addFilterBefore(exceptionHandlerFilter, SignOutCheckFilter.class) .build(); } } diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java index 3c1218d13..c71252f1f 100644 --- a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java @@ -13,10 +13,8 @@ import java.io.IOException; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; -import static com.example.solidconnection.auth.service.AuthService.SIGN_OUT_VALUE; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; -import static com.example.solidconnection.util.JwtUtils.parseSubject; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @@ -25,24 +23,20 @@ public class SignOutCheckFilter extends OncePerRequestFilter { private final RedisTemplate redisTemplate; private final JwtProperties jwtProperties; - private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = parseTokenFromRequest(request); - if (token == null || !isSignOut(token)) { - filterChain.doFilter(request, response); - return; + if (token != null && hasSignedOut(token)) { + throw new CustomException(USER_ALREADY_SIGN_OUT); } - - jwtAuthenticationEntryPoint.customCommence(response, new CustomException(USER_ALREADY_SIGN_OUT)); + filterChain.doFilter(request, response); } - private boolean isSignOut(String accessToken) { - String subject = parseSubject(accessToken, jwtProperties.secret()); - String refreshToken = REFRESH.addPrefixToSubject(subject); - return SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(refreshToken)); + private boolean hasSignedOut(String accessToken) { + String blacklistKey = BLACKLIST.addPrefixToSubject(accessToken); + return redisTemplate.opsForValue().get(blacklistKey) != null; } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java new file mode 100644 index 000000000..d9d0b582c --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.siteuser.domain; + +public enum AuthType { + + KAKAO, + APPLE, + EMAIL, + ; +} diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index e518a5efb..2c2a5d8be 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -17,6 +17,8 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -32,15 +34,25 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @AllArgsConstructor +@Table(uniqueConstraints = { + @UniqueConstraint( + name = "uk_site_user_email_auth_type", + columnNames = {"email", "auth_type"} + ) +}) public class SiteUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 100) + @Column(name = "email", nullable = false, length = 100) private String email; + @Column(name = "auth_type", nullable = false, length = 100) + @Enumerated(EnumType.STRING) + private AuthType authType; + @Setter @Column(nullable = false, length = 100) private String nickname; @@ -100,5 +112,25 @@ public SiteUser( this.preparationStage = preparationStage; this.role = role; this.gender = gender; + this.authType = AuthType.KAKAO; + } + + public SiteUser( + String email, + String nickname, + String profileImageUrl, + String birth, + PreparationStatus preparationStage, + Role role, + Gender gender, + AuthType authType) { + this.email = email; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.birth = birth; + this.preparationStage = preparationStage; + this.role = role; + this.gender = gender; + this.authType = authType; } } diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java index a3775365d..3a1b58520 100644 --- a/src/main/java/com/example/solidconnection/util/JwtUtils.java +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -6,6 +6,8 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; +import java.util.Date; + import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; @Component @@ -25,22 +27,37 @@ public static String parseTokenFromRequest(HttpServletRequest request) { return token.substring(TOKEN_PREFIX.length()); } - public static String parseSubject(String token, String secretKey) { + public static String parseSubjectIgnoringExpiration(String token, String secretKey) { try { return extractSubject(token, secretKey); } catch (ExpiredJwtException e) { return e.getClaims().getSubject(); + } catch (Exception e) { + throw new CustomException(INVALID_TOKEN); } } - public static String parseSubjectOrElseThrow(String token, String secretKey) { + public static String parseSubject(String token, String secretKey) { try { return extractSubject(token, secretKey); - } catch (ExpiredJwtException e) { + } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } } + public static boolean isExpired(String token, String secretKey) { + try { + Date expiration = Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody() + .getExpiration(); + return expiration.before(new Date()); + } catch (Exception e) { + return true; + } + } + private static String extractSubject(String token, String secretKey) throws ExpiredJwtException { return Jwts.parser() .setSigningKey(secretKey) diff --git a/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql b/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql new file mode 100644 index 000000000..e89c4aa1b --- /dev/null +++ b/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql @@ -0,0 +1,13 @@ +ALTER TABLE site_user +ADD COLUMN auth_type ENUM('KAKAO', 'APPLE', 'EMAIL'); + +UPDATE site_user +SET auth_type = 'KAKAO' +WHERE auth_type IS NULL; + +ALTER TABLE site_user +MODIFY COLUMN auth_type ENUM('KAKAO', 'APPLE', 'EMAIL') NOT NULL; + +ALTER TABLE site_user +ADD CONSTRAINT uk_site_user_email_auth_type +UNIQUE (email, auth_type); diff --git a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java similarity index 96% rename from src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java rename to src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java index d3992a33a..8cc91e2c0 100644 --- a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java @@ -1,7 +1,6 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.service; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; diff --git a/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java b/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java new file mode 100644 index 000000000..f4e8dc666 --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java @@ -0,0 +1,91 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +class ExceptionHandlerFilterTest { + + @Autowired + private ExceptionHandlerFilter exceptionHandlerFilter; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + @BeforeEach() + void setUp() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + } + + @Test + void 필터_체인에서_예외가_발생하면_SecurityContext_를_초기화한다() throws Exception { + // given + Authentication authentication = mock(TestingAuthenticationToken.class); + SecurityContextHolder.getContext().setAuthentication(authentication); + willThrow(new RuntimeException()).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } + + @Test + void 필터_체인에서_예외가_발생하지_않으면_다음_필터로_진행한다() throws Exception { + // given + willDoNothing().given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + @ParameterizedTest + @MethodSource("provideException") + void 필터_체인에서_예외가_발생하면_예외_응답을_반환한다(Throwable throwable) throws Exception { + // given + willThrow(throwable).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + private static Stream provideException() { + return Stream.of( + new RuntimeException(), + new CustomException(ErrorCode.INVALID_TOKEN) + ); + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java index c0256f75a..16e3639f1 100644 --- a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java +++ b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -17,7 +18,9 @@ import java.util.Date; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; @@ -89,12 +92,10 @@ class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { .compact(); request = createRequestWithToken(token); - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + // when & then + assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_TOKEN.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } @@ -109,12 +110,10 @@ class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { .compact(); request = createRequestWithToken(token); - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + // when & then + assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_TOKEN.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } } diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java index 13544152b..a067bf9d9 100644 --- a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -17,9 +18,9 @@ import java.util.Date; import java.util.Objects; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; @@ -55,15 +56,15 @@ void setUp() { @Test void 로그아웃한_토큰이면_예외를_응답한다() throws Exception { // given - request = createTokenRequest(subject); - String refreshTokenKey = REFRESH.addPrefixToSubject(subject); + String token = createToken(subject); + request = createRequest(token); + String refreshTokenKey = BLACKLIST.addPrefixToSubject(token); redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); - // when - signOutCheckFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(response.getStatus()).isEqualTo(USER_ALREADY_SIGN_OUT.getCode()); + // when & then + assertThatCode(() -> signOutCheckFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(USER_ALREADY_SIGN_OUT.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } @@ -82,7 +83,8 @@ void setUp() { @Test void 로그아웃하지_않은_토큰이면_다음_필터로_전달한다() throws Exception { // given - request = createTokenRequest(subject); + String token = createToken(subject); + request = createRequest(token); // when signOutCheckFilter.doFilterInternal(request, response, filterChain); @@ -91,14 +93,16 @@ void setUp() { then(filterChain).should().doFilter(request, response); } - private HttpServletRequest createTokenRequest(String subject) { - String token = Jwts.builder() - .setSubject(subject) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); + private String createToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + private HttpServletRequest createRequest(String token) { MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", "Bearer " + token); return request; diff --git a/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java new file mode 100644 index 000000000..d3433937a --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.siteuser.repository; + +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.TestContainerDataJpaTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; + +import static org.assertj.core.api.Assertions.assertThatCode; + +@TestContainerDataJpaTest +class SiteUserRepositoryTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Nested + class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다 { + + @Test + void 이메일과_인증_유형이_동일한_사용자를_저장하면_예외_응답을_반환한다() { + // given + SiteUser user1 = createSiteUser("email", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email", AuthType.KAKAO); + siteUserRepository.save(user1); + + // when, then + assertThatCode(() -> siteUserRepository.save(user2)) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + void 이메일이_같더라도_인증_유형이_다른_사용자는_정상_저장한다() { + // given + SiteUser user1 = createSiteUser("email", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email", AuthType.APPLE); + siteUserRepository.save(user1); + + // when, then + assertThatCode(() -> siteUserRepository.save(user2)) + .doesNotThrowAnyException(); + } + } + + private SiteUser createSiteUser(String email, AuthType authType) { + return new SiteUser( + email, + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE, + authType + ); + } +} diff --git a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java index bcb110c6b..fe9b74f60 100644 --- a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java +++ b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.support; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; @@ -11,6 +12,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +@ExtendWith({DatabaseClearExtension.class}) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @ActiveProfiles("test") diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index 42da9de22..a37a0e6bf 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; @@ -30,10 +29,13 @@ @TestContainerDataJpaTest @DisplayName("게시글 레포지토리 테스트") class PostRepositoryTest { + @Autowired private PostRepository postRepository; + @Autowired private BoardRepository boardRepository; + @Autowired private SiteUserRepository siteUserRepository; @@ -89,7 +91,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { Post foundPost = postRepository.getByIdUsingEntityGraph(post.getId()); foundPost.getPostImageList().size(); // 추가쿼리 발생하지 않는다. @@ -98,7 +99,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { // given Long invalidId = -1L; @@ -114,7 +114,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회한다() { Post foundPost = postRepository.getById(post.getId()); @@ -122,7 +121,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { Long invalidId = -1L; @@ -136,7 +134,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글_좋아요를_등록한다() { // given Long likeCount = post.getLikeCount(); @@ -150,7 +147,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글_좋아요를_삭제한다() { // given Long likeCount = post.getLikeCount(); diff --git a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java index 4dfc11540..95bdd5a52 100644 --- a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java +++ b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java @@ -12,7 +12,7 @@ import java.util.Date; import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -59,7 +59,7 @@ class 요청으로부터_토큰을_추출한다 { } @Nested - class 토큰으로부터_subject_를_추출한다 { + class 유효한_토큰으로부터_subject_를_추출한다 { @Test void 유효한_토큰의_subject_를_추출한다() { @@ -75,13 +75,29 @@ class 토큰으로부터_subject_를_추출한다 { } @Test - void 유효하지_않은_토큰의_subject_를_추출한다() { + void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { + // given + String subject = "subject123"; + String token = createExpiredToken(subject); + + // when + assertThatCode(() -> parseSubject(token, jwtSecretKey)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + @Nested + class 만료된_토큰으로부터_subject_를_추출한다 { + + @Test + void 만료된_토큰의_subject_를_예외를_발생시키지_않고_추출한다() { // given String subject = "subject999"; - String token = createInvalidToken(subject); + String token = createExpiredToken(subject); // when - String extractedSubject = parseSubject(token, jwtSecretKey); + String extractedSubject = parseSubjectIgnoringExpiration(token, jwtSecretKey); // then assertThat(extractedSubject).isEqualTo(subject); @@ -90,16 +106,56 @@ class 토큰으로부터_subject_를_추출한다 { @Test void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { // given - String subject = "subject123"; - String token = createInvalidToken(subject); + String token = createExpiredUnsignedToken("hackers secret key"); - // when - assertThatCode(() -> parseSubjectOrElseThrow(token, jwtSecretKey)) + // when & then + assertThatCode(() -> parseSubjectIgnoringExpiration(token, jwtSecretKey)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } } + + @Nested + class 토큰이_만료되었는지_확인한다 { + + @Test + void 서명된_토큰의_만료_여부를_반환한다() { + // given + String subject = "subject123"; + String validToken = createValidToken(subject); + String expiredToken = createExpiredToken(subject); + + // when + boolean isExpired1 = JwtUtils.isExpired(validToken, jwtSecretKey); + boolean isExpired2 = JwtUtils.isExpired(expiredToken, jwtSecretKey); + + // then + assertAll( + () -> assertThat(isExpired1).isFalse(), + () -> assertThat(isExpired2).isTrue() + ); + } + + @Test + void 서명되지_않은_토큰의_만료_여부를_반환한다() { + // given + String subject = "subject123"; + String validToken = createValidToken(subject); + String expiredToken = createExpiredToken(subject); + + // when + boolean isExpired1 = JwtUtils.isExpired(validToken, "wrong-secret-key"); + boolean isExpired2 = JwtUtils.isExpired(expiredToken, "wrong-secret-key"); + + // then + assertAll( + () -> assertThat(isExpired1).isTrue(), + () -> assertThat(isExpired2).isTrue() + ); + } + } + private String createValidToken(String subject) { return Jwts.builder() .setSubject(subject) @@ -109,7 +165,7 @@ private String createValidToken(String subject) { .compact(); } - private String createInvalidToken(String subject) { + private String createExpiredToken(String subject) { return Jwts.builder() .setSubject(subject) .setIssuedAt(new Date()) @@ -117,4 +173,13 @@ private String createInvalidToken(String subject) { .signWith(SignatureAlgorithm.HS256, jwtSecretKey) .compact(); } + + private String createExpiredUnsignedToken(String jwtSecretKey) { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } } From b37becd4a9a6de7a6ffefd8487bea74316e16b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:47:43 +0900 Subject: [PATCH 131/158] =?UTF-8?q?test:=20=EC=96=B4=ED=95=99=EC=A0=90?= =?UTF-8?q?=EC=88=98=20=EA=B4=80=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: GPA 등록 관련 통합테스트 코드 추가 * test: 어학 시험 점수 등록 관련 통합테스트 코드 추가 * test: GPA 조회 관련 통합테스트 코드 추가 * test: 어학시험 조회 관련 통합테스트 코드 추가 * test: GPA 점수 등록 테스트 시 VerifyStatus 검증 추가 * refactor: GPA 테스트 시 Long 타입 long으로 변경 * test: GpaScore 전체 url이 아닌 resource까지의 path로 변경 --- .../score/service/ScoreServiceTest.java | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java new file mode 100644 index 000000000..4a511d867 --- /dev/null +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -0,0 +1,204 @@ +package com.example.solidconnection.score.service; + +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.dto.GpaScoreRequest; +import com.example.solidconnection.score.dto.GpaScoreStatus; +import com.example.solidconnection.score.dto.GpaScoreStatusResponse; +import com.example.solidconnection.score.dto.LanguageTestScoreRequest; +import com.example.solidconnection.score.dto.LanguageTestScoreStatus; +import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.type.VerifyStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("점수 서비스 테스트") +class ScoreServiceTest extends BaseIntegrationTest { + + @Autowired + private ScoreService scoreService; + + @Autowired + private GpaScoreRepository gpaScoreRepository; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + + @Test + void GPA_점수_상태를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + List scores = List.of( + createGpaScore(testUser, 3.5, 4.5), + createGpaScore(testUser, 3.8, 4.5) + ); + + // when + GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser.getEmail()); + + // then + assertThat(response.gpaScoreStatusList()) + .hasSize(scores.size()) + .containsExactlyInAnyOrder( + scores.stream() + .map(GpaScoreStatus::from) + .toArray(GpaScoreStatus[]::new) + ); + } + + @Test + void GPA_점수가_없는_경우_빈_리스트를_반환한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser.getEmail()); + + // then + assertThat(response.gpaScoreStatusList()).isEmpty(); + } + + @Test + void 어학_시험_점수_상태를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + List scores = List.of( + createLanguageTestScore(testUser, LanguageTestType.TOEIC, "100"), + createLanguageTestScore(testUser, LanguageTestType.TOEFL_IBT, "7.5") + ); + + // when + LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser.getEmail()); + + // then + assertThat(response.languageTestScoreStatusList()) + .hasSize(scores.size()) + .containsExactlyInAnyOrder( + scores.stream() + .map(LanguageTestScoreStatus::from) + .toArray(LanguageTestScoreStatus[]::new) + ); + } + + @Test + void 어학_시험_점수가_없는_경우_빈_리스트를_반환한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser.getEmail()); + + // then + assertThat(response.languageTestScoreStatusList()).isEmpty(); + } + + @Test + void GPA_점수를_등록한다() { + // given + SiteUser testUser = createSiteUser(); + GpaScoreRequest request = createGpaScoreRequest(); + + // when + long scoreId = scoreService.submitGpaScore(testUser.getEmail(), request); + GpaScore savedScore = gpaScoreRepository.findById(scoreId).orElseThrow(); + + // then + assertAll( + () -> assertThat(savedScore.getId()).isEqualTo(scoreId), + () -> assertThat(savedScore.getGpa().getGpa()).isEqualTo(request.gpa()), + () -> assertThat(savedScore.getGpa().getGpaCriteria()).isEqualTo(request.gpaCriteria()), + () -> assertThat(savedScore.getIssueDate()).isEqualTo(request.issueDate()), + () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) + ); + } + + @Test + void 어학_시험_점수를_등록한다() { + // given + SiteUser testUser = createSiteUser(); + LanguageTestScoreRequest request = createLanguageTestScoreRequest(); + + // when + long scoreId = scoreService.submitLanguageTestScore(testUser.getEmail(), request); + LanguageTestScore savedScore = languageTestScoreRepository.findById(scoreId).orElseThrow(); + + // then + assertAll( + () -> assertThat(savedScore.getId()).isEqualTo(scoreId), + () -> assertThat(savedScore.getLanguageTest().getLanguageTestType()).isEqualTo(request.languageTestType()), + () -> assertThat(savedScore.getLanguageTest().getLanguageTestScore()).isEqualTo(request.languageTestScore()), + () -> assertThat(savedScore.getIssueDate()).isEqualTo(request.issueDate()), + () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) + ); + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private GpaScore createGpaScore(SiteUser siteUser, double gpa, double gpaCriteria) { + GpaScore gpaScore = new GpaScore( + new Gpa(gpa, gpaCriteria, "/gpa-report.pdf"), + siteUser, + LocalDate.now() + ); + return gpaScoreRepository.save(gpaScore); + } + + private LanguageTestScore createLanguageTestScore(SiteUser siteUser, LanguageTestType languageTestType, String score) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(languageTestType, score, "/gpa-report.pdf"), + LocalDate.now(), + siteUser + ); + return languageTestScoreRepository.save(languageTestScore); + } + + private GpaScoreRequest createGpaScoreRequest() { + return new GpaScoreRequest( + 3.5, + 4.5, + LocalDate.now(), + "/gpa-report.pdf" + ); + } + + private LanguageTestScoreRequest createLanguageTestScoreRequest() { + return new LanguageTestScoreRequest( + LanguageTestType.TOEFL_IBT, + "100", + LocalDate.now(), + "/gpa-report.pdf" + ); + } +} From 68560a1826e0fe3e46cdfc05dade6bd29f206a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:31:46 +0900 Subject: [PATCH 132/158] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 게시글 생성 관련 통합테스트 코드 추가 * test: 게시글 수정 관련 통합테스트 코드 추가 * refactor: PostService Query, Command, Like Service로 분리 * test: 게시글 삭제 관련 통합테스트 코드 추가 * test: 게시글 조회 관련 통합테스트 코드 추가 * test: 게시글 좋아요 관련 통합테스트 코드 추가 * refactor: 유효하지 않은 코드 조회 시 에러 발생 테스트코드 삭제 * chore: 예외 응답 테스트명 "~면_예외_응답을_반환한다"로 통일 * style: 사용하지 않는 import문 제거 * refactor: 유저와 게시판 BaseIntegrationTest에서 미리 생성하도록 변경 * refactor: BaseIntegrationTest에서 생성한 유저와 게시판 사용하는 것으로 변경 - 기존 유저와 게시판 생성하는 private 함수 제거 * test: when 절 코드를 then 절로 이동 --- .../post/controller/PostController.java | 21 +- ...stService.java => PostCommandService.java} | 153 ++----- .../post/service/PostLikeService.java | 71 ++++ .../post/service/PostQueryService.java | 76 ++++ .../solidconnection/service/RedisService.java | 4 + .../PostLikeCountConcurrencyTest.java | 10 +- .../post/service/PostCommandServiceTest.java | 373 ++++++++++++++++++ .../post/service/PostLikeServiceTest.java | 136 +++++++ .../post/service/PostQueryServiceTest.java | 127 ++++++ .../integration/BaseIntegrationTest.java | 54 +++ .../unit/service/PostServiceTest.java | 132 ++++--- 11 files changed, 967 insertions(+), 190 deletions(-) rename src/main/java/com/example/solidconnection/post/service/{PostService.java => PostCommandService.java} (60%) create mode 100644 src/main/java/com/example/solidconnection/post/service/PostLikeService.java create mode 100644 src/main/java/com/example/solidconnection/post/service/PostQueryService.java create mode 100644 src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index 05cdfc574..c3ff3ce3a 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -8,7 +8,9 @@ import com.example.solidconnection.post.dto.PostLikeResponse; import com.example.solidconnection.post.dto.PostUpdateRequest; import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.post.service.PostCommandService; +import com.example.solidconnection.post.service.PostLikeService; +import com.example.solidconnection.post.service.PostQueryService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -32,7 +34,9 @@ @RequestMapping("/communities") public class PostController { - private final PostService postService; + private final PostQueryService postQueryService; + private final PostCommandService postCommandService; + private final PostLikeService postLikeService; @PostMapping(value = "/{code}/posts") public ResponseEntity createPost( @@ -44,7 +48,7 @@ public ResponseEntity createPost( if (imageFile == null) { imageFile = Collections.emptyList(); } - PostCreateResponse post = postService + PostCreateResponse post = postCommandService .createPost(principal.getName(), code, postCreateRequest, imageFile); return ResponseEntity.ok().body(post); } @@ -60,19 +64,18 @@ public ResponseEntity updatePost( if (imageFile == null) { imageFile = Collections.emptyList(); } - PostUpdateResponse postUpdateResponse = postService + PostUpdateResponse postUpdateResponse = postCommandService .updatePost(principal.getName(), code, postId, postUpdateRequest, imageFile); return ResponseEntity.ok().body(postUpdateResponse); } - @GetMapping("/{code}/posts/{post_id}") public ResponseEntity findPostById( Principal principal, @PathVariable("code") String code, @PathVariable("post_id") Long postId) { - PostFindResponse postFindResponse = postService + PostFindResponse postFindResponse = postQueryService .findPostById(principal.getName(), code, postId); return ResponseEntity.ok().body(postFindResponse); } @@ -83,7 +86,7 @@ public ResponseEntity deletePostById( @PathVariable("code") String code, @PathVariable("post_id") Long postId) { - PostDeleteResponse postDeleteResponse = postService.deletePostById(principal.getName(), code, postId); + PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(principal.getName(), code, postId); return ResponseEntity.ok().body(postDeleteResponse); } @@ -94,7 +97,7 @@ public ResponseEntity likePost( @PathVariable("post_id") Long postId ) { - PostLikeResponse postLikeResponse = postService.likePost(principal.getName(), code, postId); + PostLikeResponse postLikeResponse = postLikeService.likePost(principal.getName(), code, postId); return ResponseEntity.ok().body(postLikeResponse); } @@ -105,7 +108,7 @@ public ResponseEntity dislikePost( @PathVariable("post_id") Long postId ) { - PostDislikeResponse postDislikeResponse = postService.dislikePost(principal.getName(), code, postId); + PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(principal.getName(), code, postId); return ResponseEntity.ok().body(postDislikeResponse); } } diff --git a/src/main/java/com/example/solidconnection/post/service/PostService.java b/src/main/java/com/example/solidconnection/post/service/PostCommandService.java similarity index 60% rename from src/main/java/com/example/solidconnection/post/service/PostService.java rename to src/main/java/com/example/solidconnection/post/service/PostCommandService.java index d31cfb97a..7b0c4f937 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostCommandService.java @@ -1,30 +1,20 @@ package com.example.solidconnection.post.service; import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.dto.PostFindBoardResponse; import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.service.CommentService; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.post.dto.PostCreateRequest; import com.example.solidconnection.post.dto.PostCreateResponse; import com.example.solidconnection.post.dto.PostDeleteResponse; -import com.example.solidconnection.post.dto.PostDislikeResponse; -import com.example.solidconnection.post.dto.PostFindPostImageResponse; -import com.example.solidconnection.post.dto.PostFindResponse; -import com.example.solidconnection.post.dto.PostLikeResponse; import com.example.solidconnection.post.dto.PostUpdateRequest; import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.repository.PostLikeRepository; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.ImgType; @@ -33,7 +23,6 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -41,72 +30,24 @@ import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; -import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_POST_LIKE; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; @Service @RequiredArgsConstructor -public class PostService { +public class PostCommandService { private final PostRepository postRepository; private final SiteUserRepository siteUserRepository; private final BoardRepository boardRepository; private final S3Service s3Service; - private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; - private final PostLikeRepository postLikeRepository; - - private String validateCode(String code) { - try { - return String.valueOf(BoardCode.valueOf(code)); - } catch (IllegalArgumentException ex) { - throw new CustomException(INVALID_BOARD_CODE); - } - } - - private void validateOwnership(Post post, String email) { - if (!post.getSiteUser().getEmail().equals(email)) { - throw new CustomException(INVALID_POST_ACCESS); - } - } - - private void validateFileSize(List imageFile) { - if (imageFile.isEmpty()) { - return; - } - if (imageFile.size() > 5) { - throw new CustomException(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES); - } - } - - private void validateQuestion(Post post) { - if (post.getIsQuestion()) { - throw new CustomException(CAN_NOT_DELETE_OR_UPDATE_QUESTION); - } - } - - private void validatePostCategory(String category) { - if (!EnumUtils.isValidEnum(PostCategory.class, category) || category.equals(PostCategory.전체.toString())) { - throw new CustomException(INVALID_POST_CATEGORY); - } - } - - private Boolean getIsOwner(Post post, String email) { - return post.getSiteUser().getEmail().equals(email); - } - - private Boolean getIsLiked(Post post, SiteUser siteUser) { - return postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser) - .isPresent(); - } @Transactional public PostCreateResponse createPost(String email, String code, PostCreateRequest postCreateRequest, List imageFile) { - // 유효성 검증 String boardCode = validateCode(code); validatePostCategory(postCreateRequest.postCategory()); @@ -126,7 +67,6 @@ public PostCreateResponse createPost(String email, String code, PostCreateReques @Transactional public PostUpdateResponse updatePost(String email, String code, Long postId, PostUpdateRequest postUpdateRequest, List imageFile) { - // 유효성 검증 String boardCode = validateCode(code); Post post = postRepository.getById(postId); @@ -155,40 +95,8 @@ private void savePostImages(List imageFile, Post post) { } } - private void removePostImages(Post post) { - for (PostImage postImage : post.getPostImageList()) { - s3Service.deletePostImage(postImage.getUrl()); - } - post.getPostImageList().clear(); - } - - @Transactional(readOnly = true) - public PostFindResponse findPostById(String email, String code, Long postId) { - - String boardCode = validateCode(code); - - Post post = postRepository.getByIdUsingEntityGraph(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); - Boolean isOwner = getIsOwner(post, email); - Boolean isLiked = getIsLiked(post, siteUser); - - PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); - PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); - List postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList()); - List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); - - // caching && 어뷰징 방지 - if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email, postId))) { - redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); - } - - return PostFindResponse.from( - post, isOwner, isLiked, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); - } - @Transactional public PostDeleteResponse deletePostById(String email, String code, Long postId) { - String boardCode = validateCode(code); Post post = postRepository.getById(postId); validateOwnership(post, email); @@ -203,40 +111,45 @@ public PostDeleteResponse deletePostById(String email, String code, Long postId) return new PostDeleteResponse(postId); } - @Transactional(isolation = Isolation.READ_COMMITTED) - public PostLikeResponse likePost(String email, String code, Long postId) { - - String boardCode = validateCode(code); - Post post = postRepository.getById(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); - validateDuplicatePostLike(post, siteUser); - - PostLike postLike = new PostLike(); - postLike.setPostAndSiteUser(post, siteUser); - postLikeRepository.save(postLike); - postRepository.increaseLikeCount(post.getId()); - - return PostLikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + private String validateCode(String code) { + try { + return String.valueOf(BoardCode.valueOf(code)); + } catch (IllegalArgumentException ex) { + throw new CustomException(INVALID_BOARD_CODE); + } } - private void validateDuplicatePostLike(Post post, SiteUser siteUser) { - if (postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser).isPresent()) { - throw new CustomException(DUPLICATE_POST_LIKE); + private void validateOwnership(Post post, String email) { + if (!post.getSiteUser().getEmail().equals(email)) { + throw new CustomException(INVALID_POST_ACCESS); } } - @Transactional(isolation = Isolation.READ_COMMITTED) - public PostDislikeResponse dislikePost(String email, String code, Long postId) { + private void validateFileSize(List imageFile) { + if (imageFile.isEmpty()) { + return; + } + if (imageFile.size() > 5) { + throw new CustomException(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES); + } + } - String boardCode = validateCode(code); - Post post = postRepository.getById(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); + private void validateQuestion(Post post) { + if (post.getIsQuestion()) { + throw new CustomException(CAN_NOT_DELETE_OR_UPDATE_QUESTION); + } + } - PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); - postLike.resetPostAndSiteUser(); - postLikeRepository.deleteById(postLike.getId()); - postRepository.decreaseLikeCount(post.getId()); + private void validatePostCategory(String category) { + if (!EnumUtils.isValidEnum(PostCategory.class, category) || category.equals(PostCategory.전체.toString())) { + throw new CustomException(INVALID_POST_CATEGORY); + } + } - return PostDislikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + private void removePostImages(Post post) { + for (PostImage postImage : post.getPostImageList()) { + s3Service.deletePostImage(postImage.getUrl()); + } + post.getPostImageList().clear(); } } diff --git a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java b/src/main/java/com/example/solidconnection/post/service/PostLikeService.java new file mode 100644 index 000000000..8a72d5f9f --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/service/PostLikeService.java @@ -0,0 +1,71 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.dto.PostDislikeResponse; +import com.example.solidconnection.post.dto.PostLikeResponse; +import com.example.solidconnection.post.repository.PostLikeRepository; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.BoardCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_POST_LIKE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; + +@Service +@RequiredArgsConstructor +public class PostLikeService { + + private final PostRepository postRepository; + private final SiteUserRepository siteUserRepository; + private final PostLikeRepository postLikeRepository; + + @Transactional(isolation = Isolation.READ_COMMITTED) + public PostLikeResponse likePost(String email, String code, Long postId) { + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + validateDuplicatePostLike(post, siteUser); + + PostLike postLike = new PostLike(); + postLike.setPostAndSiteUser(post, siteUser); + postLikeRepository.save(postLike); + postRepository.increaseLikeCount(post.getId()); + + return PostLikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public PostDislikeResponse dislikePost(String email, String code, Long postId) { + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + + PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); + postLike.resetPostAndSiteUser(); + postLikeRepository.deleteById(postLike.getId()); + postRepository.decreaseLikeCount(post.getId()); + + return PostDislikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + } + + private String validateCode(String code) { + try { + return String.valueOf(BoardCode.valueOf(code)); + } catch (IllegalArgumentException ex) { + throw new CustomException(INVALID_BOARD_CODE); + } + } + + private void validateDuplicatePostLike(Post post, SiteUser siteUser) { + if (postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser).isPresent()) { + throw new CustomException(DUPLICATE_POST_LIKE); + } + } +} diff --git a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/post/service/PostQueryService.java new file mode 100644 index 000000000..d53470124 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/service/PostQueryService.java @@ -0,0 +1,76 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.dto.PostFindBoardResponse; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.post.dto.PostFindResponse; +import com.example.solidconnection.post.repository.PostLikeRepository; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.util.RedisUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; + +@Service +@RequiredArgsConstructor +public class PostQueryService { + + private final PostRepository postRepository; + private final SiteUserRepository siteUserRepository; + private final CommentService commentService; + private final RedisService redisService; + private final RedisUtils redisUtils; + private final PostLikeRepository postLikeRepository; + + @Transactional(readOnly = true) + public PostFindResponse findPostById(String email, String code, Long postId) { + String boardCode = validateCode(code); + + Post post = postRepository.getByIdUsingEntityGraph(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + Boolean isOwner = getIsOwner(post, email); + Boolean isLiked = getIsLiked(post, siteUser); + + PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); + PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); + List postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList()); + List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); + + // caching && 어뷰징 방지 + if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email, postId))) { + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); + } + + return PostFindResponse.from( + post, isOwner, isLiked, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); + } + + private String validateCode(String code) { + try { + return String.valueOf(BoardCode.valueOf(code)); + } catch (IllegalArgumentException ex) { + throw new CustomException(INVALID_BOARD_CODE); + } + } + + private Boolean getIsOwner(Post post, String email) { + return post.getSiteUser().getEmail().equals(email); + } + + private Boolean getIsLiked(Post post, SiteUser siteUser) { + return postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser) + .isPresent(); + } +} diff --git a/src/main/java/com/example/solidconnection/service/RedisService.java b/src/main/java/com/example/solidconnection/service/RedisService.java index 93a9de74f..36be7b66f 100644 --- a/src/main/java/com/example/solidconnection/service/RedisService.java +++ b/src/main/java/com/example/solidconnection/service/RedisService.java @@ -42,4 +42,8 @@ public boolean isPresent(String key) { return Boolean.TRUE.equals(redisTemplate.opsForValue() .setIfAbsent(key, "1", Long.parseLong(VALIDATE_VIEW_COUNT_TTL.getValue()), TimeUnit.SECONDS)); } + + public boolean isKeyExists(String key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } } diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index e553eb4bb..36bd91819 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -4,7 +4,8 @@ import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.post.service.PostCommandService; +import com.example.solidconnection.post.service.PostLikeService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; @@ -30,7 +31,7 @@ class PostLikeCountConcurrencyTest { @Autowired - private PostService postService; + private PostLikeService postLikeService; @Autowired private PostRepository postRepository; @Autowired @@ -118,8 +119,8 @@ private Post createPost(Board board, SiteUser siteUser) { String email = "email" + i; executorService.submit(() -> { try { - postService.likePost(email, board.getCode(), post.getId()); - postService.dislikePost(email, board.getCode(), post.getId()); + postLikeService.likePost(email, board.getCode(), post.getId()); + postLikeService.dislikePost(email, board.getCode(), post.getId()); } finally { doneSignal.countDown(); } @@ -135,5 +136,4 @@ private Post createPost(Board board, SiteUser siteUser) { assertEquals(likeCount, postRepository.getById(post.getId()).getLikeCount()); } - } diff --git a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java new file mode 100644 index 000000000..eb1b2b652 --- /dev/null +++ b/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java @@ -0,0 +1,373 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.PostCreateRequest; +import com.example.solidconnection.post.dto.PostCreateResponse; +import com.example.solidconnection.post.dto.PostDeleteResponse; +import com.example.solidconnection.post.dto.PostUpdateRequest; +import com.example.solidconnection.post.dto.PostUpdateResponse; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.repositories.PostImageRepository; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.util.RedisUtils; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.then; + +@DisplayName("게시글 생성/수정/삭제 서비스 테스트") +class PostCommandServiceTest extends BaseIntegrationTest { + + @Autowired + private PostCommandService postCommandService; + + @MockBean + private S3Service s3Service; + + @Autowired + private RedisService redisService; + + @Autowired + private RedisUtils redisUtils; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostImageRepository postImageRepository; + + @Nested + class 게시글_생성_테스트 { + + @Test + @Transactional + void 게시글을_성공적으로_생성한다() { + // given + PostCreateRequest request = createPostCreateRequest(PostCategory.자유.name()); + List imageFiles = List.of(createImageFile()); + String expectedImageUrl = "test-image-url"; + given(s3Service.uploadFiles(any(), eq(ImgType.COMMUNITY))) + .willReturn(List.of(new UploadedFileUrlResponse(expectedImageUrl))); + + // when + PostCreateResponse response = postCommandService.createPost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + request, + imageFiles + ); + + // then + Post savedPost = postRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(response.id()).isEqualTo(savedPost.getId()), + () -> assertThat(savedPost.getTitle()).isEqualTo(request.title()), + () -> assertThat(savedPost.getContent()).isEqualTo(request.content()), + () -> assertThat(savedPost.getIsQuestion()).isEqualTo(request.isQuestion()), + () -> assertThat(savedPost.getCategory().name()).isEqualTo(request.postCategory()), + () -> assertThat(savedPost.getBoard().getCode()).isEqualTo(자유게시판.getCode()), + () -> assertThat(savedPost.getPostImageList()).hasSize(imageFiles.size()), + () -> assertThat(savedPost.getPostImageList()) + .extracting(PostImage::getUrl) + .containsExactly(expectedImageUrl) + ); + } + + @Test + void 전체_카테고리로_생성하면_예외_응답을_반환한다() { + // given + PostCreateRequest request = createPostCreateRequest(PostCategory.전체.name()); + List imageFiles = List.of(); + + // when & then + assertThatThrownBy(() -> + postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_CATEGORY.getMessage()); + } + + @Test + void 존재하지_않는_카테고리로_생성하면_예외_응답을_반환한다() { + // given + PostCreateRequest request = createPostCreateRequest("INVALID_CATEGORY"); + List imageFiles = List.of(); + + // when & then + assertThatThrownBy(() -> + postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_CATEGORY.getMessage()); + } + + @Test + void 이미지를_5개_초과하여_업로드하면_예외_응답을_반환한다() { + // given + PostCreateRequest request = createPostCreateRequest(PostCategory.자유.name()); + List imageFiles = createSixImageFiles(); + + // when & then + assertThatThrownBy(() -> + postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); + } + } + + @Nested + class 게시글_수정_테스트 { + + @Test + @Transactional + void 게시글을_성공적으로_수정한다() { + // given + String originImageUrl = "origin-image-url"; + String expectedImageUrl = "update-image-url"; + Post testPost = createPost(자유게시판, 테스트유저_1, originImageUrl); + PostUpdateRequest request = createPostUpdateRequest(); + List imageFiles = List.of(createImageFile()); + + given(s3Service.uploadFiles(any(), eq(ImgType.COMMUNITY))) + .willReturn(List.of(new UploadedFileUrlResponse(expectedImageUrl))); + + // when + PostUpdateResponse response = postCommandService.updatePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId(), + request, + imageFiles + ); + + // then + Post updatedPost = postRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(updatedPost.getTitle()).isEqualTo(request.title()), + () -> assertThat(updatedPost.getContent()).isEqualTo(request.content()), + () -> assertThat(updatedPost.getCategory().name()).isEqualTo(request.postCategory()), + () -> assertThat(updatedPost.getPostImageList()).hasSize(imageFiles.size()), + () -> assertThat(updatedPost.getPostImageList()) + .extracting(PostImage::getUrl) + .containsExactly(expectedImageUrl) + ); + then(s3Service).should().deletePostImage(originImageUrl); + } + + @Test + void 다른_사용자의_게시글을_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1, "origin-image-url"); + PostUpdateRequest request = createPostUpdateRequest(); + List imageFiles = List.of(); + + // when & then + assertThatThrownBy(() -> + postCommandService.updatePost( + 테스트유저_2.getEmail(), + 자유게시판.getCode(), + testPost.getId(), + request, + imageFiles + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_ACCESS.getMessage()); + } + + @Test + void 질문_게시글을_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createQuestionPost(자유게시판, 테스트유저_1, "origin-image-url"); + PostUpdateRequest request = createPostUpdateRequest(); + List imageFiles = List.of(); + + // when & then + assertThatThrownBy(() -> + postCommandService.updatePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId(), + request, + imageFiles + )) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); + } + + @Test + void 이미지를_5개_초과하여_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1, "origin-image-url"); + PostUpdateRequest request = createPostUpdateRequest(); + List imageFiles = createSixImageFiles(); + + // when & then + assertThatThrownBy(() -> + postCommandService.updatePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId(), + request, + imageFiles + )) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); + } + } + + @Nested + class 게시글_삭제_테스트 { + + @Test + void 게시글을_성공적으로_삭제한다() { + // given + String originImageUrl = "origin-image-url"; + Post testPost = createPost(자유게시판, 테스트유저_1, originImageUrl); + String viewCountKey = redisUtils.getPostViewCountRedisKey(testPost.getId()); + redisService.increaseViewCount(viewCountKey); + + // when + PostDeleteResponse response = postCommandService.deletePostById( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + ); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(testPost.getId()), + () -> assertThat(postRepository.findById(testPost.getId())).isEmpty(), + () -> assertThat(redisService.isKeyExists(viewCountKey)).isFalse() + ); + then(s3Service).should().deletePostImage(originImageUrl); + } + + @Test + void 다른_사용자의_게시글을_삭제하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1, "origin-image-url"); + + // when & then + assertThatThrownBy(() -> + postCommandService.deletePostById( + 테스트유저_2.getEmail(), + 자유게시판.getCode(), + testPost.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_ACCESS.getMessage()); + } + + @Test + void 질문_게시글을_삭제하면_예외_응답을_반환한다() { + // given + Post testPost = createQuestionPost(자유게시판, 테스트유저_1, "origin-image-url"); + + // when & then + assertThatThrownBy(() -> + postCommandService.deletePostById( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); + } + } + + private PostCreateRequest createPostCreateRequest(String category) { + return new PostCreateRequest( + category, + "테스트 제목", + "테스트 내용", + false + ); + } + + private MockMultipartFile createImageFile() { + return new MockMultipartFile( + "image", + "test.jpg", + "image/jpeg", + "test image content".getBytes() + ); + } + + private List createSixImageFiles() { + return List.of( + createImageFile(), + createImageFile(), + createImageFile(), + createImageFile(), + createImageFile(), + createImageFile() + ); + } + + private Post createPost(Board board, SiteUser siteUser, String originImageUrl) { + Post post = new Post( + "원본 제목", + "원본 내용", + false, + 0L, + 0L, + PostCategory.자유 + ); + post.setBoardAndSiteUser(board, siteUser); + Post savedPost = postRepository.save(post); + PostImage postImage = new PostImage(originImageUrl); + postImage.setPost(savedPost); + postImageRepository.save(postImage); + return savedPost; + } + + private Post createQuestionPost(Board board, SiteUser siteUser, String originImageUrl) { + Post post = new Post( + "질문 제목", + "질문 내용", + true, + 0L, + 0L, + PostCategory.질문 + ); + post.setBoardAndSiteUser(board, siteUser); + Post savedPost = postRepository.save(post); + PostImage postImage = new PostImage(originImageUrl); + postImage.setPost(savedPost); + postImageRepository.save(postImage); + return savedPost; + } + + private PostUpdateRequest createPostUpdateRequest() { + return new PostUpdateRequest( + PostCategory.자유.name(), + "수정된 제목", + "수정된 내용" + ); + } +} diff --git a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java new file mode 100644 index 000000000..9fe6a2704 --- /dev/null +++ b/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java @@ -0,0 +1,136 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.PostDislikeResponse; +import com.example.solidconnection.post.dto.PostLikeResponse; +import com.example.solidconnection.post.repository.PostLikeRepository; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.PostCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_POST_LIKE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("게시글 좋아요 서비스 테스트") +class PostLikeServiceTest extends BaseIntegrationTest { + + @Autowired + private PostLikeService postLikeService; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostLikeRepository postLikeRepository; + + @Nested + class 게시글_좋아요_테스트 { + + @Test + void 게시글을_성공적으로_좋아요한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + long beforeLikeCount = testPost.getLikeCount(); + + // when + PostLikeResponse response = postLikeService.likePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + ); + + // then + Post likedPost = postRepository.findById(testPost.getId()).orElseThrow(); + assertAll( + () -> assertThat(response.likeCount()).isEqualTo(beforeLikeCount + 1), + () -> assertThat(response.isLiked()).isTrue(), + () -> assertThat(likedPost.getLikeCount()).isEqualTo(beforeLikeCount + 1), + () -> assertThat(postLikeRepository.findPostLikeByPostAndSiteUser(likedPost, 테스트유저_1)).isPresent() + ); + } + + @Test + void 이미_좋아요한_게시글을_다시_좋아요하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + postLikeService.likePost(테스트유저_1.getEmail(), 자유게시판.getCode(), testPost.getId()); + + // when & then + assertThatThrownBy(() -> + postLikeService.likePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(DUPLICATE_POST_LIKE.getMessage()); + } + } + + @Nested + class 게시글_좋아요_취소_테스트 { + + @Test + void 게시글_좋아요를_성공적으로_취소한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + PostLikeResponse beforeResponse = postLikeService.likePost(테스트유저_1.getEmail(), 자유게시판.getCode(), testPost.getId()); + long beforeLikeCount = beforeResponse.likeCount(); + + // when + PostDislikeResponse response = postLikeService.dislikePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + ); + + // then + Post unlikedPost = postRepository.findById(testPost.getId()).orElseThrow(); + assertAll( + () -> assertThat(response.likeCount()).isEqualTo(beforeLikeCount - 1), + () -> assertThat(response.isLiked()).isFalse(), + () -> assertThat(unlikedPost.getLikeCount()).isEqualTo(beforeLikeCount - 1), + () -> assertThat(postLikeRepository.findPostLikeByPostAndSiteUser(unlikedPost, 테스트유저_1)).isEmpty() + ); + } + + @Test + void 좋아요하지_않은_게시글을_좋아요_취소하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + + // when & then + assertThatThrownBy(() -> + postLikeService.dislikePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_LIKE.getMessage()); + } + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "테스트 제목", + "테스트 내용", + false, + 0L, + 0L, + PostCategory.자유 + ); + post.setBoardAndSiteUser(board, siteUser); + return postRepository.save(post); + } +} diff --git a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java new file mode 100644 index 000000000..7ec36b0df --- /dev/null +++ b/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java @@ -0,0 +1,127 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.post.dto.PostFindResponse; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.repositories.PostImageRepository; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.util.RedisUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("게시글 조회 서비스 테스트") +class PostQueryServiceTest extends BaseIntegrationTest { + + @Autowired + private PostQueryService postQueryService; + + @Autowired + private RedisService redisService; + + @Autowired + private RedisUtils redisUtils; + + @Autowired + private PostRepository postRepository; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private PostImageRepository postImageRepository; + + @Test + void 게시글을_성공적으로_조회한다() { + // given + String expectedImageUrl = "test-image-url"; + List imageUrls = List.of(expectedImageUrl); + Post testPost = createPost(자유게시판, 테스트유저_1, expectedImageUrl); + List comments = createComments(testPost, 테스트유저_1, List.of("첫번째 댓글", "두번째 댓글")); + + String validateKey = redisUtils.getValidatePostViewCountRedisKey(테스트유저_1.getEmail(), testPost.getId()); + String viewCountKey = redisUtils.getPostViewCountRedisKey(testPost.getId()); + + // when + PostFindResponse response = postQueryService.findPostById( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + ); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(testPost.getId()), + () -> assertThat(response.title()).isEqualTo(testPost.getTitle()), + () -> assertThat(response.content()).isEqualTo(testPost.getContent()), + () -> assertThat(response.isQuestion()).isEqualTo(testPost.getIsQuestion()), + () -> assertThat(response.likeCount()).isEqualTo(testPost.getLikeCount()), + () -> assertThat(response.viewCount()).isEqualTo(testPost.getViewCount()), + () -> assertThat(response.postCategory()).isEqualTo(String.valueOf(testPost.getCategory())), + + () -> assertThat(response.postFindBoardResponse().code()).isEqualTo(자유게시판.getCode()), + () -> assertThat(response.postFindBoardResponse().koreanName()).isEqualTo(자유게시판.getKoreanName()), + + () -> assertThat(response.postFindSiteUserResponse().id()).isEqualTo(테스트유저_1.getId()), + () -> assertThat(response.postFindSiteUserResponse().nickname()).isEqualTo(테스트유저_1.getNickname()), + () -> assertThat(response.postFindSiteUserResponse().profileImageUrl()).isEqualTo(테스트유저_1.getProfileImageUrl()), + + () -> assertThat(response.postFindPostImageResponses()) + .hasSize(imageUrls.size()) + .extracting(PostFindPostImageResponse::url) + .containsExactlyElementsOf(imageUrls), + + () -> assertThat(response.postFindCommentResponses()) + .hasSize(comments.size()) + .extracting(PostFindCommentResponse::content) + .containsExactlyElementsOf(comments.stream().map(Comment::getContent).toList()), + + () -> assertThat(response.isOwner()).isTrue(), + () -> assertThat(response.isLiked()).isFalse(), + + () -> assertThat(redisService.isKeyExists(viewCountKey)).isTrue(), + () -> assertThat(redisService.isKeyExists(validateKey)).isTrue() + ); + } + + private Post createPost(Board board, SiteUser siteUser, String originImageUrl) { + Post post = new Post( + "원본 제목", + "원본 내용", + false, + 0L, + 0L, + PostCategory.자유 + ); + post.setBoardAndSiteUser(board, siteUser); + Post savedPost = postRepository.save(post); + PostImage postImage = new PostImage(originImageUrl); + postImage.setPost(savedPost); + postImageRepository.save(postImage); + return savedPost; + } + + private List createComments(Post post, SiteUser siteUser, List contents) { + return contents.stream() + .map(content -> { + Comment comment = new Comment(content); + comment.setPostAndSiteUser(post, siteUser); + return commentRepository.save(comment); + }) + .toList(); + } +} diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index b1f7d9203..f588b87ae 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -1,12 +1,19 @@ package com.example.solidconnection.support.integration; +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.Region; import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.DatabaseClearExtension; import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; import com.example.solidconnection.university.domain.LanguageRequirement; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; @@ -20,6 +27,10 @@ import java.util.HashSet; +import static com.example.solidconnection.type.BoardCode.AMERICAS; +import static com.example.solidconnection.type.BoardCode.ASIA; +import static com.example.solidconnection.type.BoardCode.EUROPE; +import static com.example.solidconnection.type.BoardCode.FREE; import static com.example.solidconnection.type.SemesterAvailableForDispatch.ONE_SEMESTER; import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; @@ -27,6 +38,9 @@ @ExtendWith(DatabaseClearExtension.class) public abstract class BaseIntegrationTest { + public static SiteUser 테스트유저_1; + public static SiteUser 테스트유저_2; + public static Region 영미권; public static Region 유럽; public static Region 아시아; @@ -57,6 +71,14 @@ public abstract class BaseIntegrationTest { public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; public static UniversityInfoForApply 메이지대학_지원_정보; + public static Board 미주권; + public static Board 아시아권; + public static Board 유럽권; + public static Board 자유게시판; + + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired private RegionRepository regionRepository; @@ -72,16 +94,41 @@ public abstract class BaseIntegrationTest { @Autowired private LanguageRequirementRepository languageRequirementRepository; + @Autowired + private BoardRepository boardRepository; + @Value("${university.term}") public String term; @BeforeEach public void setUpBaseData() { + setUpSiteUsers(); setUpRegions(); setUpCountries(); setUpUniversities(); setUpUniversityInfos(); setUpLanguageRequirements(); + setUpBoards(); + } + + private void setUpSiteUsers() { + 테스트유저_1 = siteUserRepository.save(new SiteUser( + "test1@example.com", + "nickname1", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE)); + + 테스트유저_2 = siteUserRepository.save(new SiteUser( + "test2@example.com", + "nickname2", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE)); } private void setUpRegions() { @@ -283,6 +330,13 @@ private void setUpLanguageRequirements() { saveLanguageTestRequirement(메이지대학_지원_정보, LanguageTestType.JLPT, "N2"); } + private void setUpBoards() { + 미주권 = boardRepository.save(new Board(AMERICAS.name(), "미주권")); + 아시아권 = boardRepository.save(new Board(ASIA.name(), "아시아권")); + 유럽권 = boardRepository.save(new Board(EUROPE.name(), "유럽권")); + 자유게시판 = boardRepository.save(new Board(FREE.name(), "자유게시판")); + } + private void saveLanguageTestRequirement( UniversityInfoForApply universityInfoForApply, LanguageTestType testType, diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java index 57c5916a9..afc899255 100644 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java @@ -14,7 +14,9 @@ import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.dto.*; import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.post.service.PostCommandService; +import com.example.solidconnection.post.service.PostLikeService; +import com.example.solidconnection.post.service.PostQueryService; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; @@ -33,21 +35,41 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; -import java.util.*; - -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) @DisplayName("게시글 서비스 테스트") class PostServiceTest { + + @InjectMocks + PostQueryService postQueryService; + + @InjectMocks + PostCommandService postCommandService; + @InjectMocks - PostService postService; + PostLikeService postLikeService; + @Mock PostRepository postRepository; @Mock @@ -75,7 +97,6 @@ class PostServiceTest { private List imageFilesWithMoreThanFiveFiles; private List uploadedFileUrlResponseList; - @BeforeEach void setUp() { siteUser = createSiteUser(); @@ -206,7 +227,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.save(any(Post.class))).thenReturn(postWithImages); // When - PostCreateResponse postCreateResponse = postService.createPost( + PostCreateResponse postCreateResponse = postCommandService.createPost( siteUser.getEmail(), board.getCode(), postCreateRequest, imageFiles); // Then @@ -227,7 +248,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.save(postCreateRequest.toEntity(siteUser, board))).thenReturn(post); // When - PostCreateResponse postCreateResponse = postService.createPost( + PostCreateResponse postCreateResponse = postCommandService.createPost( siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList()); // Then @@ -245,7 +266,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { "자유", "title", "content", false); // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postService + CustomException exception = assertThrows(CustomException.class, () -> postCommandService .createPost(siteUser.getEmail(), invalidBoardCode, postCreateRequest, Collections.emptyList())); assertThat(exception.getMessage()) .isEqualTo(INVALID_BOARD_CODE.getMessage()); @@ -261,7 +282,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { invalidPostCategory, "title", "content", false); // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postService + CustomException exception = assertThrows(CustomException.class, () -> postCommandService .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList())); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_CATEGORY.getMessage()); @@ -276,7 +297,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { "자유", "title", "content", false); // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postService + CustomException exception = assertThrows(CustomException.class, () -> postCommandService .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, imageFilesWithMoreThanFiveFiles)); assertThat(exception.getMessage()) .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); @@ -294,7 +315,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.getById(post.getId())).thenReturn(post); // When - PostUpdateResponse response = postService.updatePost( + PostUpdateResponse response = postCommandService.updatePost( siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, Collections.emptyList()); // Then @@ -311,7 +332,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.getById(postWithImages.getId())).thenReturn(postWithImages); // When - PostUpdateResponse response = postService.updatePost( + PostUpdateResponse response = postCommandService.updatePost( siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, Collections.emptyList()); // Then @@ -329,7 +350,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); // When - PostUpdateResponse response = postService.updatePost( + PostUpdateResponse response = postCommandService.updatePost( siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFiles); // Then @@ -347,7 +368,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); // When - PostUpdateResponse response = postService.updatePost( + PostUpdateResponse response = postCommandService.updatePost( siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, imageFiles); // Then @@ -365,11 +386,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(siteUser.getEmail(), invalidBoardCode, post.getId(), postUpdateRequest, imageFiles)); + postCommandService.updatePost(siteUser.getEmail(), invalidBoardCode, post.getId(), postUpdateRequest, imageFiles)); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -381,7 +402,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(siteUser.getEmail(), board.getCode(), invalidPostId, postUpdateRequest, imageFiles)); + postCommandService.updatePost(siteUser.getEmail(), board.getCode(), invalidPostId, postUpdateRequest, imageFiles)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) @@ -397,7 +418,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(invalidEmail, board.getCode(), post.getId(), postUpdateRequest, imageFiles)); + postCommandService.updatePost(invalidEmail, board.getCode(), post.getId(), postUpdateRequest, imageFiles)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ACCESS.getMessage()); assertThat(exception.getCode()) @@ -412,14 +433,13 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(siteUser.getEmail(), board.getCode(), questionPost.getId(), postUpdateRequest, imageFiles)); + postCommandService.updatePost(siteUser.getEmail(), board.getCode(), questionPost.getId(), postUpdateRequest, imageFiles)); assertThat(exception.getMessage()) .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); assertThat(exception.getCode()) .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); } - @Test void 게시글을_수정할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { // Given @@ -428,7 +448,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFilesWithMoreThanFiveFiles)); + postCommandService.updatePost(siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFilesWithMoreThanFiveFiles)); assertThat(exception.getMessage()) .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); assertThat(exception.getCode()) @@ -448,7 +468,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(commentService.findCommentsByPostId(siteUser.getEmail(), post.getId())).thenReturn(commentFindResultDTOList); // When - PostFindResponse response = postService.findPostById(siteUser.getEmail(), board.getCode(), post.getId()); + PostFindResponse response = postQueryService.findPostById(siteUser.getEmail(), board.getCode(), post.getId()); // Then PostFindResponse expectedResponse = PostFindResponse.from( @@ -474,11 +494,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.findPostById(siteUser.getEmail(), invalidBoardCode, post.getId())); + postQueryService.findPostById(siteUser.getEmail(), invalidBoardCode, post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -489,7 +509,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.findPostById(siteUser.getEmail(), board.getCode(), invalidPostId)); + postQueryService.findPostById(siteUser.getEmail(), board.getCode(), invalidPostId)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) @@ -505,7 +525,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.getById(post.getId())).thenReturn(post); // When - PostDeleteResponse postDeleteResponse = postService.deletePostById(siteUser.getEmail(), board.getCode(), post.getId()); + PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), post.getId()); // Then assertEquals(postDeleteResponse.id(), post.getId()); @@ -521,11 +541,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.deletePostById(siteUser.getEmail(), invalidBoardCode, post.getId())); + postCommandService.deletePostById(siteUser.getEmail(), invalidBoardCode, post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -536,11 +556,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.deletePostById(siteUser.getEmail(), board.getCode(), invalidPostId)); + postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), invalidPostId)); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_POST_ID.getMessage()); + .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_POST_ID.getCode()); + .isEqualTo(INVALID_POST_ID.getCode()); } @Test @@ -551,7 +571,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.deletePostById(invalidEmail, board.getCode(), post.getId()) + postCommandService.deletePostById(invalidEmail, board.getCode(), post.getId()) ); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ACCESS.getMessage()); @@ -565,11 +585,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.deletePostById(siteUser.getEmail(), board.getCode(), questionPost.getId())); + postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), questionPost.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); + .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); + .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); } /** @@ -582,7 +602,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); // When - PostLikeResponse postLikeResponse = postService.likePost(siteUser.getEmail(), board.getCode(), post.getId()); + PostLikeResponse postLikeResponse = postLikeService.likePost(siteUser.getEmail(), board.getCode(), post.getId()); // Then assertEquals(postLikeResponse, PostLikeResponse.from(post)); @@ -597,7 +617,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.likePost(siteUser.getEmail(), board.getCode(), post.getId())); + postLikeService.likePost(siteUser.getEmail(), board.getCode(), post.getId())); assertThat(exception.getMessage()) .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getMessage()); assertThat(exception.getCode()) @@ -611,11 +631,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.likePost(siteUser.getEmail(), invalidBoardCode, post.getId())); + postLikeService.likePost(siteUser.getEmail(), invalidBoardCode, post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -626,7 +646,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.likePost(siteUser.getEmail(), board.getCode(), invalidPostId)); + postLikeService.likePost(siteUser.getEmail(), board.getCode(), invalidPostId)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) @@ -642,7 +662,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenReturn(postLike); // When - PostDislikeResponse postDislikeResponse = postService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId()); + PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId()); // Then assertEquals(postDislikeResponse, PostDislikeResponse.from(post)); @@ -657,11 +677,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId())); + postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_POST_LIKE.getMessage()); + .isEqualTo(INVALID_POST_LIKE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_POST_LIKE.getCode()); + .isEqualTo(INVALID_POST_LIKE.getCode()); } @Test @@ -671,11 +691,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.dislikePost(siteUser.getEmail(), invalidBoardCode, post.getId())); + postLikeService.dislikePost(siteUser.getEmail(), invalidBoardCode, post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -686,7 +706,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.dislikePost(siteUser.getEmail(), board.getCode(), invalidPostId)); + postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), invalidPostId)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) From b66e6106c6c0dc3df26a9097a2863ad3e3e75d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:41:49 +0900 Subject: [PATCH 133/158] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20(#173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 댓글 조회 관련 통합테스트 코드 추가 * test: 댓글 생성 관련 통합테스트 코드 추가 * test: 댓글 수정 관련 통합테스트 코드 추가 * test: 댓글 삭제 관련 통합테스트 코드 추가 * test: 기존 단위테스트 코드 삭제 --- .../comment/service/CommentServiceTest.java | 420 ++++++++++ .../unit/repository/BoardRepositoryTest.java | 139 ---- .../repository/CommentRepositoryTest.java | 147 ---- .../repository/GpaScoreRepositoryTest.java | 91 --- .../LanguageTestScoreRepositoryTest.java | 96 --- .../repository/PostLikeRepositoryTest.java | 120 --- .../unit/repository/PostRepositoryTest.java | 162 ---- .../unit/service/ApplicationServiceTest.java | 259 ------- .../unit/service/BoardServiceTest.java | 152 ---- .../unit/service/CommentServiceTest.java | 483 ------------ .../unit/service/PostServiceTest.java | 715 ------------------ .../unit/service/ScoreServiceTest.java | 201 ----- .../unit/service/SiteUserServiceTest.java | 197 ----- 13 files changed, 420 insertions(+), 2762 deletions(-) create mode 100644 src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java diff --git a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java new file mode 100644 index 000000000..418a04d8c --- /dev/null +++ b/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java @@ -0,0 +1,420 @@ +package com.example.solidconnection.comment.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.dto.CommentCreateRequest; +import com.example.solidconnection.comment.dto.CommentCreateResponse; +import com.example.solidconnection.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.PostCategory; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_LEVEL; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("댓글 서비스 테스트") +class CommentServiceTest extends BaseIntegrationTest { + + @Autowired + private CommentService commentService; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private PostRepository postRepository; + + @Nested + class 댓글_조회_테스트 { + + @Test + void 게시글의_모든_댓글을_조회한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); + List comments = List.of(parentComment, childComment); + + // when + List responses = commentService.findCommentsByPostId( + 테스트유저_1.getEmail(), + testPost.getId() + ); + + // then + assertAll( + () -> assertThat(responses).hasSize(comments.size()), + () -> assertThat(responses) + .filteredOn(response -> response.id().equals(parentComment.getId())) + .singleElement() + .satisfies(response -> assertAll( + () -> assertThat(response.id()).isEqualTo(parentComment.getId()), + () -> assertThat(response.parentId()).isNull(), + () -> assertThat(response.content()).isEqualTo(parentComment.getContent()), + () -> assertThat(response.isOwner()).isTrue(), + () -> assertThat(response.createdAt()).isEqualTo(parentComment.getCreatedAt()), + () -> assertThat(response.updatedAt()).isEqualTo(parentComment.getUpdatedAt()), + + () -> assertThat(response.postFindSiteUserResponse().id()) + .isEqualTo(parentComment.getSiteUser().getId()), + () -> assertThat(response.postFindSiteUserResponse().nickname()) + .isEqualTo(parentComment.getSiteUser().getNickname()), + () -> assertThat(response.postFindSiteUserResponse().profileImageUrl()) + .isEqualTo(parentComment.getSiteUser().getProfileImageUrl()) + )), + () -> assertThat(responses) + .filteredOn(response -> response.id().equals(childComment.getId())) + .singleElement() + .satisfies(response -> assertAll( + () -> assertThat(response.id()).isEqualTo(childComment.getId()), + () -> assertThat(response.parentId()).isEqualTo(parentComment.getId()), + () -> assertThat(response.content()).isEqualTo(childComment.getContent()), + () -> assertThat(response.isOwner()).isFalse(), + () -> assertThat(response.createdAt()).isEqualTo(childComment.getCreatedAt()), + () -> assertThat(response.updatedAt()).isEqualTo(childComment.getUpdatedAt()), + + () -> assertThat(response.postFindSiteUserResponse().id()) + .isEqualTo(childComment.getSiteUser().getId()), + () -> assertThat(response.postFindSiteUserResponse().nickname()) + .isEqualTo(childComment.getSiteUser().getNickname()), + () -> assertThat(response.postFindSiteUserResponse().profileImageUrl()) + .isEqualTo(childComment.getSiteUser().getProfileImageUrl()) + )) + ); + } + } + + @Nested + class 댓글_생성_테스트 { + + @Test + void 댓글을_성공적으로_생성한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + CommentCreateRequest request = new CommentCreateRequest("테스트 댓글", null); + + // when + CommentCreateResponse response = commentService.createComment( + 테스트유저_1.getEmail(), + testPost.getId(), + request + ); + + // then + Comment savedComment = commentRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(savedComment.getId()).isEqualTo(response.id()), + () -> assertThat(savedComment.getContent()).isEqualTo(request.content()), + () -> assertThat(savedComment.getParentComment()).isNull(), + () -> assertThat(savedComment.getPost().getId()).isEqualTo(testPost.getId()), + () -> assertThat(savedComment.getSiteUser().getId()).isEqualTo(테스트유저_1.getId()) + ); + } + + @Test + void 대댓글을_성공적으로_생성한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + CommentCreateRequest request = new CommentCreateRequest("테스트 대댓글", parentComment.getId()); + + // when + CommentCreateResponse response = commentService.createComment( + 테스트유저_2.getEmail(), + testPost.getId(), + request + ); + + // then + Comment savedComment = commentRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(savedComment.getId()).isEqualTo(response.id()), + () -> assertThat(savedComment.getContent()).isEqualTo(request.content()), + () -> assertThat(savedComment.getParentComment().getId()).isEqualTo(parentComment.getId()), + () -> assertThat(savedComment.getPost().getId()).isEqualTo(testPost.getId()), + () -> assertThat(savedComment.getSiteUser().getId()).isEqualTo(테스트유저_2.getId()) + ); + } + + @Test + void 대대댓글_생성_시도하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); + CommentCreateRequest request = new CommentCreateRequest("테스트 대대댓글", childComment.getId()); + + // when & then + assertThatThrownBy(() -> + commentService.createComment( + 테스트유저_1.getEmail(), + testPost.getId(), + request + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_COMMENT_LEVEL.getMessage()); + } + + @Test + void 존재하지_않는_부모댓글로_대댓글_작성시_예외를_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + long invalidCommentId = 9999L; + CommentCreateRequest request = new CommentCreateRequest("테스트 대댓글", invalidCommentId); + + // when & then + assertThatThrownBy(() -> + commentService.createComment( + 테스트유저_1.getEmail(), + testPost.getId(), + request + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_COMMENT_ID.getMessage()); + } + } + + @Nested + class 댓글_수정_테스트 { + + @Test + void 댓글을_성공적으로_수정한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, "원본 댓글"); + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글"); + + // when + CommentUpdateResponse response = commentService.updateComment( + 테스트유저_1.getEmail(), + testPost.getId(), + comment.getId(), + request + ); + + // then + Comment updatedComment = commentRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(updatedComment.getId()).isEqualTo(comment.getId()), + () -> assertThat(updatedComment.getContent()).isEqualTo(request.content()), + () -> assertThat(updatedComment.getParentComment()).isNull(), + () -> assertThat(updatedComment.getPost().getId()).isEqualTo(testPost.getId()), + () -> assertThat(updatedComment.getSiteUser().getId()).isEqualTo(테스트유저_1.getId()) + ); + } + + @Test + void 다른_사용자의_댓글을_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, "원본 댓글"); + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글"); + + // when & then + assertThatThrownBy(() -> + commentService.updateComment( + 테스트유저_2.getEmail(), + testPost.getId(), + comment.getId(), + request + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_ACCESS.getMessage()); + } + + @Test + void 삭제된_댓글을_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, null); + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글"); + + // when & then + assertThatThrownBy(() -> + commentService.updateComment( + 테스트유저_1.getEmail(), + testPost.getId(), + comment.getId(), + request + )) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getMessage()); + } + } + + @Nested + class 댓글_삭제_테스트 { + + @Test + @Transactional + void 대댓글이_없는_댓글을_삭제한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, "테스트 댓글"); + List comments = testPost.getCommentList(); + int expectedCommentsCount = comments.size() - 1; + + // when + CommentDeleteResponse response = commentService.deleteCommentById( + 테스트유저_1.getEmail(), + testPost.getId(), + comment.getId() + ); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(comment.getId()), + () -> assertThat(commentRepository.findById(comment.getId())).isEmpty(), + () -> assertThat(testPost.getCommentList()).hasSize(expectedCommentsCount) + ); + } + + @Test + @Transactional + void 대댓글이_있는_댓글을_삭제하면_내용만_삭제된다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); + List comments = testPost.getCommentList(); + List childComments = parentComment.getCommentList(); + + // when + CommentDeleteResponse response = commentService.deleteCommentById( + 테스트유저_1.getEmail(), + testPost.getId(), + parentComment.getId() + ); + + // then + Comment deletedComment = commentRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(deletedComment.getContent()).isNull(), + () -> assertThat(deletedComment.getCommentList()) + .extracting(Comment::getId) + .containsExactlyInAnyOrder(childComment.getId()), + () -> assertThat(testPost.getCommentList()).hasSize(comments.size()), + () -> assertThat(deletedComment.getCommentList()).hasSize(childComments.size()) + ); + } + + @Test + @Transactional + void 대댓글을_삭제하면_부모댓글이_삭제되지_않는다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment1 = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글 1"); + Comment childComment2 = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글 2"); + List childComments = parentComment.getCommentList(); + int expectedChildCommentsCount = childComments.size() - 1; + + // when + CommentDeleteResponse response = commentService.deleteCommentById( + 테스트유저_2.getEmail(), + testPost.getId(), + childComment1.getId() + ); + + // then + Comment remainingParentComment = commentRepository.findById(parentComment.getId()).orElseThrow(); + List remainingChildComments = remainingParentComment.getCommentList(); + assertAll( + () -> assertThat(commentRepository.findById(response.id())).isEmpty(), + () -> assertThat(remainingParentComment.getContent()).isEqualTo(parentComment.getContent()), + () -> assertThat(remainingChildComments).hasSize(expectedChildCommentsCount), + () -> assertThat(remainingChildComments) + .extracting(Comment::getId) + .containsExactly(childComment2.getId()) + ); + } + + @Test + @Transactional + void 대댓글을_삭제하고_부모댓글이_삭제된_상태면_부모댓글도_삭제된다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); + List comments = testPost.getCommentList(); + int expectedCommentsCount = comments.size() - 2; + parentComment.deprecateComment(); + + // when + CommentDeleteResponse response = commentService.deleteCommentById( + 테스트유저_2.getEmail(), + testPost.getId(), + childComment.getId() + ); + + // then + assertAll( + () -> assertThat(commentRepository.findById(response.id())).isEmpty(), + () -> assertThat(commentRepository.findById(parentComment.getId())).isEmpty(), + () -> assertThat(testPost.getCommentList()).hasSize(expectedCommentsCount) + ); + } + + @Test + void 다른_사용자의_댓글을_삭제하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, "테스트 댓글"); + + // when & then + assertThatThrownBy(() -> + commentService.deleteCommentById( + 테스트유저_2.getEmail(), + testPost.getId(), + comment.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_ACCESS.getMessage()); + } + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "테스트 제목", + "테스트 내용", + false, + 0L, + 0L, + PostCategory.자유 + ); + post.setBoardAndSiteUser(board, siteUser); + return postRepository.save(post); + } + + private Comment createComment(Post post, SiteUser siteUser, String content) { + Comment comment = new Comment(content); + comment.setPostAndSiteUser(post, siteUser); + return commentRepository.save(comment); + } + + private Comment createChildComment(Post post, SiteUser siteUser, Comment parentComment, String content) { + Comment comment = new Comment(content); + comment.setPostAndSiteUser(post, siteUser); + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); + return commentRepository.save(comment); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java deleted file mode 100644 index 17e74d140..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PostCategory; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -@TestContainerDataJpaTest -@DisplayName("게시판 레포지토리 테스트") -class BoardRepositoryTest { - @Autowired - private PostRepository postRepository; - @Autowired - private BoardRepository boardRepository; - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private EntityManager entityManager; - - private Board board; - private SiteUser siteUser; - private Post post; - - @BeforeEach - public void setUp() { - board = createBoard(); - boardRepository.save(board); - - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - - post = createPost(board, siteUser); - post = postRepository.save(post); - - entityManager.flush(); - entityManager.clear(); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - @Test - @Transactional - public void 게시판을_조회할_때_게시글은_즉시_로딩한다() { - // when - Board foundBoard = boardRepository.getByCodeUsingEntityGraph(board.getCode()); - foundBoard.getPostList().size(); // 추가쿼리 발생하지 않는다. - - // then - assertThat(foundBoard.getCode()).isEqualTo(board.getCode()); - } - - @Test - @Transactional - public void 게시판을_조회할_때_게시글은_즉시_로딩한다_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // given - String invalidCode = "INVALID_CODE"; - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - boardRepository.getByCodeUsingEntityGraph(invalidCode); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - @Transactional - public void 게시판을_조회한다() { - // when - Board foundBoard = boardRepository.getByCode(board.getCode()); - - // then - assertEquals(board.getCode(), foundBoard.getCode()); - } - - @Test - @Transactional - public void 게시판을_조회할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // given - String invalidCode = "INVALID_CODE"; - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - boardRepository.getByCode(invalidCode); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java deleted file mode 100644 index b57288725..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PostCategory; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@TestContainerDataJpaTest -@DisplayName("댓글 레포지토리 테스트") -class CommentRepositoryTest { - @Autowired - private PostRepository postRepository; - @Autowired - private BoardRepository boardRepository; - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private CommentRepository commentRepository; - - private Board board; - private SiteUser siteUser; - private Post post; - private Comment parentComment; - private Comment childComment; - - @BeforeEach - public void setUp() { - board = createBoard(); - boardRepository.save(board); - - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - - post = createPost(board, siteUser); - post = postRepository.save(post); - - parentComment = createParentComment(); - childComment = createChildComment(); - commentRepository.save(parentComment); - commentRepository.save(childComment); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - private Comment createParentComment() { - Comment comment = new Comment( - "parent" - ); - comment.setPostAndSiteUser(post, siteUser); - return comment; - } - - private Comment createChildComment() { - Comment comment = new Comment( - "child" - ); - comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); - return comment; - } - - @Test - @Transactional - public void 재귀쿼리로_댓글트리를_조회한다() { - // when - List commentTreeByPostId = commentRepository.findCommentTreeByPostId(post.getId()); - - // then - List expectedResponse = List.of(parentComment, childComment); - assertEquals(commentTreeByPostId, expectedResponse); - } - - @Test - @Transactional - public void 댓글을_조회한다() { - // when - Comment foundComment = commentRepository.getById(parentComment.getId()); - - // then - assertEquals(parentComment, foundComment); - } - - @Test - @Transactional - public void 댓글을_조회할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { - // given - Long invalidId = -1L; - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - commentRepository.getById(invalidId); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_ID.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java deleted file mode 100644 index 3ec59a5c2..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.score.domain.GpaScore; -import com.example.solidconnection.score.repository.GpaScoreRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -@TestContainerDataJpaTest -@DisplayName("학점 레포지토리 테스트") -@Transactional -public class GpaScoreRepositoryTest { - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private GpaScoreRepository gpaScoreRepository; - - private SiteUser siteUser; - - @BeforeEach - public void setUp() { - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - @Test - public void 사용자의_학점을_조회한다_기존이력_없을_때() { - Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUser(siteUser); - assertThat(gpaScoreBySiteUser).isEqualTo(Optional.empty()); - } - - @Test - public void 사용자의_학점을_조회한다_기존이력_있을_때() { - GpaScore gpaScore = new GpaScore( - new Gpa(4.5, 4.5, "http://example.com/gpa-report.pdf"), - siteUser, - LocalDate.of(2024, 10, 10) - ); - gpaScore.setSiteUser(siteUser); - gpaScoreRepository.save(gpaScore); - - Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUser(siteUser); - assertThat(gpaScoreBySiteUser).isEqualTo(Optional.of(gpaScore)); - } - - @Test - public void 아이디와_사용자정보로_사용자의_학점을_조회한다_기존이력_없을_때() { - Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, 1L); - assertThat(gpaScoreBySiteUser).isEqualTo(Optional.empty()); - } - - @Test - public void 아이디와_사용자정보로_사용자의_학점을_조회한다_기존이력_있을_때() { - GpaScore gpaScore = new GpaScore( - new Gpa(4.5, 4.5, "http://example.com/gpa-report.pdf"), - siteUser, - LocalDate.of(2024, 10, 10) - ); - gpaScore.setSiteUser(siteUser); - gpaScoreRepository.save(gpaScore); - - Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScore.getId()); - assertThat(gpaScoreBySiteUser).isEqualTo(Optional.of(gpaScore)); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java deleted file mode 100644 index 0090088c1..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.score.domain.LanguageTestScore; -import com.example.solidconnection.score.repository.LanguageTestScoreRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -@TestContainerDataJpaTest -@DisplayName("어학성적 레포지토리 테스트") -@Transactional -public class LanguageTestScoreRepositoryTest { - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private LanguageTestScoreRepository languageTestScoreRepository; - - private SiteUser siteUser; - - @BeforeEach - public void setUp() { - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - @Test - public void 사용자의_어학성적을_조회한다_기존이력_없을_때() { - Optional languageTestScore = languageTestScoreRepository - .findLanguageTestScoreBySiteUserAndLanguageTest_LanguageTestType(siteUser, LanguageTestType.TOEIC); - assertThat(languageTestScore).isEqualTo(Optional.empty()); - } - - @Test - public void 사용자의_어학성적을_조회한다_기존이력_있을_때() { - LanguageTestScore languageTestScore = new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEIC, "990", "http://example.com/gpa-report.pdf"), - LocalDate.of(2024, 10, 10), - siteUser - ); - languageTestScore.setSiteUser(siteUser); - languageTestScoreRepository.save(languageTestScore); - - Optional languageTestScore1 = languageTestScoreRepository - .findLanguageTestScoreBySiteUserAndLanguageTest_LanguageTestType(siteUser, LanguageTestType.TOEIC); - assertThat(languageTestScore1).isEqualTo(Optional.of(languageTestScore)); - } - - @Test - public void 아이디와_사용자정보로_사용자의_어학성적을_조회한다_기존이력_없을_때() { - Optional languageTestScore = languageTestScoreRepository - .findLanguageTestScoreBySiteUserAndId(siteUser, 1L); - assertThat(languageTestScore).isEqualTo(Optional.empty()); - } - - @Test - public void 아이디와_사용자정보로_사용자의_어학성적을_조회한다_기존이력_있을_때() { - LanguageTestScore languageTestScore = new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEIC, "990", "http://example.com/gpa-report.pdf"), - LocalDate.of(2024, 10, 10), - siteUser - ); - languageTestScore.setSiteUser(siteUser); - languageTestScoreRepository.save(languageTestScore); - - Optional languageTestScore1 = languageTestScoreRepository - .findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScore.getId()); - assertThat(languageTestScore1).isEqualTo(Optional.of(languageTestScore)); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java deleted file mode 100644 index 43ac210cb..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PostCategory; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@TestContainerDataJpaTest -@DisplayName("게시글 좋아요 레포지토리 테스트") -class PostLikeRepositoryTest { - @Autowired - private PostRepository postRepository; - @Autowired - private BoardRepository boardRepository; - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private PostLikeRepository postLikeRepository; - - private Post post; - private Board board; - private SiteUser siteUser; - private PostLike postLike; - - - @BeforeEach - void setUp() { - board = createBoard(); - boardRepository.save(board); - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - post = createPost(board, siteUser); - post = postRepository.save(post); - postLike = createPostLike(post, siteUser); - postLikeRepository.save(postLike); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - private PostLike createPostLike(Post post, SiteUser siteUser) { - PostLike postLike = new PostLike(); - postLike.setPostAndSiteUser(post, siteUser); - return postLike; - } - - @Test - @Transactional - void 게시글_좋아요를_조회한다() { - // when - PostLike foundPostLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); - - // then - assertEquals(foundPostLike, postLike); - } - - @Test - @Transactional - void 게시글_좋아요를_조회할_때_유효한_좋아요가_아니라면_예외_응답을_반환한다() { - // given - postLike.resetPostAndSiteUser(); - postLikeRepository.delete(postLike); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - postLikeRepository.getByPostAndSiteUser(post, siteUser); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_LIKE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_LIKE.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java deleted file mode 100644 index a37a0e6bf..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PostCategory; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.ArrayList; -import java.util.List; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@TestContainerDataJpaTest -@DisplayName("게시글 레포지토리 테스트") -class PostRepositoryTest { - - @Autowired - private PostRepository postRepository; - - @Autowired - private BoardRepository boardRepository; - - @Autowired - private SiteUserRepository siteUserRepository; - - private Post post; - private Board board; - private SiteUser siteUser; - - @BeforeEach - void setUp() { - board = createBoard(); - boardRepository.save(board); - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - post = createPostWithImages(board, siteUser); - post = postRepository.save(post); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private Post createPostWithImages(Board board, SiteUser siteUser) { - Post postWithImages = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - postWithImages.setBoardAndSiteUser(board, siteUser); - - List postImageList = new ArrayList<>(); - postImageList.add(new PostImage("https://s3.example.com/test1.png")); - postImageList.add(new PostImage("https://s3.example.com/test2.png")); - for (PostImage postImage : postImageList) { - postImage.setPost(postWithImages); - } - return postWithImages; - } - - @Test - void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { - Post foundPost = postRepository.getByIdUsingEntityGraph(post.getId()); - foundPost.getPostImageList().size(); // 추가쿼리 발생하지 않는다. - - assertThat(foundPost).isEqualTo(post); - } - - @Test - void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // given - Long invalidId = -1L; - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - postRepository.getByIdUsingEntityGraph(invalidId); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글을_조회한다() { - Post foundPost = postRepository.getById(post.getId()); - - assertEquals(post, foundPost); - } - - @Test - void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - Long invalidId = -1L; - - CustomException exception = assertThrows(CustomException.class, () -> { - postRepository.getById(invalidId); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글_좋아요를_등록한다() { - // given - Long likeCount = post.getLikeCount(); - - // when - postRepository.increaseLikeCount(post.getId()); - - // then - Post response = postRepository.getById(post.getId()); - assertEquals(response.getLikeCount(), likeCount + 1); - } - - @Test - void 게시글_좋아요를_삭제한다() { - // given - Long likeCount = post.getLikeCount(); - postRepository.increaseLikeCount(post.getId()); - - // when - postRepository.decreaseLikeCount(post.getId()); - - // then - Post response = postRepository.getById(post.getId()); - assertEquals(response.getLikeCount(), likeCount); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java deleted file mode 100644 index dd87a383f..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java +++ /dev/null @@ -1,259 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.application.domain.Application; -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.application.dto.ApplyRequest; -import com.example.solidconnection.application.dto.UniversityChoiceRequest; -import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.application.service.ApplicationSubmissionService; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.score.domain.GpaScore; -import com.example.solidconnection.score.domain.LanguageTestScore; -import com.example.solidconnection.score.repository.GpaScoreRepository; -import com.example.solidconnection.score.repository.LanguageTestScoreRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.*; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Value; - -import java.time.LocalDate; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("지원 서비스 테스트") -public class ApplicationServiceTest { - @InjectMocks - ApplicationSubmissionService applicationSubmissionService; - @Mock - ApplicationRepository applicationRepository; - @Mock - UniversityInfoForApplyRepository universityInfoForApplyRepository; - @Mock - SiteUserRepository siteUserRepository; - @Mock - GpaScoreRepository gpaScoreRepository; - @Mock - LanguageTestScoreRepository languageTestScoreRepository; - - @Value("${university.term}") - private String term; - private SiteUser siteUser; - private GpaScore gpaScore; - private LanguageTestScore languageTestScore; - private final long gpaScoreId = 1L; - private final long languageTestScoreId = 1L; - private final long firstChoiceUniversityId = 1L; - private final long secondChoiceUniversityId = 2L; - private final long thirdChoiceUniversityId = 3L; - - @BeforeEach - void setUp() { - siteUser = new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - gpaScore = new GpaScore( - new Gpa(4.3, 4.5, "gpaScoreUrl"), - siteUser, - LocalDate.of(2024, 10, 30) - ); - languageTestScore = new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEIC, "990", "languageTestScoreUrl"), - LocalDate.of(2024, 10, 30), - siteUser - ); - } - - @Test - void 지원한다_기존_이력_없음() { - // Given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.APPROVED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); - when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.of(languageTestScore)); - when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.empty()); - - // When - boolean result = applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - - // Then - assertThat(result).isEqualTo(true); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(gpaScoreRepository, times(1)).findGpaScoreBySiteUserAndId(siteUser, gpaScoreId); - verify(languageTestScoreRepository, times(1)).findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId); - verify(applicationRepository, times(1)).save(any(Application.class)); - } - - @Test - void 지원한다_기존_이력_있음() { - // Given - Application beforeApplication = new Application( - siteUser, - new Gpa(4.5, 4.5, "beforeGpaScoreUrl"), - new LanguageTest(LanguageTestType.TOEIC, "900", "beforeLanguageTestUrl"), - term - ); - beforeApplication.setVerifyStatus(VerifyStatus.APPROVED); - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.APPROVED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, 1L)).thenReturn(Optional.of(gpaScore)); - languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); - when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, 1L)).thenReturn(Optional.of(languageTestScore)); - when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.of(beforeApplication)); - - // When - boolean result = applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - - // Then - assertThat(result).isEqualTo(true); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(gpaScoreRepository, times(1)).findGpaScoreBySiteUserAndId(siteUser, gpaScoreId); - verify(languageTestScoreRepository, times(1)).findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId); - verify(applicationRepository, times(1)).findBySiteUserAndTerm(siteUser, term); - verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(firstChoiceUniversityId, term); - verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(secondChoiceUniversityId, term); - verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(thirdChoiceUniversityId, term); - verify(applicationRepository, times(1)).save(any(Application.class)); - } - - @Test - void 지원할_때_존재하지_않는_학점이라면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.empty()); - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_GPA_SCORE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_GPA_SCORE.getCode()); - } - - @Test - void 지원할_때_승인되지_않은_학점이라면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.REJECTED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_GPA_SCORE_STATUS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_GPA_SCORE_STATUS.getCode()); - } - - @Test - void 지원할_때_존재하지_않는_어학성적이라면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.APPROVED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.empty()); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE.getCode()); - } - - @Test - void 지원할_때_승인되지_않은_어학성적이라면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.APPROVED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - languageTestScore.setVerifyStatus(VerifyStatus.REJECTED); - when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.of(languageTestScore)); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS.getCode()); - } - - @Test - void 지원할_때_학교_선택이_중복되면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, firstChoiceUniversityId, firstChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java deleted file mode 100644 index 18c37b807..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.board.service.BoardService; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.BoardFindPostResponse; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.type.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - - -@ExtendWith(MockitoExtension.class) -@DisplayName("게시판 서비스 테스트") -class BoardServiceTest { - @InjectMocks - BoardService boardService; - @Mock - BoardRepository boardRepository; - - private SiteUser siteUser; - private Board board; - private List postList = new ArrayList<>(); - private List freePostList = new ArrayList<>(); - private List questionPostList = new ArrayList<>(); - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - board = createBoard("FREE", "자유게시판"); - - Post post_question_1 = createPost("질문", board, siteUser); - Post post_free_1 = createPost("자유", board, siteUser); - Post post_free_2 = createPost("자유", board, siteUser); - - postList.add(post_question_1); - postList.add(post_free_1); - postList.add(post_free_2); - questionPostList.add(post_question_1); - freePostList.add(post_free_1); - freePostList.add(post_free_2); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard(String code, String koreanName) { - return new Board(code, koreanName); - } - - private Post createPost(String postCategory, Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf(postCategory) - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - @Test - void 게시판을_조회할_때_게시판_코드와_게시글_카테고리에_따라서_조회한다() { - // Given - String category = "자유"; - when(boardRepository.getByCodeUsingEntityGraph(board.getCode())).thenReturn(board); - - // When - List responses = boardService.findPostsByCodeAndPostCategory(board.getCode(), category); - - // Then - List expectedResponses = freePostList.stream() - .map(BoardFindPostResponse::from) - .toList(); - assertIterableEquals(expectedResponses, responses); - verify(boardRepository, times(1)).getByCodeUsingEntityGraph(board.getCode()); - } - - @Test - void 게시판을_조회할_때_카테고리가_전체라면_해당_게시판의_모든_게시글을_조회한다() { - // Given - String category = "전체"; - when(boardRepository.getByCodeUsingEntityGraph(board.getCode())).thenReturn(board); - - // When - List responses = boardService.findPostsByCodeAndPostCategory(board.getCode(), category); - - // Then - List expectedResponses = postList.stream() - .map(BoardFindPostResponse::from) - .toList(); - assertIterableEquals(expectedResponses, responses); - verify(boardRepository, times(1)).getByCodeUsingEntityGraph(board.getCode()); - } - - @Test - void 게시판을_조회할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidCode = "INVALID_CODE"; - String category = "자유"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> { - boardService.findPostsByCodeAndPostCategory(invalidCode, category); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시판을_조회할_때_유효한_카테고리가_아니라면_예외_응답을_반환한다() { - // Given - String invalidCategory = "INVALID_CATEGORY"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> { - boardService.findPostsByCodeAndPostCategory(board.getCode(), invalidCategory); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_POST_CATEGORY.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_POST_CATEGORY.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java deleted file mode 100644 index 9ced8bcd8..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java +++ /dev/null @@ -1,483 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.dto.*; -import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.comment.service.CommentService; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; -import java.util.stream.Collectors; - -import static com.example.solidconnection.custom.exception.ErrorCode.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("댓글 서비스 테스트") -class CommentServiceTest { - @InjectMocks - CommentService commentService; - @Mock - PostRepository postRepository; - @Mock - SiteUserRepository siteUserRepository; - @Mock - CommentRepository commentRepository; - - private SiteUser siteUser; - private Board board; - private Post post; - private Comment parentComment; - private Comment parentCommentWithNullContent; - private Comment childComment; - private Comment childCommentOfNullContentParent; - - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - board = createBoard(); - post = createPost(board, siteUser); - parentComment = createParentComment("parent"); - parentCommentWithNullContent = createParentComment(null); - childComment = createChildComment(parentComment); - childCommentOfNullContentParent = createChildComment(parentCommentWithNullContent); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - private Comment createParentComment(String content) { - Comment comment = new Comment( - content - ); - comment.setPostAndSiteUser(post, siteUser); - return comment; - } - - private Comment createChildComment(Comment parentComment) { - Comment comment = new Comment( - "child" - ); - comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); - return comment; - } - - /** - * 댓글 조회 - */ - - @Test - void 특정_게시글의_댓글들을_조회한다() { - // Given - List commentList = List.of(parentComment, childComment, parentCommentWithNullContent); - when(commentRepository.findCommentTreeByPostId(post.getId())).thenReturn(commentList); - - // When - List postFindCommentResponses = commentService.findCommentsByPostId( - siteUser.getEmail(), post.getId()); - - // Then - List expectedResponse = commentList.stream() - .map(comment -> PostFindCommentResponse.from(isOwner(comment, siteUser.getEmail()), comment)) - .collect(Collectors.toList()); - assertEquals(postFindCommentResponses, expectedResponse); - } - - private Boolean isOwner(Comment comment, String email) { - return comment.getSiteUser().getEmail().equals(email); - } - - /** - * 댓글 등록 - */ - @Test - void 부모_댓글을_등록한다() { - // Given - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "parent", null - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.save(any(Comment.class))).thenReturn(parentComment); - - // When - CommentCreateResponse commentCreateResponse = commentService.createComment( - siteUser.getEmail(), post.getId(), commentCreateRequest); - - // Then - assertEquals(commentCreateResponse, CommentCreateResponse.from(parentComment)); - verify(commentRepository, times(0)) - .getById(any(Long.class)); - verify(commentRepository, times(1)) - .save(commentCreateRequest.toEntity(siteUser, post, parentComment)); - } - - @Test - void 자식_댓글을_등록한다() { - // Given - Long parentCommentId = 1L; - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "child", parentCommentId - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(parentCommentId)).thenReturn(parentComment); - when(commentRepository.save(any(Comment.class))).thenReturn(childComment); - - // When - CommentCreateResponse commentCreateResponse = commentService.createComment( - siteUser.getEmail(), post.getId(), commentCreateRequest); - - // Then - assertEquals(commentCreateResponse, CommentCreateResponse.from(childComment)); - verify(commentRepository, times(1)) - .getById(parentCommentId); - verify(commentRepository, times(1)) - .save(commentCreateRequest.toEntity(siteUser, post, parentComment)); - } - - - @Test - void 댓글을_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "child", null - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.createComment(siteUser.getEmail(), invalidPostId, commentCreateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - verify(commentRepository, times(0)) - .save(any(Comment.class)); - } - - @Test - void 댓글을_등록할_때_유효한_부모_댓글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidParentCommentId = -1L; - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "child", invalidParentCommentId - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(invalidParentCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.createComment(siteUser.getEmail(), post.getId(), commentCreateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_ID.getCode()); - verify(commentRepository, times(0)) - .save(any(Comment.class)); - } - - @Test - void 댓글을_등록할_때_대대댓글_부터는_예외_응답을_반환한다() { - // Given - Long childCommentId = 1L; - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "child's child", childCommentId - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(childCommentId)).thenReturn(childComment); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.createComment(siteUser.getEmail(), post.getId(), commentCreateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_LEVEL.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_LEVEL.getCode()); - verify(commentRepository, times(0)) - .save(any(Comment.class)); - } - - /** - * 댓글 수정 - */ - @Test - void 댓글을_수정한다() { - // Given - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When - CommentUpdateResponse commentUpdateResponse = commentService.updateComment( - siteUser.getEmail(), post.getId(), parentComment.getId(), commentUpdateRequest); - - // Then - assertEquals(commentUpdateResponse.id(), parentComment.getId()); - } - - @Test - void 댓글을_수정할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(siteUser.getEmail(), invalidPostId, parentComment.getId(), commentUpdateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 댓글을_수정할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidCommentId = -1L; - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(siteUser.getEmail(), post.getId(), invalidCommentId, commentUpdateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_ID.getCode()); - } - - @Test - void 댓글을_수정할_때_이미_삭제된_댓글이라면_예외_응답을_반환한다() { - // Given - parentComment.deprecateComment(); - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(siteUser.getEmail(), post.getId(), parentComment.getId(), commentUpdateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getCode()); - } - - @Test - void 댓글을_수정할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { - // Given - String invalidEmail = "invalidEmail@test.com"; - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(invalidEmail, post.getId(), parentComment.getId(), commentUpdateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ACCESS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ACCESS.getCode()); - } - - /** - * 댓글 삭제 - */ - - @Test - void 댓글을_삭제한다_자식댓글_있음() { - // Given - Long parentCommentId = 1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( - siteUser.getEmail(), post.getId(), parentCommentId); - - // Then - assertEquals(parentComment.getContent(), null); - assertEquals(commentDeleteResponse.id(), parentCommentId); - verify(commentRepository, times(0)).deleteById(parentCommentId); - } - - @Test - void 댓글을_삭제한다_자식댓글_없음() { - // Given - Long childCommentId = 1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(childComment); - - // When - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( - siteUser.getEmail(), post.getId(), childCommentId); - - // Then - assertEquals(commentDeleteResponse.id(), childCommentId); - verify(commentRepository, times(1)).deleteById(childCommentId); - } - - @Test - void 대댓글을_삭제한다_부모댓글_유효() { - // Given - Long childCommentId = 1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(childComment); - - // When - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( - siteUser.getEmail(), post.getId(), childCommentId); - - // Then - assertEquals(commentDeleteResponse.id(), childCommentId); - verify(commentRepository, times(1)).deleteById(childCommentId); - } - - @Test - void 대댓글을_삭제한다_부모댓글_유효하지_않음() { - // Given - - Long childCommentId = 1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(childCommentOfNullContentParent); - - // When - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( - siteUser.getEmail(), post.getId(), childCommentId); - - // Then - assertEquals(commentDeleteResponse.id(), childCommentId); - verify(commentRepository, times(2)).deleteById(any()); - } - - @Test - void 댓글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.deleteCommentById(siteUser.getEmail(), invalidPostId, parentComment.getId()) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 댓글을_삭제할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidCommentId = -1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.deleteCommentById(siteUser.getEmail(), post.getId(), invalidCommentId) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_ID.getCode()); - } - - @Test - void 댓글을_삭제할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { - // Given - String invalidEmail = "invalidEmail@test.com"; - when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.deleteCommentById(invalidEmail, post.getId(), parentComment.getId()) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ACCESS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ACCESS.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java deleted file mode 100644 index afc899255..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ /dev/null @@ -1,715 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.dto.PostFindBoardResponse; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.service.CommentService; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.post.dto.PostFindPostImageResponse; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.PostLike; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.*; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostCommandService; -import com.example.solidconnection.post.service.PostLikeService; -import com.example.solidconnection.post.service.PostQueryService; -import com.example.solidconnection.s3.S3Service; -import com.example.solidconnection.s3.UploadedFileUrlResponse; -import com.example.solidconnection.service.RedisService; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.*; -import com.example.solidconnection.util.RedisUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@DisplayName("게시글 서비스 테스트") -class PostServiceTest { - - @InjectMocks - PostQueryService postQueryService; - - @InjectMocks - PostCommandService postCommandService; - - @InjectMocks - PostLikeService postLikeService; - - @Mock - PostRepository postRepository; - @Mock - SiteUserRepository siteUserRepository; - @Mock - BoardRepository boardRepository; - @Mock - PostLikeRepository postLikeRepository; - @Mock - S3Service s3Service; - @Mock - CommentService commentService; - @Mock - RedisService redisService; - @Mock - RedisUtils redisUtils; - - private SiteUser siteUser; - private Board board; - private Post post; - private Post postWithImages; - private Post questionPost; - private PostLike postLike; - private List imageFiles; - private List imageFilesWithMoreThanFiveFiles; - private List uploadedFileUrlResponseList; - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - board = createBoard(); - imageFiles = createMockImageFiles(); - imageFilesWithMoreThanFiveFiles = createMockImageFilesWithMoreThanFiveFiles(); - uploadedFileUrlResponseList = createUploadedFileUrlResponses(); - post = createPost(board, siteUser); - postWithImages = createPostWithImages(board, siteUser); - questionPost = createQuestionPost(board, siteUser); - postLike = createPostLike(post, siteUser); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - - return post; - } - - private Post createPostWithImages(Board board, SiteUser siteUser) { - Post postWithImages = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - postWithImages.setBoardAndSiteUser(board, siteUser); - - List postImageList = new ArrayList<>(); - postImageList.add(new PostImage("https://s3.example.com/test1.png")); - postImageList.add(new PostImage("https://s3.example.com/test2.png")); - for (PostImage postImage : postImageList) { - postImage.setPost(postWithImages); - } - return postWithImages; - } - - private Post createQuestionPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - true, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - private PostLike createPostLike(Post post, SiteUser siteUser) { - PostLike postLike = new PostLike(); - postLike.setPostAndSiteUser(post, siteUser); - return postLike; - } - - private List createMockImageFiles() { - List multipartFileList = new ArrayList<>(); - multipartFileList.add(new MockMultipartFile("file1", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file2", "test1.png", - "image/png", "test image content 1".getBytes())); - return multipartFileList; - } - - private List createUploadedFileUrlResponses() { - return Arrays.asList( - new UploadedFileUrlResponse("https://s3.example.com/test1.png"), - new UploadedFileUrlResponse("https://s3.example.com/test2.png") - ); - } - - private List createMockImageFilesWithMoreThanFiveFiles() { - List multipartFileList = new ArrayList<>(); - multipartFileList.add(new MockMultipartFile("file1", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file2", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file3", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file4", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file5", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file6", "test1.png", - "image/png", "test image content 1".getBytes())); - return multipartFileList; - } - - /** - * 게시글 등록 - */ - @Test - void 게시글을_등록한다_이미지_있음() { - // Given - PostCreateRequest postCreateRequest = new PostCreateRequest( - "자유", "title", "content", false); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(boardRepository.getByCode(board.getCode())).thenReturn(board); - when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); - when(postRepository.save(any(Post.class))).thenReturn(postWithImages); - - // When - PostCreateResponse postCreateResponse = postCommandService.createPost( - siteUser.getEmail(), board.getCode(), postCreateRequest, imageFiles); - - // Then - assertEquals(postCreateResponse, PostCreateResponse.from(postWithImages)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(boardRepository, times(1)).getByCode(board.getCode()); - verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); - verify(postRepository, times(1)).save(any(Post.class)); - } - - @Test - void 게시글을_등록한다_이미지_없음() { - // Given - PostCreateRequest postCreateRequest = new PostCreateRequest( - "자유", "title", "content", false); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(boardRepository.getByCode(board.getCode())).thenReturn(board); - when(postRepository.save(postCreateRequest.toEntity(siteUser, board))).thenReturn(post); - - // When - PostCreateResponse postCreateResponse = postCommandService.createPost( - siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList()); - - // Then - assertEquals(postCreateResponse, PostCreateResponse.from(post)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(boardRepository, times(1)).getByCode(board.getCode()); - verify(postRepository, times(1)).save(postCreateRequest.toEntity(siteUser, board)); - } - - @Test - void 게시글을_등록할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - PostCreateRequest postCreateRequest = new PostCreateRequest( - "자유", "title", "content", false); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postCommandService - .createPost(siteUser.getEmail(), invalidBoardCode, postCreateRequest, Collections.emptyList())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글을_등록할_때_유효한_카테고리가_아니라면_예외_응답을_반환한다() { - // Given - String invalidPostCategory = "invalidPostCategory"; - PostCreateRequest postCreateRequest = new PostCreateRequest( - invalidPostCategory, "title", "content", false); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postCommandService - .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_CATEGORY.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_CATEGORY.getCode()); - } - - @Test - void 게시글을_등록할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { - // Given - PostCreateRequest postCreateRequest = new PostCreateRequest( - "자유", "title", "content", false); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postCommandService - .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, imageFilesWithMoreThanFiveFiles)); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getCode()); - } - - /** - * 게시글 수정 - */ - @Test - void 게시글을_수정한다_기존_사진_없음_수정_사진_없음() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("질문", "updateTitle", "updateContent"); - when(postRepository.getById(post.getId())).thenReturn(post); - - // When - PostUpdateResponse response = postCommandService.updatePost( - siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, Collections.emptyList()); - - // Then - assertEquals(response, PostUpdateResponse.from(post)); - verify(postRepository, times(1)).getById(post.getId()); - verify(s3Service, times(0)).deletePostImage(any(String.class)); - verify(s3Service, times(0)).uploadFiles(anyList(), any(ImgType.class)); - } - - @Test - void 게시글을_수정한다_기존_사진_있음_수정_사진_없음() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); - when(postRepository.getById(postWithImages.getId())).thenReturn(postWithImages); - - // When - PostUpdateResponse response = postCommandService.updatePost( - siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, Collections.emptyList()); - - // Then - assertEquals(response, PostUpdateResponse.from(postWithImages)); - verify(postRepository, times(1)).getById(postWithImages.getId()); - verify(s3Service, times(imageFiles.size())).deletePostImage(any(String.class)); - verify(s3Service, times(0)).uploadFiles(anyList(), any(ImgType.class)); - } - - @Test - void 게시글을_수정한다_기존_사진_없음_수정_사진_있음() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); - when(postRepository.getById(post.getId())).thenReturn(post); - when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); - - // When - PostUpdateResponse response = postCommandService.updatePost( - siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFiles); - - // Then - assertEquals(response, PostUpdateResponse.from(post)); - verify(postRepository, times(1)).getById(post.getId()); - verify(s3Service, times(0)).deletePostImage(any(String.class)); - verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); - } - - @Test - void 게시글을_수정한다_기존_사진_있음_수정_사진_있음() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); - when(postRepository.getById(postWithImages.getId())).thenReturn(postWithImages); - when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); - - // When - PostUpdateResponse response = postCommandService.updatePost( - siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, imageFiles); - - // Then - assertEquals(response, PostUpdateResponse.from(postWithImages)); - verify(postRepository, times(1)).getById(postWithImages.getId()); - verify(s3Service, times(imageFiles.size())).deletePostImage(any(String.class)); - verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); - } - - @Test - void 게시글을_수정할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(siteUser.getEmail(), invalidBoardCode, post.getId(), postUpdateRequest, imageFiles)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글을_수정할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(siteUser.getEmail(), board.getCode(), invalidPostId, postUpdateRequest, imageFiles)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글을_수정할_때_본인의_게시글이_아니라면_예외_응답을_반환한다() { - // Given - String invalidEmail = "invalidEmail@example.com"; - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - when(postRepository.getById(post.getId())).thenReturn(post); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(invalidEmail, board.getCode(), post.getId(), postUpdateRequest, imageFiles)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ACCESS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ACCESS.getCode()); - } - - @Test - void 게시글을_수정할_때_질문글_이라면_예외_응답을_반환한다() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - when(postRepository.getById(questionPost.getId())).thenReturn(questionPost); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(siteUser.getEmail(), board.getCode(), questionPost.getId(), postUpdateRequest, imageFiles)); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); - } - - @Test - void 게시글을_수정할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - when(postRepository.getById(post.getId())).thenReturn(post); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFilesWithMoreThanFiveFiles)); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getCode()); - } - - /** - * 게시글 조회 - */ - @Test - void 게시글을_찾는다() { - // Given - List commentFindResultDTOList = new ArrayList<>(); - when(postRepository.getByIdUsingEntityGraph(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser)).thenReturn(Optional.empty()); - when(commentService.findCommentsByPostId(siteUser.getEmail(), post.getId())).thenReturn(commentFindResultDTOList); - - // When - PostFindResponse response = postQueryService.findPostById(siteUser.getEmail(), board.getCode(), post.getId()); - - // Then - PostFindResponse expectedResponse = PostFindResponse.from( - post, - true, - false, - PostFindBoardResponse.from(post.getBoard()), - PostFindSiteUserResponse.from(post.getSiteUser()), - commentFindResultDTOList, - PostFindPostImageResponse.from(post.getPostImageList()) - ); - assertEquals(expectedResponse, response); - verify(postRepository, times(1)).getByIdUsingEntityGraph(post.getId()); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(postLikeRepository, times(1)).findPostLikeByPostAndSiteUser(post, siteUser); - verify(commentService, times(1)).findCommentsByPostId(siteUser.getEmail(), post.getId()); - } - - @Test - void 게시글을_찾을_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postQueryService.findPostById(siteUser.getEmail(), invalidBoardCode, post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글을_찾을_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(postRepository.getByIdUsingEntityGraph(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postQueryService.findPostById(siteUser.getEmail(), board.getCode(), invalidPostId)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - /** - * 게시글 삭제 - */ - @Test - void 게시글을_삭제한다() { - // Give - when(postRepository.getById(post.getId())).thenReturn(post); - - // When - PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), post.getId()); - - // Then - assertEquals(postDeleteResponse.id(), post.getId()); - verify(postRepository, times(1)).getById(post.getId()); - verify(redisService, times(1)).deleteKey(redisUtils.getPostViewCountRedisKey(post.getId())); - verify(postRepository, times(1)).deleteById(post.getId()); - } - - @Test - void 게시글을_삭제할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.deletePostById(siteUser.getEmail(), invalidBoardCode, post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), invalidPostId)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글을_삭제할_때_자신의_게시글이_아니라면_예외_응답을_반환한다() { - // Given - String invalidEmail = "invalidEmail@example.com"; - when(postRepository.getById(post.getId())).thenReturn(post); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.deletePostById(invalidEmail, board.getCode(), post.getId()) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ACCESS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ACCESS.getCode()); - } - - @Test - void 게시글을_삭제할_때_질문글_이라면_예외_응답을_반환한다() { - when(postRepository.getById(questionPost.getId())).thenReturn(questionPost); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), questionPost.getId())); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); - } - - /** - * 게시글 좋아요 - */ - @Test - void 게시글_좋아요를_등록한다() { - // Given - when(postRepository.getById(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // When - PostLikeResponse postLikeResponse = postLikeService.likePost(siteUser.getEmail(), board.getCode(), post.getId()); - - // Then - assertEquals(postLikeResponse, PostLikeResponse.from(post)); - verify(postLikeRepository, times(1)).save(any(PostLike.class)); - } - - @Test - void 게시글_좋아요를_등록할_때_중복된_좋아요라면_예외_응답을_반환한다() { - when(postRepository.getById(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser)).thenReturn(Optional.of(postLike)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.likePost(siteUser.getEmail(), board.getCode(), post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getCode()); - } - - @Test - void 게시글_좋아요를_등록할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.likePost(siteUser.getEmail(), invalidBoardCode, post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글_좋아요를_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.likePost(siteUser.getEmail(), board.getCode(), invalidPostId)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글_좋아요를_삭제한다() { - // Given - Long likeCount = post.getLikeCount(); - when(postRepository.getById(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenReturn(postLike); - - // When - PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId()); - - // Then - assertEquals(postDislikeResponse, PostDislikeResponse.from(post)); - verify(postLikeRepository, times(1)).deleteById(post.getId()); - } - - @Test - void 게시글_좋아요를_삭제할_때_존재하지_않는_좋아요라면_예외_응답을_반환한다() { - when(postRepository.getById(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenThrow(new CustomException(INVALID_POST_LIKE)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_LIKE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_LIKE.getCode()); - } - - @Test - void 게시글_좋아요를_삭제할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.dislikePost(siteUser.getEmail(), invalidBoardCode, post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글_좋아요를_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), invalidPostId)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java deleted file mode 100644 index 39deadb54..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.score.domain.GpaScore; -import com.example.solidconnection.score.domain.LanguageTestScore; -import com.example.solidconnection.score.dto.*; -import com.example.solidconnection.score.repository.GpaScoreRepository; -import com.example.solidconnection.score.repository.LanguageTestScoreRepository; -import com.example.solidconnection.score.service.ScoreService; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("점수 서비스 테스트") -public class ScoreServiceTest { - @InjectMocks - ScoreService scoreService; - @Mock - GpaScoreRepository gpaScoreRepository; - @Mock - LanguageTestScoreRepository languageTestScoreRepository; - @Mock - SiteUserRepository siteUserRepository; - - private SiteUser siteUser; - private GpaScore beforeGpaScore; - private GpaScore beforeGpaScore2; - private LanguageTestScore beforeLanguageTestScore; - private LanguageTestScore beforeLanguageTestScore2; - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - beforeGpaScore = createBeforeGpaScore(siteUser, 4.5); - beforeGpaScore2 = createBeforeGpaScore(siteUser, 4.3); - beforeLanguageTestScore = createBeforeLanguageTestScore(siteUser); - beforeLanguageTestScore2 = createBeforeLanguageTestScore2(siteUser); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private GpaScore createBeforeGpaScore(SiteUser siteUser, Double gpa) { - return new GpaScore( - new Gpa(gpa, 4.5, "http://example.com/gpa-report.pdf"), - siteUser, - LocalDate.of(2024, 10, 20) - ); - } - - private LanguageTestScore createBeforeLanguageTestScore(SiteUser siteUser) { - return new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEIC, "900", "http://example.com/gpa-report.pdf"), - LocalDate.of(2024, 10, 30), - siteUser - ); - } - - private LanguageTestScore createBeforeLanguageTestScore2(SiteUser siteUser) { - return new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEFL_IBT, "100", "http://example.com/gpa-report.pdf"), - LocalDate.of(2024, 10, 30), - siteUser - ); - } - - @Test - void 학점을_등록한다_기존이력이_없을_때() { - // Given - GpaScoreRequest gpaScoreRequest = new GpaScoreRequest( - 4.5, 4.5, LocalDate.of(2024, 10, 20), "http://example.com/gpa-report.pdf" - ); - GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser, gpaScoreRequest.issueDate()); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(gpaScoreRepository.save(newGpaScore)).thenReturn(newGpaScore); - - // 새로운 gpa 저장하게된다. - scoreService.submitGpaScore(siteUser.getEmail(), gpaScoreRequest); - - // Then - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(gpaScoreRepository, times(1)).save(any(GpaScore.class)); - } - - @Test - void 어학성적을_등록한다_기존이력이_없을_때() { - // Given - LanguageTestScoreRequest languageTestScoreRequest = new LanguageTestScoreRequest( - LanguageTestType.TOEIC, "900", - LocalDate.of(2024, 10, 30), "http://example.com/gpa-report.pdf" - ); - LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); - LanguageTestScore languageTestScore = new LanguageTestScore(languageTest, LocalDate.of(2024, 10, 30), siteUser); - - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(languageTestScoreRepository.save(any(LanguageTestScore.class))).thenReturn(languageTestScore); - - //when - scoreService.submitLanguageTestScore(siteUser.getEmail(), languageTestScoreRequest); - - // Then - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(languageTestScoreRepository, times(1)).save(any(LanguageTestScore.class)); - } - - @Test - void 학점이력을_조회한다_제출이력이_있을_때() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - beforeGpaScore.setSiteUser(siteUser); - beforeGpaScore2.setSiteUser(siteUser); - - // when - GpaScoreStatusResponse gpaScoreStatusResponse = scoreService.getGpaScoreStatus(siteUser.getEmail()); - - // Then - List expectedStatusList = List.of( - GpaScoreStatus.from(beforeGpaScore), - GpaScoreStatus.from(beforeGpaScore2) - ); - assertThat(gpaScoreStatusResponse.gpaScoreStatusList()) - .hasSize(2) - .containsExactlyElementsOf(expectedStatusList); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - } - - @Test - void 학점이력을_조회한다_제출이력이_없을_때() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // when - GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser.getEmail()); - - // Then - assertThat(gpaScoreStatus.gpaScoreStatusList()).isEmpty(); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - } - - - @Test - void 어학이력을_조회한다_제출이력이_있을_때() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - beforeLanguageTestScore.setSiteUser(siteUser); - beforeLanguageTestScore2.setSiteUser(siteUser); - - // when - LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser.getEmail()); - - // Then - List expectedStatusList = List.of( - LanguageTestScoreStatus.from(beforeLanguageTestScore), - LanguageTestScoreStatus.from(beforeLanguageTestScore2) - ); - assertThat(languageTestScoreStatus.languageTestScoreStatusList()) - .hasSize(2) - .containsExactlyElementsOf(expectedStatusList); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - } - - @Test - void 어학이력을_조회한다_제출이력이_없을_때() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // when - LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser.getEmail()); - - // Then - assertThat(languageTestScoreStatus.languageTestScoreStatusList()).isEmpty(); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java deleted file mode 100644 index 860f76e11..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.s3.S3Service; -import com.example.solidconnection.s3.UploadedFileUrlResponse; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; -import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; -import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.siteuser.service.SiteUserService; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.ImgType; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -import java.time.LocalDateTime; - -import static com.example.solidconnection.custom.exception.ErrorCode.*; -import static com.example.solidconnection.siteuser.service.SiteUserService.NICKNAME_LAST_CHANGE_DATE_FORMAT; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("유저 서비스 테스트") -public class SiteUserServiceTest { - @InjectMocks - SiteUserService siteUserService; - @Mock - SiteUserRepository siteUserRepository; - @Mock - LikedUniversityRepository likedUniversityRepository; - @Mock - S3Service s3Service; - - private SiteUser siteUser; - private MultipartFile imageFile; - private UploadedFileUrlResponse uploadedFileUrlResponse; - private final String defaultProfileImageUrl = "http://k.kakaocdn.net/dn/o2c5A/btsASaNh2Lr/Xum5kRyuErD8LIuLQEWfC0/img_640x640.jpg"; - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - imageFile = createMockImageFile(); - uploadedFileUrlResponse = createUploadedFileUrlResponse(); - - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profile/fajwoiejoiewjfoi", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private MultipartFile createMockImageFile() { - return new MockMultipartFile("file1", "test1.png", - "image/png", "test image content 1".getBytes()); - - } - - private UploadedFileUrlResponse createUploadedFileUrlResponse() { - return new UploadedFileUrlResponse("profile/fajwoiejoiewjfoi"); - } - - @Test - void 초기_프로필_이미지를_수정한다_kakao() { - siteUser.setProfileImageUrl(defaultProfileImageUrl); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); - - // When - ProfileImageUpdateResponse profileImageUpdateResponse = - siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); - // Then - assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(s3Service, times(0)).deleteExProfile(siteUser.getEmail()); - verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); - verify(siteUserRepository, times(1)).save(any(SiteUser.class)); - } - - @Test - void 초기_프로필_이미지를_수정한다_null() { - siteUser.setProfileImageUrl(null); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); - - // When - ProfileImageUpdateResponse profileImageUpdateResponse = - siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); - // Then - assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(s3Service, times(0)).deleteExProfile(siteUser.getEmail()); - verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); - verify(siteUserRepository, times(1)).save(any(SiteUser.class)); - } - - @Test - void 프로필_이미지를_수정한다() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); - - // When - ProfileImageUpdateResponse profileImageUpdateResponse = - siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); - // Then - assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(s3Service, times(1)).deleteExProfile(siteUser.getEmail()); - verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); - verify(siteUserRepository, times(1)).save(any(SiteUser.class)); - } - - @Test - void 프로필_이미지를_수정할_때_이미지가_없다면_예외_응답을_반환한다() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - siteUserService.updateProfileImage(siteUser.getEmail(), null)); - assertThat(exception.getMessage()) - .isEqualTo(PROFILE_IMAGE_NEEDED.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(PROFILE_IMAGE_NEEDED.getCode()); - } - - @Test - void 닉네임을_수정한다() { - // Given - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // When - NicknameUpdateResponse nicknameUpdateResponse - = siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest); - // Then - assertEquals( nicknameUpdateResponse, NicknameUpdateResponse.from(siteUser)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(siteUserRepository, times(1)).save(any(SiteUser.class)); - } - - @Test - void 닉네임을_수정할_때_중복된_닉네임이라면_예외_응답을_반환한다() { - // Given - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(siteUserRepository.existsByNickname(nicknameUpdateRequest.nickname())).thenReturn(true); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest)); - assertThat(exception.getMessage()) - .isEqualTo(NICKNAME_ALREADY_EXISTED.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(NICKNAME_ALREADY_EXISTED.getCode()); - } - - @Test - void 닉네임을_수정할_때_변경_가능_기한이_지나지_않았다면_예외_응답을_반환한다() { - // Given - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); - siteUser.setNicknameModifiedAt(LocalDateTime.now()); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest)); - - String formatLastModifiedAt - = String.format("(마지막 수정 시간 : %s)", NICKNAME_LAST_CHANGE_DATE_FORMAT.format(siteUser.getNicknameModifiedAt())); - CustomException expectedException = new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); - assertThat(exception.getMessage()).isEqualTo(expectedException.getMessage()); - assertThat(exception.getCode()).isEqualTo(expectedException.getCode()); - } -} From 16b907f74e4ae6bd3a613f4a974706a5b36b68f4 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 30 Jan 2025 05:33:16 +0900 Subject: [PATCH 134/158] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=8C=80=ED=95=99=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 공통 추천 대학 로직 변경 - 해당 학기에 열리는 대학들을 랜덤으로 가져오도록 * refactor: 불필요한 셔플 제거 - 랜덤으로 가져오는 것을 다시 셔플할 필요는 없으므로 * refactor: 클래스 이름 변경 * style: 필드 선언 순서 변경 - final 이 위로 가도록 * refactor: native query 를 사용하도록 --- .../UniversityInfoForApplyRepository.java | 8 +++ .../service/GeneralRecommendUniversities.java | 50 ------------------- .../GeneralUniversityRecommendService.java | 35 +++++++++++++ .../service/UniversityRecommendService.java | 7 ++- .../e2e/UniversityRecommendTest.java | 10 ++-- ...GeneralUniversityRecommendServiceTest.java | 41 +++++++++++++++ .../UniversityRecommendServiceTest.java | 8 +-- 7 files changed, 96 insertions(+), 63 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java create mode 100644 src/main/java/com/example/solidconnection/university/service/GeneralUniversityRecommendService.java create mode 100644 src/test/java/com/example/solidconnection/university/service/GeneralUniversityRecommendServiceTest.java diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index 4adc0d718..60474c13d 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -45,6 +45,14 @@ OR u.region.code IN ( """) List findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(@Param("siteUser") SiteUser siteUser, @Param("term") String term); + @Query(value = """ + SELECT * + FROM university_info_for_apply + WHERE term = :term + ORDER BY RAND() LIMIT :limitNum + """, nativeQuery = true) + List findRandomByTerm(@Param("term") String term, @Param("limitNum") int limitNum); + default UniversityInfoForApply getUniversityInfoForApplyById(Long id) { return findById(id) .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java deleted file mode 100644 index 92054eee6..000000000 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.example.solidconnection.university.service; - -import com.example.solidconnection.repositories.CountryRepository; -import com.example.solidconnection.university.domain.UniversityInfoForApply; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; - -import java.util.List; - -import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; - -@RequiredArgsConstructor -@Component -public class GeneralRecommendUniversities { - - /* - * 매 선발 시기(term) 마다 지원할 수 있는 대학교가 달라지므르, 추천 대학교도 달라져야 한다. - * 하지만 매번 추천 대학교를 바꾸기에는 번거롭다. - * 따라서 '추천 대학교 후보'들을 설정하고, DB 에서 현재 term 에 대해 찾아지는 대학교만 추천 대학교로 지정한다. - * */ - @Getter - private final List recommendUniversities; - private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final CountryRepository countryRepository; - private final List candidates = List.of( - "오스트라바 대학", "RMIT멜버른공과대학(A형)", "알브슈타트 지그마링엔 대학", - "뉴저지시티대학(A형)", "도요대학", "템플대학(A형)", "빈 공과대학교", - "리스본대학 공과대학", "바덴뷔르템베르크 산학협력대학", "긴다이대학", "네바다주립대학 라스베이거스(B형)", "릴 가톨릭 대학", - "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교", - "밀라노공과대학", "장물랭리옹3세대학교", "시드니대학", "아우크스부르크대학", "쳄니츠 공과대학", "북경외국어대학교 IBS" - ); - - @Value("${university.term}") - public String term; - - @EventListener(ApplicationReadyEvent.class) - public void init() { - int i = 0; - while (recommendUniversities.size() < RECOMMEND_UNIVERSITY_NUM && i < candidates.size()) { - universityInfoForApplyRepository.findFirstByKoreanNameAndTerm(candidates.get(i), term) - .ifPresent(recommendUniversities::add); - i++; - } - } -} diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralUniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/GeneralUniversityRecommendService.java new file mode 100644 index 000000000..d39fee1ec --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/service/GeneralUniversityRecommendService.java @@ -0,0 +1,35 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; + +@Service +@RequiredArgsConstructor +public class GeneralUniversityRecommendService { + + /* + * 해당 시기에 열리는 대학교들 중 랜덤으로 선택해서 목록을 구성한다. + * */ + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Getter + private List recommendUniversities; + + @Value("${university.term}") + public String term; + + @EventListener(ApplicationReadyEvent.class) + public void init() { + recommendUniversities = universityInfoForApplyRepository.findRandomByTerm(term, RECOMMEND_UNIVERSITY_NUM); + } +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index 6a6a43fbf..654b08390 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -23,7 +23,7 @@ public class UniversityRecommendService { public static final int RECOMMEND_UNIVERSITY_NUM = 6; private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final GeneralRecommendUniversities generalRecommendUniversities; + private final GeneralUniversityRecommendService generalUniversityRecommendService; private final SiteUserRepository siteUserRepository; @Value("${university.term}") @@ -56,7 +56,7 @@ public UniversityRecommendsResponse getPersonalRecommends(String email) { } private List getGeneralRecommendsExcludingSelected(List alreadyPicked) { - List generalRecommend = new ArrayList<>(generalRecommendUniversities.getRecommendUniversities()); + List generalRecommend = new ArrayList<>(generalUniversityRecommendService.getRecommendUniversities()); generalRecommend.removeAll(alreadyPicked); Collections.shuffle(generalRecommend); return generalRecommend.subList(0, RECOMMEND_UNIVERSITY_NUM - alreadyPicked.size()); @@ -68,8 +68,7 @@ private List getGeneralRecommendsExcludingSelected(List< @Transactional(readOnly = true) @ThunderingHerdCaching(key = "university:recommend:general", cacheManager = "customCacheManager", ttlSec = 86400) public UniversityRecommendsResponse getGeneralRecommends() { - List generalRecommends = new ArrayList<>(generalRecommendUniversities.getRecommendUniversities()); - Collections.shuffle(generalRecommends); + List generalRecommends = new ArrayList<>(generalUniversityRecommendService.getRecommendUniversities()); return new UniversityRecommendsResponse(generalRecommends.stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList()); diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index 00afbc8e3..4f3bd3042 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -10,7 +10,7 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.dto.UniversityRecommendsResponse; -import com.example.solidconnection.university.service.GeneralRecommendUniversities; +import com.example.solidconnection.university.service.GeneralUniversityRecommendService; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -41,7 +41,7 @@ class UniversityRecommendTest extends UniversityDataSetUpEndToEndTest { private TokenProvider tokenProvider; @Autowired - private GeneralRecommendUniversities generalRecommendUniversities; + private GeneralUniversityRecommendService generalUniversityRecommendService; private SiteUser siteUser; private String accessToken; @@ -51,7 +51,7 @@ void setUp() { // setUp - 회원 정보 저장 String email = "email@email.com"; siteUser = siteUserRepository.save(createSiteUserByEmail(email)); - generalRecommendUniversities.init(); + generalUniversityRecommendService.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); @@ -156,7 +156,7 @@ void setUp() { .extract().as(UniversityRecommendsResponse.class); List generalRecommendUniversities - = this.generalRecommendUniversities.getRecommendUniversities().stream() + = this.generalUniversityRecommendService.getRecommendUniversities().stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList(); assertAll( @@ -179,7 +179,7 @@ void setUp() { .extract().as(UniversityRecommendsResponse.class); List generalRecommendUniversities - = this.generalRecommendUniversities.getRecommendUniversities().stream() + = this.generalUniversityRecommendService.getRecommendUniversities().stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList(); assertAll( diff --git a/src/test/java/com/example/solidconnection/university/service/GeneralUniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/GeneralUniversityRecommendServiceTest.java new file mode 100644 index 000000000..d93765a44 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/GeneralUniversityRecommendServiceTest.java @@ -0,0 +1,41 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.util.List; + +import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("공통 추천 대학 서비스 테스트") +@TestContainerSpringBootTest +class GeneralUniversityRecommendServiceTest extends BaseIntegrationTest { + + @Autowired + private GeneralUniversityRecommendService generalUniversityRecommendService; + + @Value("${university.term}") + private String term; + + @Test + void 모집_시기의_대학들_중에서_랜덤하게_N개를_추천_목록으로_구성한다() { + // given + generalUniversityRecommendService.init(); + List universities = generalUniversityRecommendService.getRecommendUniversities(); + + // when & then + assertAll( + () -> assertThat(universities) + .extracting("term") + .allMatch(term::equals), + () -> assertThat(universities).hasSize(RECOMMEND_UNIVERSITY_NUM) + ); + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java index cadd45aaf..17d951614 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java @@ -38,11 +38,11 @@ class UniversityRecommendServiceTest extends BaseIntegrationTest { private InterestedCountyRepository interestedCountyRepository; @Autowired - private GeneralRecommendUniversities generalRecommendUniversities; + private GeneralUniversityRecommendService generalUniversityRecommendService; @BeforeEach void setUp() { - generalRecommendUniversities.init(); + generalUniversityRecommendService.init(); } @Test @@ -118,7 +118,7 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIVERSITY_NUM) .containsExactlyInAnyOrderElementsOf( - generalRecommendUniversities.getRecommendUniversities().stream() + generalUniversityRecommendService.getRecommendUniversities().stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList() ); @@ -133,7 +133,7 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIVERSITY_NUM) .containsExactlyInAnyOrderElementsOf( - generalRecommendUniversities.getRecommendUniversities().stream() + generalUniversityRecommendService.getRecommendUniversities().stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList() ); From 96547704692263eb90e29e50533abb5f296c58b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:57:40 +0900 Subject: [PATCH 135/158] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/service/BoardServiceTest.java | 72 +++++++++++++++++++ .../integration/BaseIntegrationTest.java | 49 +++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java diff --git a/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java new file mode 100644 index 000000000..98c2b28fa --- /dev/null +++ b/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java @@ -0,0 +1,72 @@ +package com.example.solidconnection.board.service; + +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.BoardFindPostResponse; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.type.PostCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.ZonedDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("게시판 서비스 테스트") +class BoardServiceTest extends BaseIntegrationTest { + + @Autowired + private BoardService boardService; + + @Test + void 게시판_코드와_카테고리로_게시글_목록을_조회한다() { + // given + List posts = List.of( + 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, + 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 + ); + List expectedPosts = posts.stream() + .filter(post -> post.getCategory().equals(PostCategory.자유) && post.getBoard().getCode().equals(BoardCode.FREE.name())) + .toList(); + List expectedResponses = BoardFindPostResponse.from(expectedPosts); + + // when + List actualResponses = boardService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.자유.name() + ); + + // then + assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses); + } + + @Test + void 전체_카테고리로_조회시_해당_게시판의_모든_게시글을_조회한다() { + // given + List posts = List.of( + 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, + 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 + ); + List expectedPosts = posts.stream() + .filter(post -> post.getBoard().getCode().equals(BoardCode.FREE.name())) + .toList(); + List expectedResponses = BoardFindPostResponse.from(expectedPosts); + + // when + List actualResponses = boardService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.전체.name() + ); + + // then + assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses); + } +} diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index f588b87ae..054cf6851 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -3,8 +3,12 @@ import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.entity.Region; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.PostImageRepository; import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -12,6 +16,7 @@ import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; import com.example.solidconnection.university.domain.LanguageRequirement; @@ -76,6 +81,15 @@ public abstract class BaseIntegrationTest { public static Board 유럽권; public static Board 자유게시판; + public static Post 미주권_자유게시글; + public static Post 아시아권_자유게시글; + public static Post 유럽권_자유게시글; + public static Post 자유게시판_자유게시글; + public static Post 미주권_질문게시글; + public static Post 아시아권_질문게시글; + public static Post 유럽권_질문게시글; + public static Post 자유게시판_질문게시글; + @Autowired private SiteUserRepository siteUserRepository; @@ -97,6 +111,12 @@ public abstract class BaseIntegrationTest { @Autowired private BoardRepository boardRepository; + @Autowired + private PostRepository postRepository; + + @Autowired + private PostImageRepository postImageRepository; + @Value("${university.term}") public String term; @@ -109,6 +129,7 @@ public void setUpBaseData() { setUpUniversityInfos(); setUpLanguageRequirements(); setUpBoards(); + setUpPosts(); } private void setUpSiteUsers() { @@ -337,6 +358,17 @@ private void setUpBoards() { 자유게시판 = boardRepository.save(new Board(FREE.name(), "자유게시판")); } + private void setUpPosts() { + 미주권_자유게시글 = createPost(미주권, 테스트유저_1, "미주권 자유게시글", "미주권 자유게시글 내용", PostCategory.자유); + 아시아권_자유게시글 = createPost(아시아권, 테스트유저_2, "아시아권 자유게시글", "아시아권 자유게시글 내용", PostCategory.자유); + 유럽권_자유게시글 = createPost(유럽권, 테스트유저_1, "유럽권 자유게시글", "유럽권 자유게시글 내용", PostCategory.자유); + 자유게시판_자유게시글 = createPost(자유게시판, 테스트유저_2, "자유게시판 자유게시글", "자유게시판 자유게시글 내용", PostCategory.자유); + 미주권_질문게시글 = createPost(미주권, 테스트유저_1, "미주권 질문게시글", "미주권 질문게시글 내용", PostCategory.질문); + 아시아권_질문게시글 = createPost(아시아권, 테스트유저_2, "아시아권 질문게시글", "아시아권 질문게시글 내용", PostCategory.질문); + 유럽권_질문게시글 = createPost(유럽권, 테스트유저_1, "유럽권 질문게시글", "유럽권 질문게시글 내용", PostCategory.질문); + 자유게시판_질문게시글 = createPost(자유게시판, 테스트유저_2, "자유게시판 질문게시글", "자유게시판 질문게시글 내용", PostCategory.질문); + } + private void saveLanguageTestRequirement( UniversityInfoForApply universityInfoForApply, LanguageTestType testType, @@ -351,4 +383,21 @@ private void saveLanguageTestRequirement( universityInfoForApplyRepository.save(universityInfoForApply); languageRequirementRepository.save(languageRequirement); } + + private Post createPost(Board board, SiteUser siteUser, String title, String content, PostCategory category) { + Post post = new Post( + title, + content, + false, + 0L, + 0L, + category + ); + post.setBoardAndSiteUser(board, siteUser); + Post savedPost = postRepository.save(post); + PostImage postImage = new PostImage("imageUrl"); + postImage.setPost(savedPost); + postImageRepository.save(postImage); + return savedPost; + } } From 89eca057716d882486a18e19d517673dbc97abe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:07:41 +0900 Subject: [PATCH 136/158] =?UTF-8?q?test:=20=EC=A7=80=EC=9B=90=EC=84=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 지원서 제출 관련 통합테스트 코드 추가 * test: 지원자 목록 조회 관련 통합테스트 코드 추가 * test: 경쟁자 목록 조회 관련 통합테스트 코드 추가 * test: 이번학기 지원자 대학국문 필터링 조회 시 secondChoice 검증 추가 * style: 불필요한 개행 삭제 * refactor: 테스트 메서드 이름에 컨벤션 적용 * test: UniversityChoiceRequest 생성 시 불필요한 인자 제거 * test: firstChoice 중복 검증 제외 * test: cntainsAll로 검증하는 것으로 변경 * test: 테스트유저2로 검증하는 것으로 변경 --- .../service/ApplicationQueryServiceTest.java | 214 ++++++++++++++++++ .../ApplicationSubmissionServiceTest.java | 202 +++++++++++++++++ .../integration/BaseIntegrationTest.java | 146 +++++++++++- 3 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java new file mode 100644 index 000000000..b8f5cd283 --- /dev/null +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java @@ -0,0 +1,214 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.dto.ApplicantResponse; +import com.example.solidconnection.application.dto.ApplicationsResponse; +import com.example.solidconnection.application.dto.UniversityApplicantsResponse; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지원서 조회 서비스 테스트") +class ApplicationQueryServiceTest extends BaseIntegrationTest { + + @Autowired + private ApplicationQueryService applicationQueryService; + + @Nested + class 지원자_목록_조회_테스트 { + + @Test + void 이번_학기_전체_지원자를_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_2.getEmail(), + "", + "" + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, false))), + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))), + UniversityApplicantsResponse.of(코펜하겐IT대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_7_코펜하겐IT대학_X_X_지원서, false))) + )); + + assertThat(response.secondChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))), + UniversityApplicantsResponse.of(그라츠대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, false))), + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))) + )); + + assertThat(response.thirdChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))), + UniversityApplicantsResponse.of(서던덴마크대학교_지원_정보, + List.of(ApplicantResponse.of(테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, false))), + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))) + )); + } + + @Test + void 이번_학기_특정_지역_지원자를_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_2.getEmail(), + 영미권.getCode(), + "" + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))) + )); + + assertThat(response.secondChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))) + )); + } + + @Test + void 이번_학기_지원자를_대학_국문_이름으로_필터링해서_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_2.getEmail(), + null, + "일본" + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, false))) + )); + + assertThat(response.secondChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(메이지대학_지원_정보, List.of()) + )); + + assertThat(response.thirdChoice()).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))) + ); + } + + @Test + void 이전_학기_지원자는_조회되지_않는다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_1.getEmail(), + "", + "" + ); + + // then + assertThat(response.firstChoice()).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(이전학기_지원서, false))) + )); + assertThat(response.secondChoice()).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(이전학기_지원서, false))) + )); + assertThat(response.thirdChoice()).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(이전학기_지원서, false))) + )); + } + } + + @Nested + class 경쟁자_목록_조회_테스트 { + + @Test + void 이번_학기_지원한_대학의_경쟁자_목록을_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( + 테스트유저_2.getEmail() + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))) + )); + + assertThat(response.secondChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))) + )); + + assertThat(response.thirdChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))) + )); + } + + @Test + void 이번_학기_지원한_대학_중_미선택이_있을_때_경쟁자_목록을_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( + 테스트유저_7.getEmail() + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(코펜하겐IT대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_7_코펜하겐IT대학_X_X_지원서, true))) + )); + + assertThat(response.secondChoice()).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(코펜하겐IT대학_지원_정보, List.of()) + ); + + assertThat(response.thirdChoice()).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(코펜하겐IT대학_지원_정보, List.of()) + ); + } + + @Test + void 이번_학기_지원한_대학이_모두_미선택일_때_경쟁자_목록을_조회한다() { + //when + ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( + 테스트유저_6.getEmail() + ); + + // then + assertThat(response.firstChoice()).isEmpty(); + assertThat(response.secondChoice()).isEmpty(); + assertThat(response.thirdChoice()).isEmpty(); + } + } +} diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java new file mode 100644 index 000000000..84f130d54 --- /dev/null +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -0,0 +1,202 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.dto.ApplyRequest; +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.VerifyStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; + +import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; +import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE_STATUS; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("지원서 제출 서비스 테스트") +class ApplicationSubmissionServiceTest extends BaseIntegrationTest { + + @Autowired + private ApplicationSubmissionService applicationSubmissionService; + + @Autowired + private ApplicationRepository applicationRepository; + + @Autowired + private GpaScoreRepository gpaScoreRepository; + + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + + @Test + void 정상적으로_지원서를_제출한다() { + // given + GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createApprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + 네바다주립대학_라스베이거스_지원_정보.getId(), + 메모리얼대학_세인트존스_A_지원_정보.getId() + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + // when + boolean result = applicationSubmissionService.apply(테스트유저_1.getEmail(), request); + + // then + Application savedApplication = applicationRepository.findBySiteUserAndTerm(테스트유저_1, term).orElseThrow(); + assertAll( + () -> assertThat(result).isTrue(), + () -> assertThat(savedApplication.getGpa()).isEqualTo(gpaScore.getGpa()), + () -> assertThat(savedApplication.getLanguageTest()).isEqualTo(languageTestScore.getLanguageTest()), + () -> assertThat(savedApplication.getVerifyStatus()).isEqualTo(VerifyStatus.APPROVED), + () -> assertThat(savedApplication.getNicknameForApply()).isNotNull(), + () -> assertThat(savedApplication.getUpdateCount()).isZero(), + () -> assertThat(savedApplication.getTerm()).isEqualTo(term), + () -> assertThat(savedApplication.isDelete()).isFalse(), + () -> assertThat(savedApplication.getFirstChoiceUniversity().getId()).isEqualTo(괌대학_A_지원_정보.getId()), + () -> assertThat(savedApplication.getSecondChoiceUniversity().getId()).isEqualTo(네바다주립대학_라스베이거스_지원_정보.getId()), + () -> assertThat(savedApplication.getThirdChoiceUniversity().getId()).isEqualTo(메모리얼대학_세인트존스_A_지원_정보.getId()), + () -> assertThat(savedApplication.getSiteUser().getId()).isEqualTo(테스트유저_1.getId()) + ); + } + + @Test + void 미승인된_GPA_성적으로_지원하면_예외_응답을_반환한다() { + // given + GpaScore gpaScore = createUnapprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createApprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + null, + null + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + // when & then + assertThatCode(() -> + applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + ) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_GPA_SCORE_STATUS.getMessage()); + } + + @Test + void 미승인된_어학성적으로_지원하면_예외_응답을_반환한다() { + // given + GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createUnapprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + null, + null + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + // when & then + assertThatCode(() -> + applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + ) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); + } + + @Test + void 동일한_대학을_중복_선택하면_예외_응답을_반환한다() { + // given + GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createUnapprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + 괌대학_A_지원_정보.getId(), + 메모리얼대학_세인트존스_A_지원_정보.getId() + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + // when & then + assertThatCode(() -> + applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + ) + .isInstanceOf(CustomException.class) + .hasMessage(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + } + + @Test + void 지원서_수정_횟수를_초과하면_예외_응답을_반환한다() { + // given + GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createApprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + null, + null + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + for (int i = 0; i < APPLICATION_UPDATE_COUNT_LIMIT + 1; i++) { + applicationSubmissionService.apply(테스트유저_1.getEmail(), request); + } + + // when & then + assertThatCode(() -> + applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + ) + .isInstanceOf(CustomException.class) + .hasMessage(APPLY_UPDATE_LIMIT_EXCEED.getMessage()); + } + + private GpaScore createUnapprovedGpaScore(SiteUser siteUser) { + GpaScore gpaScore = new GpaScore( + new Gpa(4.0, 4.5, "/gpa-report.pdf"), + siteUser, + LocalDate.now() + ); + return gpaScoreRepository.save(gpaScore); + } + + private GpaScore createApprovedGpaScore(SiteUser siteUser) { + GpaScore gpaScore = new GpaScore( + new Gpa(4.0, 4.5, "/gpa-report.pdf"), + siteUser, + LocalDate.now() + ); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + return gpaScoreRepository.save(gpaScore); + } + + private LanguageTestScore createUnapprovedLanguageTestScore(SiteUser siteUser) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), + LocalDate.now(), + siteUser + ); + return languageTestScoreRepository.save(languageTestScore); + } + + private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), + LocalDate.now(), + siteUser + ); + languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); + return languageTestScoreRepository.save(languageTestScore); + } +} diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index 054cf6851..ec29b8499 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -1,5 +1,9 @@ package com.example.solidconnection.support.integration; +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.repository.ApplicationRepository; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.entity.Country; @@ -10,6 +14,10 @@ import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.repositories.PostImageRepository; import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.DatabaseClearExtension; @@ -19,6 +27,7 @@ import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; +import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.LanguageRequirement; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; @@ -30,7 +39,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import java.time.LocalDate; import java.util.HashSet; +import java.util.List; import static com.example.solidconnection.type.BoardCode.AMERICAS; import static com.example.solidconnection.type.BoardCode.ASIA; @@ -45,6 +56,12 @@ public abstract class BaseIntegrationTest { public static SiteUser 테스트유저_1; public static SiteUser 테스트유저_2; + public static SiteUser 테스트유저_3; + public static SiteUser 테스트유저_4; + public static SiteUser 테스트유저_5; + public static SiteUser 테스트유저_6; + public static SiteUser 테스트유저_7; + public static SiteUser 이전학기_지원자; public static Region 영미권; public static Region 유럽; @@ -76,6 +93,14 @@ public abstract class BaseIntegrationTest { public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; public static UniversityInfoForApply 메이지대학_지원_정보; + public static Application 테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서; + public static Application 테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서; + public static Application 테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서; + public static Application 테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서; + public static Application 테스트유저_6_X_X_X_지원서; + public static Application 테스트유저_7_코펜하겐IT대학_X_X_지원서; + public static Application 이전학기_지원서; + public static Board 미주권; public static Board 아시아권; public static Board 유럽권; @@ -108,6 +133,15 @@ public abstract class BaseIntegrationTest { @Autowired private LanguageRequirementRepository languageRequirementRepository; + @Autowired + private ApplicationRepository applicationRepository; + + @Autowired + private GpaScoreRepository gpaScoreRepository; + + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + @Autowired private BoardRepository boardRepository; @@ -128,6 +162,7 @@ public void setUpBaseData() { setUpUniversities(); setUpUniversityInfos(); setUpLanguageRequirements(); + setUpApplications(); setUpBoards(); setUpPosts(); } @@ -150,6 +185,60 @@ private void setUpSiteUsers() { PreparationStatus.CONSIDERING, Role.MENTEE, Gender.FEMALE)); + + 테스트유저_3 = siteUserRepository.save(new SiteUser( + "test3@example.com", + "nickname3", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE)); + + 테스트유저_4 = siteUserRepository.save(new SiteUser( + "test4@example.com", + "nickname4", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE)); + + 테스트유저_5 = siteUserRepository.save(new SiteUser( + "test5@example.com", + "nickname5", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE)); + + 테스트유저_6 = siteUserRepository.save(new SiteUser( + "test6@example.com", + "nickname6", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE)); + + 테스트유저_7 = siteUserRepository.save(new SiteUser( + "test7@example.com", + "nickname7", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE)); + + 이전학기_지원자 = siteUserRepository.save(new SiteUser( + "old@example.com", + "oldNickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE)); } private void setUpRegions() { @@ -351,6 +440,41 @@ private void setUpLanguageRequirements() { saveLanguageTestRequirement(메이지대학_지원_정보, LanguageTestType.JLPT, "N2"); } + private void setUpApplications() { + 테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서 = new Application(테스트유저_2, createApprovedGpaScore(테스트유저_2).getGpa(), createApprovedLanguageTestScore(테스트유저_2).getLanguageTest(), + term, 괌대학_B_지원_정보, 괌대학_A_지원_정보, 린츠_카톨릭대학_지원_정보, "user2_nickname"); + + 테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서 = new Application(테스트유저_3, createApprovedGpaScore(테스트유저_3).getGpa(), createApprovedLanguageTestScore(테스트유저_3).getLanguageTest(), + term, 괌대학_A_지원_정보, 괌대학_B_지원_정보, 그라츠공과대학_지원_정보, "user3_nickname"); + + 테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서 = new Application(테스트유저_4, createApprovedGpaScore(테스트유저_4).getGpa(), createApprovedLanguageTestScore(테스트유저_4).getLanguageTest(), + term, 메이지대학_지원_정보, 그라츠대학_지원_정보, 서던덴마크대학교_지원_정보, "user4_nickname"); + + 테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서 = new Application(테스트유저_5, createApprovedGpaScore(테스트유저_5).getGpa(), createApprovedLanguageTestScore(테스트유저_5).getLanguageTest(), + term, 네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "user5_nickname"); + + 테스트유저_6_X_X_X_지원서 = new Application(테스트유저_6, createApprovedGpaScore(테스트유저_6).getGpa(), createApprovedLanguageTestScore(테스트유저_6).getLanguageTest(), + term, null, null, null, "user6_nickname"); + + 테스트유저_7_코펜하겐IT대학_X_X_지원서 = new Application(테스트유저_7, createApprovedGpaScore(테스트유저_7).getGpa(), createApprovedLanguageTestScore(테스트유저_7).getLanguageTest(), + term, 코펜하겐IT대학_지원_정보, null, null, "user7_nickname"); + + 이전학기_지원서 = new Application(이전학기_지원자, createApprovedGpaScore(이전학기_지원자).getGpa(), createApprovedLanguageTestScore(이전학기_지원자).getLanguageTest(), + "1988-1", 네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "old_nickname"); + + 테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_6_X_X_X_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_7_코펜하겐IT대학_X_X_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 이전학기_지원서.setVerifyStatus(VerifyStatus.APPROVED); + + applicationRepository.saveAll(List.of( + 테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, 테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, 테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, 테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, + 테스트유저_6_X_X_X_지원서, 테스트유저_7_코펜하겐IT대학_X_X_지원서, 이전학기_지원서)); + } + private void setUpBoards() { 미주권 = boardRepository.save(new Board(AMERICAS.name(), "미주권")); 아시아권 = boardRepository.save(new Board(ASIA.name(), "아시아권")); @@ -384,7 +508,27 @@ private void saveLanguageTestRequirement( languageRequirementRepository.save(languageRequirement); } - private Post createPost(Board board, SiteUser siteUser, String title, String content, PostCategory category) { + private GpaScore createApprovedGpaScore(SiteUser siteUser) { + GpaScore gpaScore = new GpaScore( + new Gpa(4.0, 4.5, "/gpa-report.pdf"), + siteUser, + LocalDate.now() + ); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + return gpaScoreRepository.save(gpaScore); + } + + private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), + LocalDate.now(), + siteUser + ); + languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); + return languageTestScoreRepository.save(languageTestScore); + } + + private Post createPost (Board board, SiteUser siteUser, String title, String content, PostCategory category){ Post post = new Post( title, content, From 2fc9d85f33e51fe46cc12041bcf63b1773953ccf Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 6 Feb 2025 07:48:15 +0900 Subject: [PATCH 137/158] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=EC=9D=98?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=97=90=EC=84=9C=20=EC=9B=90?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=98=95=ED=83=9C=EB=A1=9C=20=EB=B0=9B?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: manager, provider, userDetails 적용 - 클래스 자체는 많아졌지만, 이 코드를 처음 보는 개발자도 쉽게 이해할 수 있도록, 스프링 시큐리티의 표준 스펙에 맞게 개발하였음 * refactor: 패키지 이동 - 설정(configuration)인 것들과 아닌 것들 분리 * test: Authentication 테스트 작성 * test: userDetailsService 테스트 작성 * test: AuthenticationProvider 테스트 작성 * feat: 인증 정보를 객체로 바꾸는 resolver 구현 - 서비스 코드가 더이상 어떤 정보가 token 의 subject인지 몰라도 되도록 결합도를 낮춤 * test: resolver 테스트 작성 * refactor: 탈퇴한 사용자인지 검증 추가, 함수 분리 * test: 탈퇴한 사용자 검증 테스트 작성 * style: 불필요한 개행 삭제 * refactor: 리졸버 이름 변경 * feat: 리졸버 활성화 * refactor: 지원서 관련 코드에 리졸버 적용 * refactor: 인증 관련 코드에 리졸버 적용 * refactor: 댓글 관련 코드에 리졸버 적용 * refactor: 사용자 관련 코드에 리졸버 적용 * refactor: 게시글 관련 코드에 리졸버 적용 * refactor: 대학 관련 코드에 리졸버 적용 * refactor: 성적 관련 코드에 리졸버 적용 * refactor: S3 관련 코드에 리졸버 적용 * test: 사용자와 성적이 양방향 관계가 되도록 수정 - 이전에는 siteUser.email로 조회를 해와서 DB에 있는 내용이 바로 반영된 SiteUser를 대상으로 했기 때문에 siteUser.getGpaScoreList 나 siteUser.getLanguageTestScoreList를 했을 때 조회가 되었었다. - 하지만 SiteUser 객체 자체를 넘기도록 테스트가 바뀌었고, 테스트 코드에서 Transactional 사용하는 것을 지양하기 위해서 양방향을 명시적으로 걸어주도록 코드를 수정했다. --- .../controller/ApplicationController.java | 27 +-- .../service/ApplicationQueryService.java | 13 +- .../service/ApplicationSubmissionService.java | 4 +- .../auth/controller/AuthController.java | 32 ++-- .../auth/service/AuthService.java | 7 +- .../auth/service/SignInService.java | 21 ++- .../auth/service/SignUpService.java | 8 +- .../auth/service/TokenProvider.java | 12 +- .../comment/controller/CommentController.java | 25 ++- .../comment/service/CommentService.java | 29 ++-- .../security/AuthenticationManagerConfig.java | 25 +++ .../config/security/JwtAuthentication.java | 29 ---- .../security/JwtAuthenticationFilter.java | 49 ------ .../security/SecurityConfiguration.java | 3 + .../config/web/WebMvcConfig.java | 27 +++ .../custom/resolver/AuthorizedUser.java | 11 ++ .../resolver/AuthorizedUserResolver.java | 38 +++++ .../custom/resolver/ExpiredToken.java | 11 ++ .../custom/resolver/ExpiredTokenResolver.java | 34 ++++ .../ExpiredTokenAuthentication.java | 18 ++ .../authentication/JwtAuthentication.java | 30 ++++ .../SiteUserAuthentication.java | 16 ++ .../filter}/ExceptionHandlerFilter.java | 2 +- .../filter/JwtAuthenticationFilter.java | 55 ++++++ .../security/filter}/SignOutCheckFilter.java | 3 +- .../ExpiredTokenAuthenticationProvider.java | 34 ++++ .../SiteUserAuthenticationProvider.java | 37 ++++ .../userdetails/SiteUserDetails.java} | 15 +- .../userdetails/SiteUserDetailsService.java | 48 ++++++ .../post/controller/PostController.java | 50 +++--- .../post/service/PostCommandService.java | 17 +- .../post/service/PostLikeService.java | 8 +- .../post/service/PostQueryService.java | 15 +- .../solidconnection/s3/S3Controller.java | 19 ++- .../example/solidconnection/s3/S3Service.java | 9 +- .../score/controller/ScoreController.java | 30 ++-- .../score/service/ScoreService.java | 15 +- .../controller/SiteUserController.java | 36 ++-- .../repository/LikedUniversityRepository.java | 4 +- .../repository/SiteUserRepository.java | 13 +- .../siteuser/service/SiteUserService.java | 22 +-- .../controller/UniversityController.java | 43 ++--- .../service/UniversityLikeService.java | 8 +- .../service/UniversityRecommendService.java | 5 +- .../solidconnection/util/RedisUtils.java | 4 +- .../service/ApplicationQueryServiceTest.java | 14 +- .../ApplicationSubmissionServiceTest.java | 12 +- .../comment/service/CommentServiceTest.java | 26 +-- .../PostLikeCountConcurrencyTest.java | 24 +-- .../PostViewCountConcurrencyTest.java | 8 +- .../concurrency/ThunderingHerdTest.java | 6 +- .../security/JwtAuthenticationFilterTest.java | 126 -------------- .../resolver/AuthorizedUserResolverTest.java | 67 ++++++++ .../resolver/ExpiredTokenResolverTest.java | 43 +++++ .../ExpiredTokenAuthenticationTest.java | 64 +++++++ .../SiteUserAuthenticationTest.java | 73 ++++++++ .../filter}/ExceptionHandlerFilterTest.java | 2 +- .../filter/JwtAuthenticationFilterTest.java | 116 +++++++++++++ .../filter}/SignOutCheckFilterTest.java | 3 +- ...xpiredTokenAuthenticationProviderTest.java | 80 +++++++++ .../SiteUserAuthenticationProviderTest.java | 159 ++++++++++++++++++ .../SiteUserDetailsServiceTest.java | 104 ++++++++++++ .../e2e/ApplicantsQueryTest.java | 43 +++-- .../solidconnection/e2e/MyPageTest.java | 20 ++- .../solidconnection/e2e/MyPageUpdateTest.java | 17 +- .../solidconnection/e2e/SignInTest.java | 11 +- .../solidconnection/e2e/SignUpTest.java | 5 +- .../e2e/UniversityDetailTest.java | 4 +- .../e2e/UniversityLikeTest.java | 12 +- .../e2e/UniversityRecommendTest.java | 4 +- .../e2e/UniversitySearchTest.java | 18 +- .../post/service/PostCommandServiceTest.java | 22 +-- .../post/service/PostLikeServiceTest.java | 12 +- .../post/service/PostQueryServiceTest.java | 4 +- .../score/service/ScoreServiceTest.java | 15 +- .../siteuser/service/SiteUserServiceTest.java | 26 +-- .../service/UniversityLikeServiceTest.java | 14 +- .../UniversityRecommendServiceTest.java | 8 +- 78 files changed, 1485 insertions(+), 638 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/config/security/AuthenticationManagerConfig.java delete mode 100644 src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java delete mode 100644 src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java create mode 100644 src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java create mode 100644 src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java create mode 100644 src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java create mode 100644 src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthentication.java rename src/main/java/com/example/solidconnection/{config/security => custom/security/filter}/ExceptionHandlerFilter.java (97%) create mode 100644 src/main/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilter.java rename src/main/java/com/example/solidconnection/{config/security => custom/security/filter}/SignOutCheckFilter.java (92%) create mode 100644 src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProvider.java rename src/main/java/com/example/solidconnection/{config/security/JwtUserDetails.java => custom/security/userdetails/SiteUserDetails.java} (63%) create mode 100644 src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsService.java delete mode 100644 src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolverTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthenticationTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthenticationTest.java rename src/test/java/com/example/solidconnection/{config/security => custom/security/filter}/ExceptionHandlerFilterTest.java (98%) create mode 100644 src/test/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilterTest.java rename src/test/java/com/example/solidconnection/{config/security => custom/security/filter}/SignOutCheckFilterTest.java (96%) create mode 100644 src/test/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index dce62235f..6d8c45fbf 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -5,6 +5,8 @@ import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.service.ApplicationQueryService; import com.example.solidconnection.application.service.ApplicationSubmissionService; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -16,8 +18,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RequiredArgsConstructor @RequestMapping("/application") @RestController @@ -29,9 +29,10 @@ public class ApplicationController { // 지원서 제출하기 api @PostMapping() public ResponseEntity apply( - Principal principal, - @Valid @RequestBody ApplyRequest applyRequest) { - boolean result = applicationSubmissionService.apply(principal.getName(), applyRequest); + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody ApplyRequest applyRequest + ) { + boolean result = applicationSubmissionService.apply(siteUser, applyRequest); return ResponseEntity .status(HttpStatus.OK) .body(new ApplicationSubmissionResponse(result)); @@ -39,20 +40,22 @@ public ResponseEntity apply( @GetMapping public ResponseEntity getApplicants( - Principal principal, + @AuthorizedUser SiteUser siteUser, @RequestParam(required = false, defaultValue = "") String region, - @RequestParam(required = false, defaultValue = "") String keyword) { - applicationQueryService.validateSiteUserCanViewApplicants(principal.getName()); - ApplicationsResponse result = applicationQueryService.getApplicants(principal.getName(), region, keyword); + @RequestParam(required = false, defaultValue = "") String keyword + ) { + applicationQueryService.validateSiteUserCanViewApplicants(siteUser); + ApplicationsResponse result = applicationQueryService.getApplicants(siteUser, region, keyword); return ResponseEntity .ok(result); } @GetMapping("/competitors") public ResponseEntity getApplicantsForUserCompetitors( - Principal principal) { - applicationQueryService.validateSiteUserCanViewApplicants(principal.getName()); - ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(principal.getName()); + @AuthorizedUser SiteUser siteUser + ) { + applicationQueryService.validateSiteUserCanViewApplicants(siteUser); + ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(siteUser); return ResponseEntity .ok(result); } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 68cf9c0aa..170d7cf13 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -8,7 +8,6 @@ import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; @@ -34,7 +33,6 @@ public class ApplicationQueryService { private final ApplicationRepository applicationRepository; private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final SiteUserRepository siteUserRepository; private final UniversityFilterRepositoryImpl universityFilterRepository; @Value("${university.term}") @@ -49,9 +47,7 @@ public class ApplicationQueryService { * */ @Transactional(readOnly = true) @ThunderingHerdCaching(key = "application:query:{1}:{2}", cacheManager = "customCacheManager", ttlSec = 86400) - public ApplicationsResponse getApplicants(String email, String regionCode, String keyword) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - + public ApplicationsResponse getApplicants(SiteUser siteUser, String regionCode, String keyword) { // 국가와 키워드와 지역을 통해 대학을 필터링한다. List universities = universityFilterRepository.findByRegionCodeAndKeywords(regionCode, List.of(keyword)); @@ -64,9 +60,7 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin } @Transactional(readOnly = true) - public ApplicationsResponse getApplicantsByUserApplications(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - + public ApplicationsResponse getApplicantsByUserApplications(SiteUser siteUser) { Application userLatestApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term); List userAppliedUniversities = Arrays.asList( Optional.ofNullable(userLatestApplication.getFirstChoiceUniversity()) @@ -91,8 +85,7 @@ public ApplicationsResponse getApplicantsByUserApplications(String email) { // 학기별로 상태가 관리된다. // 금학기에 지원이력이 있는 사용자만 지원정보를 확인할 수 있도록 한다. @Transactional(readOnly = true) - public void validateSiteUserCanViewApplicants(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public void validateSiteUserCanViewApplicants(SiteUser siteUser) { VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term).getVerifyStatus(); if (verifyStatus != VerifyStatus.APPROVED) { throw new CustomException(APPLICATION_NOT_APPROVED); diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index f82e9ad76..beb2f0cb0 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -38,7 +38,6 @@ public class ApplicationSubmissionService { private final ApplicationRepository applicationRepository; private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final SiteUserRepository siteUserRepository; private final GpaScoreRepository gpaScoreRepository; private final LanguageTestScoreRepository languageTestScoreRepository; @@ -48,8 +47,7 @@ public class ApplicationSubmissionService { // 학점 및 어학성적이 모두 유효한 경우에만 지원서 등록이 가능하다. // 기존에 있던 status field 우선 APRROVED로 입력시킨다. @Transactional - public boolean apply(String email, ApplyRequest applyRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) { UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest(); validateUniversityChoices(universityChoiceRequest); diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 5f48124dc..1f6415157 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -8,6 +8,10 @@ import com.example.solidconnection.auth.service.AuthService; import com.example.solidconnection.auth.service.SignInService; import com.example.solidconnection.auth.service.SignUpService; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.custom.resolver.ExpiredToken; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,8 +21,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RequiredArgsConstructor @RequestMapping("/auth") @RestController @@ -29,32 +31,42 @@ public class AuthController { private final SignInService signInService; @PostMapping("/kakao") - public ResponseEntity processKakaoOauth(@RequestBody KakaoCodeRequest kakaoCodeRequest) { + public ResponseEntity processKakaoOauth( + @RequestBody KakaoCodeRequest kakaoCodeRequest + ) { KakaoOauthResponse kakaoOauthResponse = signInService.signIn(kakaoCodeRequest); return ResponseEntity.ok(kakaoOauthResponse); } @PostMapping("/sign-up") - public ResponseEntity signUp(@Valid @RequestBody SignUpRequest signUpRequest) { + public ResponseEntity signUp( + @Valid @RequestBody SignUpRequest signUpRequest + ) { SignUpResponse signUpResponseDto = signUpService.signUp(signUpRequest); return ResponseEntity.ok(signUpResponseDto); } @PostMapping("/sign-out") - public ResponseEntity signOut(Principal principal) { - authService.signOut(principal.getName()); + public ResponseEntity signOut( + @ExpiredToken ExpiredTokenAuthentication expiredToken + ) { + authService.signOut(expiredToken.getToken()); return ResponseEntity.ok().build(); } @PatchMapping("/quit") - public ResponseEntity quit(Principal principal) { - authService.quit(principal.getName()); + public ResponseEntity quit( + @AuthorizedUser SiteUser siteUser + ) { + authService.quit(siteUser); return ResponseEntity.ok().build(); } @PostMapping("/reissue") - public ResponseEntity reissueToken(Principal principal) { - ReissueResponse reissueResponse = authService.reissue(principal.getName()); + public ResponseEntity reissueToken( + @ExpiredToken ExpiredTokenAuthentication expiredToken + ) { + ReissueResponse reissueResponse = authService.reissue(expiredToken.getSubject()); return ResponseEntity.ok(reissueResponse); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index e16044e97..aed6f922f 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -4,7 +4,6 @@ import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -23,11 +22,8 @@ @Service public class AuthService { - public static final String SIGN_OUT_VALUE = "signOut"; - private final RedisTemplate redisTemplate; private final TokenProvider tokenProvider; - private final SiteUserRepository siteUserRepository; /* * 로그아웃 한다. @@ -48,8 +44,7 @@ public void signOut(String accessToken) { * - e.g. 2024-01-01 18:00 탈퇴 시, 2024-01-02 00:00 가 탈퇴일이 된다. * */ @Transactional - public void quit(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public void quit(SiteUser siteUser) { LocalDate tomorrow = LocalDate.now().plusDays(1); siteUser.setQuitedAt(tomorrow); } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index 2cd356d73..ae4947596 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -7,12 +7,15 @@ import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @RequiredArgsConstructor @Service public class SignInService { @@ -35,11 +38,12 @@ public class SignInService { public KakaoOauthResponse signIn(KakaoCodeRequest kakaoCodeRequest) { KakaoUserInfoDto kakaoUserInfoDto = kakaoOAuthClient.processOauth(kakaoCodeRequest.code()); String email = kakaoUserInfoDto.kakaoAccountDto().email(); - boolean isAlreadyRegistered = siteUserRepository.existsByEmail(email); + Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(email, AuthType.KAKAO); - if (isAlreadyRegistered) { - resetQuitedAt(email); - return getSignInInfo(email); + if (optionalSiteUser.isPresent()) { + SiteUser siteUser = optionalSiteUser.get(); + resetQuitedAt(siteUser); + return getSignInInfo(siteUser); } return getFirstAccessInfo(kakaoUserInfoDto); @@ -47,8 +51,7 @@ public KakaoOauthResponse signIn(KakaoCodeRequest kakaoCodeRequest) { // 계적 복구 기한이 지난 회원은 자정마다 삭제된다. (UserRemovalScheduler 참고) // 따라서 DB 에서 조회되었다면 아직 기한이 지나지 않았다는 뜻이므로, 탈퇴 날짜를 초기화한다. - private void resetQuitedAt(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + private void resetQuitedAt(SiteUser siteUser) { if (siteUser.getQuitedAt() == null) { return; } @@ -56,9 +59,9 @@ private void resetQuitedAt(String email) { siteUser.setQuitedAt(null); } - private SignInResponse getSignInInfo(String email) { - String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + private SignInResponse getSignInInfo(SiteUser siteUser) { + String accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignInResponse(true, accessToken, refreshToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java index 5cbd781eb..697cdbdc0 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -10,6 +10,7 @@ import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.Role; @@ -45,6 +46,7 @@ public class SignUpService { * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. * */ + // todo: 여러가지 가입 방법 적용해야 함 @Transactional public SignUpResponse signUp(SignUpRequest signUpRequest) { // 검증 @@ -62,14 +64,14 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { saveInterestedCountry(signUpRequest, savedSiteUser); // 토큰 발급 - String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + String accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignUpResponse(accessToken, refreshToken); } private void validateUserNotDuplicated(String email) { - if (siteUserRepository.existsByEmail(email)) { + if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.KAKAO)) { throw new CustomException(USER_ALREADY_EXISTED); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 9cba77c36..2dbf288ad 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -2,6 +2,7 @@ import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.siteuser.domain.SiteUser; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -12,8 +13,8 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; @RequiredArgsConstructor @Component @@ -22,8 +23,13 @@ public class TokenProvider { private final RedisTemplate redisTemplate; private final JwtProperties jwtProperties; - public String generateToken(String email, TokenType tokenType) { - Claims claims = Jwts.claims().setSubject(email); + public String generateToken(SiteUser siteUser, TokenType tokenType) { + String subject = siteUser.getId().toString(); + return generateToken(subject, tokenType); + } + + public String generateToken(String string, TokenType tokenType) { + Claims claims = Jwts.claims().setSubject(string); Date now = new Date(); Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); return Jwts.builder() diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java index a7eaab252..fda360b4a 100644 --- a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java +++ b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java @@ -6,6 +6,8 @@ import com.example.solidconnection.comment.dto.CommentUpdateRequest; import com.example.solidconnection.comment.dto.CommentUpdateResponse; import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,8 +19,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RestController @RequiredArgsConstructor @RequestMapping("/posts") @@ -28,35 +28,32 @@ public class CommentController { @PostMapping("/{post_id}/comments") public ResponseEntity createComment( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("post_id") Long postId, @Valid @RequestBody CommentCreateRequest commentCreateRequest ) { - CommentCreateResponse commentCreateResponse = commentService.createComment( - principal.getName(), postId, commentCreateRequest); - return ResponseEntity.ok().body(commentCreateResponse); + CommentCreateResponse response = commentService.createComment(siteUser, postId, commentCreateRequest); + return ResponseEntity.ok().body(response); } @PatchMapping("/{post_id}/comments/{comment_id}") public ResponseEntity updateComment( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("post_id") Long postId, @PathVariable("comment_id") Long commentId, @Valid @RequestBody CommentUpdateRequest commentUpdateRequest ) { - CommentUpdateResponse commentUpdateResponse = commentService.updateComment( - principal.getName(), postId, commentId, commentUpdateRequest - ); - return ResponseEntity.ok().body(commentUpdateResponse); + CommentUpdateResponse response = commentService.updateComment(siteUser, postId, commentId, commentUpdateRequest); + return ResponseEntity.ok().body(response); } @DeleteMapping("/{post_id}/comments/{comment_id}") public ResponseEntity deleteCommentById( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("post_id") Long postId, @PathVariable("comment_id") Long commentId ) { - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById(principal.getName(), postId, commentId); - return ResponseEntity.ok().body(commentDeleteResponse); + CommentDeleteResponse response = commentService.deleteCommentById(siteUser, postId, commentId); + return ResponseEntity.ok().body(response); } } diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 7d25ee5f6..b7c1c6068 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -12,7 +12,6 @@ import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,25 +28,22 @@ public class CommentService { private final CommentRepository commentRepository; - private final SiteUserRepository siteUserRepository; private final PostRepository postRepository; @Transactional(readOnly = true) - public List findCommentsByPostId(String email, Long postId) { + public List findCommentsByPostId(SiteUser siteUser, Long postId) { return commentRepository.findCommentTreeByPostId(postId) .stream() - .map(comment -> PostFindCommentResponse.from(isOwner(comment, email), comment)) + .map(comment -> PostFindCommentResponse.from(isOwner(comment, siteUser), comment)) .collect(Collectors.toList()); } - private Boolean isOwner(Comment comment, String email) { - return comment.getSiteUser().getEmail().equals(email); + private Boolean isOwner(Comment comment, SiteUser siteUser) { + return comment.getSiteUser().getId().equals(siteUser.getId()); } @Transactional - public CommentCreateResponse createComment(String email, Long postId, CommentCreateRequest commentCreateRequest) { - - SiteUser siteUser = siteUserRepository.getByEmail(email); + public CommentCreateResponse createComment(SiteUser siteUser, Long postId, CommentCreateRequest commentCreateRequest) { Post post = postRepository.getById(postId); Comment parentComment = null; @@ -68,13 +64,11 @@ private void validateCommentDepth(Comment parentComment) { } @Transactional - public CommentUpdateResponse updateComment(String email, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { - - SiteUser siteUser = siteUserRepository.getByEmail(email); + public CommentUpdateResponse updateComment(SiteUser siteUser, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { Post post = postRepository.getById(postId); Comment comment = commentRepository.getById(commentId); validateDeprecated(comment); - validateOwnership(comment, email); + validateOwnership(comment, siteUser); comment.updateContent(commentUpdateRequest.content()); @@ -88,11 +82,10 @@ private void validateDeprecated(Comment comment) { } @Transactional - public CommentDeleteResponse deleteCommentById(String email, Long postId, Long commentId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public CommentDeleteResponse deleteCommentById(SiteUser siteUser, Long postId, Long commentId) { Post post = postRepository.getById(postId); Comment comment = commentRepository.getById(commentId); - validateOwnership(comment, email); + validateOwnership(comment, siteUser); if (comment.getParentComment() != null) { // 대댓글인 경우 @@ -119,8 +112,8 @@ public CommentDeleteResponse deleteCommentById(String email, Long postId, Long c return new CommentDeleteResponse(commentId); } - private void validateOwnership(Comment comment, String email) { - if (!comment.getSiteUser().getEmail().equals(email)) { + private void validateOwnership(Comment comment, SiteUser siteUser) { + if (!comment.getSiteUser().getId().equals(siteUser.getId())) { throw new CustomException(INVALID_POST_ACCESS); } } diff --git a/src/main/java/com/example/solidconnection/config/security/AuthenticationManagerConfig.java b/src/main/java/com/example/solidconnection/config/security/AuthenticationManagerConfig.java new file mode 100644 index 000000000..785283d7d --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/AuthenticationManagerConfig.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.security.provider.ExpiredTokenAuthenticationProvider; +import com.example.solidconnection.custom.security.provider.SiteUserAuthenticationProvider; +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.authentication.ProviderManager; + +@RequiredArgsConstructor +@Configuration +public class AuthenticationManagerConfig { + + private final SiteUserAuthenticationProvider siteUserAuthenticationProvider; + private final ExpiredTokenAuthenticationProvider expiredTokenAuthenticationProvider; + + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager( + siteUserAuthenticationProvider, + expiredTokenAuthenticationProvider + ); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java deleted file mode 100644 index 84692709a..000000000 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.solidconnection.config.security; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; - -import java.util.Collection; - -public class JwtAuthentication extends AbstractAuthenticationToken { - - private final String token; - private final Object principal; - - public JwtAuthentication(Object principal, String token, Collection authorities) { - super(authorities); - this.token = token; - this.principal = principal; - setAuthenticated(true); - } - - @Override - public Object getCredentials() { - return this.token; - } - - @Override - public Object getPrincipal() { - return this.principal; - } -} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java deleted file mode 100644 index 5c7ab9f97..000000000 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.solidconnection.config.security; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; - -@Component -@RequiredArgsConstructor -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private static final String REISSUE_URI = "/auth/reissue"; - private static final String REISSUE_METHOD = "post"; - - private final JwtProperties jwtProperties; - - @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) throws ServletException, IOException { - String token = parseTokenFromRequest(request); - if (token == null || isReissueRequest(request)) { - filterChain.doFilter(request, response); - return; - } - - String subject = parseSubject(token, jwtProperties.secret()); - UserDetails userDetails = new JwtUserDetails(subject); - Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - } - - private boolean isReissueRequest(HttpServletRequest request) { - return REISSUE_URI.equals(request.getRequestURI()) && REISSUE_METHOD.equals(request.getMethod()); - } -} diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 3f6307f8f..6851b3e8c 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -1,5 +1,8 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.security.filter.ExceptionHandlerFilter; +import com.example.solidconnection.custom.security.filter.JwtAuthenticationFilter; +import com.example.solidconnection.custom.security.filter.SignOutCheckFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java b/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java new file mode 100644 index 000000000..10e468f56 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.config.web; + + +import com.example.solidconnection.custom.resolver.AuthorizedUserResolver; +import com.example.solidconnection.custom.resolver.ExpiredTokenResolver; +import lombok.RequiredArgsConstructor; +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 +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final AuthorizedUserResolver authorizedUserResolver; + private final ExpiredTokenResolver expiredTokenResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.addAll(List.of( + authorizedUserResolver, + expiredTokenResolver + )); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java new file mode 100644 index 000000000..b14d80994 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.custom.resolver; + +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 AuthorizedUser { +} diff --git a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java new file mode 100644 index 000000000..93707b007 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java @@ -0,0 +1,38 @@ +package com.example.solidconnection.custom.resolver; + +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +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 +@RequiredArgsConstructor +public class AuthorizedUserResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthorizedUser.class) + && parameter.getParameterType().equals(SiteUser.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + try { + SiteUserDetails principal = (SiteUserDetails) SecurityContextHolder.getContext() + .getAuthentication() + .getPrincipal(); + return principal.getSiteUser(); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java new file mode 100644 index 000000000..61abff98c --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.custom.resolver; + +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 ExpiredToken { +} diff --git a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java new file mode 100644 index 000000000..691136438 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.custom.resolver; + +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +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 +@RequiredArgsConstructor +public class ExpiredTokenResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(ExpiredToken.class) + && parameter.getParameterType().equals(ExpiredTokenAuthentication.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + try { + return SecurityContextHolder.getContext().getAuthentication(); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java new file mode 100644 index 000000000..811ea6a1b --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.custom.security.authentication; + +public class ExpiredTokenAuthentication extends JwtAuthentication { + + public ExpiredTokenAuthentication(String token) { + super(token, null); + setAuthenticated(false); + } + + public ExpiredTokenAuthentication(String token, String subject) { + super(token, subject); + setAuthenticated(false); + } + + public String getSubject() { + return (String) getPrincipal(); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java new file mode 100644 index 000000000..ba195caff --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.custom.security.authentication; + +import org.springframework.security.authentication.AbstractAuthenticationToken; + +public abstract class JwtAuthentication extends AbstractAuthenticationToken { + + private final String credentials; + + private final Object principal; + + public JwtAuthentication(String token, Object principal) { + super(null); + this.credentials = token; + this.principal = principal; + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + public final String getToken() { + return (String) getCredentials(); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthentication.java new file mode 100644 index 000000000..3387cee55 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthentication.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.custom.security.authentication; + +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; + +public class SiteUserAuthentication extends JwtAuthentication { + + public SiteUserAuthentication(String token) { + super(token, null); + setAuthenticated(false); + } + + public SiteUserAuthentication(String token, SiteUserDetails principal) { + super(token, principal); + setAuthenticated(true); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java similarity index 97% rename from src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java rename to src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java index 59022c198..8d09bfada 100644 --- a/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.filter; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.ErrorResponse; diff --git a/src/main/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 000000000..3f5bce556 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,55 @@ +package com.example.solidconnection.custom.security.filter; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.custom.security.authentication.JwtAuthentication; +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.example.solidconnection.util.JwtUtils.isExpired; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; + + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtProperties jwtProperties; + private final AuthenticationManager authenticationManager; + + @Override + public void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseTokenFromRequest(request); + if (token == null) { + filterChain.doFilter(request, response); + return; + } + + JwtAuthentication authToken = createAuthentication(token); + Authentication auth = authenticationManager.authenticate(authToken); + SecurityContextHolder.getContext().setAuthentication(auth); + + filterChain.doFilter(request, response); + } + + private JwtAuthentication createAuthentication(String token) { + if (isExpired(token, jwtProperties.secret())) { + return new ExpiredTokenAuthentication(token); + } + return new SiteUserAuthentication(token); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java similarity index 92% rename from src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java rename to src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java index c71252f1f..90fb6866e 100644 --- a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java @@ -1,5 +1,6 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.filter; +import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java b/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java new file mode 100644 index 000000000..d7461a0e6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.custom.security.provider; + + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.custom.security.authentication.JwtAuthentication; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; + +@Component +@RequiredArgsConstructor +public class ExpiredTokenAuthenticationProvider implements AuthenticationProvider { + + private final JwtProperties jwtProperties; + + @Override + public Authentication authenticate(Authentication auth) throws AuthenticationException { + JwtAuthentication jwtAuth = (JwtAuthentication) auth; + String token = jwtAuth.getToken(); + String subject = parseSubjectIgnoringExpiration(token, jwtProperties.secret()); + + return new ExpiredTokenAuthentication(token, subject); + } + + @Override + public boolean supports(Class authentication) { + return ExpiredTokenAuthentication.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProvider.java b/src/main/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProvider.java new file mode 100644 index 000000000..25f211710 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProvider.java @@ -0,0 +1,37 @@ +package com.example.solidconnection.custom.security.provider; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetailsService; +import com.example.solidconnection.custom.security.authentication.JwtAuthentication; +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.util.JwtUtils.parseSubject; + +@Component +@RequiredArgsConstructor +public class SiteUserAuthenticationProvider implements AuthenticationProvider { + + private final JwtProperties jwtProperties; + private final SiteUserDetailsService siteUserDetailsService; + + @Override + public Authentication authenticate(Authentication auth) throws AuthenticationException { + JwtAuthentication jwtAuth = (JwtAuthentication) auth; + String token = jwtAuth.getToken(); + + String username = parseSubject(token, jwtProperties.secret()); + SiteUserDetails userDetails = (SiteUserDetails) siteUserDetailsService.loadUserByUsername(username); + return new SiteUserAuthentication(token, userDetails); + } + + @Override + public boolean supports(Class authentication) { + return SiteUserAuthentication.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java similarity index 63% rename from src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java rename to src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java index b3bbda5fa..36a0b815a 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java +++ b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java @@ -1,16 +1,23 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.userdetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import lombok.Getter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; -public class JwtUserDetails implements UserDetails { +public class SiteUserDetails implements UserDetails { + // userDetails 에서 userName 은 사용자 식별자를 의미함 private final String userName; - public JwtUserDetails(String userName) { - this.userName = userName; + @Getter + private final SiteUser siteUser; + + public SiteUserDetails(SiteUser siteUser) { + this.siteUser = siteUser; + this.userName = String.valueOf(siteUser.getId()); } @Override diff --git a/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsService.java b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsService.java new file mode 100644 index 000000000..fd23fa899 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsService.java @@ -0,0 +1,48 @@ +package com.example.solidconnection.custom.security.userdetails; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +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 static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; + +@Service +@RequiredArgsConstructor +public class SiteUserDetailsService implements UserDetailsService { + + private final SiteUserRepository siteUserRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + long siteUserId = getSiteUserId(username); + SiteUser siteUser = getSiteUser(siteUserId); + validateNotQuit(siteUser); + + return new SiteUserDetails(siteUser); + } + + private long getSiteUserId(String username) { + try { + return Long.parseLong(username); + } catch (NumberFormatException e) { + throw new CustomException(INVALID_TOKEN, "인증 정보가 지정된 형식과 일치하지 않습니다."); + } + } + + private SiteUser getSiteUser(long siteUserId) { + return siteUserRepository.findById(siteUserId) + .orElseThrow(() -> new CustomException(AUTHENTICATION_FAILED, "인증 정보에 해당하는 사용자를 찾을 수 없습니다.")); + } + + private void validateNotQuit(SiteUser siteUser) { + if (siteUser.getQuitedAt() != null) { + throw new CustomException(AUTHENTICATION_FAILED, "탈퇴한 사용자입니다."); + } + } +} diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index c3ff3ce3a..bc3f9d123 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -1,5 +1,6 @@ package com.example.solidconnection.post.controller; +import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.post.dto.PostCreateRequest; import com.example.solidconnection.post.dto.PostCreateResponse; import com.example.solidconnection.post.dto.PostDeleteResponse; @@ -11,6 +12,7 @@ import com.example.solidconnection.post.service.PostCommandService; import com.example.solidconnection.post.service.PostLikeService; import com.example.solidconnection.post.service.PostQueryService; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -25,7 +27,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.security.Principal; import java.util.Collections; import java.util.List; @@ -40,75 +41,72 @@ public class PostController { @PostMapping(value = "/{code}/posts") public ResponseEntity createPost( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, @Valid @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, - @RequestParam(value = "file", required = false) List imageFile) { - + @RequestParam(value = "file", required = false) List imageFile + ) { if (imageFile == null) { imageFile = Collections.emptyList(); } - PostCreateResponse post = postCommandService - .createPost(principal.getName(), code, postCreateRequest, imageFile); + PostCreateResponse post = postCommandService.createPost(siteUser, code, postCreateRequest, imageFile); return ResponseEntity.ok().body(post); } @PatchMapping(value = "/{code}/posts/{post_id}") public ResponseEntity updatePost( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, @PathVariable("post_id") Long postId, @Valid @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, - @RequestParam(value = "file", required = false) List imageFile) { - + @RequestParam(value = "file", required = false) List imageFile + ) { if (imageFile == null) { imageFile = Collections.emptyList(); } - PostUpdateResponse postUpdateResponse = postCommandService - .updatePost(principal.getName(), code, postId, postUpdateRequest, imageFile); + PostUpdateResponse postUpdateResponse = postCommandService.updatePost( + siteUser, code, postId, postUpdateRequest, imageFile + ); return ResponseEntity.ok().body(postUpdateResponse); } @GetMapping("/{code}/posts/{post_id}") public ResponseEntity findPostById( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, - @PathVariable("post_id") Long postId) { - - PostFindResponse postFindResponse = postQueryService - .findPostById(principal.getName(), code, postId); + @PathVariable("post_id") Long postId + ) { + PostFindResponse postFindResponse = postQueryService.findPostById(siteUser, code, postId); return ResponseEntity.ok().body(postFindResponse); } @DeleteMapping(value = "/{code}/posts/{post_id}") public ResponseEntity deletePostById( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, - @PathVariable("post_id") Long postId) { - - PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(principal.getName(), code, postId); + @PathVariable("post_id") Long postId + ) { + PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(siteUser, code, postId); return ResponseEntity.ok().body(postDeleteResponse); } @PostMapping(value = "/{code}/posts/{post_id}/like") public ResponseEntity likePost( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, @PathVariable("post_id") Long postId ) { - - PostLikeResponse postLikeResponse = postLikeService.likePost(principal.getName(), code, postId); + PostLikeResponse postLikeResponse = postLikeService.likePost(siteUser, code, postId); return ResponseEntity.ok().body(postLikeResponse); } @DeleteMapping(value = "/{code}/posts/{post_id}/like") public ResponseEntity dislikePost( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, @PathVariable("post_id") Long postId ) { - - PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(principal.getName(), code, postId); + PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(siteUser, code, postId); return ResponseEntity.ok().body(postDislikeResponse); } } diff --git a/src/main/java/com/example/solidconnection/post/service/PostCommandService.java b/src/main/java/com/example/solidconnection/post/service/PostCommandService.java index 7b0c4f937..74eb86310 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostCommandService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostCommandService.java @@ -15,7 +15,6 @@ import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.ImgType; import com.example.solidconnection.type.PostCategory; @@ -39,14 +38,13 @@ public class PostCommandService { private final PostRepository postRepository; - private final SiteUserRepository siteUserRepository; private final BoardRepository boardRepository; private final S3Service s3Service; private final RedisService redisService; private final RedisUtils redisUtils; @Transactional - public PostCreateResponse createPost(String email, String code, PostCreateRequest postCreateRequest, + public PostCreateResponse createPost(SiteUser siteUser, String code, PostCreateRequest postCreateRequest, List imageFile) { // 유효성 검증 String boardCode = validateCode(code); @@ -54,7 +52,6 @@ public PostCreateResponse createPost(String email, String code, PostCreateReques validateFileSize(imageFile); // 객체 생성 - SiteUser siteUser = siteUserRepository.getByEmail(email); Board board = boardRepository.getByCode(boardCode); Post post = postCreateRequest.toEntity(siteUser, board); // 이미지 처리 @@ -65,12 +62,12 @@ public PostCreateResponse createPost(String email, String code, PostCreateReques } @Transactional - public PostUpdateResponse updatePost(String email, String code, Long postId, PostUpdateRequest postUpdateRequest, + public PostUpdateResponse updatePost(SiteUser siteUser, String code, Long postId, PostUpdateRequest postUpdateRequest, List imageFile) { // 유효성 검증 String boardCode = validateCode(code); Post post = postRepository.getById(postId); - validateOwnership(post, email); + validateOwnership(post, siteUser); validateQuestion(post); validateFileSize(imageFile); @@ -96,10 +93,10 @@ private void savePostImages(List imageFile, Post post) { } @Transactional - public PostDeleteResponse deletePostById(String email, String code, Long postId) { + public PostDeleteResponse deletePostById(SiteUser siteUser, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getById(postId); - validateOwnership(post, email); + validateOwnership(post, siteUser); validateQuestion(post); removePostImages(post); @@ -119,8 +116,8 @@ private String validateCode(String code) { } } - private void validateOwnership(Post post, String email) { - if (!post.getSiteUser().getEmail().equals(email)) { + private void validateOwnership(Post post, SiteUser siteUser) { + if (!post.getSiteUser().getId().equals(siteUser.getId())) { throw new CustomException(INVALID_POST_ACCESS); } } diff --git a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java b/src/main/java/com/example/solidconnection/post/service/PostLikeService.java index 8a72d5f9f..5aaf994c7 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostLikeService.java @@ -8,7 +8,6 @@ import com.example.solidconnection.post.repository.PostLikeRepository; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,14 +22,12 @@ public class PostLikeService { private final PostRepository postRepository; - private final SiteUserRepository siteUserRepository; private final PostLikeRepository postLikeRepository; @Transactional(isolation = Isolation.READ_COMMITTED) - public PostLikeResponse likePost(String email, String code, Long postId) { + public PostLikeResponse likePost(SiteUser siteUser, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getById(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); validateDuplicatePostLike(post, siteUser); PostLike postLike = new PostLike(); @@ -42,10 +39,9 @@ public PostLikeResponse likePost(String email, String code, Long postId) { } @Transactional(isolation = Isolation.READ_COMMITTED) - public PostDislikeResponse dislikePost(String email, String code, Long postId) { + public PostDislikeResponse dislikePost(SiteUser siteUser, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getById(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); postLike.resetPostAndSiteUser(); diff --git a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/post/service/PostQueryService.java index d53470124..a45ca3968 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostQueryService.java @@ -12,7 +12,6 @@ import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; @@ -28,28 +27,26 @@ public class PostQueryService { private final PostRepository postRepository; - private final SiteUserRepository siteUserRepository; private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; private final PostLikeRepository postLikeRepository; @Transactional(readOnly = true) - public PostFindResponse findPostById(String email, String code, Long postId) { + public PostFindResponse findPostById(SiteUser siteUser, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getByIdUsingEntityGraph(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); - Boolean isOwner = getIsOwner(post, email); + Boolean isOwner = getIsOwner(post, siteUser); Boolean isLiked = getIsLiked(post, siteUser); PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); List postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList()); - List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); + List commentFindResultDTOList = commentService.findCommentsByPostId(siteUser, postId); // caching && 어뷰징 방지 - if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email, postId))) { + if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), postId))) { redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); } @@ -65,8 +62,8 @@ private String validateCode(String code) { } } - private Boolean getIsOwner(Post post, String email) { - return post.getSiteUser().getEmail().equals(email); + private Boolean getIsOwner(Post post, SiteUser siteUser) { + return post.getSiteUser().getId().equals(siteUser.getId()); } private Boolean getIsLiked(Post post, SiteUser siteUser) { diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java index 0f32a4ab6..26f9160c0 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -1,5 +1,7 @@ package com.example.solidconnection.s3; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.ImgType; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -11,8 +13,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.security.Principal; - @RequiredArgsConstructor @RequestMapping("/file") @RestController @@ -34,29 +34,34 @@ public class S3Controller { @PostMapping("/profile/pre") public ResponseEntity uploadPreProfileImage( - @RequestParam("file") MultipartFile imageFile) { + @RequestParam("file") MultipartFile imageFile + ) { UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); return ResponseEntity.ok(profileImageUrl); } @PostMapping("/profile/post") public ResponseEntity uploadPostProfileImage( - @RequestParam("file") MultipartFile imageFile, Principal principal) { + @AuthorizedUser SiteUser siteUser, + @RequestParam("file") MultipartFile imageFile + ) { UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); - s3Service.deleteExProfile(principal.getName()); + s3Service.deleteExProfile(siteUser); return ResponseEntity.ok(profileImageUrl); } @PostMapping("/gpa") public ResponseEntity uploadGpaImage( - @RequestParam("file") MultipartFile imageFile) { + @RequestParam("file") MultipartFile imageFile + ) { UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA); return ResponseEntity.ok(profileImageUrl); } @PostMapping("/language-test") public ResponseEntity uploadLanguageImage( - @RequestParam("file") MultipartFile imageFile) { + @RequestParam("file") MultipartFile imageFile + ) { UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST); return ResponseEntity.ok(profileImageUrl); } diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 049be9fa3..2f3c633dd 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -109,8 +109,8 @@ private String getFileExtension(String fileName) { * - 기존 파일의 key(S3파일명)를 찾는다. * - S3에서 파일을 삭제한다. * */ - public void deleteExProfile(String email) { - String key = getExProfileImageUrl(email); + public void deleteExProfile(SiteUser siteUser) { + String key = siteUser.getProfileImageUrl(); deleteFile(key); } @@ -129,9 +129,4 @@ private void deleteFile(String fileName) { throw new CustomException(S3_CLIENT_EXCEPTION); } } - - private String getExProfileImageUrl(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - return siteUser.getProfileImageUrl(); - } } diff --git a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java index 42ee7b009..6c54ab5fe 100644 --- a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java +++ b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java @@ -1,10 +1,12 @@ package com.example.solidconnection.score.controller; +import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.score.dto.GpaScoreRequest; import com.example.solidconnection.score.dto.GpaScoreStatusResponse; import com.example.solidconnection.score.dto.LanguageTestScoreRequest; import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; import com.example.solidconnection.score.service.ScoreService; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -14,8 +16,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RestController @RequestMapping("/score") @RequiredArgsConstructor @@ -26,32 +26,38 @@ public class ScoreController { // 학점을 등록하는 api @PostMapping("/gpa") public ResponseEntity submitGpaScore( - Principal principal, - @Valid @RequestBody GpaScoreRequest gpaScoreRequest) { - Long id = scoreService.submitGpaScore(principal.getName(), gpaScoreRequest); + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody GpaScoreRequest gpaScoreRequest + ) { + Long id = scoreService.submitGpaScore(siteUser, gpaScoreRequest); return ResponseEntity.ok(id); } // 어학성적을 등록하는 api @PostMapping("/languageTest") public ResponseEntity submitLanguageTestScore( - Principal principal, - @Valid @RequestBody LanguageTestScoreRequest languageTestScoreRequest) { - Long id = scoreService.submitLanguageTestScore(principal.getName(), languageTestScoreRequest); + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody LanguageTestScoreRequest languageTestScoreRequest + ) { + Long id = scoreService.submitLanguageTestScore(siteUser, languageTestScoreRequest); return ResponseEntity.ok(id); } // 학점 상태를 확인하는 api @GetMapping("/gpa") - public ResponseEntity getGpaScoreStatus(Principal principal) { - GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(principal.getName()); + public ResponseEntity getGpaScoreStatus( + @AuthorizedUser SiteUser siteUser + ) { + GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser); return ResponseEntity.ok(gpaScoreStatus); } // 어학 성적 상태를 확인하는 api @GetMapping("/languageTest") - public ResponseEntity getLanguageTestScoreStatus(Principal principal) { - LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(principal.getName()); + public ResponseEntity getLanguageTestScoreStatus( + @AuthorizedUser SiteUser siteUser + ) { + LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser); return ResponseEntity.ok(languageTestScoreStatus); } } diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index d09038fa5..e6c9d5c6e 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -12,7 +12,6 @@ import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,12 +27,9 @@ public class ScoreService { private final GpaScoreRepository gpaScoreRepository; private final LanguageTestScoreRepository languageTestScoreRepository; - private final SiteUserRepository siteUserRepository; @Transactional - public Long submitGpaScore(String email, GpaScoreRequest gpaScoreRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - + public Long submitGpaScore(SiteUser siteUser, GpaScoreRequest gpaScoreRequest) { GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser, gpaScoreRequest.issueDate()); newGpaScore.setSiteUser(siteUser); GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); // 저장 후 반환된 객체 @@ -41,8 +37,7 @@ public Long submitGpaScore(String email, GpaScoreRequest gpaScoreRequest) { } @Transactional - public Long submitLanguageTestScore(String email, LanguageTestScoreRequest languageTestScoreRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public Long submitLanguageTestScore(SiteUser siteUser, LanguageTestScoreRequest languageTestScoreRequest) { LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); LanguageTestScore newScore = new LanguageTestScore( @@ -53,8 +48,7 @@ public Long submitLanguageTestScore(String email, LanguageTestScoreRequest langu } @Transactional(readOnly = true) - public GpaScoreStatusResponse getGpaScoreStatus(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public GpaScoreStatusResponse getGpaScoreStatus(SiteUser siteUser) { List gpaScoreStatusList = Optional.ofNullable(siteUser.getGpaScoreList()) .map(scores -> scores.stream() @@ -65,8 +59,7 @@ public GpaScoreStatusResponse getGpaScoreStatus(String email) { } @Transactional(readOnly = true) - public LanguageTestScoreStatusResponse getLanguageTestScoreStatus(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public LanguageTestScoreStatusResponse getLanguageTestScoreStatus(SiteUser siteUser) { List languageTestScoreStatusList = Optional.ofNullable(siteUser.getLanguageTestScoreList()) .map(scores -> scores.stream() diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java index c0d58356f..11c154243 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -1,5 +1,7 @@ package com.example.solidconnection.siteuser.controller; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; @@ -17,8 +19,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.security.Principal; - @RequiredArgsConstructor @RequestMapping("/my-page") @RestController @@ -27,32 +27,36 @@ class SiteUserController { private final SiteUserService siteUserService; @GetMapping - public ResponseEntity getMyPageInfo(Principal principal) { - MyPageResponse myPageResponse = siteUserService.getMyPageInfo(principal.getName()); - return ResponseEntity - .ok(myPageResponse); + public ResponseEntity getMyPageInfo( + @AuthorizedUser SiteUser siteUser + ) { + MyPageResponse myPageResponse = siteUserService.getMyPageInfo(siteUser); + return ResponseEntity.ok(myPageResponse); } @GetMapping("/update") - public ResponseEntity getMyPageInfoToUpdate(Principal principal) { - MyPageUpdateResponse myPageUpdateDto = siteUserService.getMyPageInfoToUpdate(principal.getName()); - return ResponseEntity - .ok(myPageUpdateDto); + public ResponseEntity getMyPageInfoToUpdate( + @AuthorizedUser SiteUser siteUser + ) { + MyPageUpdateResponse myPageUpdateDto = siteUserService.getMyPageInfoToUpdate(siteUser); + return ResponseEntity.ok(myPageUpdateDto); } @PatchMapping("/update/profileImage") public ResponseEntity updateProfileImage( - Principal principal, - @RequestParam(value = "file", required = false) MultipartFile imageFile) { - ProfileImageUpdateResponse profileImageUpdateResponse = siteUserService.updateProfileImage(principal.getName(), imageFile); + @AuthorizedUser SiteUser siteUser, + @RequestParam(value = "file", required = false) MultipartFile imageFile + ) { + ProfileImageUpdateResponse profileImageUpdateResponse = siteUserService.updateProfileImage(siteUser, imageFile); return ResponseEntity.ok().body(profileImageUpdateResponse); } @PatchMapping("/update/nickname") public ResponseEntity updateNickname( - Principal principal, - @Valid @RequestBody NicknameUpdateRequest nicknameUpdateRequest) { - NicknameUpdateResponse nicknameUpdateResponse = siteUserService.updateNickname(principal.getName(), nicknameUpdateRequest); + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody NicknameUpdateRequest nicknameUpdateRequest + ) { + NicknameUpdateResponse nicknameUpdateResponse = siteUserService.updateNickname(siteUser, nicknameUpdateRequest); return ResponseEntity.ok().body(nicknameUpdateResponse); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java index c3793eb06..d15949723 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java @@ -10,9 +10,9 @@ public interface LikedUniversityRepository extends JpaRepository { - List findAllBySiteUser_Email(String email); + List findAllBySiteUser_Id(long siteUserId); - int countBySiteUser_Email(String email); + int countBySiteUser_Id(long siteUserId); Optional findBySiteUserAndUniversityInfoForApply(SiteUser siteUser, UniversityInfoForApply universityInfoForApply); } diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java index 6b77252c5..e0617f046 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java @@ -1,6 +1,6 @@ package com.example.solidconnection.siteuser.repository; -import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -11,22 +11,15 @@ import java.util.List; import java.util.Optional; -import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; - @Repository public interface SiteUserRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByEmailAndAuthType(String email, AuthType authType); - boolean existsByEmail(String email); + boolean existsByEmailAndAuthType(String email, AuthType authType); boolean existsByNickname(String nickname); @Query("SELECT u FROM SiteUser u WHERE u.quitedAt <= :cutoffDate") List findUsersToBeRemoved(@Param("cutoffDate") LocalDate cutoffDate); - - default SiteUser getByEmail(String email) { - return findByEmail(email) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - } } diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index a7a2e5d71..c181c2809 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -42,9 +42,8 @@ public class SiteUserService { * 마이페이지 정보를 조회한다. * */ @Transactional(readOnly = true) - public MyPageResponse getMyPageInfo(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - int likedUniversityCount = likedUniversityRepository.countBySiteUser_Email(email); + public MyPageResponse getMyPageInfo(SiteUser siteUser) { + int likedUniversityCount = likedUniversityRepository.countBySiteUser_Id(siteUser.getId()); return MyPageResponse.of(siteUser, likedUniversityCount); } @@ -52,8 +51,7 @@ public MyPageResponse getMyPageInfo(String email) { * 내 정보를 수정하기 위한 마이페이지 정보를 조회한다. (닉네임, 프로필 사진) * */ @Transactional(readOnly = true) - public MyPageUpdateResponse getMyPageInfoToUpdate(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public MyPageUpdateResponse getMyPageInfoToUpdate(SiteUser siteUser) { return MyPageUpdateResponse.from(siteUser); } @@ -61,9 +59,8 @@ public MyPageUpdateResponse getMyPageInfoToUpdate(String email) { * 관심 대학교 목록을 조회한다. * */ @Transactional(readOnly = true) - public List getWishUniversity(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - List likedUniversities = likedUniversityRepository.findAllBySiteUser_Email(siteUser.getEmail()); + public List getWishUniversity(SiteUser siteUser) { + List likedUniversities = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()); return likedUniversities.stream() .map(likedUniversity -> UniversityInfoForApplyPreviewResponse.from(likedUniversity.getUniversityInfoForApply())) .toList(); @@ -73,13 +70,12 @@ public List getWishUniversity(String emai * 프로필 이미지를 수정한다. * */ @Transactional - public ProfileImageUpdateResponse updateProfileImage(String email, MultipartFile imageFile) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public ProfileImageUpdateResponse updateProfileImage(SiteUser siteUser, MultipartFile imageFile) { validateProfileImage(imageFile); // 프로필 이미지를 처음 수정하는 경우에는 deleteExProfile 수행하지 않음 if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { - s3Service.deleteExProfile(email); + s3Service.deleteExProfile(siteUser); } UploadedFileUrlResponse uploadedFileUrlResponse = s3Service.uploadFile(imageFile, ImgType.PROFILE); siteUser.setProfileImageUrl(uploadedFileUrlResponse.fileUrl()); @@ -102,9 +98,7 @@ private boolean isDefaultProfileImage(String profileImageUrl) { * 닉네임을 수정한다. * */ @Transactional - public NicknameUpdateResponse updateNickname(String email, NicknameUpdateRequest nicknameUpdateRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - + public NicknameUpdateResponse updateNickname(SiteUser siteUser, NicknameUpdateRequest nicknameUpdateRequest) { validateNicknameDuplicated(nicknameUpdateRequest.nickname()); validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 1acfcb931..505bfe072 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -1,5 +1,7 @@ package com.example.solidconnection.university.controller; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.service.SiteUserService; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.dto.IsLikeResponse; @@ -19,7 +21,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; import java.util.List; @RequiredArgsConstructor @@ -34,42 +35,45 @@ public class UniversityController { @GetMapping("/recommends") public ResponseEntity getUniversityRecommends( - Principal principal) { - if (principal == null) { + @AuthorizedUser SiteUser siteUser + ) { + if (siteUser == null) { return ResponseEntity.ok(universityRecommendService.getGeneralRecommends()); } else { - return ResponseEntity.ok(universityRecommendService.getPersonalRecommends(principal.getName())); + return ResponseEntity.ok(universityRecommendService.getPersonalRecommends(siteUser)); } } @GetMapping("/like") - public ResponseEntity> getMyWishUniversity(Principal principal) { - List wishUniversities - = siteUserService.getWishUniversity(principal.getName()); - return ResponseEntity - .ok(wishUniversities); + public ResponseEntity> getMyWishUniversity( + @AuthorizedUser SiteUser siteUser + ) { + List wishUniversities = siteUserService.getWishUniversity(siteUser); + return ResponseEntity.ok(wishUniversities); } @GetMapping("/{universityInfoForApplyId}/like") public ResponseEntity getIsLiked( - Principal principal, - @PathVariable Long universityInfoForApplyId) { - IsLikeResponse isLiked = universityLikeService.getIsLiked(principal.getName(), universityInfoForApplyId); + @AuthorizedUser SiteUser siteUser, + @PathVariable Long universityInfoForApplyId + ) { + IsLikeResponse isLiked = universityLikeService.getIsLiked(siteUser, universityInfoForApplyId); return ResponseEntity.ok(isLiked); } @PostMapping("/{universityInfoForApplyId}/like") public ResponseEntity addWishUniversity( - Principal principal, - @PathVariable Long universityInfoForApplyId) { - LikeResultResponse likeResultResponse = universityLikeService.likeUniversity(principal.getName(), universityInfoForApplyId); - return ResponseEntity - .ok(likeResultResponse); + @AuthorizedUser SiteUser siteUser, + @PathVariable Long universityInfoForApplyId + ) { + LikeResultResponse likeResultResponse = universityLikeService.likeUniversity(siteUser, universityInfoForApplyId); + return ResponseEntity.ok(likeResultResponse); } @GetMapping("/detail/{universityInfoForApplyId}") public ResponseEntity getUniversityDetails( - @PathVariable Long universityInfoForApplyId) { + @PathVariable Long universityInfoForApplyId + ) { UniversityDetailResponse universityDetailResponse = universityQueryService.getUniversityDetail(universityInfoForApplyId); return ResponseEntity.ok(universityDetailResponse); } @@ -80,7 +84,8 @@ public ResponseEntity> searchUnivers @RequestParam(required = false, defaultValue = "") String region, @RequestParam(required = false, defaultValue = "") List keyword, @RequestParam(required = false, defaultValue = "") LanguageTestType testType, - @RequestParam(required = false, defaultValue = "") String testScore) { + @RequestParam(required = false, defaultValue = "") String testScore + ) { List universityInfoForApplyPreviewResponse = universityQueryService.searchUniversity(region, keyword, testType, testScore).universityInfoForApplyPreviewResponses(); return ResponseEntity.ok(universityInfoForApplyPreviewResponse); diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java index 4b15e5b8d..d926bc516 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java @@ -2,7 +2,6 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.domain.LikedUniversity; import com.example.solidconnection.university.domain.UniversityInfoForApply; import com.example.solidconnection.university.dto.IsLikeResponse; @@ -24,7 +23,6 @@ public class UniversityLikeService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final LikedUniversityRepository likedUniversityRepository; - private final SiteUserRepository siteUserRepository; @Value("${university.term}") public String term; @@ -34,8 +32,7 @@ public class UniversityLikeService { * - 이미 좋아요가 눌러져있다면, 좋아요를 취소한다. * */ @Transactional - public LikeResultResponse likeUniversity(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public LikeResultResponse likeUniversity(SiteUser siteUser, Long universityInfoForApplyId) { UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); @@ -56,8 +53,7 @@ public LikeResultResponse likeUniversity(String email, Long universityInfoForApp * '좋아요'한 대학교인지 확인한다. * */ @Transactional(readOnly = true) - public IsLikeResponse getIsLiked(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public IsLikeResponse getIsLiked(SiteUser siteUser, Long universityInfoForApplyId) { UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); boolean isLike = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply).isPresent(); return new IsLikeResponse(isLike); diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index 654b08390..4d9ab6242 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -2,7 +2,6 @@ import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.domain.UniversityInfoForApply; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.dto.UniversityRecommendsResponse; @@ -24,7 +23,6 @@ public class UniversityRecommendService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final GeneralUniversityRecommendService generalUniversityRecommendService; - private final SiteUserRepository siteUserRepository; @Value("${university.term}") private String term; @@ -36,8 +34,7 @@ public class UniversityRecommendService { * - 맞춤 추천 대학교의 수가 6개보다 적다면, 공통 추천 대학교 후보에서 이번 term 에 열리는 학교들을 부족한 수 만큼 불러온다. * */ @Transactional(readOnly = true) - public UniversityRecommendsResponse getPersonalRecommends(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public UniversityRecommendsResponse getPersonalRecommends(SiteUser siteUser) { // 맞춤 추천 대학교를 불러온다. List personalRecommends = universityInfoForApplyRepository .findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(siteUser, term); diff --git a/src/main/java/com/example/solidconnection/util/RedisUtils.java b/src/main/java/com/example/solidconnection/util/RedisUtils.java index 6c56fa73f..ed67acac0 100644 --- a/src/main/java/com/example/solidconnection/util/RedisUtils.java +++ b/src/main/java/com/example/solidconnection/util/RedisUtils.java @@ -44,8 +44,8 @@ public String getPostViewCountRedisKey(Long postId) { return VIEW_COUNT_KEY_PREFIX.getValue() + postId; } - public String getValidatePostViewCountRedisKey(String email, Long postId) { - return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + email; + public String getValidatePostViewCountRedisKey(long siteUserId, Long postId) { + return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + siteUserId; } public Long getPostIdFromPostViewCountRedisKey(String key) { diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java index b8f5cd283..f06116ebb 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java @@ -26,7 +26,7 @@ class 지원자_목록_조회_테스트 { void 이번_학기_전체_지원자를_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicants( - 테스트유저_2.getEmail(), + 테스트유저_2, "", "" ); @@ -72,7 +72,7 @@ class 지원자_목록_조회_테스트 { void 이번_학기_특정_지역_지원자를_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicants( - 테스트유저_2.getEmail(), + 테스트유저_2, 영미권.getCode(), "" ); @@ -99,7 +99,7 @@ class 지원자_목록_조회_테스트 { void 이번_학기_지원자를_대학_국문_이름으로_필터링해서_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicants( - 테스트유저_2.getEmail(), + 테스트유저_2, null, "일본" ); @@ -124,7 +124,7 @@ class 지원자_목록_조회_테스트 { void 이전_학기_지원자는_조회되지_않는다() { // when ApplicationsResponse response = applicationQueryService.getApplicants( - 테스트유저_1.getEmail(), + 테스트유저_1, "", "" ); @@ -152,7 +152,7 @@ class 경쟁자_목록_조회_테스트 { void 이번_학기_지원한_대학의_경쟁자_목록을_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( - 테스트유저_2.getEmail() + 테스트유저_2 ); // then @@ -180,7 +180,7 @@ class 경쟁자_목록_조회_테스트 { void 이번_학기_지원한_대학_중_미선택이_있을_때_경쟁자_목록을_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( - 테스트유저_7.getEmail() + 테스트유저_7 ); // then @@ -202,7 +202,7 @@ class 경쟁자_목록_조회_테스트 { void 이번_학기_지원한_대학이_모두_미선택일_때_경쟁자_목록을_조회한다() { //when ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( - 테스트유저_6.getEmail() + 테스트유저_6 ); // then diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java index 84f130d54..911172bfd 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -58,7 +58,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); // when - boolean result = applicationSubmissionService.apply(테스트유저_1.getEmail(), request); + boolean result = applicationSubmissionService.apply(테스트유저_1, request); // then Application savedApplication = applicationRepository.findBySiteUserAndTerm(테스트유저_1, term).orElseThrow(); @@ -92,7 +92,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { // when & then assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + applicationSubmissionService.apply(테스트유저_1, request) ) .isInstanceOf(CustomException.class) .hasMessage(INVALID_GPA_SCORE_STATUS.getMessage()); @@ -112,7 +112,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { // when & then assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + applicationSubmissionService.apply(테스트유저_1, request) ) .isInstanceOf(CustomException.class) .hasMessage(INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); @@ -132,7 +132,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { // when & then assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + applicationSubmissionService.apply(테스트유저_1, request) ) .isInstanceOf(CustomException.class) .hasMessage(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); @@ -151,12 +151,12 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); for (int i = 0; i < APPLICATION_UPDATE_COUNT_LIMIT + 1; i++) { - applicationSubmissionService.apply(테스트유저_1.getEmail(), request); + applicationSubmissionService.apply(테스트유저_1, request); } // when & then assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + applicationSubmissionService.apply(테스트유저_1, request) ) .isInstanceOf(CustomException.class) .hasMessage(APPLY_UPDATE_LIMIT_EXCEED.getMessage()); diff --git a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java index 418a04d8c..d38463dcb 100644 --- a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java @@ -56,7 +56,7 @@ class 댓글_조회_테스트 { // when List responses = commentService.findCommentsByPostId( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId() ); @@ -114,7 +114,7 @@ class 댓글_생성_테스트 { // when CommentCreateResponse response = commentService.createComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), request ); @@ -139,7 +139,7 @@ class 댓글_생성_테스트 { // when CommentCreateResponse response = commentService.createComment( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), request ); @@ -166,7 +166,7 @@ class 댓글_생성_테스트 { // when & then assertThatThrownBy(() -> commentService.createComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), request )) @@ -184,7 +184,7 @@ class 댓글_생성_테스트 { // when & then assertThatThrownBy(() -> commentService.createComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), request )) @@ -205,7 +205,7 @@ class 댓글_수정_테스트 { // when CommentUpdateResponse response = commentService.updateComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), comment.getId(), request @@ -232,7 +232,7 @@ class 댓글_수정_테스트 { // when & then assertThatThrownBy(() -> commentService.updateComment( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), comment.getId(), request @@ -251,7 +251,7 @@ class 댓글_수정_테스트 { // when & then assertThatThrownBy(() -> commentService.updateComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), comment.getId(), request @@ -275,7 +275,7 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), comment.getId() ); @@ -300,7 +300,7 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), parentComment.getId() ); @@ -330,7 +330,7 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), childComment1.getId() ); @@ -361,7 +361,7 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), childComment.getId() ); @@ -383,7 +383,7 @@ class 댓글_삭제_테스트 { // when & then assertThatThrownBy(() -> commentService.deleteCommentById( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), comment.getId() )) diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index 36bd91819..544b31b4c 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -4,7 +4,6 @@ import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostCommandService; import com.example.solidconnection.post.service.PostLikeService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -24,6 +23,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static org.junit.jupiter.api.Assertions.assertEquals; @TestContainerSpringBootTest @@ -57,7 +57,6 @@ void setUp() { siteUserRepository.save(siteUser); post = createPost(board, siteUser); postRepository.save(post); - createSiteUsers(); } private SiteUser createSiteUser() { @@ -72,22 +71,6 @@ private SiteUser createSiteUser() { ); } - private void createSiteUsers() { - for (int i = 0; i < 1000; i++) { - - SiteUser siteUser = new SiteUser( - "email" + i, - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - siteUserRepository.save(siteUser); - } - } - private Board createBoard() { return new Board( "FREE", "자유게시판"); @@ -117,10 +100,11 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { String email = "email" + i; + SiteUser tmpSiteUser = siteUserRepository.save(createSiteUserByEmail(email)); executorService.submit(() -> { try { - postLikeService.likePost(email, board.getCode(), post.getId()); - postLikeService.dislikePost(email, board.getCode(), post.getId()); + postLikeService.likePost(tmpSiteUser, board.getCode(), post.getId()); + postLikeService.dislikePost(tmpSiteUser, board.getCode(), post.getId()); } finally { doneSignal.countDown(); } diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index dcd423168..678e2b084 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -96,7 +96,7 @@ private Post createPost(Board board, SiteUser siteUser) { @Test public void 게시글을_조회할_때_조회수_동시성_문제를_해결한다() throws InterruptedException { - redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), post.getId())); ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); @@ -126,7 +126,7 @@ private Post createPost(Board board, SiteUser siteUser) { @Test public void 게시글을_조회할_때_조회수_조작_문제를_해결한다() throws InterruptedException { - redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), post.getId())); ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); @@ -134,7 +134,7 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), post.getId())); if (isFirstTime) { redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } @@ -147,7 +147,7 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), post.getId())); if (isFirstTime) { redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } diff --git a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java index dce720610..35ab993f5 100644 --- a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java @@ -66,9 +66,9 @@ private SiteUser createSiteUser() { executorService.submit(() -> { try { List tasks = Arrays.asList( - () -> applicationQueryService.getApplicants(siteUser.getEmail(), "", ""), - () -> applicationQueryService.getApplicants(siteUser.getEmail(), "ASIA", ""), - () -> applicationQueryService.getApplicants(siteUser.getEmail(), "", "추오") + () -> applicationQueryService.getApplicants(siteUser, "", ""), + () -> applicationQueryService.getApplicants(siteUser, "ASIA", ""), + () -> applicationQueryService.getApplicants(siteUser, "", "추오") ); Collections.shuffle(tasks); tasks.forEach(Runnable::run); diff --git a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java deleted file mode 100644 index 16e3639f1..000000000 --- a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.example.solidconnection.config.security; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.support.TestContainerSpringBootTest; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.util.Date; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.spy; - -@TestContainerSpringBootTest -@DisplayName("토큰 인증 필터 테스트") -class JwtAuthenticationFilterTest { - - @Autowired - private JwtAuthenticationFilter jwtAuthenticationFilter; - - @Autowired - private JwtProperties jwtProperties; - - private HttpServletRequest request; - private HttpServletResponse response; - private FilterChain filterChain; - - @BeforeEach - void setUp() { - response = new MockHttpServletResponse(); - filterChain = spy(FilterChain.class); - SecurityContextHolder.clearContext(); - } - - @Test - public void 유효한_토큰에_대한_인증_정보를_저장한다() throws Exception { - // given - String token = Jwts.builder() - .setSubject("subject") - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - request = createRequestWithToken(token); - - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()) - .isExactlyInstanceOf(JwtAuthentication.class); - then(filterChain).should().doFilter(request, response); - } - - @Test - public void 토큰이_없으면_다음_필터로_진행한다() throws Exception { - // given - request = new MockHttpServletRequest(); - - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - then(filterChain).should().doFilter(request, response); - } - - @Nested - class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { - - @Test - public void 만료된_토큰으로_인증하면_예외를_응답한다() throws Exception { - // given - String token = Jwts.builder() - .setSubject("subject") - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() - 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - request = createRequestWithToken(token); - - // when & then - assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) - .isInstanceOf(CustomException.class) - .hasMessage(INVALID_TOKEN.getMessage()); - then(filterChain).shouldHaveNoMoreInteractions(); - } - - @Test - public void 서명하지_않은_토큰으로_인증하면_예외를_응답한다() throws Exception { - // given - String token = Jwts.builder() - .setSubject("subject") - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() - 1000)) - .signWith(SignatureAlgorithm.HS256, "wrongSecretKey") - .compact(); - request = createRequestWithToken(token); - - // when & then - assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) - .isInstanceOf(CustomException.class) - .hasMessage(INVALID_TOKEN.getMessage()); - then(filterChain).shouldHaveNoMoreInteractions(); - } - } - - private HttpServletRequest createRequestWithToken(String token) { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Bearer " + token); - return request; - } -} diff --git a/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java b/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java new file mode 100644 index 000000000..763fdf101 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java @@ -0,0 +1,67 @@ +package com.example.solidconnection.custom.resolver; + + +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestContainerSpringBootTest +@DisplayName("인증된 사용자 argument resolver 테스트") +class AuthorizedUserResolverTest { + + @Autowired + private AuthorizedUserResolver authorizedUserResolver; + + @Autowired + private SiteUserRepository siteUserRepository; + + @BeforeEach + void setUp() { + SecurityContextHolder.clearContext(); + } + + @Test + void security_context_에_저장된_인증된_사용자를_반환한다() throws Exception { + // given + SiteUser siteUser = siteUserRepository.save(createSiteUser()); + SiteUserDetails userDetails = new SiteUserDetails(siteUser); + SiteUserAuthentication authentication = new SiteUserAuthentication("token", userDetails); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // when + SiteUser resolveSiteUser = (SiteUser) authorizedUserResolver.resolveArgument(null, null, null, null); + + // then + assertThat(resolveSiteUser).isEqualTo(siteUser); + } + + @Test + void security_context_에_저장된_사용자가_없으면_null_을_반환한다() throws Exception { + // when, then + assertThat(authorizedUserResolver.resolveArgument(null, null, null, null)).isNull(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolverTest.java b/src/test/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolverTest.java new file mode 100644 index 000000000..a0393dbc7 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolverTest.java @@ -0,0 +1,43 @@ +package com.example.solidconnection.custom.resolver; + +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestContainerSpringBootTest +@DisplayName("만료된 토큰 argument resolver 테스트") +class ExpiredTokenResolverTest { + + @BeforeEach + void setUp() { + SecurityContextHolder.clearContext(); + } + + @Autowired + private ExpiredTokenResolver expiredTokenResolver; + + @Test + void security_context_에_저장된_만료시간을_검증하지_않는_토큰을_반환한다() throws Exception { + // given + ExpiredTokenAuthentication authentication = new ExpiredTokenAuthentication("token"); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // when + ExpiredTokenAuthentication expiredTokenAuthentication = (ExpiredTokenAuthentication) expiredTokenResolver.resolveArgument(null, null, null, null); + + // then + assertThat(expiredTokenAuthentication.getToken()).isEqualTo("token"); + } + + @Test + void security_context_에_저장된_만료시간을_검증하지_않는_토큰이_없으면_null_을_반환한다() throws Exception { + // when, then + assertThat(expiredTokenResolver.resolveArgument(null, null, null, null)).isNull(); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthenticationTest.java b/src/test/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthenticationTest.java new file mode 100644 index 000000000..9ef78d0c7 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthenticationTest.java @@ -0,0 +1,64 @@ +package com.example.solidconnection.custom.security.authentication; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("만료된 토큰 인증 정보 테스트") +class ExpiredTokenAuthenticationTest { + + @Test + void 인증_정보에_저장된_토큰을_반환한다() { + // given + String token = "token123"; + ExpiredTokenAuthentication auth = new ExpiredTokenAuthentication(token); + + // when + String result = auth.getToken(); + + // then + assertThat(result).isEqualTo(token); + } + + @Test + void 인증_정보에_저장된_토큰의_subject_를_반환한다() { + // given + String subject = "subject321"; + String token = createToken(subject); + ExpiredTokenAuthentication auth = new ExpiredTokenAuthentication(token, subject); + + // when + String result = auth.getSubject(); + + // then + assertThat(result).isEqualTo(subject); + } + + @Test + void 항상_isAuthenticated_는_false_를_반환한다() { + // given + ExpiredTokenAuthentication auth1 = new ExpiredTokenAuthentication("token"); + ExpiredTokenAuthentication auth2 = new ExpiredTokenAuthentication("token", "subject"); + + // when & then + assertAll( + () -> assertThat(auth1.isAuthenticated()).isFalse(), + () -> assertThat(auth2.isAuthenticated()).isFalse() + ); + } + + private String createToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, "secret") + .compact(); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthenticationTest.java b/src/test/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthenticationTest.java new file mode 100644 index 000000000..6932fcd28 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthenticationTest.java @@ -0,0 +1,73 @@ +package com.example.solidconnection.custom.security.authentication; + +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SiteUserAuthenticationTest { + + @Test + void 인증_정보에_저장된_토큰을_반환한다() { + // given + String token = "token"; + SiteUserAuthentication authentication = new SiteUserAuthentication(token); + + // when + String result = authentication.getToken(); + + // then + assertThat(result).isEqualTo(token); + } + + @Test + void 인증_정보에_저장된_사용자를_반환한다() { + // given + SiteUserDetails userDetails = new SiteUserDetails(createSiteUser()); + SiteUserAuthentication authentication = new SiteUserAuthentication("token", userDetails); + + // when & then + SiteUserDetails actual = (SiteUserDetails) authentication.getPrincipal(); + + // then + assertThat(actual) + .extracting("siteUser") + .extracting("id") + .isEqualTo(userDetails.getSiteUser().getId()); + } + + @Test + void 인증_전에_생성되면_isAuthenticated_는_false_를_반환한다() { + // given + SiteUserAuthentication authentication = new SiteUserAuthentication("token"); + + // when & then + assertThat(authentication.isAuthenticated()).isFalse(); + } + + @Test + void 인증_후에_생성되면_isAuthenticated_는_true_를_반환한다() { + // given + SiteUserDetails userDetails = new SiteUserDetails(createSiteUser()); + SiteUserAuthentication authentication = new SiteUserAuthentication("token", userDetails); + + // when & then + assertThat(authentication.isAuthenticated()).isTrue(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java similarity index 98% rename from src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java rename to src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java index f4e8dc666..091a75eb8 100644 --- a/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.filter; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilterTest.java new file mode 100644 index 000000000..cbca9c5f2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilterTest.java @@ -0,0 +1,116 @@ +package com.example.solidconnection.custom.security.filter; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetailsService; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +@DisplayName("토큰 인증 필터 테스트") +class JwtAuthenticationFilterTest { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + private JwtProperties jwtProperties; + + @MockBean + private SiteUserDetailsService siteUserDetailsService; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + @BeforeEach() + void setUp() { + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + SecurityContextHolder.clearContext(); + } + + @Test + public void 토큰이_없으면_다음_필터로_진행한다() throws Exception { + // given + request = new MockHttpServletRequest(); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + then(filterChain).should().doFilter(request, response); + } + + @Nested + class 토큰이_있으면_컨텍스트에_저장한다 { + + @Test + void 유효한_토큰을_컨텍스트에_저장한다() throws Exception { + // given + Date validExpiration = new Date(System.currentTimeMillis() + 1000); + String token = createTokenWithExpiration(validExpiration); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()) + .isExactlyInstanceOf(SiteUserAuthentication.class); + then(filterChain).should().doFilter(request, response); + } + + @Test + void 만료된_토큰을_컨텍스트에_저장한다() throws Exception { + // given + Date invalidExpiration = new Date(System.currentTimeMillis() - 1000); + String token = createTokenWithExpiration(invalidExpiration); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()) + .isExactlyInstanceOf(ExpiredTokenAuthentication.class); + then(filterChain).should().doFilter(request, response); + } + } + + private String createTokenWithExpiration(Date expiration) { + return Jwts.builder() + .setSubject("1") + .setIssuedAt(new Date()) + .setExpiration(expiration) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private HttpServletRequest createRequestWithToken(String token) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + token); + return request; + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java similarity index 96% rename from src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java rename to src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java index a067bf9d9..7eac22c71 100644 --- a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java @@ -1,5 +1,6 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.filter; +import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; diff --git a/src/test/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProviderTest.java b/src/test/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProviderTest.java new file mode 100644 index 000000000..ad6053359 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProviderTest.java @@ -0,0 +1,80 @@ +package com.example.solidconnection.custom.security.provider; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; + +import java.net.PasswordAuthentication; +import java.util.Date; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.*; + +@TestContainerSpringBootTest +@DisplayName("만료된 토큰 provider 테스트") +class ExpiredTokenAuthenticationProviderTest { + + @Autowired + private ExpiredTokenAuthenticationProvider expiredTokenAuthenticationProvider; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 처리할_수_있는_타입인지를_반환한다() { + // given + Class supportedType = ExpiredTokenAuthentication.class; + Class notSupportedType = PasswordAuthentication.class; + + // when & then + assertAll( + () -> assertTrue(expiredTokenAuthenticationProvider.supports(supportedType)), + () -> assertFalse(expiredTokenAuthenticationProvider.supports(notSupportedType)) + ); + } + + @Test + void 만료된_토큰의_인증_정보를_반환한다() { + // given + String expiredToken = createExpiredToken(); + ExpiredTokenAuthentication ExpiredTokenAuthentication = new ExpiredTokenAuthentication(expiredToken); + + // when + Authentication result = expiredTokenAuthenticationProvider.authenticate(ExpiredTokenAuthentication); + + // then + assertAll( + () -> assertThat(result).isInstanceOf(ExpiredTokenAuthentication.class), + () -> assertThat(result.isAuthenticated()).isFalse() + ); + } + + @Test + void 유효하지_않은_토큰이면_예외_응답을_반환한다() { + // given + ExpiredTokenAuthentication ExpiredTokenAuthentication = new ExpiredTokenAuthentication("invalid token"); + + // when & then + assertThatCode(() -> expiredTokenAuthenticationProvider.authenticate(ExpiredTokenAuthentication)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_TOKEN.getMessage()); + } + + private String createExpiredToken() { + return Jwts.builder() + .setSubject("1") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProviderTest.java b/src/test/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProviderTest.java new file mode 100644 index 000000000..46d7498a2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProviderTest.java @@ -0,0 +1,159 @@ +package com.example.solidconnection.custom.security.provider; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; + +import java.net.PasswordAuthentication; +import java.util.Date; + +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("사용자 인증정보 provider 테스트") +class SiteUserAuthenticationProviderTest { + + @Autowired + private SiteUserAuthenticationProvider siteUserAuthenticationProvider; + + @Autowired + private JwtProperties jwtProperties; + + @Autowired + private SiteUserRepository siteUserRepository; + + private SiteUser siteUser; + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + } + + @Test + void 처리할_수_있는_타입인지를_반환한다() { + // given + Class supportedType = SiteUserAuthentication.class; + Class notSupportedType = PasswordAuthentication.class; + + // when & then + assertAll( + () -> assertThat(siteUserAuthenticationProvider.supports(supportedType)).isTrue(), + () -> assertThat(siteUserAuthenticationProvider.supports(notSupportedType)).isFalse() + ); + } + + @Test + void 유효한_토큰이면_정상적으로_인증_정보를_반환한다() { + // given + String token = createValidToken(siteUser.getId()); + SiteUserAuthentication auth = new SiteUserAuthentication(token); + + // when + Authentication result = siteUserAuthenticationProvider.authenticate(auth); + + // then + assertThat(result).isNotNull(); + assertAll( + () -> assertThat(result.getCredentials()).isEqualTo(token), + () -> assertThat(result.getPrincipal().getClass()).isEqualTo(SiteUserDetails.class) + ); + } + + @Nested + class 예외_응답을_반환하다 { + + @Test + void 유효하지_않은_토큰이면_예외_응답을_반환한다() { + // given + SiteUserAuthentication expiredAuth = new SiteUserAuthentication(createExpiredToken()); + + // when & then + assertThatCode(() -> siteUserAuthenticationProvider.authenticate(expiredAuth)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_TOKEN.getMessage()); + } + + @Test + void 사용자_정보의_형식이_다르면_예외_응답을_반환한다() { + // given + SiteUserAuthentication wrongSubjectTypeAuth = new SiteUserAuthentication(createWrongSubjectTypeToken()); + + // when & then + assertThatCode(() -> siteUserAuthenticationProvider.authenticate(wrongSubjectTypeAuth)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_TOKEN.getMessage()); + } + + @Test + void 유효한_토큰이지만_해당되는_사용자가_없으면_예외_응답을_반환한다() { + // given + long notExistingUserId = siteUser.getId() + 100; + String token = createValidToken(notExistingUserId); + SiteUserAuthentication auth = new SiteUserAuthentication(token); + + // when & then + assertThatCode(() -> siteUserAuthenticationProvider.authenticate(auth)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(AUTHENTICATION_FAILED.getMessage()); + } + } + + private String createValidToken(long id) { + return Jwts.builder() + .setSubject(String.valueOf(id)) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private String createExpiredToken() { + return Jwts.builder() + .setSubject(String.valueOf(siteUser.getId())) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private String createWrongSubjectTypeToken() { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java new file mode 100644 index 000000000..731f840f3 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java @@ -0,0 +1,104 @@ +package com.example.solidconnection.custom.security.userdetails; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; + +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("사용자 인증 정보 서비스 테스트") +@TestContainerSpringBootTest +class SiteUserDetailsServiceTest { + + @Autowired + private SiteUserDetailsService userDetailsService; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Test + void 사용자_인증_정보를_반환한다() { + // given + SiteUser siteUser = siteUserRepository.save(createSiteUser()); + String username = getUserName(siteUser); + + // when + SiteUserDetails userDetails = (SiteUserDetails) userDetailsService.loadUserByUsername(username); + + // then + assertAll( + () -> assertThat(userDetails.getUsername()).isEqualTo(username), + () -> assertThat(userDetails.getSiteUser()).extracting("id").isEqualTo(siteUser.getId()) + ); + } + + @Nested + class 예외_응답을_반환한다 { + + @Test + void 지정되지_않은_형식의_식별자가_주어지면_예외_응답을_반환한다() { + // given + String username = "notNumber"; + + // when & then + assertThatCode(() -> userDetailsService.loadUserByUsername(username)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_TOKEN.getMessage()); + } + + @Test + void 식별자에_해당하는_사용자가_없으면_예외_응답을_반환한다() { + // given + String username = "1234"; + + // when & then + assertThatCode(() -> userDetailsService.loadUserByUsername(username)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(AUTHENTICATION_FAILED.getMessage()); + } + + @Test + void 탈퇴한_사용자이면_예외_응답을_반환한다() { + // given + SiteUser siteUser = createSiteUser(); + siteUser.setQuitedAt(LocalDate.now()); + siteUserRepository.save(siteUser); + String username = getUserName(siteUser); + + // when & then + assertThatCode(() -> userDetailsService.loadUserByUsername(username)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(AUTHENTICATION_FAILED.getMessage()); + } + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private String getUserName(SiteUser siteUser) { + return siteUser.getId().toString(); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 6b739248b..40f39e646 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -7,8 +7,8 @@ import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; @@ -30,13 +30,13 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { @Autowired - SiteUserRepository siteUserRepository; + private SiteUserRepository siteUserRepository; @Autowired - ApplicationRepository applicationRepository; + private ApplicationRepository applicationRepository; @Autowired - TokenProvider tokenProvider; + private TokenProvider tokenProvider; private String accessToken; private String adminAccessToken; @@ -55,24 +55,8 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { @BeforeEach public void setUpUserAndToken() { - // setUp - 회원 정보 저장 - String email = "email@email.com"; - SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); - - // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); - - adminAccessToken = tokenProvider.generateToken("email5", TokenType.ACCESS); - String adminRefreshToken = tokenProvider.generateToken("email5", TokenType.REFRESH); - tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); - - user6AccessToken = tokenProvider.generateToken("email6", TokenType.ACCESS); - String user6RefreshToken = tokenProvider.generateToken("email6", TokenType.REFRESH); - tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); - // setUp - 사용자 정보 저장 + SiteUser 나 = siteUserRepository.save(createSiteUserByEmail("my-email")); SiteUser 사용자1 = siteUserRepository.save(createSiteUserByEmail("email1")); SiteUser 사용자2 = siteUserRepository.save(createSiteUserByEmail("email2")); SiteUser 사용자3 = siteUserRepository.save(createSiteUserByEmail("email3")); @@ -80,16 +64,30 @@ public void setUpUserAndToken() { SiteUser 사용자5_관리자 = siteUserRepository.save(createSiteUserByEmail("email5")); SiteUser 사용자6 = siteUserRepository.save(createSiteUserByEmail("email6")); + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenProvider.generateToken(나, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(나, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + + adminAccessToken = tokenProvider.generateToken(사용자5_관리자, TokenType.ACCESS); + String adminRefreshToken = tokenProvider.generateToken(사용자5_관리자, TokenType.REFRESH); + tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); + + user6AccessToken = tokenProvider.generateToken(사용자6, TokenType.ACCESS); + String user6RefreshToken = tokenProvider.generateToken(사용자6, TokenType.REFRESH); + tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); + // setUp - 지원 정보 저장 Gpa gpa = createDummyGpa(); LanguageTest languageTest = createDummyLanguageTest(); - 나의_지원정보 = new Application(siteUser, gpa, languageTest, term); + 나의_지원정보 = new Application(나, gpa, languageTest, term); 사용자1_지원정보 = new Application(사용자1, gpa, languageTest, term); 사용자2_지원정보 = new Application(사용자2, gpa, languageTest, term); 사용자3_지원정보 = new Application(사용자3, gpa, languageTest, term); 사용자4_이전학기_지원정보 = new Application(사용자4_이전학기_지원자, gpa, languageTest, beforeTerm); 사용자5_관리자_지원정보 = new Application(사용자5_관리자, gpa, languageTest, term); 사용자6_지원정보 = new Application(사용자6, gpa, languageTest, term); + 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 린츠_카톨릭대학_지원_정보, "0"); 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 그라츠공과대학_지원_정보, "1"); 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, 서던덴마크대학교_지원_정보, "2"); @@ -337,5 +335,4 @@ public void setUpUserAndToken() { assertThat(secondChoiceApplicants.size()).isEqualTo(choicedUniversityCount); assertThat(thirdChoiceApplicants.size()).isEqualTo(choicedUniversityCount); } - } diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index fb42216c9..567b1016d 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -19,21 +19,24 @@ @DisplayName("마이페이지 테스트") class MyPageTest extends BaseEndToEndTest { - private final String email = "email@email.com"; + private SiteUser siteUser; + @Autowired private SiteUserRepository siteUserRepository; + @Autowired private TokenProvider tokenProvider; + private String accessToken; @BeforeEach public void setUpUserAndToken() { // setUp - 회원 정보 저장 - siteUserRepository.save(createSiteUserByEmail(email)); + siteUser = siteUserRepository.save(createSiteUserByEmail("email")); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @@ -48,11 +51,10 @@ public void setUpUserAndToken() { .statusCode(HttpStatus.OK.value()) .extract().as(MyPageResponse.class); - SiteUser savedSiteUser = siteUserRepository.getByEmail(email); assertAll("불러온 마이 페이지 정보가 DB의 정보와 일치한다.", - () -> assertThat(myPageResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), - () -> assertThat(myPageResponse.birth()).isEqualTo(savedSiteUser.getBirth()), - () -> assertThat(myPageResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl()), - () -> assertThat(myPageResponse.email()).isEqualTo(savedSiteUser.getEmail())); + () -> assertThat(myPageResponse.nickname()).isEqualTo(siteUser.getNickname()), + () -> assertThat(myPageResponse.birth()).isEqualTo(siteUser.getBirth()), + () -> assertThat(myPageResponse.profileImageUrl()).isEqualTo(siteUser.getProfileImageUrl()), + () -> assertThat(myPageResponse.email()).isEqualTo(siteUser.getEmail())); } } diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java index 6d7f52032..025ddb7d7 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -37,17 +37,15 @@ class MyPageUpdateTest extends BaseEndToEndTest { private SiteUser siteUser; - private final String email = "email@email.com"; - @BeforeEach public void setUpUserAndToken() { // setUp - 회원 정보 저장 - siteUser = createSiteUserByEmail(email); + siteUser = createSiteUserByEmail("email"); siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @@ -62,10 +60,9 @@ public void setUpUserAndToken() { .statusCode(HttpStatus.OK.value()) .extract().as(MyPageUpdateResponse.class); - SiteUser savedSiteUser = siteUserRepository.getByEmail(email); assertAll("불러온 마이 페이지 정보가 DB의 정보와 일치한다.", - () -> assertThat(myPageUpdateResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), - () -> assertThat(myPageUpdateResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl())); + () -> assertThat(myPageUpdateResponse.nickname()).isEqualTo(siteUser.getNickname()), + () -> assertThat(myPageUpdateResponse.profileImageUrl()).isEqualTo(siteUser.getProfileImageUrl())); } @Test @@ -82,9 +79,9 @@ public void setUpUserAndToken() { .statusCode(HttpStatus.OK.value()) .extract().as(NicknameUpdateResponse.class); - SiteUser savedSiteUser = siteUserRepository.getByEmail(email); + SiteUser updatedSiteUser = siteUserRepository.findById(siteUser.getId()).get(); assertAll("마이 페이지 정보가 수정된다.", - () -> assertThat(nicknameUpdateResponse.nickname()).isEqualTo(savedSiteUser.getNickname())); + () -> assertThat(nicknameUpdateResponse.nickname()).isEqualTo(updatedSiteUser.getNickname())); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index efd5ad1d7..26eba657a 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -79,7 +79,7 @@ class SignInTest extends BaseEndToEndTest { .willReturn(createKakaoUserInfoDtoByEmail(email)); // setUp - 사용자 정보 저장 - siteUserRepository.save(createSiteUserByEmail(email)); + SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); // request - body 생성 및 요청 KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); @@ -95,7 +95,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(siteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -112,7 +112,7 @@ class SignInTest extends BaseEndToEndTest { SiteUser siteUserFixture = createSiteUserByEmail(email); LocalDate justBeforeRemoval = LocalDate.now().minusDays(ACCOUNT_RECOVER_DURATION - 1); siteUserFixture.setQuitedAt(justBeforeRemoval); - siteUserRepository.save(siteUserFixture); + SiteUser siteUser = siteUserRepository.save(siteUserFixture); // request - body 생성 및 요청 KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); @@ -124,12 +124,13 @@ class SignInTest extends BaseEndToEndTest { .statusCode(HttpStatus.OK.value()) .extract().as(SignInResponse.class); + SiteUser updatedSiteUser = siteUserRepository.findById(siteUser.getId()).get(); assertAll("리프레스 토큰과 엑세스 토큰을 응답하고, 탈퇴 날짜를 초기화한다.", () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull(), - () -> assertThat(siteUserRepository.getByEmail(email).getQuitedAt()).isNull()); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) + () -> assertThat(updatedSiteUser.getQuitedAt()).isNull()); + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(siteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 07dafb539..1eb152387 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -12,6 +12,7 @@ import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.Gender; @@ -86,7 +87,7 @@ class SignUpTest extends BaseEndToEndTest { .statusCode(HttpStatus.OK.value()) .extract().as(SignUpResponse.class); - SiteUser savedSiteUser = siteUserRepository.getByEmail(email); + SiteUser savedSiteUser = siteUserRepository.findByEmailAndAuthType(email, AuthType.KAKAO).get(); assertAll( "회원 정보를 저장한다.", () -> assertThat(savedSiteUser.getId()).isNotNull(), @@ -109,7 +110,7 @@ class SignUpTest extends BaseEndToEndTest { () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(savedSiteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index 947f44fd0..b7e112d00 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -36,8 +36,8 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index dccd1092f..301b373c4 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -33,8 +33,6 @@ @DisplayName("대학교 좋아요 테스트") class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { - private final String email = "email@email.com"; - @Autowired private SiteUserRepository siteUserRepository; @@ -53,12 +51,12 @@ class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { @BeforeEach public void setUpUserAndToken() { // setUp - 회원 정보 저장 - siteUser = createSiteUserByEmail(email); + siteUser = createSiteUserByEmail("email@email.com"); siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @@ -102,7 +100,7 @@ public void setUpUserAndToken() { .extract().as(LikeResultResponse.class); Optional likedUniversity - = likedUniversityRepository.findAllBySiteUser_Email(email).stream().findFirst(); + = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()).stream().findFirst(); assertAll("좋아요 누른 대학교를 저장하고 좋아요 성공 응답을 반환한다.", () -> assertThat(likedUniversity).isPresent(), () -> assertThat(likedUniversity.get().getId()).isEqualTo(괌대학_A_지원_정보.getId()), @@ -125,7 +123,7 @@ public void setUpUserAndToken() { .extract().as(LikeResultResponse.class); Optional likedUniversity - = likedUniversityRepository.findAllBySiteUser_Email(email).stream().findFirst(); + = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()).stream().findFirst(); assertAll("좋아요 누른 대학교를 삭제하고, 좋아요 취소 응답을 반환한다.", () -> assertThat(likedUniversity).isEmpty(), () -> assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE) diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index 4f3bd3042..358f779cd 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -54,8 +54,8 @@ void setUp() { generalUniversityRecommendService.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java index 4859f9fe2..22abbfb53 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -1,12 +1,10 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -21,17 +19,9 @@ @DisplayName("대학교 검색 테스트") class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { - private final String email = "email@email.com"; - @Autowired private SiteUserRepository siteUserRepository; - @Autowired - private UniversityInfoForApplyRepository universityInfoForApplyRepository; - - @Autowired - private LikedUniversityRepository likedUniversityRepository; - @Autowired private TokenProvider tokenProvider; @@ -41,12 +31,12 @@ class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { @BeforeEach public void setUpUserAndToken() { // setUp - 회원 정보 저장 - siteUser = createSiteUserByEmail(email); + siteUser = createSiteUserByEmail("email@email.com"); siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } diff --git a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java index eb1b2b652..3cdc5a40c 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java @@ -78,7 +78,7 @@ class 게시글_생성_테스트 { // when PostCreateResponse response = postCommandService.createPost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), request, imageFiles @@ -108,7 +108,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(INVALID_POST_CATEGORY.getMessage()); } @@ -121,7 +121,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(INVALID_POST_CATEGORY.getMessage()); } @@ -134,7 +134,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); } @@ -158,7 +158,7 @@ class 게시글_수정_테스트 { // when PostUpdateResponse response = postCommandService.updatePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId(), request, @@ -189,7 +189,7 @@ class 게시글_수정_테스트 { // when & then assertThatThrownBy(() -> postCommandService.updatePost( - 테스트유저_2.getEmail(), + 테스트유저_2, 자유게시판.getCode(), testPost.getId(), request, @@ -209,7 +209,7 @@ class 게시글_수정_테스트 { // when & then assertThatThrownBy(() -> postCommandService.updatePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId(), request, @@ -229,7 +229,7 @@ class 게시글_수정_테스트 { // when & then assertThatThrownBy(() -> postCommandService.updatePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId(), request, @@ -253,7 +253,7 @@ class 게시글_삭제_테스트 { // when PostDeleteResponse response = postCommandService.deletePostById( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() ); @@ -275,7 +275,7 @@ class 게시글_삭제_테스트 { // when & then assertThatThrownBy(() -> postCommandService.deletePostById( - 테스트유저_2.getEmail(), + 테스트유저_2, 자유게시판.getCode(), testPost.getId() )) @@ -291,7 +291,7 @@ class 게시글_삭제_테스트 { // when & then assertThatThrownBy(() -> postCommandService.deletePostById( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() )) diff --git a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java index 9fe6a2704..460b9a15b 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java @@ -44,7 +44,7 @@ class 게시글_좋아요_테스트 { // when PostLikeResponse response = postLikeService.likePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() ); @@ -63,12 +63,12 @@ class 게시글_좋아요_테스트 { void 이미_좋아요한_게시글을_다시_좋아요하면_예외_응답을_반환한다() { // given Post testPost = createPost(자유게시판, 테스트유저_1); - postLikeService.likePost(테스트유저_1.getEmail(), 자유게시판.getCode(), testPost.getId()); + postLikeService.likePost(테스트유저_1, 자유게시판.getCode(), testPost.getId()); // when & then assertThatThrownBy(() -> postLikeService.likePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() )) @@ -84,12 +84,12 @@ class 게시글_좋아요_취소_테스트 { void 게시글_좋아요를_성공적으로_취소한다() { // given Post testPost = createPost(자유게시판, 테스트유저_1); - PostLikeResponse beforeResponse = postLikeService.likePost(테스트유저_1.getEmail(), 자유게시판.getCode(), testPost.getId()); + PostLikeResponse beforeResponse = postLikeService.likePost(테스트유저_1, 자유게시판.getCode(), testPost.getId()); long beforeLikeCount = beforeResponse.likeCount(); // when PostDislikeResponse response = postLikeService.dislikePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() ); @@ -112,7 +112,7 @@ class 게시글_좋아요_취소_테스트 { // when & then assertThatThrownBy(() -> postLikeService.dislikePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() )) diff --git a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java index 7ec36b0df..d9acf5845 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java @@ -53,12 +53,12 @@ class PostQueryServiceTest extends BaseIntegrationTest { Post testPost = createPost(자유게시판, 테스트유저_1, expectedImageUrl); List comments = createComments(testPost, 테스트유저_1, List.of("첫번째 댓글", "두번째 댓글")); - String validateKey = redisUtils.getValidatePostViewCountRedisKey(테스트유저_1.getEmail(), testPost.getId()); + String validateKey = redisUtils.getValidatePostViewCountRedisKey(테스트유저_1.getId(), testPost.getId()); String viewCountKey = redisUtils.getPostViewCountRedisKey(testPost.getId()); // when PostFindResponse response = postQueryService.findPostById( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() ); diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 4a511d867..681b708a2 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -55,7 +55,7 @@ class ScoreServiceTest extends BaseIntegrationTest { ); // when - GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser.getEmail()); + GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser); // then assertThat(response.gpaScoreStatusList()) @@ -73,7 +73,7 @@ class ScoreServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser.getEmail()); + GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser); // then assertThat(response.gpaScoreStatusList()).isEmpty(); @@ -87,9 +87,10 @@ class ScoreServiceTest extends BaseIntegrationTest { createLanguageTestScore(testUser, LanguageTestType.TOEIC, "100"), createLanguageTestScore(testUser, LanguageTestType.TOEFL_IBT, "7.5") ); + siteUserRepository.save(testUser); // when - LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser.getEmail()); + LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser); // then assertThat(response.languageTestScoreStatusList()) @@ -107,7 +108,7 @@ class ScoreServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser.getEmail()); + LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser); // then assertThat(response.languageTestScoreStatusList()).isEmpty(); @@ -120,7 +121,7 @@ class ScoreServiceTest extends BaseIntegrationTest { GpaScoreRequest request = createGpaScoreRequest(); // when - long scoreId = scoreService.submitGpaScore(testUser.getEmail(), request); + long scoreId = scoreService.submitGpaScore(testUser, request); GpaScore savedScore = gpaScoreRepository.findById(scoreId).orElseThrow(); // then @@ -140,7 +141,7 @@ class ScoreServiceTest extends BaseIntegrationTest { LanguageTestScoreRequest request = createLanguageTestScoreRequest(); // when - long scoreId = scoreService.submitLanguageTestScore(testUser.getEmail(), request); + long scoreId = scoreService.submitLanguageTestScore(testUser, request); LanguageTestScore savedScore = languageTestScoreRepository.findById(scoreId).orElseThrow(); // then @@ -172,6 +173,7 @@ private GpaScore createGpaScore(SiteUser siteUser, double gpa, double gpaCriteri siteUser, LocalDate.now() ); + gpaScore.setSiteUser(siteUser); return gpaScoreRepository.save(gpaScore); } @@ -181,6 +183,7 @@ private LanguageTestScore createLanguageTestScore(SiteUser siteUser, LanguageTes LocalDate.now(), siteUser ); + languageTestScore.setSiteUser(siteUser); return languageTestScoreRepository.save(languageTestScore); } diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java index 8fdae031e..9fc6410d8 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java @@ -64,7 +64,7 @@ class SiteUserServiceTest extends BaseIntegrationTest { int likedUniversityCount = createLikedUniversities(testUser); // when - MyPageResponse response = siteUserService.getMyPageInfo(testUser.getEmail()); + MyPageResponse response = siteUserService.getMyPageInfo(testUser); // then Assertions.assertAll( @@ -84,7 +84,7 @@ class SiteUserServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - MyPageUpdateResponse response = siteUserService.getMyPageInfoToUpdate(testUser.getEmail()); + MyPageUpdateResponse response = siteUserService.getMyPageInfoToUpdate(testUser); // then Assertions.assertAll( @@ -100,7 +100,7 @@ class SiteUserServiceTest extends BaseIntegrationTest { int likedUniversityCount = createLikedUniversities(testUser); // when - List response = siteUserService.getWishUniversity(testUser.getEmail()); + List response = siteUserService.getWishUniversity(testUser); // then assertThat(response) @@ -127,7 +127,7 @@ class 프로필_이미지_수정_테스트 { // when ProfileImageUpdateResponse response = siteUserService.updateProfileImage( - testUser.getEmail(), + testUser, imageFile ); @@ -144,7 +144,7 @@ class 프로필_이미지_수정_테스트 { .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when - siteUserService.updateProfileImage(testUser.getEmail(), imageFile); + siteUserService.updateProfileImage(testUser, imageFile); // then then(s3Service).should(never()).deleteExProfile(any()); @@ -159,10 +159,10 @@ class 프로필_이미지_수정_테스트 { .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when - siteUserService.updateProfileImage(testUser.getEmail(), imageFile); + siteUserService.updateProfileImage(testUser, imageFile); // then - then(s3Service).should().deleteExProfile(testUser.getEmail()); + then(s3Service).should().deleteExProfile(testUser); } @Test @@ -172,7 +172,7 @@ class 프로필_이미지_수정_테스트 { MockMultipartFile emptyFile = createEmptyImageFile(); // when & then - assertThatCode(() -> siteUserService.updateProfileImage(testUser.getEmail(), emptyFile)) + assertThatCode(() -> siteUserService.updateProfileImage(testUser, emptyFile)) .isInstanceOf(CustomException.class) .hasMessage(PROFILE_IMAGE_NEEDED.getMessage()); } @@ -190,12 +190,12 @@ class 닉네임_수정_테스트 { // when NicknameUpdateResponse response = siteUserService.updateNickname( - testUser.getEmail(), + testUser, request ); // then - SiteUser updatedUser = siteUserRepository.getByEmail(testUser.getEmail()); + SiteUser updatedUser = siteUserRepository.findById(testUser.getId()).get(); assertThat(updatedUser.getNicknameModifiedAt()).isNotNull(); assertThat(response.nickname()).isEqualTo(newNickname); } @@ -208,7 +208,7 @@ class 닉네임_수정_테스트 { NicknameUpdateRequest request = new NicknameUpdateRequest("duplicatedNickname"); // when & then - assertThatCode(() -> siteUserService.updateNickname(testUser.getEmail(), request)) + assertThatCode(() -> siteUserService.updateNickname(testUser, request)) .isInstanceOf(CustomException.class) .hasMessage(NICKNAME_ALREADY_EXISTED.getMessage()); } @@ -225,7 +225,7 @@ class 닉네임_수정_테스트 { // when & then assertThatCode(() -> - siteUserService.updateNickname(testUser.getEmail(), request)) + siteUserService.updateNickname(testUser, request)) .isInstanceOf(CustomException.class) .hasMessage(createExpectedErrorMessage(modifiedAt)); } @@ -278,7 +278,7 @@ private int createLikedUniversities(SiteUser testUser) { likedUniversityRepository.save(likedUniversity1); likedUniversityRepository.save(likedUniversity2); likedUniversityRepository.save(likedUniversity3); - return likedUniversityRepository.countBySiteUser_Email(testUser.getEmail()); + return likedUniversityRepository.countBySiteUser_Id(testUser.getId()); } private MockMultipartFile createValidImageFile() { diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java index 14371486c..51958ed5d 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java @@ -40,8 +40,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - LikeResultResponse response = universityLikeService.likeUniversity( - testUser.getEmail(), 괌대학_A_지원_정보.getId()); + LikeResultResponse response = universityLikeService.likeUniversity(testUser, 괌대학_A_지원_정보.getId()); // then assertThat(response.result()).isEqualTo(LIKE_SUCCESS_MESSAGE); @@ -56,8 +55,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { saveLikedUniversity(testUser, 괌대학_A_지원_정보); // when - LikeResultResponse response = universityLikeService.likeUniversity( - testUser.getEmail(), 괌대학_A_지원_정보.getId()); + LikeResultResponse response = universityLikeService.likeUniversity(testUser, 괌대학_A_지원_정보.getId()); // then assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE); @@ -72,7 +70,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { Long invalidUniversityId = 9999L; // when & then - assertThatCode(() -> universityLikeService.likeUniversity(testUser.getEmail(), invalidUniversityId)) + assertThatCode(() -> universityLikeService.likeUniversity(testUser, invalidUniversityId)) .isInstanceOf(CustomException.class) .hasMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); } @@ -84,7 +82,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { saveLikedUniversity(testUser, 괌대학_A_지원_정보); // when - IsLikeResponse response = universityLikeService.getIsLiked(testUser.getEmail(), 괌대학_A_지원_정보.getId()); + IsLikeResponse response = universityLikeService.getIsLiked(testUser, 괌대학_A_지원_정보.getId()); // then assertThat(response.isLike()).isTrue(); @@ -96,7 +94,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - IsLikeResponse response = universityLikeService.getIsLiked(testUser.getEmail(), 괌대학_A_지원_정보.getId()); + IsLikeResponse response = universityLikeService.getIsLiked(testUser, 괌대학_A_지원_정보.getId()); // then assertThat(response.isLike()).isFalse(); @@ -109,7 +107,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { Long invalidUniversityId = 9999L; // when & then - assertThatCode(() -> universityLikeService.getIsLiked(testUser.getEmail(), invalidUniversityId)) + assertThatCode(() -> universityLikeService.getIsLiked(testUser, invalidUniversityId)) .isInstanceOf(CustomException.class) .hasMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); } diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java index 17d951614..102eb6dd6 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java @@ -52,7 +52,7 @@ void setUp() { interestedRegionRepository.save(new InterestedRegion(testUser, 영미권)); // when - UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser); // then assertThat(response.recommendedUniversities()) @@ -72,7 +72,7 @@ void setUp() { interestedCountyRepository.save(new InterestedCountry(testUser, 덴마크)); // when - UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser); // then assertThat(response.recommendedUniversities()) @@ -91,7 +91,7 @@ void setUp() { interestedCountyRepository.save(new InterestedCountry(testUser, 덴마크)); // when - UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser); // then assertThat(response.recommendedUniversities()) @@ -112,7 +112,7 @@ void setUp() { SiteUser testUser = createSiteUser(); // when - UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser); // then assertThat(response.recommendedUniversities()) From ecc186a84d6d0279fdb5df00d5a4739dbe1475c7 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 6 Feb 2025 11:31:03 +0900 Subject: [PATCH 138/158] =?UTF-8?q?refactor:=20TokenProvider=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B0=81=20=ED=86=A0=ED=81=B0=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=BA=A1=EC=8A=90?= =?UTF-8?q?=ED=99=94=20(#183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: TokenProvider 에서 각 토큰에 대한 로직을 캡슐화 * refactor: SignUpTokenProvider 생성, TokenProvider 추상화 * refactor: 함수 이름 변경 - 범용적으로 사용되는 지금 상황에 적합하도록 * chore: 달성한 todo 제거 * refactor: 변수명 변경, 불필요한 변수 할당 제가 --- .../auth/domain/TokenType.java | 8 +- .../auth/service/AuthService.java | 25 +-- .../auth/service/AuthTokenProvider.java | 53 +++++ .../auth/service/SignInService.java | 12 +- .../auth/service/SignUpService.java | 10 +- .../auth/service/SignUpTokenProvider.java | 26 +++ .../auth/service/TokenProvider.java | 28 +-- .../auth/service/TokenValidator.java | 10 +- .../security/filter/SignOutCheckFilter.java | 10 +- .../auth/service/AuthTokenProviderTest.java | 187 ++++++++++++++++++ .../auth/service/SignUpTokenProviderTest.java | 70 +++++++ .../auth/service/TokenProviderTest.java | 95 --------- .../filter/SignOutCheckFilterTest.java | 2 +- .../e2e/ApplicantsQueryTest.java | 20 +- .../solidconnection/e2e/MyPageTest.java | 10 +- .../solidconnection/e2e/MyPageUpdateTest.java | 10 +- .../solidconnection/e2e/SignInTest.java | 8 +- .../solidconnection/e2e/SignUpTest.java | 20 +- .../e2e/UniversityDetailTest.java | 12 +- .../e2e/UniversityLikeTest.java | 14 +- .../e2e/UniversityRecommendTest.java | 10 +- .../e2e/UniversitySearchTest.java | 10 +- 22 files changed, 422 insertions(+), 228 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java create mode 100644 src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java delete mode 100644 src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java index ad5607a27..caf1c7a9d 100644 --- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -7,8 +7,8 @@ public enum TokenType { ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days - KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60), // 1hour - BLACKLIST("BLACKLIST:", ACCESS.expireTime) + BLACKLIST("BLACKLIST:", ACCESS.expireTime), + SIGN_UP("SIGN_UP:", 1000 * 60 * 10), // 10min ; private final String prefix; @@ -19,7 +19,7 @@ public enum TokenType { this.expireTime = expireTime; } - public String addPrefixToSubject(String subject) { - return prefix + subject; + public String addPrefix(String string) { + return prefix + string; } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index aed6f922f..04bcadde7 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -5,37 +5,26 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.ObjectUtils; import java.time.LocalDate; -import java.util.concurrent.TimeUnit; +import java.util.Optional; -import static com.example.solidconnection.auth.domain.TokenType.ACCESS; -import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @RequiredArgsConstructor @Service public class AuthService { - private final RedisTemplate redisTemplate; - private final TokenProvider tokenProvider; + private final AuthTokenProvider authTokenProvider; /* * 로그아웃 한다. * - 엑세스 토큰을 블랙리스트에 추가한다. * */ public void signOut(String accessToken) { - redisTemplate.opsForValue().set( - BLACKLIST.addPrefixToSubject(accessToken), - accessToken, - BLACKLIST.getExpireTime(), - TimeUnit.MILLISECONDS - ); + authTokenProvider.generateAndSaveBlackListToken(accessToken); } /* @@ -56,14 +45,12 @@ public void quit(SiteUser siteUser) { * */ public ReissueResponse reissue(String subject) { // 리프레시 토큰 만료 확인 - String refreshTokenKey = REFRESH.addPrefixToSubject(subject); - String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); - if (ObjectUtils.isEmpty(refreshToken)) { + Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); + if (optionalRefreshToken.isEmpty()) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = tokenProvider.generateToken(subject, ACCESS); - tokenProvider.saveToken(newAccessToken, ACCESS); + String newAccessToken = authTokenProvider.generateAccessToken(subject); return new ReissueResponse(newAccessToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java new file mode 100644 index 000000000..da040a8d5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.siteuser.domain.SiteUser; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; + +@Component +public class AuthTokenProvider extends TokenProvider { + + public AuthTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + super(jwtProperties, redisTemplate); + } + + public String generateAccessToken(SiteUser siteUser) { + String subject = siteUser.getId().toString(); + return generateToken(subject, TokenType.ACCESS); + } + + public String generateAccessToken(String subject) { + return generateToken(subject, TokenType.ACCESS); + } + + public String generateAndSaveRefreshToken(SiteUser siteUser) { + String subject = siteUser.getId().toString(); + String refreshToken = generateToken(subject, TokenType.REFRESH); + return saveToken(refreshToken, TokenType.REFRESH); + } + + public String generateAndSaveBlackListToken(String accessToken) { + String blackListToken = generateToken(accessToken, TokenType.BLACKLIST); + return saveToken(blackListToken, TokenType.BLACKLIST); + } + + public Optional findRefreshToken(String subject) { + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); + return Optional.ofNullable(redisTemplate.opsForValue().get(refreshTokenKey)); + } + + public Optional findBlackListToken(String subject) { + String blackListTokenKey = TokenType.BLACKLIST.addPrefix(subject); + return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey)); + } + + public String getEmail(String token) { + return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index ae4947596..8ca39eb62 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -6,7 +6,6 @@ import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -20,7 +19,8 @@ @Service public class SignInService { - private final TokenProvider tokenProvider; + private final AuthTokenProvider authTokenProvider; + private final SignUpTokenProvider signUpTokenProvider; private final SiteUserRepository siteUserRepository; private final KakaoOAuthClient kakaoOAuthClient; @@ -60,15 +60,13 @@ private void resetQuitedAt(SiteUser siteUser) { } private SignInResponse getSignInInfo(SiteUser siteUser) { - String accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = authTokenProvider.generateAccessToken(siteUser); + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); return new SignInResponse(true, accessToken, refreshToken); } private FirstAccessResponse getFirstAccessInfo(KakaoUserInfoDto kakaoUserInfoDto) { - String kakaoOauthToken = tokenProvider.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); - tokenProvider.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); + String kakaoOauthToken = signUpTokenProvider.generateAndSaveSignUpToken(kakaoUserInfoDto.kakaoAccountDto().email()); return FirstAccessResponse.of(kakaoUserInfoDto, kakaoOauthToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java index 697cdbdc0..788b07e44 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -2,7 +2,6 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; @@ -28,7 +27,7 @@ public class SignUpService { private final TokenValidator tokenValidator; - private final TokenProvider tokenProvider; + private final AuthTokenProvider authTokenProvider; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -51,7 +50,7 @@ public class SignUpService { public SignUpResponse signUp(SignUpRequest signUpRequest) { // 검증 tokenValidator.validateKakaoToken(signUpRequest.kakaoOauthToken()); - String email = tokenProvider.getEmail(signUpRequest.kakaoOauthToken()); + String email = authTokenProvider.getEmail(signUpRequest.kakaoOauthToken()); validateNicknameDuplicated(signUpRequest.nickname()); validateUserNotDuplicated(email); @@ -64,9 +63,8 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { saveInterestedCountry(signUpRequest, savedSiteUser); // 토큰 발급 - String accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = authTokenProvider.generateAccessToken(siteUser); + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); return new SignUpResponse(accessToken, refreshToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java new file mode 100644 index 000000000..f04bf112b --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +public class SignUpTokenProvider extends TokenProvider { + + public SignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + super(jwtProperties, redisTemplate); + } + + public String generateAndSaveSignUpToken(String email) { + String signUpToken = generateToken(email, TokenType.SIGN_UP); + return saveToken(signUpToken, TokenType.SIGN_UP); + } + + public Optional findSignUpToken(String email) { + String signUpKey = TokenType.SIGN_UP.addPrefix(email); + return Optional.ofNullable(redisTemplate.opsForValue().get(signUpKey)); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 2dbf288ad..f5f638ab3 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -2,33 +2,27 @@ import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.config.security.JwtProperties; -import com.example.solidconnection.siteuser.domain.SiteUser; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; import java.util.Date; import java.util.concurrent.TimeUnit; import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; -@RequiredArgsConstructor -@Component -public class TokenProvider { +public abstract class TokenProvider { - private final RedisTemplate redisTemplate; - private final JwtProperties jwtProperties; + protected final JwtProperties jwtProperties; + protected final RedisTemplate redisTemplate; - public String generateToken(SiteUser siteUser, TokenType tokenType) { - String subject = siteUser.getId().toString(); - return generateToken(subject, tokenType); + public TokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + this.jwtProperties = jwtProperties; + this.redisTemplate = redisTemplate; } - public String generateToken(String string, TokenType tokenType) { + protected final String generateToken(String string, TokenType tokenType) { Claims claims = Jwts.claims().setSubject(string); Date now = new Date(); Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); @@ -40,18 +34,14 @@ public String generateToken(String string, TokenType tokenType) { .compact(); } - public String saveToken(String token, TokenType tokenType) { + protected final String saveToken(String token, TokenType tokenType) { String subject = parseSubject(token, jwtProperties.secret()); redisTemplate.opsForValue().set( - tokenType.addPrefixToSubject(subject), + tokenType.addPrefix(subject), token, tokenType.getExpireTime(), TimeUnit.MILLISECONDS ); return token; } - - public String getEmail(String token) { - return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); - } } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java index 8c17ad00c..a87a4aa2c 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java @@ -14,8 +14,8 @@ import java.util.Objects; import static com.example.solidconnection.auth.domain.TokenType.ACCESS; -import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.domain.TokenType.SIGN_UP; import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; import static com.example.solidconnection.custom.exception.ErrorCode.EMPTY_TOKEN; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN; @@ -38,7 +38,7 @@ public void validateAccessToken(String token) { public void validateKakaoToken(String token) { validateTokenNotEmpty(token); - validateTokenNotExpired(token, KAKAO_OAUTH); + validateTokenNotExpired(token, SIGN_UP); validateKakaoTokenNotUsed(token); } @@ -55,7 +55,7 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { if (tokenType.equals(ACCESS)) { throw new CustomException(ACCESS_TOKEN_EXPIRED); } - if (token.equals(KAKAO_OAUTH)) { + if (token.equals(SIGN_UP)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } @@ -63,14 +63,14 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { private void validateRefreshToken(String token) { String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email)) == null) { + if (redisTemplate.opsForValue().get(REFRESH.addPrefix(email)) == null) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } } private void validateKakaoTokenNotUsed(String token) { String email = getClaim(token).getSubject(); - if (!Objects.equals(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email)), token)) { + if (!Objects.equals(redisTemplate.opsForValue().get(SIGN_UP.addPrefix(email)), token)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } diff --git a/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java index 90fb6866e..2cef8d1ac 100644 --- a/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java @@ -1,6 +1,6 @@ package com.example.solidconnection.custom.security.filter; -import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -8,13 +8,11 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @@ -22,8 +20,7 @@ @RequiredArgsConstructor public class SignOutCheckFilter extends OncePerRequestFilter { - private final RedisTemplate redisTemplate; - private final JwtProperties jwtProperties; + private final AuthTokenProvider authTokenProvider; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @@ -37,7 +34,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, } private boolean hasSignedOut(String accessToken) { - String blacklistKey = BLACKLIST.addPrefixToSubject(accessToken); - return redisTemplate.opsForValue().get(blacklistKey) != null; + return authTokenProvider.findBlackListToken(accessToken).isPresent(); } } diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java new file mode 100644 index 000000000..f5616973f --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -0,0 +1,187 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.util.JwtUtils; +import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("인증 토큰 제공자 테스트") +class AuthTokenProviderTest { + + @Autowired + private AuthTokenProvider authTokenProvider; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + private SiteUser siteUser; + private String subject; + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + subject = siteUser.getId().toString(); + } + + @Nested + class 액세스_토큰을_제공한다 { + + @Test + void SiteUser_로_액세스_토큰을_생성한다() { + // when + String token = authTokenProvider.generateAccessToken(siteUser); + + // then + String actualSubject = JwtUtils.parseSubject(token, jwtProperties.secret()); + assertThat(actualSubject).isEqualTo(subject); + } + + @Test + void subject_로_액세스_토큰을_생성한다() { + // given + String subject = "subject123"; + + // when + String token = authTokenProvider.generateAccessToken(subject); + + // then + String actualSubject = JwtUtils.parseSubject(token, jwtProperties.secret()); + assertThat(actualSubject).isEqualTo(subject); + } + } + + @Nested + class 리프레시_토큰을_제공한다 { + + @Test + void SiteUser_로_리프레시_토큰을_생성하고_저장한다() { + // when + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + + // then + String actualSubject = JwtUtils.parseSubject(refreshToken, jwtProperties.secret()); + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); + assertAll( + () -> assertThat(actualSubject).isEqualTo(subject), + () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isEqualTo(refreshToken) + ); + } + + @Test + void 저장된_리프레시_토큰을_조회한다() { + // given + String refreshToken = "refreshToken"; + redisTemplate.opsForValue().set(TokenType.REFRESH.addPrefix(subject), refreshToken); + + // when + Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); + + // then + assertThat(optionalRefreshToken.get()).isEqualTo(refreshToken); + } + + @Test + void 저장되지_않은_리프레시_토큰을_조회한다() { + // when + Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); + + // then + assertThat(optionalRefreshToken).isEmpty(); + } + } + + @Nested + class 블랙리스트_토큰을_제공한다 { + + @Test + void 엑세스_토큰으로_블랙리스트_토큰을_생성하고_저장한다() { + // when + String accessToken = "accessToken"; + String blackListToken = authTokenProvider.generateAndSaveBlackListToken(accessToken); + + // then + String actualSubject = JwtUtils.parseSubject(blackListToken, jwtProperties.secret()); + String blackListTokenKey = TokenType.BLACKLIST.addPrefix(accessToken); + assertAll( + () -> assertThat(actualSubject).isEqualTo(accessToken), + () -> assertThat(redisTemplate.opsForValue().get(blackListTokenKey)).isEqualTo(blackListToken) + ); + } + + @Test + void 저장된_블랙리스트_토큰을_조회한다() { + // given + String accessToken = "accessToken"; + String blackListToken = "token"; + redisTemplate.opsForValue().set(TokenType.BLACKLIST.addPrefix(accessToken), blackListToken); + + // when + Optional optionalBlackListToken = authTokenProvider.findBlackListToken(accessToken); + + // then + assertThat(optionalBlackListToken).hasValue(blackListToken); + } + + @Test + void 저장되지_않은_블랙리스트_토큰을_조회한다() { + // when + Optional optionalBlackListToken = authTokenProvider.findBlackListToken("accessToken"); + + // then + assertThat(optionalBlackListToken).isEmpty(); + } + } + + @Test + void 토큰을_생성한다() { + // when + String subject = "subject123"; + String token = authTokenProvider.generateToken(subject, TokenType.ACCESS); + + // then + String extractedSubject = Jwts.parser() + .setSigningKey(jwtProperties.secret()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + assertThat(subject).isEqualTo(extractedSubject); + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java new file mode 100644 index 000000000..382008d8c --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java @@ -0,0 +1,70 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.util.JwtUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("회원가입 토큰 제공자 테스트") +class SignUpTokenProviderTest { + + @Autowired + private SignUpTokenProvider signUpTokenProvider; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 회원가입_토큰을_생성하고_저장한다() { + // when + String email = "email"; + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email); + + // then + String actualSubject = JwtUtils.parseSubject(signUpToken, jwtProperties.secret()); + String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); + assertAll( + () -> assertThat(actualSubject).isEqualTo(email), + () -> assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isEqualTo(signUpToken) + ); + } + + @Test + void 저장된_회원가입_토큰을_조회한다() { + // given + String email = "email"; + String signUpToken = "token"; + redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), signUpToken); + + // when + Optional actualSignUpToken = signUpTokenProvider.findSignUpToken(email); + + // then + assertThat(actualSignUpToken).hasValue(signUpToken); + } + + @Test + void 저장되지_않은_회원가입_토큰을_조회한다() { + // given + String email = "email"; + + // when + Optional actualSignUpToken = signUpTokenProvider.findSignUpToken(email); + + // then + assertThat(actualSignUpToken).isEmpty(); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java deleted file mode 100644 index 8cc91e2c0..000000000 --- a/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.config.security.JwtProperties; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.support.TestContainerSpringBootTest; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; - -import java.util.Date; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - -@TestContainerSpringBootTest -@DisplayName("TokenProvider 테스트") -class TokenProviderTest { - - @Autowired - private TokenProvider tokenProvider; - - @Autowired - private RedisTemplate redisTemplate; - - @Autowired - private JwtProperties jwtProperties; - - @Test - void 토큰을_생성한다() { - // when - String subject = "subject123"; - String token = tokenProvider.generateToken(subject, TokenType.ACCESS); - - // then - String extractedSubject = Jwts.parser() - .setSigningKey(jwtProperties.secret()) - .parseClaimsJws(token) - .getBody() - .getSubject(); - assertThat(subject).isEqualTo(extractedSubject); - } - - @Nested - class 토큰을_저장한다 { - - @Test - void 토큰이_유효하면_저장한다() { - // given - String subject = "subject321"; - String token = createValidToken(subject); - - // when - tokenProvider.saveToken(token, TokenType.ACCESS); - - // then - String savedToken = redisTemplate.opsForValue().get(TokenType.ACCESS.addPrefixToSubject(subject)); - assertThat(savedToken).isEqualTo(token); - } - - @Test - void 토큰이_유효하지않으면_예외가_발생한다() { - // given - String token = createInvalidToken(); - - // when & then - assertThatCode(() -> tokenProvider.saveToken(token, TokenType.REFRESH)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); - } - } - - private String createValidToken(String subject) { - return Jwts.builder() - .setSubject(subject) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - } - - private String createInvalidToken() { - return Jwts.builder() - .setSubject("subject") - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() - 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - } -} diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java index 7eac22c71..a11d8d28a 100644 --- a/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java @@ -59,7 +59,7 @@ void setUp() { // given String token = createToken(subject); request = createRequest(token); - String refreshTokenKey = BLACKLIST.addPrefixToSubject(token); + String refreshTokenKey = BLACKLIST.addPrefix(token); redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); // when & then diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 40f39e646..fa2cf0b0b 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -7,8 +7,7 @@ import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; @@ -36,7 +35,7 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { private ApplicationRepository applicationRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; private String adminAccessToken; @@ -65,17 +64,14 @@ public void setUpUserAndToken() { SiteUser 사용자6 = siteUserRepository.save(createSiteUserByEmail("email6")); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(나, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(나, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(나); + authTokenProvider.generateAndSaveRefreshToken(나); - adminAccessToken = tokenProvider.generateToken(사용자5_관리자, TokenType.ACCESS); - String adminRefreshToken = tokenProvider.generateToken(사용자5_관리자, TokenType.REFRESH); - tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); + adminAccessToken = authTokenProvider.generateAccessToken(사용자5_관리자); + authTokenProvider.generateAndSaveRefreshToken(사용자5_관리자); - user6AccessToken = tokenProvider.generateToken(사용자6, TokenType.ACCESS); - String user6RefreshToken = tokenProvider.generateToken(사용자6, TokenType.REFRESH); - tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); + user6AccessToken = authTokenProvider.generateAccessToken(사용자6); + authTokenProvider.generateAndSaveRefreshToken(사용자6); // setUp - 지원 정보 저장 Gpa gpa = createDummyGpa(); diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index 567b1016d..7a0ae07f4 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -25,7 +24,7 @@ class MyPageTest extends BaseEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; @@ -35,9 +34,8 @@ public void setUpUserAndToken() { siteUser = siteUserRepository.save(createSiteUserByEmail("email")); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java index 025ddb7d7..b16f3b822 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; @@ -31,7 +30,7 @@ class MyPageUpdateTest extends BaseEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; @@ -44,9 +43,8 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index 26eba657a..8d3ddc75f 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -18,8 +18,8 @@ import java.time.LocalDate; -import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.domain.TokenType.SIGN_UP; import static com.example.solidconnection.e2e.DynamicFixture.createKakaoUserInfoDtoByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.scheduler.UserRemovalScheduler.ACCOUNT_RECOVER_DURATION; @@ -65,7 +65,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.nickname()).isEqualTo(kakaoProfileDto.nickname()), () -> assertThat(response.profileImageUrl()).isEqualTo(kakaoProfileDto.profileImageUrl()), () -> assertThat(response.kakaoOauthToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(SIGN_UP.addPrefix(email))) .as("카카오 인증 토큰을 저장한다.") .isEqualTo(response.kakaoOauthToken()); } @@ -95,7 +95,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(siteUser.getId().toString()))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefix(siteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -130,7 +130,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull(), () -> assertThat(updatedSiteUser.getQuitedAt()).isNull()); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(siteUser.getId().toString()))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefix(siteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 1eb152387..1bbe150a8 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -2,7 +2,8 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.AuthTokenProvider; +import com.example.solidconnection.auth.service.SignUpTokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -27,7 +28,6 @@ import java.util.List; -import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; @@ -56,7 +56,10 @@ class SignUpTest extends BaseEndToEndTest { InterestedCountyRepository interestedCountyRepository; @Autowired - TokenProvider tokenProvider; + AuthTokenProvider authTokenProvider; + + @Autowired + SignUpTokenProvider signUpTokenProvider; @Autowired RedisTemplate redisTemplate; @@ -71,8 +74,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); - tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); @@ -110,7 +112,7 @@ class SignUpTest extends BaseEndToEndTest { () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(savedSiteUser.getId().toString()))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefix(savedSiteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -124,8 +126,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); - tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -150,8 +151,7 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = tokenProvider.generateToken(alreadyExistEmail, KAKAO_OAUTH); - tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index b7e112d00..01b2b5730 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.LanguageRequirementResponse; @@ -24,7 +23,7 @@ class UniversityDetailTest extends UniversityDataSetUpEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; @@ -36,11 +35,10 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } - + @Test void 대학교_정보를_조회한다() { // request - 요청 diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index 301b373c4..3b5733d82 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -28,7 +27,7 @@ import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_CANCELED_MESSAGE; import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_SUCCESS_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; @DisplayName("대학교 좋아요 테스트") class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { @@ -43,7 +42,7 @@ class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { private LikedUniversityRepository likedUniversityRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; private SiteUser siteUser; @@ -55,9 +54,8 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test @@ -138,7 +136,7 @@ public void setUpUserAndToken() { // request - 요청 IsLikeResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .get("/university/"+ 괌대학_A_지원_정보.getId() +"/like") + .get("/university/" + 괌대학_A_지원_정보.getId() + "/like") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(IsLikeResponse.class); diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index 358f779cd..8e1e8184f 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; import com.example.solidconnection.repositories.InterestedCountyRepository; @@ -38,7 +37,7 @@ class UniversityRecommendTest extends UniversityDataSetUpEndToEndTest { private InterestedCountyRepository interestedCountyRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; @Autowired private GeneralUniversityRecommendService generalUniversityRecommendService; @@ -54,9 +53,8 @@ void setUp() { generalUniversityRecommendService.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java index 22abbfb53..3b508d014 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; @@ -23,7 +22,7 @@ class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; private SiteUser siteUser; @@ -35,9 +34,8 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test From b9a4af846fa7287b952363a12ad53531e80ea328 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Fri, 7 Feb 2025 20:29:29 +0900 Subject: [PATCH 139/158] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EA=B5=AC=ED=98=84=20(#184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: kakao oauth 관련 값을 ConfigurationProperties 로 변경 * refactor: Oauth -> OAuth 용어 통일 * refactor: SignInService 가 로그인만 담당하도록 * refactor: 사용자 정보를 가져오는 의미가 드러나도록 함수명 변경 * refactor: 다양한 OAuth 종류를 포괄하도록 이름 변경 * refactor: OAuthService 추상화 * refactor: oauth 관련 서비스 패키지 이동 * feat: 애플 OAuth 설정 관리 클래스 생성 * feat: 애플 client secret 생성 클래스 생성 * feat: 애플 OAuthClient 구현 * feat: 애플 OAuthService 구현 * chore: 주석 내용 수정 * refactor: 회원가입 토큰에 가입 방법이 포함되도록 SignUpTokenProvider 수정 * refactor: 다양한 회원 가입이 가능하도록 회원 가입 로직 수정 * feat: 애플 인증 엔드포인트 추가 * feat: 애플 공개키를 가져와서 id_token 을 하도록 * refactor: 공개키를 캐싱하도록 * refactor: 잘못된 만료 시간 수정 * refactor: 응답 필드 명 변경 * refactor: code 요청 유효성 검사 추가 * chore: 주석 수정 --- .../auth/client/AppleOAuthClient.java | 83 ++++++++ .../AppleOAuthClientSecretProvider.java | 73 +++++++ .../auth/client/ApplePublicKeyProvider.java | 94 +++++++++ .../auth/client/KakaoOAuthClient.java | 48 ++--- .../auth/controller/AuthController.java | 38 ++-- .../auth/dto/SignInResponse.java | 6 +- .../auth/dto/SignUpRequest.java | 10 +- .../auth/dto/SignUpResponse.java | 6 - .../auth/dto/kakao/FirstAccessResponse.java | 19 -- .../auth/dto/kakao/KakaoCodeRequest.java | 5 - .../auth/dto/kakao/KakaoOauthResponse.java | 4 - .../auth/dto/oauth/AppleTokenDto.java | 10 + .../auth/dto/oauth/AppleUserInfoDto.java | 24 +++ .../dto/{kakao => oauth}/KakaoTokenDto.java | 2 +- .../{kakao => oauth}/KakaoUserInfoDto.java | 20 +- .../auth/dto/oauth/OAuthCodeRequest.java | 9 + .../auth/dto/oauth/OAuthResponse.java | 4 + .../auth/dto/oauth/OAuthSignInResponse.java | 7 + .../auth/dto/oauth/OAuthUserInfoDto.java | 10 + .../auth/dto/oauth/SignUpPrepareResponse.java | 19 ++ .../auth/service/SignInService.java | 55 +----- .../auth/service/SignUpTokenProvider.java | 26 --- .../auth/service/TokenValidator.java | 84 -------- .../auth/service/oauth/AppleOAuthService.java | 30 +++ .../auth/service/oauth/KakaoOAuthService.java | 30 +++ .../auth/service/oauth/OAuthService.java | 61 ++++++ .../OAuthSignUpService.java} | 54 +++--- .../service/oauth/SignUpTokenProvider.java | 81 ++++++++ .../client/AppleOAuthClientProperties.java | 15 ++ .../client/KakaoOAuthClientProperties.java | 12 ++ .../{rest => client}/RestTemplateConfig.java | 2 +- .../custom/exception/ErrorCode.java | 14 ++ .../solidconnection/util/JwtUtils.java | 10 +- .../auth/service/SignInServiceTest.java | 88 +++++++++ .../auth/service/SignUpTokenProviderTest.java | 70 ------- .../oauth/SignUpTokenProviderTest.java | 182 ++++++++++++++++++ .../solidconnection/e2e/DynamicFixture.java | 2 +- .../solidconnection/e2e/SignInTest.java | 42 ++-- .../solidconnection/e2e/SignUpTest.java | 18 +- 39 files changed, 979 insertions(+), 388 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java create mode 100644 src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java create mode 100644 src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java rename src/main/java/com/example/solidconnection/auth/dto/{kakao => oauth}/KakaoTokenDto.java (85%) rename src/main/java/com/example/solidconnection/auth/dto/{kakao => oauth}/KakaoUserInfoDto.java (61%) create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthUserInfoDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java delete mode 100644 src/main/java/com/example/solidconnection/auth/service/TokenValidator.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java rename src/main/java/com/example/solidconnection/auth/service/{SignUpService.java => oauth/OAuthSignUpService.java} (62%) create mode 100644 src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java create mode 100644 src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java rename src/main/java/com/example/solidconnection/config/{rest => client}/RestTemplateConfig.java (91%) create mode 100644 src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java new file mode 100644 index 000000000..aef1309af --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java @@ -0,0 +1,83 @@ +package com.example.solidconnection.auth.client; + +import com.example.solidconnection.auth.dto.oauth.AppleTokenDto; +import com.example.solidconnection.auth.dto.oauth.AppleUserInfoDto; +import com.example.solidconnection.config.client.AppleOAuthClientProperties; +import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.security.PublicKey; +import java.util.Objects; + +import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_AUTHORIZATION_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN; + +/* + * 애플 인증을 위한 OAuth2 클라이언트 + * https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens + * */ +@Component +@RequiredArgsConstructor +public class AppleOAuthClient { + + private final RestTemplate restTemplate; + private final AppleOAuthClientProperties properties; + private final AppleOAuthClientSecretProvider clientSecretProvider; + private final ApplePublicKeyProvider publicKeyProvider; + + public AppleUserInfoDto processOAuth(String code) { + String idToken = requestIdToken(code); + PublicKey applePublicKey = publicKeyProvider.getApplePublicKey(idToken); + return new AppleUserInfoDto(parseEmailFromToken(applePublicKey, idToken)); + } + + public String requestIdToken(String code) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap formData = buildFormData(code); + + try { + ResponseEntity response = restTemplate.exchange( + properties.tokenUrl(), + HttpMethod.POST, + new HttpEntity<>(formData, headers), + AppleTokenDto.class + ); + return Objects.requireNonNull(response.getBody()).idToken(); + } catch (Exception e) { + throw new CustomException(APPLE_AUTHORIZATION_FAILED, e.getMessage()); + } + } + + private MultiValueMap buildFormData(String code) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("client_id", properties.clientId()); + formData.add("client_secret", clientSecretProvider.generateClientSecret()); + formData.add("code", code); + formData.add("grant_type", "authorization_code"); + formData.add("redirect_uri", properties.redirectUrl()); + return formData; + } + + private String parseEmailFromToken(PublicKey applePublicKey, String idToken) { + try { + return Jwts.parser() + .setSigningKey(applePublicKey) + .parseClaimsJws(idToken) + .getBody() + .get("email", String.class); + } catch (Exception e) { + throw new CustomException(INVALID_APPLE_ID_TOKEN); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java new file mode 100644 index 000000000..2de0b7291 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java @@ -0,0 +1,73 @@ +package com.example.solidconnection.auth.client; + +import com.example.solidconnection.config.client.AppleOAuthClientProperties; +import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.apache.tomcat.util.codec.binary.Base64; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; +import java.util.stream.Collectors; + +import static com.example.solidconnection.custom.exception.ErrorCode.FAILED_TO_READ_APPLE_PRIVATE_KEY; + +/* + * 애플 OAuth 에 필요하 클라이언트 시크릿은 매번 동적으로 생성해야 한다. + * 클라이언트 시크릿은 애플 개발자 계정에서 발급받은 개인키(*.p8)를 사용하여 JWT 를 생성한다. + * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret + * */ +@Component +@RequiredArgsConstructor +public class AppleOAuthClientSecretProvider { + + private static final String KEY_ID_HEADER = "kid"; + private static final long TOKEN_DURATION = 1000 * 60 * 10; // 10min + private static final String SECRET_KEY_PATH = "secret/AppleOAuthKey.p8"; + + private final AppleOAuthClientProperties appleOAuthClientProperties; + private PrivateKey privateKey; + + @PostConstruct + private void initPrivateKey() { + privateKey = readPrivateKey(); + } + + public String generateClientSecret() { + Date now = new Date(); + Date expiration = new Date(now.getTime() + TOKEN_DURATION); + + return Jwts.builder() + .setHeaderParam("alg", "ES256") + .setHeaderParam(KEY_ID_HEADER, appleOAuthClientProperties.keyId()) + .setSubject(appleOAuthClientProperties.clientId()) + .setIssuer(appleOAuthClientProperties.teamId()) + .setAudience(appleOAuthClientProperties.clientSecretAudienceUrl()) + .setExpiration(expiration) + .signWith(SignatureAlgorithm.ES256, privateKey) + .compact(); + } + + private PrivateKey readPrivateKey() { + try (InputStream is = getClass().getClassLoader().getResourceAsStream(SECRET_KEY_PATH); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + + String secretKey = reader.lines().collect(Collectors.joining("\n")); + byte[] encoded = Base64.decodeBase64(secretKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(keySpec); + } catch (Exception e) { + throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java b/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java new file mode 100644 index 000000000..1cc708cc7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java @@ -0,0 +1,94 @@ +package com.example.solidconnection.auth.client; + +import com.example.solidconnection.config.client.AppleOAuthClientProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ExpiredJwtException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_ID_TOKEN_EXPIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_PUBLIC_KEY_NOT_FOUND; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN; +import static org.apache.tomcat.util.codec.binary.Base64.decodeBase64URLSafe; + +/* +* idToken 검증을 위해서 애플의 공개키를 가져온다. +* - 애플 공개키는 주기적으로 바뀐다. 이를 효율적으로 관리하기 위해 캐싱한다. +* - idToken 의 헤더에 있는 kid 값에 해당하는 키가 캐싱되어있으면 그것을 반환한다. +* - 그렇지 않다면 공개키가 바뀌었다는 뜻이므로, JSON 형식의 공개키 목록을 받아오고 캐시를 갱신한다. +* https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature +* */ +@Component +@RequiredArgsConstructor +public class ApplePublicKeyProvider { + + private final AppleOAuthClientProperties properties; + private final RestTemplate restTemplate; + + private final Map applePublicKeyCache = new ConcurrentHashMap<>(); + + public PublicKey getApplePublicKey(String idToken) { + try { + String kid = getKeyIdFromTokenHeader(idToken); + if (applePublicKeyCache.containsKey(kid)) { + return applePublicKeyCache.get(kid); + } + + fetchApplePublicKeys(); + if (applePublicKeyCache.containsKey(kid)) { + return applePublicKeyCache.get(kid); + } else { + throw new CustomException(APPLE_PUBLIC_KEY_NOT_FOUND); + } + } catch (ExpiredJwtException e) { + throw new CustomException(APPLE_ID_TOKEN_EXPIRED); + } catch (Exception e) { + throw new CustomException(INVALID_APPLE_ID_TOKEN); + } + } + + /* + * idToken 은 JWS 이므로, 원칙적으로는 서명까지 검증되어야 parsing 이 가능하다 + * 하지만 이 시점에서는 서명(=공개키)을 알 수 없으므로, Jwt 를 직접 인코딩하여 헤더를 가져온다. + * */ + private String getKeyIdFromTokenHeader(String idToken) throws JsonProcessingException { + String[] jwtParts = idToken.split("\\."); + if (jwtParts.length < 2) { + throw new CustomException(INVALID_APPLE_ID_TOKEN); + } + String headerJson = new String(Base64.getUrlDecoder().decode(jwtParts[0]), StandardCharsets.UTF_8); + return new ObjectMapper().readTree(headerJson).get("kid").asText(); + } + + private void fetchApplePublicKeys() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + ResponseEntity response = restTemplate.getForEntity(properties.publicKeyUrl(), String.class); + JsonNode jsonNode = objectMapper.readTree(response.getBody()).get("keys"); + + applePublicKeyCache.clear(); + for (JsonNode key : jsonNode) { + applePublicKeyCache.put(key.get("kid").asText(), generatePublicKey(key)); + } + } + + private PublicKey generatePublicKey(JsonNode key) throws Exception { + BigInteger modulus = new BigInteger(1, decodeBase64URLSafe(key.get("n").asText())); + BigInteger exponent = new BigInteger(1, decodeBase64URLSafe(key.get("e").asText())); + RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); + return KeyFactory.getInstance("RSA").generatePublic(spec); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java index 9862d0074..5d625cb7c 100644 --- a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java +++ b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java @@ -1,10 +1,10 @@ package com.example.solidconnection.auth.client; -import com.example.solidconnection.auth.dto.kakao.KakaoTokenDto; -import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.KakaoTokenDto; +import com.example.solidconnection.auth.dto.oauth.KakaoUserInfoDto; +import com.example.solidconnection.config.client.KakaoOAuthClientProperties; import com.example.solidconnection.custom.exception.CustomException; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -20,38 +20,24 @@ import static com.example.solidconnection.custom.exception.ErrorCode.KAKAO_REDIRECT_URI_MISMATCH; import static com.example.solidconnection.custom.exception.ErrorCode.KAKAO_USER_INFO_FAIL; +/* + * 카카오 인증을 위한 OAuth2 클라이언트 + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + * */ @Component @RequiredArgsConstructor public class KakaoOAuthClient { private final RestTemplate restTemplate; + private final KakaoOAuthClientProperties kakaoOAuthClientProperties; - @Value("${kakao.redirect_uri}") - public String redirectUri; - - @Value("${kakao.client_id}") - private String clientId; - - @Value("${kakao.token_url}") - private String tokenUrl; - - @Value("${kakao.user_info_url}") - private String userInfoUrl; - - /* - * 클라이언트에서 사용자가 카카오 로그인을 하면, 클라이언트는 '카카오 인가 코드'를 받아, 서버에 넘겨준다. - * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code - * 서버는 카카오 인증 코드를 사용해 카카오 서버로부터 '카카오 토큰'을 받아온다. - * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token - * 그리고 카카오 엑세스 토큰으로 카카오 서버에 요청해 '카카오 사용자 정보'를 받아온다. - * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info - * */ - public KakaoUserInfoDto processOauth(String code) { + public KakaoUserInfoDto getUserInfo(String code) { String kakaoAccessToken = getKakaoAccessToken(code); return getKakaoUserInfo(kakaoAccessToken); } - // 카카오 토큰 요청 private String getKakaoAccessToken(String code) { try { ResponseEntity response = restTemplate.exchange( @@ -72,30 +58,26 @@ private String getKakaoAccessToken(String code) { } } - // 카카오 엑세스 토큰 요청하는 URI 생성 private String buildTokenUri(String code) { - return UriComponentsBuilder.fromHttpUrl(tokenUrl) + return UriComponentsBuilder.fromHttpUrl(kakaoOAuthClientProperties.tokenUrl()) .queryParam("grant_type", "authorization_code") - .queryParam("client_id", clientId) - .queryParam("redirect_uri", redirectUri) + .queryParam("client_id", kakaoOAuthClientProperties.clientId()) + .queryParam("redirect_uri", kakaoOAuthClientProperties.redirectUrl()) .queryParam("code", code) .toUriString(); } - // 카카오 사용자 정보 요청 private KakaoUserInfoDto getKakaoUserInfo(String accessToken) { HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); - // 사용자의 정보 요청 ResponseEntity response = restTemplate.exchange( - userInfoUrl, + kakaoOAuthClientProperties.userInfoUrl(), HttpMethod.GET, new HttpEntity<>(headers), KakaoUserInfoDto.class ); - // 응답 예외처리 if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { return response.getBody(); } else { diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 1f6415157..aa3ce4f20 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -1,13 +1,14 @@ package com.example.solidconnection.auth.controller; import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; +import com.example.solidconnection.auth.dto.oauth.OAuthResponse; import com.example.solidconnection.auth.service.AuthService; -import com.example.solidconnection.auth.service.SignInService; -import com.example.solidconnection.auth.service.SignUpService; +import com.example.solidconnection.auth.service.oauth.AppleOAuthService; +import com.example.solidconnection.auth.service.oauth.KakaoOAuthService; +import com.example.solidconnection.auth.service.oauth.OAuthSignUpService; import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.custom.resolver.ExpiredToken; import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; @@ -27,23 +28,32 @@ public class AuthController { private final AuthService authService; - private final SignUpService signUpService; - private final SignInService signInService; + private final OAuthSignUpService oAuthSignUpService; + private final AppleOAuthService appleOAuthService; + private final KakaoOAuthService kakaoOAuthService; + + @PostMapping("/apple") + public ResponseEntity processAppleOAuth( + @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest + ) { + OAuthResponse oAuthResponse = appleOAuthService.processOAuth(oAuthCodeRequest); + return ResponseEntity.ok(oAuthResponse); + } @PostMapping("/kakao") - public ResponseEntity processKakaoOauth( - @RequestBody KakaoCodeRequest kakaoCodeRequest + public ResponseEntity processKakaoOAuth( + @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest ) { - KakaoOauthResponse kakaoOauthResponse = signInService.signIn(kakaoCodeRequest); - return ResponseEntity.ok(kakaoOauthResponse); + OAuthResponse oAuthResponse = kakaoOAuthService.processOAuth(oAuthCodeRequest); + return ResponseEntity.ok(oAuthResponse); } @PostMapping("/sign-up") - public ResponseEntity signUp( + public ResponseEntity signUp( @Valid @RequestBody SignUpRequest signUpRequest ) { - SignUpResponse signUpResponseDto = signUpService.signUp(signUpRequest); - return ResponseEntity.ok(signUpResponseDto); + SignInResponse signInResponse = oAuthSignUpService.signUp(signUpRequest); + return ResponseEntity.ok(signInResponse); } @PostMapping("/sign-out") diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java index 400491b42..a4ae442e2 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java @@ -1,9 +1,7 @@ package com.example.solidconnection.auth.dto; -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; - public record SignInResponse( - boolean isRegistered, String accessToken, - String refreshToken) implements KakaoOauthResponse { + String refreshToken +) { } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java index fcb68cad1..b28b467bd 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.auth.dto; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; @@ -10,7 +11,7 @@ import java.util.List; public record SignUpRequest( - String kakaoOauthToken, + String signUpToken, List interestedRegions, List interestedCountries, PreparationStatus preparationStatus, @@ -23,15 +24,16 @@ public record SignUpRequest( @JsonFormat(pattern = "yyyy-MM-dd") String birth) { - public SiteUser toSiteUser(String email, Role role) { + public SiteUser toSiteUser(String email, AuthType authType) { return new SiteUser( email, this.nickname, this.profileImageUrl, this.birth, this.preparationStatus, - role, - this.gender + Role.MENTEE, + this.gender, + authType ); } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java deleted file mode 100644 index 2d74610cc..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.solidconnection.auth.dto; - -public record SignUpResponse( - String accessToken, - String refreshToken) { -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java deleted file mode 100644 index 6d7130bf0..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -public record FirstAccessResponse( - boolean isRegistered, - String nickname, - String email, - String profileImageUrl, - String kakaoOauthToken) implements KakaoOauthResponse { - - public static FirstAccessResponse of(KakaoUserInfoDto kakaoUserInfoDto, String kakaoOauthToken) { - return new FirstAccessResponse( - false, - kakaoUserInfoDto.kakaoAccountDto().profile().nickname(), - kakaoUserInfoDto.kakaoAccountDto().email(), - kakaoUserInfoDto.kakaoAccountDto().profile().profileImageUrl(), - kakaoOauthToken - ); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java deleted file mode 100644 index 4fcfc5576..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -public record KakaoCodeRequest( - String code) { -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java deleted file mode 100644 index 1e2320e35..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -public interface KakaoOauthResponse { -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java new file mode 100644 index 000000000..6772cb2c2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.auth.dto.oauth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record AppleTokenDto( + @JsonProperty("id_token") String idToken +) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java new file mode 100644 index 000000000..5c4363e51 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.auth.dto.oauth; + +/* +* 애플로부터 사용자의 정보를 받아올 때 사용한다. +* 카카오와 달리 애플은 더 엄격하게 사용자 정보를 관리하여, 이름이나 프로필 이미지 url 을 제공하지 않는다. +* 따라서 닉네임, 프로필 정보는 null 을 반환한다. +* */ +public record AppleUserInfoDto(String email) implements OAuthUserInfoDto { + + @Override + public String getEmail() { + return email; + } + + @Override + public String getProfileImageUrl() { + return null; + } + + @Override + public String getNickname() { + return null; + } +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java similarity index 85% rename from src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java rename to src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java index 767645e3b..6d4ccd10c 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.dto.kakao; +package com.example.solidconnection.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoUserInfoDto.java similarity index 61% rename from src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java rename to src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoUserInfoDto.java index 85aea091d..fbd975b50 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoUserInfoDto.java @@ -1,11 +1,11 @@ -package com.example.solidconnection.auth.dto.kakao; +package com.example.solidconnection.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public record KakaoUserInfoDto( - @JsonProperty("kakao_account") KakaoAccountDto kakaoAccountDto) { + @JsonProperty("kakao_account") KakaoAccountDto kakaoAccountDto) implements OAuthUserInfoDto { @JsonIgnoreProperties(ignoreUnknown = true) public record KakaoAccountDto( @@ -16,6 +16,22 @@ public record KakaoAccountDto( public record KakaoProfileDto( @JsonProperty("profile_image_url") String profileImageUrl, String nickname) { + } } + + @Override + public String getEmail() { + return kakaoAccountDto.email; + } + + @Override + public String getProfileImageUrl() { + return kakaoAccountDto.profile.profileImageUrl; + } + + @Override + public String getNickname() { + return kakaoAccountDto.profile.nickname; + } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java new file mode 100644 index 000000000..abbdb7802 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.auth.dto.oauth; + +import jakarta.validation.constraints.NotBlank; + +public record OAuthCodeRequest( + + @NotBlank(message = "인증 코드를 입력해주세요.") + String code) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java new file mode 100644 index 000000000..ddbe121f7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java @@ -0,0 +1,4 @@ +package com.example.solidconnection.auth.dto.oauth; + +public interface OAuthResponse { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java new file mode 100644 index 000000000..8ad429876 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.auth.dto.oauth; + +public record OAuthSignInResponse( + boolean isRegistered, + String accessToken, + String refreshToken) implements OAuthResponse { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthUserInfoDto.java new file mode 100644 index 000000000..ed794851b --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthUserInfoDto.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.auth.dto.oauth; + +public interface OAuthUserInfoDto { + + String getEmail(); + + String getProfileImageUrl(); + + String getNickname(); +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java new file mode 100644 index 000000000..5a6c60c57 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.auth.dto.oauth; + +public record SignUpPrepareResponse( + boolean isRegistered, + String nickname, + String email, + String profileImageUrl, + String signUpToken) implements OAuthResponse { + + public static SignUpPrepareResponse of(OAuthUserInfoDto oAuthUserInfoDto, String signUpToken) { + return new SignUpPrepareResponse( + false, + oAuthUserInfoDto.getNickname(), + oAuthUserInfoDto.getEmail(), + oAuthUserInfoDto.getProfileImageUrl(), + signUpToken + ); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index 8ca39eb62..820d2e573 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -1,72 +1,29 @@ package com.example.solidconnection.auth.service; -import com.example.solidconnection.auth.client.KakaoOAuthClient; import com.example.solidconnection.auth.dto.SignInResponse; -import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - -@RequiredArgsConstructor @Service +@RequiredArgsConstructor public class SignInService { private final AuthTokenProvider authTokenProvider; - private final SignUpTokenProvider signUpTokenProvider; - private final SiteUserRepository siteUserRepository; - private final KakaoOAuthClient kakaoOAuthClient; - /* - * 카카오에서 받아온 사용자 정보에 있는 이메일을 통해 기존 회원인지, 신규 회원인지 판별하고, 이에 따라 다르게 응답한다. - * 기존 회원 : 로그인 - * - 우리 서비스의 탈퇴 회원 방침을 적용한다. (계정 복구 기간 안에 접속하면 탈퇴를 무효화) - * - 액세스 토큰과 리프레시 토큰을 발급한다. - * 신규 회원 : 회원가입 페이지로 리다이렉트할 때 필요한 정보 제공 - * - 회원가입 시 입력하는 '닉네임'과 '프로필 사진' 부분을 미리 채우기 위해 사용자 정보를 리턴한다. - * - 또한, 우리 서비스에서 카카오 인증을 받았는지 나타내기 위한 'kakaoOauthToken' 을 발급해서 응답한다. - * - 회원가입할 때 클라이언트는 이때 발급받은 kakaoOauthToken 를 요청에 포함해 요청한다. (SignUpService 참고) - * */ @Transactional - public KakaoOauthResponse signIn(KakaoCodeRequest kakaoCodeRequest) { - KakaoUserInfoDto kakaoUserInfoDto = kakaoOAuthClient.processOauth(kakaoCodeRequest.code()); - String email = kakaoUserInfoDto.kakaoAccountDto().email(); - Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(email, AuthType.KAKAO); - - if (optionalSiteUser.isPresent()) { - SiteUser siteUser = optionalSiteUser.get(); - resetQuitedAt(siteUser); - return getSignInInfo(siteUser); - } - - return getFirstAccessInfo(kakaoUserInfoDto); + public SignInResponse signIn(SiteUser siteUser) { + resetQuitedAt(siteUser); + String accessToken = authTokenProvider.generateAccessToken(siteUser); + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + return new SignInResponse(accessToken, refreshToken); } - // 계적 복구 기한이 지난 회원은 자정마다 삭제된다. (UserRemovalScheduler 참고) - // 따라서 DB 에서 조회되었다면 아직 기한이 지나지 않았다는 뜻이므로, 탈퇴 날짜를 초기화한다. private void resetQuitedAt(SiteUser siteUser) { if (siteUser.getQuitedAt() == null) { return; } - siteUser.setQuitedAt(null); } - - private SignInResponse getSignInInfo(SiteUser siteUser) { - String accessToken = authTokenProvider.generateAccessToken(siteUser); - String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); - return new SignInResponse(true, accessToken, refreshToken); - } - - private FirstAccessResponse getFirstAccessInfo(KakaoUserInfoDto kakaoUserInfoDto) { - String kakaoOauthToken = signUpTokenProvider.generateAndSaveSignUpToken(kakaoUserInfoDto.kakaoAccountDto().email()); - return FirstAccessResponse.of(kakaoUserInfoDto, kakaoOauthToken); - } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java deleted file mode 100644 index f04bf112b..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.config.security.JwtProperties; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@Component -public class SignUpTokenProvider extends TokenProvider { - - public SignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { - super(jwtProperties, redisTemplate); - } - - public String generateAndSaveSignUpToken(String email) { - String signUpToken = generateToken(email, TokenType.SIGN_UP); - return saveToken(signUpToken, TokenType.SIGN_UP); - } - - public Optional findSignUpToken(String email) { - String signUpKey = TokenType.SIGN_UP.addPrefix(email); - return Optional.ofNullable(redisTemplate.opsForValue().get(signUpKey)); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java deleted file mode 100644 index a87a4aa2c..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.custom.exception.CustomException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import java.util.Date; -import java.util.Objects; - -import static com.example.solidconnection.auth.domain.TokenType.ACCESS; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; -import static com.example.solidconnection.auth.domain.TokenType.SIGN_UP; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; -import static com.example.solidconnection.custom.exception.ErrorCode.EMPTY_TOKEN; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN; -import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; - -@Component -@RequiredArgsConstructor -public class TokenValidator { - - private final RedisTemplate redisTemplate; - - @Value("${jwt.secret}") - private String secretKey; - - public void validateAccessToken(String token) { - validateTokenNotEmpty(token); - validateTokenNotExpired(token, ACCESS); - validateRefreshToken(token); - } - - public void validateKakaoToken(String token) { - validateTokenNotEmpty(token); - validateTokenNotExpired(token, SIGN_UP); - validateKakaoTokenNotUsed(token); - } - - private void validateTokenNotEmpty(String token) { - if (!StringUtils.hasText(token)) { - throw new CustomException(EMPTY_TOKEN); - } - } - - private void validateTokenNotExpired(String token, TokenType tokenType) { - Date expiration = getClaim(token).getExpiration(); - long now = new Date().getTime(); - if ((expiration.getTime() - now) < 0) { - if (tokenType.equals(ACCESS)) { - throw new CustomException(ACCESS_TOKEN_EXPIRED); - } - if (token.equals(SIGN_UP)) { - throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); - } - } - } - - private void validateRefreshToken(String token) { - String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(REFRESH.addPrefix(email)) == null) { - throw new CustomException(REFRESH_TOKEN_EXPIRED); - } - } - - private void validateKakaoTokenNotUsed(String token) { - String email = getClaim(token).getSubject(); - if (!Objects.equals(redisTemplate.opsForValue().get(SIGN_UP.addPrefix(email)), token)) { - throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); - } - } - - private Claims getClaim(String token) { - return Jwts.parser() - .setSigningKey(this.secretKey) - .parseClaimsJws(token) - .getBody(); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java new file mode 100644 index 000000000..2af82e07d --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.client.AppleOAuthClient; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.stereotype.Service; + +@Service +public class AppleOAuthService extends OAuthService { + + private final AppleOAuthClient appleOAuthClient; + + public AppleOAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, + AppleOAuthClient appleOAuthClient, SignInService signInService) { + super(signUpTokenProvider, siteUserRepository, signInService); + this.appleOAuthClient = appleOAuthClient; + } + + @Override + protected OAuthUserInfoDto getOAuthUserInfo(String code) { + return appleOAuthClient.processOAuth(code); + } + + @Override + protected AuthType getAuthType() { + return AuthType.APPLE; + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java new file mode 100644 index 000000000..5dc6faea1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.client.KakaoOAuthClient; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.stereotype.Service; + +@Service +public class KakaoOAuthService extends OAuthService { + + private final KakaoOAuthClient kakaoOAuthClient; + + public KakaoOAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, + KakaoOAuthClient kakaoOAuthClient, SignInService signInService) { + super(signUpTokenProvider, siteUserRepository, signInService); + this.kakaoOAuthClient = kakaoOAuthClient; + } + + @Override + protected OAuthUserInfoDto getOAuthUserInfo(String code) { + return kakaoOAuthClient.getUserInfo(code); + } + + @Override + protected AuthType getAuthType() { + return AuthType.KAKAO; + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java new file mode 100644 index 000000000..4f37db060 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java @@ -0,0 +1,61 @@ +package com.example.solidconnection.auth.service.oauth; + + +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; +import com.example.solidconnection.auth.dto.oauth.OAuthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; +import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +/* + * OAuth 제공자로부터 이메일을 받아 기존 회원인지, 신규 회원인지 판별하고, 이에 따라 다르게 응답한다. + * 기존 회원 : 로그인한다. + * 신규 회원 : 회원가입할 때 필요한 정보를 제공한다. + * */ +public abstract class OAuthService { + + private final SignUpTokenProvider signUpTokenProvider; + private final SignInService signInService; + private final SiteUserRepository siteUserRepository; + + protected OAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, SignInService signInService) { + this.signUpTokenProvider = signUpTokenProvider; + this.siteUserRepository = siteUserRepository; + this.signInService = signInService; + } + + @Transactional + public OAuthResponse processOAuth(OAuthCodeRequest oauthCodeRequest) { + OAuthUserInfoDto userInfoDto = getOAuthUserInfo(oauthCodeRequest.code()); + String email = userInfoDto.getEmail(); + Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(email, getAuthType()); + + if (optionalSiteUser.isPresent()) { + SiteUser siteUser = optionalSiteUser.get(); + return getSignInResponse(siteUser); + } + + return getSignUpPrepareResponse(userInfoDto); + } + + protected final OAuthSignInResponse getSignInResponse(SiteUser siteUser) { + SignInResponse signInResponse = signInService.signIn(siteUser); + return new OAuthSignInResponse(true, signInResponse.accessToken(), signInResponse.refreshToken()); + } + + protected final SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto) { + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), getAuthType()); + return SignUpPrepareResponse.of(userInfoDto, signUpToken); + } + + protected abstract OAuthUserInfoDto getOAuthUserInfo(String code); + protected abstract AuthType getAuthType(); +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java similarity index 62% rename from src/main/java/com/example/solidconnection/auth/service/SignUpService.java rename to src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java index 788b07e44..7b6d44d26 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java @@ -1,7 +1,8 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.oauth; +import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.auth.dto.SignUpResponse; +import com.example.solidconnection.auth.service.SignInService; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; @@ -12,7 +13,6 @@ import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.Role; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,10 +24,10 @@ @RequiredArgsConstructor @Service -public class SignUpService { +public class OAuthSignUpService { - private final TokenValidator tokenValidator; - private final AuthTokenProvider authTokenProvider; + private final SignUpTokenProvider signUpTokenProvider; + private final SignInService signInService; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -35,43 +35,31 @@ public class SignUpService { private final InterestedCountyRepository interestedCountyRepository; /* - * 회원가입을 한다. - * - 카카오로 최초 로그인 시 우리 서비스에서 발급한 카카오 토큰 kakaoOauthToken 을 검증한다. - * - 이는 '카카오 인증을 하지 않고 회원가입 api 만으로 회원가입 하는 상황'을 방지하기 위함이다. - * - 만약 api 만으로 회원가입을 한다면, 카카오 인증과 이메일에 대한 검증 없이 회원가입이 가능해진다. - * - 이메일은 우리 서비스에서 사용자를 식별하는 중요한 정보이기 때문에 '우리 서비스에서 발급한 카카오 토큰인지 검증하는' 단계가 필요하다. + * OAuth 인증 후 회원가입을 한다. + * - 우리 서버에서 OAuth 인증했음을 확인하기 위한 signUpToken 을 검증한다. * - 사용자 정보를 DB에 저장한다. * - 관심 국가와 지역을 DB에 저장한다. * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. * */ - // todo: 여러가지 가입 방법 적용해야 함 @Transactional - public SignUpResponse signUp(SignUpRequest signUpRequest) { + public SignInResponse signUp(SignUpRequest signUpRequest) { // 검증 - tokenValidator.validateKakaoToken(signUpRequest.kakaoOauthToken()); - String email = authTokenProvider.getEmail(signUpRequest.kakaoOauthToken()); + signUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); validateNicknameDuplicated(signUpRequest.nickname()); - validateUserNotDuplicated(email); + String email = signUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + AuthType authType = signUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); + validateUserNotDuplicated(email, authType); // 사용자 저장 - SiteUser siteUser = signUpRequest.toSiteUser(email, Role.MENTEE); - SiteUser savedSiteUser = siteUserRepository.save(siteUser); + SiteUser siteUser = siteUserRepository.save(signUpRequest.toSiteUser(email, authType)); // 관심 지역, 국가 저장 - saveInterestedRegion(signUpRequest, savedSiteUser); - saveInterestedCountry(signUpRequest, savedSiteUser); + saveInterestedRegion(signUpRequest, siteUser); + saveInterestedCountry(signUpRequest, siteUser); - // 토큰 발급 - String accessToken = authTokenProvider.generateAccessToken(siteUser); - String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); - return new SignUpResponse(accessToken, refreshToken); - } - - private void validateUserNotDuplicated(String email) { - if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.KAKAO)) { - throw new CustomException(USER_ALREADY_EXISTED); - } + // 로그인 + return signInService.signIn(siteUser); } private void validateNicknameDuplicated(String nickname) { @@ -80,6 +68,12 @@ private void validateNicknameDuplicated(String nickname) { } } + private void validateUserNotDuplicated(String email, AuthType authType) { + if (siteUserRepository.existsByEmailAndAuthType(email, authType)) { + throw new CustomException(USER_ALREADY_EXISTED); + } + } + private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) { List interestedRegionNames = signUpRequest.interestedRegions(); List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream() diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java new file mode 100644 index 000000000..5399dc1eb --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java @@ -0,0 +1,81 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static com.example.solidconnection.util.JwtUtils.parseClaims; +import static com.example.solidconnection.util.JwtUtils.parseSubject; + +@Component +public class SignUpTokenProvider extends TokenProvider { + + static final String AUTH_TYPE_CLAIM_KEY = "authType"; + + public SignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + super(jwtProperties, redisTemplate); + } + + public String generateAndSaveSignUpToken(String email, AuthType authType) { + Map authTypeClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, authType)); + Claims claims = Jwts.claims(authTypeClaim).setSubject(email); + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + TokenType.SIGN_UP.getExpireTime()); + + String signUpToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) + .compact(); + return saveToken(signUpToken, TokenType.SIGN_UP); + } + + public void validateSignUpToken(String token) { + validateFormatAndExpiration(token); + String email = parseEmail(token); + validateIssuedByServer(email); + } + + private void validateFormatAndExpiration(String token) { + try { + Claims claims = parseClaims(token, jwtProperties.secret()); + Objects.requireNonNull(claims.getSubject()); + String serializedAuthType = claims.get(AUTH_TYPE_CLAIM_KEY, String.class); + AuthType.valueOf(serializedAuthType); + } catch (Exception e) { + throw new CustomException(OAUTH_SIGN_UP_TOKEN_INVALID); + } + } + + private void validateIssuedByServer(String email) { + String key = TokenType.SIGN_UP.addPrefix(email); + if (redisTemplate.opsForValue().get(key) == null) { + throw new CustomException(OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); + } + } + + public String parseEmail(String token) { + return parseSubject(token, jwtProperties.secret()); + } + + public AuthType parseAuthType(String token) { + Claims claims = parseClaims(token, jwtProperties.secret()); + String authTypeStr = claims.get(AUTH_TYPE_CLAIM_KEY, String.class); + return AuthType.valueOf(authTypeStr); + } +} diff --git a/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java b/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java new file mode 100644 index 000000000..609e9ee89 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.config.client; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "oauth.apple") +public record AppleOAuthClientProperties( + String tokenUrl, + String clientSecretAudienceUrl, + String redirectUrl, + String publicKeyUrl, + String clientId, + String teamId, + String keyId +) { +} diff --git a/src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java b/src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java new file mode 100644 index 000000000..73b196d76 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.config.client; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "oauth.kakao") +public record KakaoOAuthClientProperties( + String tokenUrl, + String userInfoUrl, + String redirectUrl, + String clientId +) { +} diff --git a/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java b/src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java similarity index 91% rename from src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java rename to src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java index 51f7205be..36ce3f67b 100644 --- a/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java +++ b/src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.config.rest; +package com.example.solidconnection.config.client; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 8c3032284..d3fdf136d 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -11,6 +11,16 @@ @AllArgsConstructor public enum ErrorCode { + // apple + APPLE_AUTHORIZATION_FAILED(HttpStatus.BAD_REQUEST.value(), "애플 인증에 실패했습니다."), + APPLE_ID_TOKEN_MISSING_EMAIL(HttpStatus.BAD_REQUEST.value(), "애플 idToken 에 이메일이 없습니다."), + APPLE_ID_TOKEN_EXPIRED(HttpStatus.BAD_REQUEST.value(), "애플 idToken 이 만료되었습니다."), + INVALID_APPLE_ID_TOKEN(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 애플 idToken 입니다."), + APPLE_ID_TOKEN_MALFORMED(HttpStatus.BAD_REQUEST.value(), "애플 idToken 의 형식이 잘못되었습니다."), + APPLE_PUBLIC_KEY_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR.value(), "idToken 를 서명한 애플 공개키를 찾을 수 없습니다"), + FAILED_TO_READ_APPLE_PRIVATE_KEY(HttpStatus.INTERNAL_SERVER_ERROR.value(), "애플 private key 파일을 읽을 수 없습니다."), + APPLE_CLIENT_SECRET_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR.value(), "애플 client secret JWT 생성에 실패했습니다."), + // kakao KAKAO_REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST.value(), "리다이렉트 uri가 잘못되었습니다."), INVALID_OR_EXPIRED_KAKAO_AUTH_CODE(HttpStatus.BAD_REQUEST.value(), "사용할 수 없는 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), @@ -18,6 +28,10 @@ public enum ErrorCode { KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(), "카카오 사용자 정보 조회에 실패했습니다."), INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN(HttpStatus.BAD_REQUEST.value(), "우리 서비스에서 발급한 카카오 토큰이 아닙니다"), + // oauth + OAUTH_SIGN_UP_TOKEN_INVALID(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 회원가입 토큰입니다."), + OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER(HttpStatus.BAD_REQUEST.value(), "회원가입 토큰이 우리 서버에서 발급되지 않았습니다."), + // data not found UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 대학교 지원 정보입니다."), UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND_FOR_TERM(HttpStatus.NOT_FOUND.value(), "해당하는 대학교가 이번 모집 기간에 열리지 않았습니다."), diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java index 3a1b58520..d3ea8fed9 100644 --- a/src/main/java/com/example/solidconnection/util/JwtUtils.java +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -1,6 +1,7 @@ package com.example.solidconnection.util; import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import jakarta.servlet.http.HttpServletRequest; @@ -29,7 +30,7 @@ public static String parseTokenFromRequest(HttpServletRequest request) { public static String parseSubjectIgnoringExpiration(String token, String secretKey) { try { - return extractSubject(token, secretKey); + return parseClaims(token, secretKey).getSubject(); } catch (ExpiredJwtException e) { return e.getClaims().getSubject(); } catch (Exception e) { @@ -39,7 +40,7 @@ public static String parseSubjectIgnoringExpiration(String token, String secretK public static String parseSubject(String token, String secretKey) { try { - return extractSubject(token, secretKey); + return parseClaims(token, secretKey).getSubject(); } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } @@ -58,11 +59,10 @@ public static boolean isExpired(String token, String secretKey) { } } - private static String extractSubject(String token, String secretKey) throws ExpiredJwtException { + public static Claims parseClaims(String token, String secretKey) throws ExpiredJwtException { return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) - .getBody() - .getSubject(); + .getBody(); } } diff --git a/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java new file mode 100644 index 000000000..b80c4ca5d --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java @@ -0,0 +1,88 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.util.JwtUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("로그인 서비스 테스트") +@TestContainerSpringBootTest +class SignInServiceTest { + + @Autowired + private SignInService signInService; + + @Autowired + private JwtProperties jwtProperties; + + @Autowired + private AuthTokenProvider authTokenProvider; + + @Autowired + private SiteUserRepository siteUserRepository; + + private SiteUser siteUser; + private String subject; + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + subject = siteUser.getId().toString(); + } + + @Test + void 성공적으로_로그인한다() { + // when + SignInResponse signInResponse = signInService.signIn(siteUser); + + // then + String accessTokenSubject = JwtUtils.parseSubject(signInResponse.accessToken(), jwtProperties.secret()); + String refreshTokenSubject = JwtUtils.parseSubject(signInResponse.refreshToken(), jwtProperties.secret()); + Optional savedRefreshToken = authTokenProvider.findRefreshToken(subject); + assertAll( + () -> assertThat(accessTokenSubject).isEqualTo(subject), + () -> assertThat(refreshTokenSubject).isEqualTo(subject), + () -> assertThat(savedRefreshToken).hasValue(signInResponse.refreshToken())); + } + + @Test + void 탈퇴한_이력이_있으면_초기화한다() { + // given + siteUser.setQuitedAt(LocalDate.now().minusDays(1)); + siteUserRepository.save(siteUser); + + // when + signInService.signIn(siteUser); + + // then + assertThat(siteUser.getQuitedAt()).isNull(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java deleted file mode 100644 index 382008d8c..000000000 --- a/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.config.security.JwtProperties; -import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.util.JwtUtils; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -@TestContainerSpringBootTest -@DisplayName("회원가입 토큰 제공자 테스트") -class SignUpTokenProviderTest { - - @Autowired - private SignUpTokenProvider signUpTokenProvider; - - @Autowired - private RedisTemplate redisTemplate; - - @Autowired - private JwtProperties jwtProperties; - - @Test - void 회원가입_토큰을_생성하고_저장한다() { - // when - String email = "email"; - String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email); - - // then - String actualSubject = JwtUtils.parseSubject(signUpToken, jwtProperties.secret()); - String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); - assertAll( - () -> assertThat(actualSubject).isEqualTo(email), - () -> assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isEqualTo(signUpToken) - ); - } - - @Test - void 저장된_회원가입_토큰을_조회한다() { - // given - String email = "email"; - String signUpToken = "token"; - redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), signUpToken); - - // when - Optional actualSignUpToken = signUpTokenProvider.findSignUpToken(email); - - // then - assertThat(actualSignUpToken).hasValue(signUpToken); - } - - @Test - void 저장되지_않은_회원가입_토큰을_조회한다() { - // given - String email = "email"; - - // when - Optional actualSignUpToken = signUpTokenProvider.findSignUpToken(email); - - // then - assertThat(actualSignUpToken).isEmpty(); - } -} diff --git a/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java new file mode 100644 index 000000000..d3a1efac1 --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java @@ -0,0 +1,182 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.util.JwtUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static com.example.solidconnection.auth.service.oauth.SignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("회원가입 토큰 제공자 테스트") +class SignUpTokenProviderTest { + + @Autowired + private SignUpTokenProvider signUpTokenProvider; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 회원가입_토큰을_생성하고_저장한다() { + // given + String email = "email"; + AuthType authType = AuthType.KAKAO; + + // when + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType); + + // then + Claims claims = JwtUtils.parseClaims(signUpToken, jwtProperties.secret()); + String actualSubject = claims.getSubject(); + AuthType actualAuthType = AuthType.valueOf(claims.get(AUTH_TYPE_CLAIM_KEY, String.class)); + String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); + assertAll( + () -> assertThat(actualSubject).isEqualTo(email), + () -> assertThat(actualAuthType).isEqualTo(authType), + () -> assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isEqualTo(signUpToken) + ); + } + + @Nested + class 주어진_회원가입_토큰을_검증한다 { + + @Test + void 검증_성공한다() { + // given + String email = "email@test.com"; + Map claim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); + String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); + redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); + } + + @Test + void 만료되었으면_예외_응답을_반환한다() { + // given + String expiredToken = createExpiredToken(); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(expiredToken)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + } + + @Test + void 정해진_형식에_맞지_않으면_예외_응답을_반환한다_jwt_가_아닌_토큰() { + // given + String notJwt = "not jwt"; + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(notJwt)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + } + + @Test + void 정해진_형식에_맞지_않으면_예외_응답을_반환한다_authType_클래스_불일치() { + // given + Map wrongClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, "카카오")); + String wrongAuthType = createBaseJwtBuilder().addClaims(wrongClaim).compact(); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(wrongAuthType)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + } + + @Test + void 정해진_형식에_맞지_않으면_예외_응답을_반환한다_subject_누락() { + // given + Map claim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); + String noSubject = createBaseJwtBuilder().addClaims(claim).compact(); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(noSubject)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + } + + @Test + void 우리_서버에_발급된_토큰이_아니면_예외_응답을_반환한다() { + // given + Map validClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); + String signUpToken = createBaseJwtBuilder().addClaims(validClaim).setSubject("email").compact(); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(signUpToken)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER.getMessage()); + } + } + + @Test + void 회원가입_토큰에서_이메일을_추출한다() { + // given + String email = "email@test.com"; + Map claim = Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE); + String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); + redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); + + // when + String extractedEmail = signUpTokenProvider.parseEmail(validToken); + + // then + assertThat(extractedEmail).isEqualTo(email); + } + + @Test + void 회원가입_토큰에서_인증_타입을_추출한다() { + // given + AuthType authType = AuthType.APPLE; + Map claim = Map.of(AUTH_TYPE_CLAIM_KEY, authType); + String validToken = createBaseJwtBuilder().setSubject("email").addClaims(claim).compact(); + + // when + AuthType extractedAuthType = signUpTokenProvider.parseAuthType(validToken); + + // then + assertThat(extractedAuthType).isEqualTo(authType); + } + + private String createExpiredToken() { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private JwtBuilder createBaseJwtBuilder() { + return Jwts.builder() + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java index a549b62a2..09c97d46e 100644 --- a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java +++ b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java @@ -2,7 +2,7 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.KakaoUserInfoDto; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.LanguageTestType; diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index 8d3ddc75f..cc16f71c1 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -1,10 +1,10 @@ package com.example.solidconnection.e2e; import com.example.solidconnection.auth.client.KakaoOAuthClient; -import com.example.solidconnection.auth.dto.SignInResponse; -import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; -import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; +import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; +import com.example.solidconnection.auth.dto.oauth.KakaoUserInfoDto; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.restassured.RestAssured; @@ -45,18 +45,18 @@ class SignInTest extends BaseEndToEndTest { String kakaoCode = "kakaoCode"; String email = "email@email.com"; KakaoUserInfoDto kakaoUserInfoDto = createKakaoUserInfoDtoByEmail(email); - given(kakaoOAuthClient.processOauth(kakaoCode)) + given(kakaoOAuthClient.getUserInfo(kakaoCode)) .willReturn(kakaoUserInfoDto); // request - body 생성 및 요청 - KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); - FirstAccessResponse response = RestAssured.given().log().all() + OAuthCodeRequest OAuthCodeRequest = new OAuthCodeRequest(kakaoCode); + SignUpPrepareResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) - .body(kakaoCodeRequest) + .body(OAuthCodeRequest) .when().post("/auth/kakao") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(FirstAccessResponse.class); + .extract().as(SignUpPrepareResponse.class); KakaoUserInfoDto.KakaoAccountDto.KakaoProfileDto kakaoProfileDto = kakaoUserInfoDto.kakaoAccountDto().profile(); assertAll("카카오톡 사용자 정보를 응답한다.", @@ -64,10 +64,10 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.email()).isEqualTo(email), () -> assertThat(response.nickname()).isEqualTo(kakaoProfileDto.nickname()), () -> assertThat(response.profileImageUrl()).isEqualTo(kakaoProfileDto.profileImageUrl()), - () -> assertThat(response.kakaoOauthToken()).isNotNull()); + () -> assertThat(response.signUpToken()).isNotNull()); assertThat(redisTemplate.opsForValue().get(SIGN_UP.addPrefix(email))) .as("카카오 인증 토큰을 저장한다.") - .isEqualTo(response.kakaoOauthToken()); + .isEqualTo(response.signUpToken()); } @Test @@ -75,21 +75,21 @@ class SignInTest extends BaseEndToEndTest { // stub - kakaoOAuthClient 가 정해진 사용자 프로필 정보를 반환하도록 String kakaoCode = "kakaoCode"; String email = "email@email.com"; - given(kakaoOAuthClient.processOauth(kakaoCode)) + given(kakaoOAuthClient.getUserInfo(kakaoCode)) .willReturn(createKakaoUserInfoDtoByEmail(email)); // setUp - 사용자 정보 저장 SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); // request - body 생성 및 요청 - KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); - SignInResponse response = RestAssured.given().log().all() + OAuthCodeRequest oAuthCodeRequest = new OAuthCodeRequest(kakaoCode); + OAuthSignInResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) - .body(kakaoCodeRequest) + .body(oAuthCodeRequest) .when().post("/auth/kakao") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(SignInResponse.class); + .extract().as(OAuthSignInResponse.class); assertAll("리프레스 토큰과 엑세스 토큰을 응답한다.", () -> assertThat(response.isRegistered()).isTrue(), @@ -105,7 +105,7 @@ class SignInTest extends BaseEndToEndTest { // stub - kakaoOAuthClient 가 정해진 사용자 프로필 정보를 반환하도록 String kakaoCode = "kakaoCode"; String email = "email@email.com"; - given(kakaoOAuthClient.processOauth(kakaoCode)) + given(kakaoOAuthClient.getUserInfo(kakaoCode)) .willReturn(createKakaoUserInfoDtoByEmail(email)); // setUp - 계정 복구 기간이 되지 않은 사용자 저장 @@ -115,14 +115,14 @@ class SignInTest extends BaseEndToEndTest { SiteUser siteUser = siteUserRepository.save(siteUserFixture); // request - body 생성 및 요청 - KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); - SignInResponse response = RestAssured.given().log().all() + OAuthCodeRequest OAuthCodeRequest = new OAuthCodeRequest(kakaoCode); + OAuthSignInResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) - .body(kakaoCodeRequest) + .body(OAuthCodeRequest) .when().post("/auth/kakao") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(SignInResponse.class); + .extract().as(OAuthSignInResponse.class); SiteUser updatedSiteUser = siteUserRepository.findById(siteUser.getId()).get(); assertAll("리프레스 토큰과 엑세스 토큰을 응답하고, 탈퇴 날짜를 초기화한다.", diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 1bbe150a8..7d01f365c 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -1,9 +1,9 @@ package com.example.solidconnection.e2e; +import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.auth.dto.SignUpResponse; import com.example.solidconnection.auth.service.AuthTokenProvider; -import com.example.solidconnection.auth.service.SignUpTokenProvider; +import com.example.solidconnection.auth.service.oauth.SignUpTokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -29,8 +29,8 @@ import java.util.List; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; -import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByNickName; @@ -74,20 +74,20 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); List interestedCountryNames = List.of("프랑스", "독일"); SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, interestedRegionNames, interestedCountryNames, PreparationStatus.CONSIDERING, "profile", Gender.FEMALE, "nickname", "2000-01-01"); - SignUpResponse response = RestAssured.given().log().all() + SignInResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) .when().post("/auth/sign-up") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(SignUpResponse.class); + .extract().as(SignInResponse.class); SiteUser savedSiteUser = siteUserRepository.findByEmailAndAuthType(email, AuthType.KAKAO).get(); assertAll( @@ -126,7 +126,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -151,7 +151,7 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail, AuthType.KAKAO); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -181,6 +181,6 @@ class SignUpTest extends BaseEndToEndTest { .extract().as(ErrorResponse.class); assertThat(errorResponse.message()) - .contains(JWT_EXCEPTION.getMessage()); + .contains(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); } } From 25e9f407b159d187538532a85f9c7fea6c038e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 8 Feb 2025 09:39:33 +0900 Subject: [PATCH 140/158] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=84=20=EC=BB=AC=EB=9F=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: issueDate 컬럼 제거 * chore: issueDate 컬럼 제거 마이그레이션 파일 추가 * test: 모든 테스트 코드 정상 작동하도록 수정 * style: 사용하지 않는 import문 제거 --- .../service/ApplicationSubmissionService.java | 1 - .../example/solidconnection/score/domain/GpaScore.java | 7 +------ .../score/domain/LanguageTestScore.java | 7 +------ .../solidconnection/score/dto/GpaScoreRequest.java | 5 ----- .../solidconnection/score/dto/GpaScoreStatus.java | 4 ---- .../score/dto/LanguageTestScoreRequest.java | 6 ------ .../score/dto/LanguageTestScoreStatus.java | 4 ---- .../score/dto/LanguageTestScoreStatusResponse.java | 1 - .../solidconnection/score/service/ScoreService.java | 4 ++-- .../db/migration/V4__remove_issue_date_columns.sql | 5 +++++ .../service/ApplicationSubmissionServiceTest.java | 10 ++-------- .../score/service/ScoreServiceTest.java | 9 +-------- .../support/integration/BaseIntegrationTest.java | 5 +---- 13 files changed, 13 insertions(+), 55 deletions(-) create mode 100644 src/main/resources/db/migration/V4__remove_issue_date_columns.sql diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index beb2f0cb0..c7652dce0 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -10,7 +10,6 @@ import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.UniversityInfoForApply; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; diff --git a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java index 17b5cca48..54df13759 100644 --- a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java +++ b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java @@ -18,8 +18,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDate; - @Getter @Entity @NoArgsConstructor @@ -33,8 +31,6 @@ public class GpaScore extends BaseEntity { @Embedded private Gpa gpa; - private LocalDate issueDate; - @Setter @Column(columnDefinition = "varchar(50) not null default 'PENDING'") @Enumerated(EnumType.STRING) @@ -45,10 +41,9 @@ public class GpaScore extends BaseEntity { @ManyToOne private SiteUser siteUser; - public GpaScore(Gpa gpa, SiteUser siteUser, LocalDate issueDate) { + public GpaScore(Gpa gpa, SiteUser siteUser) { this.gpa = gpa; this.siteUser = siteUser; - this.issueDate = issueDate; this.verifyStatus = VerifyStatus.PENDING; this.rejectedReason = null; } diff --git a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java index 88501f686..7939e1db8 100644 --- a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java +++ b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java @@ -18,8 +18,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDate; - @Getter @Entity @NoArgsConstructor @@ -33,8 +31,6 @@ public class LanguageTestScore extends BaseEntity { @Embedded private LanguageTest languageTest; - private LocalDate issueDate; - @Setter @Column(columnDefinition = "varchar(50) not null default 'PENDING'") @Enumerated(EnumType.STRING) @@ -45,9 +41,8 @@ public class LanguageTestScore extends BaseEntity { @ManyToOne private SiteUser siteUser; - public LanguageTestScore(LanguageTest languageTest, LocalDate issueDate, SiteUser siteUser) { + public LanguageTestScore(LanguageTest languageTest, SiteUser siteUser) { this.languageTest = languageTest; - this.issueDate = issueDate; this.verifyStatus = VerifyStatus.PENDING; this.siteUser = siteUser; } diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java index 5227ba9ed..613ac5b54 100644 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - public record GpaScoreRequest( @NotNull(message = "학점을 입력해주세요.") Double gpa, @@ -13,9 +11,6 @@ public record GpaScoreRequest( @NotNull(message = "학점 기준을 입력해주세요.") Double gpaCriteria, - @NotNull(message = "발급일자를 입력해주세요.") - LocalDate issueDate, - @NotBlank(message = "대학 성적 증명서를 첨부해주세요.") String gpaReportUrl) { diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java index 0361cf0e7..5798e3cf0 100644 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java @@ -4,12 +4,9 @@ import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.type.VerifyStatus; -import java.time.LocalDate; - public record GpaScoreStatus( Long id, Gpa gpa, - LocalDate issueDate, VerifyStatus verifyStatus, String rejectedReason ) { @@ -17,7 +14,6 @@ public static GpaScoreStatus from(GpaScore gpaScore) { return new GpaScoreStatus( gpaScore.getId(), gpaScore.getGpa(), - gpaScore.getIssueDate(), gpaScore.getVerifyStatus(), gpaScore.getRejectedReason() ); diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java index c39e5fcb9..92522949e 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java @@ -1,13 +1,10 @@ package com.example.solidconnection.score.dto; - import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.type.LanguageTestType; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - public record LanguageTestScoreRequest( @NotNull(message = "어학 종류를 입력해주세요.") LanguageTestType languageTestType, @@ -15,9 +12,6 @@ public record LanguageTestScoreRequest( @NotBlank(message = "어학 점수를 입력해주세요.") String languageTestScore, - @NotNull(message = "발급일자를 입력해주세요.") - LocalDate issueDate, - @NotBlank(message = "어학 증명서를 첨부해주세요.") String languageTestReportUrl) { diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java index 2d1d8fcb1..9e5fcae4f 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java @@ -4,12 +4,9 @@ import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.type.VerifyStatus; -import java.time.LocalDate; - public record LanguageTestScoreStatus( Long id, LanguageTest languageTest, - LocalDate issueDate, VerifyStatus verifyStatus, String rejectedReason ) { @@ -17,7 +14,6 @@ public static LanguageTestScoreStatus from(LanguageTestScore languageTestScore) return new LanguageTestScoreStatus( languageTestScore.getId(), languageTestScore.getLanguageTest(), - languageTestScore.getIssueDate(), languageTestScore.getVerifyStatus(), languageTestScore.getRejectedReason() ); diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java index 3d4f74894..e19c0e855 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java @@ -1,6 +1,5 @@ package com.example.solidconnection.score.dto; - import java.util.List; public record LanguageTestScoreStatusResponse( diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index e6c9d5c6e..45efb2aa1 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -30,7 +30,7 @@ public class ScoreService { @Transactional public Long submitGpaScore(SiteUser siteUser, GpaScoreRequest gpaScoreRequest) { - GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser, gpaScoreRequest.issueDate()); + GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser); newGpaScore.setSiteUser(siteUser); GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); // 저장 후 반환된 객체 return savedNewGpaScore.getId(); // 저장된 GPA Score의 ID 반환 @@ -41,7 +41,7 @@ public Long submitLanguageTestScore(SiteUser siteUser, LanguageTestScoreRequest LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); LanguageTestScore newScore = new LanguageTestScore( - languageTest, languageTestScoreRequest.issueDate(), siteUser); + languageTest, siteUser); newScore.setSiteUser(siteUser); LanguageTestScore savedNewScore = languageTestScoreRepository.save(newScore); // 새로 저장한 객체 return savedNewScore.getId(); // 저장된 객체의 ID 반환 diff --git a/src/main/resources/db/migration/V4__remove_issue_date_columns.sql b/src/main/resources/db/migration/V4__remove_issue_date_columns.sql new file mode 100644 index 000000000..9a8e0700b --- /dev/null +++ b/src/main/resources/db/migration/V4__remove_issue_date_columns.sql @@ -0,0 +1,5 @@ +ALTER TABLE gpa_score + DROP COLUMN issue_date; + +ALTER TABLE language_test_score + DROP COLUMN issue_date; \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java index 911172bfd..1d40d094b 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -19,8 +19,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.time.LocalDate; - import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; @@ -165,8 +163,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { private GpaScore createUnapprovedGpaScore(SiteUser siteUser) { GpaScore gpaScore = new GpaScore( new Gpa(4.0, 4.5, "/gpa-report.pdf"), - siteUser, - LocalDate.now() + siteUser ); return gpaScoreRepository.save(gpaScore); } @@ -174,8 +171,7 @@ private GpaScore createUnapprovedGpaScore(SiteUser siteUser) { private GpaScore createApprovedGpaScore(SiteUser siteUser) { GpaScore gpaScore = new GpaScore( new Gpa(4.0, 4.5, "/gpa-report.pdf"), - siteUser, - LocalDate.now() + siteUser ); gpaScore.setVerifyStatus(VerifyStatus.APPROVED); return gpaScoreRepository.save(gpaScore); @@ -184,7 +180,6 @@ private GpaScore createApprovedGpaScore(SiteUser siteUser) { private LanguageTestScore createUnapprovedLanguageTestScore(SiteUser siteUser) { LanguageTestScore languageTestScore = new LanguageTestScore( new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), - LocalDate.now(), siteUser ); return languageTestScoreRepository.save(languageTestScore); @@ -193,7 +188,6 @@ private LanguageTestScore createUnapprovedLanguageTestScore(SiteUser siteUser) { private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { LanguageTestScore languageTestScore = new LanguageTestScore( new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), - LocalDate.now(), siteUser ); languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 681b708a2..038aa91b6 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.time.LocalDate; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -129,7 +128,6 @@ class ScoreServiceTest extends BaseIntegrationTest { () -> assertThat(savedScore.getId()).isEqualTo(scoreId), () -> assertThat(savedScore.getGpa().getGpa()).isEqualTo(request.gpa()), () -> assertThat(savedScore.getGpa().getGpaCriteria()).isEqualTo(request.gpaCriteria()), - () -> assertThat(savedScore.getIssueDate()).isEqualTo(request.issueDate()), () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) ); } @@ -149,7 +147,6 @@ class ScoreServiceTest extends BaseIntegrationTest { () -> assertThat(savedScore.getId()).isEqualTo(scoreId), () -> assertThat(savedScore.getLanguageTest().getLanguageTestType()).isEqualTo(request.languageTestType()), () -> assertThat(savedScore.getLanguageTest().getLanguageTestScore()).isEqualTo(request.languageTestScore()), - () -> assertThat(savedScore.getIssueDate()).isEqualTo(request.issueDate()), () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) ); } @@ -170,8 +167,7 @@ private SiteUser createSiteUser() { private GpaScore createGpaScore(SiteUser siteUser, double gpa, double gpaCriteria) { GpaScore gpaScore = new GpaScore( new Gpa(gpa, gpaCriteria, "/gpa-report.pdf"), - siteUser, - LocalDate.now() + siteUser ); gpaScore.setSiteUser(siteUser); return gpaScoreRepository.save(gpaScore); @@ -180,7 +176,6 @@ private GpaScore createGpaScore(SiteUser siteUser, double gpa, double gpaCriteri private LanguageTestScore createLanguageTestScore(SiteUser siteUser, LanguageTestType languageTestType, String score) { LanguageTestScore languageTestScore = new LanguageTestScore( new LanguageTest(languageTestType, score, "/gpa-report.pdf"), - LocalDate.now(), siteUser ); languageTestScore.setSiteUser(siteUser); @@ -191,7 +186,6 @@ private GpaScoreRequest createGpaScoreRequest() { return new GpaScoreRequest( 3.5, 4.5, - LocalDate.now(), "/gpa-report.pdf" ); } @@ -200,7 +194,6 @@ private LanguageTestScoreRequest createLanguageTestScoreRequest() { return new LanguageTestScoreRequest( LanguageTestType.TOEFL_IBT, "100", - LocalDate.now(), "/gpa-report.pdf" ); } diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index ec29b8499..f7378468c 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -39,7 +39,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import java.time.LocalDate; import java.util.HashSet; import java.util.List; @@ -511,8 +510,7 @@ private void saveLanguageTestRequirement( private GpaScore createApprovedGpaScore(SiteUser siteUser) { GpaScore gpaScore = new GpaScore( new Gpa(4.0, 4.5, "/gpa-report.pdf"), - siteUser, - LocalDate.now() + siteUser ); gpaScore.setVerifyStatus(VerifyStatus.APPROVED); return gpaScoreRepository.save(gpaScore); @@ -521,7 +519,6 @@ private GpaScore createApprovedGpaScore(SiteUser siteUser) { private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { LanguageTestScore languageTestScore = new LanguageTestScore( new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), - LocalDate.now(), siteUser ); languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); From 2c8b20078fa9465be1909bef021b439f42460299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:43:32 +0900 Subject: [PATCH 141/158] =?UTF-8?q?refactor:=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B4=80=EB=A0=A8=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: board, comment, post 패키지를 community 패키지로 이동 및 통합 * refactor: 게시글 목록 조회 API PostController로 이동 * test: 변경된 패키지 구조에 맞게 테스트 구조 변경 * refactor: 레이어별 패키지 구조를 도메인 중심으로 변경 --- .../board/service/BoardService.java | 61 --------------- .../board/controller/BoardController.java | 18 +---- .../{ => community}/board/domain/Board.java | 4 +- .../board/dto/PostFindBoardResponse.java | 4 +- .../board/repository/BoardRepository.java | 4 +- .../community/board/service/BoardService.java | 9 +++ .../comment/controller/CommentController.java | 14 ++-- .../comment/domain/Comment.java | 4 +- .../comment/dto/CommentCreateRequest.java | 6 +- .../comment/dto/CommentCreateResponse.java | 4 +- .../comment/dto/CommentDeleteResponse.java | 2 +- .../comment/dto/CommentUpdateRequest.java | 2 +- .../comment/dto/CommentUpdateResponse.java | 4 +- .../comment/dto/PostFindCommentResponse.java | 4 +- .../comment/repository/CommentRepository.java | 4 +- .../comment/service/CommentService.java | 24 +++--- .../post/controller/PostController.java | 35 ++++++--- .../{ => community}/post/domain/Post.java | 9 +-- .../post/domain}/PostImage.java | 3 +- .../{ => community}/post/domain/PostLike.java | 2 +- .../post/dto/PostCreateRequest.java | 6 +- .../post/dto/PostCreateResponse.java | 4 +- .../post/dto/PostDeleteResponse.java | 2 +- .../post/dto/PostDislikeResponse.java | 4 +- .../post/dto/PostFindPostImageResponse.java | 4 +- .../post/dto/PostFindResponse.java | 8 +- .../post/dto/PostLikeResponse.java | 4 +- .../post/dto/PostListResponse.java} | 16 ++-- .../post/dto/PostUpdateRequest.java | 2 +- .../post/dto/PostUpdateResponse.java | 4 +- .../post/repository}/PostImageRepository.java | 4 +- .../post/repository/PostLikeRepository.java | 6 +- .../post/repository/PostRepository.java | 4 +- .../post/service/PostCommandService.java | 22 +++--- .../post/service/PostLikeService.java | 14 ++-- .../post/service/PostQueryService.java | 56 +++++++++++--- .../service/UpdateViewCountService.java | 4 +- .../siteuser/domain/SiteUser.java | 6 +- .../board/service/BoardServiceTest.java | 72 ----------------- .../comment/service/CommentServiceTest.java | 26 +++---- .../post/service/PostCommandServiceTest.java | 22 +++--- .../post/service/PostLikeServiceTest.java | 14 ++-- .../post/service/PostQueryServiceTest.java | 77 ++++++++++++++++--- .../PostLikeCountConcurrencyTest.java | 10 +-- .../PostViewCountConcurrencyTest.java | 8 +- .../integration/BaseIntegrationTest.java | 12 +-- 46 files changed, 293 insertions(+), 335 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/board/service/BoardService.java rename src/main/java/com/example/solidconnection/{ => community}/board/controller/BoardController.java (52%) rename src/main/java/com/example/solidconnection/{ => community}/board/domain/Board.java (85%) rename src/main/java/com/example/solidconnection/{ => community}/board/dto/PostFindBoardResponse.java (69%) rename src/main/java/com/example/solidconnection/{ => community}/board/repository/BoardRepository.java (88%) create mode 100644 src/main/java/com/example/solidconnection/community/board/service/BoardService.java rename src/main/java/com/example/solidconnection/{ => community}/comment/controller/CommentController.java (80%) rename src/main/java/com/example/solidconnection/{ => community}/comment/domain/Comment.java (96%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentCreateRequest.java (81%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentCreateResponse.java (62%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentDeleteResponse.java (50%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentUpdateRequest.java (85%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentUpdateResponse.java (62%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/PostFindCommentResponse.java (88%) rename src/main/java/com/example/solidconnection/{ => community}/comment/repository/CommentRepository.java (91%) rename src/main/java/com/example/solidconnection/{ => community}/comment/service/CommentService.java (84%) rename src/main/java/com/example/solidconnection/{ => community}/post/controller/PostController.java (75%) rename src/main/java/com/example/solidconnection/{ => community}/post/domain/Post.java (92%) rename src/main/java/com/example/solidconnection/{entity => community/post/domain}/PostImage.java (90%) rename src/main/java/com/example/solidconnection/{ => community}/post/domain/PostLike.java (96%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostCreateRequest.java (87%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostCreateResponse.java (62%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostDeleteResponse.java (50%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostDislikeResponse.java (68%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostFindPostImageResponse.java (82%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostFindResponse.java (86%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostLikeResponse.java (68%) rename src/main/java/com/example/solidconnection/{post/dto/BoardFindPostResponse.java => community/post/dto/PostListResponse.java} (72%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostUpdateRequest.java (92%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostUpdateResponse.java (62%) rename src/main/java/com/example/solidconnection/{repositories => community/post/repository}/PostImageRepository.java (61%) rename src/main/java/com/example/solidconnection/{ => community}/post/repository/PostLikeRepository.java (79%) rename src/main/java/com/example/solidconnection/{ => community}/post/repository/PostRepository.java (93%) rename src/main/java/com/example/solidconnection/{ => community}/post/service/PostCommandService.java (87%) rename src/main/java/com/example/solidconnection/{ => community}/post/service/PostLikeService.java (83%) rename src/main/java/com/example/solidconnection/{ => community}/post/service/PostQueryService.java (55%) delete mode 100644 src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java rename src/test/java/com/example/solidconnection/{ => community}/comment/service/CommentServiceTest.java (95%) rename src/test/java/com/example/solidconnection/{ => community}/post/service/PostCommandServiceTest.java (94%) rename src/test/java/com/example/solidconnection/{ => community}/post/service/PostLikeServiceTest.java (91%) rename src/test/java/com/example/solidconnection/{ => community}/post/service/PostQueryServiceTest.java (60%) diff --git a/src/main/java/com/example/solidconnection/board/service/BoardService.java b/src/main/java/com/example/solidconnection/board/service/BoardService.java deleted file mode 100644 index 2513e0903..000000000 --- a/src/main/java/com/example/solidconnection/board/service/BoardService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.solidconnection.board.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.BoardFindPostResponse; -import com.example.solidconnection.type.BoardCode; -import com.example.solidconnection.type.PostCategory; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.EnumUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.stream.Collectors; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; - -@Service -@RequiredArgsConstructor -public class BoardService { - private final BoardRepository boardRepository; - - @Transactional(readOnly = true) - public List findPostsByCodeAndPostCategory(String code, String category) { - - String boardCode = validateCode(code); - PostCategory postCategory = validatePostCategory(category); - - Board board = boardRepository.getByCodeUsingEntityGraph(boardCode); - List postList = getPostListByPostCategory(board.getPostList(), postCategory); - - return BoardFindPostResponse.from(postList); - } - - private String validateCode(String code) { - try { - return String.valueOf(BoardCode.valueOf(code)); - } catch (IllegalArgumentException ex) { - throw new CustomException(ErrorCode.INVALID_BOARD_CODE); - } - } - - private PostCategory validatePostCategory(String category) { - if (!EnumUtils.isValidEnum(PostCategory.class, category)) { - throw new CustomException(INVALID_POST_CATEGORY); - } - return PostCategory.valueOf(category); - } - - private List getPostListByPostCategory(List postList, PostCategory postCategory) { - if (postCategory.equals(PostCategory.전체)) { - return postList; - } - return postList.stream() - .filter(post -> post.getCategory().equals(postCategory)) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/com/example/solidconnection/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java similarity index 52% rename from src/main/java/com/example/solidconnection/board/controller/BoardController.java rename to src/main/java/com/example/solidconnection/community/board/controller/BoardController.java index f6ebb27d0..9329535a1 100644 --- a/src/main/java/com/example/solidconnection/board/controller/BoardController.java +++ b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java @@ -1,14 +1,10 @@ -package com.example.solidconnection.board.controller; +package com.example.solidconnection.community.board.controller; -import com.example.solidconnection.board.service.BoardService; -import com.example.solidconnection.post.dto.BoardFindPostResponse; import com.example.solidconnection.type.BoardCode; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; @@ -19,8 +15,6 @@ @RequestMapping("/communities") public class BoardController { - private final BoardService boardService; - // todo: 회원별로 접근 가능한 게시판 목록 조회 기능 개발 @GetMapping() public ResponseEntity findAccessibleCodes() { @@ -30,14 +24,4 @@ public ResponseEntity findAccessibleCodes() { } return ResponseEntity.ok().body(accessibleCodeList); } - - @GetMapping("/{code}") - public ResponseEntity findPostsByCodeAndCategory( - @PathVariable(value = "code") String code, - @RequestParam(value = "category", defaultValue = "전체") String category) { - - List postsByCodeAndPostCategory = boardService - .findPostsByCodeAndPostCategory(code, category); - return ResponseEntity.ok().body(postsByCodeAndPostCategory); - } } diff --git a/src/main/java/com/example/solidconnection/board/domain/Board.java b/src/main/java/com/example/solidconnection/community/board/domain/Board.java similarity index 85% rename from src/main/java/com/example/solidconnection/board/domain/Board.java rename to src/main/java/com/example/solidconnection/community/board/domain/Board.java index 77d0aada8..fbf13b44d 100644 --- a/src/main/java/com/example/solidconnection/board/domain/Board.java +++ b/src/main/java/com/example/solidconnection/community/board/domain/Board.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.board.domain; +package com.example.solidconnection.community.board.domain; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java b/src/main/java/com/example/solidconnection/community/board/dto/PostFindBoardResponse.java similarity index 69% rename from src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java rename to src/main/java/com/example/solidconnection/community/board/dto/PostFindBoardResponse.java index b06baa305..e4f66afdd 100644 --- a/src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java +++ b/src/main/java/com/example/solidconnection/community/board/dto/PostFindBoardResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.board.dto; +package com.example.solidconnection.community.board.dto; -import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.community.board.domain.Board; public record PostFindBoardResponse( String code, diff --git a/src/main/java/com/example/solidconnection/board/repository/BoardRepository.java b/src/main/java/com/example/solidconnection/community/board/repository/BoardRepository.java similarity index 88% rename from src/main/java/com/example/solidconnection/board/repository/BoardRepository.java rename to src/main/java/com/example/solidconnection/community/board/repository/BoardRepository.java index 5c4538279..06dd01161 100644 --- a/src/main/java/com/example/solidconnection/board/repository/BoardRepository.java +++ b/src/main/java/com/example/solidconnection/community/board/repository/BoardRepository.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.board.repository; +package com.example.solidconnection.community.board.repository; -import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; import org.springframework.data.jpa.repository.EntityGraph; diff --git a/src/main/java/com/example/solidconnection/community/board/service/BoardService.java b/src/main/java/com/example/solidconnection/community/board/service/BoardService.java new file mode 100644 index 000000000..c918f8126 --- /dev/null +++ b/src/main/java/com/example/solidconnection/community/board/service/BoardService.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.community.board.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class BoardService { +} diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java similarity index 80% rename from src/main/java/com/example/solidconnection/comment/controller/CommentController.java rename to src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java index fda360b4a..e215fea72 100644 --- a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java +++ b/src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java @@ -1,11 +1,11 @@ -package com.example.solidconnection.comment.controller; +package com.example.solidconnection.community.comment.controller; -import com.example.solidconnection.comment.dto.CommentCreateRequest; -import com.example.solidconnection.comment.dto.CommentCreateResponse; -import com.example.solidconnection.comment.dto.CommentDeleteResponse; -import com.example.solidconnection.comment.dto.CommentUpdateRequest; -import com.example.solidconnection.comment.dto.CommentUpdateResponse; -import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.community.comment.dto.CommentCreateRequest; +import com.example.solidconnection.community.comment.dto.CommentCreateResponse; +import com.example.solidconnection.community.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.community.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.community.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.community.comment.service.CommentService; import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; diff --git a/src/main/java/com/example/solidconnection/comment/domain/Comment.java b/src/main/java/com/example/solidconnection/community/comment/domain/Comment.java similarity index 96% rename from src/main/java/com/example/solidconnection/comment/domain/Comment.java rename to src/main/java/com/example/solidconnection/community/comment/domain/Comment.java index a4d147a61..abed4b8f0 100644 --- a/src/main/java/com/example/solidconnection/comment/domain/Comment.java +++ b/src/main/java/com/example/solidconnection/community/comment/domain/Comment.java @@ -1,7 +1,7 @@ -package com.example.solidconnection.comment.domain; +package com.example.solidconnection.community.comment.domain; import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java similarity index 81% rename from src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java index c2065685b..610f602c8 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java @@ -1,7 +1,7 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateResponse.java similarity index 62% rename from src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateResponse.java index 60d7529c2..58964f326 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; -import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.community.comment.domain.Comment; public record CommentCreateResponse( Long id diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentDeleteResponse.java similarity index 50% rename from src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentDeleteResponse.java index 393e4fe8b..5283bb87f 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentDeleteResponse.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; public record CommentDeleteResponse( Long id diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateRequest.java similarity index 85% rename from src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateRequest.java index d99429931..6e14dab45 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateRequest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateResponse.java similarity index 62% rename from src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateResponse.java index b621ab111..5446753e4 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; -import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.community.comment.domain.Comment; public record CommentUpdateResponse( Long id diff --git a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java similarity index 88% rename from src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java rename to src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java index a0d68066a..f1fd78ad0 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; -import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.community.comment.domain.Comment; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import java.time.ZonedDateTime; diff --git a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java similarity index 91% rename from src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java rename to src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java index ce37c42a1..e5feb3f04 100644 --- a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.comment.repository; +package com.example.solidconnection.community.comment.repository; -import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.community.comment.domain.Comment; import com.example.solidconnection.custom.exception.CustomException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java similarity index 84% rename from src/main/java/com/example/solidconnection/comment/service/CommentService.java rename to src/main/java/com/example/solidconnection/community/comment/service/CommentService.java index b7c1c6068..209dd6987 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java @@ -1,16 +1,16 @@ -package com.example.solidconnection.comment.service; - -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.dto.CommentCreateRequest; -import com.example.solidconnection.comment.dto.CommentCreateResponse; -import com.example.solidconnection.comment.dto.CommentDeleteResponse; -import com.example.solidconnection.comment.dto.CommentUpdateRequest; -import com.example.solidconnection.comment.dto.CommentUpdateResponse; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.repository.CommentRepository; +package com.example.solidconnection.community.comment.service; + +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.comment.dto.CommentCreateRequest; +import com.example.solidconnection.community.comment.dto.CommentCreateResponse; +import com.example.solidconnection.community.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.community.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.community.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.comment.repository.CommentRepository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/community/post/controller/PostController.java similarity index 75% rename from src/main/java/com/example/solidconnection/post/controller/PostController.java rename to src/main/java/com/example/solidconnection/community/post/controller/PostController.java index bc3f9d123..a2479f08b 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/community/post/controller/PostController.java @@ -1,17 +1,18 @@ -package com.example.solidconnection.post.controller; +package com.example.solidconnection.community.post.controller; +import com.example.solidconnection.community.post.dto.PostListResponse; import com.example.solidconnection.custom.resolver.AuthorizedUser; -import com.example.solidconnection.post.dto.PostCreateRequest; -import com.example.solidconnection.post.dto.PostCreateResponse; -import com.example.solidconnection.post.dto.PostDeleteResponse; -import com.example.solidconnection.post.dto.PostDislikeResponse; -import com.example.solidconnection.post.dto.PostFindResponse; -import com.example.solidconnection.post.dto.PostLikeResponse; -import com.example.solidconnection.post.dto.PostUpdateRequest; -import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.service.PostCommandService; -import com.example.solidconnection.post.service.PostLikeService; -import com.example.solidconnection.post.service.PostQueryService; +import com.example.solidconnection.community.post.dto.PostCreateRequest; +import com.example.solidconnection.community.post.dto.PostCreateResponse; +import com.example.solidconnection.community.post.dto.PostDeleteResponse; +import com.example.solidconnection.community.post.dto.PostDislikeResponse; +import com.example.solidconnection.community.post.dto.PostFindResponse; +import com.example.solidconnection.community.post.dto.PostLikeResponse; +import com.example.solidconnection.community.post.dto.PostUpdateRequest; +import com.example.solidconnection.community.post.dto.PostUpdateResponse; +import com.example.solidconnection.community.post.service.PostCommandService; +import com.example.solidconnection.community.post.service.PostLikeService; +import com.example.solidconnection.community.post.service.PostQueryService; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -39,6 +40,16 @@ public class PostController { private final PostCommandService postCommandService; private final PostLikeService postLikeService; + @GetMapping("/{code}") + public ResponseEntity findPostsByCodeAndCategory( + @PathVariable(value = "code") String code, + @RequestParam(value = "category", defaultValue = "전체") String category) { + + List postsByCodeAndPostCategory = postQueryService + .findPostsByCodeAndPostCategory(code, category); + return ResponseEntity.ok().body(postsByCodeAndPostCategory); + } + @PostMapping(value = "/{code}/posts") public ResponseEntity createPost( @AuthorizedUser SiteUser siteUser, diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/community/post/domain/Post.java similarity index 92% rename from src/main/java/com/example/solidconnection/post/domain/Post.java rename to src/main/java/com/example/solidconnection/community/post/domain/Post.java index 31125f8bd..4d96b9b22 100644 --- a/src/main/java/com/example/solidconnection/post/domain/Post.java +++ b/src/main/java/com/example/solidconnection/community/post/domain/Post.java @@ -1,10 +1,9 @@ -package com.example.solidconnection.post.domain; +package com.example.solidconnection.community.post.domain; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.comment.domain.Comment; import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.post.dto.PostUpdateRequest; +import com.example.solidconnection.community.post.dto.PostUpdateRequest; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; import jakarta.persistence.CascadeType; diff --git a/src/main/java/com/example/solidconnection/entity/PostImage.java b/src/main/java/com/example/solidconnection/community/post/domain/PostImage.java similarity index 90% rename from src/main/java/com/example/solidconnection/entity/PostImage.java rename to src/main/java/com/example/solidconnection/community/post/domain/PostImage.java index 653beecc4..5bf885741 100644 --- a/src/main/java/com/example/solidconnection/entity/PostImage.java +++ b/src/main/java/com/example/solidconnection/community/post/domain/PostImage.java @@ -1,6 +1,5 @@ -package com.example.solidconnection.entity; +package com.example.solidconnection.community.post.domain; -import com.example.solidconnection.post.domain.Post; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; diff --git a/src/main/java/com/example/solidconnection/post/domain/PostLike.java b/src/main/java/com/example/solidconnection/community/post/domain/PostLike.java similarity index 96% rename from src/main/java/com/example/solidconnection/post/domain/PostLike.java rename to src/main/java/com/example/solidconnection/community/post/domain/PostLike.java index 9edf4052e..bbe1ff361 100644 --- a/src/main/java/com/example/solidconnection/post/domain/PostLike.java +++ b/src/main/java/com/example/solidconnection/community/post/domain/PostLike.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.post.domain; +package com.example.solidconnection.community.post.domain; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.persistence.Entity; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java similarity index 87% rename from src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java index a1ba1c696..db271a80f 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java @@ -1,7 +1,7 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateResponse.java similarity index 62% rename from src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostCreateResponse.java index a514ffca6..51cc0c72e 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; public record PostCreateResponse( Long id diff --git a/src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostDeleteResponse.java similarity index 50% rename from src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostDeleteResponse.java index 23c67670d..f98f5264f 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostDeleteResponse.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; public record PostDeleteResponse( Long id diff --git a/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostDislikeResponse.java similarity index 68% rename from src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostDislikeResponse.java index 14de9987d..83ffc8305 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostDislikeResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; public record PostDislikeResponse( Long likeCount, diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostFindPostImageResponse.java similarity index 82% rename from src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostFindPostImageResponse.java index 63adf0020..648bdb72c 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostFindPostImageResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.community.post.domain.PostImage; import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostFindResponse.java similarity index 86% rename from src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostFindResponse.java index 1562dd5bc..735defac1 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostFindResponse.java @@ -1,8 +1,8 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.board.dto.PostFindBoardResponse; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.board.dto.PostFindBoardResponse; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import java.time.ZonedDateTime; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostLikeResponse.java similarity index 68% rename from src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostLikeResponse.java index 35d7d58c9..35b2840c0 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostLikeResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; public record PostLikeResponse( Long likeCount, diff --git a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostListResponse.java similarity index 72% rename from src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostListResponse.java index 4f475824c..f02af017e 100644 --- a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostListResponse.java @@ -1,13 +1,13 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostImage; +import com.example.solidconnection.community.post.domain.Post; import java.time.ZonedDateTime; import java.util.List; import java.util.stream.Collectors; -public record BoardFindPostResponse( +public record PostListResponse( Long id, String title, String content, @@ -19,8 +19,8 @@ public record BoardFindPostResponse( String url ) { - public static BoardFindPostResponse from(Post post) { - return new BoardFindPostResponse( + public static PostListResponse from(Post post) { + return new PostListResponse( post.getId(), post.getTitle(), post.getContent(), @@ -33,9 +33,9 @@ public static BoardFindPostResponse from(Post post) { ); } - public static List from(List postList) { + public static List from(List postList) { return postList.stream() - .map(BoardFindPostResponse::from) + .map(PostListResponse::from) .collect(Collectors.toList()); } diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java b/src/main/java/com/example/solidconnection/community/post/dto/PostUpdateRequest.java similarity index 92% rename from src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostUpdateRequest.java index b9bdc6f54..339be3519 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostUpdateRequest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostUpdateResponse.java similarity index 62% rename from src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostUpdateResponse.java index 70d656766..5c35f031d 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostUpdateResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; public record PostUpdateResponse( Long id diff --git a/src/main/java/com/example/solidconnection/repositories/PostImageRepository.java b/src/main/java/com/example/solidconnection/community/post/repository/PostImageRepository.java similarity index 61% rename from src/main/java/com/example/solidconnection/repositories/PostImageRepository.java rename to src/main/java/com/example/solidconnection/community/post/repository/PostImageRepository.java index 0ae776877..54c43f375 100644 --- a/src/main/java/com/example/solidconnection/repositories/PostImageRepository.java +++ b/src/main/java/com/example/solidconnection/community/post/repository/PostImageRepository.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.repositories; +package com.example.solidconnection.community.post.repository; -import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.community.post.domain.PostImage; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java b/src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java similarity index 79% rename from src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java rename to src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java index bebde7a92..417e97310 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java +++ b/src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java @@ -1,8 +1,8 @@ -package com.example.solidconnection.post.repository; +package com.example.solidconnection.community.post.repository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostLike; import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java similarity index 93% rename from src/main/java/com/example/solidconnection/post/repository/PostRepository.java rename to src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java index e96881147..336189b05 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java +++ b/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java @@ -1,7 +1,7 @@ -package com.example.solidconnection.post.repository; +package com.example.solidconnection.community.post.repository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; diff --git a/src/main/java/com/example/solidconnection/post/service/PostCommandService.java b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java similarity index 87% rename from src/main/java/com/example/solidconnection/post/service/PostCommandService.java rename to src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java index 74eb86310..1e66b52a4 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostCommandService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java @@ -1,16 +1,16 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.repository.BoardRepository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostCreateRequest; -import com.example.solidconnection.post.dto.PostCreateResponse; -import com.example.solidconnection.post.dto.PostDeleteResponse; -import com.example.solidconnection.post.dto.PostUpdateRequest; -import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.PostImage; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostCreateRequest; +import com.example.solidconnection.community.post.dto.PostCreateResponse; +import com.example.solidconnection.community.post.dto.PostDeleteResponse; +import com.example.solidconnection.community.post.dto.PostUpdateRequest; +import com.example.solidconnection.community.post.dto.PostUpdateResponse; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; diff --git a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java b/src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java similarity index 83% rename from src/main/java/com/example/solidconnection/post/service/PostLikeService.java rename to src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java index 5aaf994c7..045c069cd 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java @@ -1,12 +1,12 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; -import com.example.solidconnection.post.dto.PostDislikeResponse; -import com.example.solidconnection.post.dto.PostLikeResponse; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostLike; +import com.example.solidconnection.community.post.dto.PostDislikeResponse; +import com.example.solidconnection.community.post.dto.PostLikeResponse; +import com.example.solidconnection.community.post.repository.PostLikeRepository; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.BoardCode; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java similarity index 55% rename from src/main/java/com/example/solidconnection/post/service/PostQueryService.java rename to src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java index a45ca3968..1d7f292ea 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java @@ -1,36 +1,56 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; -import com.example.solidconnection.board.dto.PostFindBoardResponse; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.dto.PostFindBoardResponse; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.post.dto.PostListResponse; +import com.example.solidconnection.community.board.repository.BoardRepository; +import com.example.solidconnection.community.comment.service.CommentService; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostFindPostImageResponse; -import com.example.solidconnection.post.dto.PostFindResponse; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.community.post.dto.PostFindResponse; +import com.example.solidconnection.community.post.repository.PostLikeRepository; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.stream.Collectors; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; @Service @RequiredArgsConstructor public class PostQueryService { + private final BoardRepository boardRepository; private final PostRepository postRepository; + private final PostLikeRepository postLikeRepository; private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; - private final PostLikeRepository postLikeRepository; + + @Transactional(readOnly = true) + public List findPostsByCodeAndPostCategory(String code, String category) { + + String boardCode = validateCode(code); + PostCategory postCategory = validatePostCategory(category); + + Board board = boardRepository.getByCodeUsingEntityGraph(boardCode); + List postList = getPostListByPostCategory(board.getPostList(), postCategory); + + return PostListResponse.from(postList); + } @Transactional(readOnly = true) public PostFindResponse findPostById(SiteUser siteUser, String code, Long postId) { @@ -70,4 +90,20 @@ private Boolean getIsLiked(Post post, SiteUser siteUser) { return postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser) .isPresent(); } + + private PostCategory validatePostCategory(String category) { + if (!EnumUtils.isValidEnum(PostCategory.class, category)) { + throw new CustomException(INVALID_POST_CATEGORY); + } + return PostCategory.valueOf(category); + } + + private List getPostListByPostCategory(List postList, PostCategory postCategory) { + if (postCategory.equals(PostCategory.전체)) { + return postList; + } + return postList.stream() + .filter(post -> post.getCategory().equals(postCategory)) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java b/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java index 55d4d9eba..2b67e25ec 100644 --- a/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java +++ b/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java @@ -1,7 +1,7 @@ package com.example.solidconnection.service; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 2c2a5d8be..83a439b19 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -1,8 +1,8 @@ package com.example.solidconnection.siteuser.domain; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostLike; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.type.Gender; diff --git a/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java deleted file mode 100644 index 98c2b28fa..000000000 --- a/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.example.solidconnection.board.service; - -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.BoardFindPostResponse; -import com.example.solidconnection.support.integration.BaseIntegrationTest; -import com.example.solidconnection.type.BoardCode; -import com.example.solidconnection.type.PostCategory; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.time.ZonedDateTime; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("게시판 서비스 테스트") -class BoardServiceTest extends BaseIntegrationTest { - - @Autowired - private BoardService boardService; - - @Test - void 게시판_코드와_카테고리로_게시글_목록을_조회한다() { - // given - List posts = List.of( - 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, - 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 - ); - List expectedPosts = posts.stream() - .filter(post -> post.getCategory().equals(PostCategory.자유) && post.getBoard().getCode().equals(BoardCode.FREE.name())) - .toList(); - List expectedResponses = BoardFindPostResponse.from(expectedPosts); - - // when - List actualResponses = boardService.findPostsByCodeAndPostCategory( - BoardCode.FREE.name(), - PostCategory.자유.name() - ); - - // then - assertThat(actualResponses) - .usingRecursiveComparison() - .ignoringFieldsOfTypes(ZonedDateTime.class) - .isEqualTo(expectedResponses); - } - - @Test - void 전체_카테고리로_조회시_해당_게시판의_모든_게시글을_조회한다() { - // given - List posts = List.of( - 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, - 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 - ); - List expectedPosts = posts.stream() - .filter(post -> post.getBoard().getCode().equals(BoardCode.FREE.name())) - .toList(); - List expectedResponses = BoardFindPostResponse.from(expectedPosts); - - // when - List actualResponses = boardService.findPostsByCodeAndPostCategory( - BoardCode.FREE.name(), - PostCategory.전체.name() - ); - - // then - assertThat(actualResponses) - .usingRecursiveComparison() - .ignoringFieldsOfTypes(ZonedDateTime.class) - .isEqualTo(expectedResponses); - } -} diff --git a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java similarity index 95% rename from src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java rename to src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java index d38463dcb..fca6cd41e 100644 --- a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java @@ -1,17 +1,17 @@ -package com.example.solidconnection.comment.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.dto.CommentCreateRequest; -import com.example.solidconnection.comment.dto.CommentCreateResponse; -import com.example.solidconnection.comment.dto.CommentDeleteResponse; -import com.example.solidconnection.comment.dto.CommentUpdateRequest; -import com.example.solidconnection.comment.dto.CommentUpdateResponse; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.repository.CommentRepository; +package com.example.solidconnection.community.comment.service; + +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.comment.dto.CommentCreateRequest; +import com.example.solidconnection.community.comment.dto.CommentCreateResponse; +import com.example.solidconnection.community.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.community.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.community.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.comment.repository.CommentRepository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.PostCategory; diff --git a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java similarity index 94% rename from src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java rename to src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java index 3cdc5a40c..a8052a89c 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java @@ -1,16 +1,16 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; -import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostCreateRequest; -import com.example.solidconnection.post.dto.PostCreateResponse; -import com.example.solidconnection.post.dto.PostDeleteResponse; -import com.example.solidconnection.post.dto.PostUpdateRequest; -import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.repositories.PostImageRepository; +import com.example.solidconnection.community.post.domain.PostImage; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostCreateRequest; +import com.example.solidconnection.community.post.dto.PostCreateResponse; +import com.example.solidconnection.community.post.dto.PostDeleteResponse; +import com.example.solidconnection.community.post.dto.PostUpdateRequest; +import com.example.solidconnection.community.post.dto.PostUpdateResponse; +import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.community.post.repository.PostImageRepository; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; diff --git a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java similarity index 91% rename from src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java rename to src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java index 460b9a15b..1b1e1d2fd 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java @@ -1,12 +1,12 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; -import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostDislikeResponse; -import com.example.solidconnection.post.dto.PostLikeResponse; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostDislikeResponse; +import com.example.solidconnection.community.post.dto.PostLikeResponse; +import com.example.solidconnection.community.post.repository.PostLikeRepository; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.PostCategory; diff --git a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java similarity index 60% rename from src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java rename to src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java index d9acf5845..33246e981 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java @@ -1,24 +1,27 @@ -package com.example.solidconnection.post.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostFindPostImageResponse; -import com.example.solidconnection.post.dto.PostFindResponse; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.repositories.PostImageRepository; +package com.example.solidconnection.community.post.service; + +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.post.dto.PostListResponse; +import com.example.solidconnection.community.comment.repository.CommentRepository; +import com.example.solidconnection.community.post.domain.PostImage; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.community.post.dto.PostFindResponse; +import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.community.post.repository.PostImageRepository; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.util.RedisUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.time.ZonedDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -45,6 +48,56 @@ class PostQueryServiceTest extends BaseIntegrationTest { @Autowired private PostImageRepository postImageRepository; + @Test + void 게시판_코드와_카테고리로_게시글_목록을_조회한다() { + // given + List posts = List.of( + 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, + 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 + ); + List expectedPosts = posts.stream() + .filter(post -> post.getCategory().equals(PostCategory.자유) && post.getBoard().getCode().equals(BoardCode.FREE.name())) + .toList(); + List expectedResponses = PostListResponse.from(expectedPosts); + + // when + List actualResponses = postQueryService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.자유.name() + ); + + // then + assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses); + } + + @Test + void 전체_카테고리로_조회시_해당_게시판의_모든_게시글을_조회한다() { + // given + List posts = List.of( + 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, + 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 + ); + List expectedPosts = posts.stream() + .filter(post -> post.getBoard().getCode().equals(BoardCode.FREE.name())) + .toList(); + List expectedResponses = PostListResponse.from(expectedPosts); + + // when + List actualResponses = postQueryService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.전체.name() + ); + + // then + assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses); + } + @Test void 게시글을_성공적으로_조회한다() { // given diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index 544b31b4c..3903f31ff 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -1,10 +1,10 @@ package com.example.solidconnection.concurrency; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostLikeService; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.repository.BoardRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.community.post.service.PostLikeService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index 678e2b084..2cb6eaa27 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -1,9 +1,9 @@ package com.example.solidconnection.concurrency; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.repository.BoardRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index f7378468c..989e0bc31 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -4,15 +4,15 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.repository.BoardRepository; import com.example.solidconnection.entity.Country; -import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.community.post.domain.PostImage; import com.example.solidconnection.entity.Region; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.repositories.CountryRepository; -import com.example.solidconnection.repositories.PostImageRepository; +import com.example.solidconnection.community.post.repository.PostImageRepository; import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; From 0adbb4cf4389049077b3594347821677f5c63696 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sun, 9 Feb 2025 02:07:30 +0900 Subject: [PATCH 142/158] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84=20(#188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 사용자에 password 컬럼 추가 * feat: 비밀번호 암호화 빈 추가 * refactor: Oauth 가입용 토큰 제공자 이름 변경 * feat: 이메일 가입용 토큰 제공자 구현 * feat: 이메일 회원가입 구현, 회원가입 서비스 추상화 * feat: 이메일 로그인 구현 * feat: 이메일 로그인, 회원가입 엔트포인트 추가 * refactor: 잘못 이해해서 다르게 구현한 api 수정 * test: 이메일 로그인 테스트 작성 * refactor: 이메일 형식으로 검증하노록 dto 어노테이션 수정 * refactor: 이메일 중복 검증이 앞에 위치하도록 --- .../auth/controller/AuthController.java | 35 +++++++ .../auth/dto/EmailSignInRequest.java | 13 +++ .../auth/dto/EmailSignUpTokenRequest.java | 14 +++ .../auth/dto/EmailSignUpTokenResponse.java | 6 ++ .../auth/dto/SignUpRequest.java | 16 ++- .../service/CommonSignUpTokenProvider.java | 27 +++++ .../auth/service/EmailSignInService.java | 43 ++++++++ .../auth/service/EmailSignUpService.java | 54 ++++++++++ .../service/EmailSignUpTokenProvider.java | 92 +++++++++++++++++ .../auth/service/SignUpService.java | 90 +++++++++++++++++ .../auth/service/oauth/AppleOAuthService.java | 4 +- .../auth/service/oauth/KakaoOAuthService.java | 4 +- .../auth/service/oauth/OAuthService.java | 8 +- .../service/oauth/OAuthSignUpService.java | 83 ++++------------ ...der.java => OAuthSignUpTokenProvider.java} | 12 +-- .../security/SecurityConfiguration.java | 7 ++ .../custom/exception/ErrorCode.java | 6 +- .../siteuser/domain/AuthType.java | 4 + .../siteuser/domain/SiteUser.java | 25 +++++ .../db/migration/V5__add_password_column.sql | 2 + .../auth/service/EmailSignInServiceTest.java | 99 +++++++++++++++++++ ...java => OAuthSignUpTokenProviderTest.java} | 40 ++++---- .../solidconnection/e2e/SignUpTest.java | 14 +-- 23 files changed, 591 insertions(+), 107 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/SignUpService.java rename src/main/java/com/example/solidconnection/auth/service/oauth/{SignUpTokenProvider.java => OAuthSignUpTokenProvider.java} (88%) create mode 100644 src/main/resources/db/migration/V5__add_password_column.sql create mode 100644 src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java rename src/test/java/com/example/solidconnection/auth/service/oauth/{SignUpTokenProviderTest.java => OAuthSignUpTokenProviderTest.java} (79%) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index aa3ce4f20..80520942f 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -1,17 +1,25 @@ package com.example.solidconnection.auth.controller; +import com.example.solidconnection.auth.dto.EmailSignInRequest; +import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; +import com.example.solidconnection.auth.dto.EmailSignUpTokenResponse; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; import com.example.solidconnection.auth.service.AuthService; +import com.example.solidconnection.auth.service.CommonSignUpTokenProvider; +import com.example.solidconnection.auth.service.EmailSignInService; +import com.example.solidconnection.auth.service.EmailSignUpService; +import com.example.solidconnection.auth.service.EmailSignUpTokenProvider; import com.example.solidconnection.auth.service.oauth.AppleOAuthService; import com.example.solidconnection.auth.service.oauth.KakaoOAuthService; import com.example.solidconnection.auth.service.oauth.OAuthSignUpService; import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.custom.resolver.ExpiredToken; import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -31,6 +39,10 @@ public class AuthController { private final OAuthSignUpService oAuthSignUpService; private final AppleOAuthService appleOAuthService; private final KakaoOAuthService kakaoOAuthService; + private final EmailSignInService emailSignInService; + private final EmailSignUpService emailSignUpService; + private final EmailSignUpTokenProvider emailSignUpTokenProvider; + private final CommonSignUpTokenProvider commonSignUpTokenProvider; @PostMapping("/apple") public ResponseEntity processAppleOAuth( @@ -48,10 +60,33 @@ public ResponseEntity processKakaoOAuth( return ResponseEntity.ok(oAuthResponse); } + @PostMapping("/email/sign-in") + public ResponseEntity signInWithEmail( + @Valid @RequestBody EmailSignInRequest signInRequest + ) { + SignInResponse signInResponse = emailSignInService.signIn(signInRequest); + return ResponseEntity.ok(signInResponse); + } + + /* 이메일 회원가입 시 signUpToken 을 발급받기 위한 api */ + @PostMapping("/email/sign-up") + public ResponseEntity signUpWithEmail( + @Valid @RequestBody EmailSignUpTokenRequest signUpRequest + ) { + emailSignUpService.validateUniqueEmail(signUpRequest.email()); + String signUpToken = emailSignUpTokenProvider.generateAndSaveSignUpToken(signUpRequest); + return ResponseEntity.ok(new EmailSignUpTokenResponse(signUpToken)); + } + @PostMapping("/sign-up") public ResponseEntity signUp( @Valid @RequestBody SignUpRequest signUpRequest ) { + AuthType authType = commonSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); + if (AuthType.isEmail(authType)) { + SignInResponse signInResponse = emailSignUpService.signUp(signUpRequest); + return ResponseEntity.ok(signInResponse); + } SignInResponse signInResponse = oAuthSignUpService.signUp(signUpRequest); return ResponseEntity.ok(signInResponse); } diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java new file mode 100644 index 000000000..306a8185a --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record EmailSignInRequest( + + @NotBlank(message = "이메일을 입력해주세요.") + String email, + + @NotBlank(message = "비밀번호를 입력해주세요.") + String password +) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java new file mode 100644 index 000000000..92073b434 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record EmailSignUpTokenRequest( + + @Email(message = "이메일을 입력해주세요.") + String email, + + @NotBlank(message = "비밀번호를 입력해주세요.") + String password +) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java new file mode 100644 index 000000000..c8e983d0c --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.auth.dto; + +public record EmailSignUpTokenResponse( + String signUpToken +) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java index b28b467bd..43f8e6caf 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java @@ -24,7 +24,7 @@ public record SignUpRequest( @JsonFormat(pattern = "yyyy-MM-dd") String birth) { - public SiteUser toSiteUser(String email, AuthType authType) { + public SiteUser toOAuthSiteUser(String email, AuthType authType) { return new SiteUser( email, this.nickname, @@ -36,4 +36,18 @@ public SiteUser toSiteUser(String email, AuthType authType) { authType ); } + + public SiteUser toEmailSiteUser(String email, String encodedPassword) { + return new SiteUser( + email, + this.nickname, + this.profileImageUrl, + this.birth, + this.preparationStatus, + Role.MENTEE, + this.gender, + AuthType.EMAIL, + encodedPassword + ); + } } diff --git a/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java new file mode 100644 index 000000000..3d0eda53b --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.util.JwtUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.auth.service.EmailSignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; + +@Component +@RequiredArgsConstructor +public class CommonSignUpTokenProvider { + + private final JwtProperties jwtProperties; + + public AuthType parseAuthType(String signUpToken) { + try { + String authTypeStr = JwtUtils.parseClaims(signUpToken, jwtProperties.secret()).get(AUTH_TYPE_CLAIM_KEY, String.class); + return AuthType.valueOf(authTypeStr); + } catch (Exception e) { + throw new CustomException(SIGN_UP_TOKEN_INVALID); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java new file mode 100644 index 000000000..bbbb4f85c --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java @@ -0,0 +1,43 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.EmailSignInRequest; +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; + +/* + * 보안을 위해 이메일과 비밀번호 중 무엇이 틀렸는지 구체적으로 응답하지 않는다. + * */ +@Service +@RequiredArgsConstructor +public class EmailSignInService { + + private final SignInService signInService; + private final SiteUserRepository siteUserRepository; + private final PasswordEncoder passwordEncoder; + + public SignInResponse signIn(EmailSignInRequest signInRequest) { + Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(signInRequest.email(), AuthType.EMAIL); + if (optionalSiteUser.isPresent()) { + SiteUser siteUser = optionalSiteUser.get(); + validatePassword(signInRequest.password(), siteUser.getPassword()); + return signInService.signIn(siteUser); + } + throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요."); + } + + private void validatePassword(String rawPassword, String encodedPassword) throws CustomException { + if (!passwordEncoder.matches(rawPassword, encodedPassword)) { + throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요."); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java new file mode 100644 index 000000000..37f6681ea --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; + +@Service +public class EmailSignUpService extends SignUpService { + + private final EmailSignUpTokenProvider emailSignUpTokenProvider; + + public EmailSignUpService(SignInService signInService, SiteUserRepository siteUserRepository, + RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, + CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository, + EmailSignUpTokenProvider emailSignUpTokenProvider) { + super(signInService, siteUserRepository, regionRepository, interestedRegionRepository, countryRepository, interestedCountyRepository); + this.emailSignUpTokenProvider = emailSignUpTokenProvider; + } + + public void validateUniqueEmail(String email) { + if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) { + throw new CustomException(USER_ALREADY_EXISTED); + } + } + + @Override + protected void validateSignUpToken(SignUpRequest signUpRequest) { + emailSignUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); + } + + @Override + protected void validateUserNotDuplicated(SignUpRequest signUpRequest) { + String email = emailSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) { + throw new CustomException(USER_ALREADY_EXISTED); + } + } + + @Override + protected SiteUser createSiteUser(SignUpRequest signUpRequest) { + String email = emailSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + String encodedPassword = emailSignUpTokenProvider.parseEncodedPassword(signUpRequest.signUpToken()); + return signUpRequest.toEmailSiteUser(email, encodedPassword); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java new file mode 100644 index 000000000..1c27a87bd --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java @@ -0,0 +1,92 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static com.example.solidconnection.util.JwtUtils.parseClaims; +import static com.example.solidconnection.util.JwtUtils.parseSubject; + +@Component +public class EmailSignUpTokenProvider extends TokenProvider { + + static final String PASSWORD_CLAIM_KEY = "password"; + static final String AUTH_TYPE_CLAIM_KEY = "authType"; + + private final PasswordEncoder passwordEncoder; + + public EmailSignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate, + PasswordEncoder passwordEncoder) { + super(jwtProperties, redisTemplate); + this.passwordEncoder = passwordEncoder; + } + + public String generateAndSaveSignUpToken(EmailSignUpTokenRequest request) { + String email = request.email(); + String password = request.password(); + String encodedPassword = passwordEncoder.encode(password); + Map emailSignUpClaims = new HashMap<>(Map.of( + PASSWORD_CLAIM_KEY, encodedPassword, + AUTH_TYPE_CLAIM_KEY, AuthType.EMAIL + )); + Claims claims = Jwts.claims(emailSignUpClaims).setSubject(email); + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + TokenType.SIGN_UP.getExpireTime()); + + String signUpToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) + .compact(); + return saveToken(signUpToken, TokenType.SIGN_UP); + } + + public void validateSignUpToken(String token) { + validateFormatAndExpiration(token); + String email = parseEmail(token); + validateIssuedByServer(email); + } + + private void validateFormatAndExpiration(String token) { + try { + Claims claims = parseClaims(token, jwtProperties.secret()); + Objects.requireNonNull(claims.getSubject()); + String encodedPassword = claims.get(PASSWORD_CLAIM_KEY, String.class); + Objects.requireNonNull(encodedPassword); + } catch (Exception e) { + throw new CustomException(SIGN_UP_TOKEN_INVALID); + } + } + + private void validateIssuedByServer(String email) { + String key = TokenType.SIGN_UP.addPrefix(email); + if (redisTemplate.opsForValue().get(key) == null) { + throw new CustomException(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); + } + } + + public String parseEmail(String token) { + return parseSubject(token, jwtProperties.secret()); + } + + public String parseEncodedPassword(String token) { + Claims claims = parseClaims(token, jwtProperties.secret()); + return claims.get(PASSWORD_CLAIM_KEY, String.class); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java new file mode 100644 index 000000000..319083658 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -0,0 +1,90 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.InterestedCountry; +import com.example.solidconnection.entity.InterestedRegion; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; + +/* + * 우리 서버에서 인증되었음을 확인하기 위한 signUpToken 을 검증한다. + * - 사용자 정보를 DB에 저장한다. + * - 관심 국가와 지역을 DB에 저장한다. + * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. + * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. + * */ +public abstract class SignUpService { + + protected final SignInService signInService; + protected final SiteUserRepository siteUserRepository; + protected final RegionRepository regionRepository; + protected final InterestedRegionRepository interestedRegionRepository; + protected final CountryRepository countryRepository; + protected final InterestedCountyRepository interestedCountyRepository; + + protected SignUpService(SignInService signInService, SiteUserRepository siteUserRepository, + RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, + CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository) { + this.signInService = signInService; + this.siteUserRepository = siteUserRepository; + this.regionRepository = regionRepository; + this.interestedRegionRepository = interestedRegionRepository; + this.countryRepository = countryRepository; + this.interestedCountyRepository = interestedCountyRepository; + } + + @Transactional + public SignInResponse signUp(SignUpRequest signUpRequest) { + // 검증 + validateSignUpToken(signUpRequest); + validateUserNotDuplicated(signUpRequest); + validateNicknameDuplicated(signUpRequest.nickname()); + + // 사용자 저장 + SiteUser siteUser = siteUserRepository.save(createSiteUser(signUpRequest)); + + // 관심 지역, 국가 저장 + saveInterestedRegion(signUpRequest, siteUser); + saveInterestedCountry(signUpRequest, siteUser); + + // 로그인 + return signInService.signIn(siteUser); + } + + private void validateNicknameDuplicated(String nickname) { + if (siteUserRepository.existsByNickname(nickname)) { + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + + private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) { + List interestedRegionNames = signUpRequest.interestedRegions(); + List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream() + .map(region -> new InterestedRegion(savedSiteUser, region)) + .toList(); + interestedRegionRepository.saveAll(interestedRegions); + } + + private void saveInterestedCountry(SignUpRequest signUpRequest, SiteUser savedSiteUser) { + List interestedCountryNames = signUpRequest.interestedCountries(); + List interestedCountries = countryRepository.findByKoreanNames(interestedCountryNames).stream() + .map(country -> new InterestedCountry(savedSiteUser, country)) + .toList(); + interestedCountyRepository.saveAll(interestedCountries); + } + + protected abstract void validateSignUpToken(SignUpRequest signUpRequest); + protected abstract void validateUserNotDuplicated(SignUpRequest signUpRequest); + protected abstract SiteUser createSiteUser(SignUpRequest signUpRequest); +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java index 2af82e07d..2605ad89f 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java @@ -12,9 +12,9 @@ public class AppleOAuthService extends OAuthService { private final AppleOAuthClient appleOAuthClient; - public AppleOAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, + public AppleOAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, AppleOAuthClient appleOAuthClient, SignInService signInService) { - super(signUpTokenProvider, siteUserRepository, signInService); + super(OAuthSignUpTokenProvider, siteUserRepository, signInService); this.appleOAuthClient = appleOAuthClient; } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java index 5dc6faea1..c2202ab2a 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java @@ -12,9 +12,9 @@ public class KakaoOAuthService extends OAuthService { private final KakaoOAuthClient kakaoOAuthClient; - public KakaoOAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, + public KakaoOAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, KakaoOAuthClient kakaoOAuthClient, SignInService signInService) { - super(signUpTokenProvider, siteUserRepository, signInService); + super(OAuthSignUpTokenProvider, siteUserRepository, signInService); this.kakaoOAuthClient = kakaoOAuthClient; } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java index 4f37db060..6e9bf7030 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java @@ -22,12 +22,12 @@ * */ public abstract class OAuthService { - private final SignUpTokenProvider signUpTokenProvider; + private final OAuthSignUpTokenProvider OAuthSignUpTokenProvider; private final SignInService signInService; private final SiteUserRepository siteUserRepository; - protected OAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, SignInService signInService) { - this.signUpTokenProvider = signUpTokenProvider; + protected OAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, SignInService signInService) { + this.OAuthSignUpTokenProvider = OAuthSignUpTokenProvider; this.siteUserRepository = siteUserRepository; this.signInService = signInService; } @@ -52,7 +52,7 @@ protected final OAuthSignInResponse getSignInResponse(SiteUser siteUser) { } protected final SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto) { - String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), getAuthType()); + String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), getAuthType()); return SignUpPrepareResponse.of(userInfoDto, signUpToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java index 7b6d44d26..a46728bb2 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java @@ -1,11 +1,9 @@ package com.example.solidconnection.auth.service.oauth; -import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.auth.service.SignUpService; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.InterestedCountry; -import com.example.solidconnection.entity.InterestedRegion; import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; @@ -13,80 +11,41 @@ import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.List; - -import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; -@RequiredArgsConstructor @Service -public class OAuthSignUpService { - - private final SignUpTokenProvider signUpTokenProvider; - private final SignInService signInService; - private final SiteUserRepository siteUserRepository; - private final RegionRepository regionRepository; - private final InterestedRegionRepository interestedRegionRepository; - private final CountryRepository countryRepository; - private final InterestedCountyRepository interestedCountyRepository; - - /* - * OAuth 인증 후 회원가입을 한다. - * - 우리 서버에서 OAuth 인증했음을 확인하기 위한 signUpToken 을 검증한다. - * - 사용자 정보를 DB에 저장한다. - * - 관심 국가와 지역을 DB에 저장한다. - * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. - * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. - * */ - @Transactional - public SignInResponse signUp(SignUpRequest signUpRequest) { - // 검증 - signUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); - validateNicknameDuplicated(signUpRequest.nickname()); - String email = signUpTokenProvider.parseEmail(signUpRequest.signUpToken()); - AuthType authType = signUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); - validateUserNotDuplicated(email, authType); - - // 사용자 저장 - SiteUser siteUser = siteUserRepository.save(signUpRequest.toSiteUser(email, authType)); +public class OAuthSignUpService extends SignUpService { - // 관심 지역, 국가 저장 - saveInterestedRegion(signUpRequest, siteUser); - saveInterestedCountry(signUpRequest, siteUser); + private final OAuthSignUpTokenProvider oAuthSignUpTokenProvider; - // 로그인 - return signInService.signIn(siteUser); + OAuthSignUpService(SignInService signInService, SiteUserRepository siteUserRepository, + RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, + CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository, + OAuthSignUpTokenProvider oAuthSignUpTokenProvider) { + super(signInService, siteUserRepository, regionRepository, interestedRegionRepository, countryRepository, interestedCountyRepository); + this.oAuthSignUpTokenProvider = oAuthSignUpTokenProvider; } - private void validateNicknameDuplicated(String nickname) { - if (siteUserRepository.existsByNickname(nickname)) { - throw new CustomException(NICKNAME_ALREADY_EXISTED); - } + @Override + protected void validateSignUpToken(SignUpRequest signUpRequest) { + oAuthSignUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); } - private void validateUserNotDuplicated(String email, AuthType authType) { + @Override + protected void validateUserNotDuplicated(SignUpRequest signUpRequest) { + String email = oAuthSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + AuthType authType = oAuthSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); if (siteUserRepository.existsByEmailAndAuthType(email, authType)) { throw new CustomException(USER_ALREADY_EXISTED); } } - private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) { - List interestedRegionNames = signUpRequest.interestedRegions(); - List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream() - .map(region -> new InterestedRegion(savedSiteUser, region)) - .toList(); - interestedRegionRepository.saveAll(interestedRegions); - } - - private void saveInterestedCountry(SignUpRequest signUpRequest, SiteUser savedSiteUser) { - List interestedCountryNames = signUpRequest.interestedCountries(); - List interestedCountries = countryRepository.findByKoreanNames(interestedCountryNames).stream() - .map(country -> new InterestedCountry(savedSiteUser, country)) - .toList(); - interestedCountyRepository.saveAll(interestedCountries); + @Override + protected SiteUser createSiteUser(SignUpRequest signUpRequest) { + String email = oAuthSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + AuthType authType = oAuthSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); + return signUpRequest.toOAuthSiteUser(email, authType); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java similarity index 88% rename from src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java rename to src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java index 5399dc1eb..c3a95dbe9 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java @@ -16,17 +16,17 @@ import java.util.Map; import java.util.Objects; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; import static com.example.solidconnection.util.JwtUtils.parseClaims; import static com.example.solidconnection.util.JwtUtils.parseSubject; @Component -public class SignUpTokenProvider extends TokenProvider { +public class OAuthSignUpTokenProvider extends TokenProvider { static final String AUTH_TYPE_CLAIM_KEY = "authType"; - public SignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + public OAuthSignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { super(jwtProperties, redisTemplate); } @@ -58,14 +58,14 @@ private void validateFormatAndExpiration(String token) { String serializedAuthType = claims.get(AUTH_TYPE_CLAIM_KEY, String.class); AuthType.valueOf(serializedAuthType); } catch (Exception e) { - throw new CustomException(OAUTH_SIGN_UP_TOKEN_INVALID); + throw new CustomException(SIGN_UP_TOKEN_INVALID); } } private void validateIssuedByServer(String email) { String key = TokenType.SIGN_UP.addPrefix(email); if (redisTemplate.opsForValue().get(key) == null) { - throw new CustomException(OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); + throw new CustomException(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); } } diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 6851b3e8c..98492568b 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -10,6 +10,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -40,6 +42,11 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index d3fdf136d..cd0bb6695 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -28,9 +28,9 @@ public enum ErrorCode { KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(), "카카오 사용자 정보 조회에 실패했습니다."), INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN(HttpStatus.BAD_REQUEST.value(), "우리 서비스에서 발급한 카카오 토큰이 아닙니다"), - // oauth - OAUTH_SIGN_UP_TOKEN_INVALID(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 회원가입 토큰입니다."), - OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER(HttpStatus.BAD_REQUEST.value(), "회원가입 토큰이 우리 서버에서 발급되지 않았습니다."), + // sign up token + SIGN_UP_TOKEN_INVALID(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 회원가입 토큰입니다."), + SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER(HttpStatus.BAD_REQUEST.value(), "회원가입 토큰이 우리 서버에서 발급되지 않았습니다."), // data not found UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 대학교 지원 정보입니다."), diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java index d9d0b582c..d13462298 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java @@ -6,4 +6,8 @@ public enum AuthType { APPLE, EMAIL, ; + + public static boolean isEmail(AuthType authType) { + return EMAIL.equals(authType); + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 83a439b19..b1cf6c1cc 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -82,6 +82,9 @@ public class SiteUser { @Setter private LocalDate quitedAt; + @Column(nullable = true) + private String password; + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List postList = new ArrayList<>(); @@ -133,4 +136,26 @@ public SiteUser( this.gender = gender; this.authType = authType; } + + // todo: 가입 방법에 따라서 정해진 인자만 받고, 그렇지 않을 경우 예외 발생하도록 수정 필요 + public SiteUser( + String email, + String nickname, + String profileImageUrl, + String birth, + PreparationStatus preparationStage, + Role role, + Gender gender, + AuthType authType, + String password) { + this.email = email; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.birth = birth; + this.preparationStage = preparationStage; + this.role = role; + this.gender = gender; + this.authType = authType; + this.password = password; + } } diff --git a/src/main/resources/db/migration/V5__add_password_column.sql b/src/main/resources/db/migration/V5__add_password_column.sql new file mode 100644 index 000000000..948e2a97d --- /dev/null +++ b/src/main/resources/db/migration/V5__add_password_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE site_user +ADD COLUMN password VARCHAR(255) NULL; diff --git a/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java new file mode 100644 index 000000000..e9663f5df --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java @@ -0,0 +1,99 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.EmailSignInRequest; +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("이메일 로그인 서비스 테스트") +@TestContainerSpringBootTest +class EmailSignInServiceTest { + + @Autowired + private EmailSignInService emailSignInService; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Test + void 로그인에_성공한다() { + // given + String email = "testEmail"; + String rawPassword = "testPassword"; + SiteUser siteUser = createSiteUser(email, rawPassword); + siteUserRepository.save(siteUser); + EmailSignInRequest signInRequest = new EmailSignInRequest(siteUser.getEmail(), rawPassword); + + // when + SignInResponse signInResponse = emailSignInService.signIn(signInRequest); + + // then + assertAll( + () -> Assertions.assertThat(signInResponse.accessToken()).isNotNull(), + () -> Assertions.assertThat(signInResponse.refreshToken()).isNotNull() + ); + } + + @Nested + class 로그인에_실패한다 { + + @Test + void 이메일과_일치하는_사용자가_없으면_예외_응답을_반환한다() { + // given + EmailSignInRequest signInRequest = new EmailSignInRequest("이메일", "비밀번호"); + + // when & then + assertThatCode(() -> emailSignInService.signIn(signInRequest)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + void 비밀번호가_일치하지_않으면_예외_응답을_반환한다() { + // given + String email = "testEmail"; + SiteUser siteUser = createSiteUser(email, "testPassword"); + siteUserRepository.save(siteUser); + EmailSignInRequest signInRequest = new EmailSignInRequest(email, "틀린비밀번호"); + + // when & then + assertThatCode(() -> emailSignInService.signIn(signInRequest)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } + } + + private SiteUser createSiteUser(String email, String rawPassword) { + String encodedPassword = passwordEncoder.encode(rawPassword); + return new SiteUser( + email, + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE, + AuthType.EMAIL, + encodedPassword + ); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java similarity index 79% rename from src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java rename to src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java index d3a1efac1..12ab6f666 100644 --- a/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java @@ -20,19 +20,19 @@ import java.util.HashMap; import java.util.Map; -import static com.example.solidconnection.auth.service.oauth.SignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static com.example.solidconnection.auth.service.oauth.OAuthSignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; @TestContainerSpringBootTest -@DisplayName("회원가입 토큰 제공자 테스트") -class SignUpTokenProviderTest { +@DisplayName("OAuth 회원가입 토큰 제공자 테스트") +class OAuthSignUpTokenProviderTest { @Autowired - private SignUpTokenProvider signUpTokenProvider; + private OAuthSignUpTokenProvider OAuthSignUpTokenProvider; @Autowired private RedisTemplate redisTemplate; @@ -47,7 +47,7 @@ class SignUpTokenProviderTest { AuthType authType = AuthType.KAKAO; // when - String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType); + String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, authType); // then Claims claims = JwtUtils.parseClaims(signUpToken, jwtProperties.secret()); @@ -73,7 +73,7 @@ class 주어진_회원가입_토큰을_검증한다 { redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); } @Test @@ -82,9 +82,9 @@ class 주어진_회원가입_토큰을_검증한다 { String expiredToken = createExpiredToken(); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(expiredToken)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(expiredToken)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @Test @@ -93,9 +93,9 @@ class 주어진_회원가입_토큰을_검증한다 { String notJwt = "not jwt"; // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(notJwt)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(notJwt)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @Test @@ -105,9 +105,9 @@ class 주어진_회원가입_토큰을_검증한다 { String wrongAuthType = createBaseJwtBuilder().addClaims(wrongClaim).compact(); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(wrongAuthType)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(wrongAuthType)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @Test @@ -117,9 +117,9 @@ class 주어진_회원가입_토큰을_검증한다 { String noSubject = createBaseJwtBuilder().addClaims(claim).compact(); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(noSubject)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(noSubject)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @Test @@ -129,9 +129,9 @@ class 주어진_회원가입_토큰을_검증한다 { String signUpToken = createBaseJwtBuilder().addClaims(validClaim).setSubject("email").compact(); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(signUpToken)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(signUpToken)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER.getMessage()); } } @@ -144,7 +144,7 @@ class 주어진_회원가입_토큰을_검증한다 { redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); // when - String extractedEmail = signUpTokenProvider.parseEmail(validToken); + String extractedEmail = OAuthSignUpTokenProvider.parseEmail(validToken); // then assertThat(extractedEmail).isEqualTo(email); @@ -158,7 +158,7 @@ class 주어진_회원가입_토큰을_검증한다 { String validToken = createBaseJwtBuilder().setSubject("email").addClaims(claim).compact(); // when - AuthType extractedAuthType = signUpTokenProvider.parseAuthType(validToken); + AuthType extractedAuthType = OAuthSignUpTokenProvider.parseAuthType(validToken); // then assertThat(extractedAuthType).isEqualTo(authType); diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 7d01f365c..6599c20cc 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -3,7 +3,7 @@ import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.service.AuthTokenProvider; -import com.example.solidconnection.auth.service.oauth.SignUpTokenProvider; +import com.example.solidconnection.auth.service.oauth.OAuthSignUpTokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -30,7 +30,7 @@ import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByNickName; @@ -59,7 +59,7 @@ class SignUpTest extends BaseEndToEndTest { AuthTokenProvider authTokenProvider; @Autowired - SignUpTokenProvider signUpTokenProvider; + OAuthSignUpTokenProvider OAuthSignUpTokenProvider; @Autowired RedisTemplate redisTemplate; @@ -74,7 +74,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); + String generatedKakaoToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); @@ -126,7 +126,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); + String generatedKakaoToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -151,7 +151,7 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail, AuthType.KAKAO); + String generatedKakaoToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail, AuthType.KAKAO); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -181,6 +181,6 @@ class SignUpTest extends BaseEndToEndTest { .extract().as(ErrorResponse.class); assertThat(errorResponse.message()) - .contains(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .contains(SIGN_UP_TOKEN_INVALID.getMessage()); } } From 306d8b6094a5e0cf1bbb0ebb21fe9b7449e960ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 9 Feb 2025 09:59:18 +0900 Subject: [PATCH 143/158] =?UTF-8?q?refactor:=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EA=B3=BC=20=EA=B4=80=EB=A0=A8=EB=90=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=80=20Dto=20=EB=82=B4=EB=B6=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=98=EA=B2=8C=20=ED=95=9C=EB=8B=A4.=20(#187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 서비스의 대학 선택 검증을 DTO에서 수행하도록 변경 * refactor: 대학 지망 검증 에러 메시지를 상수로 분리 * test: 기존 서비스 통합테스트에서 대학 선택 유효성 검사 테스트로 변경 * feat: 기본 응답메시지 수정 * feat: 대학 선택 검증 메시지를 ErrorCode enum으로 통합 * style: 불필요한 개행 제거 * feat: 대학 선택 필수값 검증 로직 통합 * refactor: 검증 로직 리팩토링 및 가독성 개선 --- .../application/dto/ApplyRequest.java | 3 + .../dto/UniversityChoiceRequest.java | 5 +- .../service/ApplicationSubmissionService.java | 22 +---- .../custom/exception/ErrorCode.java | 3 + .../annotation/ValidUniversityChoice.java | 20 ++++ .../ValidUniversityChoiceValidator.java | 62 ++++++++++++ .../ApplicationSubmissionServiceTest.java | 20 ---- .../ValidUniversityChoiceValidatorTest.java | 99 +++++++++++++++++++ 8 files changed, 191 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/custom/validation/annotation/ValidUniversityChoice.java create mode 100644 src/main/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidator.java create mode 100644 src/test/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidatorTest.java diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java index 49c4b01ce..7c4da1c99 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java @@ -1,14 +1,17 @@ package com.example.solidconnection.application.dto; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; public record ApplyRequest( + @NotNull(message = "gpa score id를 입력해주세요.") Long gpaScoreId, @NotNull(message = "language test score id를 입력해주세요.") Long languageTestScoreId, + @Valid UniversityChoiceRequest universityChoiceRequest ) { } diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java index 2d05cfe5b..d219dbc2e 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java @@ -1,11 +1,10 @@ package com.example.solidconnection.application.dto; -import jakarta.validation.constraints.NotNull; +import com.example.solidconnection.custom.validation.annotation.ValidUniversityChoice; +@ValidUniversityChoice public record UniversityChoiceRequest( - @NotNull(message = "1지망 대학교를 입력해주세요.") Long firstChoiceUniversityId, - Long secondChoiceUniversityId, Long thirdChoiceUniversityId) { } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index c7652dce0..dec092f5e 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -18,7 +18,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -48,7 +50,6 @@ public class ApplicationSubmissionService { @Transactional public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) { UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest(); - validateUniversityChoices(universityChoiceRequest); Long gpaScoreId = applyRequest.gpaScoreId(); Long languageTestScoreId = applyRequest.languageTestScoreId(); @@ -116,23 +117,4 @@ private void validateUpdateLimitNotExceed(Application application) { throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); } } - - // 입력값 유효성 검증 - private void validateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) { - Set uniqueUniversityIds = new HashSet<>(); - uniqueUniversityIds.add(universityChoiceRequest.firstChoiceUniversityId()); - if (universityChoiceRequest.secondChoiceUniversityId() != null) { - addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.secondChoiceUniversityId()); - } - if (universityChoiceRequest.thirdChoiceUniversityId() != null) { - addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.thirdChoiceUniversityId()); - } - } - - private void addUniversityChoice(Set uniqueUniversityIds, Long universityId) { - boolean notAdded = !uniqueUniversityIds.add(universityId); - if (notAdded) { - throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); - } - } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index cd0bb6695..529430749 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -66,6 +66,9 @@ public enum ErrorCode { CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2, 3지망에 동일한 대학교를 입력할 수 없습니다."), CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 " + MIN_DAYS_BETWEEN_NICKNAME_CHANGES + "일이 지나지 않았습니다."), PROFILE_IMAGE_NEEDED(HttpStatus.BAD_REQUEST.value(), "프로필 이미지가 필요합니다."), + FIRST_CHOICE_REQUIRED(HttpStatus.BAD_REQUEST.value(), "1지망 대학교를 입력해주세요."), + THIRD_CHOICE_REQUIRES_SECOND(HttpStatus.BAD_REQUEST.value(), "2지망 없이 3지망을 선택할 수 없습니다."), + DUPLICATE_UNIVERSITY_CHOICE(HttpStatus.BAD_REQUEST.value(), "지망 선택이 중복되었습니다."), // community INVALID_POST_CATEGORY(HttpStatus.BAD_REQUEST.value(), "잘못된 카테고리명입니다."), diff --git a/src/main/java/com/example/solidconnection/custom/validation/annotation/ValidUniversityChoice.java b/src/main/java/com/example/solidconnection/custom/validation/annotation/ValidUniversityChoice.java new file mode 100644 index 000000000..7e5827113 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/validation/annotation/ValidUniversityChoice.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.custom.validation.annotation; + +import com.example.solidconnection.custom.validation.validator.ValidUniversityChoiceValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ValidUniversityChoiceValidator.class) +public @interface ValidUniversityChoice { + + String message() default "유효하지 않은 지망 대학 선택입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidator.java b/src/main/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidator.java new file mode 100644 index 000000000..6ac9fe1c2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidator.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.custom.validation.validator; + +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.custom.validation.annotation.ValidUniversityChoice; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_UNIVERSITY_CHOICE; +import static com.example.solidconnection.custom.exception.ErrorCode.FIRST_CHOICE_REQUIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.THIRD_CHOICE_REQUIRES_SECOND; + +public class ValidUniversityChoiceValidator implements ConstraintValidator { + + @Override + public boolean isValid(UniversityChoiceRequest request, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + + if (isFirstChoiceNotSelected(request)) { + context.buildConstraintViolationWithTemplate(FIRST_CHOICE_REQUIRED.getMessage()) + .addConstraintViolation(); + return false; + } + + if (isThirdChoiceWithoutSecond(request)) { + context.buildConstraintViolationWithTemplate(THIRD_CHOICE_REQUIRES_SECOND.getMessage()) + .addConstraintViolation(); + return false; + } + + if (isDuplicate(request)) { + context.buildConstraintViolationWithTemplate(DUPLICATE_UNIVERSITY_CHOICE.getMessage()) + .addConstraintViolation(); + return false; + } + + return true; + } + + private boolean isFirstChoiceNotSelected(UniversityChoiceRequest request) { + return request.firstChoiceUniversityId() == null; + } + + private boolean isThirdChoiceWithoutSecond(UniversityChoiceRequest request) { + return request.thirdChoiceUniversityId() != null && request.secondChoiceUniversityId() == null; + } + + private boolean isDuplicate(UniversityChoiceRequest request) { + Set uniqueIds = new HashSet<>(); + return Stream.of( + request.firstChoiceUniversityId(), + request.secondChoiceUniversityId(), + request.thirdChoiceUniversityId() + ) + .filter(Objects::nonNull) + .anyMatch(id -> !uniqueIds.add(id)); + } +} diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java index 1d40d094b..ffd3818ce 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -116,26 +116,6 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { .hasMessage(INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); } - @Test - void 동일한_대학을_중복_선택하면_예외_응답을_반환한다() { - // given - GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); - LanguageTestScore languageTestScore = createUnapprovedLanguageTestScore(테스트유저_1); - UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( - 괌대학_A_지원_정보.getId(), - 괌대학_A_지원_정보.getId(), - 메모리얼대학_세인트존스_A_지원_정보.getId() - ); - ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); - - // when & then - assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1, request) - ) - .isInstanceOf(CustomException.class) - .hasMessage(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); - } - @Test void 지원서_수정_횟수를_초과하면_예외_응답을_반환한다() { // given diff --git a/src/test/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidatorTest.java b/src/test/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidatorTest.java new file mode 100644 index 000000000..b0267a08b --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidatorTest.java @@ -0,0 +1,99 @@ +package com.example.solidconnection.custom.validation.validator; + +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_UNIVERSITY_CHOICE; +import static com.example.solidconnection.custom.exception.ErrorCode.FIRST_CHOICE_REQUIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.THIRD_CHOICE_REQUIRES_SECOND; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("대학 선택 유효성 검사 테스트") +class ValidUniversityChoiceValidatorTest { + + private static final String MESSAGE = "message"; + + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void 정상적인_지망_선택은_유효하다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(1L, 2L, 3L); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations).isEmpty(); + } + + @Test + void 첫_번째_지망만_선택하는_것은_유효하다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(1L, null, null); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations).isEmpty(); + } + + @Test + void 두_번째_지망_없이_세_번째_지망을_선택하면_예외_응답을_반환한다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(1L, null, 3L); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .extracting(MESSAGE) + .contains(THIRD_CHOICE_REQUIRES_SECOND.getMessage()); + } + + @Test + void 첫_번째_지망을_선택하지_않으면_예외_응답을_반환한다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(null, 2L, 3L); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .isNotEmpty() + .extracting(MESSAGE) + .contains(FIRST_CHOICE_REQUIRED.getMessage()); + } + + @Test + void 대학을_중복_선택하면_예외_응답을_반환한다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(1L, 1L, 2L); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .isNotEmpty() + .extracting(MESSAGE) + .contains(DUPLICATE_UNIVERSITY_CHOICE.getMessage()); + } +} From 75927c81a08855f53a5eb20c0c8e2054eec585a6 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 11 Feb 2025 12:51:39 +0900 Subject: [PATCH 144/158] =?UTF-8?q?fix:=20=EC=8B=A4=ED=8C=A8=ED=95=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=95=B4=EA=B2=B0,=20=EC=84=9C?= =?UTF-8?q?=EB=B8=8C=EB=AA=A8=EB=93=88=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 깨지는 테스트 해결 - 같은 email, 닉네임으로 회원가입하는 유저에 대한 테스트케이스가 '같은 닉네임이면 예외를 반환한다' 였는데, 검증 순서 상 email 중복 검증이 더 중요한 검증이므로 앞에 있어서 '같은 이메일로 로그인한 회원이 있다'는 메세지가 발생했다. 테스트 데이터에스 email 을 각각 다르게 주어 이를 해결한다. * chore: submodule 업데이트 --- src/main/resources/secret | 2 +- src/test/java/com/example/solidconnection/e2e/SignUpTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/secret b/src/main/resources/secret index 80a569b4c..44128519c 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 80a569b4c023225c77874e140521c703010414eb +Subproject commit 44128519c61cf80b02e113b1cd4e6387c8f54add diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 6599c20cc..f6b356178 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -125,7 +125,7 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String email = "email@email.com"; + String email = "test@email.com"; String generatedKakaoToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 From 14bd0b9bcdb17f6ac67d92c126a666b0a5ba617e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:50:59 +0900 Subject: [PATCH 145/158] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20r?= =?UTF-8?q?ole=20=EC=B6=94=EA=B0=80=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Role에 ADMIN 추가 및 Spring Security 권한 처리 추가 * feat: 사용자 권한 처리 및 ADMIN 경로 보호 추가 * refactor: Security 필터 체인 실행 순서 조정 * feat: 인증 안된 사용자 및 권한 없는 사용자 예외 처리 추가 * test: ExceptionHandlerFilter 관련 테스트 코드 추가 * chore: ADMIN Role 추가 마이그레이션 파일 추가 * fix: JWT 인증 시 권한 정보 누락 문제 해결 - JwtAuthentication 생성자에서 권한 정보를 상위 클래스에 전달하도록 수정 * fix: 만료된 토큰에 대한 인증 처리 시 예외 발생 문제 해결 - JwtAuthentication 생성자에서 권한 정보 처리 로직 수정 * refactor: customException를 활용하여 코드 간결화 * refactor: 불필요한 중복 코드 제거 * refactor: 삼항 연산자로 예외 처리 코드 간결화 * style: 형 변환 시 공백 추가 * refactor: Role enum을 순수 도메인 객체로 변경 * test: 테스트 코드 추가 * refactor: 매퍼 security.userdetails 패키지로 이동 * test: SiteUserDetailsTest 추가 및 권한 테스트 이동 --- .../security/SecurityConfiguration.java | 10 +++- .../custom/exception/ErrorCode.java | 1 + .../authentication/JwtAuthentication.java | 7 ++- .../filter/ExceptionHandlerFilter.java | 26 ++++++---- .../userdetails/SecurityRoleMapper.java | 18 +++++++ .../security/userdetails/SiteUserDetails.java | 2 +- .../example/solidconnection/type/Role.java | 4 +- .../migration/V6__add_admin_to_role_enum.sql | 2 + .../filter/ExceptionHandlerFilterTest.java | 39 ++++++++++++++ .../SiteUserDetailsServiceTest.java | 2 +- .../userdetails/SiteUserDetailsTest.java | 51 +++++++++++++++++++ 11 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/custom/security/userdetails/SecurityRoleMapper.java create mode 100644 src/main/resources/db/migration/V6__add_admin_to_role_enum.sql create mode 100644 src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsTest.java diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 98492568b..6afc199de 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -13,11 +13,14 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import static com.example.solidconnection.type.Role.ADMIN; + @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -55,10 +58,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .formLogin(AbstractHttpConfigurer::disable) .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/**").hasRole(ADMIN.name()) + .anyRequest().permitAll() + ) .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) .addFilterBefore(signOutCheckFilter, JwtAuthenticationFilter.class) - .addFilterBefore(exceptionHandlerFilter, SignOutCheckFilter.class) + .addFilterAfter(exceptionHandlerFilter, ExceptionTranslationFilter.class) .build(); } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 529430749..8c74ea55b 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -48,6 +48,7 @@ public enum ErrorCode { AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), + ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."), // s3 S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java index ba195caff..6c9f2fa21 100644 --- a/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java @@ -1,6 +1,9 @@ package com.example.solidconnection.custom.security.authentication; import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collections; public abstract class JwtAuthentication extends AbstractAuthenticationToken { @@ -9,7 +12,9 @@ public abstract class JwtAuthentication extends AbstractAuthenticationToken { private final Object principal; public JwtAuthentication(String token, Object principal) { - super(null); + super(principal instanceof UserDetails ? + ((UserDetails) principal).getAuthorities() : + Collections.emptyList()); this.credentials = token; this.principal = principal; } diff --git a/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java index 8d09bfada..1b8fac2bb 100644 --- a/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java @@ -1,6 +1,7 @@ package com.example.solidconnection.custom.security.filter; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; @@ -9,12 +10,16 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED; import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; @Component @@ -31,25 +36,28 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, filterChain.doFilter(request, response); } catch (CustomException e) { customCommence(response, e); + } catch (AccessDeniedException e) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + ErrorCode errorCode = auth instanceof AnonymousAuthenticationToken ? AUTHENTICATION_FAILED : ACCESS_DENIED; + generalCommence(response, e, errorCode); } catch (Exception e) { - generalCommence(response, e); + generalCommence(response, e, AUTHENTICATION_FAILED); } } public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { - SecurityContextHolder.clearContext(); ErrorResponse errorResponse = new ErrorResponse(customException); - writeResponse(response, errorResponse); + writeResponse(response, errorResponse, customException.getCode()); } - public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { - SecurityContextHolder.clearContext(); - ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); - writeResponse(response, errorResponse); + public void generalCommence(HttpServletResponse response, Exception exception, ErrorCode errorCode) throws IOException { + ErrorResponse errorResponse = new ErrorResponse(errorCode, exception.getMessage()); + writeResponse(response, errorResponse, errorCode.getCode()); } - private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse, int statusCode) throws IOException { + SecurityContextHolder.clearContext(); + response.setStatus(statusCode); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); diff --git a/src/main/java/com/example/solidconnection/custom/security/userdetails/SecurityRoleMapper.java b/src/main/java/com/example/solidconnection/custom/security/userdetails/SecurityRoleMapper.java new file mode 100644 index 000000000..3af238f13 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/userdetails/SecurityRoleMapper.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.custom.security.userdetails; + +import com.example.solidconnection.type.Role; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.List; + +public class SecurityRoleMapper { + + private static final String ROLE_PREFIX = "ROLE_"; + + private SecurityRoleMapper() { + } + + public static List mapRoleToAuthorities(Role role) { + return List.of(new SimpleGrantedAuthority(ROLE_PREFIX + role.name())); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java index 36a0b815a..008f77ef5 100644 --- a/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java +++ b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java @@ -27,7 +27,7 @@ public String getUsername() { @Override public Collection getAuthorities() { - return null; + return SecurityRoleMapper.mapRoleToAuthorities(siteUser.getRole()); } @Override diff --git a/src/main/java/com/example/solidconnection/type/Role.java b/src/main/java/com/example/solidconnection/type/Role.java index aaf464bf8..8223e8de0 100644 --- a/src/main/java/com/example/solidconnection/type/Role.java +++ b/src/main/java/com/example/solidconnection/type/Role.java @@ -1,6 +1,8 @@ package com.example.solidconnection.type; public enum Role { + + ADMIN, MENTOR, - MENTEE + MENTEE; } diff --git a/src/main/resources/db/migration/V6__add_admin_to_role_enum.sql b/src/main/resources/db/migration/V6__add_admin_to_role_enum.sql new file mode 100644 index 000000000..a661a2a61 --- /dev/null +++ b/src/main/resources/db/migration/V6__add_admin_to_role_enum.sql @@ -0,0 +1,2 @@ +ALTER TABLE site_user + modify ROLE enum ('MENTEE', 'MENTOR', 'ADMIN') NOT NULL; diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java index 091a75eb8..fd4bd62a8 100644 --- a/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java @@ -13,8 +13,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import java.util.stream.Stream; @@ -82,10 +85,46 @@ void setUp() { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); } + @Test + void 익명_사용자의_접근_거부시_401_예외_응답을_반환한다() throws Exception { + // given + Authentication anonymousAuth = getAnonymousAuth(); + SecurityContextHolder.getContext().setAuthentication(anonymousAuth); + willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + void 인증된_사용자의_접근_거부하면_403_예외_응답을_반환한다() throws Exception { + // given + Authentication auth = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + SecurityContextHolder.getContext().setAuthentication(auth); + willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + } + private static Stream provideException() { return Stream.of( new RuntimeException(), new CustomException(ErrorCode.INVALID_TOKEN) ); } + + private Authentication getAnonymousAuth() { + return new AnonymousAuthenticationToken( + "key", + "anonymousUser", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS") + ); + } } diff --git a/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java index 731f840f3..99e463955 100644 --- a/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java @@ -18,7 +18,7 @@ import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; @DisplayName("사용자 인증 정보 서비스 테스트") @TestContainerSpringBootTest diff --git a/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsTest.java b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsTest.java new file mode 100644 index 000000000..912072d2b --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsTest.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.custom.security.userdetails; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("사용자 인증 정보 테스트") +@TestContainerSpringBootTest +class SiteUserDetailsTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Test + void 사용자_권한을_정상적으로_반환한다() { + // given + SiteUser siteUser = siteUserRepository.save(createSiteUser()); + SiteUserDetails siteUserDetails = new SiteUserDetails(siteUser); + + // when + Collection authorities = siteUserDetails.getAuthorities(); + + // then + assertThat(authorities) + .extracting("authority") + .containsExactly("ROLE_" + siteUser.getRole().name()); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} From 89c2bceddb207d0406d1507f469bd35bd20c55fc Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Fri, 14 Feb 2025 11:18:17 +0900 Subject: [PATCH 146/158] =?UTF-8?q?refactor:=20=EB=82=B4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EC=99=80=20=EB=8C=80=ED=95=99=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?api=20=EC=88=98=EC=A0=95=20(#195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 대학 uri 복수형으로 수정 * refactor: 개발 환경 테스트 데이터 추가 * refactor: 사용자 정보 uri 수정 * chore: 의미 없는 throws 제거 * refactor: 위시 학교 추가, 삭제 api 분리 * chore: 사용하지 않은 의존성 제거 --- .../auth/service/EmailSignInService.java | 2 +- .../custom/exception/ErrorCode.java | 2 + .../controller/SiteUserController.java | 36 +---- .../siteuser/service/SiteUserService.java | 87 +++++------- .../controller/UniversityController.java | 16 ++- .../service/UniversityLikeService.java | 28 +++- src/main/resources/data.sql | 5 + .../solidconnection/e2e/MyPageTest.java | 2 +- .../solidconnection/e2e/MyPageUpdateTest.java | 133 ------------------ .../e2e/UniversityDetailTest.java | 2 +- .../e2e/UniversityLikeTest.java | 29 +--- .../e2e/UniversityRecommendTest.java | 10 +- .../e2e/UniversitySearchTest.java | 14 +- .../siteuser/service/SiteUserServiceTest.java | 61 +++----- .../service/UniversityLikeServiceTest.java | 91 ++++++++---- 15 files changed, 188 insertions(+), 330 deletions(-) delete mode 100644 src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java index bbbb4f85c..3e26309a5 100644 --- a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java @@ -35,7 +35,7 @@ public SignInResponse signIn(EmailSignInRequest signInRequest) { throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요."); } - private void validatePassword(String rawPassword, String encodedPassword) throws CustomException { + private void validatePassword(String rawPassword, String encodedPassword) { if (!passwordEncoder.matches(rawPassword, encodedPassword)) { throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요."); } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 8c74ea55b..dff7b13f0 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -84,6 +84,8 @@ public enum ErrorCode { CAN_NOT_UPDATE_DEPRECATED_COMMENT(HttpStatus.BAD_REQUEST.value(), "이미 삭제된 댓글을 수정할 수 없습니다."), INVALID_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글 좋아요입니다."), DUPLICATE_POST_LIKE(HttpStatus.BAD_REQUEST.value(), "이미 좋아요한 게시글입니다."), + ALREADY_LIKED_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "이미 좋아요한 대학입니다."), + NOT_LIKED_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "좋아요하지 않은 대학입니다."), // score INVALID_GPA_SCORE(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 학점입니다."), diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java index 11c154243..2f43337ed 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -3,24 +3,18 @@ import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; -import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; -import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; -import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; import com.example.solidconnection.siteuser.service.SiteUserService; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor -@RequestMapping("/my-page") +@RequestMapping("/my") @RestController class SiteUserController { @@ -34,29 +28,13 @@ public ResponseEntity getMyPageInfo( return ResponseEntity.ok(myPageResponse); } - @GetMapping("/update") - public ResponseEntity getMyPageInfoToUpdate( - @AuthorizedUser SiteUser siteUser - ) { - MyPageUpdateResponse myPageUpdateDto = siteUserService.getMyPageInfoToUpdate(siteUser); - return ResponseEntity.ok(myPageUpdateDto); - } - - @PatchMapping("/update/profileImage") - public ResponseEntity updateProfileImage( - @AuthorizedUser SiteUser siteUser, - @RequestParam(value = "file", required = false) MultipartFile imageFile - ) { - ProfileImageUpdateResponse profileImageUpdateResponse = siteUserService.updateProfileImage(siteUser, imageFile); - return ResponseEntity.ok().body(profileImageUpdateResponse); - } - - @PatchMapping("/update/nickname") - public ResponseEntity updateNickname( + @PatchMapping + public ResponseEntity updateMyPageInfo( @AuthorizedUser SiteUser siteUser, - @Valid @RequestBody NicknameUpdateRequest nicknameUpdateRequest + @RequestParam("file") MultipartFile imageFile, + @RequestParam("nickname") String nickname ) { - NicknameUpdateResponse nicknameUpdateResponse = siteUserService.updateNickname(siteUser, nicknameUpdateRequest); - return ResponseEntity.ok().body(nicknameUpdateResponse); + siteUserService.updateMyPageInfo(siteUser, imageFile, nickname); + return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index c181c2809..30297283a 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -5,10 +5,6 @@ import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; -import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; -import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; -import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.ImgType; @@ -48,68 +44,27 @@ public MyPageResponse getMyPageInfo(SiteUser siteUser) { } /* - * 내 정보를 수정하기 위한 마이페이지 정보를 조회한다. (닉네임, 프로필 사진) - * */ - @Transactional(readOnly = true) - public MyPageUpdateResponse getMyPageInfoToUpdate(SiteUser siteUser) { - return MyPageUpdateResponse.from(siteUser); - } - - /* - * 관심 대학교 목록을 조회한다. - * */ - @Transactional(readOnly = true) - public List getWishUniversity(SiteUser siteUser) { - List likedUniversities = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()); - return likedUniversities.stream() - .map(likedUniversity -> UniversityInfoForApplyPreviewResponse.from(likedUniversity.getUniversityInfoForApply())) - .toList(); - } - - /* - * 프로필 이미지를 수정한다. + * 마이페이지 정보를 수정한다. * */ @Transactional - public ProfileImageUpdateResponse updateProfileImage(SiteUser siteUser, MultipartFile imageFile) { - validateProfileImage(imageFile); + public void updateMyPageInfo(SiteUser siteUser, MultipartFile imageFile, String nickname) { + validateNicknameUnique(nickname); + validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); + validateProfileImageNotEmpty(imageFile); - // 프로필 이미지를 처음 수정하는 경우에는 deleteExProfile 수행하지 않음 if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { s3Service.deleteExProfile(siteUser); } - UploadedFileUrlResponse uploadedFileUrlResponse = s3Service.uploadFile(imageFile, ImgType.PROFILE); - siteUser.setProfileImageUrl(uploadedFileUrlResponse.fileUrl()); - siteUserRepository.save(siteUser); - - return ProfileImageUpdateResponse.from(siteUser); - } - - private void validateProfileImage(MultipartFile imageFile) { - if (imageFile == null || imageFile.isEmpty()) { - throw new CustomException(PROFILE_IMAGE_NEEDED); - } - } - private boolean isDefaultProfileImage(String profileImageUrl) { - String prefix = "profile/"; - return profileImageUrl == null || !profileImageUrl.startsWith(prefix); - } + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE); + String profileImageUrl = uploadedFile.fileUrl(); - /* - * 닉네임을 수정한다. - * */ - @Transactional - public NicknameUpdateResponse updateNickname(SiteUser siteUser, NicknameUpdateRequest nicknameUpdateRequest) { - validateNicknameDuplicated(nicknameUpdateRequest.nickname()); - validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); - - siteUser.setNickname(nicknameUpdateRequest.nickname()); + siteUser.setProfileImageUrl(profileImageUrl); + siteUser.setNickname(nickname); siteUser.setNicknameModifiedAt(LocalDateTime.now()); siteUserRepository.save(siteUser); - - return NicknameUpdateResponse.from(siteUser); } - private void validateNicknameDuplicated(String nickname) { + private void validateNicknameUnique(String nickname) { if (siteUserRepository.existsByNickname(nickname)) { throw new CustomException(NICKNAME_ALREADY_EXISTED); } @@ -125,4 +80,26 @@ private void validateNicknameNotChangedRecently(LocalDateTime lastModifiedAt) { throw new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); } } + + private void validateProfileImageNotEmpty(MultipartFile imageFile) { + if (imageFile == null || imageFile.isEmpty()) { + throw new CustomException(PROFILE_IMAGE_NEEDED); + } + } + + private boolean isDefaultProfileImage(String profileImageUrl) { + String prefix = "profile/"; + return profileImageUrl == null || !profileImageUrl.startsWith(prefix); + } + + /* + * 관심 대학교 목록을 조회한다. + * */ + @Transactional(readOnly = true) + public List getWishUniversity(SiteUser siteUser) { + List likedUniversities = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()); + return likedUniversities.stream() + .map(likedUniversity -> UniversityInfoForApplyPreviewResponse.from(likedUniversity.getUniversityInfoForApply())) + .toList(); + } } diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 505bfe072..66da87095 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -14,6 +14,7 @@ import com.example.solidconnection.university.service.UniversityRecommendService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -24,7 +25,7 @@ import java.util.List; @RequiredArgsConstructor -@RequestMapping("/university") +@RequestMapping("/universities") @RestController public class UniversityController { @@ -33,7 +34,7 @@ public class UniversityController { private final UniversityRecommendService universityRecommendService; private final SiteUserService siteUserService; - @GetMapping("/recommends") + @GetMapping("/recommend") public ResponseEntity getUniversityRecommends( @AuthorizedUser SiteUser siteUser ) { @@ -70,7 +71,16 @@ public ResponseEntity addWishUniversity( return ResponseEntity.ok(likeResultResponse); } - @GetMapping("/detail/{universityInfoForApplyId}") + @DeleteMapping("/{universityInfoForApplyId}/like") + public ResponseEntity cancelWishUniversity( + @AuthorizedUser SiteUser siteUser, + @PathVariable Long universityInfoForApplyId + ) { + LikeResultResponse likeResultResponse = universityLikeService.cancelLikeUniversity(siteUser, universityInfoForApplyId); + return ResponseEntity.ok(likeResultResponse); + } + + @GetMapping("/{universityInfoForApplyId}") public ResponseEntity getUniversityDetails( @PathVariable Long universityInfoForApplyId ) { diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java index d926bc516..85971663b 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java @@ -1,5 +1,6 @@ package com.example.solidconnection.university.service; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.university.domain.LikedUniversity; @@ -14,6 +15,9 @@ import java.util.Optional; +import static com.example.solidconnection.custom.exception.ErrorCode.ALREADY_LIKED_UNIVERSITY; +import static com.example.solidconnection.custom.exception.ErrorCode.NOT_LIKED_UNIVERSITY; + @RequiredArgsConstructor @Service public class UniversityLikeService { @@ -29,16 +33,14 @@ public class UniversityLikeService { /* * 대학교를 '좋아요' 한다. - * - 이미 좋아요가 눌러져있다면, 좋아요를 취소한다. * */ @Transactional public LikeResultResponse likeUniversity(SiteUser siteUser, Long universityInfoForApplyId) { UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); - Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); - if (alreadyLikedUniversity.isPresent()) { - likedUniversityRepository.delete(alreadyLikedUniversity.get()); - return new LikeResultResponse(LIKE_CANCELED_MESSAGE); + Optional optionalLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); + if (optionalLikedUniversity.isPresent()) { + throw new CustomException(ALREADY_LIKED_UNIVERSITY); } LikedUniversity likedUniversity = LikedUniversity.builder() @@ -49,6 +51,22 @@ public LikeResultResponse likeUniversity(SiteUser siteUser, Long universityInfoF return new LikeResultResponse(LIKE_SUCCESS_MESSAGE); } + /* + * 대학교 '좋아요'를 취소한다. + * */ + @Transactional + public LikeResultResponse cancelLikeUniversity(SiteUser siteUser, long universityInfoForApplyId) throws CustomException { + UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); + + Optional optionalLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); + if (optionalLikedUniversity.isEmpty()) { + throw new CustomException(NOT_LIKED_UNIVERSITY); + } + + likedUniversityRepository.delete(optionalLikedUniversity.get()); + return new LikeResultResponse(LIKE_CANCELED_MESSAGE); + } + /* * '좋아요'한 대학교인지 확인한다. * */ diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 477fc03f5..5ae08770d 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -41,6 +41,11 @@ VALUES ('BN', '브루나이', 'ASIA'), ('MY', '말레이시아', 'ASIA'), ('RU', '러시아', 'EUROPE'); +INSERT INTO site_user (birth, email, nickname, profile_image_url, gender, preparation_stage, role, password, auth_type) +VALUES ('1999-01-01', 'test@test.email', 'yonso','https://github.com/nayonsoso.png', + 'FEMALE', 'CONSIDERING', 'MENTEE', + '$2a$10$psmwlxPfqWnIlq9JrlQJkuXr1XtjRNsyVOgcTWYZub5jFfn0TML76', 'EMAIL'); -- 12341234 + INSERT INTO university(id, country_code, region_code, english_name, format_name, korean_name, accommodation_url, english_course_url, homepage_url, details_for_local, logo_image_url, background_image_url) diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index 7a0ae07f4..dd2ce1e3a 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -44,7 +44,7 @@ public void setUpUserAndToken() { MyPageResponse myPageResponse = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .get("/my-page") + .get("/my") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(MyPageResponse.class); diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java deleted file mode 100644 index b16f3b822..000000000 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.example.solidconnection.e2e; - -import com.example.solidconnection.auth.service.AuthTokenProvider; -import com.example.solidconnection.custom.response.ErrorResponse; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; -import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; -import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import io.restassured.RestAssured; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; - -import java.time.LocalDateTime; - -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; -import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; -import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; -import static com.example.solidconnection.siteuser.service.SiteUserService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -@DisplayName("마이페이지 수정 테스트") -class MyPageUpdateTest extends BaseEndToEndTest { - - @Autowired - private SiteUserRepository siteUserRepository; - - @Autowired - private AuthTokenProvider authTokenProvider; - - private String accessToken; - - private SiteUser siteUser; - - @BeforeEach - public void setUpUserAndToken() { - // setUp - 회원 정보 저장 - siteUser = createSiteUserByEmail("email"); - siteUserRepository.save(siteUser); - - // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = authTokenProvider.generateAccessToken(siteUser); - authTokenProvider.generateAndSaveRefreshToken(siteUser); - } - - @Test - void 수정을_위해_수정_전_정보를_조회한다() { - // request - 요청 - MyPageUpdateResponse myPageUpdateResponse = RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .log().all() - .get("/my-page/update") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().as(MyPageUpdateResponse.class); - - assertAll("불러온 마이 페이지 정보가 DB의 정보와 일치한다.", - () -> assertThat(myPageUpdateResponse.nickname()).isEqualTo(siteUser.getNickname()), - () -> assertThat(myPageUpdateResponse.profileImageUrl()).isEqualTo(siteUser.getProfileImageUrl())); - } - - @Test - void 닉네임을_수정한다() { - // request - body 생성 및 요청 - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); - NicknameUpdateResponse nicknameUpdateResponse = RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .log().all() - .body(nicknameUpdateRequest) - .contentType("application/json") - .patch("/my-page/update/nickname") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().as(NicknameUpdateResponse.class); - - SiteUser updatedSiteUser = siteUserRepository.findById(siteUser.getId()).get(); - assertAll("마이 페이지 정보가 수정된다.", - () -> assertThat(nicknameUpdateResponse.nickname()).isEqualTo(updatedSiteUser.getNickname())); - } - - @Test - void 닉네임을_수정할_때_닉네임이_중복된다면_예외_응답을_반환한다() { - // setUp - 같은 닉네임을 갖는 다른 회원 정보 저장 - SiteUser existUser = createSiteUserByEmail("existUser"); - String duplicateNickname = "duplicateNickname"; - existUser.setNickname(duplicateNickname); - siteUserRepository.save(existUser); - - // request - body 생성 및 요청 - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("duplicateNickname"); - ErrorResponse response = RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .log().all() - .body(nicknameUpdateRequest) - .contentType("application/json") - .patch("/my-page/update/nickname") - .then().log().all() - .statusCode(HttpStatus.CONFLICT.value()) - .extract().as(ErrorResponse.class); - - assertThat(response.message()) - .isEqualTo(NICKNAME_ALREADY_EXISTED.getMessage()); - } - - @Test - void 닉네임을_수정할_때_닉네임_변경_가능_기한이_지나지않았다면_예외_응답을_반환한다() { - // setUp - 회원 정보 저장 (닉네임 변경 가능 시간이 되기 1분 전) - LocalDateTime nicknameModifiedAt = LocalDateTime.now() - .minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES) - .plusMinutes(1); - siteUser.setNicknameModifiedAt(nicknameModifiedAt); - siteUserRepository.save(siteUser); - - // request - body 생성 및 요청 - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); - ErrorResponse response = RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .log().all() - .body(nicknameUpdateRequest) - .contentType("application/json") - .patch("/my-page/update/nickname") - .then().log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract().as(ErrorResponse.class); - - assertThat(response.message()) - .contains(CAN_NOT_CHANGE_NICKNAME_YET.getMessage()); - } -} diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index 01b2b5730..c7a364ebf 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -45,7 +45,7 @@ public void setUpUserAndToken() { UniversityDetailResponse response = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .get("/university/detail/" + 메이지대학_지원_정보.getId()) + .get("/universities/" + 메이지대학_지원_정보.getId()) .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(UniversityDetailResponse.class); diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index 3b5733d82..693d6d91b 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -24,7 +24,6 @@ import static com.example.solidconnection.e2e.DynamicFixture.createLikedUniversity; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createUniversityForApply; -import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_CANCELED_MESSAGE; import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_SUCCESS_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -73,7 +72,7 @@ public void setUpUserAndToken() { List wishUniversities = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .get("/university/like") + .get("/universities/like") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); @@ -92,7 +91,7 @@ public void setUpUserAndToken() { LikeResultResponse response = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .post("/university/" + 괌대학_A_지원_정보.getId() + "/like") + .post("/universities/" + 괌대학_A_지원_정보.getId() + "/like") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(LikeResultResponse.class); @@ -106,28 +105,6 @@ public void setUpUserAndToken() { ); } - @Test - void 이미_좋아요한_대학교에_좋아요를_누른다() { - // setUp - 대학교 좋아요 저장 - likedUniversityRepository.save(createLikedUniversity(siteUser, 괌대학_A_지원_정보)); - - // request - 요청 - LikeResultResponse response = RestAssured.given() - .header("Authorization", "Bearer " + accessToken) - .log().all() - .post("/university/" + 괌대학_A_지원_정보.getId() + "/like") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .extract().as(LikeResultResponse.class); - - Optional likedUniversity - = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()).stream().findFirst(); - assertAll("좋아요 누른 대학교를 삭제하고, 좋아요 취소 응답을 반환한다.", - () -> assertThat(likedUniversity).isEmpty(), - () -> assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE) - ); - } - @Test void 대학의_좋아요_여부를_조회한다() { // setUp - 대학교 좋아요 저장 @@ -136,7 +113,7 @@ public void setUpUserAndToken() { // request - 요청 IsLikeResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .get("/university/" + 괌대학_A_지원_정보.getId() + "/like") + .get("/universities/" + 괌대학_A_지원_정보.getId() + "/like") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(IsLikeResponse.class); diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index 8e1e8184f..9939d1a54 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -66,7 +66,7 @@ void setUp() { UniversityRecommendsResponse response = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .get("/university/recommends") + .get("/universities/recommend") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(UniversityRecommendsResponse.class); @@ -94,7 +94,7 @@ void setUp() { UniversityRecommendsResponse response = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .get("/university/recommends") + .get("/universities/recommend") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(UniversityRecommendsResponse.class); @@ -121,7 +121,7 @@ void setUp() { UniversityRecommendsResponse response = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .get("/university/recommends") + .get("/universities/recommend") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(UniversityRecommendsResponse.class); @@ -148,7 +148,7 @@ void setUp() { UniversityRecommendsResponse response = RestAssured.given() .header("Authorization", "Bearer " + accessToken) .log().all() - .get("/university/recommends") + .get("/universities/recommend") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(UniversityRecommendsResponse.class); @@ -171,7 +171,7 @@ void setUp() { // request - 요청 UniversityRecommendsResponse response = RestAssured.given() .log().all() - .get("/university/recommends") + .get("/universities/recommend") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(UniversityRecommendsResponse.class); diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java index 3b508d014..5d4ff71fd 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -43,7 +43,7 @@ public void setUpUserAndToken() { // request - 요청 List response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .when().get("/university/search") + .when().get("/universities/search") .then().log().all() .statusCode(200) .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); @@ -67,7 +67,7 @@ public void setUpUserAndToken() { // request - 요청 List response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .when().get("/university/search?region=" + 영미권.getCode()) + .when().get("/universities/search?region=" + 영미권.getCode()) .then().log().all() .statusCode(200) .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); @@ -85,7 +85,7 @@ public void setUpUserAndToken() { // request - 요청 List response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .when().get("/university/search?keyword=라") + .when().get("/universities/search?keyword=라") .then().log().all() .statusCode(200) .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); @@ -102,7 +102,7 @@ public void setUpUserAndToken() { // request - 요청 List response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .when().get("/university/search?keyword=라&keyword=일본") + .when().get("/universities/search?keyword=라&keyword=일본") .then().log().all() .statusCode(200) .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); @@ -120,7 +120,7 @@ public void setUpUserAndToken() { // request - 요청 List response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .when().get("/university/search?testType=TOEFL_IBT") + .when().get("/universities/search?testType=TOEFL_IBT") .then().log().all() .statusCode(200) .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); @@ -139,7 +139,7 @@ public void setUpUserAndToken() { // request - 요청 List response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .when().get("/university/search?testType=TOEFL_IBT&testScore=70") + .when().get("/universities/search?testType=TOEFL_IBT&testScore=70") .then().log().all() .statusCode(200) .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); @@ -155,7 +155,7 @@ public void setUpUserAndToken() { // request - 요청 List response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .when().get("/university/search?region=EUROPE&testType=TOEFL_IBT&testScore=70") + .when().get("/universities/search?region=EUROPE&testType=TOEFL_IBT&testScore=70") .then().log().all() .statusCode(200) .extract().jsonPath().getList(".", UniversityInfoForApplyPreviewResponse.class); diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java index 9fc6410d8..cb256bc0f 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java @@ -5,10 +5,7 @@ import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; -import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; -import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; -import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.integration.BaseIntegrationTest; @@ -19,6 +16,7 @@ import com.example.solidconnection.university.domain.LikedUniversity; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -36,11 +34,11 @@ import static com.example.solidconnection.siteuser.service.SiteUserService.NICKNAME_LAST_CHANGE_DATE_FORMAT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; -import static org.mockito.BDDMockito.never; import static org.mockito.BDDMockito.any; import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.then; @DisplayName("유저 서비스 테스트") class SiteUserServiceTest extends BaseIntegrationTest { @@ -78,21 +76,6 @@ class SiteUserServiceTest extends BaseIntegrationTest { ); } - @Test - void 내_정보를_수정하기_위한_마이페이지_정보를_조회한다() { - // given - SiteUser testUser = createSiteUser(); - - // when - MyPageUpdateResponse response = siteUserService.getMyPageInfoToUpdate(testUser); - - // then - Assertions.assertAll( - () -> assertThat(response.nickname()).isEqualTo(testUser.getNickname()), - () -> assertThat(response.profileImageUrl()).isEqualTo(testUser.getProfileImageUrl()) - ); - } - @Test void 관심_대학교_목록을_조회한다() { // given @@ -126,13 +109,10 @@ class 프로필_이미지_수정_테스트 { .willReturn(new UploadedFileUrlResponse(expectedUrl)); // when - ProfileImageUpdateResponse response = siteUserService.updateProfileImage( - testUser, - imageFile - ); + siteUserService.updateMyPageInfo(testUser, imageFile, "newNickname"); // then - assertThat(response.profileImageUrl()).isEqualTo(expectedUrl); + assertThat(testUser.getProfileImageUrl()).isEqualTo(expectedUrl); } @Test @@ -144,7 +124,7 @@ class 프로필_이미지_수정_테스트 { .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when - siteUserService.updateProfileImage(testUser, imageFile); + siteUserService.updateMyPageInfo(testUser, imageFile, "newNickname"); // then then(s3Service).should(never()).deleteExProfile(any()); @@ -159,7 +139,7 @@ class 프로필_이미지_수정_테스트 { .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when - siteUserService.updateProfileImage(testUser, imageFile); + siteUserService.updateMyPageInfo(testUser, imageFile, "newNickname"); // then then(s3Service).should().deleteExProfile(testUser); @@ -172,7 +152,7 @@ class 프로필_이미지_수정_테스트 { MockMultipartFile emptyFile = createEmptyImageFile(); // when & then - assertThatCode(() -> siteUserService.updateProfileImage(testUser, emptyFile)) + assertThatCode(() -> siteUserService.updateMyPageInfo(testUser, emptyFile, "newNickname")) .isInstanceOf(CustomException.class) .hasMessage(PROFILE_IMAGE_NEEDED.getMessage()); } @@ -181,23 +161,26 @@ class 프로필_이미지_수정_테스트 { @Nested class 닉네임_수정_테스트 { + @BeforeEach + void setUp() { + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); + } + @Test void 닉네임을_성공적으로_수정한다() { // given SiteUser testUser = createSiteUser(); + MockMultipartFile imageFile = createValidImageFile(); String newNickname = "newNickname"; - NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); // when - NicknameUpdateResponse response = siteUserService.updateNickname( - testUser, - request - ); + siteUserService.updateMyPageInfo(testUser, imageFile, newNickname); // then SiteUser updatedUser = siteUserRepository.findById(testUser.getId()).get(); assertThat(updatedUser.getNicknameModifiedAt()).isNotNull(); - assertThat(response.nickname()).isEqualTo(newNickname); + assertThat(updatedUser.getNickname()).isEqualTo(newNickname); } @Test @@ -205,10 +188,10 @@ class 닉네임_수정_테스트 { // given createDuplicatedSiteUser(); SiteUser testUser = createSiteUser(); - NicknameUpdateRequest request = new NicknameUpdateRequest("duplicatedNickname"); + MockMultipartFile imageFile = createValidImageFile(); // when & then - assertThatCode(() -> siteUserService.updateNickname(testUser, request)) + assertThatCode(() -> siteUserService.updateMyPageInfo(testUser, imageFile, "duplicatedNickname")) .isInstanceOf(CustomException.class) .hasMessage(NICKNAME_ALREADY_EXISTED.getMessage()); } @@ -217,6 +200,7 @@ class 닉네임_수정_테스트 { void 최소_대기기간이_지나지_않은_상태에서_변경하면_예외_응답을_반환한다() { // given SiteUser testUser = createSiteUser(); + MockMultipartFile imageFile = createValidImageFile(); LocalDateTime modifiedAt = LocalDateTime.now().minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES - 1); testUser.setNicknameModifiedAt(modifiedAt); siteUserRepository.save(testUser); @@ -224,8 +208,7 @@ class 닉네임_수정_테스트 { NicknameUpdateRequest request = new NicknameUpdateRequest("newNickname"); // when & then - assertThatCode(() -> - siteUserService.updateNickname(testUser, request)) + assertThatCode(() -> siteUserService.updateMyPageInfo(testUser, imageFile, "nickname12")) .isInstanceOf(CustomException.class) .hasMessage(createExpectedErrorMessage(modifiedAt)); } diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java index 51958ed5d..ec9b704a6 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java @@ -13,14 +13,18 @@ import com.example.solidconnection.university.dto.IsLikeResponse; import com.example.solidconnection.university.dto.LikeResultResponse; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import static com.example.solidconnection.custom.exception.ErrorCode.ALREADY_LIKED_UNIVERSITY; +import static com.example.solidconnection.custom.exception.ErrorCode.NOT_LIKED_UNIVERSITY; import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_CANCELED_MESSAGE; import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_SUCCESS_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; @DisplayName("대학교 좋아요 서비스 테스트") class UniversityLikeServiceTest extends BaseIntegrationTest { @@ -34,33 +38,70 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { @Autowired private SiteUserRepository siteUserRepository; - @Test - void 대학_좋아요를_등록한다() { - // given - SiteUser testUser = createSiteUser(); - - // when - LikeResultResponse response = universityLikeService.likeUniversity(testUser, 괌대학_A_지원_정보.getId()); - - // then - assertThat(response.result()).isEqualTo(LIKE_SUCCESS_MESSAGE); - assertThat(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply( - testUser, 괌대학_A_지원_정보)).isPresent(); + @Nested + class 대학_좋아요를_등록한다 { + + @Test + void 성공적으로_좋아요를_등록한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + LikeResultResponse response = universityLikeService.likeUniversity(testUser, 괌대학_A_지원_정보.getId()); + + // then + assertAll( + () -> assertThat(response.result()).isEqualTo(LIKE_SUCCESS_MESSAGE), + () -> assertThat(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply( + testUser, 괌대학_A_지원_정보 + )).isPresent() + ); + } + + @Test + void 이미_좋아요한_대학이면_예외_응답을_반환한다() { + // given + SiteUser testUser = createSiteUser(); + saveLikedUniversity(testUser, 괌대학_A_지원_정보); + + // when & then + assertThatCode(() -> universityLikeService.likeUniversity(testUser, 괌대학_A_지원_정보.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ALREADY_LIKED_UNIVERSITY.getMessage()); + } } - @Test - void 대학_좋아요를_취소한다() { - // given - SiteUser testUser = createSiteUser(); - saveLikedUniversity(testUser, 괌대학_A_지원_정보); - - // when - LikeResultResponse response = universityLikeService.likeUniversity(testUser, 괌대학_A_지원_정보.getId()); - - // then - assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE); - assertThat(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply( - testUser, 괌대학_A_지원_정보)).isEmpty(); + @Nested + class 대학_좋아요를_취소한다 { + + @Test + void 성공적으로_좋아요를_취소한다() { + // given + SiteUser testUser = createSiteUser(); + saveLikedUniversity(testUser, 괌대학_A_지원_정보); + + // when + LikeResultResponse response = universityLikeService.cancelLikeUniversity(testUser, 괌대학_A_지원_정보.getId()); + + // then + assertAll( + () -> assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE), + () -> assertThat(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply( + testUser, 괌대학_A_지원_정보 + )).isEmpty() + ); + } + + @Test + void 좋아요하지_않은_대학이면_예외_응답을_반환한다() { + // given + SiteUser testUser = createSiteUser(); + + // when & then + assertThatCode(() -> universityLikeService.cancelLikeUniversity(testUser, 괌대학_A_지원_정보.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(NOT_LIKED_UNIVERSITY.getMessage()); + } } @Test From ea76630448a49571fa7376a8ec13bad5fcd180d9 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Fri, 14 Feb 2025 13:58:30 +0900 Subject: [PATCH 147/158] =?UTF-8?q?fix:=20=EC=9D=B8=EC=A6=9D=EA=B3=BC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=EB=90=98=EC=96=B4=20=EB=A6=AC=EC=A1=B8?= =?UTF-8?q?=EB=B2=84=EC=99=80=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=EC=97=90=EC=84=9C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=9C=20=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0=20(#201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 사용자 인증이 선택적으로 필요한 곳을 명시하도록 * fix: 만료된 토큰 객체가 아니라 토큰 자체를 사용하도록 * test: 인증된 사용자 리졸버 테스트 보강 * chore: 작업하며 발견한 개선이 필요한 부분 표시 --- .../auth/controller/AuthController.java | 21 ++++-- .../custom/resolver/AuthorizedUser.java | 1 + .../resolver/AuthorizedUserResolver.java | 20 ++++-- .../custom/resolver/ExpiredToken.java | 1 + .../custom/resolver/ExpiredTokenResolver.java | 1 + .../ExpiredTokenAuthentication.java | 1 + .../ExpiredTokenAuthenticationProvider.java | 1 + .../controller/UniversityController.java | 2 +- .../resolver/AuthorizedUserResolverTest.java | 66 +++++++++++++++---- 9 files changed, 92 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 80520942f..9c84e8d22 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -16,14 +16,15 @@ import com.example.solidconnection.auth.service.oauth.AppleOAuthService; import com.example.solidconnection.auth.service.oauth.KakaoOAuthService; import com.example.solidconnection.auth.service.oauth.OAuthSignUpService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; import com.example.solidconnection.custom.resolver.AuthorizedUser; -import com.example.solidconnection.custom.resolver.ExpiredToken; -import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -93,9 +94,13 @@ public ResponseEntity signUp( @PostMapping("/sign-out") public ResponseEntity signOut( - @ExpiredToken ExpiredTokenAuthentication expiredToken + Authentication authentication ) { - authService.signOut(expiredToken.getToken()); + String token = authentication.getCredentials().toString(); + if (token == null) { + throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); + } + authService.signOut(token); return ResponseEntity.ok().build(); } @@ -109,9 +114,13 @@ public ResponseEntity quit( @PostMapping("/reissue") public ResponseEntity reissueToken( - @ExpiredToken ExpiredTokenAuthentication expiredToken + Authentication authentication ) { - ReissueResponse reissueResponse = authService.reissue(expiredToken.getSubject()); + String token = authentication.getCredentials().toString(); + if (token == null) { + throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다."); + } + ReissueResponse reissueResponse = authService.reissue(token); return ResponseEntity.ok(reissueResponse); } } diff --git a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java index b14d80994..fa1db7f74 100644 --- a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java +++ b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java @@ -8,4 +8,5 @@ @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface AuthorizedUser { + boolean required() default true; } diff --git a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java index 93707b007..f4ba9fe7f 100644 --- a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java +++ b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java @@ -1,9 +1,11 @@ package com.example.solidconnection.custom.resolver; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; 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; @@ -11,6 +13,8 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; + @Component @RequiredArgsConstructor public class AuthorizedUserResolver implements HandlerMethodArgumentResolver { @@ -25,11 +29,19 @@ public boolean supportsParameter(MethodParameter parameter) { public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, - WebDataBinderFactory binderFactory) throws Exception { + WebDataBinderFactory binderFactory) { + SiteUser siteUser = extractSiteUserFromAuthentication(); + if (parameter.getParameterAnnotation(AuthorizedUser.class).required() && siteUser == null) { + throw new CustomException(AUTHENTICATION_FAILED, "로그인 상태가 아닙니다."); + } + + return siteUser; + } + + private SiteUser extractSiteUserFromAuthentication() { try { - SiteUserDetails principal = (SiteUserDetails) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + SiteUserDetails principal = (SiteUserDetails) authentication.getPrincipal(); return principal.getSiteUser(); } catch (Exception e) { return null; diff --git a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java index 61abff98c..5de4ad95a 100644 --- a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java +++ b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java @@ -5,6 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함 @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface ExpiredToken { diff --git a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java index 691136438..7547a1d61 100644 --- a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java +++ b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java @@ -10,6 +10,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함 @Component @RequiredArgsConstructor public class ExpiredTokenResolver implements HandlerMethodArgumentResolver { diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java index 811ea6a1b..061484674 100644 --- a/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java @@ -1,5 +1,6 @@ package com.example.solidconnection.custom.security.authentication; +// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함 public class ExpiredTokenAuthentication extends JwtAuthentication { public ExpiredTokenAuthentication(String token) { diff --git a/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java b/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java index d7461a0e6..01b065a19 100644 --- a/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java +++ b/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java @@ -12,6 +12,7 @@ import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; +// todo: 사용되지 않음, 다른 PR에서 삭제하고 더 효율적인 구조를 고민해봐야 함 @Component @RequiredArgsConstructor public class ExpiredTokenAuthenticationProvider implements AuthenticationProvider { diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 66da87095..83d90f600 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -36,7 +36,7 @@ public class UniversityController { @GetMapping("/recommend") public ResponseEntity getUniversityRecommends( - @AuthorizedUser SiteUser siteUser + @AuthorizedUser(required = false) SiteUser siteUser ) { if (siteUser == null) { return ResponseEntity.ok(universityRecommendService.getGeneralRecommends()); diff --git a/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java b/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java index 763fdf101..779474c27 100644 --- a/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java +++ b/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java @@ -1,6 +1,7 @@ package com.example.solidconnection.custom.resolver; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -11,11 +12,18 @@ import com.example.solidconnection.type.Role; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; 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 static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; @TestContainerSpringBootTest @DisplayName("인증된 사용자 argument resolver 테스트") @@ -33,28 +41,58 @@ void setUp() { } @Test - void security_context_에_저장된_인증된_사용자를_반환한다() throws Exception { + void security_context_에_저장된_인증된_사용자를_반환한다() { // given - SiteUser siteUser = siteUserRepository.save(createSiteUser()); - SiteUserDetails userDetails = new SiteUserDetails(siteUser); - SiteUserAuthentication authentication = new SiteUserAuthentication("token", userDetails); + SiteUser siteUser = createAndSaveSiteUser(); + Authentication authentication = createAuthenticationWithUser(siteUser); SecurityContextHolder.getContext().setAuthentication(authentication); + MethodParameter parameter = mock(MethodParameter.class); + AuthorizedUser authorizedUser = mock(AuthorizedUser.class); + given(parameter.getParameterAnnotation(AuthorizedUser.class)).willReturn(authorizedUser); + given(authorizedUser.required()).willReturn(false); + // when - SiteUser resolveSiteUser = (SiteUser) authorizedUserResolver.resolveArgument(null, null, null, null); + SiteUser resolveSiteUser = (SiteUser) authorizedUserResolver.resolveArgument(parameter, null, null, null); // then assertThat(resolveSiteUser).isEqualTo(siteUser); } - @Test - void security_context_에_저장된_사용자가_없으면_null_을_반환한다() throws Exception { - // when, then - assertThat(authorizedUserResolver.resolveArgument(null, null, null, null)).isNull(); + @Nested + class security_context_에_저장된_사용자가_없는_경우 { + + @Test + void required_가_true_이면_예외_응답을_반환한다() { + // given + MethodParameter parameter = mock(MethodParameter.class); + AuthorizedUser authorizedUser = mock(AuthorizedUser.class); + given(parameter.getParameterAnnotation(AuthorizedUser.class)).willReturn(authorizedUser); + given(authorizedUser.required()).willReturn(true); + + // when, then + assertThatCode(() -> authorizedUserResolver.resolveArgument(parameter, null, null, null)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(AUTHENTICATION_FAILED.getMessage()); + } + + @Test + void required_가_false_이면_null_을_반환한다() { + // given + MethodParameter parameter = mock(MethodParameter.class); + AuthorizedUser authorizedUser = mock(AuthorizedUser.class); + given(parameter.getParameterAnnotation(AuthorizedUser.class)).willReturn(authorizedUser); + given(authorizedUser.required()).willReturn(false); + + // when, then + assertThat( + authorizedUserResolver.resolveArgument(parameter, null, null, null) + ).isNull(); + } } - private SiteUser createSiteUser() { - return new SiteUser( + private SiteUser createAndSaveSiteUser() { + SiteUser siteUser = new SiteUser( "test@example.com", "nickname", "profileImageUrl", @@ -63,5 +101,11 @@ private SiteUser createSiteUser() { Role.MENTEE, Gender.MALE ); + return siteUserRepository.save(siteUser); + } + + private SiteUserAuthentication createAuthenticationWithUser(SiteUser siteUser) { + SiteUserDetails userDetails = new SiteUserDetails(siteUser); + return new SiteUserAuthentication("token", userDetails); } } From ba5193b4428249e86c63f32fda124a851bef0556 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Fri, 14 Feb 2025 16:20:37 +0900 Subject: [PATCH 148/158] =?UTF-8?q?refactor:=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0,=20=EC=A7=80=EC=9B=90,=20=EC=84=B1=EC=A0=81=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20api=20=EC=88=98=EC=A0=95=20(#196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 지원서 uri 복수형으로 변경 * refactor: 게시판 uri 변경 - communities -> boards * refactor: 게시글 uri 변경 - communities/{code}/posts -> posts * fix: 영속성 컨텍스트 때문에 동작하지 않는 부분 해결 * refactor: 댓글 uri 변경 * fix: 영속성 컨텍스트 때문에 동작하지 않는 부분 해결 * refactor: 댓글 uri 변경 * fix: 영속성 컨텍스트로 인한 문제 해결 * chore: 잘못 추가한 라인 삭제 --- .../controller/ApplicationController.java | 4 +- .../board/controller/BoardController.java | 19 ++++++- .../comment/controller/CommentController.java | 17 +++---- .../comment/dto/CommentCreateRequest.java | 4 ++ .../comment/service/CommentService.java | 25 ++++++--- .../post/controller/PostController.java | 45 +++++----------- .../community/post/dto/PostCreateRequest.java | 3 ++ .../post/service/PostCommandService.java | 36 ++++++------- .../post/service/PostLikeService.java | 31 +++++------ .../post/service/PostQueryService.java | 4 +- .../score/controller/ScoreController.java | 24 +++++---- .../score/dto/GpaScoreRequest.java | 15 +----- .../score/dto/LanguageTestScoreRequest.java | 15 +----- .../score/service/ScoreService.java | 51 +++++++++++++++---- .../comment/service/CommentServiceTest.java | 22 ++------ .../post/service/PostCommandServiceTest.java | 23 +++------ .../post/service/PostLikeServiceTest.java | 8 +-- .../post/service/PostQueryServiceTest.java | 1 - .../PostLikeCountConcurrencyTest.java | 4 +- .../e2e/ApplicantsQueryTest.java | 16 +++--- .../score/service/ScoreServiceTest.java | 40 ++++++++++++--- 21 files changed, 211 insertions(+), 196 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index 6d8c45fbf..892597e90 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor -@RequestMapping("/application") +@RequestMapping("/applications") @RestController public class ApplicationController { @@ -27,7 +27,7 @@ public class ApplicationController { private final ApplicationQueryService applicationQueryService; // 지원서 제출하기 api - @PostMapping() + @PostMapping public ResponseEntity apply( @AuthorizedUser SiteUser siteUser, @Valid @RequestBody ApplyRequest applyRequest diff --git a/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java index 9329535a1..40410bdbe 100644 --- a/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java +++ b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java @@ -1,10 +1,14 @@ package com.example.solidconnection.community.board.controller; +import com.example.solidconnection.community.post.dto.PostListResponse; +import com.example.solidconnection.community.post.service.PostQueryService; import com.example.solidconnection.type.BoardCode; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; @@ -12,11 +16,13 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/communities") +@RequestMapping("/boards") public class BoardController { + private final PostQueryService postQueryService; + // todo: 회원별로 접근 가능한 게시판 목록 조회 기능 개발 - @GetMapping() + @GetMapping public ResponseEntity findAccessibleCodes() { List accessibleCodeList = new ArrayList<>(); for (BoardCode boardCode : BoardCode.values()) { @@ -24,4 +30,13 @@ public ResponseEntity findAccessibleCodes() { } return ResponseEntity.ok().body(accessibleCodeList); } + + @GetMapping("/{code}") + public ResponseEntity findPostsByCodeAndCategory( + @PathVariable(value = "code") String code, + @RequestParam(value = "category", defaultValue = "전체") String category) { + List postsByCodeAndPostCategory = postQueryService + .findPostsByCodeAndPostCategory(code, category); + return ResponseEntity.ok().body(postsByCodeAndPostCategory); + } } diff --git a/src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java index e215fea72..d096f6cc9 100644 --- a/src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java +++ b/src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java @@ -21,39 +21,36 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/posts") +@RequestMapping("/comments") public class CommentController { private final CommentService commentService; - @PostMapping("/{post_id}/comments") + @PostMapping public ResponseEntity createComment( @AuthorizedUser SiteUser siteUser, - @PathVariable("post_id") Long postId, @Valid @RequestBody CommentCreateRequest commentCreateRequest ) { - CommentCreateResponse response = commentService.createComment(siteUser, postId, commentCreateRequest); + CommentCreateResponse response = commentService.createComment(siteUser, commentCreateRequest); return ResponseEntity.ok().body(response); } - @PatchMapping("/{post_id}/comments/{comment_id}") + @PatchMapping("/{comment_id}") public ResponseEntity updateComment( @AuthorizedUser SiteUser siteUser, - @PathVariable("post_id") Long postId, @PathVariable("comment_id") Long commentId, @Valid @RequestBody CommentUpdateRequest commentUpdateRequest ) { - CommentUpdateResponse response = commentService.updateComment(siteUser, postId, commentId, commentUpdateRequest); + CommentUpdateResponse response = commentService.updateComment(siteUser, commentId, commentUpdateRequest); return ResponseEntity.ok().body(response); } - @DeleteMapping("/{post_id}/comments/{comment_id}") + @DeleteMapping("/{comment_id}") public ResponseEntity deleteCommentById( @AuthorizedUser SiteUser siteUser, - @PathVariable("post_id") Long postId, @PathVariable("comment_id") Long commentId ) { - CommentDeleteResponse response = commentService.deleteCommentById(siteUser, postId, commentId); + CommentDeleteResponse response = commentService.deleteCommentById(siteUser, commentId); return ResponseEntity.ok().body(response); } } diff --git a/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java index 610f602c8..13c512a0c 100644 --- a/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java @@ -4,9 +4,13 @@ import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public record CommentCreateRequest( + @NotNull(message = "게시글 ID를 설정해주세요.") + Long postId, + @NotBlank(message = "댓글 내용은 빈 값일 수 없습니다.") @Size(min = 1, max = 255, message = "댓글 내용은 최소 1자 이상, 최대 255자 이하여야 합니다.") String content, diff --git a/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java index 209dd6987..76138b356 100644 --- a/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java @@ -8,10 +8,11 @@ import com.example.solidconnection.community.comment.dto.CommentUpdateResponse; import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; import com.example.solidconnection.community.comment.repository.CommentRepository; -import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +23,7 @@ import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_LEVEL; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; @Service @RequiredArgsConstructor @@ -29,6 +31,7 @@ public class CommentService { private final CommentRepository commentRepository; private final PostRepository postRepository; + private final SiteUserRepository siteUserRepository; @Transactional(readOnly = true) public List findCommentsByPostId(SiteUser siteUser, Long postId) { @@ -43,15 +46,23 @@ private Boolean isOwner(Comment comment, SiteUser siteUser) { } @Transactional - public CommentCreateResponse createComment(SiteUser siteUser, Long postId, CommentCreateRequest commentCreateRequest) { - Post post = postRepository.getById(postId); + public CommentCreateResponse createComment(SiteUser siteUser, CommentCreateRequest commentCreateRequest) { + Post post = postRepository.getById(commentCreateRequest.postId()); Comment parentComment = null; if (commentCreateRequest.parentId() != null) { parentComment = commentRepository.getById(commentCreateRequest.parentId()); validateCommentDepth(parentComment); } - Comment createdComment = commentRepository.save(commentCreateRequest.toEntity(siteUser, post, parentComment)); + + /* + * todo: siteUser를 영속 상태로 만들 수 있도록 컨트롤러에서 siteUserId 를 넘겨줄 것인지, + * siteUser 에 postList 를 FetchType.EAGER 로 설정할 것인지, + * post 와 siteUser 사이의 양방향을 끊을 것인지 생각해봐야한다. + */ + SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + Comment comment = commentCreateRequest.toEntity(siteUser1, post, parentComment); + Comment createdComment = commentRepository.save(comment); return CommentCreateResponse.from(createdComment); } @@ -64,8 +75,7 @@ private void validateCommentDepth(Comment parentComment) { } @Transactional - public CommentUpdateResponse updateComment(SiteUser siteUser, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { - Post post = postRepository.getById(postId); + public CommentUpdateResponse updateComment(SiteUser siteUser, Long commentId, CommentUpdateRequest commentUpdateRequest) { Comment comment = commentRepository.getById(commentId); validateDeprecated(comment); validateOwnership(comment, siteUser); @@ -82,8 +92,7 @@ private void validateDeprecated(Comment comment) { } @Transactional - public CommentDeleteResponse deleteCommentById(SiteUser siteUser, Long postId, Long commentId) { - Post post = postRepository.getById(postId); + public CommentDeleteResponse deleteCommentById(SiteUser siteUser, Long commentId) { Comment comment = commentRepository.getById(commentId); validateOwnership(comment, siteUser); diff --git a/src/main/java/com/example/solidconnection/community/post/controller/PostController.java b/src/main/java/com/example/solidconnection/community/post/controller/PostController.java index a2479f08b..ee422930a 100644 --- a/src/main/java/com/example/solidconnection/community/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/community/post/controller/PostController.java @@ -1,7 +1,5 @@ package com.example.solidconnection.community.post.controller; -import com.example.solidconnection.community.post.dto.PostListResponse; -import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.community.post.dto.PostCreateRequest; import com.example.solidconnection.community.post.dto.PostCreateResponse; import com.example.solidconnection.community.post.dto.PostDeleteResponse; @@ -13,6 +11,7 @@ import com.example.solidconnection.community.post.service.PostCommandService; import com.example.solidconnection.community.post.service.PostLikeService; import com.example.solidconnection.community.post.service.PostQueryService; +import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -33,41 +32,29 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/communities") +@RequestMapping("/posts") public class PostController { private final PostQueryService postQueryService; private final PostCommandService postCommandService; private final PostLikeService postLikeService; - @GetMapping("/{code}") - public ResponseEntity findPostsByCodeAndCategory( - @PathVariable(value = "code") String code, - @RequestParam(value = "category", defaultValue = "전체") String category) { - - List postsByCodeAndPostCategory = postQueryService - .findPostsByCodeAndPostCategory(code, category); - return ResponseEntity.ok().body(postsByCodeAndPostCategory); - } - - @PostMapping(value = "/{code}/posts") + @PostMapping public ResponseEntity createPost( @AuthorizedUser SiteUser siteUser, - @PathVariable("code") String code, @Valid @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, @RequestParam(value = "file", required = false) List imageFile ) { if (imageFile == null) { imageFile = Collections.emptyList(); } - PostCreateResponse post = postCommandService.createPost(siteUser, code, postCreateRequest, imageFile); + PostCreateResponse post = postCommandService.createPost(siteUser, postCreateRequest, imageFile); return ResponseEntity.ok().body(post); } - @PatchMapping(value = "/{code}/posts/{post_id}") + @PatchMapping(value = "/{post_id}") public ResponseEntity updatePost( @AuthorizedUser SiteUser siteUser, - @PathVariable("code") String code, @PathVariable("post_id") Long postId, @Valid @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, @RequestParam(value = "file", required = false) List imageFile @@ -76,48 +63,44 @@ public ResponseEntity updatePost( imageFile = Collections.emptyList(); } PostUpdateResponse postUpdateResponse = postCommandService.updatePost( - siteUser, code, postId, postUpdateRequest, imageFile + siteUser, postId, postUpdateRequest, imageFile ); return ResponseEntity.ok().body(postUpdateResponse); } - @GetMapping("/{code}/posts/{post_id}") + @GetMapping("/{post_id}") public ResponseEntity findPostById( @AuthorizedUser SiteUser siteUser, - @PathVariable("code") String code, @PathVariable("post_id") Long postId ) { - PostFindResponse postFindResponse = postQueryService.findPostById(siteUser, code, postId); + PostFindResponse postFindResponse = postQueryService.findPostById(siteUser, postId); return ResponseEntity.ok().body(postFindResponse); } - @DeleteMapping(value = "/{code}/posts/{post_id}") + @DeleteMapping(value = "/{post_id}") public ResponseEntity deletePostById( @AuthorizedUser SiteUser siteUser, - @PathVariable("code") String code, @PathVariable("post_id") Long postId ) { - PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(siteUser, code, postId); + PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(siteUser, postId); return ResponseEntity.ok().body(postDeleteResponse); } - @PostMapping(value = "/{code}/posts/{post_id}/like") + @PostMapping(value = "/{post_id}/like") public ResponseEntity likePost( @AuthorizedUser SiteUser siteUser, - @PathVariable("code") String code, @PathVariable("post_id") Long postId ) { - PostLikeResponse postLikeResponse = postLikeService.likePost(siteUser, code, postId); + PostLikeResponse postLikeResponse = postLikeService.likePost(siteUser, postId); return ResponseEntity.ok().body(postLikeResponse); } - @DeleteMapping(value = "/{code}/posts/{post_id}/like") + @DeleteMapping(value = "/{post_id}/like") public ResponseEntity dislikePost( @AuthorizedUser SiteUser siteUser, - @PathVariable("code") String code, @PathVariable("post_id") Long postId ) { - PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(siteUser, code, postId); + PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(siteUser, postId); return ResponseEntity.ok().body(postDislikeResponse); } } diff --git a/src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java index db271a80f..5e6590b20 100644 --- a/src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java @@ -9,6 +9,9 @@ import jakarta.validation.constraints.Size; public record PostCreateRequest( + @NotNull(message = "게시글 카테고리를 설정해주세요.") + String boardCode, + @NotNull(message = "게시글 카테고리를 설정해주세요.") String postCategory, diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java index 1e66b52a4..b95cbcf1b 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java @@ -2,20 +2,20 @@ import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.community.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.community.post.domain.PostImage; import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostImage; import com.example.solidconnection.community.post.dto.PostCreateRequest; import com.example.solidconnection.community.post.dto.PostCreateResponse; import com.example.solidconnection.community.post.dto.PostDeleteResponse; import com.example.solidconnection.community.post.dto.PostUpdateRequest; import com.example.solidconnection.community.post.dto.PostUpdateResponse; import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.ImgType; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.util.RedisUtils; @@ -29,9 +29,9 @@ import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; @Service @RequiredArgsConstructor @@ -42,18 +42,24 @@ public class PostCommandService { private final S3Service s3Service; private final RedisService redisService; private final RedisUtils redisUtils; + private final SiteUserRepository siteUserRepository; @Transactional - public PostCreateResponse createPost(SiteUser siteUser, String code, PostCreateRequest postCreateRequest, + public PostCreateResponse createPost(SiteUser siteUser, PostCreateRequest postCreateRequest, List imageFile) { // 유효성 검증 - String boardCode = validateCode(code); validatePostCategory(postCreateRequest.postCategory()); validateFileSize(imageFile); // 객체 생성 - Board board = boardRepository.getByCode(boardCode); - Post post = postCreateRequest.toEntity(siteUser, board); + Board board = boardRepository.getByCode(postCreateRequest.boardCode()); + /* + * todo: siteUser를 영속 상태로 만들 수 있도록 컨트롤러에서 siteUserId 를 넘겨줄 것인지, + * siteUser 에 postList 를 FetchType.EAGER 로 설정할 것인지, + * post 와 siteUser 사이의 양방향을 끊을 것인지 생각해봐야한다. + */ + SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + Post post = postCreateRequest.toEntity(siteUser1, board); // 이미지 처리 savePostImages(imageFile, post); Post createdPost = postRepository.save(post); @@ -62,10 +68,9 @@ public PostCreateResponse createPost(SiteUser siteUser, String code, PostCreateR } @Transactional - public PostUpdateResponse updatePost(SiteUser siteUser, String code, Long postId, PostUpdateRequest postUpdateRequest, + public PostUpdateResponse updatePost(SiteUser siteUser, Long postId, PostUpdateRequest postUpdateRequest, List imageFile) { // 유효성 검증 - String boardCode = validateCode(code); Post post = postRepository.getById(postId); validateOwnership(post, siteUser); validateQuestion(post); @@ -93,8 +98,7 @@ private void savePostImages(List imageFile, Post post) { } @Transactional - public PostDeleteResponse deletePostById(SiteUser siteUser, String code, Long postId) { - String boardCode = validateCode(code); + public PostDeleteResponse deletePostById(SiteUser siteUser, Long postId) { Post post = postRepository.getById(postId); validateOwnership(post, siteUser); validateQuestion(post); @@ -108,14 +112,6 @@ public PostDeleteResponse deletePostById(SiteUser siteUser, String code, Long po return new PostDeleteResponse(postId); } - private String validateCode(String code) { - try { - return String.valueOf(BoardCode.valueOf(code)); - } catch (IllegalArgumentException ex) { - throw new CustomException(INVALID_BOARD_CODE); - } - } - private void validateOwnership(Post post, SiteUser siteUser) { if (!post.getSiteUser().getId().equals(siteUser.getId())) { throw new CustomException(INVALID_POST_ACCESS); diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java b/src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java index 045c069cd..98d1a239f 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java @@ -1,21 +1,21 @@ package com.example.solidconnection.community.post.service; -import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.community.post.domain.PostLike; import com.example.solidconnection.community.post.dto.PostDislikeResponse; import com.example.solidconnection.community.post.dto.PostLikeResponse; import com.example.solidconnection.community.post.repository.PostLikeRepository; import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_POST_LIKE; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; @Service @RequiredArgsConstructor @@ -23,15 +23,21 @@ public class PostLikeService { private final PostRepository postRepository; private final PostLikeRepository postLikeRepository; + private final SiteUserRepository siteUserRepository; @Transactional(isolation = Isolation.READ_COMMITTED) - public PostLikeResponse likePost(SiteUser siteUser, String code, Long postId) { - String boardCode = validateCode(code); + public PostLikeResponse likePost(SiteUser siteUser, Long postId) { Post post = postRepository.getById(postId); validateDuplicatePostLike(post, siteUser); - PostLike postLike = new PostLike(); - postLike.setPostAndSiteUser(post, siteUser); + + /* + * todo: siteUser를 영속 상태로 만들 수 있도록 컨트롤러에서 siteUserId 를 넘겨줄 것인지, + * siteUser 에 postList 를 FetchType.EAGER 로 설정할 것인지, + * post 와 siteUser 사이의 양방향을 끊을 것인지 생각해봐야한다. + */ + SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + postLike.setPostAndSiteUser(post, siteUser1); postLikeRepository.save(postLike); postRepository.increaseLikeCount(post.getId()); @@ -39,8 +45,7 @@ public PostLikeResponse likePost(SiteUser siteUser, String code, Long postId) { } @Transactional(isolation = Isolation.READ_COMMITTED) - public PostDislikeResponse dislikePost(SiteUser siteUser, String code, Long postId) { - String boardCode = validateCode(code); + public PostDislikeResponse dislikePost(SiteUser siteUser, Long postId) { Post post = postRepository.getById(postId); PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); @@ -51,14 +56,6 @@ public PostDislikeResponse dislikePost(SiteUser siteUser, String code, Long post return PostDislikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 } - private String validateCode(String code) { - try { - return String.valueOf(BoardCode.valueOf(code)); - } catch (IllegalArgumentException ex) { - throw new CustomException(INVALID_BOARD_CODE); - } - } - private void validateDuplicatePostLike(Post post, SiteUser siteUser) { if (postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser).isPresent()) { throw new CustomException(DUPLICATE_POST_LIKE); diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java index 1d7f292ea..66cbb5faa 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java @@ -53,9 +53,7 @@ public List findPostsByCodeAndPostCategory(String code, String } @Transactional(readOnly = true) - public PostFindResponse findPostById(SiteUser siteUser, String code, Long postId) { - String boardCode = validateCode(code); - + public PostFindResponse findPostById(SiteUser siteUser, Long postId) { Post post = postRepository.getByIdUsingEntityGraph(postId); Boolean isOwner = getIsOwner(post, siteUser); Boolean isLiked = getIsLiked(post, siteUser); diff --git a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java index 6c54ab5fe..4ea560657 100644 --- a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java +++ b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java @@ -12,39 +12,43 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; 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.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; @RestController -@RequestMapping("/score") +@RequestMapping("/scores") @RequiredArgsConstructor public class ScoreController { private final ScoreService scoreService; // 학점을 등록하는 api - @PostMapping("/gpa") + @PostMapping("/gpas") public ResponseEntity submitGpaScore( @AuthorizedUser SiteUser siteUser, - @Valid @RequestBody GpaScoreRequest gpaScoreRequest + @Valid @RequestPart("gpaScoreRequest") GpaScoreRequest gpaScoreRequest, + @RequestParam("file") MultipartFile file ) { - Long id = scoreService.submitGpaScore(siteUser, gpaScoreRequest); + Long id = scoreService.submitGpaScore(siteUser, gpaScoreRequest, file); return ResponseEntity.ok(id); } // 어학성적을 등록하는 api - @PostMapping("/languageTest") + @PostMapping("/language-tests") public ResponseEntity submitLanguageTestScore( @AuthorizedUser SiteUser siteUser, - @Valid @RequestBody LanguageTestScoreRequest languageTestScoreRequest + @Valid @RequestPart("languageTestScoreRequest") LanguageTestScoreRequest languageTestScoreRequest, + @RequestParam("file") MultipartFile file ) { - Long id = scoreService.submitLanguageTestScore(siteUser, languageTestScoreRequest); + Long id = scoreService.submitLanguageTestScore(siteUser, languageTestScoreRequest, file); return ResponseEntity.ok(id); } // 학점 상태를 확인하는 api - @GetMapping("/gpa") + @GetMapping("/gpas") public ResponseEntity getGpaScoreStatus( @AuthorizedUser SiteUser siteUser ) { @@ -53,7 +57,7 @@ public ResponseEntity getGpaScoreStatus( } // 어학 성적 상태를 확인하는 api - @GetMapping("/languageTest") + @GetMapping("/language-tests") public ResponseEntity getLanguageTestScoreStatus( @AuthorizedUser SiteUser siteUser ) { diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java index 613ac5b54..beafbf2e3 100644 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java @@ -1,7 +1,5 @@ package com.example.solidconnection.score.dto; -import com.example.solidconnection.application.domain.Gpa; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record GpaScoreRequest( @@ -9,15 +7,6 @@ public record GpaScoreRequest( Double gpa, @NotNull(message = "학점 기준을 입력해주세요.") - Double gpaCriteria, - - @NotBlank(message = "대학 성적 증명서를 첨부해주세요.") - String gpaReportUrl) { - - public Gpa toGpa() { - return new Gpa( - this.gpa, - this.gpaCriteria, - this.gpaReportUrl); - } + Double gpaCriteria +) { } diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java index 92522949e..de9329898 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java @@ -1,6 +1,5 @@ package com.example.solidconnection.score.dto; -import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.type.LanguageTestType; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -10,16 +9,6 @@ public record LanguageTestScoreRequest( LanguageTestType languageTestType, @NotBlank(message = "어학 점수를 입력해주세요.") - String languageTestScore, - - @NotBlank(message = "어학 증명서를 첨부해주세요.") - String languageTestReportUrl) { - - public LanguageTest toLanguageTest() { - return new LanguageTest( - this.languageTestType, - this.languageTestScore, - this.languageTestReportUrl - ); - } + String languageTestScore +) { } diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index 45efb2aa1..66592d339 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -1,6 +1,10 @@ package com.example.solidconnection.score.service; +import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.score.dto.GpaScoreRequest; @@ -12,45 +16,70 @@ import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.ImgType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; + @Service @RequiredArgsConstructor public class ScoreService { private final GpaScoreRepository gpaScoreRepository; + private final S3Service s3Service; private final LanguageTestScoreRepository languageTestScoreRepository; + private final SiteUserRepository siteUserRepository; @Transactional - public Long submitGpaScore(SiteUser siteUser, GpaScoreRequest gpaScoreRequest) { - GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser); - newGpaScore.setSiteUser(siteUser); + public Long submitGpaScore(SiteUser siteUser, GpaScoreRequest gpaScoreRequest, MultipartFile file) { + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.GPA); + Gpa gpa = new Gpa(gpaScoreRequest.gpa(), gpaScoreRequest.gpaCriteria(), uploadedFile.fileUrl()); + + /* + * todo: siteUser를 영속 상태로 만들 수 있도록 컨트롤러에서 siteUserId 를 넘겨줄 것인지, + * siteUser 에 gpaScoreList 를 FetchType.EAGER 로 설정할 것인지, + * gpa 와 siteUser 사이의 양방향을 끊을 것인지 생각해봐야한다. + */ + SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + GpaScore newGpaScore = new GpaScore(gpa, siteUser1); + newGpaScore.setSiteUser(siteUser1); GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); // 저장 후 반환된 객체 return savedNewGpaScore.getId(); // 저장된 GPA Score의 ID 반환 } @Transactional - public Long submitLanguageTestScore(SiteUser siteUser, LanguageTestScoreRequest languageTestScoreRequest) { - LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); + public Long submitLanguageTestScore(SiteUser siteUser, LanguageTestScoreRequest languageTestScoreRequest, MultipartFile file) { + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.LANGUAGE_TEST); + LanguageTest languageTest = new LanguageTest(languageTestScoreRequest.languageTestType(), + languageTestScoreRequest.languageTestScore(), uploadedFile.fileUrl()); - LanguageTestScore newScore = new LanguageTestScore( - languageTest, siteUser); - newScore.setSiteUser(siteUser); + /* + * todo: siteUser를 영속 상태로 만들 수 있도록 컨트롤러에서 siteUserId 를 넘겨줄 것인지, + * siteUser 에 languageTestScoreList 를 FetchType.EAGER 로 설정할 것인지, + * languageTest 와 siteUser 사이의 양방향을 끊을 것인지 생각해봐야한다. + */ + SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + LanguageTestScore newScore = new LanguageTestScore(languageTest, siteUser1); + newScore.setSiteUser(siteUser1); LanguageTestScore savedNewScore = languageTestScoreRepository.save(newScore); // 새로 저장한 객체 return savedNewScore.getId(); // 저장된 객체의 ID 반환 } @Transactional(readOnly = true) public GpaScoreStatusResponse getGpaScoreStatus(SiteUser siteUser) { + // todo: ditto + SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); List gpaScoreStatusList = - Optional.ofNullable(siteUser.getGpaScoreList()) + Optional.ofNullable(siteUser1.getGpaScoreList()) .map(scores -> scores.stream() .map(GpaScoreStatus::from) .collect(Collectors.toList())) @@ -60,8 +89,10 @@ public GpaScoreStatusResponse getGpaScoreStatus(SiteUser siteUser) { @Transactional(readOnly = true) public LanguageTestScoreStatusResponse getLanguageTestScoreStatus(SiteUser siteUser) { + // todo: ditto + SiteUser siteUser1 = siteUserRepository.findById(siteUser.getId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND)); List languageTestScoreStatusList = - Optional.ofNullable(siteUser.getLanguageTestScoreList()) + Optional.ofNullable(siteUser1.getLanguageTestScoreList()) .map(scores -> scores.stream() .map(LanguageTestScoreStatus::from) .collect(Collectors.toList())) diff --git a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java index fca6cd41e..ee74bb90b 100644 --- a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java @@ -9,9 +9,9 @@ import com.example.solidconnection.community.comment.dto.CommentUpdateResponse; import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; import com.example.solidconnection.community.comment.repository.CommentRepository; -import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.PostCategory; @@ -110,12 +110,11 @@ class 댓글_생성_테스트 { void 댓글을_성공적으로_생성한다() { // given Post testPost = createPost(자유게시판, 테스트유저_1); - CommentCreateRequest request = new CommentCreateRequest("테스트 댓글", null); + CommentCreateRequest request = new CommentCreateRequest(testPost.getId(), "테스트 댓글", null); // when CommentCreateResponse response = commentService.createComment( 테스트유저_1, - testPost.getId(), request ); @@ -135,12 +134,11 @@ class 댓글_생성_테스트 { // given Post testPost = createPost(자유게시판, 테스트유저_1); Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); - CommentCreateRequest request = new CommentCreateRequest("테스트 대댓글", parentComment.getId()); + CommentCreateRequest request = new CommentCreateRequest(testPost.getId(), "테스트 대댓글", parentComment.getId()); // when CommentCreateResponse response = commentService.createComment( 테스트유저_2, - testPost.getId(), request ); @@ -161,13 +159,12 @@ class 댓글_생성_테스트 { Post testPost = createPost(자유게시판, 테스트유저_1); Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); - CommentCreateRequest request = new CommentCreateRequest("테스트 대대댓글", childComment.getId()); + CommentCreateRequest request = new CommentCreateRequest(testPost.getId(), "테스트 대대댓글", childComment.getId()); // when & then assertThatThrownBy(() -> commentService.createComment( 테스트유저_1, - testPost.getId(), request )) .isInstanceOf(CustomException.class) @@ -179,13 +176,12 @@ class 댓글_생성_테스트 { // given Post testPost = createPost(자유게시판, 테스트유저_1); long invalidCommentId = 9999L; - CommentCreateRequest request = new CommentCreateRequest("테스트 대댓글", invalidCommentId); + CommentCreateRequest request = new CommentCreateRequest(testPost.getId(), "테스트 대댓글", invalidCommentId); // when & then assertThatThrownBy(() -> commentService.createComment( 테스트유저_1, - testPost.getId(), request )) .isInstanceOf(CustomException.class) @@ -206,7 +202,6 @@ class 댓글_수정_테스트 { // when CommentUpdateResponse response = commentService.updateComment( 테스트유저_1, - testPost.getId(), comment.getId(), request ); @@ -233,7 +228,6 @@ class 댓글_수정_테스트 { assertThatThrownBy(() -> commentService.updateComment( 테스트유저_2, - testPost.getId(), comment.getId(), request )) @@ -252,7 +246,6 @@ class 댓글_수정_테스트 { assertThatThrownBy(() -> commentService.updateComment( 테스트유저_1, - testPost.getId(), comment.getId(), request )) @@ -276,7 +269,6 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( 테스트유저_1, - testPost.getId(), comment.getId() ); @@ -301,7 +293,6 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( 테스트유저_1, - testPost.getId(), parentComment.getId() ); @@ -331,7 +322,6 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( 테스트유저_2, - testPost.getId(), childComment1.getId() ); @@ -362,7 +352,6 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( 테스트유저_2, - testPost.getId(), childComment.getId() ); @@ -384,7 +373,6 @@ class 댓글_삭제_테스트 { assertThatThrownBy(() -> commentService.deleteCommentById( 테스트유저_2, - testPost.getId(), comment.getId() )) .isInstanceOf(CustomException.class) diff --git a/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java index a8052a89c..328a1dc41 100644 --- a/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java @@ -1,16 +1,16 @@ package com.example.solidconnection.community.post.service; import com.example.solidconnection.community.board.domain.Board; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.community.post.domain.PostImage; import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostImage; import com.example.solidconnection.community.post.dto.PostCreateRequest; import com.example.solidconnection.community.post.dto.PostCreateResponse; import com.example.solidconnection.community.post.dto.PostDeleteResponse; import com.example.solidconnection.community.post.dto.PostUpdateRequest; import com.example.solidconnection.community.post.dto.PostUpdateResponse; -import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.community.post.repository.PostImageRepository; +import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; @@ -37,9 +37,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.any; import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; @DisplayName("게시글 생성/수정/삭제 서비스 테스트") @@ -79,7 +79,6 @@ class 게시글_생성_테스트 { // when PostCreateResponse response = postCommandService.createPost( 테스트유저_1, - 자유게시판.getCode(), request, imageFiles ); @@ -108,7 +107,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(INVALID_POST_CATEGORY.getMessage()); } @@ -121,7 +120,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(INVALID_POST_CATEGORY.getMessage()); } @@ -134,7 +133,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); } @@ -159,7 +158,6 @@ class 게시글_수정_테스트 { // when PostUpdateResponse response = postCommandService.updatePost( 테스트유저_1, - 자유게시판.getCode(), testPost.getId(), request, imageFiles @@ -190,7 +188,6 @@ class 게시글_수정_테스트 { assertThatThrownBy(() -> postCommandService.updatePost( 테스트유저_2, - 자유게시판.getCode(), testPost.getId(), request, imageFiles @@ -210,7 +207,6 @@ class 게시글_수정_테스트 { assertThatThrownBy(() -> postCommandService.updatePost( 테스트유저_1, - 자유게시판.getCode(), testPost.getId(), request, imageFiles @@ -230,7 +226,6 @@ class 게시글_수정_테스트 { assertThatThrownBy(() -> postCommandService.updatePost( 테스트유저_1, - 자유게시판.getCode(), testPost.getId(), request, imageFiles @@ -254,7 +249,6 @@ class 게시글_삭제_테스트 { // when PostDeleteResponse response = postCommandService.deletePostById( 테스트유저_1, - 자유게시판.getCode(), testPost.getId() ); @@ -276,7 +270,6 @@ class 게시글_삭제_테스트 { assertThatThrownBy(() -> postCommandService.deletePostById( 테스트유저_2, - 자유게시판.getCode(), testPost.getId() )) .isInstanceOf(CustomException.class) @@ -292,7 +285,6 @@ class 게시글_삭제_테스트 { assertThatThrownBy(() -> postCommandService.deletePostById( 테스트유저_1, - 자유게시판.getCode(), testPost.getId() )) .isInstanceOf(CustomException.class) @@ -302,6 +294,7 @@ class 게시글_삭제_테스트 { private PostCreateRequest createPostCreateRequest(String category) { return new PostCreateRequest( + 자유게시판.getCode(), category, "테스트 제목", "테스트 내용", diff --git a/src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java index 1b1e1d2fd..23fa6bf50 100644 --- a/src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java @@ -45,7 +45,6 @@ class 게시글_좋아요_테스트 { // when PostLikeResponse response = postLikeService.likePost( 테스트유저_1, - 자유게시판.getCode(), testPost.getId() ); @@ -63,13 +62,12 @@ class 게시글_좋아요_테스트 { void 이미_좋아요한_게시글을_다시_좋아요하면_예외_응답을_반환한다() { // given Post testPost = createPost(자유게시판, 테스트유저_1); - postLikeService.likePost(테스트유저_1, 자유게시판.getCode(), testPost.getId()); + postLikeService.likePost(테스트유저_1, testPost.getId()); // when & then assertThatThrownBy(() -> postLikeService.likePost( 테스트유저_1, - 자유게시판.getCode(), testPost.getId() )) .isInstanceOf(CustomException.class) @@ -84,13 +82,12 @@ class 게시글_좋아요_취소_테스트 { void 게시글_좋아요를_성공적으로_취소한다() { // given Post testPost = createPost(자유게시판, 테스트유저_1); - PostLikeResponse beforeResponse = postLikeService.likePost(테스트유저_1, 자유게시판.getCode(), testPost.getId()); + PostLikeResponse beforeResponse = postLikeService.likePost(테스트유저_1, testPost.getId()); long beforeLikeCount = beforeResponse.likeCount(); // when PostDislikeResponse response = postLikeService.dislikePost( 테스트유저_1, - 자유게시판.getCode(), testPost.getId() ); @@ -113,7 +110,6 @@ class 게시글_좋아요_취소_테스트 { assertThatThrownBy(() -> postLikeService.dislikePost( 테스트유저_1, - 자유게시판.getCode(), testPost.getId() )) .isInstanceOf(CustomException.class) diff --git a/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java index 33246e981..fc7926698 100644 --- a/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java @@ -112,7 +112,6 @@ class PostQueryServiceTest extends BaseIntegrationTest { // when PostFindResponse response = postQueryService.findPostById( 테스트유저_1, - 자유게시판.getCode(), testPost.getId() ); diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index 3903f31ff..52b9f24f0 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -103,8 +103,8 @@ private Post createPost(Board board, SiteUser siteUser) { SiteUser tmpSiteUser = siteUserRepository.save(createSiteUserByEmail(email)); executorService.submit(() -> { try { - postLikeService.likePost(tmpSiteUser, board.getCode(), post.getId()); - postLikeService.dislikePost(tmpSiteUser, board.getCode(), post.getId()); + postLikeService.likePost(tmpSiteUser, post.getId()); + postLikeService.dislikePost(tmpSiteUser, post.getId()); } finally { doneSignal.countDown(); } diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index fa2cf0b0b..868eac179 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -105,7 +105,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/application") + .get("/applications") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -151,7 +151,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/application?region=" + 영미권.getCode()) + .get("/applications?region=" + 영미권.getCode()) .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -178,7 +178,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/application?keyword=라") + .get("/applications?keyword=라") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -204,7 +204,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/application?keyword=일본") + .get("/applications?keyword=일본") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -224,7 +224,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/application") + .get("/applications") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -253,7 +253,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) .when().log().all() - .get("/application/competitors") + .get("/applications/competitors") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -295,7 +295,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + user6AccessToken) .when().log().all() - .get("/application/competitors") + .get("/applications/competitors") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); @@ -316,7 +316,7 @@ public void setUpUserAndToken() { ApplicationsResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + adminAccessToken) .when().log().all() - .get("/application/competitors") + .get("/applications/competitors") .then().log().all() .statusCode(200) .extract().as(ApplicationsResponse.class); diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 038aa91b6..0617e1c25 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -2,6 +2,8 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.score.dto.GpaScoreRequest; @@ -16,6 +18,7 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.ImgType; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -23,11 +26,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.BDDMockito.given; @DisplayName("점수 서비스 테스트") class ScoreServiceTest extends BaseIntegrationTest { @@ -44,6 +50,9 @@ class ScoreServiceTest extends BaseIntegrationTest { @Autowired private LanguageTestScoreRepository languageTestScoreRepository; + @MockBean + private S3Service s3Service; + @Test void GPA_점수_상태를_조회한다() { // given @@ -118,9 +127,12 @@ class ScoreServiceTest extends BaseIntegrationTest { // given SiteUser testUser = createSiteUser(); GpaScoreRequest request = createGpaScoreRequest(); + MockMultipartFile file = createFile(); + String fileUrl = "/gpa-report.pdf"; + given(s3Service.uploadFile(file, ImgType.GPA)).willReturn(new UploadedFileUrlResponse(fileUrl)); // when - long scoreId = scoreService.submitGpaScore(testUser, request); + long scoreId = scoreService.submitGpaScore(testUser, request, file); GpaScore savedScore = gpaScoreRepository.findById(scoreId).orElseThrow(); // then @@ -128,7 +140,8 @@ class ScoreServiceTest extends BaseIntegrationTest { () -> assertThat(savedScore.getId()).isEqualTo(scoreId), () -> assertThat(savedScore.getGpa().getGpa()).isEqualTo(request.gpa()), () -> assertThat(savedScore.getGpa().getGpaCriteria()).isEqualTo(request.gpaCriteria()), - () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) + () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), + () -> assertThat(savedScore.getGpa().getGpaReportUrl()).isEqualTo(fileUrl) ); } @@ -137,9 +150,12 @@ class ScoreServiceTest extends BaseIntegrationTest { // given SiteUser testUser = createSiteUser(); LanguageTestScoreRequest request = createLanguageTestScoreRequest(); + MockMultipartFile file = createFile(); + String fileUrl = "/gpa-report.pdf"; + given(s3Service.uploadFile(file, ImgType.LANGUAGE_TEST)).willReturn(new UploadedFileUrlResponse(fileUrl)); // when - long scoreId = scoreService.submitLanguageTestScore(testUser, request); + long scoreId = scoreService.submitLanguageTestScore(testUser, request, file); LanguageTestScore savedScore = languageTestScoreRepository.findById(scoreId).orElseThrow(); // then @@ -147,7 +163,8 @@ class ScoreServiceTest extends BaseIntegrationTest { () -> assertThat(savedScore.getId()).isEqualTo(scoreId), () -> assertThat(savedScore.getLanguageTest().getLanguageTestType()).isEqualTo(request.languageTestType()), () -> assertThat(savedScore.getLanguageTest().getLanguageTestScore()).isEqualTo(request.languageTestScore()), - () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) + () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING), + () -> assertThat(savedScore.getLanguageTest().getLanguageTestReportUrl()).isEqualTo(fileUrl) ); } @@ -185,16 +202,23 @@ private LanguageTestScore createLanguageTestScore(SiteUser siteUser, LanguageTes private GpaScoreRequest createGpaScoreRequest() { return new GpaScoreRequest( 3.5, - 4.5, - "/gpa-report.pdf" + 4.5 ); } private LanguageTestScoreRequest createLanguageTestScoreRequest() { return new LanguageTestScoreRequest( LanguageTestType.TOEFL_IBT, - "100", - "/gpa-report.pdf" + "100" + ); + } + + private MockMultipartFile createFile() { + return new MockMultipartFile( + "image", + "test.jpg", + "image/jpeg", + "test image content".getBytes() ); } } From 74050187550ba1e5a2d130db78d681b21992b683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:22:48 +0900 Subject: [PATCH 149/158] =?UTF-8?q?feat:=20university=5Finfo=5Ffor=5Fapply?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=20details=20=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=20=EA=B8=B8=EC=9D=B4=20=EC=A6=9D=EA=B0=80=20(#212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: details 컬럼 길이 500 → 1000 확장 * feat: Flyway 마이그레이션 스크립트 추가 --- .../university/domain/UniversityInfoForApply.java | 2 +- .../resources/db/migration/V7__expand_details_column_length.sql | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V7__expand_details_column_length.sql diff --git a/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java b/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java index 6a1cdf4dd..e1a87fe83 100644 --- a/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java +++ b/src/main/java/com/example/solidconnection/university/domain/UniversityInfoForApply.java @@ -73,7 +73,7 @@ public class UniversityInfoForApply { @Column(length = 1000) private String detailsForEnglishCourse; - @Column(length = 500) + @Column(length = 1000) private String details; @OneToMany(mappedBy = "universityInfoForApply", fetch = FetchType.EAGER) diff --git a/src/main/resources/db/migration/V7__expand_details_column_length.sql b/src/main/resources/db/migration/V7__expand_details_column_length.sql new file mode 100644 index 000000000..452cc12a7 --- /dev/null +++ b/src/main/resources/db/migration/V7__expand_details_column_length.sql @@ -0,0 +1,2 @@ +ALTER TABLE university_info_for_apply + modify details VARCHAR(1000) NULL; \ No newline at end of file From dc31e4d8cd83bd71558b9d1f61d0d9e975eef0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:03:59 +0900 Subject: [PATCH 150/158] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20authType?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/siteuser/dto/MyPageResponse.java | 3 +++ .../solidconnection/siteuser/service/SiteUserServiceTest.java | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java index 66a7dbef2..9af5e6b2d 100644 --- a/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java +++ b/src/main/java/com/example/solidconnection/siteuser/dto/MyPageResponse.java @@ -1,5 +1,6 @@ package com.example.solidconnection.siteuser.dto; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.Role; @@ -7,6 +8,7 @@ public record MyPageResponse( String nickname, String profileImageUrl, Role role, + AuthType authType, String birth, String email, int likedPostCount, @@ -18,6 +20,7 @@ public static MyPageResponse of(SiteUser siteUser, int likedUniversityCount) { siteUser.getNickname(), siteUser.getProfileImageUrl(), siteUser.getRole(), + siteUser.getAuthType(), siteUser.getBirth(), siteUser.getEmail(), 0, // TODO: 커뮤니티 기능 생기면 업데이트 필요 diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java index cb256bc0f..c6236aedf 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java @@ -69,6 +69,7 @@ class SiteUserServiceTest extends BaseIntegrationTest { () -> assertThat(response.nickname()).isEqualTo(testUser.getNickname()), () -> assertThat(response.profileImageUrl()).isEqualTo(testUser.getProfileImageUrl()), () -> assertThat(response.role()).isEqualTo(testUser.getRole()), + () -> assertThat(response.authType()).isEqualTo(testUser.getAuthType()), () -> assertThat(response.birth()).isEqualTo(testUser.getBirth()), () -> assertThat(response.email()).isEqualTo(testUser.getEmail()), () -> assertThat(response.likedPostCount()).isEqualTo(testUser.getPostLikeList().size()), From e99038539914f81a9e493dbb5ddf2bae68fff4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:40:56 +0900 Subject: [PATCH 151/158] =?UTF-8?q?fix:=20=EC=9C=A0=ED=9A=A8=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=ED=86=A0=ED=81=B0=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20api=20=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20500=20error?= =?UTF-8?q?=20=EB=B0=9C=EC=83=9D=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#210)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 인증/인가 실패 예외 처리 핸들러 추가 * fix: 인증/인가 실패 핸들러 적용 및 필터 순서 변경 * refactor: ExceptionHandlerFilter 인가 예외 처리 제거 --- .../security/SecurityConfiguration.java | 11 +++- .../exception/CustomAccessDeniedHandler.java | 31 +++++++++++ .../CustomAuthenticationEntryPoint.java | 31 +++++++++++ .../custom/response/ErrorResponse.java | 4 ++ .../filter/ExceptionHandlerFilter.java | 8 --- .../CustomAccessDeniedHandlerTest.java | 51 ++++++++++++++++++ .../CustomAuthenticationEntryPointTest.java | 52 +++++++++++++++++++ .../filter/ExceptionHandlerFilterTest.java | 28 ---------- 8 files changed, 178 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandler.java create mode 100644 src/main/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPoint.java create mode 100644 src/test/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandlerTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPointTest.java diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 6afc199de..1d0b110bb 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -1,5 +1,7 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomAccessDeniedHandler; +import com.example.solidconnection.custom.exception.CustomAuthenticationEntryPoint; import com.example.solidconnection.custom.security.filter.ExceptionHandlerFilter; import com.example.solidconnection.custom.security.filter.JwtAuthenticationFilter; import com.example.solidconnection.custom.security.filter.SignOutCheckFilter; @@ -13,7 +15,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; @@ -30,6 +31,8 @@ public class SecurityConfiguration { private final ExceptionHandlerFilter exceptionHandlerFilter; private final SignOutCheckFilter signOutCheckFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; @Bean public CorsConfigurationSource corsConfigurationSource() { @@ -62,9 +65,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/admin/**").hasRole(ADMIN.name()) .anyRequest().permitAll() ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler) + ) .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) .addFilterBefore(signOutCheckFilter, JwtAuthenticationFilter.class) - .addFilterAfter(exceptionHandlerFilter, ExceptionTranslationFilter.class) + .addFilterBefore(exceptionHandlerFilter, SignOutCheckFilter.class) .build(); } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandler.java b/src/main/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandler.java new file mode 100644 index 000000000..52b1725fc --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandler.java @@ -0,0 +1,31 @@ +package com.example.solidconnection.custom.exception; + +import com.example.solidconnection.custom.response.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper objectMapper; + + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + ErrorResponse errorResponse = + new ErrorResponse(ErrorCode.ACCESS_DENIED); + response.setStatus(ErrorCode.ACCESS_DENIED.getCode()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPoint.java new file mode 100644 index 000000000..20f0786b7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPoint.java @@ -0,0 +1,31 @@ +package com.example.solidconnection.custom.exception; + +import com.example.solidconnection.custom.response.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + ErrorResponse errorResponse = + new ErrorResponse(ErrorCode.AUTHENTICATION_FAILED); + response.setStatus(ErrorCode.AUTHENTICATION_FAILED.getCode()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java index 22c173f1d..83cc02622 100644 --- a/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java +++ b/src/main/java/com/example/solidconnection/custom/response/ErrorResponse.java @@ -9,6 +9,10 @@ public ErrorResponse(CustomException e) { this(e.getMessage()); } + public ErrorResponse(ErrorCode e) { + this(e.getMessage()); + } + public ErrorResponse(ErrorCode e, String detail) { this(e.getMessage() + " : " + detail); } diff --git a/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java index 1b8fac2bb..2db133b8f 100644 --- a/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java @@ -10,16 +10,12 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED; import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; @Component @@ -36,10 +32,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, filterChain.doFilter(request, response); } catch (CustomException e) { customCommence(response, e); - } catch (AccessDeniedException e) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - ErrorCode errorCode = auth instanceof AnonymousAuthenticationToken ? AUTHENTICATION_FAILED : ACCESS_DENIED; - generalCommence(response, e, errorCode); } catch (Exception e) { generalCommence(response, e, AUTHENTICATION_FAILED); } diff --git a/src/test/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandlerTest.java b/src/test/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandlerTest.java new file mode 100644 index 000000000..7e4cae5b2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandlerTest.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.custom.exception; + +import com.example.solidconnection.custom.response.ErrorResponse; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; + +import java.io.IOException; + +import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED; +import static org.assertj.core.api.Assertions.assertThat; + +@TestContainerSpringBootTest +@DisplayName("커스텀 인가 예외 처리 테스트") +class CustomAccessDeniedHandlerTest { + + @Autowired + private CustomAccessDeniedHandler accessDeniedHandler; + + @Autowired + private ObjectMapper objectMapper; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @BeforeEach + void setUp() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + @Test + void 권한이_없는_사용자_접근시_403_예외_응답을_반환한다() throws IOException { + // given + AccessDeniedException accessDeniedException = new AccessDeniedException(ACCESS_DENIED.getMessage()); + + // when + accessDeniedHandler.handle(request, response, accessDeniedException); + + // then + ErrorResponse errorResponse = objectMapper.readValue(response.getContentAsString(), ErrorResponse.class); + assertThat(response.getStatus()).isEqualTo(ACCESS_DENIED.getCode()); + assertThat(errorResponse.message()).isEqualTo(ACCESS_DENIED.getMessage()); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPointTest.java b/src/test/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPointTest.java new file mode 100644 index 000000000..2cef64481 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPointTest.java @@ -0,0 +1,52 @@ +package com.example.solidconnection.custom.exception; + +import com.example.solidconnection.custom.response.ErrorResponse; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.AuthenticationException; + +import java.io.IOException; + +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; +import static org.assertj.core.api.Assertions.assertThat; + +@TestContainerSpringBootTest +@DisplayName("커스텀 인증 예외 처리 테스트") +class CustomAuthenticationEntryPointTest { + + @Autowired + private CustomAuthenticationEntryPoint authenticationEntryPoint; + + @Autowired + private ObjectMapper objectMapper; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @BeforeEach + void setUp() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + @Test + void 인증되지_않은_사용자_접근시_401_예외_응답을_반환한다() throws IOException { + // given + AuthenticationException authException = new AuthenticationServiceException(AUTHENTICATION_FAILED.getMessage()); + + // when + authenticationEntryPoint.commence(request, response, authException); + + // then + ErrorResponse errorResponse = objectMapper.readValue(response.getContentAsString(), ErrorResponse.class); + assertThat(response.getStatus()).isEqualTo(AUTHENTICATION_FAILED.getCode()); + assertThat(errorResponse.message()).isEqualTo(AUTHENTICATION_FAILED.getMessage()); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java index fd4bd62a8..f1b3c7359 100644 --- a/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java @@ -85,34 +85,6 @@ void setUp() { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); } - @Test - void 익명_사용자의_접근_거부시_401_예외_응답을_반환한다() throws Exception { - // given - Authentication anonymousAuth = getAnonymousAuth(); - SecurityContextHolder.getContext().setAuthentication(anonymousAuth); - willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response); - - // when - exceptionHandlerFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); - } - - @Test - void 인증된_사용자의_접근_거부하면_403_예외_응답을_반환한다() throws Exception { - // given - Authentication auth = new TestingAuthenticationToken("user", "password", "ROLE_USER"); - SecurityContextHolder.getContext().setAuthentication(auth); - willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response); - - // when - exceptionHandlerFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); - } - private static Stream provideException() { return Stream.of( new RuntimeException(), From 0702640b2618c83fd01e049ae4a23704e9a6cccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 15 Feb 2025 19:24:01 +0900 Subject: [PATCH 152/158] =?UTF-8?q?fix:=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B8=80=20=EB=AA=A9=EB=A1=9D=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=97=86=EC=9D=B4=20=EB=B3=B4=EC=9D=B4=EB=8A=94=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95,=20=EC=84=9C=EB=B8=8C=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 커뮤니티 글 목록 토큰 없이 안보이도록 수정 * chore: submodule 업데이트 --- .../community/board/controller/BoardController.java | 3 +++ src/main/resources/secret | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java index 40410bdbe..a87552796 100644 --- a/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java +++ b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java @@ -2,6 +2,8 @@ import com.example.solidconnection.community.post.dto.PostListResponse; import com.example.solidconnection.community.post.service.PostQueryService; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.BoardCode; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -33,6 +35,7 @@ public ResponseEntity findAccessibleCodes() { @GetMapping("/{code}") public ResponseEntity findPostsByCodeAndCategory( + @AuthorizedUser SiteUser siteUser, @PathVariable(value = "code") String code, @RequestParam(value = "category", defaultValue = "전체") String category) { List postsByCodeAndPostCategory = postQueryService diff --git a/src/main/resources/secret b/src/main/resources/secret index 44128519c..88d71bb3e 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 44128519c61cf80b02e113b1cd4e6387c8f54add +Subproject commit 88d71bb3ee42d1733e4e7dcdb9516a4fa0a5bea9 From 1fdf36ccf8995722e9661c6536fe86b0f546ce6d Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sun, 16 Feb 2025 04:39:42 +0900 Subject: [PATCH 153/158] =?UTF-8?q?chore:=20stage=20CD=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=9E=91=EC=84=B1=20(#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: stage용 CD 추가 * chore: 기존 릴리즈 cd를 stage와 확실히 구분되게 수정 * fix: syntax 에러 수정 * chore: 스테이지 서버 배포 테스트가 prod에 영향을 주지 않도록 --- .../workflows/{release.yml => prod-cd.yml} | 4 +- .github/workflows/stage-cd.yml | 75 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) rename .github/workflows/{release.yml => prod-cd.yml} (94%) create mode 100644 .github/workflows/stage-cd.yml diff --git a/.github/workflows/release.yml b/.github/workflows/prod-cd.yml similarity index 94% rename from .github/workflows/release.yml rename to .github/workflows/prod-cd.yml index 43c2e6e80..7a2601800 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/prod-cd.yml @@ -1,8 +1,8 @@ -name: Build Gradle and Deploy +name: "[PROD] Build Gradle and Deploy" on: push: - branches: [ "release" ] + branches: [ "release" ] # todo: 스테이지 서버 cd 테스트 후 master 로 변경 필요 workflow_dispatch: jobs: diff --git a/.github/workflows/stage-cd.yml b/.github/workflows/stage-cd.yml new file mode 100644 index 000000000..7aa70fe65 --- /dev/null +++ b/.github/workflows/stage-cd.yml @@ -0,0 +1,75 @@ +name: "[STAGE] Build Gradle and Deploy" + +on: + push: + branches: [ "stage-test" ] # todo: 스테이지 서버 cd 테스트 후 release 로 변경 필요 + workflow_dispatch: + +jobs: + build-gradle: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout the code + uses: actions/checkout@v4 + with: + token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + + - name: Grant execute permission for Gradle wrapper(gradlew) + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew bootJar -Dspring.profiles.active=prod + + - name: Copy jar file to remote + uses: appleboy/scp-action@master + with: + host: ${{ secrets.STAGE_HOST }} + username: ${{ secrets.STAGE_USERNAME }} + key: ${{ secrets.STAGE_PRIVATE_KEY }} + source: "./build/libs/*.jar" + target: "/home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage/" + + - name: Copy docker file to remote + uses: appleboy/scp-action@master + with: + host: ${{ secrets.STAGE_HOST }} + username: ${{ secrets.STAGE_USERNAME }} + key: ${{ secrets.STAGE_PRIVATE_KEY }} + source: "./Dockerfile" + target: "/home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage/" + + - name: Copy docker compose file to remote + uses: appleboy/scp-action@master + with: + host: ${{ secrets.STAGE_HOST }} + username: ${{ secrets.STAGE_USERNAME }} + key: ${{ secrets.STAGE_PRIVATE_KEY }} + source: "./docker-compose.yml" + target: "/home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage/" + + - name: Run docker compose + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.STAGE_HOST }} + username: ${{ secrets.STAGE_USERNAME }} + key: ${{ secrets.STAGE_PRIVATE_KEY }} + script_stop: true + script: | + cd /home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage + docker compose down + docker compose up -d --build From b9e58229070aa6169a631918a4079a58788c96c2 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sun, 16 Feb 2025 15:48:36 +0900 Subject: [PATCH 154/158] =?UTF-8?q?chore:=20=EC=8A=A4=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20profile=20=EB=B3=80=EA=B2=BD=20(#217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: stage profile 변경 * chore: 서브모듈 업데이트 * chore: 서브모듈 업데이트 --- .github/workflows/stage-cd.yml | 2 +- src/main/resources/secret | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stage-cd.yml b/.github/workflows/stage-cd.yml index 7aa70fe65..3409a3829 100644 --- a/.github/workflows/stage-cd.yml +++ b/.github/workflows/stage-cd.yml @@ -33,7 +33,7 @@ jobs: run: chmod +x ./gradlew - name: Build with Gradle - run: ./gradlew bootJar -Dspring.profiles.active=prod + run: ./gradlew bootJar -Dspring.profiles.active=stage - name: Copy jar file to remote uses: appleboy/scp-action@master diff --git a/src/main/resources/secret b/src/main/resources/secret index 88d71bb3e..496ba4a63 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 88d71bb3ee42d1733e4e7dcdb9516a4fa0a5bea9 +Subproject commit 496ba4a63952ff154508f60b66c456bfd125e519 From 5bd700dd7754967cd8db1ef0f9e0e621e380c31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:39:58 +0900 Subject: [PATCH 155/158] =?UTF-8?q?fix:=20=EC=A7=80=EC=9B=90=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20=EA=B0=B1=EC=8B=A0=20=EB=AC=B8=EC=A0=9C=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0=20(#220)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 캐시 키를 단일화하여 관리 - 지원서 등록 시 캐시 자동 삭제 로직 추가 --- .../application/service/ApplicationQueryService.java | 3 ++- .../service/ApplicationSubmissionService.java | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 170d7cf13..3208d24af 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -46,7 +46,8 @@ public class ApplicationQueryService { * - 1지망, 2지망 지원자들을 조회한다. * */ @Transactional(readOnly = true) - @ThunderingHerdCaching(key = "application:query:{1}:{2}", cacheManager = "customCacheManager", ttlSec = 86400) + // todo: 임시로 단일 키로 캐시 적용. 추후 캐싱 전략 재검토 필요. + @ThunderingHerdCaching(key = "applications:all", cacheManager = "customCacheManager", ttlSec = 86400) public ApplicationsResponse getApplicants(SiteUser siteUser, String regionCode, String keyword) { // 국가와 키워드와 지역을 통해 대학을 필터링한다. List universities diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index dec092f5e..ea05c3c0c 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -4,6 +4,7 @@ import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.dto.UniversityChoiceRequest; import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.cache.annotation.DefaultCacheOut; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; @@ -18,14 +19,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; import java.util.Optional; -import java.util.Set; import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; -import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE_STATUS; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE; @@ -48,6 +44,11 @@ public class ApplicationSubmissionService { // 학점 및 어학성적이 모두 유효한 경우에만 지원서 등록이 가능하다. // 기존에 있던 status field 우선 APRROVED로 입력시킨다. @Transactional + // todo: 임시로 새로운 신청 생성 시 기존 캐싱 데이터를 삭제한다. 추후 수정 필요 + @DefaultCacheOut( + key = {"applications:all"}, + cacheManager = "customCacheManager" + ) public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) { UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest(); From c54ce99530f889cc6e80783aad0f2db79154c268 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Mon, 17 Feb 2025 19:09:32 +0900 Subject: [PATCH 156/158] =?UTF-8?q?chore:=20stage,=20prod=20docker-compose?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC=20(#218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: spring profile을 docker file에서 지정하지 않도록 * chore: docker compose 파일 prod, stage 분리 * chore: cd에서 환경에 맞는 docker-compse를 쓰도록 * chore: 서브모듈 업데이트 --- .github/workflows/prod-cd.yml | 6 ++-- .github/workflows/stage-cd.yml | 6 ++-- Dockerfile | 2 +- docker-compose.yml => docker-compose.prod.yml | 3 +- docker-compose.stage.yml | 33 +++++++++++++++++++ src/main/resources/secret | 2 +- 6 files changed, 43 insertions(+), 9 deletions(-) rename docker-compose.yml => docker-compose.prod.yml (92%) create mode 100644 docker-compose.stage.yml diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index 7a2601800..75d9e7c11 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -33,7 +33,7 @@ jobs: run: chmod +x ./gradlew - name: Build with Gradle - run: ./gradlew bootJar -Dspring.profiles.active=prod + run: ./gradlew bootJar - name: Copy jar file to remote uses: appleboy/scp-action@master @@ -59,7 +59,7 @@ jobs: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.PRIVATE_KEY }} - source: "./docker-compose.yml" + source: "./docker-compose.prod.yml" target: "/home/${{ secrets.USERNAME }}/solid-connect-server/" - name: Run docker compose @@ -72,4 +72,4 @@ jobs: script: | cd /home/${{ secrets.USERNAME }}/solid-connect-server docker compose down - docker compose up -d --build + docker compose -f docker-compose.prod.yml up -d --build diff --git a/.github/workflows/stage-cd.yml b/.github/workflows/stage-cd.yml index 3409a3829..cc0972e44 100644 --- a/.github/workflows/stage-cd.yml +++ b/.github/workflows/stage-cd.yml @@ -33,7 +33,7 @@ jobs: run: chmod +x ./gradlew - name: Build with Gradle - run: ./gradlew bootJar -Dspring.profiles.active=stage + run: ./gradlew bootJar - name: Copy jar file to remote uses: appleboy/scp-action@master @@ -59,7 +59,7 @@ jobs: host: ${{ secrets.STAGE_HOST }} username: ${{ secrets.STAGE_USERNAME }} key: ${{ secrets.STAGE_PRIVATE_KEY }} - source: "./docker-compose.yml" + source: "./docker-compose.stage.yml" target: "/home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage/" - name: Run docker compose @@ -72,4 +72,4 @@ jobs: script: | cd /home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage docker compose down - docker compose up -d --build + docker compose -f docker-compose.stage.yml up -d --build diff --git a/Dockerfile b/Dockerfile index 4742ac87a..773d1ba16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ARG JAR_FILE=./build/libs/solid-connection-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar # 시스템 진입점 정의 -ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=prod"] +ENTRYPOINT ["java", "-jar", "/app.jar"] # 볼륨 설정 VOLUME /tmp diff --git a/docker-compose.yml b/docker-compose.prod.yml similarity index 92% rename from docker-compose.yml rename to docker-compose.prod.yml index 8813a4e58..9517a07aa 100644 --- a/docker-compose.yml +++ b/docker-compose.prod.yml @@ -25,7 +25,8 @@ services: ports: - "8080:8080" environment: + - SPRING_PROFILES_ACTIVE=prod - SPRING_DATA_REDIS_HOST=redis - SPRING_DATA_REDIS_PORT=6379 depends_on: - - redis \ No newline at end of file + - redis diff --git a/docker-compose.stage.yml b/docker-compose.stage.yml new file mode 100644 index 000000000..3a97a6411 --- /dev/null +++ b/docker-compose.stage.yml @@ -0,0 +1,33 @@ +version: '3.8' + +services: + redis: + image: redis:latest + container_name: redis + ports: + - "6379:6379" + network_mode: host + + redis-exporter: + image: oliver006/redis_exporter + container_name: redis-exporter + ports: + - "9121:9121" + environment: + REDIS_ADDR: "localhost:6379" + depends_on: + - redis + network_mode: host + + solid-connection-stage: + build: + context: . + dockerfile: Dockerfile + container_name: solid-connection-stage + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=stage + depends_on: + - redis + network_mode: host diff --git a/src/main/resources/secret b/src/main/resources/secret index 496ba4a63..f7b34bdc6 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 496ba4a63952ff154508f60b66c456bfd125e519 +Subproject commit f7b34bdc615f141ac82ed05c10557f95f5b45a04 From 8f8cd67a754aea7282af2eab19ec861fab5f7501 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Mon, 17 Feb 2025 19:37:10 +0900 Subject: [PATCH 157/158] =?UTF-8?q?chore:=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=9D=B4=EB=A6=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#223)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prod-cd.yml | 2 +- .github/workflows/stage-cd.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index 75d9e7c11..9030c80f5 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -2,7 +2,7 @@ name: "[PROD] Build Gradle and Deploy" on: push: - branches: [ "release" ] # todo: 스테이지 서버 cd 테스트 후 master 로 변경 필요 + branches: [ "master" ] workflow_dispatch: jobs: diff --git a/.github/workflows/stage-cd.yml b/.github/workflows/stage-cd.yml index cc0972e44..41ff68b37 100644 --- a/.github/workflows/stage-cd.yml +++ b/.github/workflows/stage-cd.yml @@ -2,7 +2,7 @@ name: "[STAGE] Build Gradle and Deploy" on: push: - branches: [ "stage-test" ] # todo: 스테이지 서버 cd 테스트 후 release 로 변경 필요 + branches: [ "release" ] workflow_dispatch: jobs: From c8fe1a8bc23de18aeed71abd9a7819455731744f Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Mon, 17 Feb 2025 20:39:14 +0900 Subject: [PATCH 158/158] =?UTF-8?q?chore:=20=EC=84=9C=EB=B8=8C=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(#224)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/secret | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/secret b/src/main/resources/secret index f7b34bdc6..95ffe4882 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit f7b34bdc615f141ac82ed05c10557f95f5b45a04 +Subproject commit 95ffe48824a26d4b0c52c5c03a8cf40fc2cda098