From 6ec65ac618517b4f42c66ffadd212c136d9ef852 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 28 Apr 2024 21:33:28 +0900 Subject: [PATCH 001/273] =?UTF-8?q?chore:=20devenv/db=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devenv/db/Dockerfiles | 5 +++++ devenv/db/Makefile | 15 +++++++++++++++ devenv/db/compose.yml | 21 +++++++++++++++++++++ devenv/db/conf.d/my.cnf | 11 +++++++++++ devenv/db/initdb.d/.gitkeep | 0 5 files changed, 52 insertions(+) create mode 100644 devenv/db/Dockerfiles create mode 100644 devenv/db/Makefile create mode 100644 devenv/db/compose.yml create mode 100644 devenv/db/conf.d/my.cnf create mode 100644 devenv/db/initdb.d/.gitkeep diff --git a/devenv/db/Dockerfiles b/devenv/db/Dockerfiles new file mode 100644 index 00000000..e0436f3c --- /dev/null +++ b/devenv/db/Dockerfiles @@ -0,0 +1,5 @@ +FROM mariadb:10.11 + +# windows에서 volume mount 할 경우, 파일 권한이 777로 변경되는 문제가 있어서 아래와 같은 작업을 추가 함 +COPY conf.d/my.cnf /etc/mysql/conf.d/my.cnf +RUN chmod 644 /etc/mysql/conf.d/my.cnf diff --git a/devenv/db/Makefile b/devenv/db/Makefile new file mode 100644 index 00000000..1481badb --- /dev/null +++ b/devenv/db/Makefile @@ -0,0 +1,15 @@ +.PHONY:up +up: + docker compose up -d --build + +.PHONY:down +down: + docker compose down + +.PHONY:log +log: + docker compose logs -f + +.PHONY:clean +clean: + docker compose down -v diff --git a/devenv/db/compose.yml b/devenv/db/compose.yml new file mode 100644 index 00000000..1ffefea0 --- /dev/null +++ b/devenv/db/compose.yml @@ -0,0 +1,21 @@ +services: + db: + build: + context: . + dockerfile: Dockerfiles + volumes: + - ./initdb.d:/docker-entrypoint-initdb.d + - db-data:/var/lib/mysql + environment: + MYSQL_PORT: 3306 + MYSQL_ROOT_PASSWORD: rootPw + MYSQL_DATABASE: vook + MYSQL_USER: user + MYSQL_PASSWORD: userPw + TZ: Asia/Seoul + ports: + - "3307:3306" + restart: always + +volumes: + db-data: {} diff --git a/devenv/db/conf.d/my.cnf b/devenv/db/conf.d/my.cnf new file mode 100644 index 00000000..c6314654 --- /dev/null +++ b/devenv/db/conf.d/my.cnf @@ -0,0 +1,11 @@ +[client] +default-character-set = utf8mb4 + +[mysql] +default-character-set = utf8mb4 + +[mysqld] +character-set-client-handshake = FALSE +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +lower_case_table_names = 1 diff --git a/devenv/db/initdb.d/.gitkeep b/devenv/db/initdb.d/.gitkeep new file mode 100644 index 00000000..e69de29b From 6b0448cfadf3bf51e638dfdb6dca5fcc97bf6f3c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 28 Apr 2024 21:34:33 +0900 Subject: [PATCH 002/273] =?UTF-8?q?chore:=20devenv/meilisearch=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devenv/meilisearch/Makefile | 15 +++++++++++++++ devenv/meilisearch/compose.yml | 13 +++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 devenv/meilisearch/Makefile create mode 100644 devenv/meilisearch/compose.yml diff --git a/devenv/meilisearch/Makefile b/devenv/meilisearch/Makefile new file mode 100644 index 00000000..fcbaae30 --- /dev/null +++ b/devenv/meilisearch/Makefile @@ -0,0 +1,15 @@ +.PHONY:up +up: + docker compose up -d + +.PHONY:down +down: + docker compose down + +.PHONY:clean +clean: + docker compose down -v + +.PHONY:log +log: + docker compose logs -f --since 1m diff --git a/devenv/meilisearch/compose.yml b/devenv/meilisearch/compose.yml new file mode 100644 index 00000000..b95dc0a7 --- /dev/null +++ b/devenv/meilisearch/compose.yml @@ -0,0 +1,13 @@ +services: + meilisearch: + image: getmeili/meilisearch:v1.7.6 + volumes: + - meili_data:/meili_data + ports: + - "7700:7700" + environment: + - MEILI_ENV=development + - MEILI_MASTER_KEY=aSampleMasterKey + +volumes: + meili_data: {} From 87e4caa50ad76aa595686df9cf954da6c6248ad9 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 28 Apr 2024 21:44:07 +0900 Subject: [PATCH 003/273] =?UTF-8?q?chore:=20api=20module=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/.gitignore | 37 +++ api/build.gradle | 44 ++++ api/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes api/gradle/wrapper/gradle-wrapper.properties | 7 + api/gradlew | 249 ++++++++++++++++++ api/gradlew.bat | 92 +++++++ api/settings.gradle | 1 + .../java/vook/server/api/ApiApplication.java | 13 + api/src/main/resources/application-local.yml | 15 ++ api/src/main/resources/application.yml | 3 + .../vook/server/api/ApiApplicationTests.java | 13 + 11 files changed, 474 insertions(+) create mode 100644 api/.gitignore create mode 100644 api/build.gradle create mode 100644 api/gradle/wrapper/gradle-wrapper.jar create mode 100644 api/gradle/wrapper/gradle-wrapper.properties create mode 100755 api/gradlew create mode 100644 api/gradlew.bat create mode 100644 api/settings.gradle create mode 100644 api/src/main/java/vook/server/api/ApiApplication.java create mode 100644 api/src/main/resources/application-local.yml create mode 100644 api/src/main/resources/application.yml create mode 100644 api/src/test/java/vook/server/api/ApiApplicationTests.java diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/api/.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/api/build.gradle b/api/build.gradle new file mode 100644 index 00000000..f6dc2b83 --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.5' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'vook.server' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '21' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // spring + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // mariadb + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + + // meilisearch + implementation 'com.meilisearch.sdk:meilisearch-java:0.11.8' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/api/gradle/wrapper/gradle-wrapper.jar b/api/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + 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/api/gradlew.bat b/api/gradlew.bat new file mode 100644 index 00000000..25da30db --- /dev/null +++ b/api/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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/api/settings.gradle b/api/settings.gradle new file mode 100644 index 00000000..5cd7dd3b --- /dev/null +++ b/api/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'api' diff --git a/api/src/main/java/vook/server/api/ApiApplication.java b/api/src/main/java/vook/server/api/ApiApplication.java new file mode 100644 index 00000000..f4b4a345 --- /dev/null +++ b/api/src/main/java/vook/server/api/ApiApplication.java @@ -0,0 +1,13 @@ +package vook.server.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiApplication.class, args); + } + +} diff --git a/api/src/main/resources/application-local.yml b/api/src/main/resources/application-local.yml new file mode 100644 index 00000000..a164c031 --- /dev/null +++ b/api/src/main/resources/application-local.yml @@ -0,0 +1,15 @@ +spring: + datasource: + driver-class-name: org.mariadb.jdbc.Driver + url: jdbc:mariadb://localhost:3307/vook + username: user + password: userPw + jpa: + hibernate: + ddl-auto: update +server: + port: 8080 +service: + meilisearch: + host: http://localhost:7700 + apiKey: aSampleMasterKey diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml new file mode 100644 index 00000000..74559710 --- /dev/null +++ b/api/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + default: local diff --git a/api/src/test/java/vook/server/api/ApiApplicationTests.java b/api/src/test/java/vook/server/api/ApiApplicationTests.java new file mode 100644 index 00000000..e8ec174f --- /dev/null +++ b/api/src/test/java/vook/server/api/ApiApplicationTests.java @@ -0,0 +1,13 @@ +package vook.server.api; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApiApplicationTests { + + @Test + void contextLoads() { + } + +} From 2f2aa2341831956ede8c1cc0e4644e1eb1e0c33a Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 28 Apr 2024 22:08:15 +0900 Subject: [PATCH 004/273] =?UTF-8?q?chore:=20springdoc-openapi=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle | 4 ++++ api/src/main/resources/application.yml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/api/build.gradle b/api/build.gradle index f6dc2b83..41cd939c 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -37,6 +37,10 @@ dependencies { // meilisearch implementation 'com.meilisearch.sdk:meilisearch-java:0.11.8' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.5.0' } tasks.named('test') { diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 74559710..84c373a2 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -1,3 +1,8 @@ spring: profiles: default: local +springdoc: + swagger-ui: + path: /swagger-ui.html + api-docs: + path: /api-docs From 3292b8d4ed35b207f2c4d97a0dc7e28095170f4d Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 28 Apr 2024 22:04:46 +0900 Subject: [PATCH 005/273] =?UTF-8?q?feat:=20=ED=97=AC=EC=8A=A4=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20API=20=EC=B6=94=EA=B0=80=20#3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/web/routes/health/HealthApi.java | 22 ++++++++++++++++++ .../web/routes/health/HealthController.java | 15 ++++++++++++ .../vook/server/api/testhelper/ApiTest.java | 13 +++++++++++ .../routes/health/HealthControllerTest.java | 23 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 api/src/main/java/vook/server/api/web/routes/health/HealthApi.java create mode 100644 api/src/main/java/vook/server/api/web/routes/health/HealthController.java create mode 100644 api/src/test/java/vook/server/api/testhelper/ApiTest.java create mode 100644 api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java diff --git a/api/src/main/java/vook/server/api/web/routes/health/HealthApi.java b/api/src/main/java/vook/server/api/web/routes/health/HealthApi.java new file mode 100644 index 00000000..e107d6c3 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/health/HealthApi.java @@ -0,0 +1,22 @@ +package vook.server.api.web.routes.health; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "health") +public interface HealthApi { + + @Operation( + summary = "서버 상태 확인", + description = """ + - 서버의 상태를 체크하는 API입니다.""") + @ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "text/plain" + ) + ) + String health(); +} diff --git a/api/src/main/java/vook/server/api/web/routes/health/HealthController.java b/api/src/main/java/vook/server/api/web/routes/health/HealthController.java new file mode 100644 index 00000000..c0cd88f2 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/health/HealthController.java @@ -0,0 +1,15 @@ +package vook.server.api.web.routes.health; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/health") +public class HealthController implements HealthApi { + + @GetMapping + public String health() { + return "OK"; + } +} diff --git a/api/src/test/java/vook/server/api/testhelper/ApiTest.java b/api/src/test/java/vook/server/api/testhelper/ApiTest.java new file mode 100644 index 00000000..d4318400 --- /dev/null +++ b/api/src/test/java/vook/server/api/testhelper/ApiTest.java @@ -0,0 +1,13 @@ +package vook.server.api.testhelper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public abstract class ApiTest { + + @Autowired + protected TestRestTemplate rest; + +} diff --git a/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java b/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java new file mode 100644 index 00000000..950e5119 --- /dev/null +++ b/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java @@ -0,0 +1,23 @@ +package vook.server.api.web.routes.health; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import vook.server.api.testhelper.ApiTest; + +import static org.assertj.core.api.Assertions.assertThat; + +class HealthControllerTest extends ApiTest { + + @Test + @DisplayName("헬스체크") + void health() { + // when + ResponseEntity res = rest.getForEntity("/health", String.class); + + // then + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(res.getBody()).isEqualTo("OK"); + } +} From 25a36ba57db466051ab4d35967c7f18e5eb6683a Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 1 May 2024 23:55:19 +0900 Subject: [PATCH 006/273] =?UTF-8?q?chore:=20=ED=94=84=EB=A1=A0=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=9D=84=20=EC=9C=84=ED=95=9C=20dev=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EC=84=9C=EB=B2=84=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=9E=91=EC=84=B1=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + api/build.gradle | 5 ++ dagger/.gitattributes | 4 ++ dagger/.gitignore | 4 ++ dagger/Makefile | 16 +++++ dagger/dagger.json | 20 ++++++ dagger/go.mod | 37 +++++++++++ dagger/go.sum | 78 ++++++++++++++++++++++ dagger/main.go | 151 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 317 insertions(+) create mode 100644 dagger/.gitattributes create mode 100644 dagger/.gitignore create mode 100644 dagger/Makefile create mode 100644 dagger/dagger.json create mode 100644 dagger/go.mod create mode 100644 dagger/go.sum create mode 100644 dagger/main.go diff --git a/.gitignore b/.gitignore index 9f11b755..ec16e763 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .idea/ +dagger/secrets +dagger/out diff --git a/api/build.gradle b/api/build.gradle index 41cd939c..0469bdf3 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -43,6 +43,11 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.5.0' } +bootJar { + destinationDirectory = file(rootDir.absolutePath + '/jar') + archiveFileName = "${project.name}.jar" +} + tasks.named('test') { useJUnitPlatform() } diff --git a/dagger/.gitattributes b/dagger/.gitattributes new file mode 100644 index 00000000..3a454933 --- /dev/null +++ b/dagger/.gitattributes @@ -0,0 +1,4 @@ +/dagger.gen.go linguist-generated +/internal/dagger/** linguist-generated +/internal/querybuilder/** linguist-generated +/internal/telemetry/** linguist-generated diff --git a/dagger/.gitignore b/dagger/.gitignore new file mode 100644 index 00000000..7ebabcc1 --- /dev/null +++ b/dagger/.gitignore @@ -0,0 +1,4 @@ +/dagger.gen.go +/internal/dagger +/internal/querybuilder +/internal/telemetry diff --git a/dagger/Makefile b/dagger/Makefile new file mode 100644 index 00000000..fa27db8d --- /dev/null +++ b/dagger/Makefile @@ -0,0 +1,16 @@ +VERSION := $(shell git describe --tags --always --dirty) + +.PHONY:deploy-dev +deploy-dev: + dagger call -v deploy \ + --source-dir=../api \ + --ssh-dest=file:./secrets/dev/dest.txt \ + --ssh-key=file:./secrets/dev/ssh.key \ + --target-path=~/repo/makevook/vook-deployment/dev \ + --version=${VERSION} \ + --command="make deploy-api" + +.PHONY:build +build: + dagger call -v build-api-jar --dir=../api -o out/api.jar + dagger call -v build-api-image --jar-file=out/api.jar -o out/api_linux_arm64.tar diff --git a/dagger/dagger.json b/dagger/dagger.json new file mode 100644 index 00000000..3e613286 --- /dev/null +++ b/dagger/dagger.json @@ -0,0 +1,20 @@ +{ + "name": "vook-server", + "sdk": "go", + "dependencies": [ + { + "name": "docker", + "source": "github.com/purpleclay/daggerverse/docker@43c1c55dadf15afc9ba401dc59e04baaa3802cca" + }, + { + "name": "scp", + "source": "github.com/seungyeop-lee/daggerverse/scp@2d67873bd965c09398a08034068ff43586537f76" + }, + { + "name": "ssh", + "source": "github.com/seungyeop-lee/daggerverse/ssh@f2a065a7441d08e18d34a12a5097a5335e663b69" + } + ], + "source": ".", + "engineVersion": "v0.11.2" +} diff --git a/dagger/go.mod b/dagger/go.mod new file mode 100644 index 00000000..b8955f99 --- /dev/null +++ b/dagger/go.mod @@ -0,0 +1,37 @@ +module dagger/dagger + +go 1.22.2 + +require ( + github.com/99designs/gqlgen v0.17.44 + github.com/Khan/genqlient v0.7.0 + github.com/vektah/gqlparser/v2 v2.5.11 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 + go.opentelemetry.io/otel/sdk v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/sync v0.6.0 + google.golang.org/grpc v1.62.1 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/sosodev/duration v1.2.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/dagger/go.sum b/dagger/go.sum new file mode 100644 index 00000000..55462ae8 --- /dev/null +++ b/dagger/go.sum @@ -0,0 +1,78 @@ +github.com/99designs/gqlgen v0.17.44 h1:OS2wLk/67Y+vXM75XHbwRnNYJcbuJd4OBL76RX3NQQA= +github.com/99designs/gqlgen v0.17.44/go.mod h1:UTCu3xpK2mLI5qcMNw+HKDiEL77it/1XtAjisC4sLwM= +github.com/Khan/genqlient v0.7.0 h1:GZ1meyRnzcDTK48EjqB8t3bcfYvHArCUUvgOwpz1D4w= +github.com/Khan/genqlient v0.7.0/go.mod h1:HNyy3wZvuYwmW3Y7mkoQLZsa/R5n5yIRajS1kPBvSFM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us= +github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= +github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/dagger/main.go b/dagger/main.go new file mode 100644 index 00000000..3c2a3e44 --- /dev/null +++ b/dagger/main.go @@ -0,0 +1,151 @@ +package main + +import ( + "context" + "fmt" +) + +type VookServer struct{} + +func (v *VookServer) BuildApiJar( + // 빌드 대상의 디렉토리 + dir *Directory, +) *File { + f := dag.Container(). + From("eclipse-temurin:21-jdk"). + WithWorkdir("/app"). + WithDirectory("/app/gradle", dir.Directory("gradle")). + WithFiles("/app", []*File{dir.File("gradlew")}). + WithFiles("/app", []*File{ + dir.File("build.gradle"), + dir.File("settings.gradle"), + }). + WithExec([]string{"./gradlew"}). + WithDirectory("/app/src", dir.Directory("src")). + WithExec([]string{"./gradlew", "bootJar"}). + File("jar/api.jar") + + return f +} + +func (v *VookServer) BuildApiImage( + // jar 파일 + jarFile *File, + // profile + // +optional + profile string, +) *File { + if profile == "" { + profile = "default" + } + + dockerfile := ` +FROM eclipse-temurin:21-jre + +WORKDIR /app + +COPY app.jar app.jar + +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=` + profile + `", "app.jar"] +` + sourceDir := dag.Directory(). + WithFile("app.jar", jarFile). + WithNewFile("Dockerfile", dockerfile) + + return dag.Docker(). + Build(sourceDir, DockerBuildOpts{ + Platform: []string{"linux/arm64"}, + }). + Save(DockerBuildSaveOpts{ + Name: "api", + }). + File("api_linux_arm64.tar") +} + +func (v *VookServer) SendImage( + ctx context.Context, + sshDest *Secret, + sshKey *Secret, + path string, + imageTar *File, +) error { + sshDestText, err := sshDest.Plaintext(ctx) + if err != nil { + return err + } + + _, err = dag.Scp(). + Config(sshDestText). + WithIdentityFile(sshKey). + FileToRemote(imageTar, ScpCommanderFileToRemoteOpts{ + Target: path, + }). + Sync(ctx) + if err != nil { + return err + } + + return nil +} + +func (v *VookServer) Apply( + ctx context.Context, + destination *Secret, + sshKey *Secret, + imageTar *File, + path string, + version string, + command string, +) error { + destinationText, err := destination.Plaintext(ctx) + if err != nil { + return err + } + + filename, err := imageTar.Name(ctx) + if err != nil { + return err + } + + _, err = dag.SSH(). + Config(destinationText). + WithIdentityFile(sshKey). + Command( + fmt.Sprintf(` +cd %s +FILENAME=%s VERSION=%s %s +`, path, filename, version, command), + ). + Sync(ctx) + if err != nil { + return err + } + + return nil +} + +func (v *VookServer) Deploy( + ctx context.Context, + sourceDir *Directory, + sshDest *Secret, + sshKey *Secret, + targetPath string, + version string, + command string, +) error { + jarFile := v.BuildApiJar(sourceDir) + + imageTar := v.BuildApiImage(jarFile, "default") + + err := v.SendImage(ctx, sshDest, sshKey, targetPath, imageTar) + if err != nil { + return err + } + + err = v.Apply(ctx, sshDest, sshKey, imageTar, targetPath, version, command) + if err != nil { + return err + } + + return nil +} From 65ecc68d8b40299344238fbeb3605524b8c7fe7a Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 7 May 2024 22:31:00 +0900 Subject: [PATCH 007/273] =?UTF-8?q?fix:=20swagger=EC=97=90=EC=84=9C=20serv?= =?UTF-8?q?er=EA=B0=80=20https=EB=A1=9C=20=EC=84=A4=EC=A0=95=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/vook/server/api/ApiApplication.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/main/java/vook/server/api/ApiApplication.java b/api/src/main/java/vook/server/api/ApiApplication.java index f4b4a345..e74208e0 100644 --- a/api/src/main/java/vook/server/api/ApiApplication.java +++ b/api/src/main/java/vook/server/api/ApiApplication.java @@ -1,8 +1,11 @@ package vook.server.api; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.servers.Server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +@OpenAPIDefinition(servers = {@Server(url = "/", description = "Default Server URL")}) @SpringBootApplication public class ApiApplication { From 232930039d821a0ecf60d96565e8d9f93abc0ab7 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 8 May 2024 14:31:32 +0900 Subject: [PATCH 008/273] =?UTF-8?q?chore:=20dev=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=EC=9A=A9=20github=20action=20workflow=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 --- .github/workflows/deploy-dev.yml | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/deploy-dev.yml diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 00000000..1377923e --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,37 @@ +name: Deploy to Dev Environment + +on: + workflow_dispatch: + pull_request: + branches: + - develop + types: + - closed + +jobs: + deploy: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22.2' + - name: Deploy + uses: dagger/dagger-for-github@v5 + env: + SSH_DEST: ${{ secrets.DEV_SSH_DEST }} + SSH_KEY: ${{ secrets.DEV_SSH_KEY }} + with: + version: 0.11.2 + verb: call + module: ./dagger + args: >- + deploy + --source-dir=api + --ssh-dest=env:SSH_DEST + --ssh-key=env:SSH_KEY + --target-path=~/repo/makevook/vook-deployment/dev + --version=${{ github.sha }} + --command="make deploy-api" From 96864c6a943a3b0fe65abe0158c72175721e5db3 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 02:35:49 +0900 Subject: [PATCH 009/273] =?UTF-8?q?chore:=20CI=20=EB=8B=A8=EA=B3=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B9=8C=EB=93=9C=20=EC=A0=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=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 --- api/build.gradle | 3 +++ api/src/test/resources/application.yml | 3 +++ dagger/Makefile | 2 +- dagger/dagger.json | 2 +- dagger/main.go | 32 ++++++++++++++++++++------ 5 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 api/src/test/resources/application.yml diff --git a/api/build.gradle b/api/build.gradle index 0469bdf3..140ad5c5 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -41,6 +41,9 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.5.0' + + // h2 database + testRuntimeOnly 'com.h2database:h2' } bootJar { diff --git a/api/src/test/resources/application.yml b/api/src/test/resources/application.yml new file mode 100644 index 00000000..1648a9e0 --- /dev/null +++ b/api/src/test/resources/application.yml @@ -0,0 +1,3 @@ +spring: + datasource: + driver-class-name: org.h2.Driver diff --git a/dagger/Makefile b/dagger/Makefile index fa27db8d..45a280b6 100644 --- a/dagger/Makefile +++ b/dagger/Makefile @@ -12,5 +12,5 @@ deploy-dev: .PHONY:build build: - dagger call -v build-api-jar --dir=../api -o out/api.jar + dagger call -v build-api-jar --dir=../api --test -o out/api.jar dagger call -v build-api-image --jar-file=out/api.jar -o out/api_linux_arm64.tar diff --git a/dagger/dagger.json b/dagger/dagger.json index 3e613286..08ad7f56 100644 --- a/dagger/dagger.json +++ b/dagger/dagger.json @@ -16,5 +16,5 @@ } ], "source": ".", - "engineVersion": "v0.11.2" + "engineVersion": "v0.11.3" } diff --git a/dagger/main.go b/dagger/main.go index 3c2a3e44..70e0597a 100644 --- a/dagger/main.go +++ b/dagger/main.go @@ -2,16 +2,20 @@ package main import ( "context" + "errors" "fmt" ) type VookServer struct{} func (v *VookServer) BuildApiJar( + ctx context.Context, // 빌드 대상의 디렉토리 dir *Directory, -) *File { - f := dag.Container(). + // +optional + test bool, +) (*File, error) { + c := dag.Container(). From("eclipse-temurin:21-jdk"). WithWorkdir("/app"). WithDirectory("/app/gradle", dir.Directory("gradle")). @@ -21,11 +25,22 @@ func (v *VookServer) BuildApiJar( dir.File("settings.gradle"), }). WithExec([]string{"./gradlew"}). - WithDirectory("/app/src", dir.Directory("src")). + WithDirectory("/app/src", dir.Directory("src")) + + if test { + _, err := c. + WithExec([]string{"./gradlew", "test"}). + Sync(ctx) + if err != nil { + return nil, errors.New("test fail:" + err.Error()) + } + } + + jarFile := c. WithExec([]string{"./gradlew", "bootJar"}). File("jar/api.jar") - return f + return jarFile, nil } func (v *VookServer) BuildApiImage( @@ -54,7 +69,7 @@ ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=` + profile + `", "app.jar return dag.Docker(). Build(sourceDir, DockerBuildOpts{ - Platform: []string{"linux/arm64"}, + Platform: []Platform{"linux/arm64"}, }). Save(DockerBuildSaveOpts{ Name: "api", @@ -133,11 +148,14 @@ func (v *VookServer) Deploy( version string, command string, ) error { - jarFile := v.BuildApiJar(sourceDir) + jarFile, err := v.BuildApiJar(ctx, sourceDir, true) + if err != nil { + return err + } imageTar := v.BuildApiImage(jarFile, "default") - err := v.SendImage(ctx, sshDest, sshKey, targetPath, imageTar) + err = v.SendImage(ctx, sshDest, sshKey, targetPath, imageTar) if err != nil { return err } From 05a50024fd2a1004b75cf86bdd107ee6e689fe8d Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 18:40:22 +0900 Subject: [PATCH 010/273] =?UTF-8?q?chore:=20=EC=9D=98=EC=A1=B4=20dagger=20?= =?UTF-8?q?module=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dagger/dagger.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dagger/dagger.json b/dagger/dagger.json index 08ad7f56..cb142269 100644 --- a/dagger/dagger.json +++ b/dagger/dagger.json @@ -8,11 +8,11 @@ }, { "name": "scp", - "source": "github.com/seungyeop-lee/daggerverse/scp@2d67873bd965c09398a08034068ff43586537f76" + "source": "github.com/seungyeop-lee/daggerverse/scp@63e721cf63323ede8b1f11aeff00d41960699920" }, { "name": "ssh", - "source": "github.com/seungyeop-lee/daggerverse/ssh@f2a065a7441d08e18d34a12a5097a5335e663b69" + "source": "github.com/seungyeop-lee/daggerverse/ssh@63e721cf63323ede8b1f11aeff00d41960699920" } ], "source": ".", From 126b3129f89536a9dfdfc421864d6cbc9cf60751 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 18:40:54 +0900 Subject: [PATCH 011/273] =?UTF-8?q?fix:=20github=20actions=20workflow?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20dag?= =?UTF-8?q?ger=20version=EC=9D=84=200.11.3=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20#12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 1377923e..c679c5d1 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -24,7 +24,7 @@ jobs: SSH_DEST: ${{ secrets.DEV_SSH_DEST }} SSH_KEY: ${{ secrets.DEV_SSH_KEY }} with: - version: 0.11.2 + version: 0.11.3 verb: call module: ./dagger args: >- From 74881ae7ba856f251fb8dcc36d4d64dca9640096 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 18:58:38 +0900 Subject: [PATCH 012/273] =?UTF-8?q?feat:=20UTC=20=EC=84=A4=EC=A0=95=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/config/TimeZoneConfig.java | 17 +++++++++++++++++ api/src/main/resources/application.yml | 3 +++ .../vook/server/api/testhelper/ApiTest.java | 11 ++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/vook/server/api/config/TimeZoneConfig.java diff --git a/api/src/main/java/vook/server/api/config/TimeZoneConfig.java b/api/src/main/java/vook/server/api/config/TimeZoneConfig.java new file mode 100644 index 00000000..2c7ee3a0 --- /dev/null +++ b/api/src/main/java/vook/server/api/config/TimeZoneConfig.java @@ -0,0 +1,17 @@ +package vook.server.api.config; + +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Configuration; + +import java.util.TimeZone; + +@Configuration +public class TimeZoneConfig { + + public static final String DEFAULT_TIME_ZONE = "UTC"; + + @PostConstruct + public void init() { + TimeZone.setDefault(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); + } +} diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 84c373a2..b2b2e5f1 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -1,6 +1,9 @@ spring: profiles: default: local + jpa: + properties: + hibernate.jdbc.time_zone: UTC springdoc: swagger-ui: path: /swagger-ui.html diff --git a/api/src/test/java/vook/server/api/testhelper/ApiTest.java b/api/src/test/java/vook/server/api/testhelper/ApiTest.java index d4318400..3f7f4d30 100644 --- a/api/src/test/java/vook/server/api/testhelper/ApiTest.java +++ b/api/src/test/java/vook/server/api/testhelper/ApiTest.java @@ -1,13 +1,22 @@ package vook.server.api.testhelper; +import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import java.util.TimeZone; + +import static vook.server.api.config.TimeZoneConfig.DEFAULT_TIME_ZONE; + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class ApiTest { @Autowired protected TestRestTemplate rest; - + + @BeforeEach + void init() { + TimeZone.setDefault(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); + } } From 9626e661c798a5b794a5fa8d2e65c9b045753437 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 19:01:13 +0900 Subject: [PATCH 013/273] =?UTF-8?q?feat:=20BaseEntity=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/config/JpaConfig.java | 9 +++++++ .../vook/server/api/model/BaseEntity.java | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 api/src/main/java/vook/server/api/config/JpaConfig.java create mode 100644 api/src/main/java/vook/server/api/model/BaseEntity.java diff --git a/api/src/main/java/vook/server/api/config/JpaConfig.java b/api/src/main/java/vook/server/api/config/JpaConfig.java new file mode 100644 index 00000000..119d3612 --- /dev/null +++ b/api/src/main/java/vook/server/api/config/JpaConfig.java @@ -0,0 +1,9 @@ +package vook.server.api.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/api/src/main/java/vook/server/api/model/BaseEntity.java b/api/src/main/java/vook/server/api/model/BaseEntity.java new file mode 100644 index 00000000..c64f65ee --- /dev/null +++ b/api/src/main/java/vook/server/api/model/BaseEntity.java @@ -0,0 +1,24 @@ +package vook.server.api.model; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} From f8cdcb474470d10353de4421186a3aa655eba754 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 19:10:01 +0900 Subject: [PATCH 014/273] =?UTF-8?q?feat:=20Member=20Model=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/vook/server/api/model/Member.java | 22 +++++++++++++++++++ .../server/api/model/MemberRepository.java | 6 +++++ 2 files changed, 28 insertions(+) create mode 100644 api/src/main/java/vook/server/api/model/Member.java create mode 100644 api/src/main/java/vook/server/api/model/MemberRepository.java diff --git a/api/src/main/java/vook/server/api/model/Member.java b/api/src/main/java/vook/server/api/model/Member.java new file mode 100644 index 00000000..b5e75a50 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/Member.java @@ -0,0 +1,22 @@ +package vook.server.api.model; + +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "member") +public class Member extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + public static Member forCreateOf(String name) { + Member result = new Member(); + result.name = name; + return result; + } +} diff --git a/api/src/main/java/vook/server/api/model/MemberRepository.java b/api/src/main/java/vook/server/api/model/MemberRepository.java new file mode 100644 index 00000000..e1b73083 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/MemberRepository.java @@ -0,0 +1,6 @@ +package vook.server.api.model; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { +} From 00fc717ce34a064f5b5720d34cce01f06a5237ad Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 19:13:11 +0900 Subject: [PATCH 015/273] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=B6=94=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개발 용이성 향상을 위해 초기화 서비스를 추가 --- .../server/api/devhelper/InitService.java | 21 ++++++++++++++ .../vook/server/api/devhelper/LocalInit.java | 29 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 api/src/main/java/vook/server/api/devhelper/InitService.java create mode 100644 api/src/main/java/vook/server/api/devhelper/LocalInit.java diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java new file mode 100644 index 00000000..cb44e603 --- /dev/null +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -0,0 +1,21 @@ +package vook.server.api.devhelper; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.model.Member; +import vook.server.api.model.MemberRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class InitService { + + private final MemberRepository memberRepository; + + public void init() { + memberRepository.deleteAllInBatch(); + + memberRepository.save(Member.forCreateOf("vook")); + } +} diff --git a/api/src/main/java/vook/server/api/devhelper/LocalInit.java b/api/src/main/java/vook/server/api/devhelper/LocalInit.java new file mode 100644 index 00000000..dee48e23 --- /dev/null +++ b/api/src/main/java/vook/server/api/devhelper/LocalInit.java @@ -0,0 +1,29 @@ +package vook.server.api.devhelper; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import vook.server.api.model.MemberRepository; + +@Slf4j +@Profile("local") +@Component +@RequiredArgsConstructor +public class LocalInit { + + private final MemberRepository memberRepository; + private final InitService initService; + + @PostConstruct + public void init() { + if (memberRepository.count() > 0) { + return; + } + + initService.init(); + + log.info("초기화 완료"); + } +} From b8c63e190b76fc996324d73241c869d1dbf88816 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 19:15:45 +0900 Subject: [PATCH 016/273] =?UTF-8?q?feat:=20Glossarcy=20Model=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/vook/server/api/model/Glossary.java | 34 +++++++++++++++++++ .../server/api/model/GlossaryRepository.java | 6 ++++ 2 files changed, 40 insertions(+) create mode 100644 api/src/main/java/vook/server/api/model/Glossary.java create mode 100644 api/src/main/java/vook/server/api/model/GlossaryRepository.java diff --git a/api/src/main/java/vook/server/api/model/Glossary.java b/api/src/main/java/vook/server/api/model/Glossary.java new file mode 100644 index 00000000..68a7c3b1 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/Glossary.java @@ -0,0 +1,34 @@ +package vook.server.api.model; + +import jakarta.persistence.*; +import lombok.Getter; + +import java.util.UUID; + +@Getter +@Entity +@Table(name = "glossary") +public class Glossary extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String uid; + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + public static Glossary forCreateOf( + String name, + Member member + ) { + Glossary result = new Glossary(); + result.uid = UUID.randomUUID().toString(); + result.name = name; + result.member = member; + return result; + } +} diff --git a/api/src/main/java/vook/server/api/model/GlossaryRepository.java b/api/src/main/java/vook/server/api/model/GlossaryRepository.java new file mode 100644 index 00000000..9fb255ac --- /dev/null +++ b/api/src/main/java/vook/server/api/model/GlossaryRepository.java @@ -0,0 +1,6 @@ +package vook.server.api.model; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface GlossaryRepository extends JpaRepository { +} From f91373e0516aec48db893a5b9521151c3c2814a1 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 9 May 2024 19:26:32 +0900 Subject: [PATCH 017/273] =?UTF-8?q?feat:=20Term=20Model=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/model/Term.java | 42 +++++++++++++++++++ .../vook/server/api/model/TermSynonym.java | 31 ++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 api/src/main/java/vook/server/api/model/Term.java create mode 100644 api/src/main/java/vook/server/api/model/TermSynonym.java diff --git a/api/src/main/java/vook/server/api/model/Term.java b/api/src/main/java/vook/server/api/model/Term.java new file mode 100644 index 00000000..c48867a3 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/Term.java @@ -0,0 +1,42 @@ +package vook.server.api.model; + +import jakarta.persistence.*; +import lombok.Getter; + +import java.util.List; + +@Getter +@Entity +@Table(name = "term") +public class Term extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String term; + private String meaning; + + @ManyToOne + @JoinColumn(name = "glossary_id", nullable = false) + private Glossary glossary; + + @OneToMany(mappedBy = "term", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private List synonym; + + public static Term forCreateOf( + String term, + String meaning, + Glossary glossary + ) { + Term result = new Term(); + result.term = term; + result.meaning = meaning; + result.glossary = glossary; + return result; + } + + public void addSynonym(TermSynonym termSynonym) { + synonym.add(termSynonym); + } +} diff --git a/api/src/main/java/vook/server/api/model/TermSynonym.java b/api/src/main/java/vook/server/api/model/TermSynonym.java new file mode 100644 index 00000000..fe4a019d --- /dev/null +++ b/api/src/main/java/vook/server/api/model/TermSynonym.java @@ -0,0 +1,31 @@ +package vook.server.api.model; + +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "term_synonym") +public class TermSynonym { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String synonym; + + @ManyToOne + @JoinColumn(name = "term_id", nullable = false) + private Term term; + + public static TermSynonym forCreateOf( + String synonym, + Term term + ) { + TermSynonym result = new TermSynonym(); + result.synonym = synonym; + result.term = term; + term.addSynonym(result); + return result; + } +} From 453a66c77f7c44ec08b1b08730db707349ae20b7 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 07:36:56 +0900 Subject: [PATCH 018/273] =?UTF-8?q?chore:=20dagger=20version=200.11.4?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20#16?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-dev.yml | 2 +- dagger/dagger.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index c679c5d1..01d09668 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -24,7 +24,7 @@ jobs: SSH_DEST: ${{ secrets.DEV_SSH_DEST }} SSH_KEY: ${{ secrets.DEV_SSH_KEY }} with: - version: 0.11.3 + version: 0.11.4 verb: call module: ./dagger args: >- diff --git a/dagger/dagger.json b/dagger/dagger.json index cb142269..74a58ee9 100644 --- a/dagger/dagger.json +++ b/dagger/dagger.json @@ -16,5 +16,5 @@ } ], "source": ".", - "engineVersion": "v0.11.3" + "engineVersion": "v0.11.4" } From 6c75a8b1e0253fb50ca9874ff2712b54e2cdeaee Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 08:18:13 +0900 Subject: [PATCH 019/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=86=8D=EC=84=B1=20=EC=A0=9C=EC=95=BD=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=A0=81=EC=9A=A9=20#20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/vook/server/api/model/Term.java | 3 +++ api/src/main/java/vook/server/api/model/TermSynonym.java | 1 + 2 files changed, 4 insertions(+) diff --git a/api/src/main/java/vook/server/api/model/Term.java b/api/src/main/java/vook/server/api/model/Term.java index c48867a3..2faa0e85 100644 --- a/api/src/main/java/vook/server/api/model/Term.java +++ b/api/src/main/java/vook/server/api/model/Term.java @@ -14,7 +14,10 @@ public class Term extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(length = 100, nullable = false) private String term; + + @Column(length = 2000, nullable = false) private String meaning; @ManyToOne diff --git a/api/src/main/java/vook/server/api/model/TermSynonym.java b/api/src/main/java/vook/server/api/model/TermSynonym.java index fe4a019d..7a97867b 100644 --- a/api/src/main/java/vook/server/api/model/TermSynonym.java +++ b/api/src/main/java/vook/server/api/model/TermSynonym.java @@ -12,6 +12,7 @@ public class TermSynonym { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(length = 100, nullable = false) private String synonym; @ManyToOne From f8a10a1b5b5d99b229e55c18136bcf8314e01678 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 08:23:01 +0900 Subject: [PATCH 020/273] =?UTF-8?q?chore:=20devenv=EC=9D=98=20=EB=A9=94?= =?UTF-8?q?=EB=AA=A8=EB=A6=AC=20=EC=A0=9C=ED=95=9C=20=EB=A7=9F=20=EC=9E=AC?= =?UTF-8?q?=EB=B6=80=ED=8C=85=20=ED=9B=84=20=EA=BA=BC=EC=A7=90=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20#19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devenv/db/compose.yml | 3 ++- devenv/meilisearch/compose.yml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/devenv/db/compose.yml b/devenv/db/compose.yml index 1ffefea0..8eef0be1 100644 --- a/devenv/db/compose.yml +++ b/devenv/db/compose.yml @@ -15,7 +15,8 @@ services: TZ: Asia/Seoul ports: - "3307:3306" - restart: always + mem_limit: 300m + restart: unless-stopped volumes: db-data: {} diff --git a/devenv/meilisearch/compose.yml b/devenv/meilisearch/compose.yml index b95dc0a7..21813543 100644 --- a/devenv/meilisearch/compose.yml +++ b/devenv/meilisearch/compose.yml @@ -8,6 +8,8 @@ services: environment: - MEILI_ENV=development - MEILI_MASTER_KEY=aSampleMasterKey + mem_limit: 300m + restart: unless-stopped volumes: meili_data: {} From 3dedd72d287203a24a14eb67b9d8eba8edbd5cea Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 10:20:26 +0900 Subject: [PATCH 021/273] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20ApiRespons?= =?UTF-8?q?e=20=EC=B6=94=EA=B0=80=20#21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/web/common/CommonApiException.java | 50 +++++++++++++++++++ .../api/web/common/CommonApiResponse.java | 34 +++++++++++++ .../common/GlobalRestControllerAdvice.java | 29 +++++++++++ 3 files changed, 113 insertions(+) create mode 100644 api/src/main/java/vook/server/api/web/common/CommonApiException.java create mode 100644 api/src/main/java/vook/server/api/web/common/CommonApiResponse.java create mode 100644 api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiException.java b/api/src/main/java/vook/server/api/web/common/CommonApiException.java new file mode 100644 index 00000000..89f74040 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/common/CommonApiException.java @@ -0,0 +1,50 @@ +package vook.server.api.web.common; + +public class CommonApiException { + public static abstract class Exception extends RuntimeException { + protected String message; + + public Exception(Throwable cause) { + super(cause); + this.message = cause.getMessage(); + } + + public Exception(String message) { + super(message); + this.message = message; + } + + public Exception(String message, Throwable cause) { + super(message, cause); + this.message = message; + } + + abstract CommonApiResponse response(); + } + + public static class BadRequest extends Exception { + public BadRequest(String message, Throwable cause) { + super(message, cause); + } + + public BadRequest(String message) { + super(message); + } + + @Override + public CommonApiResponse response() { + return CommonApiResponse.noResult(400, message); + } + } + + public static class ServerError extends Exception { + public ServerError(Throwable cause) { + super(cause); + } + + @Override + public CommonApiResponse response() { + return CommonApiResponse.noResult(500, "처리되지 않은 서버 에러가 발생하였습니다."); + } + } +} diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java new file mode 100644 index 00000000..9a6559f7 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java @@ -0,0 +1,34 @@ +package vook.server.api.web.common; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CommonApiResponse { + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private Integer code; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + private String message; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private T result; + + public static CommonApiResponse ok() { + return noResult(200, "API 요청이 성공했습니다."); + } + + public static CommonApiResponse okWithResult(T result) { + CommonApiResponse response = ok(); + response.result = result; + return response; + } + + public static CommonApiResponse noResult(Integer code, String message) { + CommonApiResponse response = new CommonApiResponse<>(); + response.code = code; + response.message = message; + return response; + } +} diff --git a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java new file mode 100644 index 00000000..ba609631 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java @@ -0,0 +1,29 @@ +package vook.server.api.web.common; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalRestControllerAdvice { + + @ExceptionHandler(CommonApiException.Exception.class) + public ResponseEntity handleCommonApiException(CommonApiException.Exception e) { + log.error(e.getMessage(), e); + + CommonApiResponse response = e.response(); + + return ResponseEntity.status(response.getCode()).body(response); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.error(e.getMessage(), e); + + CommonApiResponse response = new CommonApiException.ServerError(e).response(); + + return ResponseEntity.status(response.getCode()).body(response); + } +} From 59b8d3cc83b9e362da45b5359747f58f47c68f06 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 08:33:49 +0900 Subject: [PATCH 022/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20#18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/http/glossary.http | 2 ++ .../vook/server/api/app/GlossaryService.java | 19 ++++++++++++++ .../server/api/devhelper/InitService.java | 11 +++++++- .../glossary/GlossaryRestController.java | 24 +++++++++++++++++ .../routes/glossary/GlossaryWebService.java | 21 +++++++++++++++ .../web/routes/glossary/RetrieveResponse.java | 26 +++++++++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 api/http/glossary.http create mode 100644 api/src/main/java/vook/server/api/app/GlossaryService.java create mode 100644 api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java create mode 100644 api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java create mode 100644 api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java diff --git a/api/http/glossary.http b/api/http/glossary.http new file mode 100644 index 00000000..3ec6c218 --- /dev/null +++ b/api/http/glossary.http @@ -0,0 +1,2 @@ +# 용어집 리스트 +GET http://localhost:8080/glossary diff --git a/api/src/main/java/vook/server/api/app/GlossaryService.java b/api/src/main/java/vook/server/api/app/GlossaryService.java new file mode 100644 index 00000000..555abab0 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/GlossaryService.java @@ -0,0 +1,19 @@ +package vook.server.api.app; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import vook.server.api.model.Glossary; +import vook.server.api.model.GlossaryRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class GlossaryService { + + private final GlossaryRepository repository; + + public List findAll() { + return repository.findAll(); + } +} diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index cb44e603..6f4f3abb 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -3,6 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import vook.server.api.model.Glossary; +import vook.server.api.model.GlossaryRepository; import vook.server.api.model.Member; import vook.server.api.model.MemberRepository; @@ -12,10 +14,17 @@ public class InitService { private final MemberRepository memberRepository; + private final GlossaryRepository glossaryRepository; public void init() { + glossaryRepository.deleteAllInBatch(); memberRepository.deleteAllInBatch(); - memberRepository.save(Member.forCreateOf("vook")); + Member vook = memberRepository.save(Member.forCreateOf("vook")); + + glossaryRepository.save(Glossary.forCreateOf("개발", vook)); + glossaryRepository.save(Glossary.forCreateOf("디자인", vook)); + glossaryRepository.save(Glossary.forCreateOf("마케팅", vook)); + glossaryRepository.save(Glossary.forCreateOf("실무", vook)); } } diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java new file mode 100644 index 00000000..b7299552 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java @@ -0,0 +1,24 @@ +package vook.server.api.web.routes.glossary; + +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 vook.server.api.model.Glossary; +import vook.server.api.web.common.CommonApiResponse; + +import java.util.List; + +@RestController +@RequestMapping("/glossary") +@RequiredArgsConstructor +public class GlossaryRestController { + + private final GlossaryWebService service; + + @GetMapping + public CommonApiResponse> retrieve() { + List result = service.retrieve(); + return CommonApiResponse.okWithResult(RetrieveResponse.from(result)); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java new file mode 100644 index 00000000..627c6eb0 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java @@ -0,0 +1,21 @@ +package vook.server.api.web.routes.glossary; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.GlossaryService; +import vook.server.api.model.Glossary; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class GlossaryWebService { + + private final GlossaryService glossaryService; + + public List retrieve() { + return glossaryService.findAll(); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java b/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java new file mode 100644 index 00000000..42d4bf1d --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java @@ -0,0 +1,26 @@ +package vook.server.api.web.routes.glossary; + +import lombok.Getter; +import vook.server.api.model.Glossary; + +import java.util.List; + +@Getter +public class RetrieveResponse { + + private String uid; + private String name; + + public static List from(List glossaries) { + return glossaries.stream() + .map(RetrieveResponse::from) + .toList(); + } + + public static RetrieveResponse from(Glossary glossary) { + RetrieveResponse response = new RetrieveResponse(); + response.uid = glossary.getUid(); + response.name = glossary.getName(); + return response; + } +} From 6cd0c56b1b93d79fa2ebe352ed9350dbd8b6919b Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 11:26:23 +0900 Subject: [PATCH 023/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20Swagger=20#18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/vook/server/api/ApiApplication.java | 3 -- .../vook/server/api/config/SwaggerConfig.java | 22 ++++++++++ .../swagger/GlobalOpenApiCustomizerImpl.java | 24 +++++++++++ .../GlobalOperationCustomizerImpl.java | 41 +++++++++++++++++++ .../server/api/swagger/OpenApiDefinition.java | 8 ++++ .../api/web/common/CommonApiResponse.java | 5 +-- .../api/web/routes/glossary/GlossaryApi.java | 31 ++++++++++++++ .../glossary/GlossaryRestController.java | 3 +- .../web/routes/glossary/RetrieveResponse.java | 3 ++ 9 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 api/src/main/java/vook/server/api/config/SwaggerConfig.java create mode 100644 api/src/main/java/vook/server/api/swagger/GlobalOpenApiCustomizerImpl.java create mode 100644 api/src/main/java/vook/server/api/swagger/GlobalOperationCustomizerImpl.java create mode 100644 api/src/main/java/vook/server/api/swagger/OpenApiDefinition.java create mode 100644 api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java diff --git a/api/src/main/java/vook/server/api/ApiApplication.java b/api/src/main/java/vook/server/api/ApiApplication.java index e74208e0..f4b4a345 100644 --- a/api/src/main/java/vook/server/api/ApiApplication.java +++ b/api/src/main/java/vook/server/api/ApiApplication.java @@ -1,11 +1,8 @@ package vook.server.api; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.servers.Server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@OpenAPIDefinition(servers = {@Server(url = "/", description = "Default Server URL")}) @SpringBootApplication public class ApiApplication { diff --git a/api/src/main/java/vook/server/api/config/SwaggerConfig.java b/api/src/main/java/vook/server/api/config/SwaggerConfig.java new file mode 100644 index 00000000..54057a5c --- /dev/null +++ b/api/src/main/java/vook/server/api/config/SwaggerConfig.java @@ -0,0 +1,22 @@ +package vook.server.api.config; + +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springdoc.core.customizers.GlobalOperationCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import vook.server.api.swagger.GlobalOpenApiCustomizerImpl; +import vook.server.api.swagger.GlobalOperationCustomizerImpl; + +@Configuration +public class SwaggerConfig { + + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return new GlobalOpenApiCustomizerImpl(); + } + + @Bean + public GlobalOperationCustomizer globalOperationCustomizer() { + return new GlobalOperationCustomizerImpl(); + } +} diff --git a/api/src/main/java/vook/server/api/swagger/GlobalOpenApiCustomizerImpl.java b/api/src/main/java/vook/server/api/swagger/GlobalOpenApiCustomizerImpl.java new file mode 100644 index 00000000..4e37fa34 --- /dev/null +++ b/api/src/main/java/vook/server/api/swagger/GlobalOpenApiCustomizerImpl.java @@ -0,0 +1,24 @@ +package vook.server.api.swagger; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.IntegerSchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; + +import java.util.Map; + +public class GlobalOpenApiCustomizerImpl implements GlobalOpenApiCustomizer { + @Override + public void customise(OpenAPI openApi) { + applyCommonApiResponseSchema(openApi); + } + + private static void applyCommonApiResponseSchema(OpenAPI openApi) { + openApi.getComponents() + .addSchemas("CommonApiResponse", new Schema>() + .addProperty("code", new IntegerSchema().description("응답 코드")) + .addProperty("message", new StringSchema().description("응답 메시지")) + ); + } +} diff --git a/api/src/main/java/vook/server/api/swagger/GlobalOperationCustomizerImpl.java b/api/src/main/java/vook/server/api/swagger/GlobalOperationCustomizerImpl.java new file mode 100644 index 00000000..a5ee1766 --- /dev/null +++ b/api/src/main/java/vook/server/api/swagger/GlobalOperationCustomizerImpl.java @@ -0,0 +1,41 @@ +package vook.server.api.swagger; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.examples.Example; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.responses.ApiResponse; +import org.springdoc.core.customizers.GlobalOperationCustomizer; +import org.springframework.web.method.HandlerMethod; +import vook.server.api.web.common.CommonApiResponse; + +import java.util.Map; + +public class GlobalOperationCustomizerImpl implements GlobalOperationCustomizer { + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + applyInternalServerErrorApiResponse(operation); + return operation; + } + + private static void applyInternalServerErrorApiResponse(Operation operation) { + ApiResponse internalServerErrorApiResponse = new ApiResponse() + .description("처리되지 않은 서버 에러") + .content(new Content().addMediaType( + "application/json", + new MediaType() + .schema(new Schema().$ref("#/components/schemas/CommonApiResponse")) + .examples(Map.of("서버 에러", new Example().value( + """ + { + "code": 500, + "message": "처리되지 않은 서버 에러가 발생하였습니다." + }""" + ))) + )); + + operation.getResponses().addApiResponse("500", internalServerErrorApiResponse); + } +} diff --git a/api/src/main/java/vook/server/api/swagger/OpenApiDefinition.java b/api/src/main/java/vook/server/api/swagger/OpenApiDefinition.java new file mode 100644 index 00000000..ec9fd2c5 --- /dev/null +++ b/api/src/main/java/vook/server/api/swagger/OpenApiDefinition.java @@ -0,0 +1,8 @@ +package vook.server.api.swagger; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.servers.Server; + +@OpenAPIDefinition(servers = {@Server(url = "/", description = "Default Server URL")}) +public class OpenApiDefinition { +} diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java index 9a6559f7..39bfb69d 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java @@ -8,11 +8,10 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CommonApiResponse { - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "응답 코드") private Integer code; - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "응답 메시지") private String message; - @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) private T result; public static CommonApiResponse ok() { diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java new file mode 100644 index 00000000..ae45239e --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java @@ -0,0 +1,31 @@ +package vook.server.api.web.routes.glossary; + +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import vook.server.api.web.common.CommonApiResponse; + +import java.util.List; + +@Tag(name = "glossary", description = "용어집 API") +public interface GlossaryApi { + + @Operation(summary = "용어집 리스트 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = RetrieveApiResponse.class) + ) + ), + }) + CommonApiResponse> retrieve(); + + class RetrieveApiResponse extends CommonApiResponse> { + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java index b7299552..b696d399 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java @@ -12,10 +12,11 @@ @RestController @RequestMapping("/glossary") @RequiredArgsConstructor -public class GlossaryRestController { +public class GlossaryRestController implements GlossaryApi { private final GlossaryWebService service; + @Override @GetMapping public CommonApiResponse> retrieve() { List result = service.retrieve(); diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java b/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java index 42d4bf1d..789b94fc 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java @@ -1,5 +1,6 @@ package vook.server.api.web.routes.glossary; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import vook.server.api.model.Glossary; @@ -8,7 +9,9 @@ @Getter public class RetrieveResponse { + @Schema(description = "용어집 UID", examples = "38617d11-1d8e-4f77-a2fd-8cdca9de8420") private String uid; + @Schema(description = "용어집 이름", examples = "실무") private String name; public static List from(List glossaries) { From 6f1dc7165dcc69a591d9a9e7233d5c910b2d83e8 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 21:07:50 +0900 Subject: [PATCH 024/273] =?UTF-8?q?chore:=20dagger=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 폴더 이름을 dagger -> cicd로 변경 dagger내 설정파일과 동작스크립트 분리: 초기화 속도 향상을 위해 --- .github/workflows/deploy-dev.yml | 2 +- .gitignore | 2 -- cicd/.gitignore | 2 ++ {dagger => cicd}/Makefile | 0 {dagger => cicd}/dagger.json | 2 +- {dagger => cicd/dagger}/.gitattributes | 0 {dagger => cicd/dagger}/.gitignore | 0 {dagger => cicd/dagger}/go.mod | 2 +- {dagger => cicd/dagger}/go.sum | 0 {dagger => cicd/dagger}/main.go | 0 10 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 cicd/.gitignore rename {dagger => cicd}/Makefile (100%) rename {dagger => cicd}/dagger.json (95%) rename {dagger => cicd/dagger}/.gitattributes (100%) rename {dagger => cicd/dagger}/.gitignore (100%) rename {dagger => cicd/dagger}/go.mod (98%) rename {dagger => cicd/dagger}/go.sum (100%) rename {dagger => cicd/dagger}/main.go (100%) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 01d09668..c6fc97d7 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -26,7 +26,7 @@ jobs: with: version: 0.11.4 verb: call - module: ./dagger + module: ./cicd args: >- deploy --source-dir=api diff --git a/.gitignore b/.gitignore index ec16e763..9f11b755 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ .idea/ -dagger/secrets -dagger/out diff --git a/cicd/.gitignore b/cicd/.gitignore new file mode 100644 index 00000000..123d4997 --- /dev/null +++ b/cicd/.gitignore @@ -0,0 +1,2 @@ +/secrets +/out diff --git a/dagger/Makefile b/cicd/Makefile similarity index 100% rename from dagger/Makefile rename to cicd/Makefile diff --git a/dagger/dagger.json b/cicd/dagger.json similarity index 95% rename from dagger/dagger.json rename to cicd/dagger.json index 74a58ee9..f80612a1 100644 --- a/dagger/dagger.json +++ b/cicd/dagger.json @@ -15,6 +15,6 @@ "source": "github.com/seungyeop-lee/daggerverse/ssh@63e721cf63323ede8b1f11aeff00d41960699920" } ], - "source": ".", + "source": "dagger", "engineVersion": "v0.11.4" } diff --git a/dagger/.gitattributes b/cicd/dagger/.gitattributes similarity index 100% rename from dagger/.gitattributes rename to cicd/dagger/.gitattributes diff --git a/dagger/.gitignore b/cicd/dagger/.gitignore similarity index 100% rename from dagger/.gitignore rename to cicd/dagger/.gitignore diff --git a/dagger/go.mod b/cicd/dagger/go.mod similarity index 98% rename from dagger/go.mod rename to cicd/dagger/go.mod index b8955f99..7ede703c 100644 --- a/dagger/go.mod +++ b/cicd/dagger/go.mod @@ -1,4 +1,4 @@ -module dagger/dagger +module dagger/vook-server go 1.22.2 diff --git a/dagger/go.sum b/cicd/dagger/go.sum similarity index 100% rename from dagger/go.sum rename to cicd/dagger/go.sum diff --git a/dagger/main.go b/cicd/dagger/main.go similarity index 100% rename from dagger/main.go rename to cicd/dagger/main.go From 9c0a364b92085d04ff293720c371ee7cc0ed3c8c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 22:38:30 +0900 Subject: [PATCH 025/273] =?UTF-8?q?feat:=20glossary=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EC=8B=9C=20meilisearch=EC=97=90=20index=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20#25?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/devhelper/InitService.java | 18 ++++-- .../outbound/search/MeilisearchService.java | 55 +++++++++++++++++++ .../api/outbound/search/SearchClearable.java | 5 ++ .../api/outbound/search/SearchService.java | 7 +++ 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java create mode 100644 api/src/main/java/vook/server/api/outbound/search/SearchClearable.java create mode 100644 api/src/main/java/vook/server/api/outbound/search/SearchService.java diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 6f4f3abb..db96e29f 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -7,6 +7,8 @@ import vook.server.api.model.GlossaryRepository; import vook.server.api.model.Member; import vook.server.api.model.MemberRepository; +import vook.server.api.outbound.search.SearchClearable; +import vook.server.api.outbound.search.SearchService; @Service @Transactional @@ -15,16 +17,24 @@ public class InitService { private final MemberRepository memberRepository; private final GlossaryRepository glossaryRepository; + private final SearchClearable searchClearable; + + private final SearchService searchService; public void init() { glossaryRepository.deleteAllInBatch(); memberRepository.deleteAllInBatch(); + searchClearable.clearAll(); Member vook = memberRepository.save(Member.forCreateOf("vook")); - glossaryRepository.save(Glossary.forCreateOf("개발", vook)); - glossaryRepository.save(Glossary.forCreateOf("디자인", vook)); - glossaryRepository.save(Glossary.forCreateOf("마케팅", vook)); - glossaryRepository.save(Glossary.forCreateOf("실무", vook)); + Glossary devGlossary = glossaryRepository.save(Glossary.forCreateOf("개발", vook)); + searchService.createGlossary(devGlossary); + Glossary designGlossary = glossaryRepository.save(Glossary.forCreateOf("디자인", vook)); + searchService.createGlossary(designGlossary); + Glossary marketingGlossary = glossaryRepository.save(Glossary.forCreateOf("마케팅", vook)); + searchService.createGlossary(marketingGlossary); + Glossary practiceGlossary = glossaryRepository.save(Glossary.forCreateOf("실무", vook)); + searchService.createGlossary(practiceGlossary); } } diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java new file mode 100644 index 00000000..9a3d7ef8 --- /dev/null +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -0,0 +1,55 @@ +package vook.server.api.outbound.search; + +import com.meilisearch.sdk.Client; +import com.meilisearch.sdk.Config; +import com.meilisearch.sdk.Index; +import com.meilisearch.sdk.model.IndexesQuery; +import com.meilisearch.sdk.model.Results; +import com.meilisearch.sdk.model.TaskInfo; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import vook.server.api.model.Glossary; + +import java.util.Arrays; + +@Service +@RequiredArgsConstructor +public class MeilisearchService implements SearchService, SearchClearable { + + @Value("${service.meilisearch.host:}") + private String host; + + @Value("${service.meilisearch.apiKey:}") + private String apiKey; + + private Client client; + + @PostConstruct + public void postConstruct() { + this.client = new Client(new Config(host, apiKey)); + } + + @Override + public void clearAll() { + Results indexes = client.getIndexes(new IndexesQuery() {{ + setLimit(Integer.MAX_VALUE); + }}); + Arrays.stream(indexes.getResults()).forEach(index -> { + client.deleteIndex(index.getUid()); + }); + } + + @Override + public void createGlossary(Glossary glossary) { + TaskInfo index = client.createIndex(getIndexUid(glossary)); + client.waitForTask(index.getTaskUid()); + } + + @NotNull + private static String getIndexUid(Glossary glossary) { + return glossary.getUid(); + } +} diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchClearable.java b/api/src/main/java/vook/server/api/outbound/search/SearchClearable.java new file mode 100644 index 00000000..cc17a6e1 --- /dev/null +++ b/api/src/main/java/vook/server/api/outbound/search/SearchClearable.java @@ -0,0 +1,5 @@ +package vook.server.api.outbound.search; + +public interface SearchClearable { + void clearAll(); +} diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchService.java b/api/src/main/java/vook/server/api/outbound/search/SearchService.java new file mode 100644 index 00000000..4849592d --- /dev/null +++ b/api/src/main/java/vook/server/api/outbound/search/SearchService.java @@ -0,0 +1,7 @@ +package vook.server.api.outbound.search; + +import vook.server.api.model.Glossary; + +public interface SearchService { + void createGlossary(Glossary glossary); +} From f90e0e25f2f7c9b04b018113ddf4ec1feaffd9cb Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 22:42:29 +0900 Subject: [PATCH 026/273] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=ED=99=94=20A?= =?UTF-8?q?PI=20=EC=B6=94=EA=B0=80=20#26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 개발 편의성을 위해 추가 --- api/http/init.http | 2 ++ .../server/api/web/routes/init/InitApi.java | 19 ++++++++++++++++ .../api/web/routes/init/InitController.java | 22 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 api/http/init.http create mode 100644 api/src/main/java/vook/server/api/web/routes/init/InitApi.java create mode 100644 api/src/main/java/vook/server/api/web/routes/init/InitController.java diff --git a/api/http/init.http b/api/http/init.http new file mode 100644 index 00000000..6da2bc2e --- /dev/null +++ b/api/http/init.http @@ -0,0 +1,2 @@ +### DB, Meilisearch 초기화 +POST http://localhost:8080/init diff --git a/api/src/main/java/vook/server/api/web/routes/init/InitApi.java b/api/src/main/java/vook/server/api/web/routes/init/InitApi.java new file mode 100644 index 00000000..2ae0ebe2 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/init/InitApi.java @@ -0,0 +1,19 @@ +package vook.server.api.web.routes.init; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "init", description = "초기화 API") +public interface InitApi { + + @Operation(summary = "DB, Meilisearch 데이터 초기화") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공" + ), + }) + void init(); +} diff --git a/api/src/main/java/vook/server/api/web/routes/init/InitController.java b/api/src/main/java/vook/server/api/web/routes/init/InitController.java new file mode 100644 index 00000000..dfc74f19 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/init/InitController.java @@ -0,0 +1,22 @@ +package vook.server.api.web.routes.init; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import vook.server.api.devhelper.InitService; + +@Profile({"local", "dev"}) +@RestController +@RequestMapping("/init") +@RequiredArgsConstructor +public class InitController implements InitApi { + + private final InitService initService; + + @PostMapping + public void init() { + initService.init(); + } +} From b41a9911ed1e5d224f0d6ee2b9fa2c9cbc7e5a5a Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 22:47:53 +0900 Subject: [PATCH 027/273] =?UTF-8?q?feat:=20=EB=B0=B0=ED=8F=AC=20=EC=8B=9C?= =?UTF-8?q?=20=EB=B0=B0=ED=8F=AC=20=ED=99=98=EA=B2=BD=20=EB=B3=84=20profil?= =?UTF-8?q?e=20=EC=B6=94=EA=B0=80=20#27?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-dev.yml | 1 + cicd/Makefile | 1 + cicd/dagger/main.go | 12 +++++++----- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index c6fc97d7..6976effb 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -30,6 +30,7 @@ jobs: args: >- deploy --source-dir=api + --profile=dev --ssh-dest=env:SSH_DEST --ssh-key=env:SSH_KEY --target-path=~/repo/makevook/vook-deployment/dev diff --git a/cicd/Makefile b/cicd/Makefile index 45a280b6..11d4c6c8 100644 --- a/cicd/Makefile +++ b/cicd/Makefile @@ -4,6 +4,7 @@ VERSION := $(shell git describe --tags --always --dirty) deploy-dev: dagger call -v deploy \ --source-dir=../api \ + --profile=dev \ --ssh-dest=file:./secrets/dev/dest.txt \ --ssh-key=file:./secrets/dev/ssh.key \ --target-path=~/repo/makevook/vook-deployment/dev \ diff --git a/cicd/dagger/main.go b/cicd/dagger/main.go index 70e0597a..b498ed42 100644 --- a/cicd/dagger/main.go +++ b/cicd/dagger/main.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" ) type VookServer struct{} @@ -48,10 +49,10 @@ func (v *VookServer) BuildApiImage( jarFile *File, // profile // +optional - profile string, + profile []string, ) *File { - if profile == "" { - profile = "default" + if profile == nil { + profile = []string{"default"} } dockerfile := ` @@ -61,7 +62,7 @@ WORKDIR /app COPY app.jar app.jar -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=` + profile + `", "app.jar"] +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=` + strings.Join(profile, ",") + `", "app.jar"] ` sourceDir := dag.Directory(). WithFile("app.jar", jarFile). @@ -142,6 +143,7 @@ FILENAME=%s VERSION=%s %s func (v *VookServer) Deploy( ctx context.Context, sourceDir *Directory, + profile string, sshDest *Secret, sshKey *Secret, targetPath string, @@ -153,7 +155,7 @@ func (v *VookServer) Deploy( return err } - imageTar := v.BuildApiImage(jarFile, "default") + imageTar := v.BuildApiImage(jarFile, []string{"default", profile}) err = v.SendImage(ctx, sshDest, sshKey, targetPath, imageTar) if err != nil { From 120e924f71ccb3a519746ab015f79102bdc28a21 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 10 May 2024 22:54:20 +0900 Subject: [PATCH 028/273] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=EC=97=90=EC=84=9C=20sshDest=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0=20#27?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dagger 0.11.3 버전에서 secret 값에 대한 공백 제거가 제거되었으므로, 배포 스크립트에서 공백 제거 로직을 추가 --- cicd/dagger/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cicd/dagger/main.go b/cicd/dagger/main.go index b498ed42..e81a508e 100644 --- a/cicd/dagger/main.go +++ b/cicd/dagger/main.go @@ -91,7 +91,7 @@ func (v *VookServer) SendImage( } _, err = dag.Scp(). - Config(sshDestText). + Config(strings.TrimSpace(sshDestText)). WithIdentityFile(sshKey). FileToRemote(imageTar, ScpCommanderFileToRemoteOpts{ Target: path, @@ -124,7 +124,7 @@ func (v *VookServer) Apply( } _, err = dag.SSH(). - Config(destinationText). + Config(strings.TrimSpace(destinationText)). WithIdentityFile(sshKey). Command( fmt.Sprintf(` From 6a4a0f81516aff9ca47924a549df3da1de062175 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 11:25:02 +0900 Subject: [PATCH 029/273] =?UTF-8?q?feat:=20Term=20Model=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/vook/server/api/model/Term.java | 7 ++++--- .../main/java/vook/server/api/model/TermRepository.java | 6 ++++++ api/src/main/java/vook/server/api/model/TermSynonym.java | 3 +-- .../java/vook/server/api/model/TermSynonymRepository.java | 6 ++++++ 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 api/src/main/java/vook/server/api/model/TermRepository.java create mode 100644 api/src/main/java/vook/server/api/model/TermSynonymRepository.java diff --git a/api/src/main/java/vook/server/api/model/Term.java b/api/src/main/java/vook/server/api/model/Term.java index 2faa0e85..c793783e 100644 --- a/api/src/main/java/vook/server/api/model/Term.java +++ b/api/src/main/java/vook/server/api/model/Term.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import lombok.Getter; +import java.util.ArrayList; import java.util.List; @Getter @@ -25,7 +26,7 @@ public class Term extends BaseEntity { private Glossary glossary; @OneToMany(mappedBy = "term", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - private List synonym; + private List synonyms = new ArrayList<>(); public static Term forCreateOf( String term, @@ -39,7 +40,7 @@ public static Term forCreateOf( return result; } - public void addSynonym(TermSynonym termSynonym) { - synonym.add(termSynonym); + public void addSynonym(String synonym) { + this.synonyms.add(TermSynonym.forCreateOf(synonym, this)); } } diff --git a/api/src/main/java/vook/server/api/model/TermRepository.java b/api/src/main/java/vook/server/api/model/TermRepository.java new file mode 100644 index 00000000..ca73f733 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/TermRepository.java @@ -0,0 +1,6 @@ +package vook.server.api.model; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TermRepository extends JpaRepository { +} diff --git a/api/src/main/java/vook/server/api/model/TermSynonym.java b/api/src/main/java/vook/server/api/model/TermSynonym.java index 7a97867b..98b5e1c3 100644 --- a/api/src/main/java/vook/server/api/model/TermSynonym.java +++ b/api/src/main/java/vook/server/api/model/TermSynonym.java @@ -19,14 +19,13 @@ public class TermSynonym { @JoinColumn(name = "term_id", nullable = false) private Term term; - public static TermSynonym forCreateOf( + static TermSynonym forCreateOf( String synonym, Term term ) { TermSynonym result = new TermSynonym(); result.synonym = synonym; result.term = term; - term.addSynonym(result); return result; } } diff --git a/api/src/main/java/vook/server/api/model/TermSynonymRepository.java b/api/src/main/java/vook/server/api/model/TermSynonymRepository.java new file mode 100644 index 00000000..792a437c --- /dev/null +++ b/api/src/main/java/vook/server/api/model/TermSynonymRepository.java @@ -0,0 +1,6 @@ +package vook.server.api.model; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TermSynonymRepository extends JpaRepository { +} From ca1edb1c22e9a1f4d4c75328b6560d6996b65bc3 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 12:27:00 +0900 Subject: [PATCH 030/273] =?UTF-8?q?chore:=20lombok=EC=9D=84=20test?= =?UTF-8?q?=EC=97=90=20=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/build.gradle b/api/build.gradle index 140ad5c5..d63cd82d 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -31,6 +31,8 @@ dependencies { // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' // mariadb runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' From 8906fedb0325e77caec31ed5b555049c0aeb9e13 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 12:27:33 +0900 Subject: [PATCH 031/273] =?UTF-8?q?feat:=20CsvReader=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/devhelper/CsvReader.java | 67 +++++++++++++++++++ .../server/api/devhelper/CsvReaderTest.java | 66 ++++++++++++++++++ api/src/test/resources/tsvreader/test.tsv | 3 + 3 files changed, 136 insertions(+) create mode 100644 api/src/main/java/vook/server/api/devhelper/CsvReader.java create mode 100644 api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java create mode 100644 api/src/test/resources/tsvreader/test.tsv diff --git a/api/src/main/java/vook/server/api/devhelper/CsvReader.java b/api/src/main/java/vook/server/api/devhelper/CsvReader.java new file mode 100644 index 00000000..ad18ff3f --- /dev/null +++ b/api/src/main/java/vook/server/api/devhelper/CsvReader.java @@ -0,0 +1,67 @@ +package vook.server.api.devhelper; + +import lombok.NoArgsConstructor; + +import java.io.*; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@NoArgsConstructor +public class CsvReader { + + private String DELIMITER = ","; + + public CsvReader(String delimiter) { + this.DELIMITER = delimiter; + } + + public List readValue(File file, Class clazz) { + try { + return readValue(new FileReader(file), clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public List readValue(String string, Class clazz) { + try { + return readValue(new StringReader(string), clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private List readValue(Reader reader, Class clazz) throws IOException { + List records = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(reader)) { + String line; + List fieldNames = getFieldNames(br.readLine()); + while ((line = br.readLine()) != null) { + records.add(createInstance(clazz, fieldNames, line)); + } + } + return records; + } + + private List getFieldNames(String line) { + return Arrays.asList(line.split(DELIMITER)); + } + + private T createInstance(Class clazz, List fieldNames, String line) { + String[] values = line.split(DELIMITER); + T instance; + try { + instance = clazz.getDeclaredConstructor().newInstance(); + for (int i = 0; i < fieldNames.size(); i++) { + Field field = clazz.getDeclaredField(fieldNames.get(i)); + field.setAccessible(true); + field.set(instance, values[i]); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return instance; + } +} diff --git a/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java b/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java new file mode 100644 index 00000000..58c9ad10 --- /dev/null +++ b/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java @@ -0,0 +1,66 @@ +package vook.server.api.devhelper; + +import lombok.Getter; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +class CsvReaderTest { + + @Test + @DisplayName("TSV Reader - 문자열 변환") + void readValueString() { + // given + String csvString = """ + term\tsynonyms\tdescription + Java\t자바\t자바는 객체지향 프로그래밍 언어이다. + Spring\t스프링\t스프링은 자바 기반의 프레임워크이다. + """; + + CsvReader reader = new CsvReader("\t"); + + // when + List entity = reader.readValue(csvString, Term.class); + + // then + assertThat(entity).hasSize(2); + assertThat(entity).extracting("term", "synonyms", "description") + .containsExactly( + tuple("Java", "자바", "자바는 객체지향 프로그래밍 언어이다."), + tuple("Spring", "스프링", "스프링은 자바 기반의 프레임워크이다.") + ); + } + + @Test + @DisplayName("TSV Reader - 파일 변환") + void readValueFile() { + // given + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("tsvreader/test.tsv").getFile()); + + CsvReader reader = new CsvReader("\t"); + + // when + List entity = reader.readValue(file, Term.class); + + // then + assertThat(entity).hasSize(2); + assertThat(entity).extracting("term", "synonyms", "description") + .containsExactly( + tuple("Java", "자바", "자바는 객체지향 프로그래밍 언어이다."), + tuple("Spring", "스프링", "스프링은 자바 기반의 프레임워크이다.") + ); + } + + @Getter + static class Term { + private String term; + private String synonyms; + private String description; + } +} diff --git a/api/src/test/resources/tsvreader/test.tsv b/api/src/test/resources/tsvreader/test.tsv new file mode 100644 index 00000000..c425b401 --- /dev/null +++ b/api/src/test/resources/tsvreader/test.tsv @@ -0,0 +1,3 @@ +term synonyms description +Java 자바 자바는 객체지향 프로그래밍 언어이다. +Spring 스프링 스프링은 자바 기반의 프레임워크이다. From 84763257159d7455c4fa43da690c8aa3a089b815 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 12:57:30 +0900 Subject: [PATCH 032/273] =?UTF-8?q?chore:=20SQL=EC=9D=B4=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EB=90=98=EB=8F=84=EB=A1=9D=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 디버깅 용이성 향상을 위한 설정 --- api/src/main/resources/application-local.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/src/main/resources/application-local.yml b/api/src/main/resources/application-local.yml index a164c031..c6e0d2fb 100644 --- a/api/src/main/resources/application-local.yml +++ b/api/src/main/resources/application-local.yml @@ -7,6 +7,11 @@ spring: jpa: hibernate: ddl-auto: update + show-sql: true + properties: + hibernate: + highlight_sql: false + format_sql: true server: port: 8080 service: From 005a92ac188d586c0c3a81042e6a0bce1677023c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 12:47:23 +0900 Subject: [PATCH 033/273] =?UTF-8?q?feat:=20InitService=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EA=B0=9C=EB=B0=9C=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EC=9A=A9=EC=96=B4=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/devhelper/InitService.java | 52 ++++++++++++- .../init/\352\260\234\353\260\234.tsv" | 76 +++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 "api/src/main/resources/init/\352\260\234\353\260\234.tsv" diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index db96e29f..342e901f 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -1,27 +1,37 @@ package vook.server.api.devhelper; import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.model.Glossary; -import vook.server.api.model.GlossaryRepository; -import vook.server.api.model.Member; -import vook.server.api.model.MemberRepository; +import vook.server.api.model.*; import vook.server.api.outbound.search.SearchClearable; import vook.server.api.outbound.search.SearchService; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + @Service @Transactional @RequiredArgsConstructor public class InitService { + private final ResourceLoader resourceLoader; + private final MemberRepository memberRepository; private final GlossaryRepository glossaryRepository; + private final TermRepository termRepository; + private final TermSynonymRepository termSynonymRepository; private final SearchClearable searchClearable; private final SearchService searchService; public void init() { + termSynonymRepository.deleteAllInBatch(); + termRepository.deleteAllInBatch(); glossaryRepository.deleteAllInBatch(); memberRepository.deleteAllInBatch(); searchClearable.clearAll(); @@ -36,5 +46,39 @@ public void init() { searchService.createGlossary(marketingGlossary); Glossary practiceGlossary = glossaryRepository.save(Glossary.forCreateOf("실무", vook)); searchService.createGlossary(practiceGlossary); + + List devTerms = getTerms("classpath:init/개발.tsv", devGlossary); + termRepository.saveAll(devTerms); + } + + private List getTerms(String location, Glossary glossary) { + try { + File tsvFile = resourceLoader.getResource(location).getFile(); + CsvReader tsvReader = new CsvReader("\t"); + List rawTerms = tsvReader.readValue(tsvFile, RawTerm.class); + return toTerms(rawTerms, glossary); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static @NotNull List toTerms(List rawTerms, Glossary glossary) { + return rawTerms.stream() + .map(rawTerm -> rawTerm.toTerm(glossary)) + .toList(); + } + + public static class RawTerm { + private String term; + private String synonyms; + private String meaning; + + public Term toTerm(Glossary glossary) { + Term term = Term.forCreateOf(this.term, this.meaning, glossary); + Arrays.stream(this.synonyms.split("\\n")) + .map(String::trim) + .forEach(term::addSynonym); + return term; + } } } diff --git "a/api/src/main/resources/init/\352\260\234\353\260\234.tsv" "b/api/src/main/resources/init/\352\260\234\353\260\234.tsv" new file mode 100644 index 00000000..0698203b --- /dev/null +++ "b/api/src/main/resources/init/\352\260\234\353\260\234.tsv" @@ -0,0 +1,76 @@ +term synonyms meaning +SDK Software Development Kit 특정 플랫폼이나 운영체재를 위한 앱을 만드는데 필요한 도구와 코드 모음 SDK는 앱 개발을 쉽고 빠르게 만드는 도구로 개발자가 처음부터 모든 것을 스스로 구축할 필요가 없어 시간과 노력을 절약할 수 있습니다. 예) 개발자가 지도를 앱에 추가하고 싶다면 구글 지도 SDK를 사용할 수 있습니다. 이 SDK에는 지도 표시, 사용자 위치 추적, 경로 검색 등의 기능을 위한 코드와 도구가 포함되어 있습니다. +API Application Programming Interface 프로그램이 서로 통신하고 협력할 수 있는 필수적인 연결 도구 API는 크게 데이터 공유, 기능 활용, 시스템 통합의 역할을 수행합니다. 데이터 공유 : 다른 프로그램의 데이터를 제공합니다. 가령, 날씨 앱은 API로 기상청의 날씨 데이터를 가져올 수 있습니다. 기능 활용 : 다른 프로그램의 기능을 활용할 수 있도록 합니다. 가령, 음악 앱은 API로 스포티파이 등의 음악 스트리밍 서비스의 음악을 재생할 수 있습니다. 시스템 통합 : 서로 다른 시스템을 연결해 함께 작동하도록 합니다. 가령, 온라인 쇼핑몰은 API로 주문 처리, 결제 처리 및 배송 추적을 위한 다른 시스템과 통합할 수 있습니다. +IDE Integrated Development Environment 통합 개발 환경의 약자로 개발자가 소프트웨어를 빠르고 쉽게 개발하도록 돕는 도구 IDE는 코딩, 컴파일, 디버깅 및 테스트 등 소프트웨어 개발 프로세스의 모든 단계를 위한 다양한 기능을 제공합니다. +In-app browser 인앱 브라우저 모바일 앱 혹은 데스크톱 앱 내 내장된 웹 브라우저 별도의 웹 브라우저 앱을 실행하지 않아도 앱에서 웹페이지를 볼 수 있도록 합니다. +External browser 익스터널 브라우저 익스터널 브라우저는 별도의 앱으로 설치되는 웹 브라우저입니다. 대표적으로 크롬, 사파리, 파이어폭스, 엣지 등이 있습니다. +CDC Change Data Capture 데이터베이스에 변경된 데이터를 실시간으로 추적하고 캡처하는 기술 데이터베이스에 삽입, 수정, 삭제 등의 변경 작업이 발생할 때마다 해당 변경사항을 기록하고 다른 시스템이나 프로세스에 알려줍니다. CDC는 데이터베이스의 로그 파일을 모니터링하거나 트리거를 사용해 데이터 변경사항을 감지합니다. 변경사항이 감지되면 CDC는 해당 변경 사항을 메시지 형식으로 포장하고 이를 큐에 저장합니다. +Streaming 스트리밍 미디어 파일을 인터넷을 통해 실시간으로 전송하고 재생하는 기술 이전에는 음악이나 영상을 다운로드한 후에 재생할 수 있었다면 스트리밍 기술로 파일을 다운로드하지 않고도 바로 듣거나 재생할 수 있게 되었습니다. 스트리밍에는 라이브 스트리밍과 온디맨드 스트리밍으로 나눌 수 있습니다. 라이브 스트리밍은 실시간으로 방송하고 시청하는 방식이며, 온디맨드 스트리밍은 이미 녹화된 음악, 동영상, 게임 등을 선택해 시청하는 방식입니다. +컴파일러 Compiler 개발자가 작성한 소스 코드를 컴퓨터가 이해하고 실행할 수 있는 기계어로 변환하는 소프트웨어 도구 컴파일러는 대표적으로 개발자의 소스 코드를 읽고 분석하며 소스 코드의 문법적 오류를 확인하고 오류 메시지를 출력합니다. 또한 생성된 기계어를 실행 가능한 파일 형식으로 저장합니다. +클라이언트 Client 클라이언트는 사용자 컴퓨터에 설치된 소프트웨어로 서버에 요청을 보내고 서버로부터 응답을 받습니다. 웹 브라우저, 이메일 클라이언트, 파일 공유 프로그램 등이 클라이언트에 속합니다. +서버 Server 서버는 네트워크에 연결된 컴퓨터로 클라이언트의 요청을 받아 처리하고 결과를 응답으로 보냅니다. 웹 서버, 데이터 베이스 서버, 메일 서버 등이 서버에 속합니다. +프론트엔드 Front-end 사용자가 직접 보고 상호 작용하는 웹페이지의 눈에 보이는 부분을 담당합니다. 웹 브라우저에서 보이는 모든 요소 (텍스트, 이미지, 영상, 버튼 등)와 사용자의 상호 작용(클릭, 스크롤, 입력)이 프론트엔드에 속합니다. +백엔드 Back-end 사용자가 직접 볼 수 없는 웹페이지의 서버를 담당합니다. 데이터베이스 관리, 서버 로직 처리, 사용자 인증, 보안 등 웹페이지의 핵심 기능을 구현합니다. +디버그 Debug 프로그램에서 발생하는 오류나 문제를 찾아 해결하는 과정 디버깅은 프로그램의 품질을 높이고 사용자에게 안정적인 경험을 제공합니다. 디버깅 과정에서 발생하는 오류를 정확히 재현해보고 코드를 분석해 원인을 파악합니다. 오류의 원인을 파악한 후 코드를 수정하여 오류를 해결하고 테스트를 통해 오류가 해결되었는지 확인합니다. 해결 후에는 디버깅 과정에서 발생한 오류, 오류 원인, 해결 방법 등을 문서화합니다. +빌드 Build 소스 코드를 실행 가능한 파일로 변환하는 과정 먼저 컴파일러로 소스 코드를 기계어로 변환합니다. 컴파일된 객체 파일을 연결하고 의존하는 라이브러리를 추가해 실행 가능한 파일을 만듭니다. 실행 가능한 파일과 기타 필요한 파일을 하나의 패키지로 묶습니다. 컴파일은 소스 코드를 기계어로 변환하는 과정이며 빌드 과정의 한 단계인 반면, 빌드는 소스 코드를 실행 가능한 파일로 변환하는 전체적인 과정을 의미합니다. +배포 Deployment 완성된 코드를 실제 사용 환경에 설치하고 실행 가능한 형태로 만드는 과정 배포 방법에는 수동 배포, 자동 배포, 컨테이너 배포 등이 있습니다. 수동 배포는 개발자가 직접 서버에 연결해 배포 작업을 수행하는 방식입니다. 자동 배포는 CI/CD 도구를 사용해 배포 과정을 자동화 하는 방식입니다. 컨테이너 배포는 Docker와 같은 컨테이너 기술을 사용해 배포하는 방식입니다. +반응형 웹 Responsive Website 웹사이트가 다양한 기기의 화면 크기와 해상도에 맞게 자동으로 디자인 및 레이아웃을 조정하도록 돕는 웹 디자인 접근 방식입니다. +SSO Single Sign On 사용자가 여러 시스템이나 애플리케이션에 한 번의 로그인 만으로 액세스할 수 있도록 하는 인증 방식 사용자는 시스템마다 별도 계정을 만들고 로그인 할 필요 없이 하나의 아이디와 비밀번호만 사용해 여러 시스템에 로그인할 수 있습니다. SSO는 다음과 같은 단계를 거쳐 작동합니다. 1. 사용자는 하나의 시스템에 로그인합니다. 2. 사용자 인증 정보는 중앙 인증 서버에 전송됩니다. 3. 중앙 인증 서버는 사용자 인증 정보를 검사하고 유효하면 인증 토큰을 생성합니다. 4. 인증 토큰은 사용자의 브라우저에 저장됩니다. 5. 사용자가 다른 시스템에 액세스하려고 할 때 브라우저는 저장된 인증 토큰을 자동으로 전송합니다. 6. 중앙 인증 서버는 인증 토큰을 검사하고 유효하면 사용자를 자동으로 로그인시킵니다. +SaaS Software as a Service 소프트웨어를 서비스로 제공하는 모델 SaaS 서비스 제공업체는 웹 브라우저를 통해 사용자가 액세스 할 수 있는 소프트웨어 애플리케이션을 개발, 운영 및 유지 관리합니다. 사용자는 별도의 소프트웨어를 설치하거나 하드웨어를 구매할 필요 없이 인터넷 연결만 있으면 서비스를 이용할 수 있습니다. +PasS Platform as a Service 소프트웨어 개발 및 배포를 위한 플랫폼을 서비스로 제공하는 모델 PaaS 플랫폼은 개발자들이 웹 애플리케이션을 구축, 테스트, 배포 및 관리하는 데 필요한 모든 기능과 도구를 제공합니다. 개발자는 PaaS 플랫폼을 사용하여 핵심적인 개발 작업에 집중할 수 있으며, 서버 관리, 운영 체제 관리, 네트워킹 등의 인프라 관리 작업은 PaaS 플랫폼 제공업체가 담당합니다. +IaaS Infrastructure as a Service 컴퓨팅, 스토리지, 네트워킹과 같은 기반 IT 인프라를 서비스로 제공하는 모델 IaaS 서비스 제공업체는 사용자에게 가상 머신, 서버, 스토리지, 네트워킹 장비 등을 제공하며, 사용자는 이러한 자원을 자유롭게 사용하여 원하는 시스템 및 애플리케이션을 구축할 수 있습니다. +SOAP API Simple Object Access Protocol API 웹 서비스를 위한 표준 프로토콜 XML 기반 메시지 형식을 사용하여 네트워크를 통해 분산된 응용 프로그램 간 통신을 지원합니다. SOAP API는 표준화된 웹 서비스 프로토콜이지만, 복잡하고 느리고 비효율적인 단점이 있습니다. 이러한 단점을 해결하기 위해 REST API, JSON API와 같은 다른 웹 서비스 프로토콜들이 등장했습니다. +RPC Remote Procedure Call 네트워크를 통해 원격 시스템에 있는 함수를 마치 로컬 시스템에 있는 함수처럼 호출하는 기술 개발자는 호출되는 함수가 다른 시스템에 있는지 여부를 확인하지 않고 함수를 호출할 수 있습니다. RPC는 분산 시스템에서 서로 다른 시스템 간 통신과 자원 공유를 용이하게 만들며, 클라이언트 - 서버 애플리케이션 개발에 많이 사용됩니다. +웹소캣 Websocket API 웹 클라이언트와 웹 서버 간에 지속적인 양방향 실시간 통신을 가능하게 하는 API 기존의 HTTP 기반 웹 요청과 달리 WebSocket은 한 번의 TCP 연결을 통해 클라이언트와 서버 간에 지속적으로 데이터를 주고받을 수 있으므로, 채팅, 게임, 실시간 데이터 갱신 등 실시간으로 데이터 송수신이 필요한 애플리케이션에 적합합니다. +REST API Representational State Transfer API REST 아키텍처 스타일의 디자인 원칙을 준수하는 API REST API를 RESTful API라고 불리기도 합니다. REST API는 일관되고 이해하기 쉬운 인터페이스를 제공하며, 다양한 플랫폼과 프로그래밍 언어에서 쉽게 사용할 수 있는 장점이 있습니다. REST API는 HTTP method를 사용하여 자원에 대한 작업을 수행합니다. 일반적으로 사용되는 HTTP method는 다음과 같습니다. GET: 자원을 조회합니다. POST: 자원을 생성합니다. PUT: 자원을 업데이트합니다. DELETE: 자원을 삭제합니다. +HTTP Hypertext Transfer Protocol\n하이퍼텍스트 전송 프로토콜 웹 브라우저와 웹 서버 간의 통신을 위한 기본 프로토콜 웹사이트 방문 시 사용자가 입력한 URL을 요청하고, 웹 서버는 요청에 맞는 웹 페이지나 다른 데이터를 응답으로 전송하는 데 사용됩니다. HTTP는 기본적으로 데이터를 평문으로 전송하기 때문에 도청이나 위변조 위험이 있어 최근에는 HTTPS를 사용합니다. +HTTPS Hypertext Transfer Protocol Secure HTTP에 SSL/TLS 프로토콜을 추가하여 안전하게 데이터를 전송하도록 보안 강화한 프로토콜입니다. HTTPS는 웹 브라우저와 웹 서버 간 모든 통신을 암호화해 도청이나 위변조를 방지합니다. 웹 서버의 신원을 인증해 위조 사이트로부터 사용자를 보호하며, 데이터 전송 과정에서 데이터가 변경되지 않았는지 확인합니다. +SSL Secure Sockets Layer\n보안 소켓 계층 웹 브라우저와 웹 서버 간의 통신을 암호화하여 보안하는 프로토콜 SSL은 현재 TLS(Transport Layer Security)로 이름이 변경되었지만, 여전히 SSL이라는 용어가 널리 사용되고 있습니다. +TLS 전송 계층 보안 인터넷 상의 두 시스템 간의 통신을 보호하는 보안 프로토콜 데이터 암호화, 신원 인증, 데이터 무결성 보장 기능을 제공하여 사용자의 개인정보, 로그인 정보, 금융 정보 등을 안전하게 보호합니다. 이전에 사용되었던 SSL(Secure Sockets Layer) 프로토콜의 후속 버전입니다. +프레임워크 Framework 소프트웨어 개발 과정을 돕는 도구와 라이브러리의 집합 프레임워크는 개발자가 애플리케이션의 기본 구조 및 핵심 기능을 빠르고 쉽게 구축할 수 있도록 기본적인 뼈대를 제공합니다. 대표적으로 웹에선 Django, Ruby on Rails 등이 있으며, 모바일에선 React Native, Flutter 등이 있습니다. +라이브러리 Library 라이브러리는 개발자가 특정 기능이나 작업을 수행할 수 있도록 재사용 가능한 코드 모듈을 제공합니다. 가령 NumPy, Pandas 등 데이터 처리 관련 라이브러리는 복잡한 데이터 분석 및 처리 작업을 수행하는 데 도움을 줍니다. +플러그인 Plug-in 기존 소프트웨어에 새로운 기능을 추가하거나 기존 기능을 확장하는 소프트웨어 구성 요소 일반적으로 독립적인 프로그램으로 작동하며, 호환되는 호스트 프로그램에 설치해야 합니다. 플러그인은 다양한 분야에서 사용되며, 웹 브라우저, 그래픽 편집 프로그램, 비디오 편집 프로그램, 게임 엔진 등에서 흔히 볼 수 있습니다. +파라미터 인자\nParameter 함수는 특정 작업을 수행하도록 설계된 코드 블록입니다. 함수를 호출할 때 원하는 결과를 얻도록 함수에 데이터를 전달해야 합니다. 이 데이터를 파라미터라고 부릅니다. +Argument 인자값 인자값은 함수 호출 시 실제 값을 의미하며, 파라미터에 전달됩니다. 인자값은 다양한 형태의 데이터, 숫자, 문자열, 리스트, 객체 등을 포함할 수 있습니다. 함수는 전달된 인자값을 사용하여 계산을 수행하거나 작업을 처리합니다. +AJAX Asynchronous Javascript and XML 비동기 JavaScript 및 XML의 약자로, 웹 페이지를 부분적으로 다시 로드하지 않고도 서버와 데이터를 주고받을 수 있는 웹 개발 기술 AJAX를 사용하면 사용자가 웹 페이지를 새로고침하지 않고도 데이터를 업데이트하고 새로운 콘텐츠를 로드할 수 있어 웹사이트의 사용자 경험을 크게 향상시킬 수 있습니다. 대표적으로 검색창에 입력하는 단어에 대한 자동 완성 기능, 무한 스크롤, 실시간 데이터 업데이트 등에 활용됩니다. +멀티스레드 Multi-thread 하나의 프로그램에서 동시에 여러 개의 작업을 수행하는 프로그래밍 방식입니다. 멀티스레드를 사용하면 컴퓨터의 여러 개의 CPU 코어를 효율적으로 활용하여 프로그램 성능을 향상시킬 수 있습니다. +렌더링 Rendering 렌더링은 2D 또는 3D 모델을 기반으로 이미지 또는 영상 컴퓨터 그래픽스, 영화 제작, 비디오 게임, 건축 시각화 등 다양한 분야에서 활용됩니다. 렌더링 과정은 모델 정보를 시각적 표현으로 변환하는 복잡한 계산 과정을 포함하며, 빛, 색상, 질감, 그림자 등 다양한 요소를 고려하여 현실감 넘치는 이미지 또는 영상을 생성합니다. +샌드박스 SandBox 실제 환경에 영향을 미치지 않고 새로운 소프트웨어나 기능을 테스트할 수 있는 가상의 공간입니다. 샌드박스는 개발자가 버그를 찾고 코드를 개선할 수 있는 안전한 환경을 제공합니다. +데이터 레이크 Data lake 구조화된 또는 반구조화된, 혹은 구조화되지 않은 방대한 양의 데이터를 저장하는 저장소 데이터 레이크는 기존의 데이터 웨어하우스와는 다릅니다. 데이터 웨어하우스는 일반적으로 미리 정의된 스키마를 사용하여 구조화된 데이터를 저장하는 반면, 데이터 레이크는 스키마 없이 데이터를 저장할 수 있습니다. 즉, 데이터 레이크에는 모든 유형의 데이터를 저장할 수 있으며, 데이터가 저장된 후에도 데이터 스키마를 변경할 수 있습니다. +데브옵스 DevOps 데브옵스(DevOps)는 소프트웨어 개발(Development)과 운영(Operations)을 하나의 통합된 프로세스로 연결하여 소프트웨어를 빠르게, 안정적으로, 그리고 효율적으로 제공하는 문화와 관행들의 집합 데브옵스는 개발팀과 운영팀 간의 협업을 강조하며, 자동화, 지속적인 통합 및 배포, 모니터링 등을 통해 소프트웨어 개발 및 제공 프로세스를 개선하는 데 중점을 둡니다. +CI/CD Continuous Integration/Continuous Delivery\n지속적인 통합/지속적인 배포 소프트웨어 개발 프로세스를 자동화하여 소프트웨어를 빠르고 안정적으로 제공하는 방법 CI/CD는 다음 두 단계로 구성됩니다. - 지속적인 통합(Continuous Integration, CI): 개발자가 코드를 변경할 때마다 코드를 자동으로 통합하고 테스트하는 프로세스입니다. CI는 개발 중에 발생하는 버그를 빠르게 발견하고 해결하는 데 도움이 됩니다. - 지속적인 배포(Continuous Delivery, CD): 테스트를 통과한 코드를 자동으로 배포 환경에 배포하는 프로세스입니다. CD는 소프트웨어를 빠르게 출시하고 업데이트하는 데 도움이 됩니다. +파싱 Parsing 컴퓨터 과학에서 특정 형식으로 구성된 데이터를 분석하고 해석하는 과정 파싱은 주로 텍스트 기반 데이터, 프로그래밍 언어 소스 코드, XML 문서 등을 처리하는 데 사용됩니다. 파싱 과정에서 데이터의 구조를 이해하고 의미 있는 정보를 추출합니다. +핑 Ping 컴퓨터 네트워크에서 두 장치 간의 연결성을 테스트하는 데 사용되는 도구 핑은 다음과 같은 다양한 상황에서 사용됩니다. - 네트워크 문제 진단: 네트워크 연결이 끊겨 있거나 속도가 느린 경우 핑을 사용하여 문제의 원인을 파악할 수 있습니다. - 웹 사이트 성능 테스트: 웹 사이트에 연결하는 데 걸리는 시간을 측정하는 데 핑을 사용할 수 있습니다. - 새로운 네트워크 장치 설정 확인: 새로 설치한 네트워크 장치가 올바르게 작동하는지 확인하는 데 핑을 사용할 수 있습니다. +SRE Site Reliability Engineering 소프트웨어 시스템의 안정성, 확장성, 및 성능을 유지하는 것을 담당하는 엔지니어링 분야 SRE는 전통적인 소프트웨어 개발 및 운영 팀의 역할을 결합하여 소프트웨어를 서비스로 제공하는 데 필요한 모든 측면을 관리합니다. +SSH Secure Shell Protocol 네트워크를 통해 두 컴퓨터 간의 안전한 연결을 제공하는 프로토콜 SSH는 다음과 같은 다양한 목적으로 사용됩니다. - 원격 컴퓨터 관리: 시스템 관리자는 SSH를 사용하여 원격 컴퓨터에 로그인하고 관리 작업을 수행할 수 있습니다. - 파일 전송: 사용자는 SSH를 사용하여 두 컴퓨터 간에 파일을 안전하게 전송할 수 있습니다. - 애플리케이션 실행: 사용자는 SSH를 사용하여 원격 컴퓨터에서 애플리케이션을 실행할 수 있습니다. - 포트 포워딩: 사용자는 SSH를 사용하여 한 컴퓨터의 포트를 다른 컴퓨터의 포트로 전달할 수 있습니다. +성능 테스트 BMT\nBench Marking Test 한 가지 시스템이나 제품의 성능을 객관적으로 측정하고 비교하기 위한 테스트 BMT는 일반적으로 동일한 조건에서 서로 다른 시스템 또는 제품을 테스트하여 어떤 시스템 또는 제품이 더 우수한 성능을 제공하는지를 확인하는 데 사용됩니다. +gRPC Google RPC\nGoogle Remote Procedure Call RPC(Remote Procedure Call)는 네트워크를 통해 원격 시스템에 있는 프로시저를 호출하는 기술입니다. RPC는 마치 로컬 시스템에서 함수를 호출하는 것처럼 원격 시스템의 프로시저를 호출할 수 있도록 해줍니다. gRPC는 Google에서 개발한 오픈 소스 RPC 프레임워크입니다. +NAS Network Attached Storage 네트워크를 통해 여러 사용자가 파일 저장 및 공유를 할 수 있도록 하는 장치 NAS는 다양한 용도로 사용됩니다. 개인 용도로는 사진, 음악, 비디오 등의 개인 파일을 저장하고 공유하는 데 사용할 수 있습니다. 사업 용도로는 문서, 스프레드시트, 프레젠테이션과 같은 업무 파일을 저장하고 공유하는 데 사용할 수 있습니다. 또한 NAS는 데이터 백업, 파일 스트리밍, 미디어 서버 등으로도 사용할 수 있습니다. +CDN Contents Delivery Network 웹 콘텐츠를 사용자에게 더 빠르고 안정적으로 제공하기 위해 지리적으로 분산된 네트워크를 사용하는 기술 웹사이트, 이미지, 동영상, 스트리밍 미디어 등 다양한 콘텐츠를 빠르게 전송하는 데 효과적으로 사용됩니다. +DNS Domain Name System 인터넷 주소록이라고도 불리는 시스템으로, 도메인 이름(예: www.example.com)을 IP 주소(예: 192.0.2.44)로 변환하는 역할을 합니다. 쉽게 말해, 사람이 쉽게 기억할 수 있는 도메인 이름을 컴퓨터가 이해할 수 있는 IP 주소로 변환하는 시스템이라고 볼 수 있습니다. +엔드포인트 Endpoint 네트워크에서 데이터를 주고받을 수 있는 논리적 또는 물리적 위치 간단히 말해서, 엔드포인트는 네트워크 상에서 연결하고 상호 작용할 수 있는 문과 같습니다. 각 엔드포인트는 고유한 IP 주소 또는 식별자를 가지고 있으며, 네트워크를 통해 다른 엔드포인트와 통신할 수 있습니다. +Request 요청 엔드포인트가 다른 엔드포인트에게 데이터나 작업을 수행하도록 요청하는 메시지 +Response 응답 요청에 대한 응답으로 엔드포인트에서 다른 엔드포인트로 전송하는 메시지 +JSON JavaSript Object Notation Javascript에서 사용하는 객체 정의 방법 +포트 Port 컴퓨터 또는 네트워크 장치에서 특정 애플리케이션이나 서비스를 식별하는 데 사용되는 논리적 번호 포트는 여러 애플리케이션이 동시에 같은 컴퓨터에서 실행될 수 있도록 하는 가상 통로와 같습니다. 각 포트는 고유한 번호를 가지고 있으며, 0에서 65535까지의 범위를 사용할 수 있습니다. +프로토콜 Protocol 두 시스템 간의 통신 방식을 정의하는 규칙 집합 프로토콜은 두 시스템이 서로 어떻게 대화해야 하는지에 대한 약속과 같습니다. 프로토콜에선 전송되는 데이터 형식과 데이터 전송 방식 그리고 오류 처리에 대해 정의합니다. +로드 밸런싱 Load Balancing 네트워크 트래픽을 여러 서버에 분산하여 처리하는 기술 로드 밸런싱을 통해 서버 부하를 줄이고, 성능을 향상시키며, 사용자 가용성을 높일 수 있습니다. 가령 온라인 쇼핑몰의 경우 높은 트래픽을 처리하고 고객에게 빠른 응답 속도를 제고하기 위해 사용합니다. 온라인 게임의 경우 많은 플레이어를 동시에 처리하고 게임 지연을 줄이기 위해 사용합니다. +가상화 Virtualization 하나의 컴퓨터를 여러 개의 가상 컴퓨터처럼 사용할 수 있도록 하는 기술 +데이터베이스 Database 구조화된 형태로 데이터를 저장하고 관리하는 시스템 데이터를 체계적으로 정리하고, 쉽게 찾고 사용할 수 있도록 하는 도구라고 생각하면 됩니다. +테이블 Table 테이블은 행과 열로 구성된 데이터 구조로, 데이터베이스에서 데이터를 저장하는 기본 단위 각 행은 하나의 데이터 레코드를 나타내고, 각 열은 레코드의 특정 속성을 나타냅니다. 테이블은 마치 스프레드시트와 유사한 형태로 데이터를 보여주고, 행과 열을 사용하여 원하는 데이터를 쉽게 찾고 조작할 수 있도록 합니다. +기본 키 Primary Key 테이블 내의 각 레코드를 고유하게 식별하는 열 또는 열 집합 다른 열의 값이 중복될 수 있는 반면, 기본 키 값은 항상 고유하고 null 값을 허용하지 않습니다. 기본 키는 데이터 무결성을 유지하고 중복된 데이터를 방지하는 데 중요한 역할을 합니다. +외래 키 Foreign Key 한 테이블의 열을 다른 테이블의 기본 키 열과 연결하는 관계 외래키는 두 테이블 간의 데이터 관계를 정의하고 데이터 무결성을 유지하는 데 도움이 됩니다. 예를 들어, '고객' 테이블에 '주소' 테이블의 외래키 열이 있다면, '고객' 테이블의 각 레코드는 '주소' 테이블의 고유한 레코드를 참조하게 됩니다. +인덱스 Index 테이블의 특정 열에 대한 검색 속도를 향상시키는 데이터 구조 책의 색인과 유사하게, 인덱스는 데이터베이스 시스템이 테이블 내의 특정 레코드를 빠르게 찾도록 도와줍니다. 자주 검색되는 열에 인덱스를 생성하면 데이터 검색 속도를 크게 향상시킬 수 있으며, 데이터베이스 성능을 개선하는 데 효과적인 방법입니다. +SQL Structured Query Language 관계형 데이터베이스에서 데이터를 조작하는 표준 언어 데이터베이스에 데이터를 저장하고, 검색하고, 삭제하고, 수정하는 데 사용하는 데이터베이스 프로그래밍 언어라고 생각하면 됩니다. +정규화 Normalization 데이터베이스의 데이터 구조를 체계적으로 조직하여 데이터 중복을 최소화하고, 데이터 무결성을 유지하며, 데이터베이스의 효율성을 높이는 프로세스 데이터베이스 정규화는 데이터 중복을 최소화하고 데이터 무결성을 유지하는 목적으로 사용됩니다. 또한 데이터베이스 구조를 최적화해 데이터 검색 및 조작 속도를 높이고 데이터베이스 관리 작업을 용이하게 합니다. +트랜잭션 Transaction 데이터베이스와 같은 시스템의 상태를 일관되게 변경하는 작업 단위 트랜잭션은 ACID 특성을 만족하도록 설계되어 데이터 무결성을 유지합니다. ACID는 트랜잭션의 핵심 특성을 나타내는 약자입니다. - 원자성 (Atomicity): 트랜잭션은 하나의 작업 단위로 실행되며, 트랜잭션이 완료되거나 실패할 때까지 부분적인 작업이 수행되지 않습니다. - 일관성 (Consistency): 트랜잭션이 완료되면 데이터베이스는 항상 일관된 상태를 유지합니다. 트랜잭션 중간에 데이터베이스 상태가 변경되지 않습니다. - 격리성 (Isolation): 동시에 실행되는 여러 트랜잭션은 서로 영향을 미치지 않습니다. 각 트랜잭션은 서로 독립적으로 실행되는 것처럼 작동합니다. - 지속성 (Durability): 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 저장됩니다. 시스템 장애나 데이터 손실에도 영향을 받지 않습니다. +컨벤션 Convention 일관되고 읽기 쉬운 코드 베이스 유지를 위해 개발자가 따르는 합의된 규칙 +클래스 Class 객체의 청사진 역할을 하는 설계 요소 붕어빵 틀 자체를 클래스라고 생각하면 됩니다. 붕어빵 틀은 붕어빵의 속성(팥, 깨, 모양 등)과 행동(먹는 것)을 정의하고 있으며, 이 틀을 사용하여 여러 개의 붕어빵(객체)를 만들 수 있습니다. +객체 Object 데이터와 그 데이터를 처리하는 메서드를 묶어 놓은 기본 단위 붕어빵 틀을 이용하여 만든 붕어빵 하나하나를 객체라고 생각하면 됩니다. 각 붕어빵은 틀에서 정의된 속성(팥, 깨, 모양 등)과 행동(먹는 것)을 가지고 있습니다. +객체 지향 프로그래밍 OOP 객체라는 기본 단위를 사용하여 프로그램을 설계하고 작성하는 프로그래밍 패러다임 객체는 데이터(속성)와 행동(메서드)을 가지고 있으며, 서로 상호작용하여 프로그램을 구성합니다. +CSS Cascading Style Sheets HTML 문서의 스타일을 정의하는 데 사용되는 스타일 시트 언어 HTML 문서가 웹 페이지의 구조를 담당한다면, CSS는 웹 페이지의 디자인을 담당합니다. +레거시 Regacy 오래된 기술로 개발되었지만, 여전히 사용되고 있는 컴퓨터 시스템, 소프트웨어, 하드웨어를 의미합니다. +트러블 슈팅 Trouble Shooting 시스템이나 장치에서 발생하는 문제를 진단하고 해결하는 과정 트러블 슈팅의 경우 문제의 근본 원인을 파악하고 해결 방안을 모색하는 데 초점을 맞춥니다. 문제의 증상만 해결하는 것이 아니라, 발생 원인을 제거하여 동일한 문제가 다시 발생하지 않도록 방지하는 것을 목표로 합니다. 반면 디버깅 코드는 시스템에서 오류를 찾고 수정하는 데 초점을 맞춥니다. 코드 실행 과정을 단계별로 검사하고, 오류가 발생하는 부분을 정확히 식별하여 코드를 수정하거나 버그를 제거하는 것을 목표로 합니다. +네이티브 앱 Native App 특정 모바일 운영 체제(예: iOS, Android)를 위해 자체 프로그래밍 언어(예: Swift, Java)로 개발됩니다. 일반적으로 하이브리드 앱보다 빠르고 응답성이 뛰어납니다. 네이티브 앱은 운영 체제와 직접 통합되기 때문에 하드웨어 가속 및 기타 최적화 기능을 활용할 수 있습니다. +하이브리드 앱 Hibrid App 웹 기술(예: HTML, CSS, JavaScript)을 사용하여 빌드되고, 기본적으로 웹 뷰 앱으로 작동하며, 운영 체제별 래퍼를 통해 각 플랫폼에 맞게 패키징됩니다. 웹 기술 기반으로 개발되었기 때문에 플랫폼별 업데이트나 버그 수정 없이도 유지 관리가 비교적 용이합니다. +스플래시 Splash 컴퓨터 프로그램, 웹사이트 또는 모바일 앱을 시작할 때 잠깐 나타나는 로고나 이미지 로딩 화면이라고도 불리며, 프로그램이나 앱이 로드되고 사용자 인터페이스가 준비될 때까지 대기 시간을 채우는 역할을 합니다. From e8731681c4df156c956c84fb16acb9cd0d14ebe8 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 13:26:49 +0900 Subject: [PATCH 034/273] =?UTF-8?q?chore:=20devenv=EC=9D=98=20meilisearch?= =?UTF-8?q?=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=83=81=ED=96=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit meilisearch의 indexing 시 메모리가 부족하면 죽어버리는 현상이 발생하였으므로 메모리 제한 상향 --- devenv/meilisearch/compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/meilisearch/compose.yml b/devenv/meilisearch/compose.yml index 21813543..7e2bcf6b 100644 --- a/devenv/meilisearch/compose.yml +++ b/devenv/meilisearch/compose.yml @@ -8,7 +8,7 @@ services: environment: - MEILI_ENV=development - MEILI_MASTER_KEY=aSampleMasterKey - mem_limit: 300m + mem_limit: 1000m restart: unless-stopped volumes: From 169e219a42807d4976f7be3477c4cfe95999cac4 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 13:27:27 +0900 Subject: [PATCH 035/273] =?UTF-8?q?feat:=20Initservice=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EA=B0=9C=EB=B0=9C=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EC=9A=A9=EC=96=B4=20=EC=B6=94=EA=B0=80=20=ED=9B=84=20meilis?= =?UTF-8?q?earch=EC=97=90=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A8=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/devhelper/InitService.java | 3 +- .../outbound/search/MeilisearchService.java | 47 ++++++++++++++++++- .../api/outbound/search/SearchService.java | 5 ++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 342e901f..d2126aa6 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -48,7 +48,8 @@ public void init() { searchService.createGlossary(practiceGlossary); List devTerms = getTerms("classpath:init/개발.tsv", devGlossary); - termRepository.saveAll(devTerms); + List terms = termRepository.saveAll(devTerms); + searchService.addTerms(terms, devGlossary); } private List getTerms(String location, Glossary glossary) { diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java index 9a3d7ef8..f3edba3c 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -1,5 +1,7 @@ package vook.server.api.outbound.search; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.meilisearch.sdk.Client; import com.meilisearch.sdk.Config; import com.meilisearch.sdk.Index; @@ -7,18 +9,26 @@ import com.meilisearch.sdk.model.Results; import com.meilisearch.sdk.model.TaskInfo; import jakarta.annotation.PostConstruct; +import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import vook.server.api.model.Glossary; +import vook.server.api.model.Term; +import vook.server.api.model.TermSynonym; import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class MeilisearchService implements SearchService, SearchClearable { + private final ObjectMapper objectMapper; + @Value("${service.meilisearch.host:}") private String host; @@ -44,12 +54,47 @@ public void clearAll() { @Override public void createGlossary(Glossary glossary) { - TaskInfo index = client.createIndex(getIndexUid(glossary)); + TaskInfo index = client.createIndex(getIndexUid(glossary), "id"); client.waitForTask(index.getTaskUid()); } + @Override + public void addTerms(List terms, Glossary glossary) { + Index index = client.index(getIndexUid(glossary)); + TaskInfo taskInfo = index.addDocuments(getDocuments(terms)); + client.waitForTask(taskInfo.getTaskUid()); + } + @NotNull private static String getIndexUid(Glossary glossary) { return glossary.getUid(); } + + private String getDocuments(List terms) { + try { + return objectMapper.writeValueAsString(Document.from(terms)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @Getter + @AllArgsConstructor + public static class Document { + private Long id; + private String term; + private String synonyms; + private String meaning; + + public static List from(List terms) { + return terms.stream() + .map(w -> new Document( + w.getId(), + w.getTerm(), + w.getSynonyms().stream().map(TermSynonym::getSynonym).collect(Collectors.joining("\n")), + w.getMeaning() + )) + .toList(); + } + } } diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchService.java b/api/src/main/java/vook/server/api/outbound/search/SearchService.java index 4849592d..f3782fb3 100644 --- a/api/src/main/java/vook/server/api/outbound/search/SearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/SearchService.java @@ -1,7 +1,12 @@ package vook.server.api.outbound.search; import vook.server.api.model.Glossary; +import vook.server.api.model.Term; + +import java.util.List; public interface SearchService { void createGlossary(Glossary glossary); + + void addTerms(List terms, Glossary glossary); } From 7e6db5bf1d1cadeaf8147523a1d94ea9a77e2b7c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 19:59:51 +0900 Subject: [PATCH 036/273] =?UTF-8?q?feat:=20Csv=20Reader=EC=97=90=20InputSt?= =?UTF-8?q?ream=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/devhelper/CsvReader.java | 8 +++++++ .../server/api/devhelper/CsvReaderTest.java | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/api/src/main/java/vook/server/api/devhelper/CsvReader.java b/api/src/main/java/vook/server/api/devhelper/CsvReader.java index ad18ff3f..96daf631 100644 --- a/api/src/main/java/vook/server/api/devhelper/CsvReader.java +++ b/api/src/main/java/vook/server/api/devhelper/CsvReader.java @@ -17,6 +17,14 @@ public CsvReader(String delimiter) { this.DELIMITER = delimiter; } + public List readValue(InputStream inputStream, Class clazz) { + try { + return readValue(new InputStreamReader(inputStream), clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + public List readValue(File file, Class clazz) { try { return readValue(new FileReader(file), clazz); diff --git a/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java b/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java index 58c9ad10..da94642f 100644 --- a/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java +++ b/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java @@ -3,8 +3,11 @@ import lombok.Getter; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -57,6 +60,26 @@ void readValueFile() { ); } + @Test + @DisplayName("TSV Reader - InputStream 변환") + void readValueInputStream() throws IOException { + // given + InputStream inputStream = new ClassPathResource("tsvreader/test.tsv").getInputStream(); + + CsvReader reader = new CsvReader("\t"); + + // when + List entity = reader.readValue(inputStream, Term.class); + + // then + assertThat(entity).hasSize(2); + assertThat(entity).extracting("term", "synonyms", "description") + .containsExactly( + tuple("Java", "자바", "자바는 객체지향 프로그래밍 언어이다."), + tuple("Spring", "스프링", "스프링은 자바 기반의 프레임워크이다.") + ); + } + @Getter static class Term { private String term; From d171a51529da0d42c6f55bf82f33aea6575d471f Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 20:08:35 +0900 Subject: [PATCH 037/273] =?UTF-8?q?chore:=20=EB=A1=9C=EC=BB=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9A=A9=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=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 --- cicd/Makefile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cicd/Makefile b/cicd/Makefile index 11d4c6c8..d44f9202 100644 --- a/cicd/Makefile +++ b/cicd/Makefile @@ -11,7 +11,14 @@ deploy-dev: --version=${VERSION} \ --command="make deploy-api" -.PHONY:build -build: +.PHONY:build-jar +build-jar: dagger call -v build-api-jar --dir=../api --test -o out/api.jar + +.PHONY:build-image +build-image: build-jar dagger call -v build-api-image --jar-file=out/api.jar -o out/api_linux_arm64.tar + +.PHONY:run-jar +run-jar: + (cd out && java -jar api.jar) From 3145969f88fb8d897ffa2430b9347abed8d2e17d Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 20:08:05 +0900 Subject: [PATCH 038/273] =?UTF-8?q?fix:=20jar=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=95=20=ED=9B=84=20classpath=20=EB=82=B4=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=91=EA=B7=BC=EC=9D=B4=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InputStream 을 통해 접근하는 것으로 변경 ref) https://velog.io/@haron/트러블슈팅-Spring-IDE-에서-되는데-배포하면-안-돼요 --- .../main/java/vook/server/api/devhelper/InitService.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index d2126aa6..a135b289 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -9,8 +9,8 @@ import vook.server.api.outbound.search.SearchClearable; import vook.server.api.outbound.search.SearchService; -import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.List; @@ -54,9 +54,11 @@ public void init() { private List getTerms(String location, Glossary glossary) { try { - File tsvFile = resourceLoader.getResource(location).getFile(); + // file로 바로 접근 할 경우, IDE에서는 접근 가능하나, jar로 패키징 후 실행 시에는 접근 불가능 + // ref) https://velog.io/@haron/트러블슈팅-Spring-IDE-에서-되는데-배포하면-안-돼요 + InputStream tsvFileInputStream = resourceLoader.getResource(location).getInputStream(); CsvReader tsvReader = new CsvReader("\t"); - List rawTerms = tsvReader.readValue(tsvFile, RawTerm.class); + List rawTerms = tsvReader.readValue(tsvFileInputStream, RawTerm.class); return toTerms(rawTerms, glossary); } catch (IOException e) { throw new RuntimeException(e); From fb2ada7ac9399334c60edd7f8cd884f404a3296b Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 20:41:25 +0900 Subject: [PATCH 039/273] =?UTF-8?q?feat:=20=ED=8A=B9=EC=A0=95=20=EC=9A=A9?= =?UTF-8?q?=EC=96=B4=EC=A7=91=20=EB=82=B4=20=EC=9A=A9=EC=96=B4=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#33?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/GlossaryService.java | 5 +++ .../java/vook/server/api/app/TermService.java | 20 +++++++++ .../server/api/model/GlossaryRepository.java | 3 ++ .../main/java/vook/server/api/model/Term.java | 2 +- .../vook/server/api/model/TermRepository.java | 3 ++ .../routes/glossary/FindAllTermsResponse.java | 41 +++++++++++++++++++ .../api/web/routes/glossary/GlossaryApi.java | 17 ++++++++ .../glossary/GlossaryRestController.java | 8 ++++ .../routes/glossary/GlossaryWebService.java | 9 ++++ 9 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/vook/server/api/app/TermService.java create mode 100644 api/src/main/java/vook/server/api/web/routes/glossary/FindAllTermsResponse.java diff --git a/api/src/main/java/vook/server/api/app/GlossaryService.java b/api/src/main/java/vook/server/api/app/GlossaryService.java index 555abab0..3e4815e9 100644 --- a/api/src/main/java/vook/server/api/app/GlossaryService.java +++ b/api/src/main/java/vook/server/api/app/GlossaryService.java @@ -6,6 +6,7 @@ import vook.server.api.model.GlossaryRepository; import java.util.List; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -16,4 +17,8 @@ public class GlossaryService { public List findAll() { return repository.findAll(); } + + public Optional findByUid(String uid) { + return repository.findByUid(uid); + } } diff --git a/api/src/main/java/vook/server/api/app/TermService.java b/api/src/main/java/vook/server/api/app/TermService.java new file mode 100644 index 00000000..d656d613 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/TermService.java @@ -0,0 +1,20 @@ +package vook.server.api.app; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import vook.server.api.model.Glossary; +import vook.server.api.model.Term; +import vook.server.api.model.TermRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TermService { + + private final TermRepository repository; + + public List findAllBy(Glossary glossary) { + return repository.findAllByGlossary(glossary); + } +} diff --git a/api/src/main/java/vook/server/api/model/GlossaryRepository.java b/api/src/main/java/vook/server/api/model/GlossaryRepository.java index 9fb255ac..0d5aaca5 100644 --- a/api/src/main/java/vook/server/api/model/GlossaryRepository.java +++ b/api/src/main/java/vook/server/api/model/GlossaryRepository.java @@ -2,5 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface GlossaryRepository extends JpaRepository { + Optional findByUid(String uid); } diff --git a/api/src/main/java/vook/server/api/model/Term.java b/api/src/main/java/vook/server/api/model/Term.java index c793783e..43b0e623 100644 --- a/api/src/main/java/vook/server/api/model/Term.java +++ b/api/src/main/java/vook/server/api/model/Term.java @@ -25,7 +25,7 @@ public class Term extends BaseEntity { @JoinColumn(name = "glossary_id", nullable = false) private Glossary glossary; - @OneToMany(mappedBy = "term", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "term", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) private List synonyms = new ArrayList<>(); public static Term forCreateOf( diff --git a/api/src/main/java/vook/server/api/model/TermRepository.java b/api/src/main/java/vook/server/api/model/TermRepository.java index ca73f733..326d8a62 100644 --- a/api/src/main/java/vook/server/api/model/TermRepository.java +++ b/api/src/main/java/vook/server/api/model/TermRepository.java @@ -2,5 +2,8 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface TermRepository extends JpaRepository { + List findAllByGlossary(Glossary glossary); } diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/FindAllTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/glossary/FindAllTermsResponse.java new file mode 100644 index 00000000..a42f3364 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/glossary/FindAllTermsResponse.java @@ -0,0 +1,41 @@ +package vook.server.api.web.routes.glossary; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; +import vook.server.api.model.Glossary; +import vook.server.api.model.Term; +import vook.server.api.model.TermSynonym; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +public class FindAllTermsResponse { + + private String term; + + private List synonyms; + + private String meaning; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + private String createdBy; + + public static List from(Glossary glossary, List terms) { + return terms.stream() + .map(term -> from(glossary, term)) + .toList(); + } + + public static FindAllTermsResponse from(Glossary glossary, Term term) { + FindAllTermsResponse response = new FindAllTermsResponse(); + response.term = term.getTerm(); + response.synonyms = term.getSynonyms().stream().map(TermSynonym::getSynonym).toList(); + response.meaning = term.getMeaning(); + response.createdAt = term.getCreatedAt(); + response.createdBy = glossary.getMember().getName(); + return response; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java index ae45239e..fcc47645 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java @@ -28,4 +28,21 @@ public interface GlossaryApi { class RetrieveApiResponse extends CommonApiResponse> { } + + @Operation(summary = "용어집 내 용어 리스트 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = FindAllTermsApiResponse.class) + ) + ), + }) + CommonApiResponse> findAllTerms(String glossaryUid); + + class FindAllTermsApiResponse extends CommonApiResponse> { + } + } diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java index b696d399..1c459124 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import vook.server.api.model.Glossary; @@ -22,4 +23,11 @@ public CommonApiResponse> retrieve() { List result = service.retrieve(); return CommonApiResponse.okWithResult(RetrieveResponse.from(result)); } + + @Override + @GetMapping("/{glossaryUid}/terms") + public CommonApiResponse> findAllTerms(@PathVariable String glossaryUid) { + List result = service.findAllTerms(glossaryUid); + return CommonApiResponse.okWithResult(result); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java index 627c6eb0..2f9a09c1 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java @@ -4,7 +4,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.GlossaryService; +import vook.server.api.app.TermService; import vook.server.api.model.Glossary; +import vook.server.api.model.Term; import java.util.List; @@ -14,8 +16,15 @@ public class GlossaryWebService { private final GlossaryService glossaryService; + private final TermService termService; public List retrieve() { return glossaryService.findAll(); } + + public List findAllTerms(String glossaryUid) { + Glossary glossary = glossaryService.findByUid(glossaryUid).orElseThrow(); + List terms = termService.findAllBy(glossary); + return FindAllTermsResponse.from(glossary, terms); + } } From 8438f1b080f2d97bbd06a0d092a3a9f38585c4cf Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 20:42:31 +0900 Subject: [PATCH 040/273] =?UTF-8?q?refector:=20=EC=9D=BC=EA=B4=80=EC=84=B1?= =?UTF-8?q?=20=ED=96=A5=EC=83=81=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/web/routes/glossary/GlossaryRestController.java | 5 ++--- .../server/api/web/routes/glossary/GlossaryWebService.java | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java index 1c459124..59f9e329 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java @@ -5,7 +5,6 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import vook.server.api.model.Glossary; import vook.server.api.web.common.CommonApiResponse; import java.util.List; @@ -20,8 +19,8 @@ public class GlossaryRestController implements GlossaryApi { @Override @GetMapping public CommonApiResponse> retrieve() { - List result = service.retrieve(); - return CommonApiResponse.okWithResult(RetrieveResponse.from(result)); + List result = service.retrieve(); + return CommonApiResponse.okWithResult(result); } @Override diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java index 2f9a09c1..306fe94c 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java @@ -18,8 +18,9 @@ public class GlossaryWebService { private final GlossaryService glossaryService; private final TermService termService; - public List retrieve() { - return glossaryService.findAll(); + public List retrieve() { + List glossaries = glossaryService.findAll(); + return RetrieveResponse.from(glossaries); } public List findAllTerms(String glossaryUid) { From 47b6b8904aa043039c52dbad79cecb321df90695 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 20:59:31 +0900 Subject: [PATCH 041/273] =?UTF-8?q?fix:=20=EC=A4=80=EB=B9=84=ED=95=9C=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=EC=9A=A9=EC=96=B4=EC=A7=91=EC=97=90=202?= =?UTF-8?q?=EA=B0=9C=20=EC=9D=B4=EC=83=81=EC=9D=98=20=EB=8F=99=EC=9D=98?= =?UTF-8?q?=EC=96=B4=EA=B0=80=20=EC=9E=88=EC=9D=84=20=EB=95=8C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=8C=80=EC=9D=91=20#34?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \n => //n 으로 변경 --- .../java/vook/server/api/devhelper/InitService.java | 3 ++- .../resources/init/\352\260\234\353\260\234.tsv" | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index a135b289..aa417c15 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -78,7 +78,8 @@ public static class RawTerm { public Term toTerm(Glossary glossary) { Term term = Term.forCreateOf(this.term, this.meaning, glossary); - Arrays.stream(this.synonyms.split("\\n")) + String[] synonymArray = this.synonyms.split("//n"); + Arrays.stream(synonymArray) .map(String::trim) .forEach(term::addSynonym); return term; diff --git "a/api/src/main/resources/init/\352\260\234\353\260\234.tsv" "b/api/src/main/resources/init/\352\260\234\353\260\234.tsv" index 0698203b..56ed03b0 100644 --- "a/api/src/main/resources/init/\352\260\234\353\260\234.tsv" +++ "b/api/src/main/resources/init/\352\260\234\353\260\234.tsv" @@ -23,14 +23,14 @@ SOAP API Simple Object Access Protocol API 웹 서비스를 위한 표준 프로 RPC Remote Procedure Call 네트워크를 통해 원격 시스템에 있는 함수를 마치 로컬 시스템에 있는 함수처럼 호출하는 기술 개발자는 호출되는 함수가 다른 시스템에 있는지 여부를 확인하지 않고 함수를 호출할 수 있습니다. RPC는 분산 시스템에서 서로 다른 시스템 간 통신과 자원 공유를 용이하게 만들며, 클라이언트 - 서버 애플리케이션 개발에 많이 사용됩니다. 웹소캣 Websocket API 웹 클라이언트와 웹 서버 간에 지속적인 양방향 실시간 통신을 가능하게 하는 API 기존의 HTTP 기반 웹 요청과 달리 WebSocket은 한 번의 TCP 연결을 통해 클라이언트와 서버 간에 지속적으로 데이터를 주고받을 수 있으므로, 채팅, 게임, 실시간 데이터 갱신 등 실시간으로 데이터 송수신이 필요한 애플리케이션에 적합합니다. REST API Representational State Transfer API REST 아키텍처 스타일의 디자인 원칙을 준수하는 API REST API를 RESTful API라고 불리기도 합니다. REST API는 일관되고 이해하기 쉬운 인터페이스를 제공하며, 다양한 플랫폼과 프로그래밍 언어에서 쉽게 사용할 수 있는 장점이 있습니다. REST API는 HTTP method를 사용하여 자원에 대한 작업을 수행합니다. 일반적으로 사용되는 HTTP method는 다음과 같습니다. GET: 자원을 조회합니다. POST: 자원을 생성합니다. PUT: 자원을 업데이트합니다. DELETE: 자원을 삭제합니다. -HTTP Hypertext Transfer Protocol\n하이퍼텍스트 전송 프로토콜 웹 브라우저와 웹 서버 간의 통신을 위한 기본 프로토콜 웹사이트 방문 시 사용자가 입력한 URL을 요청하고, 웹 서버는 요청에 맞는 웹 페이지나 다른 데이터를 응답으로 전송하는 데 사용됩니다. HTTP는 기본적으로 데이터를 평문으로 전송하기 때문에 도청이나 위변조 위험이 있어 최근에는 HTTPS를 사용합니다. +HTTP Hypertext Transfer Protocol//n하이퍼텍스트 전송 프로토콜 웹 브라우저와 웹 서버 간의 통신을 위한 기본 프로토콜 웹사이트 방문 시 사용자가 입력한 URL을 요청하고, 웹 서버는 요청에 맞는 웹 페이지나 다른 데이터를 응답으로 전송하는 데 사용됩니다. HTTP는 기본적으로 데이터를 평문으로 전송하기 때문에 도청이나 위변조 위험이 있어 최근에는 HTTPS를 사용합니다. HTTPS Hypertext Transfer Protocol Secure HTTP에 SSL/TLS 프로토콜을 추가하여 안전하게 데이터를 전송하도록 보안 강화한 프로토콜입니다. HTTPS는 웹 브라우저와 웹 서버 간 모든 통신을 암호화해 도청이나 위변조를 방지합니다. 웹 서버의 신원을 인증해 위조 사이트로부터 사용자를 보호하며, 데이터 전송 과정에서 데이터가 변경되지 않았는지 확인합니다. -SSL Secure Sockets Layer\n보안 소켓 계층 웹 브라우저와 웹 서버 간의 통신을 암호화하여 보안하는 프로토콜 SSL은 현재 TLS(Transport Layer Security)로 이름이 변경되었지만, 여전히 SSL이라는 용어가 널리 사용되고 있습니다. +SSL Secure Sockets Layer//n보안 소켓 계층 웹 브라우저와 웹 서버 간의 통신을 암호화하여 보안하는 프로토콜 SSL은 현재 TLS(Transport Layer Security)로 이름이 변경되었지만, 여전히 SSL이라는 용어가 널리 사용되고 있습니다. TLS 전송 계층 보안 인터넷 상의 두 시스템 간의 통신을 보호하는 보안 프로토콜 데이터 암호화, 신원 인증, 데이터 무결성 보장 기능을 제공하여 사용자의 개인정보, 로그인 정보, 금융 정보 등을 안전하게 보호합니다. 이전에 사용되었던 SSL(Secure Sockets Layer) 프로토콜의 후속 버전입니다. 프레임워크 Framework 소프트웨어 개발 과정을 돕는 도구와 라이브러리의 집합 프레임워크는 개발자가 애플리케이션의 기본 구조 및 핵심 기능을 빠르고 쉽게 구축할 수 있도록 기본적인 뼈대를 제공합니다. 대표적으로 웹에선 Django, Ruby on Rails 등이 있으며, 모바일에선 React Native, Flutter 등이 있습니다. 라이브러리 Library 라이브러리는 개발자가 특정 기능이나 작업을 수행할 수 있도록 재사용 가능한 코드 모듈을 제공합니다. 가령 NumPy, Pandas 등 데이터 처리 관련 라이브러리는 복잡한 데이터 분석 및 처리 작업을 수행하는 데 도움을 줍니다. 플러그인 Plug-in 기존 소프트웨어에 새로운 기능을 추가하거나 기존 기능을 확장하는 소프트웨어 구성 요소 일반적으로 독립적인 프로그램으로 작동하며, 호환되는 호스트 프로그램에 설치해야 합니다. 플러그인은 다양한 분야에서 사용되며, 웹 브라우저, 그래픽 편집 프로그램, 비디오 편집 프로그램, 게임 엔진 등에서 흔히 볼 수 있습니다. -파라미터 인자\nParameter 함수는 특정 작업을 수행하도록 설계된 코드 블록입니다. 함수를 호출할 때 원하는 결과를 얻도록 함수에 데이터를 전달해야 합니다. 이 데이터를 파라미터라고 부릅니다. +파라미터 인자//nParameter 함수는 특정 작업을 수행하도록 설계된 코드 블록입니다. 함수를 호출할 때 원하는 결과를 얻도록 함수에 데이터를 전달해야 합니다. 이 데이터를 파라미터라고 부릅니다. Argument 인자값 인자값은 함수 호출 시 실제 값을 의미하며, 파라미터에 전달됩니다. 인자값은 다양한 형태의 데이터, 숫자, 문자열, 리스트, 객체 등을 포함할 수 있습니다. 함수는 전달된 인자값을 사용하여 계산을 수행하거나 작업을 처리합니다. AJAX Asynchronous Javascript and XML 비동기 JavaScript 및 XML의 약자로, 웹 페이지를 부분적으로 다시 로드하지 않고도 서버와 데이터를 주고받을 수 있는 웹 개발 기술 AJAX를 사용하면 사용자가 웹 페이지를 새로고침하지 않고도 데이터를 업데이트하고 새로운 콘텐츠를 로드할 수 있어 웹사이트의 사용자 경험을 크게 향상시킬 수 있습니다. 대표적으로 검색창에 입력하는 단어에 대한 자동 완성 기능, 무한 스크롤, 실시간 데이터 업데이트 등에 활용됩니다. 멀티스레드 Multi-thread 하나의 프로그램에서 동시에 여러 개의 작업을 수행하는 프로그래밍 방식입니다. 멀티스레드를 사용하면 컴퓨터의 여러 개의 CPU 코어를 효율적으로 활용하여 프로그램 성능을 향상시킬 수 있습니다. @@ -38,13 +38,13 @@ AJAX Asynchronous Javascript and XML 비동기 JavaScript 및 XML의 약자로, 샌드박스 SandBox 실제 환경에 영향을 미치지 않고 새로운 소프트웨어나 기능을 테스트할 수 있는 가상의 공간입니다. 샌드박스는 개발자가 버그를 찾고 코드를 개선할 수 있는 안전한 환경을 제공합니다. 데이터 레이크 Data lake 구조화된 또는 반구조화된, 혹은 구조화되지 않은 방대한 양의 데이터를 저장하는 저장소 데이터 레이크는 기존의 데이터 웨어하우스와는 다릅니다. 데이터 웨어하우스는 일반적으로 미리 정의된 스키마를 사용하여 구조화된 데이터를 저장하는 반면, 데이터 레이크는 스키마 없이 데이터를 저장할 수 있습니다. 즉, 데이터 레이크에는 모든 유형의 데이터를 저장할 수 있으며, 데이터가 저장된 후에도 데이터 스키마를 변경할 수 있습니다. 데브옵스 DevOps 데브옵스(DevOps)는 소프트웨어 개발(Development)과 운영(Operations)을 하나의 통합된 프로세스로 연결하여 소프트웨어를 빠르게, 안정적으로, 그리고 효율적으로 제공하는 문화와 관행들의 집합 데브옵스는 개발팀과 운영팀 간의 협업을 강조하며, 자동화, 지속적인 통합 및 배포, 모니터링 등을 통해 소프트웨어 개발 및 제공 프로세스를 개선하는 데 중점을 둡니다. -CI/CD Continuous Integration/Continuous Delivery\n지속적인 통합/지속적인 배포 소프트웨어 개발 프로세스를 자동화하여 소프트웨어를 빠르고 안정적으로 제공하는 방법 CI/CD는 다음 두 단계로 구성됩니다. - 지속적인 통합(Continuous Integration, CI): 개발자가 코드를 변경할 때마다 코드를 자동으로 통합하고 테스트하는 프로세스입니다. CI는 개발 중에 발생하는 버그를 빠르게 발견하고 해결하는 데 도움이 됩니다. - 지속적인 배포(Continuous Delivery, CD): 테스트를 통과한 코드를 자동으로 배포 환경에 배포하는 프로세스입니다. CD는 소프트웨어를 빠르게 출시하고 업데이트하는 데 도움이 됩니다. +CI/CD Continuous Integration/Continuous Delivery//n지속적인 통합/지속적인 배포 소프트웨어 개발 프로세스를 자동화하여 소프트웨어를 빠르고 안정적으로 제공하는 방법 CI/CD는 다음 두 단계로 구성됩니다. - 지속적인 통합(Continuous Integration, CI): 개발자가 코드를 변경할 때마다 코드를 자동으로 통합하고 테스트하는 프로세스입니다. CI는 개발 중에 발생하는 버그를 빠르게 발견하고 해결하는 데 도움이 됩니다. - 지속적인 배포(Continuous Delivery, CD): 테스트를 통과한 코드를 자동으로 배포 환경에 배포하는 프로세스입니다. CD는 소프트웨어를 빠르게 출시하고 업데이트하는 데 도움이 됩니다. 파싱 Parsing 컴퓨터 과학에서 특정 형식으로 구성된 데이터를 분석하고 해석하는 과정 파싱은 주로 텍스트 기반 데이터, 프로그래밍 언어 소스 코드, XML 문서 등을 처리하는 데 사용됩니다. 파싱 과정에서 데이터의 구조를 이해하고 의미 있는 정보를 추출합니다. 핑 Ping 컴퓨터 네트워크에서 두 장치 간의 연결성을 테스트하는 데 사용되는 도구 핑은 다음과 같은 다양한 상황에서 사용됩니다. - 네트워크 문제 진단: 네트워크 연결이 끊겨 있거나 속도가 느린 경우 핑을 사용하여 문제의 원인을 파악할 수 있습니다. - 웹 사이트 성능 테스트: 웹 사이트에 연결하는 데 걸리는 시간을 측정하는 데 핑을 사용할 수 있습니다. - 새로운 네트워크 장치 설정 확인: 새로 설치한 네트워크 장치가 올바르게 작동하는지 확인하는 데 핑을 사용할 수 있습니다. SRE Site Reliability Engineering 소프트웨어 시스템의 안정성, 확장성, 및 성능을 유지하는 것을 담당하는 엔지니어링 분야 SRE는 전통적인 소프트웨어 개발 및 운영 팀의 역할을 결합하여 소프트웨어를 서비스로 제공하는 데 필요한 모든 측면을 관리합니다. SSH Secure Shell Protocol 네트워크를 통해 두 컴퓨터 간의 안전한 연결을 제공하는 프로토콜 SSH는 다음과 같은 다양한 목적으로 사용됩니다. - 원격 컴퓨터 관리: 시스템 관리자는 SSH를 사용하여 원격 컴퓨터에 로그인하고 관리 작업을 수행할 수 있습니다. - 파일 전송: 사용자는 SSH를 사용하여 두 컴퓨터 간에 파일을 안전하게 전송할 수 있습니다. - 애플리케이션 실행: 사용자는 SSH를 사용하여 원격 컴퓨터에서 애플리케이션을 실행할 수 있습니다. - 포트 포워딩: 사용자는 SSH를 사용하여 한 컴퓨터의 포트를 다른 컴퓨터의 포트로 전달할 수 있습니다. -성능 테스트 BMT\nBench Marking Test 한 가지 시스템이나 제품의 성능을 객관적으로 측정하고 비교하기 위한 테스트 BMT는 일반적으로 동일한 조건에서 서로 다른 시스템 또는 제품을 테스트하여 어떤 시스템 또는 제품이 더 우수한 성능을 제공하는지를 확인하는 데 사용됩니다. -gRPC Google RPC\nGoogle Remote Procedure Call RPC(Remote Procedure Call)는 네트워크를 통해 원격 시스템에 있는 프로시저를 호출하는 기술입니다. RPC는 마치 로컬 시스템에서 함수를 호출하는 것처럼 원격 시스템의 프로시저를 호출할 수 있도록 해줍니다. gRPC는 Google에서 개발한 오픈 소스 RPC 프레임워크입니다. +성능 테스트 BMT//nBench Marking Test 한 가지 시스템이나 제품의 성능을 객관적으로 측정하고 비교하기 위한 테스트 BMT는 일반적으로 동일한 조건에서 서로 다른 시스템 또는 제품을 테스트하여 어떤 시스템 또는 제품이 더 우수한 성능을 제공하는지를 확인하는 데 사용됩니다. +gRPC Google RPC//nGoogle Remote Procedure Call RPC(Remote Procedure Call)는 네트워크를 통해 원격 시스템에 있는 프로시저를 호출하는 기술입니다. RPC는 마치 로컬 시스템에서 함수를 호출하는 것처럼 원격 시스템의 프로시저를 호출할 수 있도록 해줍니다. gRPC는 Google에서 개발한 오픈 소스 RPC 프레임워크입니다. NAS Network Attached Storage 네트워크를 통해 여러 사용자가 파일 저장 및 공유를 할 수 있도록 하는 장치 NAS는 다양한 용도로 사용됩니다. 개인 용도로는 사진, 음악, 비디오 등의 개인 파일을 저장하고 공유하는 데 사용할 수 있습니다. 사업 용도로는 문서, 스프레드시트, 프레젠테이션과 같은 업무 파일을 저장하고 공유하는 데 사용할 수 있습니다. 또한 NAS는 데이터 백업, 파일 스트리밍, 미디어 서버 등으로도 사용할 수 있습니다. CDN Contents Delivery Network 웹 콘텐츠를 사용자에게 더 빠르고 안정적으로 제공하기 위해 지리적으로 분산된 네트워크를 사용하는 기술 웹사이트, 이미지, 동영상, 스트리밍 미디어 등 다양한 콘텐츠를 빠르게 전송하는 데 효과적으로 사용됩니다. DNS Domain Name System 인터넷 주소록이라고도 불리는 시스템으로, 도메인 이름(예: www.example.com)을 IP 주소(예: 192.0.2.44)로 변환하는 역할을 합니다. 쉽게 말해, 사람이 쉽게 기억할 수 있는 도메인 이름을 컴퓨터가 이해할 수 있는 IP 주소로 변환하는 시스템이라고 볼 수 있습니다. From 41eac2f56d63852cd853a294cabf3f4614e25a89 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 21:04:46 +0900 Subject: [PATCH 042/273] =?UTF-8?q?docs:=20model=EC=97=90=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/vook/server/api/model/Glossary.java | 10 ++++++++++ api/src/main/java/vook/server/api/model/Member.java | 6 ++++++ api/src/main/java/vook/server/api/model/Term.java | 9 +++++++++ .../main/java/vook/server/api/model/TermSynonym.java | 6 ++++++ 4 files changed, 31 insertions(+) diff --git a/api/src/main/java/vook/server/api/model/Glossary.java b/api/src/main/java/vook/server/api/model/Glossary.java index 68a7c3b1..376de763 100644 --- a/api/src/main/java/vook/server/api/model/Glossary.java +++ b/api/src/main/java/vook/server/api/model/Glossary.java @@ -5,6 +5,9 @@ import java.util.UUID; +/** + * 용어집 + */ @Getter @Entity @Table(name = "glossary") @@ -14,7 +17,14 @@ public class Glossary extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + /** + * UID; 외부 노출용 식별자 + */ private String uid; + + /** + * 용어집 이름 + */ private String name; @ManyToOne(fetch = FetchType.LAZY) diff --git a/api/src/main/java/vook/server/api/model/Member.java b/api/src/main/java/vook/server/api/model/Member.java index b5e75a50..2548cda7 100644 --- a/api/src/main/java/vook/server/api/model/Member.java +++ b/api/src/main/java/vook/server/api/model/Member.java @@ -3,6 +3,9 @@ import jakarta.persistence.*; import lombok.Getter; +/** + * 회원 + */ @Getter @Entity @Table(name = "member") @@ -12,6 +15,9 @@ public class Member extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + /** + * 회원 이름 + */ private String name; public static Member forCreateOf(String name) { diff --git a/api/src/main/java/vook/server/api/model/Term.java b/api/src/main/java/vook/server/api/model/Term.java index 43b0e623..ea551bf5 100644 --- a/api/src/main/java/vook/server/api/model/Term.java +++ b/api/src/main/java/vook/server/api/model/Term.java @@ -6,6 +6,9 @@ import java.util.ArrayList; import java.util.List; +/** + * 용어 + */ @Getter @Entity @Table(name = "term") @@ -15,9 +18,15 @@ public class Term extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + /** + * 용어 이름 + */ @Column(length = 100, nullable = false) private String term; + /** + * 용어 의미 + */ @Column(length = 2000, nullable = false) private String meaning; diff --git a/api/src/main/java/vook/server/api/model/TermSynonym.java b/api/src/main/java/vook/server/api/model/TermSynonym.java index 98b5e1c3..ab1c76bf 100644 --- a/api/src/main/java/vook/server/api/model/TermSynonym.java +++ b/api/src/main/java/vook/server/api/model/TermSynonym.java @@ -3,6 +3,9 @@ import jakarta.persistence.*; import lombok.Getter; +/** + * 용어 동의어 + */ @Getter @Entity @Table(name = "term_synonym") @@ -12,6 +15,9 @@ public class TermSynonym { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + /** + * 동의어 + */ @Column(length = 100, nullable = false) private String synonym; From 91d39c38675bc516c7174e545e64a019b86ba847 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 21:06:08 +0900 Subject: [PATCH 043/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91=20u?= =?UTF-8?q?id=EC=97=90=20=EC=A0=9C=EC=95=BD=EC=82=AC=ED=95=AD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/vook/server/api/model/Glossary.java | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/main/java/vook/server/api/model/Glossary.java b/api/src/main/java/vook/server/api/model/Glossary.java index 376de763..6c28e0aa 100644 --- a/api/src/main/java/vook/server/api/model/Glossary.java +++ b/api/src/main/java/vook/server/api/model/Glossary.java @@ -20,6 +20,7 @@ public class Glossary extends BaseEntity { /** * UID; 외부 노출용 식별자 */ + @Column(length = 36, nullable = false, unique = true) private String uid; /** From 775046a00de2ac889da3f1de5907c70ff66134b3 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 22:29:47 +0900 Subject: [PATCH 044/273] =?UTF-8?q?chore:=20querydsl=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle | 6 +++ .../server/api/config/QuerydslConfig.java | 19 ++++++++++ .../server/api/helper/QuerydslHelper.java | 37 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 api/src/main/java/vook/server/api/config/QuerydslConfig.java create mode 100644 api/src/main/java/vook/server/api/helper/QuerydslHelper.java diff --git a/api/build.gradle b/api/build.gradle index d63cd82d..67458798 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -46,6 +46,12 @@ dependencies { // h2 database testRuntimeOnly 'com.h2database:h2' + + // QueryDsl + implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" } bootJar { diff --git a/api/src/main/java/vook/server/api/config/QuerydslConfig.java b/api/src/main/java/vook/server/api/config/QuerydslConfig.java new file mode 100644 index 00000000..e1066f08 --- /dev/null +++ b/api/src/main/java/vook/server/api/config/QuerydslConfig.java @@ -0,0 +1,19 @@ +package vook.server.api.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/api/src/main/java/vook/server/api/helper/QuerydslHelper.java b/api/src/main/java/vook/server/api/helper/QuerydslHelper.java new file mode 100644 index 00000000..ac5f50a7 --- /dev/null +++ b/api/src/main/java/vook/server/api/helper/QuerydslHelper.java @@ -0,0 +1,37 @@ +package vook.server.api.helper; + +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.EntityPathBase; +import com.querydsl.core.types.dsl.PathBuilder; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class QuerydslHelper { + public static > List> toOrderSpecifiers( + EntityPathBase qClass, + Pageable pageable, + OrderSpecifier... defaultOrderSpecifier + ) { + List> result = new ArrayList<>(); + + if (pageable.getSort().isSorted()) { + for (Sort.Order o : pageable.getSort()) { + PathBuilder pathBuilder = new PathBuilder<>(qClass.getType(), qClass.getMetadata()); + OrderSpecifier order = new OrderSpecifier( + o.isAscending() ? Order.ASC : Order.DESC, + pathBuilder.get(o.getProperty()) + ); + result.add(order); + } + } else { + Collections.addAll(result, defaultOrderSpecifier); + } + + return result; + } +} From 7400dffce99600befca67688a1da5dfa40cb3bff Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 11 May 2024 21:35:30 +0900 Subject: [PATCH 045/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91=20?= =?UTF-8?q?=EB=82=B4=20=EC=9A=A9=EC=96=B4=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9A=A9=EC=96=B4=20=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EC=A0=81=EC=9A=A9=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/app/TermSearchRepository.java | 49 +++++++++++++++++++ .../java/vook/server/api/app/TermService.java | 8 +-- .../vook/server/api/model/TermRepository.java | 3 -- .../api/web/routes/glossary/GlossaryApi.java | 11 +++-- .../glossary/GlossaryRestController.java | 9 +++- .../routes/glossary/GlossaryWebService.java | 7 +-- ...sponse.java => RetrieveTermsResponse.java} | 8 +-- 7 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/TermSearchRepository.java rename api/src/main/java/vook/server/api/web/routes/glossary/{FindAllTermsResponse.java => RetrieveTermsResponse.java} (77%) diff --git a/api/src/main/java/vook/server/api/app/TermSearchRepository.java b/api/src/main/java/vook/server/api/app/TermSearchRepository.java new file mode 100644 index 00000000..3ee1bbbb --- /dev/null +++ b/api/src/main/java/vook/server/api/app/TermSearchRepository.java @@ -0,0 +1,49 @@ +package vook.server.api.app; + +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import vook.server.api.helper.QuerydslHelper; +import vook.server.api.model.QTerm; +import vook.server.api.model.Term; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class TermSearchRepository { + + private final JPAQueryFactory queryFactory; + + public List search(String glossaryUid, Pageable pageable) { + QTerm term = QTerm.term1; + + // fetch join과 pagination을 같이 하면, + // HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory + // 에러가 발생, 이를 피하기 위해 pagination이 포함된 쿼리를 서브 쿼리로 하여 수행 + JPAQuery termIdQuery = queryFactory + .select(term.id) + .from(term) + .where(term.glossary.uid.eq(glossaryUid)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()); + + QuerydslHelper + .toOrderSpecifiers(term, pageable) + .forEach(termIdQuery::orderBy); + + JPAQuery dataQuery = queryFactory + .selectFrom(term) + .leftJoin(term.synonyms).fetchJoin() + .where(term.id.in(termIdQuery)); + + // In 절에 들어간 입력순대로 출력되지 않음으로 정렬 조건을 다시 한번 설정 + QuerydslHelper + .toOrderSpecifiers(term, pageable) + .forEach(dataQuery::orderBy); + + return dataQuery.fetch(); + } +} diff --git a/api/src/main/java/vook/server/api/app/TermService.java b/api/src/main/java/vook/server/api/app/TermService.java index d656d613..2e997998 100644 --- a/api/src/main/java/vook/server/api/app/TermService.java +++ b/api/src/main/java/vook/server/api/app/TermService.java @@ -1,10 +1,10 @@ package vook.server.api.app; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import vook.server.api.model.Glossary; import vook.server.api.model.Term; -import vook.server.api.model.TermRepository; import java.util.List; @@ -12,9 +12,9 @@ @RequiredArgsConstructor public class TermService { - private final TermRepository repository; + private final TermSearchRepository searchRepository; - public List findAllBy(Glossary glossary) { - return repository.findAllByGlossary(glossary); + public List findAllBy(Glossary glossary, Pageable pageable) { + return searchRepository.search(glossary.getUid(), pageable); } } diff --git a/api/src/main/java/vook/server/api/model/TermRepository.java b/api/src/main/java/vook/server/api/model/TermRepository.java index 326d8a62..ca73f733 100644 --- a/api/src/main/java/vook/server/api/model/TermRepository.java +++ b/api/src/main/java/vook/server/api/model/TermRepository.java @@ -2,8 +2,5 @@ import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - public interface TermRepository extends JpaRepository { - List findAllByGlossary(Glossary glossary); } diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java index fcc47645..289891e1 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java @@ -6,6 +6,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.Pageable; import vook.server.api.web.common.CommonApiResponse; import java.util.List; @@ -36,13 +38,16 @@ class RetrieveApiResponse extends CommonApiResponse> { description = "성공", content = @Content( mediaType = "application/json", - schema = @Schema(implementation = FindAllTermsApiResponse.class) + schema = @Schema(implementation = RetrieveTermsApiResponse.class) ) ), }) - CommonApiResponse> findAllTerms(String glossaryUid); + CommonApiResponse> retrieveTerms( + String glossaryUid, + @ParameterObject Pageable pageable + ); - class FindAllTermsApiResponse extends CommonApiResponse> { + class RetrieveTermsApiResponse extends CommonApiResponse> { } } diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java index 59f9e329..82b7f207 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java @@ -1,6 +1,8 @@ package vook.server.api.web.routes.glossary; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,8 +27,11 @@ public CommonApiResponse> retrieve() { @Override @GetMapping("/{glossaryUid}/terms") - public CommonApiResponse> findAllTerms(@PathVariable String glossaryUid) { - List result = service.findAllTerms(glossaryUid); + public CommonApiResponse> retrieveTerms( + @PathVariable String glossaryUid, + @PageableDefault(size = Integer.MAX_VALUE, sort = "term") Pageable pageable + ) { + List result = service.retrieveTerms(glossaryUid, pageable); return CommonApiResponse.okWithResult(result); } } diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java index 306fe94c..1e0e0bef 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java @@ -1,6 +1,7 @@ package vook.server.api.web.routes.glossary; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.GlossaryService; @@ -23,9 +24,9 @@ public List retrieve() { return RetrieveResponse.from(glossaries); } - public List findAllTerms(String glossaryUid) { + public List retrieveTerms(String glossaryUid, Pageable pageable) { Glossary glossary = glossaryService.findByUid(glossaryUid).orElseThrow(); - List terms = termService.findAllBy(glossary); - return FindAllTermsResponse.from(glossary, terms); + List terms = termService.findAllBy(glossary, pageable); + return RetrieveTermsResponse.from(glossary, terms); } } diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/FindAllTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveTermsResponse.java similarity index 77% rename from api/src/main/java/vook/server/api/web/routes/glossary/FindAllTermsResponse.java rename to api/src/main/java/vook/server/api/web/routes/glossary/RetrieveTermsResponse.java index a42f3364..4d746106 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/FindAllTermsResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveTermsResponse.java @@ -10,7 +10,7 @@ import java.util.List; @Getter -public class FindAllTermsResponse { +public class RetrieveTermsResponse { private String term; @@ -23,14 +23,14 @@ public class FindAllTermsResponse { private String createdBy; - public static List from(Glossary glossary, List terms) { + public static List from(Glossary glossary, List terms) { return terms.stream() .map(term -> from(glossary, term)) .toList(); } - public static FindAllTermsResponse from(Glossary glossary, Term term) { - FindAllTermsResponse response = new FindAllTermsResponse(); + public static RetrieveTermsResponse from(Glossary glossary, Term term) { + RetrieveTermsResponse response = new RetrieveTermsResponse(); response.term = term.getTerm(); response.synonyms = term.getSynonyms().stream().map(TermSynonym::getSynonym).toList(); response.meaning = term.getMeaning(); From 711f4ab7b8d1bd8075c6bb2c227b1f5ac09c74b7 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 12 May 2024 13:33:03 +0900 Subject: [PATCH 046/273] =?UTF-8?q?chore:=20devenv=EC=9D=98=20meilisearch?= =?UTF-8?q?=20version=20up=20#39?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devenv/meilisearch/compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/meilisearch/compose.yml b/devenv/meilisearch/compose.yml index 7e2bcf6b..ce149673 100644 --- a/devenv/meilisearch/compose.yml +++ b/devenv/meilisearch/compose.yml @@ -1,6 +1,6 @@ services: meilisearch: - image: getmeili/meilisearch:v1.7.6 + image: getmeili/meilisearch:v1.8.0 volumes: - meili_data:/meili_data ports: From 548bfccb6a0bae3d9645b1e00da221a81ebc4910 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 12 May 2024 13:55:44 +0900 Subject: [PATCH 047/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=20=EC=B6=94=EA=B0=80=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../outbound/search/MeilisearchService.java | 14 +++++++++++ .../api/outbound/search/SearchParams.java | 18 ++++++++++++++ .../api/outbound/search/SearchResult.java | 23 ++++++++++++++++++ .../api/outbound/search/SearchService.java | 2 ++ .../api/web/routes/term/SearchRequest.java | 16 +++++++++++++ .../api/web/routes/term/SearchResponse.java | 21 ++++++++++++++++ .../web/routes/term/TermRestController.java | 24 +++++++++++++++++++ .../api/web/routes/term/TermWebService.java | 24 +++++++++++++++++++ 8 files changed, 142 insertions(+) create mode 100644 api/src/main/java/vook/server/api/outbound/search/SearchParams.java create mode 100644 api/src/main/java/vook/server/api/outbound/search/SearchResult.java create mode 100644 api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java create mode 100644 api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java create mode 100644 api/src/main/java/vook/server/api/web/routes/term/TermRestController.java create mode 100644 api/src/main/java/vook/server/api/web/routes/term/TermWebService.java diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java index f3edba3c..f96b13dc 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -5,8 +5,10 @@ import com.meilisearch.sdk.Client; import com.meilisearch.sdk.Config; import com.meilisearch.sdk.Index; +import com.meilisearch.sdk.SearchRequest; import com.meilisearch.sdk.model.IndexesQuery; import com.meilisearch.sdk.model.Results; +import com.meilisearch.sdk.model.Searchable; import com.meilisearch.sdk.model.TaskInfo; import jakarta.annotation.PostConstruct; import lombok.AllArgsConstructor; @@ -65,6 +67,18 @@ public void addTerms(List terms, Glossary glossary) { client.waitForTask(taskInfo.getTaskUid()); } + @Override + public SearchResult search(SearchParams params) { + SearchRequest searchRequest = SearchRequest.builder() + .q(params.getQuery()) + .attributesToHighlight(new String[]{"*"}) + .highlightPreTag("") + .highlightPostTag("") + .build(); + Searchable search = this.client.getIndex(getIndexUid(params.getGlossary())).search(searchRequest); + return SearchResult.from(search); + } + @NotNull private static String getIndexUid(Glossary glossary) { return glossary.getUid(); diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchParams.java b/api/src/main/java/vook/server/api/outbound/search/SearchParams.java new file mode 100644 index 00000000..61d1fed3 --- /dev/null +++ b/api/src/main/java/vook/server/api/outbound/search/SearchParams.java @@ -0,0 +1,18 @@ +package vook.server.api.outbound.search; + +import lombok.Getter; +import vook.server.api.model.Glossary; + +@Getter +public class SearchParams { + + private Glossary glossary; + private String query; + + public static SearchParams of(Glossary glossary, String query) { + SearchParams searchParams = new SearchParams(); + searchParams.glossary = glossary; + searchParams.query = query; + return searchParams; + } +} diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchResult.java b/api/src/main/java/vook/server/api/outbound/search/SearchResult.java new file mode 100644 index 00000000..5ead3389 --- /dev/null +++ b/api/src/main/java/vook/server/api/outbound/search/SearchResult.java @@ -0,0 +1,23 @@ +package vook.server.api.outbound.search; + +import com.meilisearch.sdk.model.Searchable; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.HashMap; + +@Getter +public class SearchResult { + + private String query; + private int processingTimeMs; + private ArrayList> hits; + + public static SearchResult from(Searchable search) { + SearchResult searchResult = new SearchResult(); + searchResult.query = search.getQuery(); + searchResult.processingTimeMs = search.getProcessingTimeMs(); + searchResult.hits = search.getHits(); + return searchResult; + } +} diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchService.java b/api/src/main/java/vook/server/api/outbound/search/SearchService.java index f3782fb3..826cbd63 100644 --- a/api/src/main/java/vook/server/api/outbound/search/SearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/SearchService.java @@ -9,4 +9,6 @@ public interface SearchService { void createGlossary(Glossary glossary); void addTerms(List terms, Glossary glossary); + + SearchResult search(SearchParams params); } diff --git a/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java b/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java new file mode 100644 index 00000000..5d663ad0 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java @@ -0,0 +1,16 @@ +package vook.server.api.web.routes.term; + +import lombok.Data; +import vook.server.api.model.Glossary; +import vook.server.api.outbound.search.SearchParams; + +@Data +public class SearchRequest { + + private String glossaryUid; + private String query; + + public SearchParams toSearchParam(Glossary glossary) { + return SearchParams.of(glossary, query); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java b/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java new file mode 100644 index 00000000..721bd514 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java @@ -0,0 +1,21 @@ +package vook.server.api.web.routes.term; + +import lombok.Getter; +import vook.server.api.outbound.search.SearchResult; + +import java.util.ArrayList; +import java.util.HashMap; + +@Getter +public class SearchResponse { + + private String query; + private ArrayList> hits; + + public static SearchResponse from(SearchResult searchResult) { + SearchResponse searchResponse = new SearchResponse(); + searchResponse.query = searchResult.getQuery(); + searchResponse.hits = searchResult.getHits(); + return searchResponse; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java b/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java new file mode 100644 index 00000000..f3bb1e69 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java @@ -0,0 +1,24 @@ +package vook.server.api.web.routes.term; + +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 vook.server.api.web.common.CommonApiResponse; + +@RestController +@RequestMapping("/terms") +@RequiredArgsConstructor +public class TermRestController { + + private final TermWebService service; + + @PostMapping("/search") + public CommonApiResponse search( + @RequestBody SearchRequest request + ) { + SearchResponse result = service.search(request); + return CommonApiResponse.okWithResult(result); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/term/TermWebService.java b/api/src/main/java/vook/server/api/web/routes/term/TermWebService.java new file mode 100644 index 00000000..00054fe9 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/term/TermWebService.java @@ -0,0 +1,24 @@ +package vook.server.api.web.routes.term; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.GlossaryService; +import vook.server.api.model.Glossary; +import vook.server.api.outbound.search.SearchResult; +import vook.server.api.outbound.search.SearchService; + +@Service +@Transactional +@RequiredArgsConstructor +public class TermWebService { + + private final GlossaryService glossaryService; + private final SearchService searchService; + + public SearchResponse search(SearchRequest request) { + Glossary glossary = glossaryService.findByUid(request.getGlossaryUid()).orElseThrow(); + SearchResult searchResult = searchService.search(request.toSearchParam(glossary)); + return SearchResponse.from(searchResult); + } +} From f3abc1be698d07eee49d7d729177a7b425796b9b Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 12 May 2024 14:04:47 +0900 Subject: [PATCH 048/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=EC=9D=98=20format=20=EC=9C=A0=EB=AC=B4=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B0=95=EC=A1=B0=20=ED=83=9C=EA=B7=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../outbound/search/MeilisearchService.java | 7 +---- .../api/outbound/search/SearchParams.java | 28 +++++++++++++---- .../api/web/routes/term/SearchRequest.java | 12 +++++++- .../api/web/routes/term/SearchResponse.java | 30 ++++++++++++++++--- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java index f96b13dc..f6e0b1f6 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -69,12 +69,7 @@ public void addTerms(List terms, Glossary glossary) { @Override public SearchResult search(SearchParams params) { - SearchRequest searchRequest = SearchRequest.builder() - .q(params.getQuery()) - .attributesToHighlight(new String[]{"*"}) - .highlightPreTag("") - .highlightPostTag("") - .build(); + SearchRequest searchRequest = params.buildSearchRequest(); Searchable search = this.client.getIndex(getIndexUid(params.getGlossary())).search(searchRequest); return SearchResult.from(search); } diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchParams.java b/api/src/main/java/vook/server/api/outbound/search/SearchParams.java index 61d1fed3..2b517fd0 100644 --- a/api/src/main/java/vook/server/api/outbound/search/SearchParams.java +++ b/api/src/main/java/vook/server/api/outbound/search/SearchParams.java @@ -1,18 +1,34 @@ package vook.server.api.outbound.search; +import com.meilisearch.sdk.SearchRequest; +import lombok.Builder; import lombok.Getter; +import org.springframework.util.StringUtils; import vook.server.api.model.Glossary; @Getter +@Builder public class SearchParams { - + + private static final String DEFAULT_HIGHLIGHT_PRE_TAG = ""; + private static final String DEFAULT_HIGHLIGHT_POST_TAG = ""; + private Glossary glossary; private String query; + private boolean withFormat; + private String highlightPreTag; + private String highlightPostTag; + + public SearchRequest buildSearchRequest() { + SearchRequest.SearchRequestBuilder builder = SearchRequest.builder(); + if (withFormat) { + builder.attributesToHighlight(new String[]{"*"}); + builder.highlightPreTag(StringUtils.hasText(highlightPreTag) ? highlightPreTag : DEFAULT_HIGHLIGHT_PRE_TAG); + builder.highlightPostTag(StringUtils.hasText(highlightPostTag) ? highlightPostTag : DEFAULT_HIGHLIGHT_POST_TAG); + } - public static SearchParams of(Glossary glossary, String query) { - SearchParams searchParams = new SearchParams(); - searchParams.glossary = glossary; - searchParams.query = query; - return searchParams; + return builder + .q(query) + .build(); } } diff --git a/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java b/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java index 5d663ad0..7b723154 100644 --- a/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java @@ -10,7 +10,17 @@ public class SearchRequest { private String glossaryUid; private String query; + private boolean withFormat; + private String highlightPreTag; + private String highlightPostTag; + public SearchParams toSearchParam(Glossary glossary) { - return SearchParams.of(glossary, query); + return SearchParams.builder() + .glossary(glossary) + .query(query) + .withFormat(withFormat) + .highlightPreTag(highlightPreTag) + .highlightPostTag(highlightPostTag) + .build(); } } diff --git a/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java b/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java index 721bd514..0eea0105 100644 --- a/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java @@ -3,19 +3,41 @@ import lombok.Getter; import vook.server.api.outbound.search.SearchResult; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.List; +import java.util.Map; @Getter public class SearchResponse { private String query; - private ArrayList> hits; + private List hits; public static SearchResponse from(SearchResult searchResult) { SearchResponse searchResponse = new SearchResponse(); searchResponse.query = searchResult.getQuery(); - searchResponse.hits = searchResult.getHits(); + searchResponse.hits = searchResult.getHits().stream().map(document -> { + Object formatted = document.get("_formatted"); + if (formatted instanceof Map formattedDocument) { + return Document.from(formattedDocument); + } else { + return Document.from(document); + } + }).toList(); return searchResponse; } + + @Getter + public static class Document { + private String term; + private String synonyms; + private String meaning; + + public static Document from(Map document) { + Document result = new Document(); + result.term = (String) document.get("term"); + result.synonyms = (String) document.get("synonyms"); + result.meaning = (String) document.get("meaning"); + return result; + } + } } From aa0418242ab30fbe5a1df64b8dc8282dc9753fbe Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 12 May 2024 14:31:20 +0900 Subject: [PATCH 049/273] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4=EC=97=90=20=EC=83=9D=EC=84=B1=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=98=A4=ED=83=80?= =?UTF-8?q?=20=EC=9A=A9=EC=9D=B8=20=EC=84=A4=EC=A0=95=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검색 조건에는 사용되지 않음 오타 용인 비활성화를 해도 띄어쓰기에 검색 가능함을 확인 --- .../outbound/search/MeilisearchService.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java index f6e0b1f6..386b8a0b 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -6,10 +6,7 @@ import com.meilisearch.sdk.Config; import com.meilisearch.sdk.Index; import com.meilisearch.sdk.SearchRequest; -import com.meilisearch.sdk.model.IndexesQuery; -import com.meilisearch.sdk.model.Results; -import com.meilisearch.sdk.model.Searchable; -import com.meilisearch.sdk.model.TaskInfo; +import com.meilisearch.sdk.model.*; import jakarta.annotation.PostConstruct; import lombok.AllArgsConstructor; import lombok.Getter; @@ -21,6 +18,7 @@ import vook.server.api.model.Term; import vook.server.api.model.TermSynonym; +import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -56,8 +54,21 @@ public void clearAll() { @Override public void createGlossary(Glossary glossary) { - TaskInfo index = client.createIndex(getIndexUid(glossary), "id"); - client.waitForTask(index.getTaskUid()); + String indexUid = getIndexUid(glossary); + TaskInfo indexCreateTask = client.createIndex(indexUid, "id"); + client.waitForTask(indexCreateTask.getTaskUid()); + + // 용어, 동의어, 뜻에 대해서만 검색 + client.index(indexUid).updateSearchableAttributesSettings(new String[]{ + "term", + "synonyms", + "meaning" + }); + + // 오타 용인을 비활성화 하여도 띄어쓰기에 대해서는 검색이 됨으로 비활성화 함 + TypoTolerance typoTolerance = new TypoTolerance(); + typoTolerance.setEnabled(false); + client.index(indexUid).updateTypoToleranceSettings(typoTolerance); } @Override @@ -94,6 +105,7 @@ public static class Document { private String term; private String synonyms; private String meaning; + private String createdAt; public static List from(List terms) { return terms.stream() @@ -101,7 +113,8 @@ public static List from(List terms) { w.getId(), w.getTerm(), w.getSynonyms().stream().map(TermSynonym::getSynonym).collect(Collectors.joining("\n")), - w.getMeaning() + w.getMeaning(), + w.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) )) .toList(); } From 2959d4e351778e383c8ea6f8fa121b6c12524ba1 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 12 May 2024 14:34:42 +0900 Subject: [PATCH 050/273] =?UTF-8?q?docs:=20=EC=9A=A9=EC=96=B4=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=EC=9D=98=20swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/web/routes/term/SearchRequest.java | 11 ++++++++ .../api/web/routes/term/TermRestApi.java | 28 +++++++++++++++++++ .../web/routes/term/TermRestController.java | 3 +- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/vook/server/api/web/routes/term/TermRestApi.java diff --git a/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java b/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java index 7b723154..a22d5bdc 100644 --- a/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java @@ -1,17 +1,28 @@ package vook.server.api.web.routes.term; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import vook.server.api.model.Glossary; import vook.server.api.outbound.search.SearchParams; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + @Data public class SearchRequest { + @Schema(description = "용어집 UID", requiredMode = REQUIRED, example = "0e840100-9302-4f8b-8110-ffd321c3ccb9") private String glossaryUid; + + @Schema(description = "검색 쿼리", requiredMode = REQUIRED, example = "하이브리드앱") private String query; + @Schema(description = "포맷 적용 여부", defaultValue = "false") private boolean withFormat; + + @Schema(description = "하이라이트 시작 태그, 포맷 적용 여부가 true일 때만 적용 됨", defaultValue = "") private String highlightPreTag; + + @Schema(description = "하이라이트 종료 태그, 포맷 적용 여부가 true일 때만 적용 됨", defaultValue = "") private String highlightPostTag; public SearchParams toSearchParam(Glossary glossary) { diff --git a/api/src/main/java/vook/server/api/web/routes/term/TermRestApi.java b/api/src/main/java/vook/server/api/web/routes/term/TermRestApi.java new file mode 100644 index 00000000..fc631a85 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/term/TermRestApi.java @@ -0,0 +1,28 @@ +package vook.server.api.web.routes.term; + +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import vook.server.api.web.common.CommonApiResponse; + +@Tag(name = "term", description = "용어 API") +public interface TermRestApi { + + @Operation(summary = "용어 검색") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + schema = @Schema(implementation = SearchApiResponse.class) + ) + ), + }) + CommonApiResponse search(SearchRequest request); + + class SearchApiResponse extends CommonApiResponse { + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java b/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java index f3bb1e69..dda61025 100644 --- a/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java @@ -10,10 +10,11 @@ @RestController @RequestMapping("/terms") @RequiredArgsConstructor -public class TermRestController { +public class TermRestController implements TermRestApi { private final TermWebService service; + @Override @PostMapping("/search") public CommonApiResponse search( @RequestBody SearchRequest request From 2d9014f315e5eb9104cc509067960179b857b168 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 12 May 2024 15:20:45 +0900 Subject: [PATCH 051/273] =?UTF-8?q?chore:=20glossary=20test=20http=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/http/glossary.http | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/api/http/glossary.http b/api/http/glossary.http index 3ec6c218..c95e2979 100644 --- a/api/http/glossary.http +++ b/api/http/glossary.http @@ -1,2 +1,11 @@ -# 용어집 리스트 +### 용어집 리스트 GET http://localhost:8080/glossary + +> {% + let devGlossaryUid = response.body.result.filter((item) => item.name === '개발')[0].uid; + console.log(devGlossaryUid); + client.global.set('uid', devGlossaryUid); +%} + +### 용어집 내 용어 리스트 +GET http://localhost:8080/glossary/{{uid}}/terms From 147bb4902c04cf409285836e2dce389cc54357a3 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 12 May 2024 15:31:28 +0900 Subject: [PATCH 052/273] =?UTF-8?q?chore:=20term=20test=20http=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/http/term.http | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 api/http/term.http diff --git a/api/http/term.http b/api/http/term.http new file mode 100644 index 00000000..e54ca755 --- /dev/null +++ b/api/http/term.http @@ -0,0 +1,23 @@ +### DB, Meilisearch 초기화 +POST http://localhost:8080/init + +### 용어집 리스트 +GET http://localhost:8080/glossary + +> {% + let devGlossaryUid = response.body.result.filter((item) => item.name === '개발')[0].uid; + console.log(devGlossaryUid); + client.global.set('uid', devGlossaryUid); +%} + +### 용어 검색 +POST http://localhost:8080/terms/search +Content-Type: application/json + +{ + "glossaryUid": "{{uid}}", + "query": "하이브리드앱", + "withFormat": true, + "highlightPreTag": "", + "highlightPostTag": "" +} From 3e5990bc9bd3f1e443560b6dcc8e815de5a0836e Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 13 May 2024 16:22:40 +0900 Subject: [PATCH 053/273] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20=EC=9A=A9?= =?UTF-8?q?=EC=96=B4=EC=A7=91=20=EB=B0=8F=20=EC=9A=A9=EC=96=B4=20API?= =?UTF-8?q?=EB=A5=BC=20=EB=8D=B0=EB=AA=A8=EC=9A=A9=20API=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20#42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/http/{term.http => demo.http} | 7 ++- api/http/glossary.http | 11 ----- .../GlossaryApi.java => demo/DemoApi.java} | 33 ++++++++++--- .../web/routes/demo/DemoRestController.java | 48 +++++++++++++++++++ .../api/web/routes/demo/DemoWebService.java | 45 +++++++++++++++++ .../reqres/RetrieveGlossariesResponse.java} | 12 ++--- .../reqres}/RetrieveTermsResponse.java | 2 +- .../reqres/SearchTermRequest.java} | 7 +-- .../reqres/SearchTermResponse.java} | 14 +++--- .../glossary/GlossaryRestController.java | 37 -------------- .../routes/glossary/GlossaryWebService.java | 32 ------------- .../api/web/routes/term/TermRestApi.java | 28 ----------- .../web/routes/term/TermRestController.java | 25 ---------- .../api/web/routes/term/TermWebService.java | 24 ---------- 14 files changed, 141 insertions(+), 184 deletions(-) rename api/http/{term.http => demo.http} (71%) delete mode 100644 api/http/glossary.http rename api/src/main/java/vook/server/api/web/routes/{glossary/GlossaryApi.java => demo/DemoApi.java} (57%) create mode 100644 api/src/main/java/vook/server/api/web/routes/demo/DemoRestController.java create mode 100644 api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java rename api/src/main/java/vook/server/api/web/routes/{glossary/RetrieveResponse.java => demo/reqres/RetrieveGlossariesResponse.java} (58%) rename api/src/main/java/vook/server/api/web/routes/{glossary => demo/reqres}/RetrieveTermsResponse.java (96%) rename api/src/main/java/vook/server/api/web/routes/{term/SearchRequest.java => demo/reqres/SearchTermRequest.java} (84%) rename api/src/main/java/vook/server/api/web/routes/{term/SearchResponse.java => demo/reqres/SearchTermResponse.java} (70%) delete mode 100644 api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java delete mode 100644 api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java delete mode 100644 api/src/main/java/vook/server/api/web/routes/term/TermRestApi.java delete mode 100644 api/src/main/java/vook/server/api/web/routes/term/TermRestController.java delete mode 100644 api/src/main/java/vook/server/api/web/routes/term/TermWebService.java diff --git a/api/http/term.http b/api/http/demo.http similarity index 71% rename from api/http/term.http rename to api/http/demo.http index e54ca755..e7a6f4ba 100644 --- a/api/http/term.http +++ b/api/http/demo.http @@ -2,7 +2,7 @@ POST http://localhost:8080/init ### 용어집 리스트 -GET http://localhost:8080/glossary +GET http://localhost:8080/demo/glossaries > {% let devGlossaryUid = response.body.result.filter((item) => item.name === '개발')[0].uid; @@ -10,8 +10,11 @@ GET http://localhost:8080/glossary client.global.set('uid', devGlossaryUid); %} +### 용어집 내 용어 리스트 +GET http://localhost:8080/demo/glossaries/{{uid}}/terms + ### 용어 검색 -POST http://localhost:8080/terms/search +POST http://localhost:8080/demo/glossaries/{{uid}}/terms/search Content-Type: application/json { diff --git a/api/http/glossary.http b/api/http/glossary.http deleted file mode 100644 index c95e2979..00000000 --- a/api/http/glossary.http +++ /dev/null @@ -1,11 +0,0 @@ -### 용어집 리스트 -GET http://localhost:8080/glossary - -> {% - let devGlossaryUid = response.body.result.filter((item) => item.name === '개발')[0].uid; - console.log(devGlossaryUid); - client.global.set('uid', devGlossaryUid); -%} - -### 용어집 내 용어 리스트 -GET http://localhost:8080/glossary/{{uid}}/terms diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java similarity index 57% rename from api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java rename to api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java index 289891e1..c17607b6 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryApi.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.glossary; +package vook.server.api.web.routes.demo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -9,11 +9,15 @@ import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Pageable; import vook.server.api.web.common.CommonApiResponse; +import vook.server.api.web.routes.demo.reqres.RetrieveGlossariesResponse; +import vook.server.api.web.routes.demo.reqres.RetrieveTermsResponse; +import vook.server.api.web.routes.demo.reqres.SearchTermRequest; +import vook.server.api.web.routes.demo.reqres.SearchTermResponse; import java.util.List; -@Tag(name = "glossary", description = "용어집 API") -public interface GlossaryApi { +@Tag(name = "demo", description = "VooK 데모용 API") +public interface DemoApi { @Operation(summary = "용어집 리스트 조회") @ApiResponses(value = { @@ -22,13 +26,13 @@ public interface GlossaryApi { description = "성공", content = @Content( mediaType = "application/json", - schema = @Schema(implementation = RetrieveApiResponse.class) + schema = @Schema(implementation = RetrieveGlossariesApiResponse.class) ) ), }) - CommonApiResponse> retrieve(); + CommonApiResponse> retrieveGlossaries(); - class RetrieveApiResponse extends CommonApiResponse> { + class RetrieveGlossariesApiResponse extends CommonApiResponse> { } @Operation(summary = "용어집 내 용어 리스트 조회") @@ -50,4 +54,21 @@ CommonApiResponse> retrieveTerms( class RetrieveTermsApiResponse extends CommonApiResponse> { } + @Operation(summary = "용어 검색") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + schema = @Schema(implementation = SearchApiTermResponse.class) + ) + ), + }) + CommonApiResponse searchTerm( + String glossaryUid, + SearchTermRequest request + ); + + class SearchApiTermResponse extends CommonApiResponse { + } } diff --git a/api/src/main/java/vook/server/api/web/routes/demo/DemoRestController.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoRestController.java new file mode 100644 index 00000000..4dd36e16 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoRestController.java @@ -0,0 +1,48 @@ +package vook.server.api.web.routes.demo; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; +import vook.server.api.web.common.CommonApiResponse; +import vook.server.api.web.routes.demo.reqres.RetrieveGlossariesResponse; +import vook.server.api.web.routes.demo.reqres.RetrieveTermsResponse; +import vook.server.api.web.routes.demo.reqres.SearchTermRequest; +import vook.server.api.web.routes.demo.reqres.SearchTermResponse; + +import java.util.List; + +@RestController +@RequestMapping("/demo") +@RequiredArgsConstructor +public class DemoRestController implements DemoApi { + + private final DemoWebService service; + + @Override + @GetMapping("/glossaries") + public CommonApiResponse> retrieveGlossaries() { + List result = service.retrieveGlossaries(); + return CommonApiResponse.okWithResult(result); + } + + @Override + @GetMapping("/glossaries/{glossaryUid}/terms") + public CommonApiResponse> retrieveTerms( + @PathVariable String glossaryUid, + @PageableDefault(size = Integer.MAX_VALUE, sort = "term") Pageable pageable + ) { + List result = service.retrieveTerms(glossaryUid, pageable); + return CommonApiResponse.okWithResult(result); + } + + @Override + @PostMapping("/glossaries/{glossaryUid}/terms/search") + public CommonApiResponse searchTerm( + @PathVariable String glossaryUid, + @RequestBody SearchTermRequest request + ) { + SearchTermResponse result = service.searchTerm(glossaryUid, request); + return CommonApiResponse.okWithResult(result); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java new file mode 100644 index 00000000..ed9302dc --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java @@ -0,0 +1,45 @@ +package vook.server.api.web.routes.demo; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.GlossaryService; +import vook.server.api.app.TermService; +import vook.server.api.model.Glossary; +import vook.server.api.model.Term; +import vook.server.api.outbound.search.SearchResult; +import vook.server.api.outbound.search.SearchService; +import vook.server.api.web.routes.demo.reqres.RetrieveGlossariesResponse; +import vook.server.api.web.routes.demo.reqres.RetrieveTermsResponse; +import vook.server.api.web.routes.demo.reqres.SearchTermRequest; +import vook.server.api.web.routes.demo.reqres.SearchTermResponse; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class DemoWebService { + + private final GlossaryService glossaryService; + private final TermService termService; + private final SearchService searchService; + + public List retrieveGlossaries() { + List glossaries = glossaryService.findAll(); + return RetrieveGlossariesResponse.from(glossaries); + } + + public List retrieveTerms(String glossaryUid, Pageable pageable) { + Glossary glossary = glossaryService.findByUid(glossaryUid).orElseThrow(); + List terms = termService.findAllBy(glossary, pageable); + return RetrieveTermsResponse.from(glossary, terms); + } + + public SearchTermResponse searchTerm(String glossaryUid, SearchTermRequest request) { + Glossary glossary = glossaryService.findByUid(glossaryUid).orElseThrow(); + SearchResult searchResult = searchService.search(request.toSearchParam(glossary)); + return SearchTermResponse.from(searchResult); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java similarity index 58% rename from api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java rename to api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java index 789b94fc..0d50c185 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.glossary; +package vook.server.api.web.routes.demo.reqres; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @@ -7,21 +7,21 @@ import java.util.List; @Getter -public class RetrieveResponse { +public class RetrieveGlossariesResponse { @Schema(description = "용어집 UID", examples = "38617d11-1d8e-4f77-a2fd-8cdca9de8420") private String uid; @Schema(description = "용어집 이름", examples = "실무") private String name; - public static List from(List glossaries) { + public static List from(List glossaries) { return glossaries.stream() - .map(RetrieveResponse::from) + .map(RetrieveGlossariesResponse::from) .toList(); } - public static RetrieveResponse from(Glossary glossary) { - RetrieveResponse response = new RetrieveResponse(); + public static RetrieveGlossariesResponse from(Glossary glossary) { + RetrieveGlossariesResponse response = new RetrieveGlossariesResponse(); response.uid = glossary.getUid(); response.name = glossary.getName(); return response; diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java similarity index 96% rename from api/src/main/java/vook/server/api/web/routes/glossary/RetrieveTermsResponse.java rename to api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java index 4d746106..003ea3d1 100644 --- a/api/src/main/java/vook/server/api/web/routes/glossary/RetrieveTermsResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.glossary; +package vook.server.api.web.routes.demo.reqres; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; diff --git a/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java similarity index 84% rename from api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java rename to api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java index a22d5bdc..e45eece2 100644 --- a/api/src/main/java/vook/server/api/web/routes/term/SearchRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.term; +package vook.server.api.web.routes.demo.reqres; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -8,10 +8,7 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @Data -public class SearchRequest { - - @Schema(description = "용어집 UID", requiredMode = REQUIRED, example = "0e840100-9302-4f8b-8110-ffd321c3ccb9") - private String glossaryUid; +public class SearchTermRequest { @Schema(description = "검색 쿼리", requiredMode = REQUIRED, example = "하이브리드앱") private String query; diff --git a/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java similarity index 70% rename from api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java rename to api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java index 0eea0105..7a97135f 100644 --- a/api/src/main/java/vook/server/api/web/routes/term/SearchResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.term; +package vook.server.api.web.routes.demo.reqres; import lombok.Getter; import vook.server.api.outbound.search.SearchResult; @@ -7,15 +7,15 @@ import java.util.Map; @Getter -public class SearchResponse { +public class SearchTermResponse { private String query; private List hits; - public static SearchResponse from(SearchResult searchResult) { - SearchResponse searchResponse = new SearchResponse(); - searchResponse.query = searchResult.getQuery(); - searchResponse.hits = searchResult.getHits().stream().map(document -> { + public static SearchTermResponse from(SearchResult searchResult) { + SearchTermResponse searchTermResponse = new SearchTermResponse(); + searchTermResponse.query = searchResult.getQuery(); + searchTermResponse.hits = searchResult.getHits().stream().map(document -> { Object formatted = document.get("_formatted"); if (formatted instanceof Map formattedDocument) { return Document.from(formattedDocument); @@ -23,7 +23,7 @@ public static SearchResponse from(SearchResult searchResult) { return Document.from(document); } }).toList(); - return searchResponse; + return searchTermResponse; } @Getter diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java deleted file mode 100644 index 82b7f207..00000000 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryRestController.java +++ /dev/null @@ -1,37 +0,0 @@ -package vook.server.api.web.routes.glossary; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import vook.server.api.web.common.CommonApiResponse; - -import java.util.List; - -@RestController -@RequestMapping("/glossary") -@RequiredArgsConstructor -public class GlossaryRestController implements GlossaryApi { - - private final GlossaryWebService service; - - @Override - @GetMapping - public CommonApiResponse> retrieve() { - List result = service.retrieve(); - return CommonApiResponse.okWithResult(result); - } - - @Override - @GetMapping("/{glossaryUid}/terms") - public CommonApiResponse> retrieveTerms( - @PathVariable String glossaryUid, - @PageableDefault(size = Integer.MAX_VALUE, sort = "term") Pageable pageable - ) { - List result = service.retrieveTerms(glossaryUid, pageable); - return CommonApiResponse.okWithResult(result); - } -} diff --git a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java b/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java deleted file mode 100644 index 1e0e0bef..00000000 --- a/api/src/main/java/vook/server/api/web/routes/glossary/GlossaryWebService.java +++ /dev/null @@ -1,32 +0,0 @@ -package vook.server.api.web.routes.glossary; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.GlossaryService; -import vook.server.api.app.TermService; -import vook.server.api.model.Glossary; -import vook.server.api.model.Term; - -import java.util.List; - -@Service -@Transactional -@RequiredArgsConstructor -public class GlossaryWebService { - - private final GlossaryService glossaryService; - private final TermService termService; - - public List retrieve() { - List glossaries = glossaryService.findAll(); - return RetrieveResponse.from(glossaries); - } - - public List retrieveTerms(String glossaryUid, Pageable pageable) { - Glossary glossary = glossaryService.findByUid(glossaryUid).orElseThrow(); - List terms = termService.findAllBy(glossary, pageable); - return RetrieveTermsResponse.from(glossary, terms); - } -} diff --git a/api/src/main/java/vook/server/api/web/routes/term/TermRestApi.java b/api/src/main/java/vook/server/api/web/routes/term/TermRestApi.java deleted file mode 100644 index fc631a85..00000000 --- a/api/src/main/java/vook/server/api/web/routes/term/TermRestApi.java +++ /dev/null @@ -1,28 +0,0 @@ -package vook.server.api.web.routes.term; - -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.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import vook.server.api.web.common.CommonApiResponse; - -@Tag(name = "term", description = "용어 API") -public interface TermRestApi { - - @Operation(summary = "용어 검색") - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "성공", - content = @Content( - schema = @Schema(implementation = SearchApiResponse.class) - ) - ), - }) - CommonApiResponse search(SearchRequest request); - - class SearchApiResponse extends CommonApiResponse { - } -} diff --git a/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java b/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java deleted file mode 100644 index dda61025..00000000 --- a/api/src/main/java/vook/server/api/web/routes/term/TermRestController.java +++ /dev/null @@ -1,25 +0,0 @@ -package vook.server.api.web.routes.term; - -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 vook.server.api.web.common.CommonApiResponse; - -@RestController -@RequestMapping("/terms") -@RequiredArgsConstructor -public class TermRestController implements TermRestApi { - - private final TermWebService service; - - @Override - @PostMapping("/search") - public CommonApiResponse search( - @RequestBody SearchRequest request - ) { - SearchResponse result = service.search(request); - return CommonApiResponse.okWithResult(result); - } -} diff --git a/api/src/main/java/vook/server/api/web/routes/term/TermWebService.java b/api/src/main/java/vook/server/api/web/routes/term/TermWebService.java deleted file mode 100644 index 00054fe9..00000000 --- a/api/src/main/java/vook/server/api/web/routes/term/TermWebService.java +++ /dev/null @@ -1,24 +0,0 @@ -package vook.server.api.web.routes.term; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.GlossaryService; -import vook.server.api.model.Glossary; -import vook.server.api.outbound.search.SearchResult; -import vook.server.api.outbound.search.SearchService; - -@Service -@Transactional -@RequiredArgsConstructor -public class TermWebService { - - private final GlossaryService glossaryService; - private final SearchService searchService; - - public SearchResponse search(SearchRequest request) { - Glossary glossary = glossaryService.findByUid(request.getGlossaryUid()).orElseThrow(); - SearchResult searchResult = searchService.search(request.toSearchParam(glossary)); - return SearchResponse.from(searchResult); - } -} From e44f146226c0428dd4c4a470ac5a1efa5459fec7 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 13 May 2024 16:45:22 +0900 Subject: [PATCH 054/273] =?UTF-8?q?feat:=20=EA=B8=B0=EC=A1=B4=20model=20?= =?UTF-8?q?=EB=B0=8F=20logic=EC=9D=84=20=EB=8D=B0=EB=AA=A8=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20#42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ository.java => DemoSearchRepository.java} | 28 ++--- .../java/vook/server/api/app/DemoService.java | 31 +++++ .../vook/server/api/app/GlossaryService.java | 24 ---- .../java/vook/server/api/app/TermService.java | 20 ---- .../server/api/devhelper/InitService.java | 49 ++++---- .../vook/server/api/devhelper/LocalInit.java | 6 +- .../server/api/model/GlossaryRepository.java | 9 -- .../java/vook/server/api/model/Member.java | 28 ----- .../server/api/model/MemberRepository.java | 6 - .../main/java/vook/server/api/model/Term.java | 55 --------- .../vook/server/api/model/TermRepository.java | 6 - .../api/model/TermSynonymRepository.java | 6 - .../{Glossary.java => demo/DemoGlossary.java} | 21 ++-- .../model/demo/DemoGlossaryRepository.java | 9 ++ .../vook/server/api/model/demo/DemoTerm.java | 56 +++++++++ .../api/model/demo/DemoTermRepository.java | 6 + .../DemoTermSynonym.java} | 20 ++-- .../model/demo/DemoTermSynonymRepository.java | 6 + ...hParams.java => DemoTermSearchParams.java} | 6 +- .../outbound/search/DemoTermSearchResult.java | 23 ++++ .../search/DemoTermSearchService.java | 99 ++++++++++++++++ .../outbound/search/MeilisearchService.java | 109 +++--------------- .../api/outbound/search/SearchClearable.java | 5 - .../api/outbound/search/SearchResult.java | 23 ---- .../api/outbound/search/SearchService.java | 14 --- .../api/web/routes/demo/DemoWebService.java | 28 +++-- .../reqres/RetrieveGlossariesResponse.java | 6 +- .../demo/reqres/RetrieveTermsResponse.java | 15 ++- .../routes/demo/reqres/SearchTermRequest.java | 10 +- .../demo/reqres/SearchTermResponse.java | 12 +- 30 files changed, 336 insertions(+), 400 deletions(-) rename api/src/main/java/vook/server/api/app/{TermSearchRepository.java => DemoSearchRepository.java} (61%) create mode 100644 api/src/main/java/vook/server/api/app/DemoService.java delete mode 100644 api/src/main/java/vook/server/api/app/GlossaryService.java delete mode 100644 api/src/main/java/vook/server/api/app/TermService.java delete mode 100644 api/src/main/java/vook/server/api/model/GlossaryRepository.java delete mode 100644 api/src/main/java/vook/server/api/model/Member.java delete mode 100644 api/src/main/java/vook/server/api/model/MemberRepository.java delete mode 100644 api/src/main/java/vook/server/api/model/Term.java delete mode 100644 api/src/main/java/vook/server/api/model/TermRepository.java delete mode 100644 api/src/main/java/vook/server/api/model/TermSynonymRepository.java rename api/src/main/java/vook/server/api/model/{Glossary.java => demo/DemoGlossary.java} (56%) create mode 100644 api/src/main/java/vook/server/api/model/demo/DemoGlossaryRepository.java create mode 100644 api/src/main/java/vook/server/api/model/demo/DemoTerm.java create mode 100644 api/src/main/java/vook/server/api/model/demo/DemoTermRepository.java rename api/src/main/java/vook/server/api/model/{TermSynonym.java => demo/DemoTermSynonym.java} (50%) create mode 100644 api/src/main/java/vook/server/api/model/demo/DemoTermSynonymRepository.java rename api/src/main/java/vook/server/api/outbound/search/{SearchParams.java => DemoTermSearchParams.java} (89%) create mode 100644 api/src/main/java/vook/server/api/outbound/search/DemoTermSearchResult.java create mode 100644 api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java delete mode 100644 api/src/main/java/vook/server/api/outbound/search/SearchClearable.java delete mode 100644 api/src/main/java/vook/server/api/outbound/search/SearchResult.java delete mode 100644 api/src/main/java/vook/server/api/outbound/search/SearchService.java diff --git a/api/src/main/java/vook/server/api/app/TermSearchRepository.java b/api/src/main/java/vook/server/api/app/DemoSearchRepository.java similarity index 61% rename from api/src/main/java/vook/server/api/app/TermSearchRepository.java rename to api/src/main/java/vook/server/api/app/DemoSearchRepository.java index 3ee1bbbb..a0aa82df 100644 --- a/api/src/main/java/vook/server/api/app/TermSearchRepository.java +++ b/api/src/main/java/vook/server/api/app/DemoSearchRepository.java @@ -6,42 +6,42 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import vook.server.api.helper.QuerydslHelper; -import vook.server.api.model.QTerm; -import vook.server.api.model.Term; +import vook.server.api.model.demo.DemoTerm; +import vook.server.api.model.demo.QDemoTerm; import java.util.List; @Repository @RequiredArgsConstructor -public class TermSearchRepository { +public class DemoSearchRepository { private final JPAQueryFactory queryFactory; - public List search(String glossaryUid, Pageable pageable) { - QTerm term = QTerm.term1; + public List searchDemoTerm(String demoGlossaryUid, Pageable pageable) { + QDemoTerm demoTerm = QDemoTerm.demoTerm; // fetch join과 pagination을 같이 하면, // HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory // 에러가 발생, 이를 피하기 위해 pagination이 포함된 쿼리를 서브 쿼리로 하여 수행 JPAQuery termIdQuery = queryFactory - .select(term.id) - .from(term) - .where(term.glossary.uid.eq(glossaryUid)) + .select(demoTerm.id) + .from(demoTerm) + .where(demoTerm.demoGlossary.uid.eq(demoGlossaryUid)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()); QuerydslHelper - .toOrderSpecifiers(term, pageable) + .toOrderSpecifiers(demoTerm, pageable) .forEach(termIdQuery::orderBy); - JPAQuery dataQuery = queryFactory - .selectFrom(term) - .leftJoin(term.synonyms).fetchJoin() - .where(term.id.in(termIdQuery)); + JPAQuery dataQuery = queryFactory + .selectFrom(demoTerm) + .leftJoin(demoTerm.synonyms).fetchJoin() + .where(demoTerm.id.in(termIdQuery)); // In 절에 들어간 입력순대로 출력되지 않음으로 정렬 조건을 다시 한번 설정 QuerydslHelper - .toOrderSpecifiers(term, pageable) + .toOrderSpecifiers(demoTerm, pageable) .forEach(dataQuery::orderBy); return dataQuery.fetch(); diff --git a/api/src/main/java/vook/server/api/app/DemoService.java b/api/src/main/java/vook/server/api/app/DemoService.java new file mode 100644 index 00000000..f5933611 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/DemoService.java @@ -0,0 +1,31 @@ +package vook.server.api.app; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import vook.server.api.model.demo.DemoGlossary; +import vook.server.api.model.demo.DemoGlossaryRepository; +import vook.server.api.model.demo.DemoTerm; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class DemoService { + + private final DemoGlossaryRepository demoGlossaryRepository; + private final DemoSearchRepository searchRepository; + + public List findAllDemoGlossary() { + return demoGlossaryRepository.findAll(); + } + + public Optional findDemoGlossaryByUid(String demoGlossaryUid) { + return demoGlossaryRepository.findByUid(demoGlossaryUid); + } + + public List findAllDemoTermBy(DemoGlossary demoGlossary, Pageable pageable) { + return searchRepository.searchDemoTerm(demoGlossary.getUid(), pageable); + } +} diff --git a/api/src/main/java/vook/server/api/app/GlossaryService.java b/api/src/main/java/vook/server/api/app/GlossaryService.java deleted file mode 100644 index 3e4815e9..00000000 --- a/api/src/main/java/vook/server/api/app/GlossaryService.java +++ /dev/null @@ -1,24 +0,0 @@ -package vook.server.api.app; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import vook.server.api.model.Glossary; -import vook.server.api.model.GlossaryRepository; - -import java.util.List; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class GlossaryService { - - private final GlossaryRepository repository; - - public List findAll() { - return repository.findAll(); - } - - public Optional findByUid(String uid) { - return repository.findByUid(uid); - } -} diff --git a/api/src/main/java/vook/server/api/app/TermService.java b/api/src/main/java/vook/server/api/app/TermService.java deleted file mode 100644 index 2e997998..00000000 --- a/api/src/main/java/vook/server/api/app/TermService.java +++ /dev/null @@ -1,20 +0,0 @@ -package vook.server.api.app; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import vook.server.api.model.Glossary; -import vook.server.api.model.Term; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class TermService { - - private final TermSearchRepository searchRepository; - - public List findAllBy(Glossary glossary, Pageable pageable) { - return searchRepository.search(glossary.getUid(), pageable); - } -} diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index aa417c15..5869e570 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -5,9 +5,8 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.model.*; -import vook.server.api.outbound.search.SearchClearable; -import vook.server.api.outbound.search.SearchService; +import vook.server.api.model.demo.*; +import vook.server.api.outbound.search.DemoTermSearchService; import java.io.IOException; import java.io.InputStream; @@ -21,38 +20,32 @@ public class InitService { private final ResourceLoader resourceLoader; - private final MemberRepository memberRepository; - private final GlossaryRepository glossaryRepository; - private final TermRepository termRepository; - private final TermSynonymRepository termSynonymRepository; - private final SearchClearable searchClearable; - - private final SearchService searchService; + private final DemoGlossaryRepository demoGlossaryRepository; + private final DemoTermRepository demoTermRepository; + private final DemoTermSynonymRepository demoTermSynonymRepository; + private final DemoTermSearchService searchService; public void init() { - termSynonymRepository.deleteAllInBatch(); - termRepository.deleteAllInBatch(); - glossaryRepository.deleteAllInBatch(); - memberRepository.deleteAllInBatch(); - searchClearable.clearAll(); - - Member vook = memberRepository.save(Member.forCreateOf("vook")); + demoTermSynonymRepository.deleteAllInBatch(); + demoTermRepository.deleteAllInBatch(); + demoGlossaryRepository.deleteAllInBatch(); + searchService.clearAll(); - Glossary devGlossary = glossaryRepository.save(Glossary.forCreateOf("개발", vook)); + DemoGlossary devGlossary = demoGlossaryRepository.save(DemoGlossary.forCreateOf("개발")); searchService.createGlossary(devGlossary); - Glossary designGlossary = glossaryRepository.save(Glossary.forCreateOf("디자인", vook)); + DemoGlossary designGlossary = demoGlossaryRepository.save(DemoGlossary.forCreateOf("디자인")); searchService.createGlossary(designGlossary); - Glossary marketingGlossary = glossaryRepository.save(Glossary.forCreateOf("마케팅", vook)); + DemoGlossary marketingGlossary = demoGlossaryRepository.save(DemoGlossary.forCreateOf("마케팅")); searchService.createGlossary(marketingGlossary); - Glossary practiceGlossary = glossaryRepository.save(Glossary.forCreateOf("실무", vook)); + DemoGlossary practiceGlossary = demoGlossaryRepository.save(DemoGlossary.forCreateOf("실무")); searchService.createGlossary(practiceGlossary); - List devTerms = getTerms("classpath:init/개발.tsv", devGlossary); - List terms = termRepository.saveAll(devTerms); - searchService.addTerms(terms, devGlossary); + List devTerms = getTerms("classpath:init/개발.tsv", devGlossary); + List terms = demoTermRepository.saveAll(devTerms); + searchService.addTerms(devGlossary, terms); } - private List getTerms(String location, Glossary glossary) { + private List getTerms(String location, DemoGlossary glossary) { try { // file로 바로 접근 할 경우, IDE에서는 접근 가능하나, jar로 패키징 후 실행 시에는 접근 불가능 // ref) https://velog.io/@haron/트러블슈팅-Spring-IDE-에서-되는데-배포하면-안-돼요 @@ -65,7 +58,7 @@ private List getTerms(String location, Glossary glossary) { } } - private static @NotNull List toTerms(List rawTerms, Glossary glossary) { + private static @NotNull List toTerms(List rawTerms, DemoGlossary glossary) { return rawTerms.stream() .map(rawTerm -> rawTerm.toTerm(glossary)) .toList(); @@ -76,8 +69,8 @@ public static class RawTerm { private String synonyms; private String meaning; - public Term toTerm(Glossary glossary) { - Term term = Term.forCreateOf(this.term, this.meaning, glossary); + public DemoTerm toTerm(DemoGlossary glossary) { + DemoTerm term = DemoTerm.forCreateOf(this.term, this.meaning, glossary); String[] synonymArray = this.synonyms.split("//n"); Arrays.stream(synonymArray) .map(String::trim) diff --git a/api/src/main/java/vook/server/api/devhelper/LocalInit.java b/api/src/main/java/vook/server/api/devhelper/LocalInit.java index dee48e23..7c6f6120 100644 --- a/api/src/main/java/vook/server/api/devhelper/LocalInit.java +++ b/api/src/main/java/vook/server/api/devhelper/LocalInit.java @@ -5,7 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import vook.server.api.model.MemberRepository; +import vook.server.api.model.demo.DemoGlossaryRepository; @Slf4j @Profile("local") @@ -13,12 +13,12 @@ @RequiredArgsConstructor public class LocalInit { - private final MemberRepository memberRepository; + private final DemoGlossaryRepository demoGlossaryRepository; private final InitService initService; @PostConstruct public void init() { - if (memberRepository.count() > 0) { + if (demoGlossaryRepository.count() > 0) { return; } diff --git a/api/src/main/java/vook/server/api/model/GlossaryRepository.java b/api/src/main/java/vook/server/api/model/GlossaryRepository.java deleted file mode 100644 index 0d5aaca5..00000000 --- a/api/src/main/java/vook/server/api/model/GlossaryRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package vook.server.api.model; - -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface GlossaryRepository extends JpaRepository { - Optional findByUid(String uid); -} diff --git a/api/src/main/java/vook/server/api/model/Member.java b/api/src/main/java/vook/server/api/model/Member.java deleted file mode 100644 index 2548cda7..00000000 --- a/api/src/main/java/vook/server/api/model/Member.java +++ /dev/null @@ -1,28 +0,0 @@ -package vook.server.api.model; - -import jakarta.persistence.*; -import lombok.Getter; - -/** - * 회원 - */ -@Getter -@Entity -@Table(name = "member") -public class Member extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * 회원 이름 - */ - private String name; - - public static Member forCreateOf(String name) { - Member result = new Member(); - result.name = name; - return result; - } -} diff --git a/api/src/main/java/vook/server/api/model/MemberRepository.java b/api/src/main/java/vook/server/api/model/MemberRepository.java deleted file mode 100644 index e1b73083..00000000 --- a/api/src/main/java/vook/server/api/model/MemberRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package vook.server.api.model; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface MemberRepository extends JpaRepository { -} diff --git a/api/src/main/java/vook/server/api/model/Term.java b/api/src/main/java/vook/server/api/model/Term.java deleted file mode 100644 index ea551bf5..00000000 --- a/api/src/main/java/vook/server/api/model/Term.java +++ /dev/null @@ -1,55 +0,0 @@ -package vook.server.api.model; - -import jakarta.persistence.*; -import lombok.Getter; - -import java.util.ArrayList; -import java.util.List; - -/** - * 용어 - */ -@Getter -@Entity -@Table(name = "term") -public class Term extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * 용어 이름 - */ - @Column(length = 100, nullable = false) - private String term; - - /** - * 용어 의미 - */ - @Column(length = 2000, nullable = false) - private String meaning; - - @ManyToOne - @JoinColumn(name = "glossary_id", nullable = false) - private Glossary glossary; - - @OneToMany(mappedBy = "term", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) - private List synonyms = new ArrayList<>(); - - public static Term forCreateOf( - String term, - String meaning, - Glossary glossary - ) { - Term result = new Term(); - result.term = term; - result.meaning = meaning; - result.glossary = glossary; - return result; - } - - public void addSynonym(String synonym) { - this.synonyms.add(TermSynonym.forCreateOf(synonym, this)); - } -} diff --git a/api/src/main/java/vook/server/api/model/TermRepository.java b/api/src/main/java/vook/server/api/model/TermRepository.java deleted file mode 100644 index ca73f733..00000000 --- a/api/src/main/java/vook/server/api/model/TermRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package vook.server.api.model; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface TermRepository extends JpaRepository { -} diff --git a/api/src/main/java/vook/server/api/model/TermSynonymRepository.java b/api/src/main/java/vook/server/api/model/TermSynonymRepository.java deleted file mode 100644 index 792a437c..00000000 --- a/api/src/main/java/vook/server/api/model/TermSynonymRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package vook.server.api.model; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface TermSynonymRepository extends JpaRepository { -} diff --git a/api/src/main/java/vook/server/api/model/Glossary.java b/api/src/main/java/vook/server/api/model/demo/DemoGlossary.java similarity index 56% rename from api/src/main/java/vook/server/api/model/Glossary.java rename to api/src/main/java/vook/server/api/model/demo/DemoGlossary.java index 6c28e0aa..a82daf04 100644 --- a/api/src/main/java/vook/server/api/model/Glossary.java +++ b/api/src/main/java/vook/server/api/model/demo/DemoGlossary.java @@ -1,17 +1,18 @@ -package vook.server.api.model; +package vook.server.api.model.demo; import jakarta.persistence.*; import lombok.Getter; +import vook.server.api.model.BaseEntity; import java.util.UUID; /** - * 용어집 + * 데모 용어집 */ @Getter @Entity -@Table(name = "glossary") -public class Glossary extends BaseEntity { +@Table(name = "demo_glossary") +public class DemoGlossary extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -28,18 +29,12 @@ public class Glossary extends BaseEntity { */ private String name; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) - private Member member; - - public static Glossary forCreateOf( - String name, - Member member + public static DemoGlossary forCreateOf( + String name ) { - Glossary result = new Glossary(); + DemoGlossary result = new DemoGlossary(); result.uid = UUID.randomUUID().toString(); result.name = name; - result.member = member; return result; } } diff --git a/api/src/main/java/vook/server/api/model/demo/DemoGlossaryRepository.java b/api/src/main/java/vook/server/api/model/demo/DemoGlossaryRepository.java new file mode 100644 index 00000000..96a223ff --- /dev/null +++ b/api/src/main/java/vook/server/api/model/demo/DemoGlossaryRepository.java @@ -0,0 +1,9 @@ +package vook.server.api.model.demo; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface DemoGlossaryRepository extends JpaRepository { + Optional findByUid(String uid); +} diff --git a/api/src/main/java/vook/server/api/model/demo/DemoTerm.java b/api/src/main/java/vook/server/api/model/demo/DemoTerm.java new file mode 100644 index 00000000..2750490a --- /dev/null +++ b/api/src/main/java/vook/server/api/model/demo/DemoTerm.java @@ -0,0 +1,56 @@ +package vook.server.api.model.demo; + +import jakarta.persistence.*; +import lombok.Getter; +import vook.server.api.model.BaseEntity; + +import java.util.ArrayList; +import java.util.List; + +/** + * 데모 용어 + */ +@Getter +@Entity +@Table(name = "demo_term") +public class DemoTerm extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 용어 이름 + */ + @Column(length = 100, nullable = false) + private String term; + + /** + * 용어 의미 + */ + @Column(length = 2000, nullable = false) + private String meaning; + + @ManyToOne + @JoinColumn(name = "demo_glossary_id", nullable = false) + private DemoGlossary demoGlossary; + + @OneToMany(mappedBy = "demoTerm", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + private List synonyms = new ArrayList<>(); + + public static DemoTerm forCreateOf( + String term, + String meaning, + DemoGlossary demoGlossary + ) { + DemoTerm result = new DemoTerm(); + result.term = term; + result.meaning = meaning; + result.demoGlossary = demoGlossary; + return result; + } + + public void addSynonym(String synonym) { + this.synonyms.add(DemoTermSynonym.forCreateOf(synonym, this)); + } +} diff --git a/api/src/main/java/vook/server/api/model/demo/DemoTermRepository.java b/api/src/main/java/vook/server/api/model/demo/DemoTermRepository.java new file mode 100644 index 00000000..04d229ae --- /dev/null +++ b/api/src/main/java/vook/server/api/model/demo/DemoTermRepository.java @@ -0,0 +1,6 @@ +package vook.server.api.model.demo; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DemoTermRepository extends JpaRepository { +} diff --git a/api/src/main/java/vook/server/api/model/TermSynonym.java b/api/src/main/java/vook/server/api/model/demo/DemoTermSynonym.java similarity index 50% rename from api/src/main/java/vook/server/api/model/TermSynonym.java rename to api/src/main/java/vook/server/api/model/demo/DemoTermSynonym.java index ab1c76bf..b537a6fb 100644 --- a/api/src/main/java/vook/server/api/model/TermSynonym.java +++ b/api/src/main/java/vook/server/api/model/demo/DemoTermSynonym.java @@ -1,15 +1,15 @@ -package vook.server.api.model; +package vook.server.api.model.demo; import jakarta.persistence.*; import lombok.Getter; /** - * 용어 동의어 + * 데모 용어 동의어 */ @Getter @Entity -@Table(name = "term_synonym") -public class TermSynonym { +@Table(name = "demo_term_synonym") +public class DemoTermSynonym { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -22,16 +22,16 @@ public class TermSynonym { private String synonym; @ManyToOne - @JoinColumn(name = "term_id", nullable = false) - private Term term; + @JoinColumn(name = "demo_term_id", nullable = false) + private DemoTerm demoTerm; - static TermSynonym forCreateOf( + static DemoTermSynonym forCreateOf( String synonym, - Term term + DemoTerm demoTerm ) { - TermSynonym result = new TermSynonym(); + DemoTermSynonym result = new DemoTermSynonym(); result.synonym = synonym; - result.term = term; + result.demoTerm = demoTerm; return result; } } diff --git a/api/src/main/java/vook/server/api/model/demo/DemoTermSynonymRepository.java b/api/src/main/java/vook/server/api/model/demo/DemoTermSynonymRepository.java new file mode 100644 index 00000000..c8a7d957 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/demo/DemoTermSynonymRepository.java @@ -0,0 +1,6 @@ +package vook.server.api.model.demo; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DemoTermSynonymRepository extends JpaRepository { +} diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchParams.java b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java similarity index 89% rename from api/src/main/java/vook/server/api/outbound/search/SearchParams.java rename to api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java index 2b517fd0..d9e1f30b 100644 --- a/api/src/main/java/vook/server/api/outbound/search/SearchParams.java +++ b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java @@ -4,16 +4,16 @@ import lombok.Builder; import lombok.Getter; import org.springframework.util.StringUtils; -import vook.server.api.model.Glossary; +import vook.server.api.model.demo.DemoGlossary; @Getter @Builder -public class SearchParams { +public class DemoTermSearchParams { private static final String DEFAULT_HIGHLIGHT_PRE_TAG = ""; private static final String DEFAULT_HIGHLIGHT_POST_TAG = ""; - private Glossary glossary; + private DemoGlossary demoGlossary; private String query; private boolean withFormat; private String highlightPreTag; diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchResult.java b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchResult.java new file mode 100644 index 00000000..73badba3 --- /dev/null +++ b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchResult.java @@ -0,0 +1,23 @@ +package vook.server.api.outbound.search; + +import com.meilisearch.sdk.model.Searchable; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.HashMap; + +@Getter +public class DemoTermSearchResult { + + private String query; + private int processingTimeMs; + private ArrayList> hits; + + public static DemoTermSearchResult from(Searchable search) { + DemoTermSearchResult demoTermSearchResult = new DemoTermSearchResult(); + demoTermSearchResult.query = search.getQuery(); + demoTermSearchResult.processingTimeMs = search.getProcessingTimeMs(); + demoTermSearchResult.hits = search.getHits(); + return demoTermSearchResult; + } +} diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java new file mode 100644 index 00000000..84159344 --- /dev/null +++ b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java @@ -0,0 +1,99 @@ +package vook.server.api.outbound.search; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.meilisearch.sdk.Index; +import com.meilisearch.sdk.SearchRequest; +import com.meilisearch.sdk.model.Searchable; +import com.meilisearch.sdk.model.TaskInfo; +import com.meilisearch.sdk.model.TypoTolerance; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import vook.server.api.model.demo.DemoGlossary; +import vook.server.api.model.demo.DemoTerm; +import vook.server.api.model.demo.DemoTermSynonym; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class DemoTermSearchService extends MeilisearchService { + + private static final String INDEX_PREFIX = "demo-"; + + private final ObjectMapper objectMapper; + + public void clearAll() { + clearAll(INDEX_PREFIX); + } + + public void createGlossary(DemoGlossary demoGlossary) { + String indexUid = getIndexUid(demoGlossary); + + TaskInfo indexCreateTask = client.createIndex(indexUid, "id"); + client.waitForTask(indexCreateTask.getTaskUid()); + + // 용어, 동의어, 뜻에 대해서만 검색 + client.index(indexUid).updateSearchableAttributesSettings(new String[]{ + "term", + "synonyms", + "meaning" + }); + + // 오타 용인을 비활성화 하여도 띄어쓰기에 대해서는 검색이 됨으로 비활성화 함 + TypoTolerance typoTolerance = new TypoTolerance(); + typoTolerance.setEnabled(false); + client.index(indexUid).updateTypoToleranceSettings(typoTolerance); + } + + public void addTerms(DemoGlossary demoGlossary, List terms) { + String indexUid = getIndexUid(demoGlossary); + Index index = client.index(indexUid); + TaskInfo taskInfo = index.addDocuments(getDocuments(terms)); + client.waitForTask(taskInfo.getTaskUid()); + } + + public DemoTermSearchResult search(DemoTermSearchParams params) { + SearchRequest searchRequest = params.buildSearchRequest(); + Searchable search = this.client.getIndex(getIndexUid(params.getDemoGlossary())).search(searchRequest); + return DemoTermSearchResult.from(search); + } + + private static String getIndexUid(DemoGlossary glossary) { + return INDEX_PREFIX + glossary.getUid(); + } + + private String getDocuments(List terms) { + try { + return objectMapper.writeValueAsString(Document.from(terms)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @Getter + @AllArgsConstructor + public static class Document { + private Long id; + private String term; + private String synonyms; + private String meaning; + private String createdAt; + + public static List from(List terms) { + return terms.stream() + .map(w -> new Document( + w.getId(), + w.getTerm(), + w.getSynonyms().stream().map(DemoTermSynonym::getSynonym).collect(Collectors.joining("\n")), + w.getMeaning(), + w.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + )) + .toList(); + } + } +} diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java index 386b8a0b..dc291d18 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -1,122 +1,39 @@ package vook.server.api.outbound.search; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.meilisearch.sdk.Client; import com.meilisearch.sdk.Config; import com.meilisearch.sdk.Index; -import com.meilisearch.sdk.SearchRequest; -import com.meilisearch.sdk.model.*; +import com.meilisearch.sdk.model.IndexesQuery; +import com.meilisearch.sdk.model.Results; import jakarta.annotation.PostConstruct; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import vook.server.api.model.Glossary; -import vook.server.api.model.Term; -import vook.server.api.model.TermSynonym; -import java.time.format.DateTimeFormatter; import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -@Service -@RequiredArgsConstructor -public class MeilisearchService implements SearchService, SearchClearable { - - private final ObjectMapper objectMapper; +public abstract class MeilisearchService { @Value("${service.meilisearch.host:}") - private String host; + protected String host; @Value("${service.meilisearch.apiKey:}") - private String apiKey; + protected String apiKey; - private Client client; + protected Client client; @PostConstruct public void postConstruct() { this.client = new Client(new Config(host, apiKey)); } - @Override - public void clearAll() { + protected void clearAll(String prefix) { Results indexes = client.getIndexes(new IndexesQuery() {{ setLimit(Integer.MAX_VALUE); }}); - Arrays.stream(indexes.getResults()).forEach(index -> { - client.deleteIndex(index.getUid()); - }); - } - - @Override - public void createGlossary(Glossary glossary) { - String indexUid = getIndexUid(glossary); - TaskInfo indexCreateTask = client.createIndex(indexUid, "id"); - client.waitForTask(indexCreateTask.getTaskUid()); - - // 용어, 동의어, 뜻에 대해서만 검색 - client.index(indexUid).updateSearchableAttributesSettings(new String[]{ - "term", - "synonyms", - "meaning" - }); - - // 오타 용인을 비활성화 하여도 띄어쓰기에 대해서는 검색이 됨으로 비활성화 함 - TypoTolerance typoTolerance = new TypoTolerance(); - typoTolerance.setEnabled(false); - client.index(indexUid).updateTypoToleranceSettings(typoTolerance); - } - - @Override - public void addTerms(List terms, Glossary glossary) { - Index index = client.index(getIndexUid(glossary)); - TaskInfo taskInfo = index.addDocuments(getDocuments(terms)); - client.waitForTask(taskInfo.getTaskUid()); - } - - @Override - public SearchResult search(SearchParams params) { - SearchRequest searchRequest = params.buildSearchRequest(); - Searchable search = this.client.getIndex(getIndexUid(params.getGlossary())).search(searchRequest); - return SearchResult.from(search); - } - - @NotNull - private static String getIndexUid(Glossary glossary) { - return glossary.getUid(); - } - - private String getDocuments(List terms) { - try { - return objectMapper.writeValueAsString(Document.from(terms)); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - @Getter - @AllArgsConstructor - public static class Document { - private Long id; - private String term; - private String synonyms; - private String meaning; - private String createdAt; - - public static List from(List terms) { - return terms.stream() - .map(w -> new Document( - w.getId(), - w.getTerm(), - w.getSynonyms().stream().map(TermSynonym::getSynonym).collect(Collectors.joining("\n")), - w.getMeaning(), - w.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - )) - .toList(); - } + Arrays.stream(indexes.getResults()) + .map(Index::getUid) + .filter(uid -> uid.startsWith(prefix)) + .forEach(uid -> { + client.deleteIndex(uid); + }); } } diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchClearable.java b/api/src/main/java/vook/server/api/outbound/search/SearchClearable.java deleted file mode 100644 index cc17a6e1..00000000 --- a/api/src/main/java/vook/server/api/outbound/search/SearchClearable.java +++ /dev/null @@ -1,5 +0,0 @@ -package vook.server.api.outbound.search; - -public interface SearchClearable { - void clearAll(); -} diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchResult.java b/api/src/main/java/vook/server/api/outbound/search/SearchResult.java deleted file mode 100644 index 5ead3389..00000000 --- a/api/src/main/java/vook/server/api/outbound/search/SearchResult.java +++ /dev/null @@ -1,23 +0,0 @@ -package vook.server.api.outbound.search; - -import com.meilisearch.sdk.model.Searchable; -import lombok.Getter; - -import java.util.ArrayList; -import java.util.HashMap; - -@Getter -public class SearchResult { - - private String query; - private int processingTimeMs; - private ArrayList> hits; - - public static SearchResult from(Searchable search) { - SearchResult searchResult = new SearchResult(); - searchResult.query = search.getQuery(); - searchResult.processingTimeMs = search.getProcessingTimeMs(); - searchResult.hits = search.getHits(); - return searchResult; - } -} diff --git a/api/src/main/java/vook/server/api/outbound/search/SearchService.java b/api/src/main/java/vook/server/api/outbound/search/SearchService.java deleted file mode 100644 index 826cbd63..00000000 --- a/api/src/main/java/vook/server/api/outbound/search/SearchService.java +++ /dev/null @@ -1,14 +0,0 @@ -package vook.server.api.outbound.search; - -import vook.server.api.model.Glossary; -import vook.server.api.model.Term; - -import java.util.List; - -public interface SearchService { - void createGlossary(Glossary glossary); - - void addTerms(List terms, Glossary glossary); - - SearchResult search(SearchParams params); -} diff --git a/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java index ed9302dc..e5b0bf08 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java @@ -4,12 +4,11 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.GlossaryService; -import vook.server.api.app.TermService; -import vook.server.api.model.Glossary; -import vook.server.api.model.Term; -import vook.server.api.outbound.search.SearchResult; -import vook.server.api.outbound.search.SearchService; +import vook.server.api.app.DemoService; +import vook.server.api.model.demo.DemoGlossary; +import vook.server.api.model.demo.DemoTerm; +import vook.server.api.outbound.search.DemoTermSearchResult; +import vook.server.api.outbound.search.DemoTermSearchService; import vook.server.api.web.routes.demo.reqres.RetrieveGlossariesResponse; import vook.server.api.web.routes.demo.reqres.RetrieveTermsResponse; import vook.server.api.web.routes.demo.reqres.SearchTermRequest; @@ -22,24 +21,23 @@ @RequiredArgsConstructor public class DemoWebService { - private final GlossaryService glossaryService; - private final TermService termService; - private final SearchService searchService; + private final DemoService demoService; + private final DemoTermSearchService searchService; public List retrieveGlossaries() { - List glossaries = glossaryService.findAll(); + List glossaries = demoService.findAllDemoGlossary(); return RetrieveGlossariesResponse.from(glossaries); } public List retrieveTerms(String glossaryUid, Pageable pageable) { - Glossary glossary = glossaryService.findByUid(glossaryUid).orElseThrow(); - List terms = termService.findAllBy(glossary, pageable); - return RetrieveTermsResponse.from(glossary, terms); + DemoGlossary glossary = demoService.findDemoGlossaryByUid(glossaryUid).orElseThrow(); + List terms = demoService.findAllDemoTermBy(glossary, pageable); + return RetrieveTermsResponse.from(terms); } public SearchTermResponse searchTerm(String glossaryUid, SearchTermRequest request) { - Glossary glossary = glossaryService.findByUid(glossaryUid).orElseThrow(); - SearchResult searchResult = searchService.search(request.toSearchParam(glossary)); + DemoGlossary glossary = demoService.findDemoGlossaryByUid(glossaryUid).orElseThrow(); + DemoTermSearchResult searchResult = searchService.search(request.toSearchParam(glossary)); return SearchTermResponse.from(searchResult); } } diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java index 0d50c185..f71dc4ce 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; -import vook.server.api.model.Glossary; +import vook.server.api.model.demo.DemoGlossary; import java.util.List; @@ -14,13 +14,13 @@ public class RetrieveGlossariesResponse { @Schema(description = "용어집 이름", examples = "실무") private String name; - public static List from(List glossaries) { + public static List from(List glossaries) { return glossaries.stream() .map(RetrieveGlossariesResponse::from) .toList(); } - public static RetrieveGlossariesResponse from(Glossary glossary) { + public static RetrieveGlossariesResponse from(DemoGlossary glossary) { RetrieveGlossariesResponse response = new RetrieveGlossariesResponse(); response.uid = glossary.getUid(); response.name = glossary.getName(); diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java index 003ea3d1..a4010c33 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java @@ -2,9 +2,8 @@ import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; -import vook.server.api.model.Glossary; -import vook.server.api.model.Term; -import vook.server.api.model.TermSynonym; +import vook.server.api.model.demo.DemoTerm; +import vook.server.api.model.demo.DemoTermSynonym; import java.time.LocalDateTime; import java.util.List; @@ -23,19 +22,19 @@ public class RetrieveTermsResponse { private String createdBy; - public static List from(Glossary glossary, List terms) { + public static List from(List terms) { return terms.stream() - .map(term -> from(glossary, term)) + .map(RetrieveTermsResponse::from) .toList(); } - public static RetrieveTermsResponse from(Glossary glossary, Term term) { + public static RetrieveTermsResponse from(DemoTerm term) { RetrieveTermsResponse response = new RetrieveTermsResponse(); response.term = term.getTerm(); - response.synonyms = term.getSynonyms().stream().map(TermSynonym::getSynonym).toList(); + response.synonyms = term.getSynonyms().stream().map(DemoTermSynonym::getSynonym).toList(); response.meaning = term.getMeaning(); response.createdAt = term.getCreatedAt(); - response.createdBy = glossary.getMember().getName(); + response.createdBy = "vook"; return response; } } diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java index e45eece2..120ed791 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java @@ -2,8 +2,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import vook.server.api.model.Glossary; -import vook.server.api.outbound.search.SearchParams; +import vook.server.api.model.demo.DemoGlossary; +import vook.server.api.outbound.search.DemoTermSearchParams; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -22,9 +22,9 @@ public class SearchTermRequest { @Schema(description = "하이라이트 종료 태그, 포맷 적용 여부가 true일 때만 적용 됨", defaultValue = "") private String highlightPostTag; - public SearchParams toSearchParam(Glossary glossary) { - return SearchParams.builder() - .glossary(glossary) + public DemoTermSearchParams toSearchParam(DemoGlossary glossary) { + return DemoTermSearchParams.builder() + .demoGlossary(glossary) .query(query) .withFormat(withFormat) .highlightPreTag(highlightPreTag) diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java index 7a97135f..dee35129 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java @@ -1,7 +1,7 @@ package vook.server.api.web.routes.demo.reqres; import lombok.Getter; -import vook.server.api.outbound.search.SearchResult; +import vook.server.api.outbound.search.DemoTermSearchResult; import java.util.List; import java.util.Map; @@ -12,10 +12,10 @@ public class SearchTermResponse { private String query; private List hits; - public static SearchTermResponse from(SearchResult searchResult) { - SearchTermResponse searchTermResponse = new SearchTermResponse(); - searchTermResponse.query = searchResult.getQuery(); - searchTermResponse.hits = searchResult.getHits().stream().map(document -> { + public static SearchTermResponse from(DemoTermSearchResult searchResult) { + SearchTermResponse searchResponse = new SearchTermResponse(); + searchResponse.query = searchResult.getQuery(); + searchResponse.hits = searchResult.getHits().stream().map(document -> { Object formatted = document.get("_formatted"); if (formatted instanceof Map formattedDocument) { return Document.from(formattedDocument); @@ -23,7 +23,7 @@ public static SearchTermResponse from(SearchResult searchResult) { return Document.from(document); } }).toList(); - return searchTermResponse; + return searchResponse; } @Getter From 8fd1571efd7b316b08e26882f4e695512b49c210 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 14 May 2024 11:23:50 +0900 Subject: [PATCH 055/273] =?UTF-8?q?fix:=20=EB=B0=B0=ED=8F=AC=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cicd/dagger/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cicd/dagger/main.go b/cicd/dagger/main.go index e81a508e..f538e818 100644 --- a/cicd/dagger/main.go +++ b/cicd/dagger/main.go @@ -129,7 +129,7 @@ func (v *VookServer) Apply( Command( fmt.Sprintf(` cd %s -FILENAME=%s VERSION=%s %s +API_FILENAME=%s API_VERSION=%s %s `, path, filename, version, command), ). Sync(ctx) From c8887bef8945d58b565aac83acd73e016740d068 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 14 May 2024 14:17:21 +0900 Subject: [PATCH 056/273] =?UTF-8?q?chore:=20staging=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20=EB=B0=B0=ED=8F=AC=EC=9A=A9=20github=20action=20workflow=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-stag.yml | 38 +++++++++++++++++++++++++++++++ cicd/Makefile | 11 +++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/workflows/deploy-stag.yml diff --git a/.github/workflows/deploy-stag.yml b/.github/workflows/deploy-stag.yml new file mode 100644 index 00000000..aa378538 --- /dev/null +++ b/.github/workflows/deploy-stag.yml @@ -0,0 +1,38 @@ +name: Deploy to Stag Environment + +on: + workflow_dispatch: + pull_request: + branches: + - release + types: + - closed + +jobs: + deploy: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22.2' + - name: Deploy + uses: dagger/dagger-for-github@v5 + env: + SSH_DEST: ${{ secrets.STAG_SSH_DEST }} + SSH_KEY: ${{ secrets.STAG_SSH_KEY }} + with: + version: 0.11.4 + verb: call + module: ./cicd + args: >- + deploy + --source-dir=api + --profile=stag + --ssh-dest=env:SSH_DEST + --ssh-key=env:SSH_KEY + --target-path=~/repo/makevook/vook-deployment/stag + --version=${{ github.sha }} + --command="make deploy-api" diff --git a/cicd/Makefile b/cicd/Makefile index d44f9202..d669dcd5 100644 --- a/cicd/Makefile +++ b/cicd/Makefile @@ -11,6 +11,17 @@ deploy-dev: --version=${VERSION} \ --command="make deploy-api" +.PHONY:deploy-stag +deploy-stag: + dagger call -v deploy \ + --source-dir=../api \ + --profile=stag \ + --ssh-dest=file:./secrets/stag/dest.txt \ + --ssh-key=file:./secrets/stag/ssh.key \ + --target-path=~/repo/makevook/vook-deployment/stag \ + --version=${VERSION} \ + --command="make deploy-api" + .PHONY:build-jar build-jar: dagger call -v build-api-jar --dir=../api --test -o out/api.jar From 43e4840c7a9022e33f5edaff3d616dfe9c9b29bd Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 14 May 2024 14:42:16 +0900 Subject: [PATCH 057/273] =?UTF-8?q?feat:=20staging=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20=EC=B4=88=EA=B8=B0=ED=99=94=20API?= =?UTF-8?q?=20=ED=98=B8=EC=B6=9C=EC=9D=B4=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20#47?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/vook/server/api/web/routes/init/InitController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/vook/server/api/web/routes/init/InitController.java b/api/src/main/java/vook/server/api/web/routes/init/InitController.java index dfc74f19..c6335e88 100644 --- a/api/src/main/java/vook/server/api/web/routes/init/InitController.java +++ b/api/src/main/java/vook/server/api/web/routes/init/InitController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RestController; import vook.server.api.devhelper.InitService; -@Profile({"local", "dev"}) +@Profile({"local", "dev", "stag"}) @RestController @RequestMapping("/init") @RequiredArgsConstructor From f38022791fcf2470c0b6a984582ec79eca2cbefc Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 14 May 2024 14:58:34 +0900 Subject: [PATCH 058/273] =?UTF-8?q?fix:=20workflow=5Fdispatch=20=EC=8B=9C?= =?UTF-8?q?=20workflow=EA=B0=80=20=EC=8B=A4=ED=96=89=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?#49?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/deploy-stag.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 6976effb..fed2eb59 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -10,7 +10,7 @@ on: jobs: deploy: - if: github.event.pull_request.merged == true + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.github/workflows/deploy-stag.yml b/.github/workflows/deploy-stag.yml index aa378538..c9ed2ae4 100644 --- a/.github/workflows/deploy-stag.yml +++ b/.github/workflows/deploy-stag.yml @@ -10,7 +10,7 @@ on: jobs: deploy: - if: github.event.pull_request.merged == true + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - name: Checkout code From 4727fd1c0f544bc1d70d8d842eb54204cc8c9298 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 16 May 2024 23:01:17 +0900 Subject: [PATCH 059/273] =?UTF-8?q?feat:=20=EB=8D=B0=EB=AA=A8=EC=97=90=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20API=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EB=B0=8F=20=EB=AA=A8=EB=8D=B8=EB=A7=81=20=EB=8B=A8=EC=88=9C?= =?UTF-8?q?=ED=99=94=20#51?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/app/DemoSearchRepository.java | 49 ------------------- .../java/vook/server/api/app/DemoService.java | 31 ------------ .../server/api/devhelper/InitService.java | 34 +++++-------- .../vook/server/api/devhelper/LocalInit.java | 6 +-- .../server/api/model/demo/DemoGlossary.java | 40 --------------- .../model/demo/DemoGlossaryRepository.java | 9 ---- .../vook/server/api/model/demo/DemoTerm.java | 8 +-- .../outbound/search/DemoTermSearchParams.java | 2 - .../search/DemoTermSearchService.java | 27 ++++------ .../outbound/search/MeilisearchService.java | 4 +- .../server/api/web/routes/demo/DemoApi.java | 46 +---------------- .../web/routes/demo/DemoRestController.java | 33 +++---------- .../api/web/routes/demo/DemoWebService.java | 25 +--------- .../reqres/RetrieveGlossariesResponse.java | 29 ----------- .../demo/reqres/RetrieveTermsResponse.java | 40 --------------- .../routes/demo/reqres/SearchTermRequest.java | 4 +- 16 files changed, 39 insertions(+), 348 deletions(-) delete mode 100644 api/src/main/java/vook/server/api/app/DemoSearchRepository.java delete mode 100644 api/src/main/java/vook/server/api/app/DemoService.java delete mode 100644 api/src/main/java/vook/server/api/model/demo/DemoGlossary.java delete mode 100644 api/src/main/java/vook/server/api/model/demo/DemoGlossaryRepository.java delete mode 100644 api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java delete mode 100644 api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java diff --git a/api/src/main/java/vook/server/api/app/DemoSearchRepository.java b/api/src/main/java/vook/server/api/app/DemoSearchRepository.java deleted file mode 100644 index a0aa82df..00000000 --- a/api/src/main/java/vook/server/api/app/DemoSearchRepository.java +++ /dev/null @@ -1,49 +0,0 @@ -package vook.server.api.app; - -import com.querydsl.jpa.impl.JPAQuery; -import com.querydsl.jpa.impl.JPAQueryFactory; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; -import vook.server.api.helper.QuerydslHelper; -import vook.server.api.model.demo.DemoTerm; -import vook.server.api.model.demo.QDemoTerm; - -import java.util.List; - -@Repository -@RequiredArgsConstructor -public class DemoSearchRepository { - - private final JPAQueryFactory queryFactory; - - public List searchDemoTerm(String demoGlossaryUid, Pageable pageable) { - QDemoTerm demoTerm = QDemoTerm.demoTerm; - - // fetch join과 pagination을 같이 하면, - // HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory - // 에러가 발생, 이를 피하기 위해 pagination이 포함된 쿼리를 서브 쿼리로 하여 수행 - JPAQuery termIdQuery = queryFactory - .select(demoTerm.id) - .from(demoTerm) - .where(demoTerm.demoGlossary.uid.eq(demoGlossaryUid)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()); - - QuerydslHelper - .toOrderSpecifiers(demoTerm, pageable) - .forEach(termIdQuery::orderBy); - - JPAQuery dataQuery = queryFactory - .selectFrom(demoTerm) - .leftJoin(demoTerm.synonyms).fetchJoin() - .where(demoTerm.id.in(termIdQuery)); - - // In 절에 들어간 입력순대로 출력되지 않음으로 정렬 조건을 다시 한번 설정 - QuerydslHelper - .toOrderSpecifiers(demoTerm, pageable) - .forEach(dataQuery::orderBy); - - return dataQuery.fetch(); - } -} diff --git a/api/src/main/java/vook/server/api/app/DemoService.java b/api/src/main/java/vook/server/api/app/DemoService.java deleted file mode 100644 index f5933611..00000000 --- a/api/src/main/java/vook/server/api/app/DemoService.java +++ /dev/null @@ -1,31 +0,0 @@ -package vook.server.api.app; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import vook.server.api.model.demo.DemoGlossary; -import vook.server.api.model.demo.DemoGlossaryRepository; -import vook.server.api.model.demo.DemoTerm; - -import java.util.List; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class DemoService { - - private final DemoGlossaryRepository demoGlossaryRepository; - private final DemoSearchRepository searchRepository; - - public List findAllDemoGlossary() { - return demoGlossaryRepository.findAll(); - } - - public Optional findDemoGlossaryByUid(String demoGlossaryUid) { - return demoGlossaryRepository.findByUid(demoGlossaryUid); - } - - public List findAllDemoTermBy(DemoGlossary demoGlossary, Pageable pageable) { - return searchRepository.searchDemoTerm(demoGlossary.getUid(), pageable); - } -} diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 5869e570..b87a2101 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -1,11 +1,12 @@ package vook.server.api.devhelper; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.model.demo.*; +import vook.server.api.model.demo.DemoTerm; +import vook.server.api.model.demo.DemoTermRepository; +import vook.server.api.model.demo.DemoTermSynonymRepository; import vook.server.api.outbound.search.DemoTermSearchService; import java.io.IOException; @@ -20,7 +21,6 @@ public class InitService { private final ResourceLoader resourceLoader; - private final DemoGlossaryRepository demoGlossaryRepository; private final DemoTermRepository demoTermRepository; private final DemoTermSynonymRepository demoTermSynonymRepository; private final DemoTermSearchService searchService; @@ -28,39 +28,31 @@ public class InitService { public void init() { demoTermSynonymRepository.deleteAllInBatch(); demoTermRepository.deleteAllInBatch(); - demoGlossaryRepository.deleteAllInBatch(); searchService.clearAll(); - DemoGlossary devGlossary = demoGlossaryRepository.save(DemoGlossary.forCreateOf("개발")); - searchService.createGlossary(devGlossary); - DemoGlossary designGlossary = demoGlossaryRepository.save(DemoGlossary.forCreateOf("디자인")); - searchService.createGlossary(designGlossary); - DemoGlossary marketingGlossary = demoGlossaryRepository.save(DemoGlossary.forCreateOf("마케팅")); - searchService.createGlossary(marketingGlossary); - DemoGlossary practiceGlossary = demoGlossaryRepository.save(DemoGlossary.forCreateOf("실무")); - searchService.createGlossary(practiceGlossary); + List devTerms = getTerms("classpath:init/개발.tsv"); + demoTermRepository.saveAll(devTerms); - List devTerms = getTerms("classpath:init/개발.tsv", devGlossary); - List terms = demoTermRepository.saveAll(devTerms); - searchService.addTerms(devGlossary, terms); + searchService.init(); + searchService.addTerms(devTerms); } - private List getTerms(String location, DemoGlossary glossary) { + private List getTerms(String location) { try { // file로 바로 접근 할 경우, IDE에서는 접근 가능하나, jar로 패키징 후 실행 시에는 접근 불가능 // ref) https://velog.io/@haron/트러블슈팅-Spring-IDE-에서-되는데-배포하면-안-돼요 InputStream tsvFileInputStream = resourceLoader.getResource(location).getInputStream(); CsvReader tsvReader = new CsvReader("\t"); List rawTerms = tsvReader.readValue(tsvFileInputStream, RawTerm.class); - return toTerms(rawTerms, glossary); + return toTerms(rawTerms); } catch (IOException e) { throw new RuntimeException(e); } } - private static @NotNull List toTerms(List rawTerms, DemoGlossary glossary) { + private static List toTerms(List rawTerms) { return rawTerms.stream() - .map(rawTerm -> rawTerm.toTerm(glossary)) + .map(RawTerm::toTerm) .toList(); } @@ -69,8 +61,8 @@ public static class RawTerm { private String synonyms; private String meaning; - public DemoTerm toTerm(DemoGlossary glossary) { - DemoTerm term = DemoTerm.forCreateOf(this.term, this.meaning, glossary); + public DemoTerm toTerm() { + DemoTerm term = DemoTerm.forCreateOf(this.term, this.meaning); String[] synonymArray = this.synonyms.split("//n"); Arrays.stream(synonymArray) .map(String::trim) diff --git a/api/src/main/java/vook/server/api/devhelper/LocalInit.java b/api/src/main/java/vook/server/api/devhelper/LocalInit.java index 7c6f6120..41e8d905 100644 --- a/api/src/main/java/vook/server/api/devhelper/LocalInit.java +++ b/api/src/main/java/vook/server/api/devhelper/LocalInit.java @@ -5,7 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import vook.server.api.model.demo.DemoGlossaryRepository; +import vook.server.api.model.demo.DemoTermRepository; @Slf4j @Profile("local") @@ -13,12 +13,12 @@ @RequiredArgsConstructor public class LocalInit { - private final DemoGlossaryRepository demoGlossaryRepository; + private final DemoTermRepository demoTermRepository; private final InitService initService; @PostConstruct public void init() { - if (demoGlossaryRepository.count() > 0) { + if (demoTermRepository.count() > 0) { return; } diff --git a/api/src/main/java/vook/server/api/model/demo/DemoGlossary.java b/api/src/main/java/vook/server/api/model/demo/DemoGlossary.java deleted file mode 100644 index a82daf04..00000000 --- a/api/src/main/java/vook/server/api/model/demo/DemoGlossary.java +++ /dev/null @@ -1,40 +0,0 @@ -package vook.server.api.model.demo; - -import jakarta.persistence.*; -import lombok.Getter; -import vook.server.api.model.BaseEntity; - -import java.util.UUID; - -/** - * 데모 용어집 - */ -@Getter -@Entity -@Table(name = "demo_glossary") -public class DemoGlossary extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * UID; 외부 노출용 식별자 - */ - @Column(length = 36, nullable = false, unique = true) - private String uid; - - /** - * 용어집 이름 - */ - private String name; - - public static DemoGlossary forCreateOf( - String name - ) { - DemoGlossary result = new DemoGlossary(); - result.uid = UUID.randomUUID().toString(); - result.name = name; - return result; - } -} diff --git a/api/src/main/java/vook/server/api/model/demo/DemoGlossaryRepository.java b/api/src/main/java/vook/server/api/model/demo/DemoGlossaryRepository.java deleted file mode 100644 index 96a223ff..00000000 --- a/api/src/main/java/vook/server/api/model/demo/DemoGlossaryRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package vook.server.api.model.demo; - -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface DemoGlossaryRepository extends JpaRepository { - Optional findByUid(String uid); -} diff --git a/api/src/main/java/vook/server/api/model/demo/DemoTerm.java b/api/src/main/java/vook/server/api/model/demo/DemoTerm.java index 2750490a..c56d2279 100644 --- a/api/src/main/java/vook/server/api/model/demo/DemoTerm.java +++ b/api/src/main/java/vook/server/api/model/demo/DemoTerm.java @@ -31,22 +31,16 @@ public class DemoTerm extends BaseEntity { @Column(length = 2000, nullable = false) private String meaning; - @ManyToOne - @JoinColumn(name = "demo_glossary_id", nullable = false) - private DemoGlossary demoGlossary; - @OneToMany(mappedBy = "demoTerm", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) private List synonyms = new ArrayList<>(); public static DemoTerm forCreateOf( String term, - String meaning, - DemoGlossary demoGlossary + String meaning ) { DemoTerm result = new DemoTerm(); result.term = term; result.meaning = meaning; - result.demoGlossary = demoGlossary; return result; } diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java index d9e1f30b..635ea421 100644 --- a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java +++ b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java @@ -4,7 +4,6 @@ import lombok.Builder; import lombok.Getter; import org.springframework.util.StringUtils; -import vook.server.api.model.demo.DemoGlossary; @Getter @Builder @@ -13,7 +12,6 @@ public class DemoTermSearchParams { private static final String DEFAULT_HIGHLIGHT_PRE_TAG = ""; private static final String DEFAULT_HIGHLIGHT_POST_TAG = ""; - private DemoGlossary demoGlossary; private String query; private boolean withFormat; private String highlightPreTag; diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java index 84159344..24378934 100644 --- a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java @@ -11,7 +11,6 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import vook.server.api.model.demo.DemoGlossary; import vook.server.api.model.demo.DemoTerm; import vook.server.api.model.demo.DemoTermSynonym; @@ -23,22 +22,20 @@ @RequiredArgsConstructor public class DemoTermSearchService extends MeilisearchService { - private static final String INDEX_PREFIX = "demo-"; + private static final String DEMO_TERMS_INDEX_UID = "demo-terms"; private final ObjectMapper objectMapper; public void clearAll() { - clearAll(INDEX_PREFIX); + clearAll(DEMO_TERMS_INDEX_UID); } - public void createGlossary(DemoGlossary demoGlossary) { - String indexUid = getIndexUid(demoGlossary); - - TaskInfo indexCreateTask = client.createIndex(indexUid, "id"); + public void init() { + TaskInfo indexCreateTask = client.createIndex(DEMO_TERMS_INDEX_UID, "id"); client.waitForTask(indexCreateTask.getTaskUid()); // 용어, 동의어, 뜻에 대해서만 검색 - client.index(indexUid).updateSearchableAttributesSettings(new String[]{ + client.index(DEMO_TERMS_INDEX_UID).updateSearchableAttributesSettings(new String[]{ "term", "synonyms", "meaning" @@ -47,26 +44,22 @@ public void createGlossary(DemoGlossary demoGlossary) { // 오타 용인을 비활성화 하여도 띄어쓰기에 대해서는 검색이 됨으로 비활성화 함 TypoTolerance typoTolerance = new TypoTolerance(); typoTolerance.setEnabled(false); - client.index(indexUid).updateTypoToleranceSettings(typoTolerance); + TaskInfo updateTypoToleranceSettingsTask = client.index(DEMO_TERMS_INDEX_UID).updateTypoToleranceSettings(typoTolerance); + client.waitForTask(updateTypoToleranceSettingsTask.getTaskUid()); } - public void addTerms(DemoGlossary demoGlossary, List terms) { - String indexUid = getIndexUid(demoGlossary); - Index index = client.index(indexUid); + public void addTerms(List terms) { + Index index = client.index(DEMO_TERMS_INDEX_UID); TaskInfo taskInfo = index.addDocuments(getDocuments(terms)); client.waitForTask(taskInfo.getTaskUid()); } public DemoTermSearchResult search(DemoTermSearchParams params) { SearchRequest searchRequest = params.buildSearchRequest(); - Searchable search = this.client.getIndex(getIndexUid(params.getDemoGlossary())).search(searchRequest); + Searchable search = this.client.getIndex(DEMO_TERMS_INDEX_UID).search(searchRequest); return DemoTermSearchResult.from(search); } - private static String getIndexUid(DemoGlossary glossary) { - return INDEX_PREFIX + glossary.getUid(); - } - private String getDocuments(List terms) { try { return objectMapper.writeValueAsString(Document.from(terms)); diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java index dc291d18..c23d55d1 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -25,13 +25,13 @@ public void postConstruct() { this.client = new Client(new Config(host, apiKey)); } - protected void clearAll(String prefix) { + protected void clearAll(String uidPrefix) { Results indexes = client.getIndexes(new IndexesQuery() {{ setLimit(Integer.MAX_VALUE); }}); Arrays.stream(indexes.getResults()) .map(Index::getUid) - .filter(uid -> uid.startsWith(prefix)) + .filter(uid -> uid.startsWith(uidPrefix)) .forEach(uid -> { client.deleteIndex(uid); }); diff --git a/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java index c17607b6..4274ccba 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java @@ -6,54 +6,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springdoc.core.annotations.ParameterObject; -import org.springframework.data.domain.Pageable; import vook.server.api.web.common.CommonApiResponse; -import vook.server.api.web.routes.demo.reqres.RetrieveGlossariesResponse; -import vook.server.api.web.routes.demo.reqres.RetrieveTermsResponse; import vook.server.api.web.routes.demo.reqres.SearchTermRequest; import vook.server.api.web.routes.demo.reqres.SearchTermResponse; -import java.util.List; - @Tag(name = "demo", description = "VooK 데모용 API") public interface DemoApi { - @Operation(summary = "용어집 리스트 조회") - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = RetrieveGlossariesApiResponse.class) - ) - ), - }) - CommonApiResponse> retrieveGlossaries(); - - class RetrieveGlossariesApiResponse extends CommonApiResponse> { - } - - @Operation(summary = "용어집 내 용어 리스트 조회") - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "성공", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = RetrieveTermsApiResponse.class) - ) - ), - }) - CommonApiResponse> retrieveTerms( - String glossaryUid, - @ParameterObject Pageable pageable - ); - - class RetrieveTermsApiResponse extends CommonApiResponse> { - } - @Operation(summary = "용어 검색") @ApiResponses(value = { @ApiResponse( @@ -64,10 +23,7 @@ class RetrieveTermsApiResponse extends CommonApiResponse searchTerm( - String glossaryUid, - SearchTermRequest request - ); + CommonApiResponse searchTerm(SearchTermRequest request); class SearchApiTermResponse extends CommonApiResponse { } diff --git a/api/src/main/java/vook/server/api/web/routes/demo/DemoRestController.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoRestController.java index 4dd36e16..c413c3bc 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/DemoRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoRestController.java @@ -1,17 +1,14 @@ package vook.server.api.web.routes.demo; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.web.bind.annotation.*; +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 vook.server.api.web.common.CommonApiResponse; -import vook.server.api.web.routes.demo.reqres.RetrieveGlossariesResponse; -import vook.server.api.web.routes.demo.reqres.RetrieveTermsResponse; import vook.server.api.web.routes.demo.reqres.SearchTermRequest; import vook.server.api.web.routes.demo.reqres.SearchTermResponse; -import java.util.List; - @RestController @RequestMapping("/demo") @RequiredArgsConstructor @@ -20,29 +17,11 @@ public class DemoRestController implements DemoApi { private final DemoWebService service; @Override - @GetMapping("/glossaries") - public CommonApiResponse> retrieveGlossaries() { - List result = service.retrieveGlossaries(); - return CommonApiResponse.okWithResult(result); - } - - @Override - @GetMapping("/glossaries/{glossaryUid}/terms") - public CommonApiResponse> retrieveTerms( - @PathVariable String glossaryUid, - @PageableDefault(size = Integer.MAX_VALUE, sort = "term") Pageable pageable - ) { - List result = service.retrieveTerms(glossaryUid, pageable); - return CommonApiResponse.okWithResult(result); - } - - @Override - @PostMapping("/glossaries/{glossaryUid}/terms/search") + @PostMapping("/terms/search") public CommonApiResponse searchTerm( - @PathVariable String glossaryUid, @RequestBody SearchTermRequest request ) { - SearchTermResponse result = service.searchTerm(glossaryUid, request); + SearchTermResponse result = service.searchTerm(request); return CommonApiResponse.okWithResult(result); } } diff --git a/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java index e5b0bf08..922fe96d 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java @@ -1,43 +1,22 @@ package vook.server.api.web.routes.demo; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.DemoService; -import vook.server.api.model.demo.DemoGlossary; -import vook.server.api.model.demo.DemoTerm; import vook.server.api.outbound.search.DemoTermSearchResult; import vook.server.api.outbound.search.DemoTermSearchService; -import vook.server.api.web.routes.demo.reqres.RetrieveGlossariesResponse; -import vook.server.api.web.routes.demo.reqres.RetrieveTermsResponse; import vook.server.api.web.routes.demo.reqres.SearchTermRequest; import vook.server.api.web.routes.demo.reqres.SearchTermResponse; -import java.util.List; - @Service @Transactional @RequiredArgsConstructor public class DemoWebService { - private final DemoService demoService; private final DemoTermSearchService searchService; - public List retrieveGlossaries() { - List glossaries = demoService.findAllDemoGlossary(); - return RetrieveGlossariesResponse.from(glossaries); - } - - public List retrieveTerms(String glossaryUid, Pageable pageable) { - DemoGlossary glossary = demoService.findDemoGlossaryByUid(glossaryUid).orElseThrow(); - List terms = demoService.findAllDemoTermBy(glossary, pageable); - return RetrieveTermsResponse.from(terms); - } - - public SearchTermResponse searchTerm(String glossaryUid, SearchTermRequest request) { - DemoGlossary glossary = demoService.findDemoGlossaryByUid(glossaryUid).orElseThrow(); - DemoTermSearchResult searchResult = searchService.search(request.toSearchParam(glossary)); + public SearchTermResponse searchTerm(SearchTermRequest request) { + DemoTermSearchResult searchResult = searchService.search(request.toSearchParam()); return SearchTermResponse.from(searchResult); } } diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java deleted file mode 100644 index f71dc4ce..00000000 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveGlossariesResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package vook.server.api.web.routes.demo.reqres; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import vook.server.api.model.demo.DemoGlossary; - -import java.util.List; - -@Getter -public class RetrieveGlossariesResponse { - - @Schema(description = "용어집 UID", examples = "38617d11-1d8e-4f77-a2fd-8cdca9de8420") - private String uid; - @Schema(description = "용어집 이름", examples = "실무") - private String name; - - public static List from(List glossaries) { - return glossaries.stream() - .map(RetrieveGlossariesResponse::from) - .toList(); - } - - public static RetrieveGlossariesResponse from(DemoGlossary glossary) { - RetrieveGlossariesResponse response = new RetrieveGlossariesResponse(); - response.uid = glossary.getUid(); - response.name = glossary.getName(); - return response; - } -} diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java deleted file mode 100644 index a4010c33..00000000 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/RetrieveTermsResponse.java +++ /dev/null @@ -1,40 +0,0 @@ -package vook.server.api.web.routes.demo.reqres; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Getter; -import vook.server.api.model.demo.DemoTerm; -import vook.server.api.model.demo.DemoTermSynonym; - -import java.time.LocalDateTime; -import java.util.List; - -@Getter -public class RetrieveTermsResponse { - - private String term; - - private List synonyms; - - private String meaning; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime createdAt; - - private String createdBy; - - public static List from(List terms) { - return terms.stream() - .map(RetrieveTermsResponse::from) - .toList(); - } - - public static RetrieveTermsResponse from(DemoTerm term) { - RetrieveTermsResponse response = new RetrieveTermsResponse(); - response.term = term.getTerm(); - response.synonyms = term.getSynonyms().stream().map(DemoTermSynonym::getSynonym).toList(); - response.meaning = term.getMeaning(); - response.createdAt = term.getCreatedAt(); - response.createdBy = "vook"; - return response; - } -} diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java index 120ed791..2965b1dd 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java @@ -2,7 +2,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import vook.server.api.model.demo.DemoGlossary; import vook.server.api.outbound.search.DemoTermSearchParams; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -22,9 +21,8 @@ public class SearchTermRequest { @Schema(description = "하이라이트 종료 태그, 포맷 적용 여부가 true일 때만 적용 됨", defaultValue = "") private String highlightPostTag; - public DemoTermSearchParams toSearchParam(DemoGlossary glossary) { + public DemoTermSearchParams toSearchParam() { return DemoTermSearchParams.builder() - .demoGlossary(glossary) .query(query) .withFormat(withFormat) .highlightPreTag(highlightPreTag) From e288766d459ca1ddee58a8142e9f479905051035 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 16 May 2024 23:22:18 +0900 Subject: [PATCH 060/273] =?UTF-8?q?feat:=20=EB=8D=B0=EB=AA=A8=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=EC=97=90=20sort=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20limit=20=ED=95=B4=EC=A0=9C=20#?= =?UTF-8?q?51?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../outbound/search/DemoTermSearchParams.java | 8 ++++++ .../search/DemoTermSearchService.java | 25 ++++++++++++++++--- .../routes/demo/reqres/SearchTermRequest.java | 14 +++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java index 635ea421..e5bab1ad 100644 --- a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java +++ b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java @@ -5,6 +5,8 @@ import lombok.Getter; import org.springframework.util.StringUtils; +import java.util.List; + @Getter @Builder public class DemoTermSearchParams { @@ -16,6 +18,7 @@ public class DemoTermSearchParams { private boolean withFormat; private String highlightPreTag; private String highlightPostTag; + private List sort; public SearchRequest buildSearchRequest() { SearchRequest.SearchRequestBuilder builder = SearchRequest.builder(); @@ -25,8 +28,13 @@ public SearchRequest buildSearchRequest() { builder.highlightPostTag(StringUtils.hasText(highlightPostTag) ? highlightPostTag : DEFAULT_HIGHLIGHT_POST_TAG); } + if (sort != null && !sort.isEmpty()) { + builder.sort(sort.toArray(new String[0])); + } + return builder .q(query) + .limit(Integer.MAX_VALUE) .build(); } } diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java index 24378934..c75fbe52 100644 --- a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java @@ -35,17 +35,36 @@ public void init() { client.waitForTask(indexCreateTask.getTaskUid()); // 용어, 동의어, 뜻에 대해서만 검색 - client.index(DEMO_TERMS_INDEX_UID).updateSearchableAttributesSettings(new String[]{ + TaskInfo updateSearchableTask = client.index(DEMO_TERMS_INDEX_UID).updateSearchableAttributesSettings(new String[]{ "term", "synonyms", "meaning" }); + client.waitForTask(updateSearchableTask.getTaskUid()); + + // 용어, 동의어, 뜻, 생성일시에 대해 정렬 가능 + TaskInfo updateSortableTask = client.index(DEMO_TERMS_INDEX_UID).updateSortableAttributesSettings(new String[]{ + "term", + "synonyms", + "meaning", + "createdAt" + }); + client.waitForTask(updateSortableTask.getTaskUid()); + + client.index(DEMO_TERMS_INDEX_UID).updateRankingRulesSettings(new String[]{ + "sort", + "words", + "typo", + "proximity", + "attribute", + "exactness" + }); // 오타 용인을 비활성화 하여도 띄어쓰기에 대해서는 검색이 됨으로 비활성화 함 TypoTolerance typoTolerance = new TypoTolerance(); typoTolerance.setEnabled(false); - TaskInfo updateTypoToleranceSettingsTask = client.index(DEMO_TERMS_INDEX_UID).updateTypoToleranceSettings(typoTolerance); - client.waitForTask(updateTypoToleranceSettingsTask.getTaskUid()); + TaskInfo updateTypoTask = client.index(DEMO_TERMS_INDEX_UID).updateTypoToleranceSettings(typoTolerance); + client.waitForTask(updateTypoTask.getTaskUid()); } public void addTerms(List terms) { diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java index 2965b1dd..529d8f78 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java @@ -4,6 +4,8 @@ import lombok.Data; import vook.server.api.outbound.search.DemoTermSearchParams; +import java.util.List; + import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @Data @@ -21,12 +23,24 @@ public class SearchTermRequest { @Schema(description = "하이라이트 종료 태그, 포맷 적용 여부가 true일 때만 적용 됨", defaultValue = "") private String highlightPostTag; + @Schema( + description = "정렬 정보, null이면 관련도 기준으로 정렬 됨", + allowableValues = { + "term:asc", "term:desc", + "synonyms:asc", "synonyms:desc", + "meaning:asc", "meaning:desc", + "createdAt:asc", "createdAt:desc" + } + ) + private List sort; + public DemoTermSearchParams toSearchParam() { return DemoTermSearchParams.builder() .query(query) .withFormat(withFormat) .highlightPreTag(highlightPreTag) .highlightPostTag(highlightPostTag) + .sort(sort) .build(); } } From 06e207f68a460e578e688726b9c3e1fceafee6f9 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 16 May 2024 23:24:12 +0900 Subject: [PATCH 061/273] =?UTF-8?q?docs:=20=EB=8D=B0=EB=AA=A8=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=20=EB=AC=B8=EC=84=9C=EC=9D=98=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=20=EC=83=81=EC=84=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/web/routes/demo/reqres/SearchTermRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java index 529d8f78..a2145757 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java @@ -11,7 +11,7 @@ @Data public class SearchTermRequest { - @Schema(description = "검색 쿼리", requiredMode = REQUIRED, example = "하이브리드앱") + @Schema(description = "검색 쿼리, 빈 문자열을 보낼경우 모든 데이터가 반환된다.", requiredMode = REQUIRED, example = "하이브리드앱") private String query; @Schema(description = "포맷 적용 여부", defaultValue = "false") From 2bd8e9b46ee9c365a008fc6fef91e06e8f8d7f64 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 00:31:24 +0900 Subject: [PATCH 062/273] =?UTF-8?q?refector:=20swagger=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/vook/server/api/config/SwaggerConfig.java | 4 ++-- .../api/{ => web}/swagger/GlobalOpenApiCustomizerImpl.java | 2 +- .../api/{ => web}/swagger/GlobalOperationCustomizerImpl.java | 2 +- .../vook/server/api/{ => web}/swagger/OpenApiDefinition.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename api/src/main/java/vook/server/api/{ => web}/swagger/GlobalOpenApiCustomizerImpl.java (95%) rename api/src/main/java/vook/server/api/{ => web}/swagger/GlobalOperationCustomizerImpl.java (97%) rename api/src/main/java/vook/server/api/{ => web}/swagger/OpenApiDefinition.java (86%) diff --git a/api/src/main/java/vook/server/api/config/SwaggerConfig.java b/api/src/main/java/vook/server/api/config/SwaggerConfig.java index 54057a5c..e2143854 100644 --- a/api/src/main/java/vook/server/api/config/SwaggerConfig.java +++ b/api/src/main/java/vook/server/api/config/SwaggerConfig.java @@ -4,8 +4,8 @@ import org.springdoc.core.customizers.GlobalOperationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import vook.server.api.swagger.GlobalOpenApiCustomizerImpl; -import vook.server.api.swagger.GlobalOperationCustomizerImpl; +import vook.server.api.web.swagger.GlobalOpenApiCustomizerImpl; +import vook.server.api.web.swagger.GlobalOperationCustomizerImpl; @Configuration public class SwaggerConfig { diff --git a/api/src/main/java/vook/server/api/swagger/GlobalOpenApiCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java similarity index 95% rename from api/src/main/java/vook/server/api/swagger/GlobalOpenApiCustomizerImpl.java rename to api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java index 4e37fa34..f1cafa0d 100644 --- a/api/src/main/java/vook/server/api/swagger/GlobalOpenApiCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java @@ -1,4 +1,4 @@ -package vook.server.api.swagger; +package vook.server.api.web.swagger; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.IntegerSchema; diff --git a/api/src/main/java/vook/server/api/swagger/GlobalOperationCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java similarity index 97% rename from api/src/main/java/vook/server/api/swagger/GlobalOperationCustomizerImpl.java rename to api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java index a5ee1766..0449495c 100644 --- a/api/src/main/java/vook/server/api/swagger/GlobalOperationCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java @@ -1,4 +1,4 @@ -package vook.server.api.swagger; +package vook.server.api.web.swagger; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.examples.Example; diff --git a/api/src/main/java/vook/server/api/swagger/OpenApiDefinition.java b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java similarity index 86% rename from api/src/main/java/vook/server/api/swagger/OpenApiDefinition.java rename to api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java index ec9fd2c5..83daa753 100644 --- a/api/src/main/java/vook/server/api/swagger/OpenApiDefinition.java +++ b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java @@ -1,4 +1,4 @@ -package vook.server.api.swagger; +package vook.server.api.web.swagger; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.servers.Server; From d6815d564307a462132174d0be4dcfa02c1ac58b Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 00:32:53 +0900 Subject: [PATCH 063/273] =?UTF-8?q?refector:=20model=20=EB=82=B4=20reposit?= =?UTF-8?q?ory=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20app=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/{model => app}/demo/DemoTermRepository.java | 3 ++- .../api/{model => app}/demo/DemoTermSynonymRepository.java | 3 ++- api/src/main/java/vook/server/api/devhelper/InitService.java | 4 ++-- api/src/main/java/vook/server/api/devhelper/LocalInit.java | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) rename api/src/main/java/vook/server/api/{model => app}/demo/DemoTermRepository.java (64%) rename api/src/main/java/vook/server/api/{model => app}/demo/DemoTermSynonymRepository.java (64%) diff --git a/api/src/main/java/vook/server/api/model/demo/DemoTermRepository.java b/api/src/main/java/vook/server/api/app/demo/DemoTermRepository.java similarity index 64% rename from api/src/main/java/vook/server/api/model/demo/DemoTermRepository.java rename to api/src/main/java/vook/server/api/app/demo/DemoTermRepository.java index 04d229ae..e304a5be 100644 --- a/api/src/main/java/vook/server/api/model/demo/DemoTermRepository.java +++ b/api/src/main/java/vook/server/api/app/demo/DemoTermRepository.java @@ -1,6 +1,7 @@ -package vook.server.api.model.demo; +package vook.server.api.app.demo; import org.springframework.data.jpa.repository.JpaRepository; +import vook.server.api.model.demo.DemoTerm; public interface DemoTermRepository extends JpaRepository { } diff --git a/api/src/main/java/vook/server/api/model/demo/DemoTermSynonymRepository.java b/api/src/main/java/vook/server/api/app/demo/DemoTermSynonymRepository.java similarity index 64% rename from api/src/main/java/vook/server/api/model/demo/DemoTermSynonymRepository.java rename to api/src/main/java/vook/server/api/app/demo/DemoTermSynonymRepository.java index c8a7d957..7298bdb0 100644 --- a/api/src/main/java/vook/server/api/model/demo/DemoTermSynonymRepository.java +++ b/api/src/main/java/vook/server/api/app/demo/DemoTermSynonymRepository.java @@ -1,6 +1,7 @@ -package vook.server.api.model.demo; +package vook.server.api.app.demo; import org.springframework.data.jpa.repository.JpaRepository; +import vook.server.api.model.demo.DemoTermSynonym; public interface DemoTermSynonymRepository extends JpaRepository { } diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index b87a2101..7ea4b9ea 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -4,9 +4,9 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.demo.DemoTermRepository; +import vook.server.api.app.demo.DemoTermSynonymRepository; import vook.server.api.model.demo.DemoTerm; -import vook.server.api.model.demo.DemoTermRepository; -import vook.server.api.model.demo.DemoTermSynonymRepository; import vook.server.api.outbound.search.DemoTermSearchService; import java.io.IOException; diff --git a/api/src/main/java/vook/server/api/devhelper/LocalInit.java b/api/src/main/java/vook/server/api/devhelper/LocalInit.java index 41e8d905..b4d92925 100644 --- a/api/src/main/java/vook/server/api/devhelper/LocalInit.java +++ b/api/src/main/java/vook/server/api/devhelper/LocalInit.java @@ -5,7 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import vook.server.api.model.demo.DemoTermRepository; +import vook.server.api.app.demo.DemoTermRepository; @Slf4j @Profile("local") From f02811ec70d350827224a89734e7d92d06b4a965 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 03:28:00 +0900 Subject: [PATCH 064/273] =?UTF-8?q?feat:=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B3=84=EC=A0=95=20=EC=83=9D=EC=84=B1=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle | 2 + .../server/api/app/auth/GeneratedToken.java | 17 ++++ .../server/api/app/auth/TokenService.java | 66 +++++++++++++ .../api/app/user/SignUpFromSocialCommand.java | 33 +++++++ .../api/app/user/SocialUserRepository.java | 10 ++ .../server/api/app/user/UserRepository.java | 10 ++ .../vook/server/api/app/user/UserService.java | 31 ++++++ .../server/api/config/SecurityConfig.java | 95 +++++++++++++++++++ .../api/config/auth/common/VookLoginUser.java | 67 +++++++++++++ .../server/api/config/auth/jwt/JWTFilter.java | 65 +++++++++++++ .../auth/oauth2/LoginSuccessHandler.java | 40 ++++++++ .../auth/oauth2/OAuth2GoogleResponse.java | 27 ++++++ .../config/auth/oauth2/OAuth2Response.java | 12 +++ .../auth/oauth2/VookOAuth2UserService.java | 57 +++++++++++ .../vook/server/api/helper/jwt/JWTHelper.java | 16 ++++ .../api/helper/jwt/JWTHelperProvider.java | 29 ++++++ .../vook/server/api/helper/jwt/JWTReader.java | 56 +++++++++++ .../vook/server/api/helper/jwt/JWTWriter.java | 76 +++++++++++++++ .../server/api/model/user/SocialUser.java | 34 +++++++ .../java/vook/server/api/model/user/User.java | 48 ++++++++++ .../vook/server/api/model/user/UserInfo.java | 27 ++++++ .../server/api/model/user/UserStatus.java | 8 ++ .../server/api/model/values/AuthValues.java | 6 ++ .../server/api/web/routes/auth/AuthApi.java | 30 ++++++ .../web/routes/auth/AuthRestController.java | 32 +++++++ .../api/web/swagger/OpenApiDefinition.java | 26 ++++- api/src/main/resources/application-local.yml | 20 ++++ 27 files changed, 939 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/vook/server/api/app/auth/GeneratedToken.java create mode 100644 api/src/main/java/vook/server/api/app/auth/TokenService.java create mode 100644 api/src/main/java/vook/server/api/app/user/SignUpFromSocialCommand.java create mode 100644 api/src/main/java/vook/server/api/app/user/SocialUserRepository.java create mode 100644 api/src/main/java/vook/server/api/app/user/UserRepository.java create mode 100644 api/src/main/java/vook/server/api/app/user/UserService.java create mode 100644 api/src/main/java/vook/server/api/config/SecurityConfig.java create mode 100644 api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java create mode 100644 api/src/main/java/vook/server/api/config/auth/jwt/JWTFilter.java create mode 100644 api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java create mode 100644 api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2GoogleResponse.java create mode 100644 api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2Response.java create mode 100644 api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java create mode 100644 api/src/main/java/vook/server/api/helper/jwt/JWTHelper.java create mode 100644 api/src/main/java/vook/server/api/helper/jwt/JWTHelperProvider.java create mode 100644 api/src/main/java/vook/server/api/helper/jwt/JWTReader.java create mode 100644 api/src/main/java/vook/server/api/helper/jwt/JWTWriter.java create mode 100644 api/src/main/java/vook/server/api/model/user/SocialUser.java create mode 100644 api/src/main/java/vook/server/api/model/user/User.java create mode 100644 api/src/main/java/vook/server/api/model/user/UserInfo.java create mode 100644 api/src/main/java/vook/server/api/model/user/UserStatus.java create mode 100644 api/src/main/java/vook/server/api/model/values/AuthValues.java create mode 100644 api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java create mode 100644 api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java diff --git a/api/build.gradle b/api/build.gradle index 67458798..2b66b72d 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -25,6 +25,8 @@ dependencies { // spring implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' developmentOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/api/src/main/java/vook/server/api/app/auth/GeneratedToken.java b/api/src/main/java/vook/server/api/app/auth/GeneratedToken.java new file mode 100644 index 00000000..eb6bdf2c --- /dev/null +++ b/api/src/main/java/vook/server/api/app/auth/GeneratedToken.java @@ -0,0 +1,17 @@ +package vook.server.api.app.auth; + +import lombok.Getter; + +@Getter +public class GeneratedToken { + + private String accessToken; + private String refreshToken; + + public static GeneratedToken of(String accessToken, String refreshToken) { + GeneratedToken generatedToken = new GeneratedToken(); + generatedToken.accessToken = accessToken; + generatedToken.refreshToken = refreshToken; + return generatedToken; + } +} diff --git a/api/src/main/java/vook/server/api/app/auth/TokenService.java b/api/src/main/java/vook/server/api/app/auth/TokenService.java new file mode 100644 index 00000000..1accdb11 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/auth/TokenService.java @@ -0,0 +1,66 @@ +package vook.server.api.app.auth; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import vook.server.api.helper.jwt.JWTHelperProvider; +import vook.server.api.helper.jwt.JWTReader; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenService { + + @Value("${service.oauth2.accessTokenExpiredMinute}") + private Integer accessTokenExpiredMinute; + + @Value("${service.oauth2.refreshTokenExpiredMinute}") + private Integer refreshTokenExpiredMinute; + + private final JWTHelperProvider jwtHelperProvider; + + public GeneratedToken generateToken(String uid) { + String access = buildAccessToken(uid); + String refresh = buildRefreshToken(uid); + return GeneratedToken.of(access, refresh); + } + + public String validateAndGetUid(String token) { + JWTReader reader = jwtHelperProvider.reader(token); + reader.validate(); + return reader.getClaim("uid"); + } + + public GeneratedToken refreshToken(String refreshToken) { + JWTReader jwtReader = jwtHelperProvider.reader(refreshToken); + jwtReader.validate(); + + if (!"refresh".equals(jwtReader.getClaim("category"))) { + throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다."); + } + + String uid = jwtReader.getClaim("uid"); + + String access = buildAccessToken(uid); + String refresh = buildRefreshToken(uid); + + return GeneratedToken.of(access, refresh); + } + + private String buildAccessToken(String uid) { + return jwtHelperProvider.builder() + .withExpiredMs(1000L * 60 * accessTokenExpiredMinute) + .withClaim("category", "access") + .withClaim("uid", uid) + .jwtString(); + } + + private String buildRefreshToken(String uid) { + return jwtHelperProvider.builder() + .withExpiredMs(1000L * 60 * refreshTokenExpiredMinute) + .withClaim("category", "refresh") + .withClaim("uid", uid) + .jwtString(); + } +} diff --git a/api/src/main/java/vook/server/api/app/user/SignUpFromSocialCommand.java b/api/src/main/java/vook/server/api/app/user/SignUpFromSocialCommand.java new file mode 100644 index 00000000..bbd8802b --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/SignUpFromSocialCommand.java @@ -0,0 +1,33 @@ +package vook.server.api.app.user; + +import lombok.Getter; +import vook.server.api.model.user.SocialUser; +import vook.server.api.model.user.User; + +@Getter +public class SignUpFromSocialCommand { + + private String provider; + private String providerUserId; + private String email; + + public static SignUpFromSocialCommand of( + String provider, + String providerUserId, + String email + ) { + SignUpFromSocialCommand command = new SignUpFromSocialCommand(); + command.provider = provider; + command.providerUserId = providerUserId; + command.email = email; + return command; + } + + public SocialUser toSocialUser(User user) { + return SocialUser.forNewOf(provider, providerUserId, user); + } + + public User toNewUser() { + return User.forSignUpFromSocialOf(email); + } +} diff --git a/api/src/main/java/vook/server/api/app/user/SocialUserRepository.java b/api/src/main/java/vook/server/api/app/user/SocialUserRepository.java new file mode 100644 index 00000000..7ad66418 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/SocialUserRepository.java @@ -0,0 +1,10 @@ +package vook.server.api.app.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import vook.server.api.model.user.SocialUser; + +import java.util.Optional; + +public interface SocialUserRepository extends JpaRepository { + Optional findByProviderAndProviderUserId(String provider, String providerUserId); +} diff --git a/api/src/main/java/vook/server/api/app/user/UserRepository.java b/api/src/main/java/vook/server/api/app/user/UserRepository.java new file mode 100644 index 00000000..a23c8935 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/UserRepository.java @@ -0,0 +1,10 @@ +package vook.server.api.app.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import vook.server.api.model.user.User; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java new file mode 100644 index 00000000..3ba45c0e --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -0,0 +1,31 @@ +package vook.server.api.app.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import vook.server.api.model.user.SocialUser; +import vook.server.api.model.user.User; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository repository; + private final SocialUserRepository socialUserRepository; + + public Optional findByProvider(String provider, String providerUserId) { + return socialUserRepository.findByProviderAndProviderUserId(provider, providerUserId); + } + + public SocialUser signUpFromSocial(SignUpFromSocialCommand command) { + User user = repository + .findByEmail(command.getEmail()) + .orElseGet(() -> repository.save(command.toNewUser())); + + SocialUser savedSocialUser = socialUserRepository.save(command.toSocialUser(user)); + user.addSocialUser(savedSocialUser); + + return savedSocialUser; + } +} diff --git a/api/src/main/java/vook/server/api/config/SecurityConfig.java b/api/src/main/java/vook/server/api/config/SecurityConfig.java new file mode 100644 index 00000000..785403a0 --- /dev/null +++ b/api/src/main/java/vook/server/api/config/SecurityConfig.java @@ -0,0 +1,95 @@ +package vook.server.api.config; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; +import org.springframework.security.web.SecurityFilterChain; +import vook.server.api.app.auth.TokenService; +import vook.server.api.config.auth.jwt.JWTFilter; +import vook.server.api.config.auth.oauth2.LoginSuccessHandler; +import vook.server.api.config.auth.oauth2.VookOAuth2UserService; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + @Value("${service.oauth2.loginFailUrl}") + private String loginFailUrl; + + private final VookOAuth2UserService oAuth2UserService; + private final LoginSuccessHandler loginSuccessHandler; + private final TokenService tokenService; + private final ClientRegistrationRepository clientRegistrationRepository; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + disableDefaultSecurity(http); + + http.authorizeHttpRequests(c -> c + .requestMatchers("/user/**").authenticated() + .anyRequest().permitAll() + ); + + http.oauth2Login(c -> c + .userInfoEndpoint(ec -> ec + .userService(oAuth2UserService) + ) + .authorizationEndpoint(ec -> ec + .authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository)) + ) + .successHandler(loginSuccessHandler) + .failureHandler((request, response, exception) -> response.sendRedirect(loginFailUrl)) + ); + + http.addFilterAfter(new JWTFilter(tokenService), OAuth2LoginAuthenticationFilter.class); + + http.exceptionHandling(e -> e + // 인증되지 않은 사용자의 요청 처리 + .authenticationEntryPoint((request, response, authException) -> response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)) + // 인가되지 않은 사용자의 요청 처리 + .accessDeniedHandler((request, response, accessDeniedException) -> response.setStatus(HttpServletResponse.SC_FORBIDDEN)) + ); + + return http.build(); + } + + private static void disableDefaultSecurity(HttpSecurity http) throws Exception { + //csrf 비활성화 + http.csrf(AbstractHttpConfigurer::disable); + + //HTTP Basic 인증 방식 비활성화 + http.httpBasic(AbstractHttpConfigurer::disable); + + //Spring Security 세션 사용 비활성화 + http.sessionManagement(c -> c + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ); + } + + /** + * OAuth2 인증 요청시 consent 확인을 강제하는 옵션 추가를 위해 사용 + */ + private static OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) { + DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + ); + authorizationRequestResolver.setAuthorizationRequestCustomizer(c -> c + .additionalParameters(params -> { + params.put("prompt", "consent"); + })); + return authorizationRequestResolver; + } +} diff --git a/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java b/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java new file mode 100644 index 00000000..9e7a54fc --- /dev/null +++ b/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java @@ -0,0 +1,67 @@ +package vook.server.api.config.auth.common; + +import lombok.Getter; +import lombok.ToString; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; +import vook.server.api.helper.jwt.JWTReader; +import vook.server.api.helper.jwt.JWTWriter; +import vook.server.api.model.user.SocialUser; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Getter +@ToString +public class VookLoginUser implements OAuth2User { + + private String uid; + + public static VookLoginUser of( + String uid + ) { + VookLoginUser user = new VookLoginUser(); + user.uid = uid; + return user; + } + + public static VookLoginUser from(SocialUser user) { + return VookLoginUser.of(user.getUser().getUid()); + } + + public static VookLoginUser from(JWTReader jwtReader) { + return VookLoginUser.of(jwtReader.getClaim("uid")); + } + + @Override + public Map getAttributes() { + return Map.of(); + } + + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public String getName() { + return this.uid; + } + + public String toAccessToken(JWTWriter jwtWriter, Integer accessTokenExpiredMinute) { + return jwtWriter + .withExpiredMs(1000L * 60 * accessTokenExpiredMinute) + .withClaim("category", "access") + .withClaim("uid", this.uid) + .jwtString(); + } + + public String toRefreshToken(JWTWriter jwtWriter, Integer refreshTokenExpiredMinute) { + return jwtWriter + .withExpiredMs(1000L * 60 * refreshTokenExpiredMinute) + .withClaim("category", "refresh") + .withClaim("uid", this.uid) + .jwtString(); + } +} diff --git a/api/src/main/java/vook/server/api/config/auth/jwt/JWTFilter.java b/api/src/main/java/vook/server/api/config/auth/jwt/JWTFilter.java new file mode 100644 index 00000000..131484db --- /dev/null +++ b/api/src/main/java/vook/server/api/config/auth/jwt/JWTFilter.java @@ -0,0 +1,65 @@ +package vook.server.api.config.auth.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import vook.server.api.app.auth.TokenService; +import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.model.values.AuthValues; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class JWTFilter extends OncePerRequestFilter { + + private final TokenService tokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String token = getAccessToken(request); + if (token == null) { + filterChain.doFilter(request, response); + return; + } + + OAuth2User oAuth2User; + try { + oAuth2User = VookLoginUser.of(tokenService.validateAndGetUid(token)); + } catch (Exception e) { + log.error("JWT validation failed", e); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + //스프링 시큐리티 인증 토큰 생성 + UsernamePasswordAuthenticationToken authToken = UsernamePasswordAuthenticationToken.authenticated(oAuth2User, null, oAuth2User.getAuthorities()); + + //세션에 사용자 등록 + SecurityContextHolder.getContext().setAuthentication(authToken); + + filterChain.doFilter(request, response); + } + + private String getAccessToken(HttpServletRequest request) { + String AuthorizationHeaderValue = request.getHeader(AuthValues.AUTHORIZATION_HEADER); + if (AuthorizationHeaderValue == null) { + return null; + } + + String token = AuthorizationHeaderValue.replaceFirst("Bearer", "").trim(); + if (!StringUtils.hasText(token)) { + return null; + } + + return token; + } +} diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java b/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java new file mode 100644 index 00000000..c1cc5f36 --- /dev/null +++ b/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java @@ -0,0 +1,40 @@ +package vook.server.api.config.auth.oauth2; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; +import vook.server.api.app.auth.GeneratedToken; +import vook.server.api.app.auth.TokenService; +import vook.server.api.config.auth.common.VookLoginUser; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class LoginSuccessHandler implements AuthenticationSuccessHandler { + + @Value("${service.oauth2.tokenNoticeUrl}") + private String tokenNoticeUrl; + + private final TokenService tokenService; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + VookLoginUser oAuth2User = (VookLoginUser) authentication.getPrincipal(); + GeneratedToken token = tokenService.generateToken(oAuth2User.getUid()); + response.sendRedirect(buildRedirectUrl(token)); + } + + private String buildRedirectUrl(GeneratedToken token) { + return UriComponentsBuilder.fromUriString(tokenNoticeUrl) + .queryParam("access", token.getAccessToken()) + .queryParam("refresh", token.getRefreshToken()) + .build() + .toUriString(); + } +} diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2GoogleResponse.java b/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2GoogleResponse.java new file mode 100644 index 00000000..a358f27d --- /dev/null +++ b/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2GoogleResponse.java @@ -0,0 +1,27 @@ +package vook.server.api.config.auth.oauth2; + +import java.util.Map; + +public class OAuth2GoogleResponse implements OAuth2Response { + + private final Map attribute; + + public OAuth2GoogleResponse(Map attribute) { + this.attribute = attribute; + } + + @Override + public String getProvider() { + return "google"; + } + + @Override + public String getProviderId() { + return attribute.get("sub").toString(); + } + + @Override + public String getEmail() { + return attribute.get("email").toString(); + } +} diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2Response.java b/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2Response.java new file mode 100644 index 00000000..aceee21b --- /dev/null +++ b/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2Response.java @@ -0,0 +1,12 @@ +package vook.server.api.config.auth.oauth2; + +public interface OAuth2Response { + //제공자 (Ex. naver, google, ...) + String getProvider(); + + //제공자에서 발급해주는 아이디(번호) + String getProviderId(); + + //이메일 + String getEmail(); +} diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java b/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java new file mode 100644 index 00000000..2d025d9f --- /dev/null +++ b/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java @@ -0,0 +1,57 @@ +package vook.server.api.config.auth.oauth2; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.user.SignUpFromSocialCommand; +import vook.server.api.app.user.UserService; +import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.model.user.SocialUser; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class VookOAuth2UserService extends DefaultOAuth2UserService { + + private final UserService userService; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + log.info("OAuth2User: {}", oAuth2User); + + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + OAuth2Response oAuth2Response = toOAuth2Response(registrationId, oAuth2User); + if (oAuth2Response == null) { + log.info("Unsupported registrationId: {}", registrationId); + return null; + } + + return userService.findByProvider(oAuth2Response.getProvider(), oAuth2Response.getProviderId()) + .map(VookLoginUser::from) + .orElseGet(() -> signUpUser(oAuth2Response)); + } + + private static OAuth2Response toOAuth2Response(String registrationId, OAuth2User oAuth2User) { + return switch (registrationId) { + case "google" -> new OAuth2GoogleResponse(oAuth2User.getAttributes()); + default -> null; + }; + } + + private VookLoginUser signUpUser(OAuth2Response oAuth2Response) { + SignUpFromSocialCommand command = SignUpFromSocialCommand.of( + oAuth2Response.getProvider(), + oAuth2Response.getProviderId(), + oAuth2Response.getEmail() + ); + SocialUser saved = userService.signUpFromSocial(command); + return VookLoginUser.from(saved); + } +} diff --git a/api/src/main/java/vook/server/api/helper/jwt/JWTHelper.java b/api/src/main/java/vook/server/api/helper/jwt/JWTHelper.java new file mode 100644 index 00000000..ba9f7150 --- /dev/null +++ b/api/src/main/java/vook/server/api/helper/jwt/JWTHelper.java @@ -0,0 +1,16 @@ +package vook.server.api.helper.jwt; + +public abstract class JWTHelper { + protected static T run(CheckedSupplier supplier) { + try { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @FunctionalInterface + protected interface CheckedSupplier { + T get() throws Exception; + } +} diff --git a/api/src/main/java/vook/server/api/helper/jwt/JWTHelperProvider.java b/api/src/main/java/vook/server/api/helper/jwt/JWTHelperProvider.java new file mode 100644 index 00000000..60e88e48 --- /dev/null +++ b/api/src/main/java/vook/server/api/helper/jwt/JWTHelperProvider.java @@ -0,0 +1,29 @@ +package vook.server.api.helper.jwt; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JWTHelperProvider { + + @Value("${service.jwt.secret}") + private String jwtSecret; + + private JWTWriter.Builder jwtWriterBuilder; + private JWTReader.Builder jwtReaderBuilder; + + @PostConstruct + public void init() { + jwtWriterBuilder = new JWTWriter.Builder(jwtSecret); + jwtReaderBuilder = new JWTReader.Builder(jwtSecret); + } + + public JWTWriter builder() { + return jwtWriterBuilder.build(); + } + + public JWTReader reader(String token) { + return jwtReaderBuilder.build(token); + } +} diff --git a/api/src/main/java/vook/server/api/helper/jwt/JWTReader.java b/api/src/main/java/vook/server/api/helper/jwt/JWTReader.java new file mode 100644 index 00000000..d742607e --- /dev/null +++ b/api/src/main/java/vook/server/api/helper/jwt/JWTReader.java @@ -0,0 +1,56 @@ +package vook.server.api.helper.jwt; + +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.MACVerifier; +import com.nimbusds.jwt.SignedJWT; + +import java.nio.charset.StandardCharsets; +import java.util.Date; + +public class JWTReader extends JWTHelper { + + private JWSVerifier verifier; + private SignedJWT signedJWT; + + static JWTReader of(String secret, String token) { + return run(() -> { + JWTReader reader = new JWTReader(); + byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); + reader.verifier = new MACVerifier(secretBytes); + reader.signedJWT = SignedJWT.parse(token); + return reader; + }); + } + + public void validate() { + run(() -> { + if (!signedJWT.verify(verifier)) { + throw new IllegalArgumentException("JWT의 서명이 올바르지 않습니다."); + } + + Date expirationTime = signedJWT.getJWTClaimsSet().getExpirationTime(); + if (expirationTime.before(new Date())) { + throw new IllegalArgumentException("JWT가 만료되었습니다."); + } + + return null; + }); + } + + public String getClaim(String claimName) { + return run(() -> signedJWT.getJWTClaimsSet().getStringClaim(claimName)); + } + + public static class Builder { + + private final String secret; + + public Builder(String jwtSecret) { + this.secret = jwtSecret; + } + + public JWTReader build(String token) { + return JWTReader.of(secret, token); + } + } +} diff --git a/api/src/main/java/vook/server/api/helper/jwt/JWTWriter.java b/api/src/main/java/vook/server/api/helper/jwt/JWTWriter.java new file mode 100644 index 00000000..7ffe22c3 --- /dev/null +++ b/api/src/main/java/vook/server/api/helper/jwt/JWTWriter.java @@ -0,0 +1,76 @@ +package vook.server.api.helper.jwt; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JWTWriter extends JWTHelper { + + private static final Long DEFAULT_EXPIRED_MS = 1000L * 60 * 30; // 30 minutes + + private JWSSigner signer; + private Long expiredMs; + private final Map claims = new HashMap<>(); + + static JWTWriter of(String secret) { + return run(() -> { + JWTWriter writer = new JWTWriter(); + byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); + writer.signer = new MACSigner(secretBytes); + return writer; + }); + } + + public JWTWriter withExpiredMs(Long expiredMs) { + this.expiredMs = expiredMs; + return this; + } + + public JWTWriter withClaim(String name, String value) { + claims.put(name, value); + return this; + } + + public String jwtString() { + return run(() -> { + JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder() + .issueTime(new Date()) + .expirationTime(new Date(System.currentTimeMillis() + (expiredMs != null ? expiredMs : DEFAULT_EXPIRED_MS))); + claims.forEach(builder::claim); + JWTClaimsSet claimsSet = builder.build(); + + SignedJWT signedJWT = new SignedJWT( + new JWSHeader(JWSAlgorithm.HS256), + claimsSet + ); + + signedJWT.sign(signer); + + return signedJWT.serialize(); + }); + } + + public static class Builder { + + private final String secret; + + public Builder(String jwtSecret) { + this.secret = jwtSecret; + } + + public JWTWriter build() { + return JWTWriter.of(secret); + } + } +} diff --git a/api/src/main/java/vook/server/api/model/user/SocialUser.java b/api/src/main/java/vook/server/api/model/user/SocialUser.java new file mode 100644 index 00000000..cf272d66 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/user/SocialUser.java @@ -0,0 +1,34 @@ +package vook.server.api.model.user; + +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "social_user") +public class SocialUser { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String provider; + + private String providerUserId; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + public static SocialUser forNewOf( + String provider, + String providerUserId, + User user + ) { + SocialUser socialUser = new SocialUser(); + socialUser.provider = provider; + socialUser.providerUserId = providerUserId; + socialUser.user = user; + return socialUser; + } +} diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java new file mode 100644 index 00000000..893273e7 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -0,0 +1,48 @@ +package vook.server.api.model.user; + +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Getter +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String uid; + + private String email; + + @Enumerated(EnumType.STRING) + private UserStatus status; + + private LocalDateTime deletedAt; + + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + private List socialUsers = new ArrayList<>(); + + @OneToOne(mappedBy = "user") + private UserInfo userInfo; + + public static User forSignUpFromSocialOf( + String email + ) { + User user = new User(); + user.uid = UUID.randomUUID().toString(); + user.email = email; + user.status = UserStatus.SOCIAL_LOGIN_COMPLETED; + return user; + } + + public void addSocialUser(SocialUser socialUser) { + socialUsers.add(socialUser); + } +} diff --git a/api/src/main/java/vook/server/api/model/user/UserInfo.java b/api/src/main/java/vook/server/api/model/user/UserInfo.java new file mode 100644 index 00000000..0c3228e6 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/user/UserInfo.java @@ -0,0 +1,27 @@ +package vook.server.api.model.user; + +import jakarta.persistence.*; + +@Entity +@Table(name = "user_info") +public class UserInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String nickname; + + @OneToOne + @JoinColumn(name = "user_id") + private User user; + + public static UserInfo forRegisterOf( + String nickname, + User user + ) { + UserInfo result = new UserInfo(); + result.nickname = nickname; + return result; + } +} diff --git a/api/src/main/java/vook/server/api/model/user/UserStatus.java b/api/src/main/java/vook/server/api/model/user/UserStatus.java new file mode 100644 index 00000000..2ca26d2b --- /dev/null +++ b/api/src/main/java/vook/server/api/model/user/UserStatus.java @@ -0,0 +1,8 @@ +package vook.server.api.model.user; + +public enum UserStatus { + SOCIAL_LOGIN_COMPLETED, // 소셜로그인 완료됨 + REGISTERED, // 가입 됨 + ONBOARDING_COMPLETED, // 온보딩 완료 됨 + WITHDRAWN // 탈퇴 됨 +} diff --git a/api/src/main/java/vook/server/api/model/values/AuthValues.java b/api/src/main/java/vook/server/api/model/values/AuthValues.java new file mode 100644 index 00000000..907f5f37 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/values/AuthValues.java @@ -0,0 +1,6 @@ +package vook.server.api.model.values; + +public class AuthValues { + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String REFRESH_AUTHORIZATION_HEADER = "X-Refresh-Authorization"; +} diff --git a/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java b/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java new file mode 100644 index 00000000..e4deb62e --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java @@ -0,0 +1,30 @@ +package vook.server.api.web.routes.auth; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseEntity; +import vook.server.api.model.values.AuthValues; + +@Tag(name = "auth", description = "인증 관련 API") +public interface AuthApi { + + @Operation( + summary = "토큰 갱신", + description = "리프레시 토큰을 이용하여 엑세스 토큰과 리프레시 토큰을 갱신합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공", + headers = { + @Header(name = AuthValues.AUTHORIZATION_HEADER, description = "갱신된 엑세스 토큰"), + @Header(name = AuthValues.REFRESH_AUTHORIZATION_HEADER, description = "갱신된 리프레시 토큰") + } + ), + }) + ResponseEntity refreshToken(String refresh, HttpServletResponse response); +} diff --git a/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java b/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java new file mode 100644 index 00000000..e27e5531 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java @@ -0,0 +1,32 @@ +package vook.server.api.web.routes.auth; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import vook.server.api.app.auth.GeneratedToken; +import vook.server.api.app.auth.TokenService; +import vook.server.api.model.values.AuthValues; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthRestController implements AuthApi { + + private final TokenService tokenService; + + @Override + @GetMapping("/refresh") + public ResponseEntity refreshToken( + @RequestHeader(AuthValues.REFRESH_AUTHORIZATION_HEADER) String refresh, + HttpServletResponse response + ) { + GeneratedToken token = tokenService.refreshToken(refresh); + response.setHeader(AuthValues.AUTHORIZATION_HEADER, token.getAccessToken()); + response.setHeader(AuthValues.REFRESH_AUTHORIZATION_HEADER, token.getRefreshToken()); + return ResponseEntity.ok().build(); + } +} diff --git a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java index 83daa753..a9b23618 100644 --- a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java +++ b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java @@ -1,8 +1,32 @@ package vook.server.api.web.swagger; import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.security.SecuritySchemes; import io.swagger.v3.oas.annotations.servers.Server; +import org.springframework.context.annotation.Configuration; +import vook.server.api.model.values.AuthValues; -@OpenAPIDefinition(servers = {@Server(url = "/", description = "Default Server URL")}) +@OpenAPIDefinition( + info = @Info(title = "Vook Server API", version = "0.1", description = "Vook Server API"), + servers = {@Server(url = "/", description = "Default Server URL")}, + security = { + @SecurityRequirement(name = "AccessToken") + } +) +@SecuritySchemes({ + @SecurityScheme( + name = "AccessToken", + description = "JWT 인증 토큰", + type = SecuritySchemeType.APIKEY, + in = SecuritySchemeIn.HEADER, + paramName = AuthValues.AUTHORIZATION_HEADER + ) +}) +@Configuration public class OpenApiDefinition { } diff --git a/api/src/main/resources/application-local.yml b/api/src/main/resources/application-local.yml index c6e0d2fb..585acaf5 100644 --- a/api/src/main/resources/application-local.yml +++ b/api/src/main/resources/application-local.yml @@ -12,9 +12,29 @@ spring: hibernate: highlight_sql: false format_sql: true + security: + oauth2: + client: + registration: + google: + client-name: google + client-id: 888756350502-376ig1il0g9mdb8vbs1nj8u291lh4hl3.apps.googleusercontent.com + client-secret: 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 + redirect-uri: http://localhost:8080/login/oauth2/code/google + authorization-grant-type: authorization_code + scope: + - profile + - email server: port: 8080 service: meilisearch: host: http://localhost:7700 apiKey: aSampleMasterKey + jwt: + secret: vmfhaltmskdlstkfkdgodyroqkfwkdba + oauth2: + tokenNoticeUrl: http://localhost:3000/auth/token + loginFailUrl: http://localhost:3000/auth?error + accessTokenExpiredMinute: 30 # 30 minutes + refreshTokenExpiredMinute: 10080 # 60 * 24 * 7 == 1 week From 2a46468b7d8828b1a05e2639a1e06d8c381206f8 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 11:26:45 +0900 Subject: [PATCH 065/273] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20API=20=EC=B6=94=EA=B0=80=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/app/user/UserRepository.java | 2 ++ .../vook/server/api/app/user/UserService.java | 4 +++ .../vook/server/api/model/user/UserInfo.java | 2 ++ .../server/api/web/routes/user/UserApi.java | 29 +++++++++++++++++++ .../api/web/routes/user/UserInfoResponse.java | 27 +++++++++++++++++ .../web/routes/user/UserRestController.java | 27 +++++++++++++++++ .../api/web/routes/user/UserWebService.java | 21 ++++++++++++++ 7 files changed, 112 insertions(+) create mode 100644 api/src/main/java/vook/server/api/web/routes/user/UserApi.java create mode 100644 api/src/main/java/vook/server/api/web/routes/user/UserInfoResponse.java create mode 100644 api/src/main/java/vook/server/api/web/routes/user/UserRestController.java create mode 100644 api/src/main/java/vook/server/api/web/routes/user/UserWebService.java diff --git a/api/src/main/java/vook/server/api/app/user/UserRepository.java b/api/src/main/java/vook/server/api/app/user/UserRepository.java index a23c8935..e926cafc 100644 --- a/api/src/main/java/vook/server/api/app/user/UserRepository.java +++ b/api/src/main/java/vook/server/api/app/user/UserRepository.java @@ -7,4 +7,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + + Optional findByUid(String uid); } diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 3ba45c0e..5260cda6 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -28,4 +28,8 @@ public SocialUser signUpFromSocial(SignUpFromSocialCommand command) { return savedSocialUser; } + + public Optional findByUid(String uid) { + return repository.findByUid(uid); + } } diff --git a/api/src/main/java/vook/server/api/model/user/UserInfo.java b/api/src/main/java/vook/server/api/model/user/UserInfo.java index 0c3228e6..ea2305fe 100644 --- a/api/src/main/java/vook/server/api/model/user/UserInfo.java +++ b/api/src/main/java/vook/server/api/model/user/UserInfo.java @@ -1,7 +1,9 @@ package vook.server.api.model.user; import jakarta.persistence.*; +import lombok.Getter; +@Getter @Entity @Table(name = "user_info") public class UserInfo { diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java new file mode 100644 index 00000000..59c64cb0 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -0,0 +1,29 @@ +package vook.server.api.web.routes.user; + +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.web.common.CommonApiResponse; + +@Tag(name = "user", description = "사용자 관련 API") +public interface UserApi { + + @Operation(summary = "사용자 정보") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + schema = @Schema(implementation = UserApiUerInfoResponse.class) + ) + ), + }) + CommonApiResponse userInfo(VookLoginUser user); + + class UserApiUerInfoResponse extends CommonApiResponse { + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserInfoResponse.java b/api/src/main/java/vook/server/api/web/routes/user/UserInfoResponse.java new file mode 100644 index 00000000..ba97fe9e --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/UserInfoResponse.java @@ -0,0 +1,27 @@ +package vook.server.api.web.routes.user; + +import lombok.Getter; +import vook.server.api.model.user.User; +import vook.server.api.model.user.UserInfo; +import vook.server.api.model.user.UserStatus; + +@Getter +public class UserInfoResponse { + + private String uid; + private String email; + private UserStatus status; + private String nickname; + + public static UserInfoResponse from(User user) { + UserInfoResponse result = new UserInfoResponse(); + result.uid = user.getUid(); + result.email = user.getEmail(); + result.status = user.getStatus(); + UserInfo userInfo = user.getUserInfo(); + if (userInfo != null) { + result.nickname = userInfo.getNickname(); + } + return result; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java new file mode 100644 index 00000000..e821fef0 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -0,0 +1,27 @@ +package vook.server.api.web.routes.user; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.web.common.CommonApiResponse; + +@Slf4j +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class UserRestController implements UserApi { + + private final UserWebService service; + + @GetMapping("/info") + public CommonApiResponse userInfo( + @AuthenticationPrincipal VookLoginUser user + ) { + UserInfoResponse response = service.userInfo(user); + return CommonApiResponse.okWithResult(response); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java new file mode 100644 index 00000000..efbae88c --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -0,0 +1,21 @@ +package vook.server.api.web.routes.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.user.UserService; +import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.model.user.User; + +@Service +@Transactional +@RequiredArgsConstructor +public class UserWebService { + + private final UserService service; + + public UserInfoResponse userInfo(VookLoginUser loginUser) { + User user = service.findByUid(loginUser.getUid()).orElseThrow(); + return UserInfoResponse.from(user); + } +} From 6c7718a8c2c850c0ba2bf1b6bb71f378e92e1f99 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 13:59:08 +0900 Subject: [PATCH 066/273] =?UTF-8?q?docs:=20=ED=86=A0=ED=81=B0=20=ED=9A=8D?= =?UTF-8?q?=EB=93=9D=20=EB=B0=A9=EB=B2=95=20=EB=AC=B8=EC=84=9C=ED=99=94=20?= =?UTF-8?q?#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/vook/server/api/web/routes/auth/AuthApi.java | 4 +++- .../server/api/web/swagger/OpenApiDefinition.java | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java b/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java index e4deb62e..239560ce 100644 --- a/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java +++ b/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java @@ -14,7 +14,9 @@ public interface AuthApi { @Operation( summary = "토큰 갱신", - description = "리프레시 토큰을 이용하여 엑세스 토큰과 리프레시 토큰을 갱신합니다." + description = """ + 리프레시 토큰을 이용하여 엑세스 토큰과 리프레시 토큰을 갱신합니다. + 리프레시 토큰은 최상위 Description에 Authorzation 항목을 참고하세요.""" ) @ApiResponses(value = { @ApiResponse( diff --git a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java index a9b23618..366e84cb 100644 --- a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java +++ b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java @@ -12,7 +12,16 @@ import vook.server.api.model.values.AuthValues; @OpenAPIDefinition( - info = @Info(title = "Vook Server API", version = "0.1", description = "Vook Server API"), + info = @Info( + title = "Vook Server API", + version = "0.1", + description = """ + Vook 서버 API 문서입니다. + + ## Authorization (토큰 획득 방법) + + - [구글 로그인](/oauth2/authorization/google)을 통해 로그인 한 후, + redirect URL에 포함된 accessToken을 사용합니다. refreshToken은 토큰 갱신 때 사용합니다."""), servers = {@Server(url = "/", description = "Default Server URL")}, security = { @SecurityRequirement(name = "AccessToken") From b6e344c6761d407cc82c21368aecbf59ae524e3e Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 14:02:45 +0900 Subject: [PATCH 067/273] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/devhelper/InitService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 7ea4b9ea..6974159d 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -6,6 +6,8 @@ import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.demo.DemoTermRepository; import vook.server.api.app.demo.DemoTermSynonymRepository; +import vook.server.api.app.user.SocialUserRepository; +import vook.server.api.app.user.UserRepository; import vook.server.api.model.demo.DemoTerm; import vook.server.api.outbound.search.DemoTermSearchService; @@ -24,10 +26,14 @@ public class InitService { private final DemoTermRepository demoTermRepository; private final DemoTermSynonymRepository demoTermSynonymRepository; private final DemoTermSearchService searchService; + private final SocialUserRepository socialUserRepository; + private final UserRepository userRepository; public void init() { demoTermSynonymRepository.deleteAllInBatch(); demoTermRepository.deleteAllInBatch(); + socialUserRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); searchService.clearAll(); List devTerms = getTerms("classpath:init/개발.tsv"); From 534a76fc24075d9e7d5ba499160a5df7128d570c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 14:04:48 +0900 Subject: [PATCH 068/273] =?UTF-8?q?docs:=20=EC=B4=88=EA=B8=B0=ED=99=94=20A?= =?UTF-8?q?PI=20=EC=84=A4=EB=AA=85=20=EC=83=81=EC=84=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/web/routes/init/InitApi.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/vook/server/api/web/routes/init/InitApi.java b/api/src/main/java/vook/server/api/web/routes/init/InitApi.java index 2ae0ebe2..0f819ae1 100644 --- a/api/src/main/java/vook/server/api/web/routes/init/InitApi.java +++ b/api/src/main/java/vook/server/api/web/routes/init/InitApi.java @@ -8,7 +8,10 @@ @Tag(name = "init", description = "초기화 API") public interface InitApi { - @Operation(summary = "DB, Meilisearch 데이터 초기화") + @Operation( + summary = "데이터 초기화", + description = "모든 데이터를 삭제하고, 데모용 데이터를 생성시킨 상태로 초기화 시킵니다." + ) @ApiResponses(value = { @ApiResponse( responseCode = "200", From ab42ace06bf888b7122bc7bfe0ab5a6001d20bca Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 14:24:18 +0900 Subject: [PATCH 069/273] =?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=EB=AC=B8=EC=A0=9C=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/test/resources/application.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/api/src/test/resources/application.yml b/api/src/test/resources/application.yml index 1648a9e0..fb7a546f 100644 --- a/api/src/test/resources/application.yml +++ b/api/src/test/resources/application.yml @@ -1,3 +1,27 @@ spring: datasource: driver-class-name: org.h2.Driver + security: + oauth2: + client: + registration: + google: + client-name: google + client-id: 888756350502-376ig1il0g9mdb8vbs1nj8u291lh4hl3.apps.googleusercontent.com + client-secret: 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 + redirect-uri: http://localhost:8080/login/oauth2/code/google + authorization-grant-type: authorization_code + scope: + - profile + - email +service: + meilisearch: + host: http://localhost:7700 + apiKey: aSampleMasterKey + jwt: + secret: vmfhaltmskdlstkfkdgodyroqkfwkdba + oauth2: + tokenNoticeUrl: http://localhost:3000/auth/token + loginFailUrl: http://localhost:3000/auth?error + accessTokenExpiredMinute: 30 # 30 minutes + refreshTokenExpiredMinute: 10080 # 60 * 24 * 7 == 1 week From 805efb9d6865e734ca4011d2203cad36aa83f108 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 15:17:05 +0900 Subject: [PATCH 070/273] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20API=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/app/terms/TermsRepository.java | 7 ++++ .../server/api/app/terms/TermsService.java | 18 ++++++++++ .../server/api/devhelper/InitService.java | 19 ++++++++++- .../vook/server/api/model/terms/Terms.java | 33 +++++++++++++++++++ .../server/api/web/routes/user/UserApi.java | 19 ++++++++++- .../web/routes/user/UserRestController.java | 9 +++++ .../web/routes/user/UserTermsResponse.java | 30 +++++++++++++++++ .../api/web/routes/user/UserWebService.java | 14 ++++++-- ...\354\232\251_\354\225\275\352\264\200.txt" | 1 + ...\354\213\240_\354\225\275\352\264\200.txt" | 1 + ...4\354\232\251\354\225\275\352\264\200.txt" | 1 + 11 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/terms/TermsRepository.java create mode 100644 api/src/main/java/vook/server/api/app/terms/TermsService.java create mode 100644 api/src/main/java/vook/server/api/model/terms/Terms.java create mode 100644 api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java create mode 100644 "api/src/main/resources/init/\352\260\234\354\235\270\354\240\225\353\263\264_\354\210\230\354\247\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" create mode 100644 "api/src/main/resources/init/\353\247\210\354\274\200\355\214\205_\353\251\224\354\235\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" create mode 100644 "api/src/main/resources/init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" diff --git a/api/src/main/java/vook/server/api/app/terms/TermsRepository.java b/api/src/main/java/vook/server/api/app/terms/TermsRepository.java new file mode 100644 index 00000000..6f5faa31 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/terms/TermsRepository.java @@ -0,0 +1,7 @@ +package vook.server.api.app.terms; + +import org.springframework.data.jpa.repository.JpaRepository; +import vook.server.api.model.terms.Terms; + +public interface TermsRepository extends JpaRepository { +} diff --git a/api/src/main/java/vook/server/api/app/terms/TermsService.java b/api/src/main/java/vook/server/api/app/terms/TermsService.java new file mode 100644 index 00000000..fa59acca --- /dev/null +++ b/api/src/main/java/vook/server/api/app/terms/TermsService.java @@ -0,0 +1,18 @@ +package vook.server.api.app.terms; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import vook.server.api.model.terms.Terms; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TermsService { + + private final TermsRepository repository; + + public List findAll() { + return repository.findAll(); + } +} diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 6974159d..4b019b0d 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -6,9 +6,11 @@ import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.demo.DemoTermRepository; import vook.server.api.app.demo.DemoTermSynonymRepository; +import vook.server.api.app.terms.TermsRepository; import vook.server.api.app.user.SocialUserRepository; import vook.server.api.app.user.UserRepository; import vook.server.api.model.demo.DemoTerm; +import vook.server.api.model.terms.Terms; import vook.server.api.outbound.search.DemoTermSearchService; import java.io.IOException; @@ -25,17 +27,23 @@ public class InitService { private final DemoTermRepository demoTermRepository; private final DemoTermSynonymRepository demoTermSynonymRepository; - private final DemoTermSearchService searchService; private final SocialUserRepository socialUserRepository; private final UserRepository userRepository; + private final TermsRepository termsRepository; + private final DemoTermSearchService searchService; public void init() { demoTermSynonymRepository.deleteAllInBatch(); demoTermRepository.deleteAllInBatch(); socialUserRepository.deleteAllInBatch(); userRepository.deleteAllInBatch(); + termsRepository.deleteAllInBatch(); searchService.clearAll(); + termsRepository.save(Terms.of("이용약관", loadContents("classpath:init/이용약관.txt"), 1L)); + termsRepository.save(Terms.of("개인정보 수집 이용 약관", loadContents("classpath:init/개인정보_수집_이용_약관.txt"), 1L)); + termsRepository.save(Terms.of("마케팅 메일 수신 약관", loadContents("classpath:init/마케팅_메일_수신_약관.txt"), 1L)); + List devTerms = getTerms("classpath:init/개발.tsv"); demoTermRepository.saveAll(devTerms); @@ -43,6 +51,15 @@ public void init() { searchService.addTerms(devTerms); } + private String loadContents(String location) { + try { + InputStream inputStream = resourceLoader.getResource(location).getInputStream(); + return new String(inputStream.readAllBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private List getTerms(String location) { try { // file로 바로 접근 할 경우, IDE에서는 접근 가능하나, jar로 패키징 후 실행 시에는 접근 불가능 diff --git a/api/src/main/java/vook/server/api/model/terms/Terms.java b/api/src/main/java/vook/server/api/model/terms/Terms.java new file mode 100644 index 00000000..bdeb4578 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/terms/Terms.java @@ -0,0 +1,33 @@ +package vook.server.api.model.terms; + +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "terms") +public class Terms { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @Column(columnDefinition = "TEXT") + private String content; + + private Long version; + + public static Terms of( + String title, + String content, + Long version + ) { + Terms result = new Terms(); + result.title = title; + result.content = content; + result.version = version; + return result; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 59c64cb0..b7b60acc 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -9,7 +9,9 @@ import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; -@Tag(name = "user", description = "사용자 관련 API") +import java.util.List; + +@Tag(name = "user", description = "사용자 관련 API **(인증 필요!)**") public interface UserApi { @Operation(summary = "사용자 정보") @@ -26,4 +28,19 @@ public interface UserApi { class UserApiUerInfoResponse extends CommonApiResponse { } + + @Operation(summary = "약관 목록") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + schema = @Schema(implementation = UserApiTermsResponse.class) + ) + ), + }) + CommonApiResponse> terms(); + + class UserApiTermsResponse extends CommonApiResponse> { + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index e821fef0..28df98c8 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -9,6 +9,8 @@ import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; +import java.util.List; + @Slf4j @RestController @RequestMapping("/user") @@ -17,6 +19,7 @@ public class UserRestController implements UserApi { private final UserWebService service; + @Override @GetMapping("/info") public CommonApiResponse userInfo( @AuthenticationPrincipal VookLoginUser user @@ -24,4 +27,10 @@ public CommonApiResponse userInfo( UserInfoResponse response = service.userInfo(user); return CommonApiResponse.okWithResult(response); } + + @Override + @GetMapping("/terms") + public CommonApiResponse> terms() { + return CommonApiResponse.okWithResult(service.terms()); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java new file mode 100644 index 00000000..ad80d913 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java @@ -0,0 +1,30 @@ +package vook.server.api.web.routes.user; + +import lombok.Getter; +import vook.server.api.model.terms.Terms; + +import java.util.List; + +@Getter +public class UserTermsResponse { + + private Long id; + private String title; + private String content; + private Long version; + + public static List from(List terms) { + return terms.stream() + .map(UserTermsResponse::from) + .toList(); + } + + public static UserTermsResponse from(Terms terms) { + UserTermsResponse response = new UserTermsResponse(); + response.id = terms.getId(); + response.title = terms.getTitle(); + response.content = terms.getContent(); + response.version = terms.getVersion(); + return response; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index efbae88c..702b6fb7 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -3,19 +3,29 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.terms.TermsService; import vook.server.api.app.user.UserService; import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.model.terms.Terms; import vook.server.api.model.user.User; +import java.util.List; + @Service @Transactional @RequiredArgsConstructor public class UserWebService { - private final UserService service; + private final UserService userService; + private final TermsService termsService; public UserInfoResponse userInfo(VookLoginUser loginUser) { - User user = service.findByUid(loginUser.getUid()).orElseThrow(); + User user = userService.findByUid(loginUser.getUid()).orElseThrow(); return UserInfoResponse.from(user); } + + public List terms() { + List terms = termsService.findAll(); + return UserTermsResponse.from(terms); + } } diff --git "a/api/src/main/resources/init/\352\260\234\354\235\270\354\240\225\353\263\264_\354\210\230\354\247\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" "b/api/src/main/resources/init/\352\260\234\354\235\270\354\240\225\353\263\264_\354\210\230\354\247\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" new file mode 100644 index 00000000..592165bd --- /dev/null +++ "b/api/src/main/resources/init/\352\260\234\354\235\270\354\240\225\353\263\264_\354\210\230\354\247\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" @@ -0,0 +1 @@ +개인정보 수집 이용 약관 내용 diff --git "a/api/src/main/resources/init/\353\247\210\354\274\200\355\214\205_\353\251\224\354\235\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" "b/api/src/main/resources/init/\353\247\210\354\274\200\355\214\205_\353\251\224\354\235\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" new file mode 100644 index 00000000..7422efbe --- /dev/null +++ "b/api/src/main/resources/init/\353\247\210\354\274\200\355\214\205_\353\251\224\354\235\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" @@ -0,0 +1 @@ +마케팅 메일 수신 약관 내용 diff --git "a/api/src/main/resources/init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" "b/api/src/main/resources/init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" new file mode 100644 index 00000000..a9814da0 --- /dev/null +++ "b/api/src/main/resources/init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" @@ -0,0 +1 @@ +이용약관 내용 From 166c0771a2a855f12b988914f5e0b73ee0f5454a Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 15:34:06 +0900 Subject: [PATCH 071/273] =?UTF-8?q?docs:=20Swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EC=97=90=20baseUrl=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B4=80=EB=A0=A8=20URL=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/web/swagger/OpenApiDefinition.java | 10 ++++++++-- api/src/main/resources/application-local.yml | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java index 366e84cb..479184e3 100644 --- a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java +++ b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java @@ -21,8 +21,14 @@ ## Authorization (토큰 획득 방법) - [구글 로그인](/oauth2/authorization/google)을 통해 로그인 한 후, - redirect URL에 포함된 accessToken을 사용합니다. refreshToken은 토큰 갱신 때 사용합니다."""), - servers = {@Server(url = "/", description = "Default Server URL")}, + redirect URL에 포함된 accessToken을 사용합니다. refreshToken은 토큰 갱신 때 사용합니다. + + ## URL 정보 + + - 구글 로그인: ${service.baseUrl}/oauth2/authorization/google + - 로그인 성공 콜백: ${service.oauth2.tokenNoticeUrl} + - 로그인 실패 (혹은 취소): ${service.oauth2.loginFailUrl}"""), + servers = {@Server(url = "${service.baseUrl}")}, security = { @SecurityRequirement(name = "AccessToken") } diff --git a/api/src/main/resources/application-local.yml b/api/src/main/resources/application-local.yml index 585acaf5..b63a741a 100644 --- a/api/src/main/resources/application-local.yml +++ b/api/src/main/resources/application-local.yml @@ -28,6 +28,7 @@ spring: server: port: 8080 service: + baseUrl: http://localhost:8080 meilisearch: host: http://localhost:7700 apiKey: aSampleMasterKey From d9502953edcbf49468c2172c8d73be5465622cc6 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 15:47:16 +0900 Subject: [PATCH 072/273] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=EC=97=90=20?= =?UTF-8?q?=ED=95=84=EC=88=98=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B2=84=EC=A0=84=20=EC=82=AD=EC=A0=9C=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/devhelper/InitService.java | 6 +++--- api/src/main/java/vook/server/api/model/terms/Terms.java | 6 +++--- .../vook/server/api/web/routes/user/UserTermsResponse.java | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 4b019b0d..1bc0f853 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -40,9 +40,9 @@ public void init() { termsRepository.deleteAllInBatch(); searchService.clearAll(); - termsRepository.save(Terms.of("이용약관", loadContents("classpath:init/이용약관.txt"), 1L)); - termsRepository.save(Terms.of("개인정보 수집 이용 약관", loadContents("classpath:init/개인정보_수집_이용_약관.txt"), 1L)); - termsRepository.save(Terms.of("마케팅 메일 수신 약관", loadContents("classpath:init/마케팅_메일_수신_약관.txt"), 1L)); + termsRepository.save(Terms.of("이용약관", loadContents("classpath:init/이용약관.txt"), true)); + termsRepository.save(Terms.of("개인정보 수집 이용 약관", loadContents("classpath:init/개인정보_수집_이용_약관.txt"), true)); + termsRepository.save(Terms.of("마케팅 메일 수신 약관", loadContents("classpath:init/마케팅_메일_수신_약관.txt"), false)); List devTerms = getTerms("classpath:init/개발.tsv"); demoTermRepository.saveAll(devTerms); diff --git a/api/src/main/java/vook/server/api/model/terms/Terms.java b/api/src/main/java/vook/server/api/model/terms/Terms.java index bdeb4578..ed2f3fa1 100644 --- a/api/src/main/java/vook/server/api/model/terms/Terms.java +++ b/api/src/main/java/vook/server/api/model/terms/Terms.java @@ -17,17 +17,17 @@ public class Terms { @Column(columnDefinition = "TEXT") private String content; - private Long version; + private Boolean required; public static Terms of( String title, String content, - Long version + Boolean required ) { Terms result = new Terms(); result.title = title; result.content = content; - result.version = version; + result.required = required; return result; } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java index ad80d913..989bf75f 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java @@ -11,7 +11,7 @@ public class UserTermsResponse { private Long id; private String title; private String content; - private Long version; + private Boolean required; public static List from(List terms) { return terms.stream() @@ -24,7 +24,7 @@ public static UserTermsResponse from(Terms terms) { response.id = terms.getId(); response.title = terms.getTitle(); response.content = terms.getContent(); - response.version = terms.getVersion(); + response.required = terms.getRequired(); return response; } } From eaa149fd1b2e02602e1f4fce4a12ef5f3fa8ca42 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 16:25:23 +0900 Subject: [PATCH 073/273] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EC=B4=88=EC=95=88=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/app/terms/TermsRepository.java | 3 + .../server/api/app/terms/TermsService.java | 12 ++++ .../server/api/app/user/RegisterCommand.java | 42 +++++++++++++ .../api/app/user/UserInfoRepository.java | 7 +++ .../vook/server/api/app/user/UserService.java | 18 ++++++ .../app/user/UserTermsAgreeRepository.java | 7 +++ .../java/vook/server/api/model/user/User.java | 11 ++++ .../vook/server/api/model/user/UserInfo.java | 1 + .../server/api/model/user/UserTermsAgree.java | 37 ++++++++++++ .../server/api/web/routes/user/UserApi.java | 9 +++ .../web/routes/user/UserRegisterRequest.java | 59 +++++++++++++++++++ .../web/routes/user/UserRestController.java | 14 ++++- .../api/web/routes/user/UserWebService.java | 8 +++ 13 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/user/RegisterCommand.java create mode 100644 api/src/main/java/vook/server/api/app/user/UserInfoRepository.java create mode 100644 api/src/main/java/vook/server/api/app/user/UserTermsAgreeRepository.java create mode 100644 api/src/main/java/vook/server/api/model/user/UserTermsAgree.java create mode 100644 api/src/main/java/vook/server/api/web/routes/user/UserRegisterRequest.java diff --git a/api/src/main/java/vook/server/api/app/terms/TermsRepository.java b/api/src/main/java/vook/server/api/app/terms/TermsRepository.java index 6f5faa31..a7c1e35f 100644 --- a/api/src/main/java/vook/server/api/app/terms/TermsRepository.java +++ b/api/src/main/java/vook/server/api/app/terms/TermsRepository.java @@ -3,5 +3,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.terms.Terms; +import java.util.List; + public interface TermsRepository extends JpaRepository { + List findAllByRequired(Boolean required); } diff --git a/api/src/main/java/vook/server/api/app/terms/TermsService.java b/api/src/main/java/vook/server/api/app/terms/TermsService.java index fa59acca..dd7e27fa 100644 --- a/api/src/main/java/vook/server/api/app/terms/TermsService.java +++ b/api/src/main/java/vook/server/api/app/terms/TermsService.java @@ -4,7 +4,9 @@ import org.springframework.stereotype.Service; import vook.server.api.model.terms.Terms; +import java.util.HashSet; import java.util.List; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -15,4 +17,14 @@ public class TermsService { public List findAll() { return repository.findAll(); } + + public Optional find(Long id) { + return repository.findById(id); + } + + public boolean includeAllRequiredTerms(List agreeTermsId) { + List requiredTerms = repository.findAllByRequired(true); + List requiredTermsId = requiredTerms.stream().map(Terms::getId).toList(); + return new HashSet<>(agreeTermsId).containsAll(requiredTermsId); + } } diff --git a/api/src/main/java/vook/server/api/app/user/RegisterCommand.java b/api/src/main/java/vook/server/api/app/user/RegisterCommand.java new file mode 100644 index 00000000..b347b7b0 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/RegisterCommand.java @@ -0,0 +1,42 @@ +package vook.server.api.app.user; + +import lombok.Getter; +import vook.server.api.model.terms.Terms; + +import java.util.List; + +@Getter +public class RegisterCommand { + + private String userUid; + private String nickname; + private List termsAgrees; + + public static RegisterCommand of( + String userUid, + String nickname, + List termsAgrees + ) { + RegisterCommand command = new RegisterCommand(); + command.userUid = userUid; + command.nickname = nickname; + command.termsAgrees = termsAgrees; + return command; + } + + @Getter + public static class TermsAgree { + private Terms terms; + private Boolean agree; + + public static TermsAgree of( + Terms terms, + Boolean agree + ) { + TermsAgree result = new TermsAgree(); + result.terms = terms; + result.agree = agree; + return result; + } + } +} diff --git a/api/src/main/java/vook/server/api/app/user/UserInfoRepository.java b/api/src/main/java/vook/server/api/app/user/UserInfoRepository.java new file mode 100644 index 00000000..b7b354cf --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/UserInfoRepository.java @@ -0,0 +1,7 @@ +package vook.server.api.app.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import vook.server.api.model.user.UserInfo; + +public interface UserInfoRepository extends JpaRepository { +} diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 5260cda6..3bf5ed4e 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -4,6 +4,8 @@ import org.springframework.stereotype.Service; import vook.server.api.model.user.SocialUser; import vook.server.api.model.user.User; +import vook.server.api.model.user.UserInfo; +import vook.server.api.model.user.UserTermsAgree; import java.util.Optional; @@ -13,6 +15,8 @@ public class UserService { private final UserRepository repository; private final SocialUserRepository socialUserRepository; + private final UserInfoRepository userInfoRepository; + private final UserTermsAgreeRepository userTermsAgreeRepository; public Optional findByProvider(String provider, String providerUserId) { return socialUserRepository.findByProviderAndProviderUserId(provider, providerUserId); @@ -32,4 +36,18 @@ public SocialUser signUpFromSocial(SignUpFromSocialCommand command) { public Optional findByUid(String uid) { return repository.findByUid(uid); } + + public void register(RegisterCommand command) { + User user = repository.findByUid(command.getUserUid()).orElseThrow(); + + UserInfo userInfo = UserInfo.forRegisterOf(command.getNickname(), user); + UserInfo savedUserInfo = userInfoRepository.save(userInfo); + user.addUserInfo(savedUserInfo); + + for (RegisterCommand.TermsAgree termsAgree : command.getTermsAgrees()) { + UserTermsAgree userTermsAgree = UserTermsAgree.of(user, termsAgree.getTerms(), termsAgree.getAgree()); + UserTermsAgree savedUserTermsAgree = userTermsAgreeRepository.save(userTermsAgree); + user.addUserTermsAgree(savedUserTermsAgree); + } + } } diff --git a/api/src/main/java/vook/server/api/app/user/UserTermsAgreeRepository.java b/api/src/main/java/vook/server/api/app/user/UserTermsAgreeRepository.java new file mode 100644 index 00000000..1146954e --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/UserTermsAgreeRepository.java @@ -0,0 +1,7 @@ +package vook.server.api.app.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import vook.server.api.model.user.UserTermsAgree; + +public interface UserTermsAgreeRepository extends JpaRepository { +} diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 893273e7..c959e65c 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -32,6 +32,9 @@ public class User { @OneToOne(mappedBy = "user") private UserInfo userInfo; + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + private List userTermsAgrees = new ArrayList<>(); + public static User forSignUpFromSocialOf( String email ) { @@ -45,4 +48,12 @@ public static User forSignUpFromSocialOf( public void addSocialUser(SocialUser socialUser) { socialUsers.add(socialUser); } + + public void addUserInfo(UserInfo userInfo) { + this.userInfo = userInfo; + } + + public void addUserTermsAgree(UserTermsAgree userTermsAgree) { + userTermsAgrees.add(userTermsAgree); + } } diff --git a/api/src/main/java/vook/server/api/model/user/UserInfo.java b/api/src/main/java/vook/server/api/model/user/UserInfo.java index ea2305fe..6b90e387 100644 --- a/api/src/main/java/vook/server/api/model/user/UserInfo.java +++ b/api/src/main/java/vook/server/api/model/user/UserInfo.java @@ -24,6 +24,7 @@ public static UserInfo forRegisterOf( ) { UserInfo result = new UserInfo(); result.nickname = nickname; + result.user = user; return result; } } diff --git a/api/src/main/java/vook/server/api/model/user/UserTermsAgree.java b/api/src/main/java/vook/server/api/model/user/UserTermsAgree.java new file mode 100644 index 00000000..b2ee9780 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/user/UserTermsAgree.java @@ -0,0 +1,37 @@ +package vook.server.api.model.user; + +import jakarta.persistence.*; +import lombok.Getter; +import vook.server.api.model.terms.Terms; + +@Getter +@Entity +@Table(name = "user_terms_agree") +public class UserTermsAgree { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "terms_id") + private Terms terms; + + private Boolean agree; + + public static UserTermsAgree of( + User user, + Terms terms, + Boolean agree + ) { + UserTermsAgree result = new UserTermsAgree(); + result.user = user; + result.terms = terms; + result.agree = agree; + return result; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index b7b60acc..fe7fec55 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -43,4 +43,13 @@ class UserApiUerInfoResponse extends CommonApiResponse { class UserApiTermsResponse extends CommonApiResponse> { } + + @Operation(summary = "회원가입") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공" + ), + }) + CommonApiResponse register(VookLoginUser user, UserRegisterRequest request); } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRegisterRequest.java b/api/src/main/java/vook/server/api/web/routes/user/UserRegisterRequest.java new file mode 100644 index 00000000..d62e92d5 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRegisterRequest.java @@ -0,0 +1,59 @@ +package vook.server.api.web.routes.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import vook.server.api.app.user.RegisterCommand; +import vook.server.api.model.terms.Terms; + +import java.util.List; +import java.util.Optional; + +@Data +public class UserRegisterRequest { + + @Schema + private String nickname; + + @Schema + private List termsAgrees; + + public RegisterCommand toCommand(String userUid, TermsFinder termsFinder) { + return RegisterCommand.of( + userUid, + nickname, + termsAgrees.stream().map(termsAgree -> { + Terms terms = termsFinder.find(termsAgree.id).orElseThrow(); + return RegisterCommand.TermsAgree.of(terms, termsAgree.agree); + }).toList() + ); + } + + @FunctionalInterface + public interface TermsFinder { + Optional find(Long id); + } + + @Schema(hidden = true) + public List getAgreeTermsId() { + return termsAgrees.stream() + .filter(t -> t.agree) + .map(t -> t.id) + .toList(); + } + + @Data + public static class TermsAgree { + private Long id; + private Boolean agree; + + public static TermsAgree of( + Long id, + Boolean agree + ) { + TermsAgree result = new TermsAgree(); + result.id = id; + result.agree = agree; + return result; + } + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index 28df98c8..f04c40ab 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -3,9 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; @@ -33,4 +31,14 @@ public CommonApiResponse userInfo( public CommonApiResponse> terms() { return CommonApiResponse.okWithResult(service.terms()); } + + @Override + @PostMapping("/register") + public CommonApiResponse register( + @AuthenticationPrincipal VookLoginUser user, + @RequestBody UserRegisterRequest request + ) { + service.register(user, request); + return CommonApiResponse.ok(); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 702b6fb7..0d48378f 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -28,4 +28,12 @@ public List terms() { List terms = termsService.findAll(); return UserTermsResponse.from(terms); } + + public void register(VookLoginUser loginUser, UserRegisterRequest request) { + List agreeTermsId = request.getAgreeTermsId(); + if (!termsService.includeAllRequiredTerms(agreeTermsId)) { + throw new IllegalArgumentException("동의 필수인 약관이 누락되었습니다."); + } + userService.register(request.toCommand(loginUser.getUid(), termsService::find)); + } } From 5855dc40a3c0697da98824046c3032d06b5ff906 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 16:43:42 +0900 Subject: [PATCH 074/273] =?UTF-8?q?docs:=20Swagger=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9D=B8=EC=A6=9D=EC=9D=B4=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EB=B6=80=EB=B6=84=20=EB=A7=8C=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=ED=95=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 --- .../server/api/web/routes/user/UserApi.java | 24 +++++++++++++++---- .../api/web/swagger/OpenApiDefinition.java | 6 +---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index fe7fec55..42c18a2c 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -5,16 +5,22 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; import java.util.List; -@Tag(name = "user", description = "사용자 관련 API **(인증 필요!)**") +@Tag(name = "user", description = "사용자 관련 API") public interface UserApi { - @Operation(summary = "사용자 정보") + @Operation( + summary = "사용자 정보", + security = { + @SecurityRequirement(name = "AccessToken") + } + ) @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -29,7 +35,12 @@ public interface UserApi { class UserApiUerInfoResponse extends CommonApiResponse { } - @Operation(summary = "약관 목록") + @Operation( + summary = "약관 목록", + security = { + @SecurityRequirement(name = "AccessToken") + } + ) @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -44,7 +55,12 @@ class UserApiUerInfoResponse extends CommonApiResponse { class UserApiTermsResponse extends CommonApiResponse> { } - @Operation(summary = "회원가입") + @Operation( + summary = "회원가입", + security = { + @SecurityRequirement(name = "AccessToken") + } + ) @ApiResponses(value = { @ApiResponse( responseCode = "200", diff --git a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java index 479184e3..4a808ad3 100644 --- a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java +++ b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityScheme; import io.swagger.v3.oas.annotations.security.SecuritySchemes; import io.swagger.v3.oas.annotations.servers.Server; @@ -28,10 +27,7 @@ - 구글 로그인: ${service.baseUrl}/oauth2/authorization/google - 로그인 성공 콜백: ${service.oauth2.tokenNoticeUrl} - 로그인 실패 (혹은 취소): ${service.oauth2.loginFailUrl}"""), - servers = {@Server(url = "${service.baseUrl}")}, - security = { - @SecurityRequirement(name = "AccessToken") - } + servers = {@Server(url = "${service.baseUrl}")} ) @SecuritySchemes({ @SecurityScheme( From 36fd371b2719eaa75a30643677c69183ebbcb885 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 17:36:47 +0900 Subject: [PATCH 075/273] =?UTF-8?q?chore:=20tbls=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 --- .tbls.yml | 6 ++++++ Makefile | 3 +++ 2 files changed, 9 insertions(+) create mode 100644 .tbls.yml create mode 100644 Makefile diff --git a/.tbls.yml b/.tbls.yml new file mode 100644 index 00000000..50a9c5ea --- /dev/null +++ b/.tbls.yml @@ -0,0 +1,6 @@ +dsn: mariadb://user:userPw@localhost:3307/vook + +docPath: docs/schema + +er: + format: mermaid diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..56f7150d --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +.PHONY:tbls +tbls: + tbls doc --rm-dist From babf285353c8f365822ba2588c548512b55c8b02 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 17:54:10 +0900 Subject: [PATCH 076/273] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=9B=84=20=ED=9A=8C=EC=9B=90=EC=9D=98=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B0=80=20REGISTERED=EA=B0=80=20=EB=90=98=EC=A7=80?= =?UTF-8?q?=20=EC=95=8A=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/vook/server/api/app/user/UserService.java | 2 ++ api/src/main/java/vook/server/api/model/user/User.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 3bf5ed4e..a52541c3 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -49,5 +49,7 @@ public void register(RegisterCommand command) { UserTermsAgree savedUserTermsAgree = userTermsAgreeRepository.save(userTermsAgree); user.addUserTermsAgree(savedUserTermsAgree); } + + user.registered(); } } diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index c959e65c..8909e393 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -56,4 +56,8 @@ public void addUserInfo(UserInfo userInfo) { public void addUserTermsAgree(UserTermsAgree userTermsAgree) { userTermsAgrees.add(userTermsAgree); } + + public void registered() { + this.status = UserStatus.REGISTERED; + } } From 73a5858dd5e39f737e04cdd2065df720c81ea5b8 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 18:07:49 +0900 Subject: [PATCH 077/273] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20API=20=EC=B4=88=EC=95=88=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/user/CompleteOnboardingCommand.java | 23 +++++++++++++++++++ .../vook/server/api/app/user/UserService.java | 7 ++++++ .../java/vook/server/api/model/user/User.java | 4 ++++ .../vook/server/api/model/user/UserInfo.java | 9 ++++++++ .../server/api/web/routes/user/UserApi.java | 14 +++++++++++ .../user/UserOnboardingCompleteRequest.java | 15 ++++++++++++ .../web/routes/user/UserRestController.java | 10 ++++++++ .../api/web/routes/user/UserWebService.java | 4 ++++ 8 files changed, 86 insertions(+) create mode 100644 api/src/main/java/vook/server/api/app/user/CompleteOnboardingCommand.java create mode 100644 api/src/main/java/vook/server/api/web/routes/user/UserOnboardingCompleteRequest.java diff --git a/api/src/main/java/vook/server/api/app/user/CompleteOnboardingCommand.java b/api/src/main/java/vook/server/api/app/user/CompleteOnboardingCommand.java new file mode 100644 index 00000000..a7b56ac2 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/CompleteOnboardingCommand.java @@ -0,0 +1,23 @@ +package vook.server.api.app.user; + +import lombok.Getter; + +@Getter +public class CompleteOnboardingCommand { + + public String userUid; + public String funnel; + public String job; + + public static CompleteOnboardingCommand of( + String userUid, + String funnel, + String job + ) { + CompleteOnboardingCommand command = new CompleteOnboardingCommand(); + command.userUid = userUid; + command.funnel = funnel; + command.job = job; + return command; + } +} diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index a52541c3..7f12f716 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -52,4 +52,11 @@ public void register(RegisterCommand command) { user.registered(); } + + public void completeOnboarding(CompleteOnboardingCommand command) { + User user = repository.findByUid(command.getUserUid()).orElseThrow(); + user.onboardingCompleted(); + + user.getUserInfo().addOnboardingInfo(command.getFunnel(), command.getJob()); + } } diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 8909e393..59d12e44 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -60,4 +60,8 @@ public void addUserTermsAgree(UserTermsAgree userTermsAgree) { public void registered() { this.status = UserStatus.REGISTERED; } + + public void onboardingCompleted() { + this.status = UserStatus.ONBOARDING_COMPLETED; + } } diff --git a/api/src/main/java/vook/server/api/model/user/UserInfo.java b/api/src/main/java/vook/server/api/model/user/UserInfo.java index 6b90e387..16fccb89 100644 --- a/api/src/main/java/vook/server/api/model/user/UserInfo.java +++ b/api/src/main/java/vook/server/api/model/user/UserInfo.java @@ -14,6 +14,10 @@ public class UserInfo { private String nickname; + private String funnel; + + private String job; + @OneToOne @JoinColumn(name = "user_id") private User user; @@ -27,4 +31,9 @@ public static UserInfo forRegisterOf( result.user = user; return result; } + + public void addOnboardingInfo(String funnel, String job) { + this.funnel = funnel; + this.job = job; + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 42c18a2c..6d5dd809 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -68,4 +68,18 @@ class UserApiTermsResponse extends CommonApiResponse> { ), }) CommonApiResponse register(VookLoginUser user, UserRegisterRequest request); + + @Operation( + summary = "온보딩 완료", + security = { + @SecurityRequirement(name = "AccessToken") + } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "성공" + ), + }) + CommonApiResponse onboardingComplete(VookLoginUser user, UserOnboardingCompleteRequest request); } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserOnboardingCompleteRequest.java b/api/src/main/java/vook/server/api/web/routes/user/UserOnboardingCompleteRequest.java new file mode 100644 index 00000000..b74ae5bc --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/UserOnboardingCompleteRequest.java @@ -0,0 +1,15 @@ +package vook.server.api.web.routes.user; + +import lombok.Data; +import vook.server.api.app.user.CompleteOnboardingCommand; + +@Data +public class UserOnboardingCompleteRequest { + + public String funnel; + public String job; + + public CompleteOnboardingCommand toCommand(String uid) { + return CompleteOnboardingCommand.of(uid, funnel, job); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index f04c40ab..f25f0577 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -41,4 +41,14 @@ public CommonApiResponse register( service.register(user, request); return CommonApiResponse.ok(); } + + @Override + @PostMapping("/onboarding/complete") + public CommonApiResponse onboardingComplete( + @AuthenticationPrincipal VookLoginUser user, + @RequestBody UserOnboardingCompleteRequest request + ) { + service.onboardingComplete(user, request); + return CommonApiResponse.ok(); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 0d48378f..74cf666d 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -36,4 +36,8 @@ public void register(VookLoginUser loginUser, UserRegisterRequest request) { } userService.register(request.toCommand(loginUser.getUid(), termsService::find)); } + + public void onboardingComplete(VookLoginUser loginUser, UserOnboardingCompleteRequest request) { + userService.completeOnboarding(request.toCommand(loginUser.getUid())); + } } From 1e96d814e585ac684bafb43b4642b0a2c8e72d30 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 18:18:19 +0900 Subject: [PATCH 078/273] =?UTF-8?q?fix:=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EC=8B=9C=20=EC=98=88=EC=99=B8=EA=B0=80=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/devhelper/InitService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 1bc0f853..9ac89bc1 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -8,7 +8,9 @@ import vook.server.api.app.demo.DemoTermSynonymRepository; import vook.server.api.app.terms.TermsRepository; import vook.server.api.app.user.SocialUserRepository; +import vook.server.api.app.user.UserInfoRepository; import vook.server.api.app.user.UserRepository; +import vook.server.api.app.user.UserTermsAgreeRepository; import vook.server.api.model.demo.DemoTerm; import vook.server.api.model.terms.Terms; import vook.server.api.outbound.search.DemoTermSearchService; @@ -27,6 +29,8 @@ public class InitService { private final DemoTermRepository demoTermRepository; private final DemoTermSynonymRepository demoTermSynonymRepository; + private final UserTermsAgreeRepository userTermsAgreeRepository; + private final UserInfoRepository userInfoRepository; private final SocialUserRepository socialUserRepository; private final UserRepository userRepository; private final TermsRepository termsRepository; @@ -35,6 +39,8 @@ public class InitService { public void init() { demoTermSynonymRepository.deleteAllInBatch(); demoTermRepository.deleteAllInBatch(); + userTermsAgreeRepository.deleteAllInBatch(); + userInfoRepository.deleteAllInBatch(); socialUserRepository.deleteAllInBatch(); userRepository.deleteAllInBatch(); termsRepository.deleteAllInBatch(); From 5cfc3ea3ddf86c45d4417b9f31fd6181d9a553f2 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 23:31:49 +0900 Subject: [PATCH 079/273] =?UTF-8?q?refector:=20=EB=AF=B8=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/config/auth/common/VookLoginUser.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java b/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java index 9e7a54fc..fe24839e 100644 --- a/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java +++ b/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java @@ -4,8 +4,6 @@ import lombok.ToString; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; -import vook.server.api.helper.jwt.JWTReader; -import vook.server.api.helper.jwt.JWTWriter; import vook.server.api.model.user.SocialUser; import java.util.Collection; @@ -30,10 +28,6 @@ public static VookLoginUser from(SocialUser user) { return VookLoginUser.of(user.getUser().getUid()); } - public static VookLoginUser from(JWTReader jwtReader) { - return VookLoginUser.of(jwtReader.getClaim("uid")); - } - @Override public Map getAttributes() { return Map.of(); @@ -49,19 +43,4 @@ public String getName() { return this.uid; } - public String toAccessToken(JWTWriter jwtWriter, Integer accessTokenExpiredMinute) { - return jwtWriter - .withExpiredMs(1000L * 60 * accessTokenExpiredMinute) - .withClaim("category", "access") - .withClaim("uid", this.uid) - .jwtString(); - } - - public String toRefreshToken(JWTWriter jwtWriter, Integer refreshTokenExpiredMinute) { - return jwtWriter - .withExpiredMs(1000L * 60 * refreshTokenExpiredMinute) - .withClaim("category", "refresh") - .withClaim("uid", this.uid) - .jwtString(); - } } From f5ab28853de268d0ae5439a8c18903849b040331 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 23:34:44 +0900 Subject: [PATCH 080/273] =?UTF-8?q?refector:=20reqres=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/web/routes/user/UserApi.java | 4 ++++ .../vook/server/api/web/routes/user/UserRestController.java | 4 ++++ .../java/vook/server/api/web/routes/user/UserWebService.java | 4 ++++ .../api/web/routes/user/{ => reqres}/UserInfoResponse.java | 2 +- .../user/{ => reqres}/UserOnboardingCompleteRequest.java | 2 +- .../api/web/routes/user/{ => reqres}/UserRegisterRequest.java | 2 +- .../api/web/routes/user/{ => reqres}/UserTermsResponse.java | 2 +- 7 files changed, 16 insertions(+), 4 deletions(-) rename api/src/main/java/vook/server/api/web/routes/user/{ => reqres}/UserInfoResponse.java (93%) rename api/src/main/java/vook/server/api/web/routes/user/{ => reqres}/UserOnboardingCompleteRequest.java (86%) rename api/src/main/java/vook/server/api/web/routes/user/{ => reqres}/UserRegisterRequest.java (96%) rename api/src/main/java/vook/server/api/web/routes/user/{ => reqres}/UserTermsResponse.java (93%) diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 6d5dd809..5df235d3 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -9,6 +9,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; +import vook.server.api.web.routes.user.reqres.UserInfoResponse; +import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; +import vook.server.api.web.routes.user.reqres.UserRegisterRequest; +import vook.server.api.web.routes.user.reqres.UserTermsResponse; import java.util.List; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index f25f0577..de6639cb 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -6,6 +6,10 @@ import org.springframework.web.bind.annotation.*; import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; +import vook.server.api.web.routes.user.reqres.UserInfoResponse; +import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; +import vook.server.api.web.routes.user.reqres.UserRegisterRequest; +import vook.server.api.web.routes.user.reqres.UserTermsResponse; import java.util.List; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 74cf666d..7567a879 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -8,6 +8,10 @@ import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.model.terms.Terms; import vook.server.api.model.user.User; +import vook.server.api.web.routes.user.reqres.UserInfoResponse; +import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; +import vook.server.api.web.routes.user.reqres.UserRegisterRequest; +import vook.server.api.web.routes.user.reqres.UserTermsResponse; import java.util.List; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserInfoResponse.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserInfoResponse.java similarity index 93% rename from api/src/main/java/vook/server/api/web/routes/user/UserInfoResponse.java rename to api/src/main/java/vook/server/api/web/routes/user/reqres/UserInfoResponse.java index ba97fe9e..23afd032 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserInfoResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserInfoResponse.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.user; +package vook.server.api.web.routes.user.reqres; import lombok.Getter; import vook.server.api.model.user.User; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserOnboardingCompleteRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java similarity index 86% rename from api/src/main/java/vook/server/api/web/routes/user/UserOnboardingCompleteRequest.java rename to api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java index b74ae5bc..a9e92ab1 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserOnboardingCompleteRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.user; +package vook.server.api.web.routes.user.reqres; import lombok.Data; import vook.server.api.app.user.CompleteOnboardingCommand; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRegisterRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java similarity index 96% rename from api/src/main/java/vook/server/api/web/routes/user/UserRegisterRequest.java rename to api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java index d62e92d5..7073f6ba 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRegisterRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.user; +package vook.server.api.web.routes.user.reqres; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserTermsResponse.java similarity index 93% rename from api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java rename to api/src/main/java/vook/server/api/web/routes/user/reqres/UserTermsResponse.java index 989bf75f..93c58e72 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserTermsResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserTermsResponse.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.user; +package vook.server.api.web.routes.user.reqres; import lombok.Getter; import vook.server.api.model.terms.Terms; From f28bb5b58dcfd92a857781232319875a09bd800a Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 23:35:44 +0900 Subject: [PATCH 081/273] =?UTF-8?q?refector:=20BaseEntity=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/vook/server/api/model/{ => common}/BaseEntity.java | 2 +- api/src/main/java/vook/server/api/model/demo/DemoTerm.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename api/src/main/java/vook/server/api/model/{ => common}/BaseEntity.java (94%) diff --git a/api/src/main/java/vook/server/api/model/BaseEntity.java b/api/src/main/java/vook/server/api/model/common/BaseEntity.java similarity index 94% rename from api/src/main/java/vook/server/api/model/BaseEntity.java rename to api/src/main/java/vook/server/api/model/common/BaseEntity.java index c64f65ee..fabc793a 100644 --- a/api/src/main/java/vook/server/api/model/BaseEntity.java +++ b/api/src/main/java/vook/server/api/model/common/BaseEntity.java @@ -1,4 +1,4 @@ -package vook.server.api.model; +package vook.server.api.model.common; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; diff --git a/api/src/main/java/vook/server/api/model/demo/DemoTerm.java b/api/src/main/java/vook/server/api/model/demo/DemoTerm.java index c56d2279..42835bce 100644 --- a/api/src/main/java/vook/server/api/model/demo/DemoTerm.java +++ b/api/src/main/java/vook/server/api/model/demo/DemoTerm.java @@ -2,7 +2,7 @@ import jakarta.persistence.*; import lombok.Getter; -import vook.server.api.model.BaseEntity; +import vook.server.api.model.common.BaseEntity; import java.util.ArrayList; import java.util.List; From 88c981a231bf0d355914421c5245f2f13535a1e5 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 23:38:03 +0900 Subject: [PATCH 082/273] =?UTF-8?q?refector:=20InitService=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=B6=80=EB=B6=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/devhelper/InitService.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 9ac89bc1..69a1ceb2 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -37,14 +37,7 @@ public class InitService { private final DemoTermSearchService searchService; public void init() { - demoTermSynonymRepository.deleteAllInBatch(); - demoTermRepository.deleteAllInBatch(); - userTermsAgreeRepository.deleteAllInBatch(); - userInfoRepository.deleteAllInBatch(); - socialUserRepository.deleteAllInBatch(); - userRepository.deleteAllInBatch(); - termsRepository.deleteAllInBatch(); - searchService.clearAll(); + deleteAll(); termsRepository.save(Terms.of("이용약관", loadContents("classpath:init/이용약관.txt"), true)); termsRepository.save(Terms.of("개인정보 수집 이용 약관", loadContents("classpath:init/개인정보_수집_이용_약관.txt"), true)); @@ -57,6 +50,24 @@ public void init() { searchService.addTerms(devTerms); } + private void deleteAll() { + // 데모 용어 + demoTermSynonymRepository.deleteAllInBatch(); + demoTermRepository.deleteAllInBatch(); + + // 사용자 + userTermsAgreeRepository.deleteAllInBatch(); + userInfoRepository.deleteAllInBatch(); + socialUserRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + + // 약관 + termsRepository.deleteAllInBatch(); + + // 검색 엔진 + searchService.clearAll(); + } + private String loadContents(String location) { try { InputStream inputStream = resourceLoader.getResource(location).getInputStream(); From 18c271e11b8532e585e9e7ee1bd1f4e1f445dcd4 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 23:40:11 +0900 Subject: [PATCH 083/273] =?UTF-8?q?refector:=20=EB=B3=B4=EC=95=88=20?= =?UTF-8?q?=EC=B7=A8=EC=95=BD=20=EB=B6=80=EB=B6=84=20TODO=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/config/auth/oauth2/LoginSuccessHandler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java b/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java index c1cc5f36..f44332ed 100644 --- a/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java +++ b/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java @@ -27,6 +27,8 @@ public class LoginSuccessHandler implements AuthenticationSuccessHandler { public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { VookLoginUser oAuth2User = (VookLoginUser) authentication.getPrincipal(); GeneratedToken token = tokenService.generateToken(oAuth2User.getUid()); + + //TODO: query parameter로 token을 전달하는 방식은 보안에 취약, 추후 code를 이용해 토큰을 교환하는 방식으로 변경 필요 response.sendRedirect(buildRedirectUrl(token)); } From 91d284655f74804baf7648be2d41ee9a1ba9650a Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 23:43:09 +0900 Subject: [PATCH 084/273] =?UTF-8?q?refector:=20app=20=EB=82=B4=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=84=B8=EB=B6=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/auth/TokenService.java | 1 + .../api/app/auth/{ => data}/GeneratedToken.java | 2 +- .../app/demo/{ => repo}/DemoTermRepository.java | 2 +- .../demo/{ => repo}/DemoTermSynonymRepository.java | 2 +- .../vook/server/api/app/terms/TermsService.java | 1 + .../api/app/terms/{ => repo}/TermsRepository.java | 2 +- .../java/vook/server/api/app/user/UserService.java | 7 +++++++ .../user/{ => data}/CompleteOnboardingCommand.java | 2 +- .../api/app/user/{ => data}/RegisterCommand.java | 2 +- .../user/{ => data}/SignUpFromSocialCommand.java | 2 +- .../app/user/{ => repo}/SocialUserRepository.java | 2 +- .../app/user/{ => repo}/UserInfoRepository.java | 2 +- .../api/app/user/{ => repo}/UserRepository.java | 2 +- .../user/{ => repo}/UserTermsAgreeRepository.java | 2 +- .../config/auth/oauth2/LoginSuccessHandler.java | 2 +- .../config/auth/oauth2/VookOAuth2UserService.java | 2 +- .../vook/server/api/devhelper/InitService.java | 14 +++++++------- .../java/vook/server/api/devhelper/LocalInit.java | 2 +- .../api/web/routes/auth/AuthRestController.java | 2 +- .../user/reqres/UserOnboardingCompleteRequest.java | 2 +- .../routes/user/reqres/UserRegisterRequest.java | 2 +- 21 files changed, 33 insertions(+), 24 deletions(-) rename api/src/main/java/vook/server/api/app/auth/{ => data}/GeneratedToken.java (91%) rename api/src/main/java/vook/server/api/app/demo/{ => repo}/DemoTermRepository.java (82%) rename api/src/main/java/vook/server/api/app/demo/{ => repo}/DemoTermSynonymRepository.java (84%) rename api/src/main/java/vook/server/api/app/terms/{ => repo}/TermsRepository.java (86%) rename api/src/main/java/vook/server/api/app/user/{ => data}/CompleteOnboardingCommand.java (92%) rename api/src/main/java/vook/server/api/app/user/{ => data}/RegisterCommand.java (96%) rename api/src/main/java/vook/server/api/app/user/{ => data}/SignUpFromSocialCommand.java (95%) rename api/src/main/java/vook/server/api/app/user/{ => repo}/SocialUserRepository.java (89%) rename api/src/main/java/vook/server/api/app/user/{ => repo}/UserInfoRepository.java (82%) rename api/src/main/java/vook/server/api/app/user/{ => repo}/UserRepository.java (88%) rename api/src/main/java/vook/server/api/app/user/{ => repo}/UserTermsAgreeRepository.java (83%) diff --git a/api/src/main/java/vook/server/api/app/auth/TokenService.java b/api/src/main/java/vook/server/api/app/auth/TokenService.java index 1accdb11..06413922 100644 --- a/api/src/main/java/vook/server/api/app/auth/TokenService.java +++ b/api/src/main/java/vook/server/api/app/auth/TokenService.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import vook.server.api.app.auth.data.GeneratedToken; import vook.server.api.helper.jwt.JWTHelperProvider; import vook.server.api.helper.jwt.JWTReader; diff --git a/api/src/main/java/vook/server/api/app/auth/GeneratedToken.java b/api/src/main/java/vook/server/api/app/auth/data/GeneratedToken.java similarity index 91% rename from api/src/main/java/vook/server/api/app/auth/GeneratedToken.java rename to api/src/main/java/vook/server/api/app/auth/data/GeneratedToken.java index eb6bdf2c..b495abd4 100644 --- a/api/src/main/java/vook/server/api/app/auth/GeneratedToken.java +++ b/api/src/main/java/vook/server/api/app/auth/data/GeneratedToken.java @@ -1,4 +1,4 @@ -package vook.server.api.app.auth; +package vook.server.api.app.auth.data; import lombok.Getter; diff --git a/api/src/main/java/vook/server/api/app/demo/DemoTermRepository.java b/api/src/main/java/vook/server/api/app/demo/repo/DemoTermRepository.java similarity index 82% rename from api/src/main/java/vook/server/api/app/demo/DemoTermRepository.java rename to api/src/main/java/vook/server/api/app/demo/repo/DemoTermRepository.java index e304a5be..dab1dd12 100644 --- a/api/src/main/java/vook/server/api/app/demo/DemoTermRepository.java +++ b/api/src/main/java/vook/server/api/app/demo/repo/DemoTermRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.demo; +package vook.server.api.app.demo.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.demo.DemoTerm; diff --git a/api/src/main/java/vook/server/api/app/demo/DemoTermSynonymRepository.java b/api/src/main/java/vook/server/api/app/demo/repo/DemoTermSynonymRepository.java similarity index 84% rename from api/src/main/java/vook/server/api/app/demo/DemoTermSynonymRepository.java rename to api/src/main/java/vook/server/api/app/demo/repo/DemoTermSynonymRepository.java index 7298bdb0..46cef6df 100644 --- a/api/src/main/java/vook/server/api/app/demo/DemoTermSynonymRepository.java +++ b/api/src/main/java/vook/server/api/app/demo/repo/DemoTermSynonymRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.demo; +package vook.server.api.app.demo.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.demo.DemoTermSynonym; diff --git a/api/src/main/java/vook/server/api/app/terms/TermsService.java b/api/src/main/java/vook/server/api/app/terms/TermsService.java index dd7e27fa..5cd87a56 100644 --- a/api/src/main/java/vook/server/api/app/terms/TermsService.java +++ b/api/src/main/java/vook/server/api/app/terms/TermsService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import vook.server.api.app.terms.repo.TermsRepository; import vook.server.api.model.terms.Terms; import java.util.HashSet; diff --git a/api/src/main/java/vook/server/api/app/terms/TermsRepository.java b/api/src/main/java/vook/server/api/app/terms/repo/TermsRepository.java similarity index 86% rename from api/src/main/java/vook/server/api/app/terms/TermsRepository.java rename to api/src/main/java/vook/server/api/app/terms/repo/TermsRepository.java index a7c1e35f..512c594f 100644 --- a/api/src/main/java/vook/server/api/app/terms/TermsRepository.java +++ b/api/src/main/java/vook/server/api/app/terms/repo/TermsRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.terms; +package vook.server.api.app.terms.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.terms.Terms; diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 7f12f716..152d8e10 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -2,6 +2,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import vook.server.api.app.user.data.CompleteOnboardingCommand; +import vook.server.api.app.user.data.RegisterCommand; +import vook.server.api.app.user.data.SignUpFromSocialCommand; +import vook.server.api.app.user.repo.SocialUserRepository; +import vook.server.api.app.user.repo.UserInfoRepository; +import vook.server.api.app.user.repo.UserRepository; +import vook.server.api.app.user.repo.UserTermsAgreeRepository; import vook.server.api.model.user.SocialUser; import vook.server.api.model.user.User; import vook.server.api.model.user.UserInfo; diff --git a/api/src/main/java/vook/server/api/app/user/CompleteOnboardingCommand.java b/api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java similarity index 92% rename from api/src/main/java/vook/server/api/app/user/CompleteOnboardingCommand.java rename to api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java index a7b56ac2..3bce9645 100644 --- a/api/src/main/java/vook/server/api/app/user/CompleteOnboardingCommand.java +++ b/api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user; +package vook.server.api.app.user.data; import lombok.Getter; diff --git a/api/src/main/java/vook/server/api/app/user/RegisterCommand.java b/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java similarity index 96% rename from api/src/main/java/vook/server/api/app/user/RegisterCommand.java rename to api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java index b347b7b0..e76e7b12 100644 --- a/api/src/main/java/vook/server/api/app/user/RegisterCommand.java +++ b/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user; +package vook.server.api.app.user.data; import lombok.Getter; import vook.server.api.model.terms.Terms; diff --git a/api/src/main/java/vook/server/api/app/user/SignUpFromSocialCommand.java b/api/src/main/java/vook/server/api/app/user/data/SignUpFromSocialCommand.java similarity index 95% rename from api/src/main/java/vook/server/api/app/user/SignUpFromSocialCommand.java rename to api/src/main/java/vook/server/api/app/user/data/SignUpFromSocialCommand.java index bbd8802b..bc7ece14 100644 --- a/api/src/main/java/vook/server/api/app/user/SignUpFromSocialCommand.java +++ b/api/src/main/java/vook/server/api/app/user/data/SignUpFromSocialCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user; +package vook.server.api.app.user.data; import lombok.Getter; import vook.server.api.model.user.SocialUser; diff --git a/api/src/main/java/vook/server/api/app/user/SocialUserRepository.java b/api/src/main/java/vook/server/api/app/user/repo/SocialUserRepository.java similarity index 89% rename from api/src/main/java/vook/server/api/app/user/SocialUserRepository.java rename to api/src/main/java/vook/server/api/app/user/repo/SocialUserRepository.java index 7ad66418..2e858bc4 100644 --- a/api/src/main/java/vook/server/api/app/user/SocialUserRepository.java +++ b/api/src/main/java/vook/server/api/app/user/repo/SocialUserRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user; +package vook.server.api.app.user.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.user.SocialUser; diff --git a/api/src/main/java/vook/server/api/app/user/UserInfoRepository.java b/api/src/main/java/vook/server/api/app/user/repo/UserInfoRepository.java similarity index 82% rename from api/src/main/java/vook/server/api/app/user/UserInfoRepository.java rename to api/src/main/java/vook/server/api/app/user/repo/UserInfoRepository.java index b7b354cf..47c18f25 100644 --- a/api/src/main/java/vook/server/api/app/user/UserInfoRepository.java +++ b/api/src/main/java/vook/server/api/app/user/repo/UserInfoRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user; +package vook.server.api.app.user.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.user.UserInfo; diff --git a/api/src/main/java/vook/server/api/app/user/UserRepository.java b/api/src/main/java/vook/server/api/app/user/repo/UserRepository.java similarity index 88% rename from api/src/main/java/vook/server/api/app/user/UserRepository.java rename to api/src/main/java/vook/server/api/app/user/repo/UserRepository.java index e926cafc..ba20060b 100644 --- a/api/src/main/java/vook/server/api/app/user/UserRepository.java +++ b/api/src/main/java/vook/server/api/app/user/repo/UserRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user; +package vook.server.api.app.user.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.user.User; diff --git a/api/src/main/java/vook/server/api/app/user/UserTermsAgreeRepository.java b/api/src/main/java/vook/server/api/app/user/repo/UserTermsAgreeRepository.java similarity index 83% rename from api/src/main/java/vook/server/api/app/user/UserTermsAgreeRepository.java rename to api/src/main/java/vook/server/api/app/user/repo/UserTermsAgreeRepository.java index 1146954e..bf212f74 100644 --- a/api/src/main/java/vook/server/api/app/user/UserTermsAgreeRepository.java +++ b/api/src/main/java/vook/server/api/app/user/repo/UserTermsAgreeRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user; +package vook.server.api.app.user.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.user.UserTermsAgree; diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java b/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java index f44332ed..f6fbd560 100644 --- a/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java +++ b/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java @@ -8,8 +8,8 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; -import vook.server.api.app.auth.GeneratedToken; import vook.server.api.app.auth.TokenService; +import vook.server.api.app.auth.data.GeneratedToken; import vook.server.api.config.auth.common.VookLoginUser; import java.io.IOException; diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java b/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java index 2d025d9f..83a9170c 100644 --- a/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java +++ b/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java @@ -8,8 +8,8 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.user.SignUpFromSocialCommand; import vook.server.api.app.user.UserService; +import vook.server.api.app.user.data.SignUpFromSocialCommand; import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.model.user.SocialUser; diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 69a1ceb2..e23e20c9 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -4,13 +4,13 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.demo.DemoTermRepository; -import vook.server.api.app.demo.DemoTermSynonymRepository; -import vook.server.api.app.terms.TermsRepository; -import vook.server.api.app.user.SocialUserRepository; -import vook.server.api.app.user.UserInfoRepository; -import vook.server.api.app.user.UserRepository; -import vook.server.api.app.user.UserTermsAgreeRepository; +import vook.server.api.app.demo.repo.DemoTermRepository; +import vook.server.api.app.demo.repo.DemoTermSynonymRepository; +import vook.server.api.app.terms.repo.TermsRepository; +import vook.server.api.app.user.repo.SocialUserRepository; +import vook.server.api.app.user.repo.UserInfoRepository; +import vook.server.api.app.user.repo.UserRepository; +import vook.server.api.app.user.repo.UserTermsAgreeRepository; import vook.server.api.model.demo.DemoTerm; import vook.server.api.model.terms.Terms; import vook.server.api.outbound.search.DemoTermSearchService; diff --git a/api/src/main/java/vook/server/api/devhelper/LocalInit.java b/api/src/main/java/vook/server/api/devhelper/LocalInit.java index b4d92925..217b368f 100644 --- a/api/src/main/java/vook/server/api/devhelper/LocalInit.java +++ b/api/src/main/java/vook/server/api/devhelper/LocalInit.java @@ -5,7 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import vook.server.api.app.demo.DemoTermRepository; +import vook.server.api.app.demo.repo.DemoTermRepository; @Slf4j @Profile("local") diff --git a/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java b/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java index e27e5531..8df4aba0 100644 --- a/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java @@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import vook.server.api.app.auth.GeneratedToken; import vook.server.api.app.auth.TokenService; +import vook.server.api.app.auth.data.GeneratedToken; import vook.server.api.model.values.AuthValues; @RestController diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java index a9e92ab1..4756bcda 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java @@ -1,7 +1,7 @@ package vook.server.api.web.routes.user.reqres; import lombok.Data; -import vook.server.api.app.user.CompleteOnboardingCommand; +import vook.server.api.app.user.data.CompleteOnboardingCommand; @Data public class UserOnboardingCompleteRequest { diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java index 7073f6ba..17bb1060 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import vook.server.api.app.user.RegisterCommand; +import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.model.terms.Terms; import java.util.List; From 2355dce7df1d73b375347c2276469c294987d142 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 23:56:40 +0900 Subject: [PATCH 085/273] =?UTF-8?q?chore:=20devenv=20=EB=8B=A8=EC=9D=BC?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devenv/{db => }/Makefile | 0 devenv/compose.yml | 34 +++++++++++++++++++ devenv/db/compose.yml | 22 ------------ devenv/{db/Dockerfiles => mariadb.Dockerfile} | 2 +- devenv/meilisearch/Makefile | 15 -------- devenv/meilisearch/compose.yml | 15 -------- 6 files changed, 35 insertions(+), 53 deletions(-) rename devenv/{db => }/Makefile (100%) create mode 100644 devenv/compose.yml delete mode 100644 devenv/db/compose.yml rename devenv/{db/Dockerfiles => mariadb.Dockerfile} (80%) delete mode 100644 devenv/meilisearch/Makefile delete mode 100644 devenv/meilisearch/compose.yml diff --git a/devenv/db/Makefile b/devenv/Makefile similarity index 100% rename from devenv/db/Makefile rename to devenv/Makefile diff --git a/devenv/compose.yml b/devenv/compose.yml new file mode 100644 index 00000000..0f2ca078 --- /dev/null +++ b/devenv/compose.yml @@ -0,0 +1,34 @@ +services: + db: + build: + context: . + dockerfile: mariadb.Dockerfile + volumes: + - ./db/initdb.d:/docker-entrypoint-initdb.d + - db-data:/var/lib/mysql + environment: + MYSQL_PORT: 3306 + MYSQL_ROOT_PASSWORD: rootPw + MYSQL_DATABASE: vook + MYSQL_USER: user + MYSQL_PASSWORD: userPw + TZ: Asia/Seoul + ports: + - "3307:3306" + mem_limit: 300m + restart: unless-stopped + meilisearch: + image: getmeili/meilisearch:v1.8.0 + volumes: + - meili_data:/meili_data + ports: + - "7700:7700" + environment: + - MEILI_ENV=development + - MEILI_MASTER_KEY=aSampleMasterKey + mem_limit: 1000m + restart: unless-stopped + +volumes: + db-data: {} + meili_data: {} diff --git a/devenv/db/compose.yml b/devenv/db/compose.yml deleted file mode 100644 index 8eef0be1..00000000 --- a/devenv/db/compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -services: - db: - build: - context: . - dockerfile: Dockerfiles - volumes: - - ./initdb.d:/docker-entrypoint-initdb.d - - db-data:/var/lib/mysql - environment: - MYSQL_PORT: 3306 - MYSQL_ROOT_PASSWORD: rootPw - MYSQL_DATABASE: vook - MYSQL_USER: user - MYSQL_PASSWORD: userPw - TZ: Asia/Seoul - ports: - - "3307:3306" - mem_limit: 300m - restart: unless-stopped - -volumes: - db-data: {} diff --git a/devenv/db/Dockerfiles b/devenv/mariadb.Dockerfile similarity index 80% rename from devenv/db/Dockerfiles rename to devenv/mariadb.Dockerfile index e0436f3c..2b6b74ef 100644 --- a/devenv/db/Dockerfiles +++ b/devenv/mariadb.Dockerfile @@ -1,5 +1,5 @@ FROM mariadb:10.11 # windows에서 volume mount 할 경우, 파일 권한이 777로 변경되는 문제가 있어서 아래와 같은 작업을 추가 함 -COPY conf.d/my.cnf /etc/mysql/conf.d/my.cnf +COPY db/conf.d/my.cnf /etc/mysql/conf.d/my.cnf RUN chmod 644 /etc/mysql/conf.d/my.cnf diff --git a/devenv/meilisearch/Makefile b/devenv/meilisearch/Makefile deleted file mode 100644 index fcbaae30..00000000 --- a/devenv/meilisearch/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -.PHONY:up -up: - docker compose up -d - -.PHONY:down -down: - docker compose down - -.PHONY:clean -clean: - docker compose down -v - -.PHONY:log -log: - docker compose logs -f --since 1m diff --git a/devenv/meilisearch/compose.yml b/devenv/meilisearch/compose.yml deleted file mode 100644 index ce149673..00000000 --- a/devenv/meilisearch/compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - meilisearch: - image: getmeili/meilisearch:v1.8.0 - volumes: - - meili_data:/meili_data - ports: - - "7700:7700" - environment: - - MEILI_ENV=development - - MEILI_MASTER_KEY=aSampleMasterKey - mem_limit: 1000m - restart: unless-stopped - -volumes: - meili_data: {} From 689afcf69b7014835369efa067c94b25bf8d318b Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 20 May 2024 23:57:59 +0900 Subject: [PATCH 086/273] =?UTF-8?q?chore:=20tbls=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tbls.yml => docs/.tbls.yml | 2 +- Makefile => docs/Makefile | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename .tbls.yml => docs/.tbls.yml (77%) rename Makefile => docs/Makefile (100%) diff --git a/.tbls.yml b/docs/.tbls.yml similarity index 77% rename from .tbls.yml rename to docs/.tbls.yml index 50a9c5ea..1aec44b5 100644 --- a/.tbls.yml +++ b/docs/.tbls.yml @@ -1,6 +1,6 @@ dsn: mariadb://user:userPw@localhost:3307/vook -docPath: docs/schema +docPath: schema er: format: mermaid diff --git a/Makefile b/docs/Makefile similarity index 100% rename from Makefile rename to docs/Makefile From a80b85f4b44eea648c709272f6e2f796608a7250 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 22 May 2024 18:14:52 +0900 Subject: [PATCH 087/273] =?UTF-8?q?chore:=20testcontainer=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20#58?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle | 8 +++++--- .../vook/server/api/ApiApplicationTests.java | 13 ------------- .../vook/server/api/testhelper/ApiTest.java | 15 +++++++++++++++ api/src/test/resources/application.yml | 19 +++++++++++++++++-- api/src/test/resources/db/conf/my.cnf | 11 +++++++++++ cicd/dagger.json | 4 ++++ cicd/dagger/main.go | 3 ++- 7 files changed, 54 insertions(+), 19 deletions(-) delete mode 100644 api/src/test/java/vook/server/api/ApiApplicationTests.java create mode 100644 api/src/test/resources/db/conf/my.cnf diff --git a/api/build.gradle b/api/build.gradle index 2b66b72d..0b82ab7f 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -46,14 +46,16 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.5.0' - // h2 database - testRuntimeOnly 'com.h2database:h2' - // QueryDsl implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.persistence:jakarta.persistence-api" annotationProcessor "jakarta.annotation:jakarta.annotation-api" + + // testcontainer + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:mariadb' } bootJar { diff --git a/api/src/test/java/vook/server/api/ApiApplicationTests.java b/api/src/test/java/vook/server/api/ApiApplicationTests.java deleted file mode 100644 index e8ec174f..00000000 --- a/api/src/test/java/vook/server/api/ApiApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package vook.server.api; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApiApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/api/src/test/java/vook/server/api/testhelper/ApiTest.java b/api/src/test/java/vook/server/api/testhelper/ApiTest.java index 3f7f4d30..6616f0eb 100644 --- a/api/src/test/java/vook/server/api/testhelper/ApiTest.java +++ b/api/src/test/java/vook/server/api/testhelper/ApiTest.java @@ -4,7 +4,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.MariaDBContainer; +import java.util.Map; import java.util.TimeZone; import static vook.server.api.config.TimeZoneConfig.DEFAULT_TIME_ZONE; @@ -15,6 +18,18 @@ public abstract class ApiTest { @Autowired protected TestRestTemplate rest; + @ServiceConnection + protected static final MariaDBContainer mariaDBContainer = new MariaDBContainer<>("mariadb:10.11") + .withDatabaseName("example") + .withUsername("user") + .withPassword("userPw") + .withConfigurationOverride("db/conf") + .withTmpFs(Map.of("/var/lib/mysql", "rw")); + + static { + mariaDBContainer.start(); + } + @BeforeEach void init() { TimeZone.setDefault(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); diff --git a/api/src/test/resources/application.yml b/api/src/test/resources/application.yml index fb7a546f..9f47bafc 100644 --- a/api/src/test/resources/application.yml +++ b/api/src/test/resources/application.yml @@ -1,6 +1,15 @@ spring: - datasource: - driver-class-name: org.h2.Driver + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + defer-datasource-initialization: true + sql: + init: + mode: always security: oauth2: client: @@ -14,6 +23,12 @@ spring: scope: - profile - email +logging: + level: + org: + hibernate: + orm: + jdbc: TRACE service: meilisearch: host: http://localhost:7700 diff --git a/api/src/test/resources/db/conf/my.cnf b/api/src/test/resources/db/conf/my.cnf new file mode 100644 index 00000000..c6314654 --- /dev/null +++ b/api/src/test/resources/db/conf/my.cnf @@ -0,0 +1,11 @@ +[client] +default-character-set = utf8mb4 + +[mysql] +default-character-set = utf8mb4 + +[mysqld] +character-set-client-handshake = FALSE +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +lower_case_table_names = 1 diff --git a/cicd/dagger.json b/cicd/dagger.json index f80612a1..c9dadecf 100644 --- a/cicd/dagger.json +++ b/cicd/dagger.json @@ -6,6 +6,10 @@ "name": "docker", "source": "github.com/purpleclay/daggerverse/docker@43c1c55dadf15afc9ba401dc59e04baaa3802cca" }, + { + "name": "dockerService", + "source": "github.com/aweris/daggerverse/docker@980888a31696001fe024dc38ff104368f4a1931d" + }, { "name": "scp", "source": "github.com/seungyeop-lee/daggerverse/scp@63e721cf63323ede8b1f11aeff00d41960699920" diff --git a/cicd/dagger/main.go b/cicd/dagger/main.go index f538e818..0634caf5 100644 --- a/cicd/dagger/main.go +++ b/cicd/dagger/main.go @@ -30,7 +30,8 @@ func (v *VookServer) BuildApiJar( if test { _, err := c. - WithExec([]string{"./gradlew", "test"}). + With(dag.DockerService().BindAsService). + WithExec([]string{"./gradlew", "test", "--info"}). Sync(ctx) if err != nil { return nil, errors.New("test fail:" + err.Error()) From 6190b4dcd77a49357a86d224031b019c5c669517 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 22 May 2024 18:47:37 +0900 Subject: [PATCH 088/273] =?UTF-8?q?chore:=20cicd=20=EB=82=B4=20java=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=EC=9A=A9=20=EB=AA=A8=EB=93=88=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cicd/dagger.json | 4 ++++ cicd/dagger/main.go | 18 ++++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cicd/dagger.json b/cicd/dagger.json index c9dadecf..d06cab5c 100644 --- a/cicd/dagger.json +++ b/cicd/dagger.json @@ -2,6 +2,10 @@ "name": "vook-server", "sdk": "go", "dependencies": [ + { + "name": "java", + "source": "github.com/seungyeop-lee/daggerverse/java@a5e511cb5adec1bf0b67ef8856d4191e61ab4711" + }, { "name": "docker", "source": "github.com/purpleclay/daggerverse/docker@43c1c55dadf15afc9ba401dc59e04baaa3802cca" diff --git a/cicd/dagger/main.go b/cicd/dagger/main.go index 0634caf5..118c33e8 100644 --- a/cicd/dagger/main.go +++ b/cicd/dagger/main.go @@ -16,21 +16,15 @@ func (v *VookServer) BuildApiJar( // +optional test bool, ) (*File, error) { - c := dag.Container(). - From("eclipse-temurin:21-jdk"). - WithWorkdir("/app"). - WithDirectory("/app/gradle", dir.Directory("gradle")). - WithFiles("/app", []*File{dir.File("gradlew")}). - WithFiles("/app", []*File{ - dir.File("build.gradle"), - dir.File("settings.gradle"), - }). - WithExec([]string{"./gradlew"}). - WithDirectory("/app/src", dir.Directory("src")) + c := dag.Java(). + Init(). + WithGradleCache(). + WithDir(dir). + Container() if test { _, err := c. - With(dag.DockerService().BindAsService). + With(dag.DockerService().WithCacheVolume("docker-var/lib/docker").BindAsService). WithExec([]string{"./gradlew", "test", "--info"}). Sync(ctx) if err != nil { From 63c4a26fa55580419a6135ee53abc4b2aa6bcfb0 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 23 May 2024 09:58:20 +0900 Subject: [PATCH 089/273] =?UTF-8?q?chore:=20meilisearch=20testcontainer=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#60?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/devhelper/InitService.java | 38 +----------- .../server/api/devhelper/TestTermsLoader.java | 52 ++++++++++++++++ .../search/DemoTermSearchService.java | 7 ++- .../search/MeilisearchProperties.java | 15 +++++ .../outbound/search/MeilisearchService.java | 15 +---- .../vook/server/api/testhelper/ApiTest.java | 20 +++++- .../api/testhelper/MeilisearchContainer.java | 34 ++++++++++ .../web/routes/demo/DemoWebServiceTest.java | 62 +++++++++++++++++++ api/src/test/resources/application.yml | 3 - 9 files changed, 192 insertions(+), 54 deletions(-) create mode 100644 api/src/main/java/vook/server/api/devhelper/TestTermsLoader.java create mode 100644 api/src/main/java/vook/server/api/outbound/search/MeilisearchProperties.java create mode 100644 api/src/test/java/vook/server/api/testhelper/MeilisearchContainer.java create mode 100644 api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index e23e20c9..76bd72c5 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; import java.util.List; @Service @@ -35,6 +34,7 @@ public class InitService { private final UserRepository userRepository; private final TermsRepository termsRepository; private final DemoTermSearchService searchService; + private final TestTermsLoader testTermsLoader; public void init() { deleteAll(); @@ -43,7 +43,7 @@ public void init() { termsRepository.save(Terms.of("개인정보 수집 이용 약관", loadContents("classpath:init/개인정보_수집_이용_약관.txt"), true)); termsRepository.save(Terms.of("마케팅 메일 수신 약관", loadContents("classpath:init/마케팅_메일_수신_약관.txt"), false)); - List devTerms = getTerms("classpath:init/개발.tsv"); + List devTerms = testTermsLoader.getTerms("classpath:init/개발.tsv"); demoTermRepository.saveAll(devTerms); searchService.init(); @@ -76,38 +76,4 @@ private String loadContents(String location) { throw new RuntimeException(e); } } - - private List getTerms(String location) { - try { - // file로 바로 접근 할 경우, IDE에서는 접근 가능하나, jar로 패키징 후 실행 시에는 접근 불가능 - // ref) https://velog.io/@haron/트러블슈팅-Spring-IDE-에서-되는데-배포하면-안-돼요 - InputStream tsvFileInputStream = resourceLoader.getResource(location).getInputStream(); - CsvReader tsvReader = new CsvReader("\t"); - List rawTerms = tsvReader.readValue(tsvFileInputStream, RawTerm.class); - return toTerms(rawTerms); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static List toTerms(List rawTerms) { - return rawTerms.stream() - .map(RawTerm::toTerm) - .toList(); - } - - public static class RawTerm { - private String term; - private String synonyms; - private String meaning; - - public DemoTerm toTerm() { - DemoTerm term = DemoTerm.forCreateOf(this.term, this.meaning); - String[] synonymArray = this.synonyms.split("//n"); - Arrays.stream(synonymArray) - .map(String::trim) - .forEach(term::addSynonym); - return term; - } - } } diff --git a/api/src/main/java/vook/server/api/devhelper/TestTermsLoader.java b/api/src/main/java/vook/server/api/devhelper/TestTermsLoader.java new file mode 100644 index 00000000..4cc9b9d2 --- /dev/null +++ b/api/src/main/java/vook/server/api/devhelper/TestTermsLoader.java @@ -0,0 +1,52 @@ +package vook.server.api.devhelper; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; +import vook.server.api.model.demo.DemoTerm; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class TestTermsLoader { + + private final ResourceLoader resourceLoader; + + public List getTerms(String location) { + try { + // file로 바로 접근 할 경우, IDE에서는 접근 가능하나, jar로 패키징 후 실행 시에는 접근 불가능 + // ref) https://velog.io/@haron/트러블슈팅-Spring-IDE-에서-되는데-배포하면-안-돼요 + InputStream tsvFileInputStream = resourceLoader.getResource(location).getInputStream(); + CsvReader tsvReader = new CsvReader("\t"); + List rawTerms = tsvReader.readValue(tsvFileInputStream, RawTerm.class); + return toTerms(rawTerms); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static List toTerms(List rawTerms) { + return rawTerms.stream() + .map(RawTerm::toTerm) + .toList(); + } + + static class RawTerm { + private String term; + private String synonyms; + private String meaning; + + public DemoTerm toTerm() { + DemoTerm term = DemoTerm.forCreateOf(this.term, this.meaning); + String[] synonymArray = this.synonyms.split("//n"); + Arrays.stream(synonymArray) + .map(String::trim) + .forEach(term::addSynonym); + return term; + } + } +} diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java index c75fbe52..86a8d71b 100644 --- a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java @@ -9,7 +9,6 @@ import com.meilisearch.sdk.model.TypoTolerance; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import vook.server.api.model.demo.DemoTerm; import vook.server.api.model.demo.DemoTermSynonym; @@ -19,13 +18,17 @@ import java.util.stream.Collectors; @Service -@RequiredArgsConstructor public class DemoTermSearchService extends MeilisearchService { private static final String DEMO_TERMS_INDEX_UID = "demo-terms"; private final ObjectMapper objectMapper; + public DemoTermSearchService(MeilisearchProperties properties, ObjectMapper objectMapper) { + super(properties); + this.objectMapper = objectMapper; + } + public void clearAll() { clearAll(DEMO_TERMS_INDEX_UID); } diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchProperties.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchProperties.java new file mode 100644 index 00000000..f54a2ccd --- /dev/null +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchProperties.java @@ -0,0 +1,15 @@ +package vook.server.api.outbound.search; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Setter +@Getter +@Component +@ConfigurationProperties(prefix = "service.meilisearch") +public class MeilisearchProperties { + private String host; + private String apiKey; +} diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java index c23d55d1..8354cbba 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -5,24 +5,15 @@ import com.meilisearch.sdk.Index; import com.meilisearch.sdk.model.IndexesQuery; import com.meilisearch.sdk.model.Results; -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Value; import java.util.Arrays; public abstract class MeilisearchService { - @Value("${service.meilisearch.host:}") - protected String host; + protected final Client client; - @Value("${service.meilisearch.apiKey:}") - protected String apiKey; - - protected Client client; - - @PostConstruct - public void postConstruct() { - this.client = new Client(new Config(host, apiKey)); + protected MeilisearchService(MeilisearchProperties properties) { + this.client = new Client(new Config(properties.getHost(), properties.getApiKey())); } protected void clearAll(String uidPrefix) { diff --git a/api/src/test/java/vook/server/api/testhelper/ApiTest.java b/api/src/test/java/vook/server/api/testhelper/ApiTest.java index 6616f0eb..689c31d0 100644 --- a/api/src/test/java/vook/server/api/testhelper/ApiTest.java +++ b/api/src/test/java/vook/server/api/testhelper/ApiTest.java @@ -5,7 +5,10 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.testcontainers.containers.MariaDBContainer; +import vook.server.api.outbound.search.MeilisearchProperties; import java.util.Map; import java.util.TimeZone; @@ -19,19 +22,34 @@ public abstract class ApiTest { protected TestRestTemplate rest; @ServiceConnection - protected static final MariaDBContainer mariaDBContainer = new MariaDBContainer<>("mariadb:10.11") + protected static final MariaDBContainer mariaDBContainer = new MariaDBContainer<>("mariadb:10.11") .withDatabaseName("example") .withUsername("user") .withPassword("userPw") .withConfigurationOverride("db/conf") .withTmpFs(Map.of("/var/lib/mysql", "rw")); + protected static final MeilisearchContainer meilisearchContainer = new MeilisearchContainer("getmeili/meilisearch:v1.8.0"); + static { mariaDBContainer.start(); + meilisearchContainer.start(); } @BeforeEach void init() { TimeZone.setDefault(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); } + + @Configuration + public static class TestConfig { + + @Bean + public MeilisearchProperties meilisearchProperties() { + MeilisearchProperties meilisearchProperties = new MeilisearchProperties(); + meilisearchProperties.setHost(meilisearchContainer.getHostUrl()); + meilisearchProperties.setApiKey(meilisearchContainer.getMasterKey()); + return meilisearchProperties; + } + } } diff --git a/api/src/test/java/vook/server/api/testhelper/MeilisearchContainer.java b/api/src/test/java/vook/server/api/testhelper/MeilisearchContainer.java new file mode 100644 index 00000000..a2c36e69 --- /dev/null +++ b/api/src/test/java/vook/server/api/testhelper/MeilisearchContainer.java @@ -0,0 +1,34 @@ +package vook.server.api.testhelper; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +public class MeilisearchContainer extends GenericContainer { + + private static final String DEFAULT_IMAGE_NAME = "getmeili/meilisearch:latest"; + + public MeilisearchContainer() { + this(DEFAULT_IMAGE_NAME); + } + + public MeilisearchContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public MeilisearchContainer(DockerImageName dockerImageName) { + super(dockerImageName); + this.addExposedPort(7700); + this.addEnv("MEILI_MASTER_KEY", "aMasterKey"); + } + + public String getMasterKey() { + return this.getEnvMap().get("MEILI_MASTER_KEY"); + } + + /** + * Meilisearch에 접속하기 위한 URL을 반환한다. + */ + public String getHostUrl() { + return "http://" + getHost() + ":" + getMappedPort(7700); + } +} diff --git a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java new file mode 100644 index 00000000..adf50091 --- /dev/null +++ b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java @@ -0,0 +1,62 @@ +package vook.server.api.web.routes.demo; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.demo.repo.DemoTermRepository; +import vook.server.api.devhelper.TestTermsLoader; +import vook.server.api.model.demo.DemoTerm; +import vook.server.api.outbound.search.DemoTermSearchService; +import vook.server.api.testhelper.ApiTest; +import vook.server.api.web.routes.demo.reqres.SearchTermRequest; +import vook.server.api.web.routes.demo.reqres.SearchTermResponse; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DemoWebServiceTest extends ApiTest { + + @Autowired + private DemoWebService demoWebService; + + @Autowired + private TestTermsLoader testTermsLoader; + @Autowired + private DemoTermRepository demoTermRepository; + @Autowired + private DemoTermSearchService demoTermSearchService; + + @BeforeAll + void beforeAll() { + List terms = testTermsLoader.getTerms("classpath:init/개발.tsv"); + demoTermRepository.saveAll(terms); + demoTermSearchService.init(); + demoTermSearchService.addTerms(terms); + } + + @AfterAll + void afterAll() { + demoTermSearchService.clearAll(); + } + + @Test + void searchTerm() { + SearchTermRequest searchTermRequest = new SearchTermRequest(); + searchTermRequest.setQuery("하이브리드앱"); + searchTermRequest.setWithFormat(false); + searchTermRequest.setHighlightPreTag(""); + searchTermRequest.setHighlightPostTag(""); + + SearchTermResponse searchTermResponse = demoWebService.searchTerm(searchTermRequest); + + assertThat(searchTermResponse).isNotNull(); + assertThat(searchTermResponse.getQuery()).isEqualTo("하이브리드앱"); + assertThat(searchTermResponse.getHits()).isNotEmpty(); + } +} diff --git a/api/src/test/resources/application.yml b/api/src/test/resources/application.yml index 9f47bafc..b8191a6f 100644 --- a/api/src/test/resources/application.yml +++ b/api/src/test/resources/application.yml @@ -30,9 +30,6 @@ logging: orm: jdbc: TRACE service: - meilisearch: - host: http://localhost:7700 - apiKey: aSampleMasterKey jwt: secret: vmfhaltmskdlstkfkdgodyroqkfwkdba oauth2: From 6cfa306700b091bbbf7ff7e0de10b8001b9ac95c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 4 Jun 2024 14:07:11 +0900 Subject: [PATCH 090/273] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20API=20=EC=82=AD=EC=A0=9C=20#62?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/web/routes/user/UserApi.java | 23 -------------- .../web/routes/user/UserRestController.java | 9 ------ .../api/web/routes/user/UserWebService.java | 7 ----- .../routes/user/reqres/UserTermsResponse.java | 30 ------------------- 4 files changed, 69 deletions(-) delete mode 100644 api/src/main/java/vook/server/api/web/routes/user/reqres/UserTermsResponse.java diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 5df235d3..48dd6468 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -12,9 +12,6 @@ import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; -import vook.server.api.web.routes.user.reqres.UserTermsResponse; - -import java.util.List; @Tag(name = "user", description = "사용자 관련 API") public interface UserApi { @@ -39,26 +36,6 @@ public interface UserApi { class UserApiUerInfoResponse extends CommonApiResponse { } - @Operation( - summary = "약관 목록", - security = { - @SecurityRequirement(name = "AccessToken") - } - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "성공", - content = @Content( - schema = @Schema(implementation = UserApiTermsResponse.class) - ) - ), - }) - CommonApiResponse> terms(); - - class UserApiTermsResponse extends CommonApiResponse> { - } - @Operation( summary = "회원가입", security = { diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index de6639cb..e4382cf9 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -9,9 +9,6 @@ import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; -import vook.server.api.web.routes.user.reqres.UserTermsResponse; - -import java.util.List; @Slf4j @RestController @@ -30,12 +27,6 @@ public CommonApiResponse userInfo( return CommonApiResponse.okWithResult(response); } - @Override - @GetMapping("/terms") - public CommonApiResponse> terms() { - return CommonApiResponse.okWithResult(service.terms()); - } - @Override @PostMapping("/register") public CommonApiResponse register( diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 7567a879..3813dd87 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -6,12 +6,10 @@ import vook.server.api.app.terms.TermsService; import vook.server.api.app.user.UserService; import vook.server.api.config.auth.common.VookLoginUser; -import vook.server.api.model.terms.Terms; import vook.server.api.model.user.User; import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; -import vook.server.api.web.routes.user.reqres.UserTermsResponse; import java.util.List; @@ -28,11 +26,6 @@ public UserInfoResponse userInfo(VookLoginUser loginUser) { return UserInfoResponse.from(user); } - public List terms() { - List terms = termsService.findAll(); - return UserTermsResponse.from(terms); - } - public void register(VookLoginUser loginUser, UserRegisterRequest request) { List agreeTermsId = request.getAgreeTermsId(); if (!termsService.includeAllRequiredTerms(agreeTermsId)) { diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserTermsResponse.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserTermsResponse.java deleted file mode 100644 index 93c58e72..00000000 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserTermsResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package vook.server.api.web.routes.user.reqres; - -import lombok.Getter; -import vook.server.api.model.terms.Terms; - -import java.util.List; - -@Getter -public class UserTermsResponse { - - private Long id; - private String title; - private String content; - private Boolean required; - - public static List from(List terms) { - return terms.stream() - .map(UserTermsResponse::from) - .toList(); - } - - public static UserTermsResponse from(Terms terms) { - UserTermsResponse response = new UserTermsResponse(); - response.id = terms.getId(); - response.title = terms.getTitle(); - response.content = terms.getContent(); - response.required = terms.getRequired(); - return response; - } -} From be7248d0d9458289496fa6d71944046828d367e5 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 4 Jun 2024 14:19:40 +0900 Subject: [PATCH 091/273] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C=20#62?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/app/terms/TermsService.java | 31 ------------ .../api/app/terms/repo/TermsRepository.java | 10 ---- .../vook/server/api/app/user/UserService.java | 15 ++---- .../api/app/user/data/RegisterCommand.java | 25 ++-------- .../user/repo/UserTermsAgreeRepository.java | 7 --- .../server/api/devhelper/InitService.java | 24 ---------- .../vook/server/api/model/terms/Terms.java | 33 ------------- .../java/vook/server/api/model/user/User.java | 7 --- .../vook/server/api/model/user/UserInfo.java | 6 ++- .../server/api/model/user/UserTermsAgree.java | 37 --------------- .../api/web/routes/user/UserWebService.java | 10 +--- .../user/reqres/UserRegisterRequest.java | 47 ++----------------- 12 files changed, 18 insertions(+), 234 deletions(-) delete mode 100644 api/src/main/java/vook/server/api/app/terms/TermsService.java delete mode 100644 api/src/main/java/vook/server/api/app/terms/repo/TermsRepository.java delete mode 100644 api/src/main/java/vook/server/api/app/user/repo/UserTermsAgreeRepository.java delete mode 100644 api/src/main/java/vook/server/api/model/terms/Terms.java delete mode 100644 api/src/main/java/vook/server/api/model/user/UserTermsAgree.java diff --git a/api/src/main/java/vook/server/api/app/terms/TermsService.java b/api/src/main/java/vook/server/api/app/terms/TermsService.java deleted file mode 100644 index 5cd87a56..00000000 --- a/api/src/main/java/vook/server/api/app/terms/TermsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package vook.server.api.app.terms; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import vook.server.api.app.terms.repo.TermsRepository; -import vook.server.api.model.terms.Terms; - -import java.util.HashSet; -import java.util.List; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class TermsService { - - private final TermsRepository repository; - - public List findAll() { - return repository.findAll(); - } - - public Optional find(Long id) { - return repository.findById(id); - } - - public boolean includeAllRequiredTerms(List agreeTermsId) { - List requiredTerms = repository.findAllByRequired(true); - List requiredTermsId = requiredTerms.stream().map(Terms::getId).toList(); - return new HashSet<>(agreeTermsId).containsAll(requiredTermsId); - } -} diff --git a/api/src/main/java/vook/server/api/app/terms/repo/TermsRepository.java b/api/src/main/java/vook/server/api/app/terms/repo/TermsRepository.java deleted file mode 100644 index 512c594f..00000000 --- a/api/src/main/java/vook/server/api/app/terms/repo/TermsRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package vook.server.api.app.terms.repo; - -import org.springframework.data.jpa.repository.JpaRepository; -import vook.server.api.model.terms.Terms; - -import java.util.List; - -public interface TermsRepository extends JpaRepository { - List findAllByRequired(Boolean required); -} diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 152d8e10..6ec626ef 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -8,11 +8,9 @@ import vook.server.api.app.user.repo.SocialUserRepository; import vook.server.api.app.user.repo.UserInfoRepository; import vook.server.api.app.user.repo.UserRepository; -import vook.server.api.app.user.repo.UserTermsAgreeRepository; import vook.server.api.model.user.SocialUser; import vook.server.api.model.user.User; import vook.server.api.model.user.UserInfo; -import vook.server.api.model.user.UserTermsAgree; import java.util.Optional; @@ -23,7 +21,6 @@ public class UserService { private final UserRepository repository; private final SocialUserRepository socialUserRepository; private final UserInfoRepository userInfoRepository; - private final UserTermsAgreeRepository userTermsAgreeRepository; public Optional findByProvider(String provider, String providerUserId) { return socialUserRepository.findByProviderAndProviderUserId(provider, providerUserId); @@ -47,16 +44,14 @@ public Optional findByUid(String uid) { public void register(RegisterCommand command) { User user = repository.findByUid(command.getUserUid()).orElseThrow(); - UserInfo userInfo = UserInfo.forRegisterOf(command.getNickname(), user); + UserInfo userInfo = UserInfo.forRegisterOf( + command.getNickname(), + user, + command.isMarketingEmailOptIn() + ); UserInfo savedUserInfo = userInfoRepository.save(userInfo); user.addUserInfo(savedUserInfo); - for (RegisterCommand.TermsAgree termsAgree : command.getTermsAgrees()) { - UserTermsAgree userTermsAgree = UserTermsAgree.of(user, termsAgree.getTerms(), termsAgree.getAgree()); - UserTermsAgree savedUserTermsAgree = userTermsAgreeRepository.save(userTermsAgree); - user.addUserTermsAgree(savedUserTermsAgree); - } - user.registered(); } diff --git a/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java b/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java index e76e7b12..e1fe00e5 100644 --- a/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java +++ b/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java @@ -1,42 +1,23 @@ package vook.server.api.app.user.data; import lombok.Getter; -import vook.server.api.model.terms.Terms; - -import java.util.List; @Getter public class RegisterCommand { private String userUid; private String nickname; - private List termsAgrees; + private boolean marketingEmailOptIn; public static RegisterCommand of( String userUid, String nickname, - List termsAgrees + boolean marketingEmailOptIn ) { RegisterCommand command = new RegisterCommand(); command.userUid = userUid; command.nickname = nickname; - command.termsAgrees = termsAgrees; + command.marketingEmailOptIn = marketingEmailOptIn; return command; } - - @Getter - public static class TermsAgree { - private Terms terms; - private Boolean agree; - - public static TermsAgree of( - Terms terms, - Boolean agree - ) { - TermsAgree result = new TermsAgree(); - result.terms = terms; - result.agree = agree; - return result; - } - } } diff --git a/api/src/main/java/vook/server/api/app/user/repo/UserTermsAgreeRepository.java b/api/src/main/java/vook/server/api/app/user/repo/UserTermsAgreeRepository.java deleted file mode 100644 index bf212f74..00000000 --- a/api/src/main/java/vook/server/api/app/user/repo/UserTermsAgreeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package vook.server.api.app.user.repo; - -import org.springframework.data.jpa.repository.JpaRepository; -import vook.server.api.model.user.UserTermsAgree; - -public interface UserTermsAgreeRepository extends JpaRepository { -} diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/InitService.java index 76bd72c5..bc0f1683 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/InitService.java @@ -6,17 +6,12 @@ import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.demo.repo.DemoTermRepository; import vook.server.api.app.demo.repo.DemoTermSynonymRepository; -import vook.server.api.app.terms.repo.TermsRepository; import vook.server.api.app.user.repo.SocialUserRepository; import vook.server.api.app.user.repo.UserInfoRepository; import vook.server.api.app.user.repo.UserRepository; -import vook.server.api.app.user.repo.UserTermsAgreeRepository; import vook.server.api.model.demo.DemoTerm; -import vook.server.api.model.terms.Terms; import vook.server.api.outbound.search.DemoTermSearchService; -import java.io.IOException; -import java.io.InputStream; import java.util.List; @Service @@ -28,21 +23,15 @@ public class InitService { private final DemoTermRepository demoTermRepository; private final DemoTermSynonymRepository demoTermSynonymRepository; - private final UserTermsAgreeRepository userTermsAgreeRepository; private final UserInfoRepository userInfoRepository; private final SocialUserRepository socialUserRepository; private final UserRepository userRepository; - private final TermsRepository termsRepository; private final DemoTermSearchService searchService; private final TestTermsLoader testTermsLoader; public void init() { deleteAll(); - termsRepository.save(Terms.of("이용약관", loadContents("classpath:init/이용약관.txt"), true)); - termsRepository.save(Terms.of("개인정보 수집 이용 약관", loadContents("classpath:init/개인정보_수집_이용_약관.txt"), true)); - termsRepository.save(Terms.of("마케팅 메일 수신 약관", loadContents("classpath:init/마케팅_메일_수신_약관.txt"), false)); - List devTerms = testTermsLoader.getTerms("classpath:init/개발.tsv"); demoTermRepository.saveAll(devTerms); @@ -56,24 +45,11 @@ private void deleteAll() { demoTermRepository.deleteAllInBatch(); // 사용자 - userTermsAgreeRepository.deleteAllInBatch(); userInfoRepository.deleteAllInBatch(); socialUserRepository.deleteAllInBatch(); userRepository.deleteAllInBatch(); - // 약관 - termsRepository.deleteAllInBatch(); - // 검색 엔진 searchService.clearAll(); } - - private String loadContents(String location) { - try { - InputStream inputStream = resourceLoader.getResource(location).getInputStream(); - return new String(inputStream.readAllBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } } diff --git a/api/src/main/java/vook/server/api/model/terms/Terms.java b/api/src/main/java/vook/server/api/model/terms/Terms.java deleted file mode 100644 index ed2f3fa1..00000000 --- a/api/src/main/java/vook/server/api/model/terms/Terms.java +++ /dev/null @@ -1,33 +0,0 @@ -package vook.server.api.model.terms; - -import jakarta.persistence.*; -import lombok.Getter; - -@Getter -@Entity -@Table(name = "terms") -public class Terms { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String title; - - @Column(columnDefinition = "TEXT") - private String content; - - private Boolean required; - - public static Terms of( - String title, - String content, - Boolean required - ) { - Terms result = new Terms(); - result.title = title; - result.content = content; - result.required = required; - return result; - } -} diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 59d12e44..d02d893f 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -32,9 +32,6 @@ public class User { @OneToOne(mappedBy = "user") private UserInfo userInfo; - @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) - private List userTermsAgrees = new ArrayList<>(); - public static User forSignUpFromSocialOf( String email ) { @@ -53,10 +50,6 @@ public void addUserInfo(UserInfo userInfo) { this.userInfo = userInfo; } - public void addUserTermsAgree(UserTermsAgree userTermsAgree) { - userTermsAgrees.add(userTermsAgree); - } - public void registered() { this.status = UserStatus.REGISTERED; } diff --git a/api/src/main/java/vook/server/api/model/user/UserInfo.java b/api/src/main/java/vook/server/api/model/user/UserInfo.java index 16fccb89..d0bffae2 100644 --- a/api/src/main/java/vook/server/api/model/user/UserInfo.java +++ b/api/src/main/java/vook/server/api/model/user/UserInfo.java @@ -14,6 +14,8 @@ public class UserInfo { private String nickname; + private Boolean marketingEmailOptIn; + private String funnel; private String job; @@ -24,11 +26,13 @@ public class UserInfo { public static UserInfo forRegisterOf( String nickname, - User user + User user, + boolean marketingEmailOptIn ) { UserInfo result = new UserInfo(); result.nickname = nickname; result.user = user; + result.marketingEmailOptIn = marketingEmailOptIn; return result; } diff --git a/api/src/main/java/vook/server/api/model/user/UserTermsAgree.java b/api/src/main/java/vook/server/api/model/user/UserTermsAgree.java deleted file mode 100644 index b2ee9780..00000000 --- a/api/src/main/java/vook/server/api/model/user/UserTermsAgree.java +++ /dev/null @@ -1,37 +0,0 @@ -package vook.server.api.model.user; - -import jakarta.persistence.*; -import lombok.Getter; -import vook.server.api.model.terms.Terms; - -@Getter -@Entity -@Table(name = "user_terms_agree") -public class UserTermsAgree { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "user_id") - private User user; - - @ManyToOne - @JoinColumn(name = "terms_id") - private Terms terms; - - private Boolean agree; - - public static UserTermsAgree of( - User user, - Terms terms, - Boolean agree - ) { - UserTermsAgree result = new UserTermsAgree(); - result.user = user; - result.terms = terms; - result.agree = agree; - return result; - } -} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 3813dd87..ff02735b 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -3,7 +3,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.terms.TermsService; import vook.server.api.app.user.UserService; import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.model.user.User; @@ -11,15 +10,12 @@ import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; -import java.util.List; - @Service @Transactional @RequiredArgsConstructor public class UserWebService { private final UserService userService; - private final TermsService termsService; public UserInfoResponse userInfo(VookLoginUser loginUser) { User user = userService.findByUid(loginUser.getUid()).orElseThrow(); @@ -27,11 +23,7 @@ public UserInfoResponse userInfo(VookLoginUser loginUser) { } public void register(VookLoginUser loginUser, UserRegisterRequest request) { - List agreeTermsId = request.getAgreeTermsId(); - if (!termsService.includeAllRequiredTerms(agreeTermsId)) { - throw new IllegalArgumentException("동의 필수인 약관이 누락되었습니다."); - } - userService.register(request.toCommand(loginUser.getUid(), termsService::find)); + userService.register(request.toCommand(loginUser.getUid())); } public void onboardingComplete(VookLoginUser loginUser, UserOnboardingCompleteRequest request) { diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java index 17bb1060..02d84fe2 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java @@ -1,59 +1,20 @@ package vook.server.api.web.routes.user.reqres; -import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import vook.server.api.app.user.data.RegisterCommand; -import vook.server.api.model.terms.Terms; - -import java.util.List; -import java.util.Optional; @Data public class UserRegisterRequest { - @Schema private String nickname; + private boolean requiredTermsAgree; + private boolean marketingEmailOptIn; - @Schema - private List termsAgrees; - - public RegisterCommand toCommand(String userUid, TermsFinder termsFinder) { + public RegisterCommand toCommand(String userUid) { return RegisterCommand.of( userUid, nickname, - termsAgrees.stream().map(termsAgree -> { - Terms terms = termsFinder.find(termsAgree.id).orElseThrow(); - return RegisterCommand.TermsAgree.of(terms, termsAgree.agree); - }).toList() + marketingEmailOptIn ); } - - @FunctionalInterface - public interface TermsFinder { - Optional find(Long id); - } - - @Schema(hidden = true) - public List getAgreeTermsId() { - return termsAgrees.stream() - .filter(t -> t.agree) - .map(t -> t.id) - .toList(); - } - - @Data - public static class TermsAgree { - private Long id; - private Boolean agree; - - public static TermsAgree of( - Long id, - Boolean agree - ) { - TermsAgree result = new TermsAgree(); - result.id = id; - result.agree = agree; - return result; - } - } } From 6b2a854146840fbf5d52479d97a44aac96498236 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 4 Jun 2024 14:58:57 +0900 Subject: [PATCH 092/273] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/vook/server/api/model/user/User.java | 1 + .../api/testhelper/HttpEntityBuilder.java | 18 +++ ...{ApiTest.java => IntegrationTestBase.java} | 7 +- .../api/testhelper/TestDataCreator.java | 45 +++++++ .../server/api/testhelper/WebApiTest.java | 10 ++ .../server/api/testhelper/WebServiceTest.java | 7 + .../web/routes/demo/DemoWebServiceTest.java | 4 +- .../routes/health/HealthControllerTest.java | 4 +- .../routes/user/UserRestControllerTest.java | 44 ++++++ .../web/routes/user/UserWebServiceTest.java | 126 ++++++++++++++++++ 10 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 api/src/test/java/vook/server/api/testhelper/HttpEntityBuilder.java rename api/src/test/java/vook/server/api/testhelper/{ApiTest.java => IntegrationTestBase.java} (89%) create mode 100644 api/src/test/java/vook/server/api/testhelper/TestDataCreator.java create mode 100644 api/src/test/java/vook/server/api/testhelper/WebApiTest.java create mode 100644 api/src/test/java/vook/server/api/testhelper/WebServiceTest.java create mode 100644 api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java create mode 100644 api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index d02d893f..cfa454b7 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -19,6 +19,7 @@ public class User { private String uid; + @Column(unique = true) private String email; @Enumerated(EnumType.STRING) diff --git a/api/src/test/java/vook/server/api/testhelper/HttpEntityBuilder.java b/api/src/test/java/vook/server/api/testhelper/HttpEntityBuilder.java new file mode 100644 index 00000000..b7d5842b --- /dev/null +++ b/api/src/test/java/vook/server/api/testhelper/HttpEntityBuilder.java @@ -0,0 +1,18 @@ +package vook.server.api.testhelper; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; + +public class HttpEntityBuilder { + + private final HttpHeaders headers = new HttpHeaders(); + + public HttpEntityBuilder addHeader(String key, String value) { + headers.add(key, value); + return this; + } + + public HttpEntity build() { + return new HttpEntity<>(headers); + } +} diff --git a/api/src/test/java/vook/server/api/testhelper/ApiTest.java b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java similarity index 89% rename from api/src/test/java/vook/server/api/testhelper/ApiTest.java rename to api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java index 689c31d0..44e6479a 100644 --- a/api/src/test/java/vook/server/api/testhelper/ApiTest.java +++ b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java @@ -1,9 +1,7 @@ package vook.server.api.testhelper; import org.junit.jupiter.api.BeforeEach; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -16,10 +14,7 @@ import static vook.server.api.config.TimeZoneConfig.DEFAULT_TIME_ZONE; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public abstract class ApiTest { - - @Autowired - protected TestRestTemplate rest; +public abstract class IntegrationTestBase { @ServiceConnection protected static final MariaDBContainer mariaDBContainer = new MariaDBContainer<>("mariadb:10.11") diff --git a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java new file mode 100644 index 00000000..7d9f3125 --- /dev/null +++ b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java @@ -0,0 +1,45 @@ +package vook.server.api.testhelper; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.auth.TokenService; +import vook.server.api.app.auth.data.GeneratedToken; +import vook.server.api.app.user.UserService; +import vook.server.api.app.user.data.CompleteOnboardingCommand; +import vook.server.api.app.user.data.RegisterCommand; +import vook.server.api.app.user.data.SignUpFromSocialCommand; +import vook.server.api.model.user.SocialUser; +import vook.server.api.model.user.User; + +@Component +@Transactional +@RequiredArgsConstructor +public class TestDataCreator { + + private final UserService userService; + private final TokenService tokenService; + + public User createUnregisteredUser() { + SocialUser user = userService.signUpFromSocial( + SignUpFromSocialCommand.of("testProvider", "testProviderUserId", "testEmail@test.com") + ); + return user.getUser(); + } + + public User createRegisteredUser() { + User user = createUnregisteredUser(); + userService.register(RegisterCommand.of(user.getUid(), "testNickname", true)); + return userService.findByUid(user.getUid()).orElseThrow(); + } + + public User createCompletedOnboardingUser() { + User user = createRegisteredUser(); + userService.completeOnboarding(CompleteOnboardingCommand.of(user.getUid(), "testFunnel", "testJob")); + return userService.findByUid(user.getUid()).orElseThrow(); + } + + public GeneratedToken createToken(User user) { + return tokenService.generateToken(user.getUid()); + } +} diff --git a/api/src/test/java/vook/server/api/testhelper/WebApiTest.java b/api/src/test/java/vook/server/api/testhelper/WebApiTest.java new file mode 100644 index 00000000..d8abc27f --- /dev/null +++ b/api/src/test/java/vook/server/api/testhelper/WebApiTest.java @@ -0,0 +1,10 @@ +package vook.server.api.testhelper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; + +public abstract class WebApiTest extends IntegrationTestBase { + + @Autowired + protected TestRestTemplate rest; +} diff --git a/api/src/test/java/vook/server/api/testhelper/WebServiceTest.java b/api/src/test/java/vook/server/api/testhelper/WebServiceTest.java new file mode 100644 index 00000000..d223b7d7 --- /dev/null +++ b/api/src/test/java/vook/server/api/testhelper/WebServiceTest.java @@ -0,0 +1,7 @@ +package vook.server.api.testhelper; + +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public abstract class WebServiceTest extends IntegrationTestBase { +} diff --git a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java index adf50091..c955ab11 100644 --- a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java @@ -10,7 +10,7 @@ import vook.server.api.devhelper.TestTermsLoader; import vook.server.api.model.demo.DemoTerm; import vook.server.api.outbound.search.DemoTermSearchService; -import vook.server.api.testhelper.ApiTest; +import vook.server.api.testhelper.IntegrationTestBase; import vook.server.api.web.routes.demo.reqres.SearchTermRequest; import vook.server.api.web.routes.demo.reqres.SearchTermResponse; @@ -20,7 +20,7 @@ @Transactional @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class DemoWebServiceTest extends ApiTest { +class DemoWebServiceTest extends IntegrationTestBase { @Autowired private DemoWebService demoWebService; diff --git a/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java b/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java index 950e5119..5dd285d2 100644 --- a/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java @@ -4,11 +4,11 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import vook.server.api.testhelper.ApiTest; +import vook.server.api.testhelper.WebApiTest; import static org.assertj.core.api.Assertions.assertThat; -class HealthControllerTest extends ApiTest { +class HealthControllerTest extends WebApiTest { @Test @DisplayName("헬스체크") diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java new file mode 100644 index 00000000..677f683a --- /dev/null +++ b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java @@ -0,0 +1,44 @@ +package vook.server.api.web.routes.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import vook.server.api.app.auth.data.GeneratedToken; +import vook.server.api.model.user.User; +import vook.server.api.model.user.UserStatus; +import vook.server.api.testhelper.HttpEntityBuilder; +import vook.server.api.testhelper.TestDataCreator; +import vook.server.api.testhelper.WebApiTest; + +import static org.assertj.core.api.Assertions.assertThat; + +class UserRestControllerTest extends WebApiTest { + + @Autowired + TestDataCreator testDataCreator; + + @Test + void userInfo() { + // given + User unregisteredUser = testDataCreator.createUnregisteredUser(); + GeneratedToken token = testDataCreator.createToken(unregisteredUser); + + // when + var res = rest.exchange( + "/user/info", + HttpMethod.GET, + new HttpEntityBuilder() + .addHeader("Authorization", "Bearer " + token.getAccessToken()) + .build(), + UserApi.UserApiUerInfoResponse.class + ); + + // then + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(res.getBody()).isNotNull(); + assertThat(res.getBody().getResult().getUid()).isEqualTo(unregisteredUser.getUid()); + assertThat(res.getBody().getResult().getEmail()).isEqualTo(unregisteredUser.getEmail()); + assertThat(res.getBody().getResult().getStatus()).isEqualTo(UserStatus.SOCIAL_LOGIN_COMPLETED); + } +} diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java new file mode 100644 index 00000000..daa3a691 --- /dev/null +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -0,0 +1,126 @@ +package vook.server.api.web.routes.user; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import vook.server.api.app.user.UserService; +import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.model.user.User; +import vook.server.api.model.user.UserStatus; +import vook.server.api.testhelper.TestDataCreator; +import vook.server.api.testhelper.WebServiceTest; +import vook.server.api.web.routes.user.reqres.UserInfoResponse; +import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; +import vook.server.api.web.routes.user.reqres.UserRegisterRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +class UserWebServiceTest extends WebServiceTest { + + @Autowired + UserWebService userWebService; + + @Autowired + TestDataCreator testDataCreator; + @Autowired + UserService userService; + + @Test + @DisplayName("사용자 정보 조회 - 정상; 회원가입 전 사용자") + void userInfo1() { + // given + User unregisteredUser = testDataCreator.createUnregisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); + + // when + UserInfoResponse response = userWebService.userInfo(vookLoginUser); + + // then + assertThat(response).isNotNull(); + assertThat(response.getUid()).isEqualTo(unregisteredUser.getUid()); + assertThat(response.getEmail()).isEqualTo(unregisteredUser.getEmail()); + assertThat(response.getNickname()).isNull(); + assertThat(response.getStatus()).isEqualTo(UserStatus.SOCIAL_LOGIN_COMPLETED); + } + + @Test + @DisplayName("사용자 정보 조회 - 정상; 회원가입 후 사용자") + void userInfo2() { + // given + User registeredUser = testDataCreator.createRegisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); + + // when + UserInfoResponse response = userWebService.userInfo(vookLoginUser); + + // then + assertThat(response).isNotNull(); + assertThat(response.getUid()).isEqualTo(registeredUser.getUid()); + assertThat(response.getEmail()).isEqualTo(registeredUser.getEmail()); + assertThat(response.getNickname()).isEqualTo(registeredUser.getUserInfo().getNickname()); + assertThat(response.getStatus()).isEqualTo(UserStatus.REGISTERED); + } + + @Test + @DisplayName("사용자 정보 조회 - 정상; 온보딩 완료 사용자") + void userInfo3() { + // given + User completedOnboardingUser = testDataCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(completedOnboardingUser.getUid()); + + // when + UserInfoResponse response = userWebService.userInfo(vookLoginUser); + + // then + assertThat(response).isNotNull(); + assertThat(response.getUid()).isEqualTo(completedOnboardingUser.getUid()); + assertThat(response.getEmail()).isEqualTo(completedOnboardingUser.getEmail()); + assertThat(response.getNickname()).isEqualTo(completedOnboardingUser.getUserInfo().getNickname()); + assertThat(response.getStatus()).isEqualTo(UserStatus.ONBOARDING_COMPLETED); + } + + @Test + @DisplayName("회원 가입 - 정상") + void register1() { + // given + User unregisteredUser = testDataCreator.createUnregisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); + + UserRegisterRequest request = new UserRegisterRequest(); + request.setNickname("nickname"); + request.setRequiredTermsAgree(true); + request.setMarketingEmailOptIn(true); + + // when + userWebService.register(vookLoginUser, request); + + // then + User user = userService.findByUid(unregisteredUser.getUid()).orElseThrow(); + assertThat(user.getStatus()).isEqualTo(UserStatus.REGISTERED); + assertThat(user.getUserInfo()).isNotNull(); + assertThat(user.getUserInfo().getNickname()).isEqualTo(request.getNickname()); + assertThat(user.getUserInfo().getMarketingEmailOptIn()).isEqualTo(request.isMarketingEmailOptIn()); + } + + @Test + @DisplayName("온보딩 완료 - 정상") + void onboardingComplete1() { + // given + User registeredUser = testDataCreator.createRegisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); + + UserOnboardingCompleteRequest request = new UserOnboardingCompleteRequest(); + request.setFunnel("testFunnel"); + request.setJob("testJob"); + + // when + userWebService.onboardingComplete(vookLoginUser, request); + + // then + User user = userService.findByUid(registeredUser.getUid()).orElseThrow(); + assertThat(user.getStatus()).isEqualTo(UserStatus.ONBOARDING_COMPLETED); + assertThat(user.getUserInfo()).isNotNull(); + assertThat(user.getUserInfo().getFunnel()).isEqualTo(request.getFunnel()); + assertThat(user.getUserInfo().getJob()).isEqualTo(request.getJob()); + } +} From 2433b24f91c180cbad2a74133b597885e321384c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 5 Jun 2024 20:43:56 +0900 Subject: [PATCH 093/273] chore: dagger version up 0.11.4 -> 0.11.6 --- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/deploy-stag.yml | 2 +- cicd/dagger.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index fed2eb59..ce183eb7 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -24,7 +24,7 @@ jobs: SSH_DEST: ${{ secrets.DEV_SSH_DEST }} SSH_KEY: ${{ secrets.DEV_SSH_KEY }} with: - version: 0.11.4 + version: 0.11.6 verb: call module: ./cicd args: >- diff --git a/.github/workflows/deploy-stag.yml b/.github/workflows/deploy-stag.yml index c9ed2ae4..ffad63f9 100644 --- a/.github/workflows/deploy-stag.yml +++ b/.github/workflows/deploy-stag.yml @@ -24,7 +24,7 @@ jobs: SSH_DEST: ${{ secrets.STAG_SSH_DEST }} SSH_KEY: ${{ secrets.STAG_SSH_KEY }} with: - version: 0.11.4 + version: 0.11.6 verb: call module: ./cicd args: >- diff --git a/cicd/dagger.json b/cicd/dagger.json index d06cab5c..12deb9a4 100644 --- a/cicd/dagger.json +++ b/cicd/dagger.json @@ -24,5 +24,5 @@ } ], "source": "dagger", - "engineVersion": "v0.11.4" + "engineVersion": "v0.11.6" } From 0cb89f9506e136818b03a36e93260344991c887d Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 5 Jun 2024 23:27:20 +0900 Subject: [PATCH 094/273] =?UTF-8?q?chore:=20=EC=95=BD=EA=B4=80=20txt=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20#62?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...47\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" | 1 - ...35\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" | 1 - .../init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" | 1 - 3 files changed, 3 deletions(-) delete mode 100644 "api/src/main/resources/init/\352\260\234\354\235\270\354\240\225\353\263\264_\354\210\230\354\247\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" delete mode 100644 "api/src/main/resources/init/\353\247\210\354\274\200\355\214\205_\353\251\224\354\235\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" delete mode 100644 "api/src/main/resources/init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" diff --git "a/api/src/main/resources/init/\352\260\234\354\235\270\354\240\225\353\263\264_\354\210\230\354\247\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" "b/api/src/main/resources/init/\352\260\234\354\235\270\354\240\225\353\263\264_\354\210\230\354\247\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" deleted file mode 100644 index 592165bd..00000000 --- "a/api/src/main/resources/init/\352\260\234\354\235\270\354\240\225\353\263\264_\354\210\230\354\247\221_\354\235\264\354\232\251_\354\225\275\352\264\200.txt" +++ /dev/null @@ -1 +0,0 @@ -개인정보 수집 이용 약관 내용 diff --git "a/api/src/main/resources/init/\353\247\210\354\274\200\355\214\205_\353\251\224\354\235\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" "b/api/src/main/resources/init/\353\247\210\354\274\200\355\214\205_\353\251\224\354\235\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" deleted file mode 100644 index 7422efbe..00000000 --- "a/api/src/main/resources/init/\353\247\210\354\274\200\355\214\205_\353\251\224\354\235\274_\354\210\230\354\213\240_\354\225\275\352\264\200.txt" +++ /dev/null @@ -1 +0,0 @@ -마케팅 메일 수신 약관 내용 diff --git "a/api/src/main/resources/init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" "b/api/src/main/resources/init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" deleted file mode 100644 index a9814da0..00000000 --- "a/api/src/main/resources/init/\354\235\264\354\232\251\354\225\275\352\264\200.txt" +++ /dev/null @@ -1 +0,0 @@ -이용약관 내용 From 87f7a079b78eca15f6d443632f44c5d7f4fd2e11 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 5 Jun 2024 23:35:01 +0900 Subject: [PATCH 095/273] =?UTF-8?q?feat:=20=EA=B3=B5=ED=86=B5=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EA=B4=80=EB=A0=A8=20=EB=B6=80=EB=B6=84=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/web/common/CommonApiException.java | 18 ++++++- .../common/GlobalRestControllerAdvice.java | 4 +- .../GlobalOperationCustomizerImpl.java | 49 ++++++++++++------- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiException.java b/api/src/main/java/vook/server/api/web/common/CommonApiException.java index 89f74040..17c97b3c 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiException.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiException.java @@ -23,6 +23,9 @@ public Exception(String message, Throwable cause) { } public static class BadRequest extends Exception { + + private static final int STATUS_CODE = 400; + public BadRequest(String message, Throwable cause) { super(message, cause); } @@ -33,18 +36,29 @@ public BadRequest(String message) { @Override public CommonApiResponse response() { - return CommonApiResponse.noResult(400, message); + return CommonApiResponse.noResult(STATUS_CODE, message); } } public static class ServerError extends Exception { + + private static final int STATUS_CODE = 500; + + public ServerError(String message, Throwable cause) { + super(message, cause); + } + public ServerError(Throwable cause) { super(cause); } @Override public CommonApiResponse response() { - return CommonApiResponse.noResult(500, "처리되지 않은 서버 에러가 발생하였습니다."); + return CommonApiResponse.noResult(STATUS_CODE, message); + } + + public CommonApiResponse response(String message) { + return CommonApiResponse.noResult(STATUS_CODE, message); } } } diff --git a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java index ba609631..7d698dfe 100644 --- a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java +++ b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java @@ -22,7 +22,9 @@ public ResponseEntity handleCommonApiException(CommonApiException.Exception e public ResponseEntity handleException(Exception e) { log.error(e.getMessage(), e); - CommonApiResponse response = new CommonApiException.ServerError(e).response(); + CommonApiResponse response = new CommonApiException + .ServerError("처리되지 않은 서버 에러가 발생하였습니다.", e) + .response(); return ResponseEntity.status(response.getCode()).body(response); } diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java index 0449495c..67878d7c 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java @@ -10,7 +10,7 @@ import org.springframework.web.method.HandlerMethod; import vook.server.api.web.common.CommonApiResponse; -import java.util.Map; +import java.util.HashMap; public class GlobalOperationCustomizerImpl implements GlobalOperationCustomizer { @@ -21,21 +21,36 @@ public Operation customize(Operation operation, HandlerMethod handlerMethod) { } private static void applyInternalServerErrorApiResponse(Operation operation) { - ApiResponse internalServerErrorApiResponse = new ApiResponse() - .description("처리되지 않은 서버 에러") - .content(new Content().addMediaType( - "application/json", - new MediaType() - .schema(new Schema().$ref("#/components/schemas/CommonApiResponse")) - .examples(Map.of("서버 에러", new Example().value( - """ - { - "code": 500, - "message": "처리되지 않은 서버 에러가 발생하였습니다." - }""" - ))) - )); - - operation.getResponses().addApiResponse("500", internalServerErrorApiResponse); + MediaType jsonType = prepareOrGetJsonMediaType(operation); + + if (jsonType.getSchema() == null) { + jsonType.setSchema(new Schema().$ref("#/components/schemas/CommonApiResponse")); + } + + jsonType.getExamples().put("처리되지 않은 서버 에러", + new Example() + .description("처리되지 않은 서버 에러") + .value(""" + { + "code": 500, + "message": "처리되지 않은 서버 에러가 발생하였습니다." + }""") + ); + } + + private static MediaType prepareOrGetJsonMediaType(Operation operation) { + ApiResponse apiResponse = operation.getResponses().computeIfAbsent("500", k -> new ApiResponse()); + + if (apiResponse.getContent() == null) { + apiResponse.setContent(new Content()); + } + + MediaType jsonType = apiResponse.getContent().computeIfAbsent("application/json", k -> new MediaType()); + + if (jsonType.getExamples() == null) { + jsonType.setExamples(new HashMap<>()); + } + + return jsonType; } } From a8f3925aad24f68d51b8d7e8a2646ad85ceda346 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 6 Jun 2024 09:57:38 +0900 Subject: [PATCH 096/273] =?UTF-8?q?feat:=20bean=20validator=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81,=20=EA=B8=B0=EB=B0=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle | 5 +- .../api/web/common/CommonApiResponse.java | 12 +++- .../common/GlobalRestControllerAdvice.java | 10 +++ .../web/routes/user/UserRestController.java | 3 +- .../user/reqres/UserRegisterRequest.java | 4 ++ .../swagger/GlobalOpenApiCustomizerImpl.java | 5 +- .../api/testhelper/HttpEntityBuilder.java | 10 ++- .../api/testhelper/IntegrationTestBase.java | 5 ++ .../server/api/testhelper/WebApiTest.java | 10 --- .../server/api/testhelper/WebServiceTest.java | 7 -- .../routes/health/HealthControllerTest.java | 4 +- .../routes/user/UserRestControllerTest.java | 67 ++++++++++++++++--- .../web/routes/user/UserWebServiceTest.java | 6 +- 13 files changed, 111 insertions(+), 37 deletions(-) delete mode 100644 api/src/test/java/vook/server/api/testhelper/WebApiTest.java delete mode 100644 api/src/test/java/vook/server/api/testhelper/WebServiceTest.java diff --git a/api/build.gradle b/api/build.gradle index 0b82ab7f..66ed837b 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -27,8 +27,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-validation' developmentOnly 'org.springframework.boot:spring-boot-devtools' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'com.vaadin.external.google', module: 'android-json' + } // lombok compileOnly 'org.projectlombok:lombok' diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java index 39bfb69d..4c74d9bc 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java @@ -8,9 +8,9 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CommonApiResponse { - @Schema(description = "응답 코드") + @Schema(description = "응답 코드", requiredMode = Schema.RequiredMode.REQUIRED) private Integer code; - @Schema(description = "응답 메시지") + @Schema(description = "응답 메시지", requiredMode = Schema.RequiredMode.REQUIRED) private String message; private T result; @@ -30,4 +30,12 @@ public static CommonApiResponse noResult(Integer code, String message) { response.message = message; return response; } + + public static CommonApiResponse withResult(Integer code, String message, T result) { + CommonApiResponse response = new CommonApiResponse<>(); + response.code = code; + response.message = message; + response.result = result; + return response; + } } diff --git a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java index 7d698dfe..32a33722 100644 --- a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java +++ b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -18,6 +19,15 @@ public ResponseEntity handleCommonApiException(CommonApiException.Exception e return ResponseEntity.status(response.getCode()).body(response); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + CommonApiResponse response = new CommonApiException + .BadRequest("요청이 잘못되었습니다.", e) + .response(); + + return ResponseEntity.status(response.getCode()).body(response); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { log.error(e.getMessage(), e); diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index e4382cf9..c463fe4a 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; @@ -31,7 +32,7 @@ public CommonApiResponse userInfo( @PostMapping("/register") public CommonApiResponse register( @AuthenticationPrincipal VookLoginUser user, - @RequestBody UserRegisterRequest request + @Validated @RequestBody UserRegisterRequest request ) { service.register(user, request); return CommonApiResponse.ok(); diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java index 02d84fe2..1eec34c9 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java @@ -1,13 +1,17 @@ package vook.server.api.web.routes.user.reqres; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import vook.server.api.app.user.data.RegisterCommand; @Data public class UserRegisterRequest { + @NotBlank private String nickname; + private boolean requiredTermsAgree; + private boolean marketingEmailOptIn; public RegisterCommand toCommand(String userUid) { diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java index f1cafa0d..81701d43 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java @@ -17,8 +17,9 @@ public void customise(OpenAPI openApi) { private static void applyCommonApiResponseSchema(OpenAPI openApi) { openApi.getComponents() .addSchemas("CommonApiResponse", new Schema>() - .addProperty("code", new IntegerSchema().description("응답 코드")) - .addProperty("message", new StringSchema().description("응답 메시지")) + .addProperty("code", new IntegerSchema().description("응답 코드")).addRequiredItem("code") + .addProperty("message", new StringSchema().description("응답 메시지")).addRequiredItem("message") + .addProperty("result", new StringSchema().description("응답 결과")) ); } } diff --git a/api/src/test/java/vook/server/api/testhelper/HttpEntityBuilder.java b/api/src/test/java/vook/server/api/testhelper/HttpEntityBuilder.java index b7d5842b..6396a655 100644 --- a/api/src/test/java/vook/server/api/testhelper/HttpEntityBuilder.java +++ b/api/src/test/java/vook/server/api/testhelper/HttpEntityBuilder.java @@ -5,14 +5,20 @@ public class HttpEntityBuilder { + private Object body; private final HttpHeaders headers = new HttpHeaders(); - public HttpEntityBuilder addHeader(String key, String value) { + public HttpEntityBuilder header(String key, String value) { headers.add(key, value); return this; } + public HttpEntityBuilder body(Object body) { + this.body = body; + return this; + } + public HttpEntity build() { - return new HttpEntity<>(headers); + return new HttpEntity<>(body, headers); } } diff --git a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java index 44e6479a..2ec5fb7b 100644 --- a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java +++ b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java @@ -1,7 +1,9 @@ package vook.server.api.testhelper; import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,6 +33,9 @@ public abstract class IntegrationTestBase { meilisearchContainer.start(); } + @Autowired + protected TestRestTemplate rest; + @BeforeEach void init() { TimeZone.setDefault(TimeZone.getTimeZone(DEFAULT_TIME_ZONE)); diff --git a/api/src/test/java/vook/server/api/testhelper/WebApiTest.java b/api/src/test/java/vook/server/api/testhelper/WebApiTest.java deleted file mode 100644 index d8abc27f..00000000 --- a/api/src/test/java/vook/server/api/testhelper/WebApiTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package vook.server.api.testhelper; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.web.client.TestRestTemplate; - -public abstract class WebApiTest extends IntegrationTestBase { - - @Autowired - protected TestRestTemplate rest; -} diff --git a/api/src/test/java/vook/server/api/testhelper/WebServiceTest.java b/api/src/test/java/vook/server/api/testhelper/WebServiceTest.java deleted file mode 100644 index d223b7d7..00000000 --- a/api/src/test/java/vook/server/api/testhelper/WebServiceTest.java +++ /dev/null @@ -1,7 +0,0 @@ -package vook.server.api.testhelper; - -import org.springframework.transaction.annotation.Transactional; - -@Transactional -public abstract class WebServiceTest extends IntegrationTestBase { -} diff --git a/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java b/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java index 5dd285d2..cc877b81 100644 --- a/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/health/HealthControllerTest.java @@ -4,11 +4,11 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import vook.server.api.testhelper.WebApiTest; +import vook.server.api.testhelper.IntegrationTestBase; import static org.assertj.core.api.Assertions.assertThat; -class HealthControllerTest extends WebApiTest { +class HealthControllerTest extends IntegrationTestBase { @Test @DisplayName("헬스체크") diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java index 677f683a..97b2a021 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java @@ -1,19 +1,25 @@ package vook.server.api.web.routes.user; +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.http.HttpMethod; import org.springframework.http.HttpStatus; import vook.server.api.app.auth.data.GeneratedToken; import vook.server.api.model.user.User; -import vook.server.api.model.user.UserStatus; import vook.server.api.testhelper.HttpEntityBuilder; +import vook.server.api.testhelper.IntegrationTestBase; import vook.server.api.testhelper.TestDataCreator; -import vook.server.api.testhelper.WebApiTest; + +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -class UserRestControllerTest extends WebApiTest { +class UserRestControllerTest extends IntegrationTestBase { + + @MockBean + UserWebService webService; @Autowired TestDataCreator testDataCreator; @@ -29,16 +35,61 @@ void userInfo() { "/user/info", HttpMethod.GET, new HttpEntityBuilder() - .addHeader("Authorization", "Bearer " + token.getAccessToken()) + .header("Authorization", "Bearer " + token.getAccessToken()) .build(), UserApi.UserApiUerInfoResponse.class ); // then assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(res.getBody()).isNotNull(); - assertThat(res.getBody().getResult().getUid()).isEqualTo(unregisteredUser.getUid()); - assertThat(res.getBody().getResult().getEmail()).isEqualTo(unregisteredUser.getEmail()); - assertThat(res.getBody().getResult().getStatus()).isEqualTo(UserStatus.SOCIAL_LOGIN_COMPLETED); + } + + @Test + @DisplayName("회원 가입 - 정상") + void register() { + // given + User unregisteredUser = testDataCreator.createUnregisteredUser(); + GeneratedToken token = testDataCreator.createToken(unregisteredUser); + + // when + var res = rest.exchange( + "/user/register", + HttpMethod.POST, + new HttpEntityBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .body(Map.of( + "nickname", "testNickname", + "onboardingComplete", true + )) + .build(), + String.class + ); + + // then + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + @DisplayName("회원 가입 - 닉네임 누락") + void registerWithoutNickname() { + // given + User unregisteredUser = testDataCreator.createUnregisteredUser(); + GeneratedToken token = testDataCreator.createToken(unregisteredUser); + + // when + var res = rest.exchange( + "/user/register", + HttpMethod.POST, + new HttpEntityBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .body(Map.of( + "onboardingComplete", true + )) + .build(), + String.class + ); + + // then + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index daa3a691..6ee11ba3 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -3,19 +3,21 @@ 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 vook.server.api.app.user.UserService; import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.model.user.User; import vook.server.api.model.user.UserStatus; +import vook.server.api.testhelper.IntegrationTestBase; import vook.server.api.testhelper.TestDataCreator; -import vook.server.api.testhelper.WebServiceTest; import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; import static org.assertj.core.api.Assertions.assertThat; -class UserWebServiceTest extends WebServiceTest { +@Transactional +class UserWebServiceTest extends IntegrationTestBase { @Autowired UserWebService userWebService; From a8cc031c5c7fe696293a397d565e0ab487328f83 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Thu, 6 Jun 2024 10:24:46 +0900 Subject: [PATCH 097/273] =?UTF-8?q?feat:=20API=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?#65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/web/common/CommonApiException.java | 53 +++++++------------ .../api/web/common/CommonApiResponse.java | 14 ++--- .../common/GlobalRestControllerAdvice.java | 20 ++----- .../swagger/GlobalOpenApiCustomizerImpl.java | 6 +-- .../GlobalOperationCustomizerImpl.java | 3 +- 5 files changed, 33 insertions(+), 63 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiException.java b/api/src/main/java/vook/server/api/web/common/CommonApiException.java index 17c97b3c..596babe3 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiException.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiException.java @@ -2,63 +2,50 @@ public class CommonApiException { public static abstract class Exception extends RuntimeException { - protected String message; + + protected String code; - public Exception(Throwable cause) { - super(cause); - this.message = cause.getMessage(); - } - - public Exception(String message) { - super(message); - this.message = message; - } - - public Exception(String message, Throwable cause) { - super(message, cause); - this.message = message; + public Exception(String code, Throwable cause) { + super(code, cause); + this.code = code; } abstract CommonApiResponse response(); + + abstract int statusCode(); } public static class BadRequest extends Exception { - private static final int STATUS_CODE = 400; - - public BadRequest(String message, Throwable cause) { - super(message, cause); + public BadRequest(String code, Throwable cause) { + super(code, cause); } - public BadRequest(String message) { - super(message); + @Override + public CommonApiResponse response() { + return CommonApiResponse.noResult(code); } @Override - public CommonApiResponse response() { - return CommonApiResponse.noResult(STATUS_CODE, message); + int statusCode() { + return 400; } } public static class ServerError extends Exception { - private static final int STATUS_CODE = 500; - - public ServerError(String message, Throwable cause) { - super(message, cause); - } - - public ServerError(Throwable cause) { - super(cause); + public ServerError(String code, Throwable cause) { + super(code, cause); } @Override public CommonApiResponse response() { - return CommonApiResponse.noResult(STATUS_CODE, message); + return CommonApiResponse.noResult(code); } - public CommonApiResponse response(String message) { - return CommonApiResponse.noResult(STATUS_CODE, message); + @Override + int statusCode() { + return 500; } } } diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java index 4c74d9bc..cc7ee7d9 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java @@ -8,14 +8,12 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CommonApiResponse { - @Schema(description = "응답 코드", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer code; - @Schema(description = "응답 메시지", requiredMode = Schema.RequiredMode.REQUIRED) - private String message; + @Schema(description = "결과 코드", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; private T result; public static CommonApiResponse ok() { - return noResult(200, "API 요청이 성공했습니다."); + return noResult("success"); } public static CommonApiResponse okWithResult(T result) { @@ -24,17 +22,15 @@ public static CommonApiResponse okWithResult(T result) { return response; } - public static CommonApiResponse noResult(Integer code, String message) { + public static CommonApiResponse noResult(String code) { CommonApiResponse response = new CommonApiResponse<>(); response.code = code; - response.message = message; return response; } - public static CommonApiResponse withResult(Integer code, String message, T result) { + public static CommonApiResponse withResult(String code, T result) { CommonApiResponse response = new CommonApiResponse<>(); response.code = code; - response.message = message; response.result = result; return response; } diff --git a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java index 32a33722..e4d86689 100644 --- a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java +++ b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java @@ -13,29 +13,19 @@ public class GlobalRestControllerAdvice { @ExceptionHandler(CommonApiException.Exception.class) public ResponseEntity handleCommonApiException(CommonApiException.Exception e) { log.error(e.getMessage(), e); - - CommonApiResponse response = e.response(); - - return ResponseEntity.status(response.getCode()).body(response); + return ResponseEntity.status(e.statusCode()).body(e.response()); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { - CommonApiResponse response = new CommonApiException - .BadRequest("요청이 잘못되었습니다.", e) - .response(); - - return ResponseEntity.status(response.getCode()).body(response); + CommonApiException.BadRequest badRequest = new CommonApiException.BadRequest("INVALID_PARAMETER", e); + return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response()); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { log.error(e.getMessage(), e); - - CommonApiResponse response = new CommonApiException - .ServerError("처리되지 않은 서버 에러가 발생하였습니다.", e) - .response(); - - return ResponseEntity.status(response.getCode()).body(response); + CommonApiException.ServerError serverError = new CommonApiException.ServerError("UNHANDLED_ERROR", e); + return ResponseEntity.status(serverError.statusCode()).body(serverError.response()); } } diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java index 81701d43..6828eff3 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java @@ -1,7 +1,6 @@ package vook.server.api.web.swagger; import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.media.IntegerSchema; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; import org.springdoc.core.customizers.GlobalOpenApiCustomizer; @@ -17,9 +16,8 @@ public void customise(OpenAPI openApi) { private static void applyCommonApiResponseSchema(OpenAPI openApi) { openApi.getComponents() .addSchemas("CommonApiResponse", new Schema>() - .addProperty("code", new IntegerSchema().description("응답 코드")).addRequiredItem("code") - .addProperty("message", new StringSchema().description("응답 메시지")).addRequiredItem("message") - .addProperty("result", new StringSchema().description("응답 결과")) + .addProperty("code", new StringSchema().description("결과 코드")).addRequiredItem("code") + .addProperty("result", new Schema<>()) ); } } diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java index 67878d7c..77409862 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java @@ -32,8 +32,7 @@ private static void applyInternalServerErrorApiResponse(Operation operation) { .description("처리되지 않은 서버 에러") .value(""" { - "code": 500, - "message": "처리되지 않은 서버 에러가 발생하였습니다." + "code": "UNHANDLED_ERROR" }""") ); } From da0ccf3f8981a45d5b133cd4accba918c4bd983e Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 7 Jun 2024 23:23:28 +0900 Subject: [PATCH 098/273] =?UTF-8?q?docs:=20OpenAPI=EC=9D=98=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=84=B1=EA=B3=B5=20=EC=9D=91=EB=8B=B5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/web/common/ApiResponseCode.java | 36 ++++++++++++++++ .../api/web/common/CommonApiException.java | 12 +++--- .../api/web/common/CommonApiResponse.java | 12 +++--- .../common/GlobalRestControllerAdvice.java | 4 +- .../server/api/web/routes/auth/AuthApi.java | 2 +- .../server/api/web/routes/demo/DemoApi.java | 2 +- .../api/web/routes/health/HealthApi.java | 4 +- .../server/api/web/routes/init/InitApi.java | 11 +---- .../api/web/routes/init/InitController.java | 4 +- .../server/api/web/routes/user/UserApi.java | 18 ++++---- .../api/web/swagger/ComponentRefConsts.java | 17 ++++++++ .../swagger/GlobalOpenApiCustomizerImpl.java | 29 ++++++++++++- .../GlobalOperationCustomizerImpl.java | 42 ++++++++++++++----- 13 files changed, 145 insertions(+), 48 deletions(-) create mode 100644 api/src/main/java/vook/server/api/web/common/ApiResponseCode.java create mode 100644 api/src/main/java/vook/server/api/web/swagger/ComponentRefConsts.java diff --git a/api/src/main/java/vook/server/api/web/common/ApiResponseCode.java b/api/src/main/java/vook/server/api/web/common/ApiResponseCode.java new file mode 100644 index 00000000..8d3d2310 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/common/ApiResponseCode.java @@ -0,0 +1,36 @@ +package vook.server.api.web.common; + +public interface ApiResponseCode { + + String code(); + + enum Ok implements ApiResponseCode { + + SUCCESS; + + @Override + public String code() { + return this.name(); + } + } + + enum BadRequest implements ApiResponseCode { + + INVALID_PARAMETER; + + @Override + public String code() { + return this.name(); + } + } + + enum ServerError implements ApiResponseCode { + + UNHANDLED_ERROR; + + @Override + public String code() { + return this.name(); + } + } +} diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiException.java b/api/src/main/java/vook/server/api/web/common/CommonApiException.java index 596babe3..2c017572 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiException.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiException.java @@ -2,11 +2,11 @@ public class CommonApiException { public static abstract class Exception extends RuntimeException { - - protected String code; - public Exception(String code, Throwable cause) { - super(code, cause); + protected ApiResponseCode code; + + public Exception(ApiResponseCode code, Throwable cause) { + super(code.code(), cause); this.code = code; } @@ -17,7 +17,7 @@ public Exception(String code, Throwable cause) { public static class BadRequest extends Exception { - public BadRequest(String code, Throwable cause) { + public BadRequest(ApiResponseCode code, Throwable cause) { super(code, cause); } @@ -34,7 +34,7 @@ int statusCode() { public static class ServerError extends Exception { - public ServerError(String code, Throwable cause) { + public ServerError(ApiResponseCode code, Throwable cause) { super(code, cause); } diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java index cc7ee7d9..b918c7a1 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiResponse.java @@ -8,12 +8,12 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CommonApiResponse { - @Schema(description = "결과 코드", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "결과 코드", requiredMode = Schema.RequiredMode.REQUIRED, example = "SUCCESS") private String code; private T result; public static CommonApiResponse ok() { - return noResult("success"); + return noResult(ApiResponseCode.Ok.SUCCESS); } public static CommonApiResponse okWithResult(T result) { @@ -22,15 +22,15 @@ public static CommonApiResponse okWithResult(T result) { return response; } - public static CommonApiResponse noResult(String code) { + public static CommonApiResponse noResult(ApiResponseCode code) { CommonApiResponse response = new CommonApiResponse<>(); - response.code = code; + response.code = code.code(); return response; } - public static CommonApiResponse withResult(String code, T result) { + public static CommonApiResponse withResult(ApiResponseCode code, T result) { CommonApiResponse response = new CommonApiResponse<>(); - response.code = code; + response.code = code.code(); response.result = result; return response; } diff --git a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java index e4d86689..73f0d79a 100644 --- a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java +++ b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java @@ -18,14 +18,14 @@ public ResponseEntity handleCommonApiException(CommonApiException.Exception e @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { - CommonApiException.BadRequest badRequest = new CommonApiException.BadRequest("INVALID_PARAMETER", e); + CommonApiException.BadRequest badRequest = new CommonApiException.BadRequest(ApiResponseCode.BadRequest.INVALID_PARAMETER, e); return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response()); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { log.error(e.getMessage(), e); - CommonApiException.ServerError serverError = new CommonApiException.ServerError("UNHANDLED_ERROR", e); + CommonApiException.ServerError serverError = new CommonApiException.ServerError(ApiResponseCode.ServerError.UNHANDLED_ERROR, e); return ResponseEntity.status(serverError.statusCode()).body(serverError.response()); } } diff --git a/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java b/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java index 239560ce..96c20654 100644 --- a/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java +++ b/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java @@ -16,7 +16,7 @@ public interface AuthApi { summary = "토큰 갱신", description = """ 리프레시 토큰을 이용하여 엑세스 토큰과 리프레시 토큰을 갱신합니다. - 리프레시 토큰은 최상위 Description에 Authorzation 항목을 참고하세요.""" + 리프레시 토큰은 최상위 Description의 Authorzation 항목을 참고하세요.""" ) @ApiResponses(value = { @ApiResponse( diff --git a/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java index 4274ccba..b48c122e 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoApi.java @@ -17,8 +17,8 @@ public interface DemoApi { @ApiResponses(value = { @ApiResponse( responseCode = "200", - description = "성공", content = @Content( + mediaType = "application/json", schema = @Schema(implementation = SearchApiTermResponse.class) ) ), diff --git a/api/src/main/java/vook/server/api/web/routes/health/HealthApi.java b/api/src/main/java/vook/server/api/web/routes/health/HealthApi.java index e107d6c3..e7c35b39 100644 --- a/api/src/main/java/vook/server/api/web/routes/health/HealthApi.java +++ b/api/src/main/java/vook/server/api/web/routes/health/HealthApi.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,7 +16,8 @@ public interface HealthApi { @ApiResponse( responseCode = "200", content = @Content( - mediaType = "text/plain" + mediaType = "text/plain", + examples = @ExampleObject(name = "성공", value = "OK") ) ) String health(); diff --git a/api/src/main/java/vook/server/api/web/routes/init/InitApi.java b/api/src/main/java/vook/server/api/web/routes/init/InitApi.java index 0f819ae1..b064ca5b 100644 --- a/api/src/main/java/vook/server/api/web/routes/init/InitApi.java +++ b/api/src/main/java/vook/server/api/web/routes/init/InitApi.java @@ -1,9 +1,8 @@ package vook.server.api.web.routes.init; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import vook.server.api.web.common.CommonApiResponse; @Tag(name = "init", description = "초기화 API") public interface InitApi { @@ -12,11 +11,5 @@ public interface InitApi { summary = "데이터 초기화", description = "모든 데이터를 삭제하고, 데모용 데이터를 생성시킨 상태로 초기화 시킵니다." ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "성공" - ), - }) - void init(); + CommonApiResponse init(); } diff --git a/api/src/main/java/vook/server/api/web/routes/init/InitController.java b/api/src/main/java/vook/server/api/web/routes/init/InitController.java index c6335e88..0d842ae0 100644 --- a/api/src/main/java/vook/server/api/web/routes/init/InitController.java +++ b/api/src/main/java/vook/server/api/web/routes/init/InitController.java @@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import vook.server.api.devhelper.InitService; +import vook.server.api.web.common.CommonApiResponse; @Profile({"local", "dev", "stag"}) @RestController @@ -16,7 +17,8 @@ public class InitController implements InitApi { private final InitService initService; @PostMapping - public void init() { + public CommonApiResponse init() { initService.init(); + return CommonApiResponse.ok(); } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 48dd6468..57f7d9ca 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -12,6 +13,7 @@ import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; +import vook.server.api.web.swagger.ComponentRefConsts; @Tag(name = "user", description = "사용자 관련 API") public interface UserApi { @@ -25,8 +27,8 @@ public interface UserApi { @ApiResponses(value = { @ApiResponse( responseCode = "200", - description = "성공", content = @Content( + mediaType = "application/json", schema = @Schema(implementation = UserApiUerInfoResponse.class) ) ), @@ -44,8 +46,12 @@ class UserApiUerInfoResponse extends CommonApiResponse { ) @ApiResponses(value = { @ApiResponse( - responseCode = "200", - description = "성공" + responseCode = "400", + content = @Content( + mediaType = "application/json", + schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), + examples = @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER) + ) ), }) CommonApiResponse register(VookLoginUser user, UserRegisterRequest request); @@ -56,11 +62,5 @@ class UserApiUerInfoResponse extends CommonApiResponse { @SecurityRequirement(name = "AccessToken") } ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "성공" - ), - }) CommonApiResponse onboardingComplete(VookLoginUser user, UserOnboardingCompleteRequest request); } diff --git a/api/src/main/java/vook/server/api/web/swagger/ComponentRefConsts.java b/api/src/main/java/vook/server/api/web/swagger/ComponentRefConsts.java new file mode 100644 index 00000000..12890cc7 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/swagger/ComponentRefConsts.java @@ -0,0 +1,17 @@ +package vook.server.api.web.swagger; + +/** + * OpenAPI ComponentRef 상수 + */ +public class ComponentRefConsts { + + public static class Schema { + public static final String COMMON_API_RESPONSE = "#/components/schemas/CommonApiResponse"; + } + + public static class Example { + public static final String SUCCESS = "#/components/examples/Success"; + public static final String INVALID_PARAMETER = "#/components/examples/InvalidParameter"; + public static final String UNHANDLED_ERROR = "#/components/examples/UnhandledError"; + } +} diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java index 6828eff3..13472111 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java @@ -1,9 +1,11 @@ package vook.server.api.web.swagger; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.examples.Example; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import vook.server.api.web.common.ApiResponseCode; import java.util.Map; @@ -15,9 +17,34 @@ public void customise(OpenAPI openApi) { private static void applyCommonApiResponseSchema(OpenAPI openApi) { openApi.getComponents() - .addSchemas("CommonApiResponse", new Schema>() + .addSchemas(getKey(ComponentRefConsts.Schema.COMMON_API_RESPONSE), new Schema>() .addProperty("code", new StringSchema().description("결과 코드")).addRequiredItem("code") .addProperty("result", new Schema<>()) + ) + .addExamples(getKey(ComponentRefConsts.Example.SUCCESS), new Example() + .description("성공") + .value(String.format(""" + { + "code": "%s" + }""", ApiResponseCode.Ok.SUCCESS.code())) + ) + .addExamples(getKey(ComponentRefConsts.Example.INVALID_PARAMETER), new Example() + .description("유효하지 않은 파라미터") + .value(String.format(""" + { + "code": "%s" + }""", ApiResponseCode.BadRequest.INVALID_PARAMETER.code())) + ) + .addExamples(getKey(ComponentRefConsts.Example.UNHANDLED_ERROR), new Example() + .description("처리되지 않은 서버 에러") + .value(String.format(""" + { + "code": "%s" + }""", ApiResponseCode.ServerError.UNHANDLED_ERROR.code())) ); } + + private static String getKey(String refString) { + return refString.substring(refString.lastIndexOf('/') + 1); + } } diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java index 77409862..b2974ee4 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java @@ -8,7 +8,6 @@ import io.swagger.v3.oas.models.responses.ApiResponse; import org.springdoc.core.customizers.GlobalOperationCustomizer; import org.springframework.web.method.HandlerMethod; -import vook.server.api.web.common.CommonApiResponse; import java.util.HashMap; @@ -16,29 +15,50 @@ public class GlobalOperationCustomizerImpl implements GlobalOperationCustomizer @Override public Operation customize(Operation operation, HandlerMethod handlerMethod) { + applyDefaultOkApiResponse(operation); applyInternalServerErrorApiResponse(operation); return operation; } + private void applyDefaultOkApiResponse(Operation operation) { + ApiResponse apiResponse = operation.getResponses().computeIfAbsent( + "200", + k -> new ApiResponse().description("OK") + ); + + if (apiResponse.getContent() == null) { + apiResponse.setContent(new Content()); + } else { + // SpringDoc이 기본으로 제공하는 Content 제거 + apiResponse.getContent().remove("*/*"); + } + + // Content가 존재하면 종료 + if (!apiResponse.getContent().isEmpty()) { + return; + } + + // Content가 존재하지 않으면 기본 성공 응답 추가 + apiResponse.getContent().computeIfAbsent("application/json", k -> new MediaType() + .schema(new Schema<>().$ref(ComponentRefConsts.Schema.COMMON_API_RESPONSE)) + .addExamples("성공", new Example().$ref(ComponentRefConsts.Example.SUCCESS))); + } + private static void applyInternalServerErrorApiResponse(Operation operation) { MediaType jsonType = prepareOrGetJsonMediaType(operation); if (jsonType.getSchema() == null) { - jsonType.setSchema(new Schema().$ref("#/components/schemas/CommonApiResponse")); + jsonType.setSchema(new Schema<>().$ref(ComponentRefConsts.Schema.COMMON_API_RESPONSE)); } - jsonType.getExamples().put("처리되지 않은 서버 에러", - new Example() - .description("처리되지 않은 서버 에러") - .value(""" - { - "code": "UNHANDLED_ERROR" - }""") - ); + jsonType.getExamples().put("처리되지 않은 서버 에러", new Example().$ref(ComponentRefConsts.Example.UNHANDLED_ERROR)); } private static MediaType prepareOrGetJsonMediaType(Operation operation) { - ApiResponse apiResponse = operation.getResponses().computeIfAbsent("500", k -> new ApiResponse()); + ApiResponse apiResponse = operation.getResponses().computeIfAbsent( + "500", + k -> new ApiResponse().description("Internal Server Error") + ); if (apiResponse.getContent() == null) { apiResponse.setContent(new Content()); From bdfcc9cee37cd4645d62177df5df1a6ccf341d10 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 8 Jun 2024 00:40:01 +0900 Subject: [PATCH 099/273] =?UTF-8?q?refector:=20web=EA=B4=80=EB=A0=A8=20aut?= =?UTF-8?q?h,=20config=20=EB=B6=80=EB=B6=84=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/{ => app}/config/JpaConfig.java | 2 +- .../server/api/{ => app}/config/QuerydslConfig.java | 2 +- .../server/api/{ => app}/config/TimeZoneConfig.java | 2 +- .../api/{app/auth => web/auth/app}/TokenService.java | 4 ++-- .../{model/values => web/auth/data}/AuthValues.java | 2 +- .../api/{app => web}/auth/data/GeneratedToken.java | 2 +- .../auth/common => web/auth/data}/VookLoginUser.java | 2 +- .../server/api/{config => web}/auth/jwt/JWTFilter.java | 8 ++++---- .../auth/oauth2/LoginSuccessHandler.java | 8 ++++---- .../auth/oauth2/OAuth2GoogleResponse.java | 2 +- .../{config => web}/auth/oauth2/OAuth2Response.java | 3 ++- .../auth/oauth2/VookOAuth2UserService.java | 4 ++-- .../server/api/{ => web}/config/SecurityConfig.java | 10 +++++----- .../server/api/{ => web}/config/SwaggerConfig.java | 2 +- .../java/vook/server/api/web/routes/auth/AuthApi.java | 2 +- .../server/api/web/routes/auth/AuthRestController.java | 6 +++--- .../java/vook/server/api/web/routes/user/UserApi.java | 2 +- .../server/api/web/routes/user/UserRestController.java | 2 +- .../server/api/web/routes/user/UserWebService.java | 2 +- .../vook/server/api/web/swagger/OpenApiDefinition.java | 2 +- .../server/api/testhelper/IntegrationTestBase.java | 2 +- .../vook/server/api/testhelper/TestDataCreator.java | 4 ++-- .../api/web/routes/user/UserRestControllerTest.java | 2 +- .../server/api/web/routes/user/UserWebServiceTest.java | 2 +- 24 files changed, 40 insertions(+), 39 deletions(-) rename api/src/main/java/vook/server/api/{ => app}/config/JpaConfig.java (84%) rename api/src/main/java/vook/server/api/{ => app}/config/QuerydslConfig.java (92%) rename api/src/main/java/vook/server/api/{ => app}/config/TimeZoneConfig.java (90%) rename api/src/main/java/vook/server/api/{app/auth => web/auth/app}/TokenService.java (95%) rename api/src/main/java/vook/server/api/{model/values => web/auth/data}/AuthValues.java (82%) rename api/src/main/java/vook/server/api/{app => web}/auth/data/GeneratedToken.java (91%) rename api/src/main/java/vook/server/api/{config/auth/common => web/auth/data}/VookLoginUser.java (95%) rename api/src/main/java/vook/server/api/{config => web}/auth/jwt/JWTFilter.java (91%) rename api/src/main/java/vook/server/api/{config => web}/auth/oauth2/LoginSuccessHandler.java (88%) rename api/src/main/java/vook/server/api/{config => web}/auth/oauth2/OAuth2GoogleResponse.java (92%) rename api/src/main/java/vook/server/api/{config => web}/auth/oauth2/OAuth2Response.java (83%) rename api/src/main/java/vook/server/api/{config => web}/auth/oauth2/VookOAuth2UserService.java (95%) rename api/src/main/java/vook/server/api/{ => web}/config/SecurityConfig.java (94%) rename api/src/main/java/vook/server/api/{ => web}/config/SwaggerConfig.java (95%) diff --git a/api/src/main/java/vook/server/api/config/JpaConfig.java b/api/src/main/java/vook/server/api/app/config/JpaConfig.java similarity index 84% rename from api/src/main/java/vook/server/api/config/JpaConfig.java rename to api/src/main/java/vook/server/api/app/config/JpaConfig.java index 119d3612..cae69ce2 100644 --- a/api/src/main/java/vook/server/api/config/JpaConfig.java +++ b/api/src/main/java/vook/server/api/app/config/JpaConfig.java @@ -1,4 +1,4 @@ -package vook.server.api.config; +package vook.server.api.app.config; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; diff --git a/api/src/main/java/vook/server/api/config/QuerydslConfig.java b/api/src/main/java/vook/server/api/app/config/QuerydslConfig.java similarity index 92% rename from api/src/main/java/vook/server/api/config/QuerydslConfig.java rename to api/src/main/java/vook/server/api/app/config/QuerydslConfig.java index e1066f08..01456e21 100644 --- a/api/src/main/java/vook/server/api/config/QuerydslConfig.java +++ b/api/src/main/java/vook/server/api/app/config/QuerydslConfig.java @@ -1,4 +1,4 @@ -package vook.server.api.config; +package vook.server.api.app.config; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; diff --git a/api/src/main/java/vook/server/api/config/TimeZoneConfig.java b/api/src/main/java/vook/server/api/app/config/TimeZoneConfig.java similarity index 90% rename from api/src/main/java/vook/server/api/config/TimeZoneConfig.java rename to api/src/main/java/vook/server/api/app/config/TimeZoneConfig.java index 2c7ee3a0..d5161cb0 100644 --- a/api/src/main/java/vook/server/api/config/TimeZoneConfig.java +++ b/api/src/main/java/vook/server/api/app/config/TimeZoneConfig.java @@ -1,4 +1,4 @@ -package vook.server.api.config; +package vook.server.api.app.config; import jakarta.annotation.PostConstruct; import org.springframework.context.annotation.Configuration; diff --git a/api/src/main/java/vook/server/api/app/auth/TokenService.java b/api/src/main/java/vook/server/api/web/auth/app/TokenService.java similarity index 95% rename from api/src/main/java/vook/server/api/app/auth/TokenService.java rename to api/src/main/java/vook/server/api/web/auth/app/TokenService.java index 06413922..6005146b 100644 --- a/api/src/main/java/vook/server/api/app/auth/TokenService.java +++ b/api/src/main/java/vook/server/api/web/auth/app/TokenService.java @@ -1,12 +1,12 @@ -package vook.server.api.app.auth; +package vook.server.api.web.auth.app; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import vook.server.api.app.auth.data.GeneratedToken; import vook.server.api.helper.jwt.JWTHelperProvider; import vook.server.api.helper.jwt.JWTReader; +import vook.server.api.web.auth.data.GeneratedToken; @Slf4j @Service diff --git a/api/src/main/java/vook/server/api/model/values/AuthValues.java b/api/src/main/java/vook/server/api/web/auth/data/AuthValues.java similarity index 82% rename from api/src/main/java/vook/server/api/model/values/AuthValues.java rename to api/src/main/java/vook/server/api/web/auth/data/AuthValues.java index 907f5f37..f279bfd0 100644 --- a/api/src/main/java/vook/server/api/model/values/AuthValues.java +++ b/api/src/main/java/vook/server/api/web/auth/data/AuthValues.java @@ -1,4 +1,4 @@ -package vook.server.api.model.values; +package vook.server.api.web.auth.data; public class AuthValues { public static final String AUTHORIZATION_HEADER = "Authorization"; diff --git a/api/src/main/java/vook/server/api/app/auth/data/GeneratedToken.java b/api/src/main/java/vook/server/api/web/auth/data/GeneratedToken.java similarity index 91% rename from api/src/main/java/vook/server/api/app/auth/data/GeneratedToken.java rename to api/src/main/java/vook/server/api/web/auth/data/GeneratedToken.java index b495abd4..cc21c5b2 100644 --- a/api/src/main/java/vook/server/api/app/auth/data/GeneratedToken.java +++ b/api/src/main/java/vook/server/api/web/auth/data/GeneratedToken.java @@ -1,4 +1,4 @@ -package vook.server.api.app.auth.data; +package vook.server.api.web.auth.data; import lombok.Getter; diff --git a/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java b/api/src/main/java/vook/server/api/web/auth/data/VookLoginUser.java similarity index 95% rename from api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java rename to api/src/main/java/vook/server/api/web/auth/data/VookLoginUser.java index fe24839e..774386cd 100644 --- a/api/src/main/java/vook/server/api/config/auth/common/VookLoginUser.java +++ b/api/src/main/java/vook/server/api/web/auth/data/VookLoginUser.java @@ -1,4 +1,4 @@ -package vook.server.api.config.auth.common; +package vook.server.api.web.auth.data; import lombok.Getter; import lombok.ToString; diff --git a/api/src/main/java/vook/server/api/config/auth/jwt/JWTFilter.java b/api/src/main/java/vook/server/api/web/auth/jwt/JWTFilter.java similarity index 91% rename from api/src/main/java/vook/server/api/config/auth/jwt/JWTFilter.java rename to api/src/main/java/vook/server/api/web/auth/jwt/JWTFilter.java index 131484db..f708fdff 100644 --- a/api/src/main/java/vook/server/api/config/auth/jwt/JWTFilter.java +++ b/api/src/main/java/vook/server/api/web/auth/jwt/JWTFilter.java @@ -1,4 +1,4 @@ -package vook.server.api.config.auth.jwt; +package vook.server.api.web.auth.jwt; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -11,9 +11,9 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; -import vook.server.api.app.auth.TokenService; -import vook.server.api.config.auth.common.VookLoginUser; -import vook.server.api.model.values.AuthValues; +import vook.server.api.web.auth.app.TokenService; +import vook.server.api.web.auth.data.AuthValues; +import vook.server.api.web.auth.data.VookLoginUser; import java.io.IOException; diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java b/api/src/main/java/vook/server/api/web/auth/oauth2/LoginSuccessHandler.java similarity index 88% rename from api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java rename to api/src/main/java/vook/server/api/web/auth/oauth2/LoginSuccessHandler.java index f6fbd560..c4899b29 100644 --- a/api/src/main/java/vook/server/api/config/auth/oauth2/LoginSuccessHandler.java +++ b/api/src/main/java/vook/server/api/web/auth/oauth2/LoginSuccessHandler.java @@ -1,4 +1,4 @@ -package vook.server.api.config.auth.oauth2; +package vook.server.api.web.auth.oauth2; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -8,9 +8,9 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; -import vook.server.api.app.auth.TokenService; -import vook.server.api.app.auth.data.GeneratedToken; -import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.web.auth.app.TokenService; +import vook.server.api.web.auth.data.GeneratedToken; +import vook.server.api.web.auth.data.VookLoginUser; import java.io.IOException; diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2GoogleResponse.java b/api/src/main/java/vook/server/api/web/auth/oauth2/OAuth2GoogleResponse.java similarity index 92% rename from api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2GoogleResponse.java rename to api/src/main/java/vook/server/api/web/auth/oauth2/OAuth2GoogleResponse.java index a358f27d..c450aae6 100644 --- a/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2GoogleResponse.java +++ b/api/src/main/java/vook/server/api/web/auth/oauth2/OAuth2GoogleResponse.java @@ -1,4 +1,4 @@ -package vook.server.api.config.auth.oauth2; +package vook.server.api.web.auth.oauth2; import java.util.Map; diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2Response.java b/api/src/main/java/vook/server/api/web/auth/oauth2/OAuth2Response.java similarity index 83% rename from api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2Response.java rename to api/src/main/java/vook/server/api/web/auth/oauth2/OAuth2Response.java index aceee21b..c758d13f 100644 --- a/api/src/main/java/vook/server/api/config/auth/oauth2/OAuth2Response.java +++ b/api/src/main/java/vook/server/api/web/auth/oauth2/OAuth2Response.java @@ -1,6 +1,7 @@ -package vook.server.api.config.auth.oauth2; +package vook.server.api.web.auth.oauth2; public interface OAuth2Response { + //제공자 (Ex. naver, google, ...) String getProvider(); diff --git a/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java b/api/src/main/java/vook/server/api/web/auth/oauth2/VookOAuth2UserService.java similarity index 95% rename from api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java rename to api/src/main/java/vook/server/api/web/auth/oauth2/VookOAuth2UserService.java index 83a9170c..202c3b8e 100644 --- a/api/src/main/java/vook/server/api/config/auth/oauth2/VookOAuth2UserService.java +++ b/api/src/main/java/vook/server/api/web/auth/oauth2/VookOAuth2UserService.java @@ -1,4 +1,4 @@ -package vook.server.api.config.auth.oauth2; +package vook.server.api.web.auth.oauth2; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -10,8 +10,8 @@ import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; import vook.server.api.app.user.data.SignUpFromSocialCommand; -import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.model.user.SocialUser; +import vook.server.api.web.auth.data.VookLoginUser; @Slf4j @Service diff --git a/api/src/main/java/vook/server/api/config/SecurityConfig.java b/api/src/main/java/vook/server/api/web/config/SecurityConfig.java similarity index 94% rename from api/src/main/java/vook/server/api/config/SecurityConfig.java rename to api/src/main/java/vook/server/api/web/config/SecurityConfig.java index 785403a0..efadee64 100644 --- a/api/src/main/java/vook/server/api/config/SecurityConfig.java +++ b/api/src/main/java/vook/server/api/web/config/SecurityConfig.java @@ -1,4 +1,4 @@ -package vook.server.api.config; +package vook.server.api.web.config; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -15,10 +15,10 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; -import vook.server.api.app.auth.TokenService; -import vook.server.api.config.auth.jwt.JWTFilter; -import vook.server.api.config.auth.oauth2.LoginSuccessHandler; -import vook.server.api.config.auth.oauth2.VookOAuth2UserService; +import vook.server.api.web.auth.app.TokenService; +import vook.server.api.web.auth.jwt.JWTFilter; +import vook.server.api.web.auth.oauth2.LoginSuccessHandler; +import vook.server.api.web.auth.oauth2.VookOAuth2UserService; @Configuration @EnableWebSecurity diff --git a/api/src/main/java/vook/server/api/config/SwaggerConfig.java b/api/src/main/java/vook/server/api/web/config/SwaggerConfig.java similarity index 95% rename from api/src/main/java/vook/server/api/config/SwaggerConfig.java rename to api/src/main/java/vook/server/api/web/config/SwaggerConfig.java index e2143854..f674c255 100644 --- a/api/src/main/java/vook/server/api/config/SwaggerConfig.java +++ b/api/src/main/java/vook/server/api/web/config/SwaggerConfig.java @@ -1,4 +1,4 @@ -package vook.server.api.config; +package vook.server.api.web.config; import org.springdoc.core.customizers.GlobalOpenApiCustomizer; import org.springdoc.core.customizers.GlobalOperationCustomizer; diff --git a/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java b/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java index 96c20654..38848043 100644 --- a/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java +++ b/api/src/main/java/vook/server/api/web/routes/auth/AuthApi.java @@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; -import vook.server.api.model.values.AuthValues; +import vook.server.api.web.auth.data.AuthValues; @Tag(name = "auth", description = "인증 관련 API") public interface AuthApi { diff --git a/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java b/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java index 8df4aba0..00019e1e 100644 --- a/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/auth/AuthRestController.java @@ -7,9 +7,9 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import vook.server.api.app.auth.TokenService; -import vook.server.api.app.auth.data.GeneratedToken; -import vook.server.api.model.values.AuthValues; +import vook.server.api.web.auth.app.TokenService; +import vook.server.api.web.auth.data.AuthValues; +import vook.server.api.web.auth.data.GeneratedToken; @RestController @RequestMapping("/auth") diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 57f7d9ca..2e2a0059 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index c463fe4a..cf58c08a 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -5,7 +5,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import vook.server.api.config.auth.common.VookLoginUser; +import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index ff02735b..1226b50c 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -4,8 +4,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; -import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.model.user.User; +import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; diff --git a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java index 4a808ad3..683f6f0a 100644 --- a/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java +++ b/api/src/main/java/vook/server/api/web/swagger/OpenApiDefinition.java @@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.security.SecuritySchemes; import io.swagger.v3.oas.annotations.servers.Server; import org.springframework.context.annotation.Configuration; -import vook.server.api.model.values.AuthValues; +import vook.server.api.web.auth.data.AuthValues; @OpenAPIDefinition( info = @Info( diff --git a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java index 2ec5fb7b..e0eeea19 100644 --- a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java +++ b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java @@ -13,7 +13,7 @@ import java.util.Map; import java.util.TimeZone; -import static vook.server.api.config.TimeZoneConfig.DEFAULT_TIME_ZONE; +import static vook.server.api.app.config.TimeZoneConfig.DEFAULT_TIME_ZONE; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class IntegrationTestBase { diff --git a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java index 7d9f3125..edcd7c8c 100644 --- a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java +++ b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java @@ -3,14 +3,14 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.auth.TokenService; -import vook.server.api.app.auth.data.GeneratedToken; import vook.server.api.app.user.UserService; import vook.server.api.app.user.data.CompleteOnboardingCommand; import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.app.user.data.SignUpFromSocialCommand; import vook.server.api.model.user.SocialUser; import vook.server.api.model.user.User; +import vook.server.api.web.auth.app.TokenService; +import vook.server.api.web.auth.data.GeneratedToken; @Component @Transactional diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java index 97b2a021..625c83f5 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java @@ -6,11 +6,11 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; -import vook.server.api.app.auth.data.GeneratedToken; import vook.server.api.model.user.User; import vook.server.api.testhelper.HttpEntityBuilder; import vook.server.api.testhelper.IntegrationTestBase; import vook.server.api.testhelper.TestDataCreator; +import vook.server.api.web.auth.data.GeneratedToken; import java.util.Map; diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 6ee11ba3..6b9d489d 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -5,11 +5,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; -import vook.server.api.config.auth.common.VookLoginUser; import vook.server.api.model.user.User; import vook.server.api.model.user.UserStatus; import vook.server.api.testhelper.IntegrationTestBase; import vook.server.api.testhelper.TestDataCreator; +import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; From 9d869745c9fcaf2ef580627b4780b947886c52ad Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sat, 8 Jun 2024 01:00:44 +0900 Subject: [PATCH 100/273] =?UTF-8?q?docs:=20accessToken=EC=9D=B4=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20API=EC=9D=98=20Response=20Code?= =?UTF-8?q?=EC=97=90=20401=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GlobalOperationCustomizerImpl.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java index b2974ee4..b90fe2dd 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java @@ -6,20 +6,39 @@ import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.security.SecurityRequirement; import org.springdoc.core.customizers.GlobalOperationCustomizer; import org.springframework.web.method.HandlerMethod; import java.util.HashMap; +import java.util.List; public class GlobalOperationCustomizerImpl implements GlobalOperationCustomizer { @Override public Operation customize(Operation operation, HandlerMethod handlerMethod) { applyDefaultOkApiResponse(operation); + applyUnauthorizedApiResponse(operation); applyInternalServerErrorApiResponse(operation); return operation; } + private void applyUnauthorizedApiResponse(Operation operation) { + List security = operation.getSecurity(); + if (security == null) { + return; + } + + security.forEach(sr -> { + if (sr.containsKey("AccessToken")) { + operation.getResponses().computeIfAbsent( + "401", + k -> new ApiResponse().description("Unauthorized") + ); + } + }); + } + private void applyDefaultOkApiResponse(Operation operation) { ApiResponse apiResponse = operation.getResponses().computeIfAbsent( "200", From 3642ddb883427b2fa0fdeefe9438bb802e57d0f2 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 9 Jun 2024 13:01:07 +0900 Subject: [PATCH 101/273] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8A=94=20funnel=EA=B3=BC=20job=EC=9D=84=20enum=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20#69?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/data/CompleteOnboardingCommand.java | 10 ++++---- .../vook/server/api/model/user/Funnel.java | 24 +++++++++++++++++++ .../java/vook/server/api/model/user/Job.java | 24 +++++++++++++++++++ .../vook/server/api/model/user/UserInfo.java | 8 ++++--- .../common/GlobalRestControllerAdvice.java | 9 +++++++ .../server/api/web/routes/user/UserApi.java | 10 ++++++++ .../reqres/UserOnboardingCompleteRequest.java | 6 +++-- .../api/testhelper/TestDataCreator.java | 4 +++- .../web/routes/user/UserWebServiceTest.java | 6 +++-- 9 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 api/src/main/java/vook/server/api/model/user/Funnel.java create mode 100644 api/src/main/java/vook/server/api/model/user/Job.java diff --git a/api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java b/api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java index 3bce9645..f8da1a54 100644 --- a/api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java +++ b/api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java @@ -1,18 +1,20 @@ package vook.server.api.app.user.data; import lombok.Getter; +import vook.server.api.model.user.Funnel; +import vook.server.api.model.user.Job; @Getter public class CompleteOnboardingCommand { public String userUid; - public String funnel; - public String job; + public Funnel funnel; + public Job job; public static CompleteOnboardingCommand of( String userUid, - String funnel, - String job + Funnel funnel, + Job job ) { CompleteOnboardingCommand command = new CompleteOnboardingCommand(); command.userUid = userUid; diff --git a/api/src/main/java/vook/server/api/model/user/Funnel.java b/api/src/main/java/vook/server/api/model/user/Funnel.java new file mode 100644 index 00000000..bce5fc38 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/user/Funnel.java @@ -0,0 +1,24 @@ +package vook.server.api.model.user; + +public enum Funnel { + //X + X, + + //페이스북 + FACEBOOK, + + //링크드인 + LINKEDIN, + + //인스타그램 + INSTAGRAM, + + //네이버 블로그 + NAVER_BLOG, + + //친구/지인 추천 + RECOMMENDATION, + + //기타 + OTHER +} diff --git a/api/src/main/java/vook/server/api/model/user/Job.java b/api/src/main/java/vook/server/api/model/user/Job.java new file mode 100644 index 00000000..37dbdd30 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/user/Job.java @@ -0,0 +1,24 @@ +package vook.server.api.model.user; + +public enum Job { + //기획자 + PLANNER, + + //디자이너 + DESIGNER, + + //개발자 + DEVELOPER, + + //마케터 + MARKETER, + + //CEO + CEO, + + //HR + HR, + + //기타 + OTHER +} diff --git a/api/src/main/java/vook/server/api/model/user/UserInfo.java b/api/src/main/java/vook/server/api/model/user/UserInfo.java index d0bffae2..a3444a75 100644 --- a/api/src/main/java/vook/server/api/model/user/UserInfo.java +++ b/api/src/main/java/vook/server/api/model/user/UserInfo.java @@ -16,9 +16,11 @@ public class UserInfo { private Boolean marketingEmailOptIn; - private String funnel; + @Enumerated(EnumType.STRING) + private Funnel funnel; - private String job; + @Enumerated(EnumType.STRING) + private Job job; @OneToOne @JoinColumn(name = "user_id") @@ -36,7 +38,7 @@ public static UserInfo forRegisterOf( return result; } - public void addOnboardingInfo(String funnel, String job) { + public void addOnboardingInfo(Funnel funnel, Job job) { this.funnel = funnel; this.job = job; } diff --git a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java index 73f0d79a..0c27133e 100644 --- a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java +++ b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -18,6 +19,14 @@ public ResponseEntity handleCommonApiException(CommonApiException.Exception e @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.debug(e.getMessage(), e); + CommonApiException.BadRequest badRequest = new CommonApiException.BadRequest(ApiResponseCode.BadRequest.INVALID_PARAMETER, e); + return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response()); + } + + @ExceptionHandler(HttpMessageConversionException.class) + public ResponseEntity handleHttpMessageConversionException(HttpMessageConversionException e) { + log.debug(e.getMessage(), e); CommonApiException.BadRequest badRequest = new CommonApiException.BadRequest(ApiResponseCode.BadRequest.INVALID_PARAMETER, e); return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response()); } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 2e2a0059..e15959fa 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -62,5 +62,15 @@ class UserApiUerInfoResponse extends CommonApiResponse { @SecurityRequirement(name = "AccessToken") } ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "400", + content = @Content( + mediaType = "application/json", + schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), + examples = @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER) + ) + ), + }) CommonApiResponse onboardingComplete(VookLoginUser user, UserOnboardingCompleteRequest request); } diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java index 4756bcda..994e50b5 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java @@ -2,12 +2,14 @@ import lombok.Data; import vook.server.api.app.user.data.CompleteOnboardingCommand; +import vook.server.api.model.user.Funnel; +import vook.server.api.model.user.Job; @Data public class UserOnboardingCompleteRequest { - public String funnel; - public String job; + public Funnel funnel; + public Job job; public CompleteOnboardingCommand toCommand(String uid) { return CompleteOnboardingCommand.of(uid, funnel, job); diff --git a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java index edcd7c8c..272a7f48 100644 --- a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java +++ b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java @@ -7,6 +7,8 @@ import vook.server.api.app.user.data.CompleteOnboardingCommand; import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.app.user.data.SignUpFromSocialCommand; +import vook.server.api.model.user.Funnel; +import vook.server.api.model.user.Job; import vook.server.api.model.user.SocialUser; import vook.server.api.model.user.User; import vook.server.api.web.auth.app.TokenService; @@ -35,7 +37,7 @@ public User createRegisteredUser() { public User createCompletedOnboardingUser() { User user = createRegisteredUser(); - userService.completeOnboarding(CompleteOnboardingCommand.of(user.getUid(), "testFunnel", "testJob")); + userService.completeOnboarding(CompleteOnboardingCommand.of(user.getUid(), Funnel.OTHER, Job.OTHER)); return userService.findByUid(user.getUid()).orElseThrow(); } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 6b9d489d..7f3dc724 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -5,6 +5,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; +import vook.server.api.model.user.Funnel; +import vook.server.api.model.user.Job; import vook.server.api.model.user.User; import vook.server.api.model.user.UserStatus; import vook.server.api.testhelper.IntegrationTestBase; @@ -112,8 +114,8 @@ void onboardingComplete1() { VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); UserOnboardingCompleteRequest request = new UserOnboardingCompleteRequest(); - request.setFunnel("testFunnel"); - request.setJob("testJob"); + request.setFunnel(Funnel.OTHER); + request.setJob(Job.OTHER); // when userWebService.onboardingComplete(vookLoginUser, request); From de9fa6a5688f1f7117aec2fb200cce03d5e3267b Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 9 Jun 2024 13:18:39 +0900 Subject: [PATCH 102/273] =?UTF-8?q?refactor:=20CommonApiException=20?= =?UTF-8?q?=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/web/common/CommonApiException.java | 56 ++++++------------- .../common/GlobalRestControllerAdvice.java | 10 ++-- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiException.java b/api/src/main/java/vook/server/api/web/common/CommonApiException.java index 2c017572..a4934b73 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiException.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiException.java @@ -1,51 +1,29 @@ package vook.server.api.web.common; -public class CommonApiException { - public static abstract class Exception extends RuntimeException { +public class CommonApiException extends RuntimeException { - protected ApiResponseCode code; + private final ApiResponseCode code; + private final int statusCode; - public Exception(ApiResponseCode code, Throwable cause) { - super(code.code(), cause); - this.code = code; - } - - abstract CommonApiResponse response(); - - abstract int statusCode(); + CommonApiException(ApiResponseCode code, int statusCode, Throwable cause) { + super(code.code(), cause); + this.code = code; + this.statusCode = statusCode; } - public static class BadRequest extends Exception { - - public BadRequest(ApiResponseCode code, Throwable cause) { - super(code, cause); - } - - @Override - public CommonApiResponse response() { - return CommonApiResponse.noResult(code); - } - - @Override - int statusCode() { - return 400; - } + public CommonApiResponse response() { + return CommonApiResponse.noResult(code); } - public static class ServerError extends Exception { - - public ServerError(ApiResponseCode code, Throwable cause) { - super(code, cause); - } + public int statusCode() { + return statusCode; + } - @Override - public CommonApiResponse response() { - return CommonApiResponse.noResult(code); - } + public static CommonApiException badRequest(ApiResponseCode code, Throwable cause) { + return new CommonApiException(code, 400, cause); + } - @Override - int statusCode() { - return 500; - } + public static CommonApiException serverError(ApiResponseCode code, Throwable cause) { + return new CommonApiException(code, 500, cause); } } diff --git a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java index 0c27133e..67ebd2c1 100644 --- a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java +++ b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java @@ -11,8 +11,8 @@ @RestControllerAdvice public class GlobalRestControllerAdvice { - @ExceptionHandler(CommonApiException.Exception.class) - public ResponseEntity handleCommonApiException(CommonApiException.Exception e) { + @ExceptionHandler(CommonApiException.class) + public ResponseEntity handleCommonApiException(CommonApiException e) { log.error(e.getMessage(), e); return ResponseEntity.status(e.statusCode()).body(e.response()); } @@ -20,21 +20,21 @@ public ResponseEntity handleCommonApiException(CommonApiException.Exception e @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { log.debug(e.getMessage(), e); - CommonApiException.BadRequest badRequest = new CommonApiException.BadRequest(ApiResponseCode.BadRequest.INVALID_PARAMETER, e); + CommonApiException badRequest = CommonApiException.badRequest(ApiResponseCode.BadRequest.INVALID_PARAMETER, e); return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response()); } @ExceptionHandler(HttpMessageConversionException.class) public ResponseEntity handleHttpMessageConversionException(HttpMessageConversionException e) { log.debug(e.getMessage(), e); - CommonApiException.BadRequest badRequest = new CommonApiException.BadRequest(ApiResponseCode.BadRequest.INVALID_PARAMETER, e); + CommonApiException badRequest = CommonApiException.badRequest(ApiResponseCode.BadRequest.INVALID_PARAMETER, e); return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response()); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { log.error(e.getMessage(), e); - CommonApiException.ServerError serverError = new CommonApiException.ServerError(ApiResponseCode.ServerError.UNHANDLED_ERROR, e); + CommonApiException serverError = CommonApiException.serverError(ApiResponseCode.ServerError.UNHANDLED_ERROR, e); return ResponseEntity.status(serverError.statusCode()).body(serverError.response()); } } From de7d24a2dd6b31e724e4d395db440de32a202849 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 9 Jun 2024 13:27:45 +0900 Subject: [PATCH 103/273] =?UTF-8?q?feat:=20=EB=AF=B8=EA=B0=80=EC=9E=85?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=EA=B0=80=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=EB=A5=BC=20=EC=8B=9C=EB=8F=84=20=ED=95=A0=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0,=20=EC=98=88=EC=99=B8=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20#69?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 5 +++++ .../NotReadyToOnboardingException.java | 4 ++++ .../java/vook/server/api/model/user/User.java | 4 ++++ .../web/routes/user/UserWebServiceTest.java | 18 ++++++++++++++++++ 4 files changed, 31 insertions(+) create mode 100644 api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 6ec626ef..2e70995c 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -5,6 +5,7 @@ import vook.server.api.app.user.data.CompleteOnboardingCommand; import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.app.user.data.SignUpFromSocialCommand; +import vook.server.api.app.user.exception.NotReadyToOnboardingException; import vook.server.api.app.user.repo.SocialUserRepository; import vook.server.api.app.user.repo.UserInfoRepository; import vook.server.api.app.user.repo.UserRepository; @@ -57,6 +58,10 @@ public void register(RegisterCommand command) { public void completeOnboarding(CompleteOnboardingCommand command) { User user = repository.findByUid(command.getUserUid()).orElseThrow(); + if (!user.isReadyToOnboarding()) { + throw new NotReadyToOnboardingException(); + } + user.onboardingCompleted(); user.getUserInfo().addOnboardingInfo(command.getFunnel(), command.getJob()); diff --git a/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java b/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java new file mode 100644 index 00000000..072078af --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java @@ -0,0 +1,4 @@ +package vook.server.api.app.user.exception; + +public class NotReadyToOnboardingException extends RuntimeException { +} diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index cfa454b7..7115ecfc 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -58,4 +58,8 @@ public void registered() { public void onboardingCompleted() { this.status = UserStatus.ONBOARDING_COMPLETED; } + + public boolean isReadyToOnboarding() { + return status == UserStatus.REGISTERED; + } } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 7f3dc724..f32684de 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; +import vook.server.api.app.user.exception.NotReadyToOnboardingException; import vook.server.api.model.user.Funnel; import vook.server.api.model.user.Job; import vook.server.api.model.user.User; @@ -17,6 +18,7 @@ import vook.server.api.web.routes.user.reqres.UserRegisterRequest; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @Transactional class UserWebServiceTest extends IntegrationTestBase { @@ -127,4 +129,20 @@ void onboardingComplete1() { assertThat(user.getUserInfo().getFunnel()).isEqualTo(request.getFunnel()); assertThat(user.getUserInfo().getJob()).isEqualTo(request.getJob()); } + + @Test + @DisplayName("온보딩 완료 - 에러; 미 가입 유저") + void onboardingCompleteError1() { + // given + User unregisteredUser = testDataCreator.createUnregisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); + + UserOnboardingCompleteRequest request = new UserOnboardingCompleteRequest(); + request.setFunnel(Funnel.OTHER); + request.setJob(Job.OTHER); + + // when + assertThatThrownBy(() -> userWebService.onboardingComplete(vookLoginUser, request)) + .isInstanceOf(NotReadyToOnboardingException.class); + } } From b83e4d479cb93e8c796fe797fa66797170a45bf2 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Sun, 9 Jun 2024 14:05:37 +0900 Subject: [PATCH 104/273] =?UTF-8?q?feat:=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EA=B7=9C=EC=B9=99=20=EC=9C=84=EB=B0=98=20API=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/common/AppException.java | 8 ++++++++ .../exception/NotReadyToOnboardingException.java | 9 ++++++++- .../server/api/web/common/ApiResponseCode.java | 4 +++- .../api/web/common/CommonApiException.java | 16 +++++++++++++++- .../web/common/GlobalRestControllerAdvice.java | 8 ++++++++ .../vook/server/api/web/routes/user/UserApi.java | 5 ++++- .../api/web/swagger/ComponentRefConsts.java | 1 + .../web/swagger/GlobalOpenApiCustomizerImpl.java | 8 ++++++++ 8 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/common/AppException.java diff --git a/api/src/main/java/vook/server/api/app/common/AppException.java b/api/src/main/java/vook/server/api/app/common/AppException.java new file mode 100644 index 00000000..80ac452b --- /dev/null +++ b/api/src/main/java/vook/server/api/app/common/AppException.java @@ -0,0 +1,8 @@ +package vook.server.api.app.common; + +import lombok.Getter; + +@Getter +public abstract class AppException extends RuntimeException { + public abstract String contents(); +} diff --git a/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java b/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java index 072078af..0cba64e1 100644 --- a/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java +++ b/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java @@ -1,4 +1,11 @@ package vook.server.api.app.user.exception; -public class NotReadyToOnboardingException extends RuntimeException { +import vook.server.api.app.common.AppException; + +public class NotReadyToOnboardingException extends AppException { + + @Override + public String contents() { + return "NotReadyToOnboarding"; + } } diff --git a/api/src/main/java/vook/server/api/web/common/ApiResponseCode.java b/api/src/main/java/vook/server/api/web/common/ApiResponseCode.java index 8d3d2310..65205ce7 100644 --- a/api/src/main/java/vook/server/api/web/common/ApiResponseCode.java +++ b/api/src/main/java/vook/server/api/web/common/ApiResponseCode.java @@ -16,7 +16,9 @@ public String code() { enum BadRequest implements ApiResponseCode { - INVALID_PARAMETER; + INVALID_PARAMETER, + VIOLATION_BUSINESS_RULE, + ; @Override public String code() { diff --git a/api/src/main/java/vook/server/api/web/common/CommonApiException.java b/api/src/main/java/vook/server/api/web/common/CommonApiException.java index a4934b73..f8ae1528 100644 --- a/api/src/main/java/vook/server/api/web/common/CommonApiException.java +++ b/api/src/main/java/vook/server/api/web/common/CommonApiException.java @@ -4,15 +4,25 @@ public class CommonApiException extends RuntimeException { private final ApiResponseCode code; private final int statusCode; + private final String message; CommonApiException(ApiResponseCode code, int statusCode, Throwable cause) { + this(code, statusCode, cause, null); + } + + CommonApiException(ApiResponseCode code, int statusCode, Throwable cause, String message) { super(code.code(), cause); this.code = code; this.statusCode = statusCode; + this.message = message; } public CommonApiResponse response() { - return CommonApiResponse.noResult(code); + if (message == null) { + return CommonApiResponse.noResult(code); + } else { + return CommonApiResponse.withResult(code, message); + } } public int statusCode() { @@ -23,6 +33,10 @@ public static CommonApiException badRequest(ApiResponseCode code, Throwable caus return new CommonApiException(code, 400, cause); } + public static CommonApiException badRequest(ApiResponseCode code, Throwable cause, String message) { + return new CommonApiException(code, 400, cause, message); + } + public static CommonApiException serverError(ApiResponseCode code, Throwable cause) { return new CommonApiException(code, 500, cause); } diff --git a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java index 67ebd2c1..5498c589 100644 --- a/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java +++ b/api/src/main/java/vook/server/api/web/common/GlobalRestControllerAdvice.java @@ -6,11 +6,19 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import vook.server.api.app.common.AppException; @Slf4j @RestControllerAdvice public class GlobalRestControllerAdvice { + @ExceptionHandler(AppException.class) + public ResponseEntity handleAppException(AppException e) { + log.error(e.getMessage(), e); + CommonApiException badRequest = CommonApiException.badRequest(ApiResponseCode.BadRequest.VIOLATION_BUSINESS_RULE, e, e.contents()); + return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response()); + } + @ExceptionHandler(CommonApiException.class) public ResponseEntity handleCommonApiException(CommonApiException e) { log.error(e.getMessage(), e); diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index e15959fa..5c958334 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -68,7 +68,10 @@ class UserApiUerInfoResponse extends CommonApiResponse { content = @Content( mediaType = "application/json", schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), - examples = @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER) + examples = { + @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER), + @ExampleObject(name = "비즈니스 규칙 위반", ref = ComponentRefConsts.Example.VIOLATION_BUSINESS_RULE) + } ) ), }) diff --git a/api/src/main/java/vook/server/api/web/swagger/ComponentRefConsts.java b/api/src/main/java/vook/server/api/web/swagger/ComponentRefConsts.java index 12890cc7..ec326039 100644 --- a/api/src/main/java/vook/server/api/web/swagger/ComponentRefConsts.java +++ b/api/src/main/java/vook/server/api/web/swagger/ComponentRefConsts.java @@ -13,5 +13,6 @@ public static class Example { public static final String SUCCESS = "#/components/examples/Success"; public static final String INVALID_PARAMETER = "#/components/examples/InvalidParameter"; public static final String UNHANDLED_ERROR = "#/components/examples/UnhandledError"; + public static final String VIOLATION_BUSINESS_RULE = "#/components/examples/ViolationBusinessRule"; } } diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java index 13472111..3554438b 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java @@ -35,6 +35,14 @@ private static void applyCommonApiResponseSchema(OpenAPI openApi) { "code": "%s" }""", ApiResponseCode.BadRequest.INVALID_PARAMETER.code())) ) + .addExamples(getKey(ComponentRefConsts.Example.VIOLATION_BUSINESS_RULE), new Example() + .description("비즈니스 규칙 위반") + .value(String.format(""" + { + "code": "%s", + "result": "규칙 위반 내용 (ex. NotReadyToOnboarding)" + }""", ApiResponseCode.BadRequest.VIOLATION_BUSINESS_RULE.code())) + ) .addExamples(getKey(ComponentRefConsts.Example.UNHANDLED_ERROR), new Example() .description("처리되지 않은 서버 에러") .value(String.format(""" From bbd1b475b9b2299e78d12b404d6035b465624614 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 10 Jun 2024 00:16:15 +0900 Subject: [PATCH 105/273] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=EC=83=81=ED=83=9C=EB=A5=BC=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20#71?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/app/user/UserService.java | 4 +--- api/src/main/java/vook/server/api/model/user/User.java | 9 +++++++-- .../main/java/vook/server/api/model/user/UserStatus.java | 1 - .../api/web/routes/user/reqres/UserInfoResponse.java | 2 ++ .../server/api/web/routes/user/UserWebServiceTest.java | 9 +++++++-- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 2e70995c..afe1b8ec 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -62,8 +62,6 @@ public void completeOnboarding(CompleteOnboardingCommand command) { throw new NotReadyToOnboardingException(); } - user.onboardingCompleted(); - - user.getUserInfo().addOnboardingInfo(command.getFunnel(), command.getJob()); + user.onboardingCompleted(command.getFunnel(), command.getJob()); } } diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 7115ecfc..1319b4b8 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -25,6 +25,9 @@ public class User { @Enumerated(EnumType.STRING) private UserStatus status; + @Column(nullable = false) + private Boolean onboardingCompleted; + private LocalDateTime deletedAt; @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) @@ -40,6 +43,7 @@ public static User forSignUpFromSocialOf( user.uid = UUID.randomUUID().toString(); user.email = email; user.status = UserStatus.SOCIAL_LOGIN_COMPLETED; + user.onboardingCompleted = false; return user; } @@ -55,8 +59,9 @@ public void registered() { this.status = UserStatus.REGISTERED; } - public void onboardingCompleted() { - this.status = UserStatus.ONBOARDING_COMPLETED; + public void onboardingCompleted(Funnel funnel, Job job) { + this.onboardingCompleted = true; + this.userInfo.addOnboardingInfo(funnel, job); } public boolean isReadyToOnboarding() { diff --git a/api/src/main/java/vook/server/api/model/user/UserStatus.java b/api/src/main/java/vook/server/api/model/user/UserStatus.java index 2ca26d2b..4169a6e9 100644 --- a/api/src/main/java/vook/server/api/model/user/UserStatus.java +++ b/api/src/main/java/vook/server/api/model/user/UserStatus.java @@ -3,6 +3,5 @@ public enum UserStatus { SOCIAL_LOGIN_COMPLETED, // 소셜로그인 완료됨 REGISTERED, // 가입 됨 - ONBOARDING_COMPLETED, // 온보딩 완료 됨 WITHDRAWN // 탈퇴 됨 } diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserInfoResponse.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserInfoResponse.java index 23afd032..698ec340 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserInfoResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserInfoResponse.java @@ -11,6 +11,7 @@ public class UserInfoResponse { private String uid; private String email; private UserStatus status; + private Boolean onboardingCompleted; private String nickname; public static UserInfoResponse from(User user) { @@ -18,6 +19,7 @@ public static UserInfoResponse from(User user) { result.uid = user.getUid(); result.email = user.getEmail(); result.status = user.getStatus(); + result.onboardingCompleted = user.getOnboardingCompleted(); UserInfo userInfo = user.getUserInfo(); if (userInfo != null) { result.nickname = userInfo.getNickname(); diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index f32684de..61985bf6 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -47,6 +47,7 @@ void userInfo1() { assertThat(response.getEmail()).isEqualTo(unregisteredUser.getEmail()); assertThat(response.getNickname()).isNull(); assertThat(response.getStatus()).isEqualTo(UserStatus.SOCIAL_LOGIN_COMPLETED); + assertThat(response.getOnboardingCompleted()).isFalse(); } @Test @@ -65,6 +66,7 @@ void userInfo2() { assertThat(response.getEmail()).isEqualTo(registeredUser.getEmail()); assertThat(response.getNickname()).isEqualTo(registeredUser.getUserInfo().getNickname()); assertThat(response.getStatus()).isEqualTo(UserStatus.REGISTERED); + assertThat(response.getOnboardingCompleted()).isFalse(); } @Test @@ -82,7 +84,8 @@ void userInfo3() { assertThat(response.getUid()).isEqualTo(completedOnboardingUser.getUid()); assertThat(response.getEmail()).isEqualTo(completedOnboardingUser.getEmail()); assertThat(response.getNickname()).isEqualTo(completedOnboardingUser.getUserInfo().getNickname()); - assertThat(response.getStatus()).isEqualTo(UserStatus.ONBOARDING_COMPLETED); + assertThat(response.getStatus()).isEqualTo(UserStatus.REGISTERED); + assertThat(response.getOnboardingCompleted()).isTrue(); } @Test @@ -103,6 +106,7 @@ void register1() { // then User user = userService.findByUid(unregisteredUser.getUid()).orElseThrow(); assertThat(user.getStatus()).isEqualTo(UserStatus.REGISTERED); + assertThat(user.getOnboardingCompleted()).isFalse(); assertThat(user.getUserInfo()).isNotNull(); assertThat(user.getUserInfo().getNickname()).isEqualTo(request.getNickname()); assertThat(user.getUserInfo().getMarketingEmailOptIn()).isEqualTo(request.isMarketingEmailOptIn()); @@ -124,7 +128,8 @@ void onboardingComplete1() { // then User user = userService.findByUid(registeredUser.getUid()).orElseThrow(); - assertThat(user.getStatus()).isEqualTo(UserStatus.ONBOARDING_COMPLETED); + assertThat(user.getStatus()).isEqualTo(UserStatus.REGISTERED); + assertThat(user.getOnboardingCompleted()).isTrue(); assertThat(user.getUserInfo()).isNotNull(); assertThat(user.getUserInfo().getFunnel()).isEqualTo(request.getFunnel()); assertThat(user.getUserInfo().getJob()).isEqualTo(request.getJob()); From af31d2ee5f8c5eacdc69e6e1aa5a46d5341f5181 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 10 Jun 2024 00:57:59 +0900 Subject: [PATCH 106/273] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20API=20Path=20=EB=B3=80=EA=B2=BD=20#71?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 6 +++--- ...rdingCommand.java => OnboardingCommand.java} | 6 +++--- .../java/vook/server/api/model/user/User.java | 2 +- .../server/api/web/routes/user/UserApi.java | 4 ++-- .../api/web/routes/user/UserRestController.java | 10 +++++----- .../api/web/routes/user/UserWebService.java | 6 +++--- .../reqres/UserOnboardingCompleteRequest.java | 17 ----------------- .../user/reqres/UserOnboardingRequest.java | 17 +++++++++++++++++ .../server/api/testhelper/TestDataCreator.java | 4 ++-- .../api/web/routes/user/UserWebServiceTest.java | 14 +++++++------- 10 files changed, 43 insertions(+), 43 deletions(-) rename api/src/main/java/vook/server/api/app/user/data/{CompleteOnboardingCommand.java => OnboardingCommand.java} (72%) delete mode 100644 api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java create mode 100644 api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingRequest.java diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index afe1b8ec..6fe937eb 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import vook.server.api.app.user.data.CompleteOnboardingCommand; +import vook.server.api.app.user.data.OnboardingCommand; import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.app.user.data.SignUpFromSocialCommand; import vook.server.api.app.user.exception.NotReadyToOnboardingException; @@ -56,12 +56,12 @@ public void register(RegisterCommand command) { user.registered(); } - public void completeOnboarding(CompleteOnboardingCommand command) { + public void onboarding(OnboardingCommand command) { User user = repository.findByUid(command.getUserUid()).orElseThrow(); if (!user.isReadyToOnboarding()) { throw new NotReadyToOnboardingException(); } - user.onboardingCompleted(command.getFunnel(), command.getJob()); + user.onboarding(command.getFunnel(), command.getJob()); } } diff --git a/api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java b/api/src/main/java/vook/server/api/app/user/data/OnboardingCommand.java similarity index 72% rename from api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java rename to api/src/main/java/vook/server/api/app/user/data/OnboardingCommand.java index f8da1a54..a0d4813c 100644 --- a/api/src/main/java/vook/server/api/app/user/data/CompleteOnboardingCommand.java +++ b/api/src/main/java/vook/server/api/app/user/data/OnboardingCommand.java @@ -5,18 +5,18 @@ import vook.server.api.model.user.Job; @Getter -public class CompleteOnboardingCommand { +public class OnboardingCommand { public String userUid; public Funnel funnel; public Job job; - public static CompleteOnboardingCommand of( + public static OnboardingCommand of( String userUid, Funnel funnel, Job job ) { - CompleteOnboardingCommand command = new CompleteOnboardingCommand(); + OnboardingCommand command = new OnboardingCommand(); command.userUid = userUid; command.funnel = funnel; command.job = job; diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 1319b4b8..071927c0 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -59,7 +59,7 @@ public void registered() { this.status = UserStatus.REGISTERED; } - public void onboardingCompleted(Funnel funnel, Job job) { + public void onboarding(Funnel funnel, Job job) { this.onboardingCompleted = true; this.userInfo.addOnboardingInfo(funnel, job); } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 5c958334..9e7fcdf2 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -11,7 +11,7 @@ import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; import vook.server.api.web.routes.user.reqres.UserInfoResponse; -import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; +import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; import vook.server.api.web.swagger.ComponentRefConsts; @@ -75,5 +75,5 @@ class UserApiUerInfoResponse extends CommonApiResponse { ) ), }) - CommonApiResponse onboardingComplete(VookLoginUser user, UserOnboardingCompleteRequest request); + CommonApiResponse onboarding(VookLoginUser user, UserOnboardingRequest request); } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index cf58c08a..f31fcddb 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -8,7 +8,7 @@ import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; import vook.server.api.web.routes.user.reqres.UserInfoResponse; -import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; +import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; @Slf4j @@ -39,12 +39,12 @@ public CommonApiResponse register( } @Override - @PostMapping("/onboarding/complete") - public CommonApiResponse onboardingComplete( + @PostMapping("/onboarding") + public CommonApiResponse onboarding( @AuthenticationPrincipal VookLoginUser user, - @RequestBody UserOnboardingCompleteRequest request + @RequestBody UserOnboardingRequest request ) { - service.onboardingComplete(user, request); + service.onboarding(user, request); return CommonApiResponse.ok(); } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 1226b50c..1c57d4fd 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -7,7 +7,7 @@ import vook.server.api.model.user.User; import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.routes.user.reqres.UserInfoResponse; -import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; +import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; @Service @@ -26,7 +26,7 @@ public void register(VookLoginUser loginUser, UserRegisterRequest request) { userService.register(request.toCommand(loginUser.getUid())); } - public void onboardingComplete(VookLoginUser loginUser, UserOnboardingCompleteRequest request) { - userService.completeOnboarding(request.toCommand(loginUser.getUid())); + public void onboarding(VookLoginUser loginUser, UserOnboardingRequest request) { + userService.onboarding(request.toCommand(loginUser.getUid())); } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java deleted file mode 100644 index 994e50b5..00000000 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingCompleteRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package vook.server.api.web.routes.user.reqres; - -import lombok.Data; -import vook.server.api.app.user.data.CompleteOnboardingCommand; -import vook.server.api.model.user.Funnel; -import vook.server.api.model.user.Job; - -@Data -public class UserOnboardingCompleteRequest { - - public Funnel funnel; - public Job job; - - public CompleteOnboardingCommand toCommand(String uid) { - return CompleteOnboardingCommand.of(uid, funnel, job); - } -} diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingRequest.java new file mode 100644 index 00000000..4ce5344c --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingRequest.java @@ -0,0 +1,17 @@ +package vook.server.api.web.routes.user.reqres; + +import lombok.Data; +import vook.server.api.app.user.data.OnboardingCommand; +import vook.server.api.model.user.Funnel; +import vook.server.api.model.user.Job; + +@Data +public class UserOnboardingRequest { + + public Funnel funnel; + public Job job; + + public OnboardingCommand toCommand(String uid) { + return OnboardingCommand.of(uid, funnel, job); + } +} diff --git a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java index 272a7f48..3032b7fb 100644 --- a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java +++ b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java @@ -4,7 +4,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; -import vook.server.api.app.user.data.CompleteOnboardingCommand; +import vook.server.api.app.user.data.OnboardingCommand; import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.app.user.data.SignUpFromSocialCommand; import vook.server.api.model.user.Funnel; @@ -37,7 +37,7 @@ public User createRegisteredUser() { public User createCompletedOnboardingUser() { User user = createRegisteredUser(); - userService.completeOnboarding(CompleteOnboardingCommand.of(user.getUid(), Funnel.OTHER, Job.OTHER)); + userService.onboarding(OnboardingCommand.of(user.getUid(), Funnel.OTHER, Job.OTHER)); return userService.findByUid(user.getUid()).orElseThrow(); } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 61985bf6..1a2b73ec 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -14,7 +14,7 @@ import vook.server.api.testhelper.TestDataCreator; import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.routes.user.reqres.UserInfoResponse; -import vook.server.api.web.routes.user.reqres.UserOnboardingCompleteRequest; +import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -114,17 +114,17 @@ void register1() { @Test @DisplayName("온보딩 완료 - 정상") - void onboardingComplete1() { + void onboarding1() { // given User registeredUser = testDataCreator.createRegisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); - UserOnboardingCompleteRequest request = new UserOnboardingCompleteRequest(); + UserOnboardingRequest request = new UserOnboardingRequest(); request.setFunnel(Funnel.OTHER); request.setJob(Job.OTHER); // when - userWebService.onboardingComplete(vookLoginUser, request); + userWebService.onboarding(vookLoginUser, request); // then User user = userService.findByUid(registeredUser.getUid()).orElseThrow(); @@ -137,17 +137,17 @@ void onboardingComplete1() { @Test @DisplayName("온보딩 완료 - 에러; 미 가입 유저") - void onboardingCompleteError1() { + void onboardingError1() { // given User unregisteredUser = testDataCreator.createUnregisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); - UserOnboardingCompleteRequest request = new UserOnboardingCompleteRequest(); + UserOnboardingRequest request = new UserOnboardingRequest(); request.setFunnel(Funnel.OTHER); request.setJob(Job.OTHER); // when - assertThatThrownBy(() -> userWebService.onboardingComplete(vookLoginUser, request)) + assertThatThrownBy(() -> userWebService.onboarding(vookLoginUser, request)) .isInstanceOf(NotReadyToOnboardingException.class); } } From b4809f1086eeace74756ca2d1fc957af46f23051 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 10 Jun 2024 01:05:10 +0900 Subject: [PATCH 107/273] =?UTF-8?q?docs:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20API=EC=97=90=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EA=B7=9C=EC=B9=99=20=EC=9C=84=EB=B0=98=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=B6=94=EA=B0=80=20#71?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/web/routes/user/UserApi.java | 5 ++++- .../server/api/web/swagger/GlobalOpenApiCustomizerImpl.java | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 9e7fcdf2..36412659 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -60,7 +60,10 @@ class UserApiUerInfoResponse extends CommonApiResponse { summary = "온보딩 완료", security = { @SecurityRequirement(name = "AccessToken") - } + }, + description = """ + 비즈니스 규칙 위반 내용 + - NotReadyToOnboarding: 회원 가입이 완료되지 않은 유저가 해당 API를 호출 할 경우""" ) @ApiResponses(value = { @ApiResponse( diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java index 3554438b..5babcca5 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java @@ -40,7 +40,7 @@ private static void applyCommonApiResponseSchema(OpenAPI openApi) { .value(String.format(""" { "code": "%s", - "result": "규칙 위반 내용 (ex. NotReadyToOnboarding)" + "result": "규칙 위반 내용" }""", ApiResponseCode.BadRequest.VIOLATION_BUSINESS_RULE.code())) ) .addExamples(getKey(ComponentRefConsts.Example.UNHANDLED_ERROR), new Example() From 7d3bab3bbe16a32bbfa90b5092314d8e7f14d21e Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 10 Jun 2024 01:12:24 +0900 Subject: [PATCH 108/273] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=EC=9D=B4=20=EB=90=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EC=9D=B4=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20API?= =?UTF-8?q?=EB=A5=BC=20=ED=98=B8=EC=B6=9C=ED=95=98=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20#72?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 13 +++++++------ .../exception/AlreadyRegisteredException.java | 11 +++++++++++ .../java/vook/server/api/model/user/User.java | 11 ++++++----- .../server/api/web/routes/user/UserApi.java | 11 +++++++++-- .../web/routes/user/UserWebServiceTest.java | 18 ++++++++++++++++++ 5 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/user/exception/AlreadyRegisteredException.java diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 6fe937eb..d9371db3 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -5,6 +5,7 @@ import vook.server.api.app.user.data.OnboardingCommand; import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.app.user.data.SignUpFromSocialCommand; +import vook.server.api.app.user.exception.AlreadyRegisteredException; import vook.server.api.app.user.exception.NotReadyToOnboardingException; import vook.server.api.app.user.repo.SocialUserRepository; import vook.server.api.app.user.repo.UserInfoRepository; @@ -44,16 +45,16 @@ public Optional findByUid(String uid) { public void register(RegisterCommand command) { User user = repository.findByUid(command.getUserUid()).orElseThrow(); + if (user.isRegistered()) { + throw new AlreadyRegisteredException(); + } - UserInfo userInfo = UserInfo.forRegisterOf( + UserInfo userInfo = userInfoRepository.save(UserInfo.forRegisterOf( command.getNickname(), user, command.isMarketingEmailOptIn() - ); - UserInfo savedUserInfo = userInfoRepository.save(userInfo); - user.addUserInfo(savedUserInfo); - - user.registered(); + )); + user.register(userInfo); } public void onboarding(OnboardingCommand command) { diff --git a/api/src/main/java/vook/server/api/app/user/exception/AlreadyRegisteredException.java b/api/src/main/java/vook/server/api/app/user/exception/AlreadyRegisteredException.java new file mode 100644 index 00000000..a7f2d1c0 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/exception/AlreadyRegisteredException.java @@ -0,0 +1,11 @@ +package vook.server.api.app.user.exception; + +import vook.server.api.app.common.AppException; + +public class AlreadyRegisteredException extends AppException { + + @Override + public String contents() { + return "AlreadyRegistered"; + } +} diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 071927c0..19627d72 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -51,12 +51,9 @@ public void addSocialUser(SocialUser socialUser) { socialUsers.add(socialUser); } - public void addUserInfo(UserInfo userInfo) { - this.userInfo = userInfo; - } - - public void registered() { + public void register(UserInfo userInfo) { this.status = UserStatus.REGISTERED; + this.userInfo = userInfo; } public void onboarding(Funnel funnel, Job job) { @@ -67,4 +64,8 @@ public void onboarding(Funnel funnel, Job job) { public boolean isReadyToOnboarding() { return status == UserStatus.REGISTERED; } + + public boolean isRegistered() { + return status == UserStatus.REGISTERED; + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 36412659..68bea73d 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -42,7 +42,11 @@ class UserApiUerInfoResponse extends CommonApiResponse { summary = "회원가입", security = { @SecurityRequirement(name = "AccessToken") - } + }, + description = """ + 비즈니스 규칙 위반 내용 + - AlreadyRegistered: 이미 회원가입이 완료된 유저가 해당 API를 호출 할 경우 + """ ) @ApiResponses(value = { @ApiResponse( @@ -50,7 +54,10 @@ class UserApiUerInfoResponse extends CommonApiResponse { content = @Content( mediaType = "application/json", schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), - examples = @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER) + examples = { + @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER), + @ExampleObject(name = "비즈니스 규칙 위반", ref = ComponentRefConsts.Example.VIOLATION_BUSINESS_RULE) + } ) ), }) diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 1a2b73ec..c329b49a 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; +import vook.server.api.app.user.exception.AlreadyRegisteredException; import vook.server.api.app.user.exception.NotReadyToOnboardingException; import vook.server.api.model.user.Funnel; import vook.server.api.model.user.Job; @@ -112,6 +113,23 @@ void register1() { assertThat(user.getUserInfo().getMarketingEmailOptIn()).isEqualTo(request.isMarketingEmailOptIn()); } + @Test + @DisplayName("회원 가입 - 에러; 이미 가입된 유저") + void registerError1() { + // given + User registeredUser = testDataCreator.createRegisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); + + UserRegisterRequest request = new UserRegisterRequest(); + request.setNickname("nickname"); + request.setRequiredTermsAgree(true); + request.setMarketingEmailOptIn(true); + + // when + assertThatThrownBy(() -> userWebService.register(vookLoginUser, request)) + .isInstanceOf(AlreadyRegisteredException.class); + } + @Test @DisplayName("온보딩 완료 - 정상") void onboarding1() { From fdfb80c23241017e66730b791b3686533dff78c8 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 10 Jun 2024 01:16:02 +0900 Subject: [PATCH 109/273] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=20=EC=98=A8?= =?UTF-8?q?=EB=B3=B4=EB=94=A9=EC=9D=B4=20=EC=99=84=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EC=9D=B4=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20API=EB=A5=BC=20=ED=98=B8=EC=B6=9C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20#71?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 4 ++++ .../exception/AlreadyOnboardingException.java | 11 +++++++++++ .../server/api/web/routes/user/UserApi.java | 3 ++- .../api/web/routes/user/UserWebServiceTest.java | 17 +++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/vook/server/api/app/user/exception/AlreadyOnboardingException.java diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index d9371db3..6a78d715 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -5,6 +5,7 @@ import vook.server.api.app.user.data.OnboardingCommand; import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.app.user.data.SignUpFromSocialCommand; +import vook.server.api.app.user.exception.AlreadyOnboardingException; import vook.server.api.app.user.exception.AlreadyRegisteredException; import vook.server.api.app.user.exception.NotReadyToOnboardingException; import vook.server.api.app.user.repo.SocialUserRepository; @@ -62,6 +63,9 @@ public void onboarding(OnboardingCommand command) { if (!user.isReadyToOnboarding()) { throw new NotReadyToOnboardingException(); } + if (user.getOnboardingCompleted()) { + throw new AlreadyOnboardingException(); + } user.onboarding(command.getFunnel(), command.getJob()); } diff --git a/api/src/main/java/vook/server/api/app/user/exception/AlreadyOnboardingException.java b/api/src/main/java/vook/server/api/app/user/exception/AlreadyOnboardingException.java new file mode 100644 index 00000000..068e2b9b --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/exception/AlreadyOnboardingException.java @@ -0,0 +1,11 @@ +package vook.server.api.app.user.exception; + +import vook.server.api.app.common.AppException; + +public class AlreadyOnboardingException extends AppException { + + @Override + public String contents() { + return "AlreadyOnboarding"; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 68bea73d..a1084251 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -70,7 +70,8 @@ class UserApiUerInfoResponse extends CommonApiResponse { }, description = """ 비즈니스 규칙 위반 내용 - - NotReadyToOnboarding: 회원 가입이 완료되지 않은 유저가 해당 API를 호출 할 경우""" + - NotReadyToOnboarding: 회원 가입이 완료되지 않은 유저가 해당 API를 호출 할 경우 + - AlreadyOnboarding: 이미 온보딩이 완료된 유저가 해당 API를 호출 할 경우""" ) @ApiResponses(value = { @ApiResponse( diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index c329b49a..9d1cd093 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; +import vook.server.api.app.user.exception.AlreadyOnboardingException; import vook.server.api.app.user.exception.AlreadyRegisteredException; import vook.server.api.app.user.exception.NotReadyToOnboardingException; import vook.server.api.model.user.Funnel; @@ -168,4 +169,20 @@ void onboardingError1() { assertThatThrownBy(() -> userWebService.onboarding(vookLoginUser, request)) .isInstanceOf(NotReadyToOnboardingException.class); } + + @Test + @DisplayName("온보딩 완료 - 에러; 이미 온보딩 완료된 유저") + void onboardingError2() { + // given + User completedOnboardingUser = testDataCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(completedOnboardingUser.getUid()); + + UserOnboardingRequest request = new UserOnboardingRequest(); + request.setFunnel(Funnel.OTHER); + request.setJob(Job.OTHER); + + // when + assertThatThrownBy(() -> userWebService.onboarding(vookLoginUser, request)) + .isInstanceOf(AlreadyOnboardingException.class); + } } From 4ee4332188de25864857e4e1fe79d51b57f38ad3 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 10 Jun 2024 01:39:46 +0900 Subject: [PATCH 110/273] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=EC=9D=98=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#72?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 2 +- .../api/app/user/data/RegisterCommand.java | 4 +- .../vook/server/api/model/user/UserInfo.java | 2 +- .../user/reqres/UserRegisterRequest.java | 13 +++- .../routes/user/UserRestControllerTest.java | 65 +++++++++++++++---- .../web/routes/user/UserWebServiceTest.java | 2 +- 6 files changed, 69 insertions(+), 19 deletions(-) diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 6a78d715..aa768792 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -53,7 +53,7 @@ public void register(RegisterCommand command) { UserInfo userInfo = userInfoRepository.save(UserInfo.forRegisterOf( command.getNickname(), user, - command.isMarketingEmailOptIn() + command.getMarketingEmailOptIn() )); user.register(userInfo); } diff --git a/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java b/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java index e1fe00e5..c6acee84 100644 --- a/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java +++ b/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java @@ -7,12 +7,12 @@ public class RegisterCommand { private String userUid; private String nickname; - private boolean marketingEmailOptIn; + private Boolean marketingEmailOptIn; public static RegisterCommand of( String userUid, String nickname, - boolean marketingEmailOptIn + Boolean marketingEmailOptIn ) { RegisterCommand command = new RegisterCommand(); command.userUid = userUid; diff --git a/api/src/main/java/vook/server/api/model/user/UserInfo.java b/api/src/main/java/vook/server/api/model/user/UserInfo.java index a3444a75..1e549c5a 100644 --- a/api/src/main/java/vook/server/api/model/user/UserInfo.java +++ b/api/src/main/java/vook/server/api/model/user/UserInfo.java @@ -29,7 +29,7 @@ public class UserInfo { public static UserInfo forRegisterOf( String nickname, User user, - boolean marketingEmailOptIn + Boolean marketingEmailOptIn ) { UserInfo result = new UserInfo(); result.nickname = nickname; diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java index 1eec34c9..16b27fa8 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java @@ -1,6 +1,10 @@ package vook.server.api.web.routes.user.reqres; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Data; import vook.server.api.app.user.data.RegisterCommand; @@ -8,11 +12,16 @@ public class UserRegisterRequest { @NotBlank + @Size(min = 1, max = 10) private String nickname; - private boolean requiredTermsAgree; + @NotNull + @AssertTrue + @Schema(allowableValues = {"true"}) + private Boolean requiredTermsAgree; - private boolean marketingEmailOptIn; + @NotNull + private Boolean marketingEmailOptIn; public RegisterCommand toCommand(String userUid) { return RegisterCommand.of( diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java index 625c83f5..404a2534 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java @@ -1,18 +1,24 @@ package vook.server.api.web.routes.user; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import vook.server.api.model.user.User; import vook.server.api.testhelper.HttpEntityBuilder; import vook.server.api.testhelper.IntegrationTestBase; import vook.server.api.testhelper.TestDataCreator; import vook.server.api.web.auth.data.GeneratedToken; +import java.util.Collection; +import java.util.List; import java.util.Map; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; @@ -58,8 +64,9 @@ void register() { new HttpEntityBuilder() .header("Authorization", "Bearer " + token.getAccessToken()) .body(Map.of( - "nickname", "testNickname", - "onboardingComplete", true + "nickname", "testName", + "requiredTermsAgree", true, + "marketingEmailOptIn", true )) .build(), String.class @@ -69,27 +76,61 @@ void register() { assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); } - @Test - @DisplayName("회원 가입 - 닉네임 누락") - void registerWithoutNickname() { + @TestFactory + @DisplayName("회원 가입 - 실패") + Collection registerError() { // given User unregisteredUser = testDataCreator.createUnregisteredUser(); GeneratedToken token = testDataCreator.createToken(unregisteredUser); - // when - var res = rest.exchange( + Function, ResponseEntity> restExchange = body -> rest.exchange( "/user/register", HttpMethod.POST, new HttpEntityBuilder() .header("Authorization", "Bearer " + token.getAccessToken()) - .body(Map.of( - "onboardingComplete", true - )) + .body(body) .build(), String.class ); - // then - assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + return List.of( + DynamicTest.dynamicTest("닉네임 누락", () -> { + var res = restExchange.apply(Map.of( + "requiredTermsAgree", true, + "marketingEmailOptIn", true + )); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }), + DynamicTest.dynamicTest("닉네임 길이 제한 초과", () -> { + var res = restExchange.apply(Map.of( + "nickname", "12345678901", + "requiredTermsAgree", true, + "marketingEmailOptIn", true + )); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }), + DynamicTest.dynamicTest("필수 약관 동의 누락", () -> { + var res = restExchange.apply(Map.of( + "nickname", "testName", + "marketingEmailOptIn", true + )); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }), + DynamicTest.dynamicTest("필수 약관 동의 false", () -> { + var res = restExchange.apply(Map.of( + "nickname", "testName", + "requiredTermsAgree", false, + "marketingEmailOptIn", true + )); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }), + DynamicTest.dynamicTest("마케팅 이메일 수신 동의 누락", () -> { + var res = restExchange.apply(Map.of( + "nickname", "testName", + "requiredTermsAgree", true + )); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }) + ); } } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 9d1cd093..f38cc185 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -111,7 +111,7 @@ void register1() { assertThat(user.getOnboardingCompleted()).isFalse(); assertThat(user.getUserInfo()).isNotNull(); assertThat(user.getUserInfo().getNickname()).isEqualTo(request.getNickname()); - assertThat(user.getUserInfo().getMarketingEmailOptIn()).isEqualTo(request.isMarketingEmailOptIn()); + assertThat(user.getUserInfo().getMarketingEmailOptIn()).isEqualTo(request.getMarketingEmailOptIn()); } @Test From d9f3f68c5b7f3c55826d32a0cb274eaff5078478 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 10 Jun 2024 01:55:07 +0900 Subject: [PATCH 111/273] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EC=9D=98=20?= =?UTF-8?q?=EA=B0=81=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B3=84=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B8=B0=EB=A1=9D=20=EC=B6=94=EA=B0=80=20#73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/model/user/SocialUser.java | 9 +++++++++ api/src/main/java/vook/server/api/model/user/User.java | 8 ++++++++ .../server/api/web/routes/user/UserWebServiceTest.java | 2 ++ 3 files changed, 19 insertions(+) diff --git a/api/src/main/java/vook/server/api/model/user/SocialUser.java b/api/src/main/java/vook/server/api/model/user/SocialUser.java index cf272d66..1b8fbaf7 100644 --- a/api/src/main/java/vook/server/api/model/user/SocialUser.java +++ b/api/src/main/java/vook/server/api/model/user/SocialUser.java @@ -2,10 +2,15 @@ import jakarta.persistence.*; import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; @Getter @Entity @Table(name = "social_user") +@EntityListeners(AuditingEntityListener.class) public class SocialUser { @Id @@ -16,6 +21,10 @@ public class SocialUser { private String providerUserId; + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + @ManyToOne @JoinColumn(name = "user_id") private User user; diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 19627d72..28ded027 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -28,6 +28,12 @@ public class User { @Column(nullable = false) private Boolean onboardingCompleted; + private LocalDateTime registeredAt; + + private LocalDateTime onboardingCompletedAt; + + private LocalDateTime lastUpdatedAt; + private LocalDateTime deletedAt; @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) @@ -54,11 +60,13 @@ public void addSocialUser(SocialUser socialUser) { public void register(UserInfo userInfo) { this.status = UserStatus.REGISTERED; this.userInfo = userInfo; + this.registeredAt = LocalDateTime.now(); } public void onboarding(Funnel funnel, Job job) { this.onboardingCompleted = true; this.userInfo.addOnboardingInfo(funnel, job); + this.onboardingCompletedAt = LocalDateTime.now(); } public boolean isReadyToOnboarding() { diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index f38cc185..eb8927f8 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -109,6 +109,7 @@ void register1() { User user = userService.findByUid(unregisteredUser.getUid()).orElseThrow(); assertThat(user.getStatus()).isEqualTo(UserStatus.REGISTERED); assertThat(user.getOnboardingCompleted()).isFalse(); + assertThat(user.getRegisteredAt()).isNotNull(); assertThat(user.getUserInfo()).isNotNull(); assertThat(user.getUserInfo().getNickname()).isEqualTo(request.getNickname()); assertThat(user.getUserInfo().getMarketingEmailOptIn()).isEqualTo(request.getMarketingEmailOptIn()); @@ -149,6 +150,7 @@ void onboarding1() { User user = userService.findByUid(registeredUser.getUid()).orElseThrow(); assertThat(user.getStatus()).isEqualTo(UserStatus.REGISTERED); assertThat(user.getOnboardingCompleted()).isTrue(); + assertThat(user.getOnboardingCompletedAt()).isNotNull(); assertThat(user.getUserInfo()).isNotNull(); assertThat(user.getUserInfo().getFunnel()).isEqualTo(request.getFunnel()); assertThat(user.getUserInfo().getJob()).isEqualTo(request.getJob()); From e96c71ba811bd94a43ced0060849113460d26492 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Mon, 10 Jun 2024 02:51:59 +0900 Subject: [PATCH 112/273] =?UTF-8?q?refactor:=20GlobalOperationCustomizerIm?= =?UTF-8?q?pl=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GlobalOperationCustomizerImpl.java | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java index b90fe2dd..70547461 100644 --- a/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java +++ b/api/src/main/java/vook/server/api/web/swagger/GlobalOperationCustomizerImpl.java @@ -17,28 +17,12 @@ public class GlobalOperationCustomizerImpl implements GlobalOperationCustomizer @Override public Operation customize(Operation operation, HandlerMethod handlerMethod) { - applyDefaultOkApiResponse(operation); - applyUnauthorizedApiResponse(operation); - applyInternalServerErrorApiResponse(operation); + applyDefaultOkApiResponse(operation); //200 + applyUnauthorizedApiResponse(operation); //401 + applyInternalServerErrorApiResponse(operation); //500 return operation; } - private void applyUnauthorizedApiResponse(Operation operation) { - List security = operation.getSecurity(); - if (security == null) { - return; - } - - security.forEach(sr -> { - if (sr.containsKey("AccessToken")) { - operation.getResponses().computeIfAbsent( - "401", - k -> new ApiResponse().description("Unauthorized") - ); - } - }); - } - private void applyDefaultOkApiResponse(Operation operation) { ApiResponse apiResponse = operation.getResponses().computeIfAbsent( "200", @@ -63,17 +47,23 @@ private void applyDefaultOkApiResponse(Operation operation) { .addExamples("성공", new Example().$ref(ComponentRefConsts.Example.SUCCESS))); } - private static void applyInternalServerErrorApiResponse(Operation operation) { - MediaType jsonType = prepareOrGetJsonMediaType(operation); - - if (jsonType.getSchema() == null) { - jsonType.setSchema(new Schema<>().$ref(ComponentRefConsts.Schema.COMMON_API_RESPONSE)); + private void applyUnauthorizedApiResponse(Operation operation) { + List security = operation.getSecurity(); + if (security == null) { + return; } - jsonType.getExamples().put("처리되지 않은 서버 에러", new Example().$ref(ComponentRefConsts.Example.UNHANDLED_ERROR)); + security.forEach(sr -> { + if (sr.containsKey("AccessToken")) { + operation.getResponses().computeIfAbsent( + "401", + k -> new ApiResponse().description("Unauthorized") + ); + } + }); } - private static MediaType prepareOrGetJsonMediaType(Operation operation) { + private void applyInternalServerErrorApiResponse(Operation operation) { ApiResponse apiResponse = operation.getResponses().computeIfAbsent( "500", k -> new ApiResponse().description("Internal Server Error") @@ -85,10 +75,15 @@ private static MediaType prepareOrGetJsonMediaType(Operation operation) { MediaType jsonType = apiResponse.getContent().computeIfAbsent("application/json", k -> new MediaType()); + if (jsonType.getSchema() == null) { + jsonType.setSchema(new Schema<>().$ref(ComponentRefConsts.Schema.COMMON_API_RESPONSE)); + } + if (jsonType.getExamples() == null) { jsonType.setExamples(new HashMap<>()); } - return jsonType; + jsonType.getExamples().put("처리되지 않은 서버 에러", new Example().$ref(ComponentRefConsts.Example.UNHANDLED_ERROR)); } + } From ea8980364d1ad48febd3812073993d32988f28a1 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 11 Jun 2024 11:47:47 +0900 Subject: [PATCH 113/273] =?UTF-8?q?refactor:=20JWTHelperProvider=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/{helper/jwt => web/auth/app}/JWTHelperProvider.java | 4 +++- .../main/java/vook/server/api/web/auth/app/TokenService.java | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) rename api/src/main/java/vook/server/api/{helper/jwt => web/auth/app}/JWTHelperProvider.java (84%) diff --git a/api/src/main/java/vook/server/api/helper/jwt/JWTHelperProvider.java b/api/src/main/java/vook/server/api/web/auth/app/JWTHelperProvider.java similarity index 84% rename from api/src/main/java/vook/server/api/helper/jwt/JWTHelperProvider.java rename to api/src/main/java/vook/server/api/web/auth/app/JWTHelperProvider.java index 60e88e48..89c22e1c 100644 --- a/api/src/main/java/vook/server/api/helper/jwt/JWTHelperProvider.java +++ b/api/src/main/java/vook/server/api/web/auth/app/JWTHelperProvider.java @@ -1,8 +1,10 @@ -package vook.server.api.helper.jwt; +package vook.server.api.web.auth.app; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import vook.server.api.helper.jwt.JWTReader; +import vook.server.api.helper.jwt.JWTWriter; @Component public class JWTHelperProvider { diff --git a/api/src/main/java/vook/server/api/web/auth/app/TokenService.java b/api/src/main/java/vook/server/api/web/auth/app/TokenService.java index 6005146b..2320d9e0 100644 --- a/api/src/main/java/vook/server/api/web/auth/app/TokenService.java +++ b/api/src/main/java/vook/server/api/web/auth/app/TokenService.java @@ -4,7 +4,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import vook.server.api.helper.jwt.JWTHelperProvider; import vook.server.api.helper.jwt.JWTReader; import vook.server.api.web.auth.data.GeneratedToken; From 04e091c992c788b831e1ceb940414d0d2943af90 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 11 Jun 2024 12:05:32 +0900 Subject: [PATCH 114/273] refactor: JWTHelperProvider#builder -> writer --- .../java/vook/server/api/web/auth/app/JWTHelperProvider.java | 2 +- .../main/java/vook/server/api/web/auth/app/TokenService.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/auth/app/JWTHelperProvider.java b/api/src/main/java/vook/server/api/web/auth/app/JWTHelperProvider.java index 89c22e1c..11430a4b 100644 --- a/api/src/main/java/vook/server/api/web/auth/app/JWTHelperProvider.java +++ b/api/src/main/java/vook/server/api/web/auth/app/JWTHelperProvider.java @@ -21,7 +21,7 @@ public void init() { jwtReaderBuilder = new JWTReader.Builder(jwtSecret); } - public JWTWriter builder() { + public JWTWriter writer() { return jwtWriterBuilder.build(); } diff --git a/api/src/main/java/vook/server/api/web/auth/app/TokenService.java b/api/src/main/java/vook/server/api/web/auth/app/TokenService.java index 2320d9e0..16038ea5 100644 --- a/api/src/main/java/vook/server/api/web/auth/app/TokenService.java +++ b/api/src/main/java/vook/server/api/web/auth/app/TokenService.java @@ -49,7 +49,7 @@ public GeneratedToken refreshToken(String refreshToken) { } private String buildAccessToken(String uid) { - return jwtHelperProvider.builder() + return jwtHelperProvider.writer() .withExpiredMs(1000L * 60 * accessTokenExpiredMinute) .withClaim("category", "access") .withClaim("uid", uid) @@ -57,7 +57,7 @@ private String buildAccessToken(String uid) { } private String buildRefreshToken(String uid) { - return jwtHelperProvider.builder() + return jwtHelperProvider.writer() .withExpiredMs(1000L * 60 * refreshTokenExpiredMinute) .withClaim("category", "refresh") .withClaim("uid", uid) From 16295cdd753e7d8bd398d885fc46f15fdfab909c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 11 Jun 2024 13:10:03 +0900 Subject: [PATCH 115/273] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 5 ++ .../java/vook/server/api/model/user/User.java | 5 ++ .../vook/server/api/model/user/UserInfo.java | 4 ++ .../server/api/web/routes/user/UserApi.java | 20 +++++++ .../web/routes/user/UserRestController.java | 10 ++++ .../routes/user/UserUpdateInfoRequest.java | 13 +++++ .../api/web/routes/user/UserWebService.java | 4 ++ .../routes/user/UserRestControllerTest.java | 57 +++++++++++++++++++ .../web/routes/user/UserWebServiceTest.java | 18 ++++++ 9 files changed, 136 insertions(+) create mode 100644 api/src/main/java/vook/server/api/web/routes/user/UserUpdateInfoRequest.java diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index aa768792..13934d18 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -69,4 +69,9 @@ public void onboarding(OnboardingCommand command) { user.onboarding(command.getFunnel(), command.getJob()); } + + public void updateInfo(String uid, String nickname) { + User user = repository.findByUid(uid).orElseThrow(); + user.update(nickname); + } } diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 28ded027..1d81330a 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -76,4 +76,9 @@ public boolean isReadyToOnboarding() { public boolean isRegistered() { return status == UserStatus.REGISTERED; } + + public void update(String nickname) { + userInfo.update(nickname); + lastUpdatedAt = LocalDateTime.now(); + } } diff --git a/api/src/main/java/vook/server/api/model/user/UserInfo.java b/api/src/main/java/vook/server/api/model/user/UserInfo.java index 1e549c5a..932ce394 100644 --- a/api/src/main/java/vook/server/api/model/user/UserInfo.java +++ b/api/src/main/java/vook/server/api/model/user/UserInfo.java @@ -42,4 +42,8 @@ public void addOnboardingInfo(Funnel funnel, Job job) { this.funnel = funnel; this.job = job; } + + public void update(String nickname) { + this.nickname = nickname; + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index a1084251..2ba8d6fa 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -87,4 +87,24 @@ class UserApiUerInfoResponse extends CommonApiResponse { ), }) CommonApiResponse onboarding(VookLoginUser user, UserOnboardingRequest request); + + @Operation( + summary = "사용자 정보 수정", + security = { + @SecurityRequirement(name = "AccessToken") + } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "400", + content = @Content( + mediaType = "application/json", + schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), + examples = { + @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER), + } + ) + ), + }) + CommonApiResponse updateInfo(VookLoginUser user, UserUpdateInfoRequest request); } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index f31fcddb..2eab3a0e 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -47,4 +47,14 @@ public CommonApiResponse onboarding( service.onboarding(user, request); return CommonApiResponse.ok(); } + + @Override + @PutMapping("/info") + public CommonApiResponse updateInfo( + @AuthenticationPrincipal VookLoginUser user, + @Validated @RequestBody UserUpdateInfoRequest request + ) { + service.updateInfo(user, request); + return CommonApiResponse.ok(); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserUpdateInfoRequest.java b/api/src/main/java/vook/server/api/web/routes/user/UserUpdateInfoRequest.java new file mode 100644 index 00000000..83a21ae5 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/user/UserUpdateInfoRequest.java @@ -0,0 +1,13 @@ +package vook.server.api.web.routes.user; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class UserUpdateInfoRequest { + + @NotBlank + @Size(min = 1, max = 10) + private String nickname; +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 1c57d4fd..5d2df3d5 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -29,4 +29,8 @@ public void register(VookLoginUser loginUser, UserRegisterRequest request) { public void onboarding(VookLoginUser loginUser, UserOnboardingRequest request) { userService.onboarding(request.toCommand(loginUser.getUid())); } + + public void updateInfo(VookLoginUser loginUser, UserUpdateInfoRequest request) { + userService.updateInfo(loginUser.getUid(), request.getNickname()); + } } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java index 404a2534..cb5d7d1f 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java @@ -9,6 +9,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import vook.server.api.model.user.User; import vook.server.api.testhelper.HttpEntityBuilder; import vook.server.api.testhelper.IntegrationTestBase; @@ -22,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@Transactional class UserRestControllerTest extends IntegrationTestBase { @MockBean @@ -133,4 +135,59 @@ Collection registerError() { }) ); } + + @Test + @DisplayName("회원 정보 수정 - 정상") + void updateInfo() { + // given + User registeredUser = testDataCreator.createRegisteredUser(); + GeneratedToken token = testDataCreator.createToken(registeredUser); + + // when + var res = rest.exchange( + "/user/info", + HttpMethod.PUT, + new HttpEntityBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .body(Map.of( + "nickname", "newName" + )) + .build(), + String.class + ); + + // then + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @TestFactory + @DisplayName("회원 정보 수정 - 실패") + Collection updateInfoError() { + // given + User registeredUser = testDataCreator.createRegisteredUser(); + GeneratedToken token = testDataCreator.createToken(registeredUser); + + Function, ResponseEntity> restExchange = body -> rest.exchange( + "/user/info", + HttpMethod.PUT, + new HttpEntityBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .body(body) + .build(), + String.class + ); + + return List.of( + DynamicTest.dynamicTest("닉네임 누락", () -> { + var res = restExchange.apply(Map.of()); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }), + DynamicTest.dynamicTest("닉네임 길이 제한 초과", () -> { + var res = restExchange.apply(Map.of( + "nickname", "12345678901" + )); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }) + ); + } } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index eb8927f8..f2205f7a 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -187,4 +187,22 @@ void onboardingError2() { assertThatThrownBy(() -> userWebService.onboarding(vookLoginUser, request)) .isInstanceOf(AlreadyOnboardingException.class); } + + @Test + @DisplayName("사용자 정보 수정 - 정상") + void updateInfo1() { + // given + User registeredUser = testDataCreator.createRegisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); + UserUpdateInfoRequest request = new UserUpdateInfoRequest(); + request.setNickname("newNickname"); + + // when + userWebService.updateInfo(vookLoginUser, request); + + // then + User user = userService.findByUid(registeredUser.getUid()).orElseThrow(); + assertThat(user.getUserInfo().getNickname()).isEqualTo("newNickname"); + assertThat(user.getLastUpdatedAt()).isNotNull(); + } } From 57ea09b669eb195011e9bb6ba678392e15440ee2 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 11 Jun 2024 13:17:32 +0900 Subject: [PATCH 116/273] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20API=20=EC=B6=94=EA=B0=80=20#77?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 8 ++++++++ .../java/vook/server/api/model/user/User.java | 11 ++++++++++- .../vook/server/api/web/routes/user/UserApi.java | 9 +++++++++ .../api/web/routes/user/UserRestController.java | 9 +++++++++ .../api/web/routes/user/UserWebService.java | 4 ++++ .../api/web/routes/user/UserWebServiceTest.java | 16 ++++++++++++++++ 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 13934d18..f4afc483 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -74,4 +74,12 @@ public void updateInfo(String uid, String nickname) { User user = repository.findByUid(uid).orElseThrow(); user.update(nickname); } + + public void withdraw(String uid) { + User user = repository.findByUid(uid).orElseThrow(); + if (user.isWithdrawn()) { + return; + } + user.withdraw(); + } } diff --git a/api/src/main/java/vook/server/api/model/user/User.java b/api/src/main/java/vook/server/api/model/user/User.java index 1d81330a..c70a6920 100644 --- a/api/src/main/java/vook/server/api/model/user/User.java +++ b/api/src/main/java/vook/server/api/model/user/User.java @@ -34,7 +34,7 @@ public class User { private LocalDateTime lastUpdatedAt; - private LocalDateTime deletedAt; + private LocalDateTime withdrawnAt; @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List socialUsers = new ArrayList<>(); @@ -81,4 +81,13 @@ public void update(String nickname) { userInfo.update(nickname); lastUpdatedAt = LocalDateTime.now(); } + + public void withdraw() { + this.status = UserStatus.WITHDRAWN; + this.withdrawnAt = LocalDateTime.now(); + } + + public boolean isWithdrawn() { + return status == UserStatus.WITHDRAWN; + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 2ba8d6fa..3b431368 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -107,4 +107,13 @@ class UserApiUerInfoResponse extends CommonApiResponse { ), }) CommonApiResponse updateInfo(VookLoginUser user, UserUpdateInfoRequest request); + + @Operation( + summary = "회원 탈퇴", + security = { + @SecurityRequirement(name = "AccessToken") + }, + description = "탈퇴된 회원에 대한 요청은 무시됩니다." + ) + CommonApiResponse withdraw(VookLoginUser user); } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index 2eab3a0e..dc5f967d 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -57,4 +57,13 @@ public CommonApiResponse updateInfo( service.updateInfo(user, request); return CommonApiResponse.ok(); } + + @Override + @PostMapping("/withdraw") + public CommonApiResponse withdraw( + @AuthenticationPrincipal VookLoginUser user + ) { + service.withdraw(user); + return CommonApiResponse.ok(); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 5d2df3d5..2bbf234b 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -33,4 +33,8 @@ public void onboarding(VookLoginUser loginUser, UserOnboardingRequest request) { public void updateInfo(VookLoginUser loginUser, UserUpdateInfoRequest request) { userService.updateInfo(loginUser.getUid(), request.getNickname()); } + + public void withdraw(VookLoginUser loginUser) { + userService.withdraw(loginUser.getUid()); + } } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index f2205f7a..1bdf78e2 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -205,4 +205,20 @@ void updateInfo1() { assertThat(user.getUserInfo().getNickname()).isEqualTo("newNickname"); assertThat(user.getLastUpdatedAt()).isNotNull(); } + + @Test + @DisplayName("탈퇴 - 정상") + void withdraw1() { + // given + User registeredUser = testDataCreator.createRegisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); + + // when + userWebService.withdraw(vookLoginUser); + + // then + User user = userService.findByUid(registeredUser.getUid()).orElseThrow(); + assertThat(user.getStatus()).isEqualTo(UserStatus.WITHDRAWN); + assertThat(user.getWithdrawnAt()).isNotNull(); + } } From 78078b52dfa10ade6f724973171d8102619c60fd Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 11 Jun 2024 13:30:51 +0900 Subject: [PATCH 117/273] =?UTF-8?q?feat:=20=ED=83=88=ED=87=B4=ED=95=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EC=9D=B4=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=EC=8B=9C=EB=8F=84=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?#77?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 4 ++++ .../user/exception/WithdrawnUserException.java | 10 ++++++++++ .../server/api/web/routes/user/UserApi.java | 2 +- .../server/api/testhelper/TestDataCreator.java | 6 ++++++ .../web/routes/user/UserWebServiceTest.java | 18 ++++++++++++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/vook/server/api/app/user/exception/WithdrawnUserException.java diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index f4afc483..151ce74d 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -8,6 +8,7 @@ import vook.server.api.app.user.exception.AlreadyOnboardingException; import vook.server.api.app.user.exception.AlreadyRegisteredException; import vook.server.api.app.user.exception.NotReadyToOnboardingException; +import vook.server.api.app.user.exception.WithdrawnUserException; import vook.server.api.app.user.repo.SocialUserRepository; import vook.server.api.app.user.repo.UserInfoRepository; import vook.server.api.app.user.repo.UserRepository; @@ -49,6 +50,9 @@ public void register(RegisterCommand command) { if (user.isRegistered()) { throw new AlreadyRegisteredException(); } + if (user.isWithdrawn()) { + throw new WithdrawnUserException(); + } UserInfo userInfo = userInfoRepository.save(UserInfo.forRegisterOf( command.getNickname(), diff --git a/api/src/main/java/vook/server/api/app/user/exception/WithdrawnUserException.java b/api/src/main/java/vook/server/api/app/user/exception/WithdrawnUserException.java new file mode 100644 index 00000000..f91b80e5 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/exception/WithdrawnUserException.java @@ -0,0 +1,10 @@ +package vook.server.api.app.user.exception; + +import vook.server.api.app.common.AppException; + +public class WithdrawnUserException extends AppException { + @Override + public String contents() { + return "WithdrawnUser"; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 3b431368..91955c98 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -46,7 +46,7 @@ class UserApiUerInfoResponse extends CommonApiResponse { description = """ 비즈니스 규칙 위반 내용 - AlreadyRegistered: 이미 회원가입이 완료된 유저가 해당 API를 호출 할 경우 - """ + - WithdrawnUser: 탈퇴한 유저가 해당 API를 호출 할 경우""" ) @ApiResponses(value = { @ApiResponse( diff --git a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java index 3032b7fb..3fe44dba 100644 --- a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java +++ b/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java @@ -41,6 +41,12 @@ public User createCompletedOnboardingUser() { return userService.findByUid(user.getUid()).orElseThrow(); } + public User createWithdrawnUser() { + User user = createCompletedOnboardingUser(); + userService.withdraw(user.getUid()); + return userService.findByUid(user.getUid()).orElseThrow(); + } + public GeneratedToken createToken(User user) { return tokenService.generateToken(user.getUid()); } diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 1bdf78e2..894ed937 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -8,6 +8,7 @@ import vook.server.api.app.user.exception.AlreadyOnboardingException; import vook.server.api.app.user.exception.AlreadyRegisteredException; import vook.server.api.app.user.exception.NotReadyToOnboardingException; +import vook.server.api.app.user.exception.WithdrawnUserException; import vook.server.api.model.user.Funnel; import vook.server.api.model.user.Job; import vook.server.api.model.user.User; @@ -132,6 +133,23 @@ void registerError1() { .isInstanceOf(AlreadyRegisteredException.class); } + @Test + @DisplayName("회원 가입 - 에러; 탈퇴한 유저") + void registerError2() { + // given + User withdrawnUser = testDataCreator.createWithdrawnUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(withdrawnUser.getUid()); + + UserRegisterRequest request = new UserRegisterRequest(); + request.setNickname("nickname"); + request.setRequiredTermsAgree(true); + request.setMarketingEmailOptIn(true); + + // when + assertThatThrownBy(() -> userWebService.register(vookLoginUser, request)) + .isInstanceOf(WithdrawnUserException.class); + } + @Test @DisplayName("온보딩 완료 - 정상") void onboarding1() { From 4c63599775fa688b269a014c9ce695063f3b460e Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 11 Jun 2024 13:40:33 +0900 Subject: [PATCH 118/273] =?UTF-8?q?feat:=20=EB=AF=B8=20=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=EA=B0=80=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=EC=9D=84=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=ED=95=A0=20=EA=B2=BD=EC=9A=B0=EC=9D=98=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80=20#76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/app/user/UserService.java | 8 ++++---- .../exception/NotRegisteredException.java | 10 ++++++++++ .../server/api/web/routes/user/UserApi.java | 6 +++++- .../web/routes/user/UserWebServiceTest.java | 19 +++++++++++++++---- 4 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/user/exception/NotRegisteredException.java diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/user/UserService.java index 151ce74d..366513c1 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/user/UserService.java @@ -5,10 +5,7 @@ import vook.server.api.app.user.data.OnboardingCommand; import vook.server.api.app.user.data.RegisterCommand; import vook.server.api.app.user.data.SignUpFromSocialCommand; -import vook.server.api.app.user.exception.AlreadyOnboardingException; -import vook.server.api.app.user.exception.AlreadyRegisteredException; -import vook.server.api.app.user.exception.NotReadyToOnboardingException; -import vook.server.api.app.user.exception.WithdrawnUserException; +import vook.server.api.app.user.exception.*; import vook.server.api.app.user.repo.SocialUserRepository; import vook.server.api.app.user.repo.UserInfoRepository; import vook.server.api.app.user.repo.UserRepository; @@ -76,6 +73,9 @@ public void onboarding(OnboardingCommand command) { public void updateInfo(String uid, String nickname) { User user = repository.findByUid(uid).orElseThrow(); + if (!user.isRegistered()) { + throw new NotRegisteredException(); + } user.update(nickname); } diff --git a/api/src/main/java/vook/server/api/app/user/exception/NotRegisteredException.java b/api/src/main/java/vook/server/api/app/user/exception/NotRegisteredException.java new file mode 100644 index 00000000..0ff2eefe --- /dev/null +++ b/api/src/main/java/vook/server/api/app/user/exception/NotRegisteredException.java @@ -0,0 +1,10 @@ +package vook.server.api.app.user.exception; + +import vook.server.api.app.common.AppException; + +public class NotRegisteredException extends AppException { + @Override + public String contents() { + return "NotRegistered"; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 91955c98..85edcf5c 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -92,7 +92,10 @@ class UserApiUerInfoResponse extends CommonApiResponse { summary = "사용자 정보 수정", security = { @SecurityRequirement(name = "AccessToken") - } + }, + description = """ + 비즈니스 규칙 위반 내용 + - NotRegistered: 가입하지 않은 유저가 해당 API를 호출 할 경우""" ) @ApiResponses(value = { @ApiResponse( @@ -102,6 +105,7 @@ class UserApiUerInfoResponse extends CommonApiResponse { schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), examples = { @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER), + @ExampleObject(name = "비즈니스 규칙 위반", ref = ComponentRefConsts.Example.VIOLATION_BUSINESS_RULE) } ) ), diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 894ed937..ca3bdcb2 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -5,10 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; -import vook.server.api.app.user.exception.AlreadyOnboardingException; -import vook.server.api.app.user.exception.AlreadyRegisteredException; -import vook.server.api.app.user.exception.NotReadyToOnboardingException; -import vook.server.api.app.user.exception.WithdrawnUserException; +import vook.server.api.app.user.exception.*; import vook.server.api.model.user.Funnel; import vook.server.api.model.user.Job; import vook.server.api.model.user.User; @@ -224,6 +221,20 @@ void updateInfo1() { assertThat(user.getLastUpdatedAt()).isNotNull(); } + @Test + @DisplayName("사용자 정보 수정 - 에러; 미 가입 유저") + void updateInfoError1() { + // given + User unregisteredUser = testDataCreator.createUnregisteredUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); + UserUpdateInfoRequest request = new UserUpdateInfoRequest(); + request.setNickname("newNickname"); + + // when + assertThatThrownBy(() -> userWebService.updateInfo(vookLoginUser, request)) + .isInstanceOf(NotRegisteredException.class); + } + @Test @DisplayName("탈퇴 - 정상") void withdraw1() { From 2d0e323d37ec491f2c856b4b6df15fde31bce5e2 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Tue, 11 Jun 2024 16:19:45 +0900 Subject: [PATCH 119/273] =?UTF-8?q?chore:=20local=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9A=A9=20oauth2=20google=20client=20id,=20secret?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/resources/application-local.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/resources/application-local.yml b/api/src/main/resources/application-local.yml index b63a741a..0557c315 100644 --- a/api/src/main/resources/application-local.yml +++ b/api/src/main/resources/application-local.yml @@ -18,8 +18,8 @@ spring: registration: google: client-name: google - client-id: 888756350502-376ig1il0g9mdb8vbs1nj8u291lh4hl3.apps.googleusercontent.com - client-secret: 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 + client-id: ${OAUTH2_GOOGLE_CLIENT_ID} + client-secret: ${OAUTH2_GOOGLE_CLIENT_SECRET} redirect-uri: http://localhost:8080/login/oauth2/code/google authorization-grant-type: authorization_code scope: From 5134c3c6352f221e67cc554c2deac12866b13922 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 00:20:37 +0900 Subject: [PATCH 120/273] =?UTF-8?q?chore:=20ci=20=EB=8B=A8=EA=B3=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=8B=A4=ED=96=89=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EA=B0=80=20=EC=B6=9C=EB=A0=A5=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cicd/dagger/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cicd/dagger/main.go b/cicd/dagger/main.go index 118c33e8..4c51d795 100644 --- a/cicd/dagger/main.go +++ b/cicd/dagger/main.go @@ -25,7 +25,7 @@ func (v *VookServer) BuildApiJar( if test { _, err := c. With(dag.DockerService().WithCacheVolume("docker-var/lib/docker").BindAsService). - WithExec([]string{"./gradlew", "test", "--info"}). + WithExec([]string{"./gradlew", "test"}). Sync(ctx) if err != nil { return nil, errors.New("test fail:" + err.Error()) From b9098cfaac448bf18b36db0b15e2ec8eb5dfdbdb Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 00:22:04 +0900 Subject: [PATCH 121/273] =?UTF-8?q?refactor:=20MeilisearchService=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=82=B4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/outbound/search/MeilisearchService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java index 8354cbba..6454109d 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java @@ -23,8 +23,6 @@ protected void clearAll(String uidPrefix) { Arrays.stream(indexes.getResults()) .map(Index::getUid) .filter(uid -> uid.startsWith(uidPrefix)) - .forEach(uid -> { - client.deleteIndex(uid); - }); + .forEach(client::deleteIndex); } } From bde9553212cbb68d0ef20dff566ba17699d9b069 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 00:28:35 +0900 Subject: [PATCH 122/273] =?UTF-8?q?refactor:=20devhelper=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=82=B4=EB=B6=80=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/devhelper/{ => app}/InitService.java | 5 +---- .../server/api/devhelper/{ => app}/TestTermsLoader.java | 7 ++++--- .../vook/server/api/devhelper/{ => helper}/CsvReader.java | 2 +- .../vook/server/api/devhelper/{ => init}/LocalInit.java | 3 ++- .../api/{ => devhelper}/web/routes/init/InitApi.java | 2 +- .../{ => devhelper}/web/routes/init/InitController.java | 4 ++-- .../server/api/devhelper/{ => helper}/CsvReaderTest.java | 2 +- .../server/api/web/routes/demo/DemoWebServiceTest.java | 2 +- 8 files changed, 13 insertions(+), 14 deletions(-) rename api/src/main/java/vook/server/api/devhelper/{ => app}/InitService.java (92%) rename api/src/main/java/vook/server/api/devhelper/{ => app}/TestTermsLoader.java (89%) rename api/src/main/java/vook/server/api/devhelper/{ => helper}/CsvReader.java (98%) rename api/src/main/java/vook/server/api/devhelper/{ => init}/LocalInit.java (88%) rename api/src/main/java/vook/server/api/{ => devhelper}/web/routes/init/InitApi.java (90%) rename api/src/main/java/vook/server/api/{ => devhelper}/web/routes/init/InitController.java (86%) rename api/src/test/java/vook/server/api/devhelper/{ => helper}/CsvReaderTest.java (98%) diff --git a/api/src/main/java/vook/server/api/devhelper/InitService.java b/api/src/main/java/vook/server/api/devhelper/app/InitService.java similarity index 92% rename from api/src/main/java/vook/server/api/devhelper/InitService.java rename to api/src/main/java/vook/server/api/devhelper/app/InitService.java index bc0f1683..aa6203d4 100644 --- a/api/src/main/java/vook/server/api/devhelper/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/app/InitService.java @@ -1,7 +1,6 @@ -package vook.server.api.devhelper; +package vook.server.api.devhelper.app; import lombok.RequiredArgsConstructor; -import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.demo.repo.DemoTermRepository; @@ -19,8 +18,6 @@ @RequiredArgsConstructor public class InitService { - private final ResourceLoader resourceLoader; - private final DemoTermRepository demoTermRepository; private final DemoTermSynonymRepository demoTermSynonymRepository; private final UserInfoRepository userInfoRepository; diff --git a/api/src/main/java/vook/server/api/devhelper/TestTermsLoader.java b/api/src/main/java/vook/server/api/devhelper/app/TestTermsLoader.java similarity index 89% rename from api/src/main/java/vook/server/api/devhelper/TestTermsLoader.java rename to api/src/main/java/vook/server/api/devhelper/app/TestTermsLoader.java index 4cc9b9d2..19bd1490 100644 --- a/api/src/main/java/vook/server/api/devhelper/TestTermsLoader.java +++ b/api/src/main/java/vook/server/api/devhelper/app/TestTermsLoader.java @@ -1,8 +1,9 @@ -package vook.server.api.devhelper; +package vook.server.api.devhelper.app; import lombok.RequiredArgsConstructor; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; +import vook.server.api.devhelper.helper.CsvReader; import vook.server.api.model.demo.DemoTerm; import java.io.IOException; @@ -29,13 +30,13 @@ public List getTerms(String location) { } } - static List toTerms(List rawTerms) { + private static List toTerms(List rawTerms) { return rawTerms.stream() .map(RawTerm::toTerm) .toList(); } - static class RawTerm { + public static class RawTerm { private String term; private String synonyms; private String meaning; diff --git a/api/src/main/java/vook/server/api/devhelper/CsvReader.java b/api/src/main/java/vook/server/api/devhelper/helper/CsvReader.java similarity index 98% rename from api/src/main/java/vook/server/api/devhelper/CsvReader.java rename to api/src/main/java/vook/server/api/devhelper/helper/CsvReader.java index 96daf631..388edd92 100644 --- a/api/src/main/java/vook/server/api/devhelper/CsvReader.java +++ b/api/src/main/java/vook/server/api/devhelper/helper/CsvReader.java @@ -1,4 +1,4 @@ -package vook.server.api.devhelper; +package vook.server.api.devhelper.helper; import lombok.NoArgsConstructor; diff --git a/api/src/main/java/vook/server/api/devhelper/LocalInit.java b/api/src/main/java/vook/server/api/devhelper/init/LocalInit.java similarity index 88% rename from api/src/main/java/vook/server/api/devhelper/LocalInit.java rename to api/src/main/java/vook/server/api/devhelper/init/LocalInit.java index 217b368f..07c9c9f5 100644 --- a/api/src/main/java/vook/server/api/devhelper/LocalInit.java +++ b/api/src/main/java/vook/server/api/devhelper/init/LocalInit.java @@ -1,4 +1,4 @@ -package vook.server.api.devhelper; +package vook.server.api.devhelper.init; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; @@ -6,6 +6,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import vook.server.api.app.demo.repo.DemoTermRepository; +import vook.server.api.devhelper.app.InitService; @Slf4j @Profile("local") diff --git a/api/src/main/java/vook/server/api/web/routes/init/InitApi.java b/api/src/main/java/vook/server/api/devhelper/web/routes/init/InitApi.java similarity index 90% rename from api/src/main/java/vook/server/api/web/routes/init/InitApi.java rename to api/src/main/java/vook/server/api/devhelper/web/routes/init/InitApi.java index b064ca5b..fb06d6f9 100644 --- a/api/src/main/java/vook/server/api/web/routes/init/InitApi.java +++ b/api/src/main/java/vook/server/api/devhelper/web/routes/init/InitApi.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.init; +package vook.server.api.devhelper.web.routes.init; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/api/src/main/java/vook/server/api/web/routes/init/InitController.java b/api/src/main/java/vook/server/api/devhelper/web/routes/init/InitController.java similarity index 86% rename from api/src/main/java/vook/server/api/web/routes/init/InitController.java rename to api/src/main/java/vook/server/api/devhelper/web/routes/init/InitController.java index 0d842ae0..2fa5cdda 100644 --- a/api/src/main/java/vook/server/api/web/routes/init/InitController.java +++ b/api/src/main/java/vook/server/api/devhelper/web/routes/init/InitController.java @@ -1,11 +1,11 @@ -package vook.server.api.web.routes.init; +package vook.server.api.devhelper.web.routes.init; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import vook.server.api.devhelper.InitService; +import vook.server.api.devhelper.app.InitService; import vook.server.api.web.common.CommonApiResponse; @Profile({"local", "dev", "stag"}) diff --git a/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java b/api/src/test/java/vook/server/api/devhelper/helper/CsvReaderTest.java similarity index 98% rename from api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java rename to api/src/test/java/vook/server/api/devhelper/helper/CsvReaderTest.java index da94642f..a341f9c3 100644 --- a/api/src/test/java/vook/server/api/devhelper/CsvReaderTest.java +++ b/api/src/test/java/vook/server/api/devhelper/helper/CsvReaderTest.java @@ -1,4 +1,4 @@ -package vook.server.api.devhelper; +package vook.server.api.devhelper.helper; import lombok.Getter; import org.junit.jupiter.api.DisplayName; diff --git a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java index c955ab11..c923eaba 100644 --- a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java @@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.demo.repo.DemoTermRepository; -import vook.server.api.devhelper.TestTermsLoader; +import vook.server.api.devhelper.app.TestTermsLoader; import vook.server.api.model.demo.DemoTerm; import vook.server.api.outbound.search.DemoTermSearchService; import vook.server.api.testhelper.IntegrationTestBase; From 266f714ee6e6970498ef3fe9f11ae35bea04a24c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 00:31:47 +0900 Subject: [PATCH 123/273] =?UTF-8?q?refactor:=20outbound.search=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=82=B4=EB=B6=80=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/devhelper/app/InitService.java | 2 +- .../outbound/search/{ => common}/MeilisearchProperties.java | 2 +- .../api/outbound/search/{ => common}/MeilisearchService.java | 2 +- .../api/outbound/search/{ => demo}/DemoTermSearchParams.java | 2 +- .../api/outbound/search/{ => demo}/DemoTermSearchResult.java | 2 +- .../api/outbound/search/{ => demo}/DemoTermSearchService.java | 4 +++- .../java/vook/server/api/web/routes/demo/DemoWebService.java | 4 ++-- .../server/api/web/routes/demo/reqres/SearchTermRequest.java | 2 +- .../server/api/web/routes/demo/reqres/SearchTermResponse.java | 2 +- .../java/vook/server/api/testhelper/IntegrationTestBase.java | 2 +- .../vook/server/api/web/routes/demo/DemoWebServiceTest.java | 2 +- 11 files changed, 14 insertions(+), 12 deletions(-) rename api/src/main/java/vook/server/api/outbound/search/{ => common}/MeilisearchProperties.java (87%) rename api/src/main/java/vook/server/api/outbound/search/{ => common}/MeilisearchService.java (94%) rename api/src/main/java/vook/server/api/outbound/search/{ => demo}/DemoTermSearchParams.java (96%) rename api/src/main/java/vook/server/api/outbound/search/{ => demo}/DemoTermSearchResult.java (93%) rename api/src/main/java/vook/server/api/outbound/search/{ => demo}/DemoTermSearchService.java (95%) diff --git a/api/src/main/java/vook/server/api/devhelper/app/InitService.java b/api/src/main/java/vook/server/api/devhelper/app/InitService.java index aa6203d4..9105e2d9 100644 --- a/api/src/main/java/vook/server/api/devhelper/app/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/app/InitService.java @@ -9,7 +9,7 @@ import vook.server.api.app.user.repo.UserInfoRepository; import vook.server.api.app.user.repo.UserRepository; import vook.server.api.model.demo.DemoTerm; -import vook.server.api.outbound.search.DemoTermSearchService; +import vook.server.api.outbound.search.demo.DemoTermSearchService; import java.util.List; diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchProperties.java b/api/src/main/java/vook/server/api/outbound/search/common/MeilisearchProperties.java similarity index 87% rename from api/src/main/java/vook/server/api/outbound/search/MeilisearchProperties.java rename to api/src/main/java/vook/server/api/outbound/search/common/MeilisearchProperties.java index f54a2ccd..03633126 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchProperties.java +++ b/api/src/main/java/vook/server/api/outbound/search/common/MeilisearchProperties.java @@ -1,4 +1,4 @@ -package vook.server.api.outbound.search; +package vook.server.api.outbound.search.common; import lombok.Getter; import lombok.Setter; diff --git a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java b/api/src/main/java/vook/server/api/outbound/search/common/MeilisearchService.java similarity index 94% rename from api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java rename to api/src/main/java/vook/server/api/outbound/search/common/MeilisearchService.java index 6454109d..f91af706 100644 --- a/api/src/main/java/vook/server/api/outbound/search/MeilisearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/common/MeilisearchService.java @@ -1,4 +1,4 @@ -package vook.server.api.outbound.search; +package vook.server.api.outbound.search.common; import com.meilisearch.sdk.Client; import com.meilisearch.sdk.Config; diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java b/api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchParams.java similarity index 96% rename from api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java rename to api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchParams.java index e5bab1ad..8a584163 100644 --- a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchParams.java +++ b/api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchParams.java @@ -1,4 +1,4 @@ -package vook.server.api.outbound.search; +package vook.server.api.outbound.search.demo; import com.meilisearch.sdk.SearchRequest; import lombok.Builder; diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchResult.java b/api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchResult.java similarity index 93% rename from api/src/main/java/vook/server/api/outbound/search/DemoTermSearchResult.java rename to api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchResult.java index 73badba3..e37bd0b9 100644 --- a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchResult.java +++ b/api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchResult.java @@ -1,4 +1,4 @@ -package vook.server.api.outbound.search; +package vook.server.api.outbound.search.demo; import com.meilisearch.sdk.model.Searchable; import lombok.Getter; diff --git a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java b/api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchService.java similarity index 95% rename from api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java rename to api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchService.java index 86a8d71b..7a17c396 100644 --- a/api/src/main/java/vook/server/api/outbound/search/DemoTermSearchService.java +++ b/api/src/main/java/vook/server/api/outbound/search/demo/DemoTermSearchService.java @@ -1,4 +1,4 @@ -package vook.server.api.outbound.search; +package vook.server.api.outbound.search.demo; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,6 +12,8 @@ import org.springframework.stereotype.Service; import vook.server.api.model.demo.DemoTerm; import vook.server.api.model.demo.DemoTermSynonym; +import vook.server.api.outbound.search.common.MeilisearchProperties; +import vook.server.api.outbound.search.common.MeilisearchService; import java.time.format.DateTimeFormatter; import java.util.List; diff --git a/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java b/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java index 922fe96d..c2779f53 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/DemoWebService.java @@ -3,8 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.outbound.search.DemoTermSearchResult; -import vook.server.api.outbound.search.DemoTermSearchService; +import vook.server.api.outbound.search.demo.DemoTermSearchResult; +import vook.server.api.outbound.search.demo.DemoTermSearchService; import vook.server.api.web.routes.demo.reqres.SearchTermRequest; import vook.server.api.web.routes.demo.reqres.SearchTermResponse; diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java index a2145757..aca45f4d 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermRequest.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import vook.server.api.outbound.search.DemoTermSearchParams; +import vook.server.api.outbound.search.demo.DemoTermSearchParams; import java.util.List; diff --git a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java index dee35129..7bb8127d 100644 --- a/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/demo/reqres/SearchTermResponse.java @@ -1,7 +1,7 @@ package vook.server.api.web.routes.demo.reqres; import lombok.Getter; -import vook.server.api.outbound.search.DemoTermSearchResult; +import vook.server.api.outbound.search.demo.DemoTermSearchResult; import java.util.List; import java.util.Map; diff --git a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java index e0eeea19..f5162dc2 100644 --- a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java +++ b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java @@ -8,7 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.testcontainers.containers.MariaDBContainer; -import vook.server.api.outbound.search.MeilisearchProperties; +import vook.server.api.outbound.search.common.MeilisearchProperties; import java.util.Map; import java.util.TimeZone; diff --git a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java index c923eaba..9671ee16 100644 --- a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java @@ -9,7 +9,7 @@ import vook.server.api.app.demo.repo.DemoTermRepository; import vook.server.api.devhelper.app.TestTermsLoader; import vook.server.api.model.demo.DemoTerm; -import vook.server.api.outbound.search.DemoTermSearchService; +import vook.server.api.outbound.search.demo.DemoTermSearchService; import vook.server.api.testhelper.IntegrationTestBase; import vook.server.api.web.routes.demo.reqres.SearchTermRequest; import vook.server.api.web.routes.demo.reqres.SearchTermResponse; From cca1101b40e7bfb2e69eb61008a2354d0b6f8d7d Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 00:54:38 +0900 Subject: [PATCH 124/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91,=20?= =?UTF-8?q?=EC=9A=A9=EC=96=B4=20=EB=AA=A8=EB=8D=B8=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?#81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/api/model/vocabulary/Term.java | 53 ++++++++++++++++++ .../api/model/vocabulary/TermSynonym.java | 35 ++++++++++++ .../api/model/vocabulary/Vocabulary.java | 54 +++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 api/src/main/java/vook/server/api/model/vocabulary/Term.java create mode 100644 api/src/main/java/vook/server/api/model/vocabulary/TermSynonym.java create mode 100644 api/src/main/java/vook/server/api/model/vocabulary/Vocabulary.java diff --git a/api/src/main/java/vook/server/api/model/vocabulary/Term.java b/api/src/main/java/vook/server/api/model/vocabulary/Term.java new file mode 100644 index 00000000..4584d459 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/vocabulary/Term.java @@ -0,0 +1,53 @@ +package vook.server.api.model.vocabulary; + +import jakarta.persistence.*; +import lombok.Getter; +import vook.server.api.model.common.BaseEntity; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Entity +@Table(name = "term") +public class Term extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 용어 이름 + */ + @Column(length = 100, nullable = false) + private String term; + + /** + * 용어 의미 + */ + @Column(length = 2000, nullable = false) + private String meaning; + + @ManyToOne + @JoinColumn(name = "vocabulary_id", nullable = false) + private Vocabulary vocabulary; + + @OneToMany(mappedBy = "term", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + private List synonyms = new ArrayList<>(); + + public static Term forCreateOf( + String term, + String meaning, + Vocabulary vocabulary + ) { + Term result = new Term(); + result.term = term; + result.meaning = meaning; + result.vocabulary = vocabulary; + return result; + } + + public void addSynonym(String synonym) { + this.synonyms.add(TermSynonym.forCreateOf(synonym, this)); + } +} diff --git a/api/src/main/java/vook/server/api/model/vocabulary/TermSynonym.java b/api/src/main/java/vook/server/api/model/vocabulary/TermSynonym.java new file mode 100644 index 00000000..a284c06c --- /dev/null +++ b/api/src/main/java/vook/server/api/model/vocabulary/TermSynonym.java @@ -0,0 +1,35 @@ +package vook.server.api.model.vocabulary; + +import jakarta.persistence.*; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "term_synonym") +public class TermSynonym { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 동의어 + */ + @Column(length = 100, nullable = false) + private String synonym; + + @ManyToOne + @JoinColumn(name = "term_id", nullable = false) + private Term term; + + static TermSynonym forCreateOf( + String synonym, + Term term + ) { + TermSynonym result = new TermSynonym(); + result.synonym = synonym; + result.term = term; + return result; + } + +} diff --git a/api/src/main/java/vook/server/api/model/vocabulary/Vocabulary.java b/api/src/main/java/vook/server/api/model/vocabulary/Vocabulary.java new file mode 100644 index 00000000..58a94637 --- /dev/null +++ b/api/src/main/java/vook/server/api/model/vocabulary/Vocabulary.java @@ -0,0 +1,54 @@ +package vook.server.api.model.vocabulary; + +import jakarta.persistence.*; +import lombok.Getter; +import vook.server.api.model.common.BaseEntity; +import vook.server.api.model.user.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Getter +@Entity +@Table(name = "vocabulary") +public class Vocabulary extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String uid; + + /** + * 용어집 이름 + */ + @Column(length = 20, nullable = false) + private String name; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @OneToMany(mappedBy = "vocabulary", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private List terms = new ArrayList<>(); + + public static Vocabulary forCreateOf( + String name, + User user + ) { + Vocabulary result = new Vocabulary(); + result.uid = UUID.randomUUID().toString(); + result.name = name; + result.user = user; + return result; + } + + public void addTerm(Term term) { + this.terms.add(term); + } + + public int termCount() { + return this.terms.size(); + } +} From adaa87ea221762fc1cdc20ba0fff80f998e08968 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 01:00:18 +0900 Subject: [PATCH 125/273] =?UTF-8?q?refactor:=20UserUpdateInfoRequest=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/java/vook/server/api/web/routes/user/UserApi.java | 1 + .../vook/server/api/web/routes/user/UserRestController.java | 1 + .../java/vook/server/api/web/routes/user/UserWebService.java | 1 + .../api/web/routes/user/{ => reqres}/UserUpdateInfoRequest.java | 2 +- .../vook/server/api/web/routes/user/UserWebServiceTest.java | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) rename api/src/main/java/vook/server/api/web/routes/user/{ => reqres}/UserUpdateInfoRequest.java (82%) diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index 85edcf5c..dc19ee16 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -13,6 +13,7 @@ import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; +import vook.server.api.web.routes.user.reqres.UserUpdateInfoRequest; import vook.server.api.web.swagger.ComponentRefConsts; @Tag(name = "user", description = "사용자 관련 API") diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java index dc5f967d..b8df5955 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserRestController.java @@ -10,6 +10,7 @@ import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; +import vook.server.api.web.routes.user.reqres.UserUpdateInfoRequest; @Slf4j @RestController diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 2bbf234b..26ffa6ed 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -9,6 +9,7 @@ import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; +import vook.server.api.web.routes.user.reqres.UserUpdateInfoRequest; @Service @Transactional diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserUpdateInfoRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserUpdateInfoRequest.java similarity index 82% rename from api/src/main/java/vook/server/api/web/routes/user/UserUpdateInfoRequest.java rename to api/src/main/java/vook/server/api/web/routes/user/reqres/UserUpdateInfoRequest.java index 83a21ae5..011c623b 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserUpdateInfoRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserUpdateInfoRequest.java @@ -1,4 +1,4 @@ -package vook.server.api.web.routes.user; +package vook.server.api.web.routes.user.reqres; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index ca3bdcb2..5198c91a 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -16,6 +16,7 @@ import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; import vook.server.api.web.routes.user.reqres.UserRegisterRequest; +import vook.server.api.web.routes.user.reqres.UserUpdateInfoRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; From c2ccca707ee7917f64982aa09f9c10fa7e53a5ac Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 01:20:14 +0900 Subject: [PATCH 126/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20API=20#81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/app/vocabulary/VocabularyService.java | 20 ++++++++++ .../vocabulary/repo/VocabularyRepository.java | 11 ++++++ .../web/routes/vocabulary/VocabularyApi.java | 38 +++++++++++++++++++ .../vocabulary/VocabularyRestController.java | 29 ++++++++++++++ .../vocabulary/VocabularyWebService.java | 28 ++++++++++++++ .../vocabulary/reqres/VocabularyResponse.java | 29 ++++++++++++++ 6 files changed, 155 insertions(+) create mode 100644 api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java create mode 100644 api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java create mode 100644 api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java create mode 100644 api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java create mode 100644 api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java create mode 100644 api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyResponse.java diff --git a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java b/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java new file mode 100644 index 00000000..91b08f1e --- /dev/null +++ b/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java @@ -0,0 +1,20 @@ +package vook.server.api.app.vocabulary; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import vook.server.api.app.vocabulary.repo.VocabularyRepository; +import vook.server.api.model.user.User; +import vook.server.api.model.vocabulary.Vocabulary; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class VocabularyService { + + private final VocabularyRepository repository; + + public List findAllBy(User user) { + return repository.findAllByUser(user); + } +} diff --git a/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java b/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java new file mode 100644 index 00000000..5ed69461 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java @@ -0,0 +1,11 @@ +package vook.server.api.app.vocabulary.repo; + +import org.springframework.data.jpa.repository.JpaRepository; +import vook.server.api.model.user.User; +import vook.server.api.model.vocabulary.Vocabulary; + +import java.util.List; + +public interface VocabularyRepository extends JpaRepository { + List findAllByUser(User user); +} diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java new file mode 100644 index 00000000..1c65a6c5 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java @@ -0,0 +1,38 @@ +package vook.server.api.web.routes.vocabulary; + +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import vook.server.api.web.auth.data.VookLoginUser; +import vook.server.api.web.common.CommonApiResponse; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; + +import java.util.List; + +@Tag(name = "vocabulary", description = "용어집 API") +public interface VocabularyApi { + + @Operation( + summary = "용어집 조회", + security = { + @SecurityRequirement(name = "AccessToken") + } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = VocabularyApiVocabulariesResponse.class) + ) + ), + }) + CommonApiResponse> vocabularies(VookLoginUser user); + + class VocabularyApiVocabulariesResponse extends CommonApiResponse> { + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java new file mode 100644 index 00000000..b5a3513c --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java @@ -0,0 +1,29 @@ +package vook.server.api.web.routes.vocabulary; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import vook.server.api.web.auth.data.VookLoginUser; +import vook.server.api.web.common.CommonApiResponse; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; + +import java.util.List; + +@RestController +@RequestMapping("/vocabularies") +@RequiredArgsConstructor +public class VocabularyRestController implements VocabularyApi { + + private final VocabularyWebService service; + + @Override + @GetMapping + public CommonApiResponse> vocabularies( + @AuthenticationPrincipal VookLoginUser user + ) { + List response = service.vocabularies(user); + return CommonApiResponse.okWithResult(response); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java new file mode 100644 index 00000000..866388e4 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java @@ -0,0 +1,28 @@ +package vook.server.api.web.routes.vocabulary; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.user.UserService; +import vook.server.api.app.vocabulary.VocabularyService; +import vook.server.api.model.user.User; +import vook.server.api.model.vocabulary.Vocabulary; +import vook.server.api.web.auth.data.VookLoginUser; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +public class VocabularyWebService { + + private final UserService userService; + private final VocabularyService service; + + public List vocabularies(VookLoginUser loginUser) { + User user = userService.findByUid(loginUser.getUid()).orElseThrow(); + List vocabularies = service.findAllBy(user); + return VocabularyResponse.from(vocabularies); + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyResponse.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyResponse.java new file mode 100644 index 00000000..4c730c48 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyResponse.java @@ -0,0 +1,29 @@ +package vook.server.api.web.routes.vocabulary.reqres; + +import lombok.Getter; +import vook.server.api.model.vocabulary.Vocabulary; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +public class VocabularyResponse { + + private String uid; + private String name; + private Integer termCount; + private LocalDateTime createdAt; + + public static List from(List vocabularies) { + return vocabularies.stream().map(VocabularyResponse::from).toList(); + } + + public static VocabularyResponse from(Vocabulary vocabulary) { + VocabularyResponse result = new VocabularyResponse(); + result.uid = vocabulary.getUid(); + result.name = vocabulary.getName(); + result.termCount = vocabulary.termCount(); + result.createdAt = vocabulary.getCreatedAt(); + return result; + } +} From 96b2225d8ebb6be19471092ce4947080cd8153b7 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 01:22:55 +0900 Subject: [PATCH 127/273] =?UTF-8?q?fix:=20UserService#findByUid=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EC=8B=9C=20=EC=BF=BC=EB=A6=AC=EA=B0=80=20?= =?UTF-8?q?2=EB=B2=88=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/app/user/repo/UserRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/src/main/java/vook/server/api/app/user/repo/UserRepository.java b/api/src/main/java/vook/server/api/app/user/repo/UserRepository.java index ba20060b..bdbca4ee 100644 --- a/api/src/main/java/vook/server/api/app/user/repo/UserRepository.java +++ b/api/src/main/java/vook/server/api/app/user/repo/UserRepository.java @@ -1,6 +1,7 @@ package vook.server.api.app.user.repo; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import vook.server.api.model.user.User; import java.util.Optional; @@ -8,5 +9,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + @Query("SELECT u FROM User u LEFT JOIN FETCH u.userInfo WHERE u.uid = :uid") Optional findByUid(String uid); } From bf998c144d2b254df58a693b188a7fb5f74dcc68 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 01:24:29 +0900 Subject: [PATCH 128/273] fix: fix typo --- .../main/java/vook/server/api/web/routes/user/UserApi.java | 4 ++-- .../server/api/web/routes/user/UserRestControllerTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java index dc19ee16..50d9cac8 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserApi.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserApi.java @@ -30,13 +30,13 @@ public interface UserApi { responseCode = "200", content = @Content( mediaType = "application/json", - schema = @Schema(implementation = UserApiUerInfoResponse.class) + schema = @Schema(implementation = UserApiUserInfoResponse.class) ) ), }) CommonApiResponse userInfo(VookLoginUser user); - class UserApiUerInfoResponse extends CommonApiResponse { + class UserApiUserInfoResponse extends CommonApiResponse { } @Operation( diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java index cb5d7d1f..b0f3a84c 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java @@ -45,7 +45,7 @@ void userInfo() { new HttpEntityBuilder() .header("Authorization", "Bearer " + token.getAccessToken()) .build(), - UserApi.UserApiUerInfoResponse.class + UserApi.UserApiUserInfoResponse.class ); // then From 6d1e4196c29fb434d841a29747048dcbf698410b Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 01:49:30 +0900 Subject: [PATCH 129/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20#81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/app/vocabulary/VocabularyService.java | 11 ++++++++ .../data/VocabularyCreateCommand.java | 23 ++++++++++++++++ .../VocabularyLimitExceededException.java | 10 +++++++ .../web/routes/vocabulary/VocabularyApi.java | 27 +++++++++++++++++++ .../vocabulary/VocabularyRestController.java | 16 ++++++++--- .../vocabulary/VocabularyWebService.java | 7 +++++ .../reqres/VocabularyCreateRequest.java | 13 +++++++++ .../vocabulary/reqres/VocabularyResponse.java | 2 ++ 8 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyCreateCommand.java create mode 100644 api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyLimitExceededException.java create mode 100644 api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyCreateRequest.java diff --git a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java b/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java index 91b08f1e..81e89171 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java +++ b/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java @@ -2,6 +2,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.app.vocabulary.exception.VocabularyLimitExceededException; import vook.server.api.app.vocabulary.repo.VocabularyRepository; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; @@ -17,4 +19,13 @@ public class VocabularyService { public List findAllBy(User user) { return repository.findAllByUser(user); } + + public Vocabulary create(VocabularyCreateCommand command) { + User user = command.getUser(); + if (repository.findAllByUser(user).size() >= 3) { + throw new VocabularyLimitExceededException(); + } + + return repository.save(Vocabulary.forCreateOf(command.getName(), user)); + } } diff --git a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyCreateCommand.java b/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyCreateCommand.java new file mode 100644 index 00000000..e178acb0 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyCreateCommand.java @@ -0,0 +1,23 @@ +package vook.server.api.app.vocabulary.data; + +import lombok.Getter; +import vook.server.api.model.user.User; +import vook.server.api.model.vocabulary.Vocabulary; + +@Getter +public class VocabularyCreateCommand { + + private String name; + private User user; + + public static VocabularyCreateCommand of(String name, User user) { + VocabularyCreateCommand command = new VocabularyCreateCommand(); + command.name = name; + command.user = user; + return command; + } + + public Vocabulary toVocabulary() { + return Vocabulary.forCreateOf(name, user); + } +} diff --git a/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyLimitExceededException.java b/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyLimitExceededException.java new file mode 100644 index 00000000..80b5e421 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyLimitExceededException.java @@ -0,0 +1,10 @@ +package vook.server.api.app.vocabulary.exception; + +import vook.server.api.app.common.AppException; + +public class VocabularyLimitExceededException extends AppException { + @Override + public String contents() { + return "VocabularyLimitExceeded"; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java index 1c65a6c5..cb605d86 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -9,7 +10,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyCreateRequest; import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; +import vook.server.api.web.swagger.ComponentRefConsts; import java.util.List; @@ -35,4 +38,28 @@ public interface VocabularyApi { class VocabularyApiVocabulariesResponse extends CommonApiResponse> { } + + @Operation( + summary = "용어집 생성", + security = { + @SecurityRequirement(name = "AccessToken") + }, + description = """ + 비즈니스 규칙 위반 내용 + - VocabularyLimitExceeded: 사용자의 용어집 생성 제한을 초과하여 용어집을 생성할 수 없는 경우 (3개 초과)""" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "400", + content = @Content( + mediaType = "application/json", + schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), + examples = { + @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER), + @ExampleObject(name = "비즈니스 규칙 위반", ref = ComponentRefConsts.Example.VIOLATION_BUSINESS_RULE) + } + ) + ), + }) + CommonApiResponse createVocabulary(VookLoginUser user, VocabularyCreateRequest request); } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java index b5a3513c..15750e26 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java @@ -2,11 +2,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.common.CommonApiResponse; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyCreateRequest; import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; import java.util.List; @@ -26,4 +26,14 @@ public CommonApiResponse> vocabularies( List response = service.vocabularies(user); return CommonApiResponse.okWithResult(response); } + + @Override + @PostMapping + public CommonApiResponse createVocabulary( + @AuthenticationPrincipal VookLoginUser user, + @Validated @RequestBody VocabularyCreateRequest request + ) { + service.createVocabulary(user, request); + return CommonApiResponse.ok(); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java index 866388e4..b5f77b3b 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java @@ -5,9 +5,11 @@ import org.springframework.transaction.annotation.Transactional; import vook.server.api.app.user.UserService; import vook.server.api.app.vocabulary.VocabularyService; +import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; import vook.server.api.web.auth.data.VookLoginUser; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyCreateRequest; import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; import java.util.List; @@ -25,4 +27,9 @@ public List vocabularies(VookLoginUser loginUser) { List vocabularies = service.findAllBy(user); return VocabularyResponse.from(vocabularies); } + + public void createVocabulary(VookLoginUser loginUser, VocabularyCreateRequest request) { + User user = userService.findByUid(loginUser.getUid()).orElseThrow(); + service.create(VocabularyCreateCommand.of(request.getName(), user)); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyCreateRequest.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyCreateRequest.java new file mode 100644 index 00000000..8795c410 --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyCreateRequest.java @@ -0,0 +1,13 @@ +package vook.server.api.web.routes.vocabulary.reqres; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class VocabularyCreateRequest { + + @NotBlank + @Size(min = 1, max = 20) + private String name; +} diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyResponse.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyResponse.java index 4c730c48..2481bef7 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyResponse.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyResponse.java @@ -1,5 +1,6 @@ package vook.server.api.web.routes.vocabulary.reqres; +import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Getter; import vook.server.api.model.vocabulary.Vocabulary; @@ -12,6 +13,7 @@ public class VocabularyResponse { private String uid; private String name; private Integer termCount; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private LocalDateTime createdAt; public static List from(List vocabularies) { From 6503e317ef676d10eaa268d61133d74ccfe6c072 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 01:51:55 +0900 Subject: [PATCH 130/273] refactor: TestDataCreator -> creator/TestUserCreator --- .../TestUserCreator.java} | 4 +-- .../routes/user/UserRestControllerTest.java | 24 ++++++++-------- .../web/routes/user/UserWebServiceTest.java | 28 +++++++++---------- 3 files changed, 28 insertions(+), 28 deletions(-) rename api/src/test/java/vook/server/api/testhelper/{TestDataCreator.java => creator/TestUserCreator.java} (96%) diff --git a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java b/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java similarity index 96% rename from api/src/test/java/vook/server/api/testhelper/TestDataCreator.java rename to api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java index 3fe44dba..4c26c083 100644 --- a/api/src/test/java/vook/server/api/testhelper/TestDataCreator.java +++ b/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java @@ -1,4 +1,4 @@ -package vook.server.api.testhelper; +package vook.server.api.testhelper.creator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -17,7 +17,7 @@ @Component @Transactional @RequiredArgsConstructor -public class TestDataCreator { +public class TestUserCreator { private final UserService userService; private final TokenService tokenService; diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java index b0f3a84c..9a09e864 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserRestControllerTest.java @@ -13,7 +13,7 @@ import vook.server.api.model.user.User; import vook.server.api.testhelper.HttpEntityBuilder; import vook.server.api.testhelper.IntegrationTestBase; -import vook.server.api.testhelper.TestDataCreator; +import vook.server.api.testhelper.creator.TestUserCreator; import vook.server.api.web.auth.data.GeneratedToken; import java.util.Collection; @@ -30,13 +30,13 @@ class UserRestControllerTest extends IntegrationTestBase { UserWebService webService; @Autowired - TestDataCreator testDataCreator; + TestUserCreator testUserCreator; @Test void userInfo() { // given - User unregisteredUser = testDataCreator.createUnregisteredUser(); - GeneratedToken token = testDataCreator.createToken(unregisteredUser); + User unregisteredUser = testUserCreator.createUnregisteredUser(); + GeneratedToken token = testUserCreator.createToken(unregisteredUser); // when var res = rest.exchange( @@ -56,8 +56,8 @@ void userInfo() { @DisplayName("회원 가입 - 정상") void register() { // given - User unregisteredUser = testDataCreator.createUnregisteredUser(); - GeneratedToken token = testDataCreator.createToken(unregisteredUser); + User unregisteredUser = testUserCreator.createUnregisteredUser(); + GeneratedToken token = testUserCreator.createToken(unregisteredUser); // when var res = rest.exchange( @@ -82,8 +82,8 @@ void register() { @DisplayName("회원 가입 - 실패") Collection registerError() { // given - User unregisteredUser = testDataCreator.createUnregisteredUser(); - GeneratedToken token = testDataCreator.createToken(unregisteredUser); + User unregisteredUser = testUserCreator.createUnregisteredUser(); + GeneratedToken token = testUserCreator.createToken(unregisteredUser); Function, ResponseEntity> restExchange = body -> rest.exchange( "/user/register", @@ -140,8 +140,8 @@ Collection registerError() { @DisplayName("회원 정보 수정 - 정상") void updateInfo() { // given - User registeredUser = testDataCreator.createRegisteredUser(); - GeneratedToken token = testDataCreator.createToken(registeredUser); + User registeredUser = testUserCreator.createRegisteredUser(); + GeneratedToken token = testUserCreator.createToken(registeredUser); // when var res = rest.exchange( @@ -164,8 +164,8 @@ void updateInfo() { @DisplayName("회원 정보 수정 - 실패") Collection updateInfoError() { // given - User registeredUser = testDataCreator.createRegisteredUser(); - GeneratedToken token = testDataCreator.createToken(registeredUser); + User registeredUser = testUserCreator.createRegisteredUser(); + GeneratedToken token = testUserCreator.createToken(registeredUser); Function, ResponseEntity> restExchange = body -> rest.exchange( "/user/info", diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index 5198c91a..b9ad2108 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -11,7 +11,7 @@ import vook.server.api.model.user.User; import vook.server.api.model.user.UserStatus; import vook.server.api.testhelper.IntegrationTestBase; -import vook.server.api.testhelper.TestDataCreator; +import vook.server.api.testhelper.creator.TestUserCreator; import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.routes.user.reqres.UserInfoResponse; import vook.server.api.web.routes.user.reqres.UserOnboardingRequest; @@ -28,7 +28,7 @@ class UserWebServiceTest extends IntegrationTestBase { UserWebService userWebService; @Autowired - TestDataCreator testDataCreator; + TestUserCreator testUserCreator; @Autowired UserService userService; @@ -36,7 +36,7 @@ class UserWebServiceTest extends IntegrationTestBase { @DisplayName("사용자 정보 조회 - 정상; 회원가입 전 사용자") void userInfo1() { // given - User unregisteredUser = testDataCreator.createUnregisteredUser(); + User unregisteredUser = testUserCreator.createUnregisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); // when @@ -55,7 +55,7 @@ void userInfo1() { @DisplayName("사용자 정보 조회 - 정상; 회원가입 후 사용자") void userInfo2() { // given - User registeredUser = testDataCreator.createRegisteredUser(); + User registeredUser = testUserCreator.createRegisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); // when @@ -74,7 +74,7 @@ void userInfo2() { @DisplayName("사용자 정보 조회 - 정상; 온보딩 완료 사용자") void userInfo3() { // given - User completedOnboardingUser = testDataCreator.createCompletedOnboardingUser(); + User completedOnboardingUser = testUserCreator.createCompletedOnboardingUser(); VookLoginUser vookLoginUser = VookLoginUser.of(completedOnboardingUser.getUid()); // when @@ -93,7 +93,7 @@ void userInfo3() { @DisplayName("회원 가입 - 정상") void register1() { // given - User unregisteredUser = testDataCreator.createUnregisteredUser(); + User unregisteredUser = testUserCreator.createUnregisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); UserRegisterRequest request = new UserRegisterRequest(); @@ -118,7 +118,7 @@ void register1() { @DisplayName("회원 가입 - 에러; 이미 가입된 유저") void registerError1() { // given - User registeredUser = testDataCreator.createRegisteredUser(); + User registeredUser = testUserCreator.createRegisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); UserRegisterRequest request = new UserRegisterRequest(); @@ -135,7 +135,7 @@ void registerError1() { @DisplayName("회원 가입 - 에러; 탈퇴한 유저") void registerError2() { // given - User withdrawnUser = testDataCreator.createWithdrawnUser(); + User withdrawnUser = testUserCreator.createWithdrawnUser(); VookLoginUser vookLoginUser = VookLoginUser.of(withdrawnUser.getUid()); UserRegisterRequest request = new UserRegisterRequest(); @@ -152,7 +152,7 @@ void registerError2() { @DisplayName("온보딩 완료 - 정상") void onboarding1() { // given - User registeredUser = testDataCreator.createRegisteredUser(); + User registeredUser = testUserCreator.createRegisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); UserOnboardingRequest request = new UserOnboardingRequest(); @@ -176,7 +176,7 @@ void onboarding1() { @DisplayName("온보딩 완료 - 에러; 미 가입 유저") void onboardingError1() { // given - User unregisteredUser = testDataCreator.createUnregisteredUser(); + User unregisteredUser = testUserCreator.createUnregisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); UserOnboardingRequest request = new UserOnboardingRequest(); @@ -192,7 +192,7 @@ void onboardingError1() { @DisplayName("온보딩 완료 - 에러; 이미 온보딩 완료된 유저") void onboardingError2() { // given - User completedOnboardingUser = testDataCreator.createCompletedOnboardingUser(); + User completedOnboardingUser = testUserCreator.createCompletedOnboardingUser(); VookLoginUser vookLoginUser = VookLoginUser.of(completedOnboardingUser.getUid()); UserOnboardingRequest request = new UserOnboardingRequest(); @@ -208,7 +208,7 @@ void onboardingError2() { @DisplayName("사용자 정보 수정 - 정상") void updateInfo1() { // given - User registeredUser = testDataCreator.createRegisteredUser(); + User registeredUser = testUserCreator.createRegisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); UserUpdateInfoRequest request = new UserUpdateInfoRequest(); request.setNickname("newNickname"); @@ -226,7 +226,7 @@ void updateInfo1() { @DisplayName("사용자 정보 수정 - 에러; 미 가입 유저") void updateInfoError1() { // given - User unregisteredUser = testDataCreator.createUnregisteredUser(); + User unregisteredUser = testUserCreator.createUnregisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(unregisteredUser.getUid()); UserUpdateInfoRequest request = new UserUpdateInfoRequest(); request.setNickname("newNickname"); @@ -240,7 +240,7 @@ void updateInfoError1() { @DisplayName("탈퇴 - 정상") void withdraw1() { // given - User registeredUser = testDataCreator.createRegisteredUser(); + User registeredUser = testUserCreator.createRegisteredUser(); VookLoginUser vookLoginUser = VookLoginUser.of(registeredUser.getUid()); // when From ce7eab309e27185889c25694aa3bddb72d7dfc29 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 02:09:48 +0900 Subject: [PATCH 131/273] =?UTF-8?q?test:=20=EC=9A=A9=EC=96=B4=EC=9E=A5=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1,=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20#81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../creator/TestVocabularyCreator.java | 21 +++++ .../VocabularyRestControllerTest.java | 86 +++++++++++++++++ .../vocabulary/VocabularyWebServiceTest.java | 92 +++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 api/src/test/java/vook/server/api/testhelper/creator/TestVocabularyCreator.java create mode 100644 api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyRestControllerTest.java create mode 100644 api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java diff --git a/api/src/test/java/vook/server/api/testhelper/creator/TestVocabularyCreator.java b/api/src/test/java/vook/server/api/testhelper/creator/TestVocabularyCreator.java new file mode 100644 index 00000000..028ccb12 --- /dev/null +++ b/api/src/test/java/vook/server/api/testhelper/creator/TestVocabularyCreator.java @@ -0,0 +1,21 @@ +package vook.server.api.testhelper.creator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.app.vocabulary.VocabularyService; +import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.model.user.User; +import vook.server.api.model.vocabulary.Vocabulary; + +@Component +@Transactional +@RequiredArgsConstructor +public class TestVocabularyCreator { + + private final VocabularyService vocabularyService; + + public Vocabulary createVocabulary(User user) { + return vocabularyService.create(VocabularyCreateCommand.of("testVocabulary", user)); + } +} diff --git a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyRestControllerTest.java new file mode 100644 index 00000000..d21a3b91 --- /dev/null +++ b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyRestControllerTest.java @@ -0,0 +1,86 @@ +package vook.server.api.web.routes.vocabulary; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import vook.server.api.model.user.User; +import vook.server.api.testhelper.HttpEntityBuilder; +import vook.server.api.testhelper.IntegrationTestBase; +import vook.server.api.testhelper.creator.TestUserCreator; +import vook.server.api.web.auth.data.GeneratedToken; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +@Transactional +class VocabularyRestControllerTest extends IntegrationTestBase { + + @MockBean + VocabularyWebService webService; + + @Autowired + TestUserCreator testUserCreator; + + @Test + @DisplayName("단어장 생성 - 정상") + void createVocabulary() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + GeneratedToken token = testUserCreator.createToken(user); + + // when + var res = rest.exchange( + "/vocabularies", + HttpMethod.POST, + new HttpEntityBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .body(Map.of("name", "단어장 이름")) + .build(), + VocabularyApi.VocabularyApiVocabulariesResponse.class + ); + + // then + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @TestFactory + @DisplayName("단어장 생성 - 실패") + Collection createVocabularyFail() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + GeneratedToken token = testUserCreator.createToken(user); + + Function, ResponseEntity> restExchange = body -> rest.exchange( + "/vocabularies", + HttpMethod.POST, + new HttpEntityBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .body(body) + .build(), + String.class + ); + + return List.of( + DynamicTest.dynamicTest("단어장 이름 누락", () -> { + var res = restExchange.apply(Map.of()); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }), + DynamicTest.dynamicTest("단어장 이름 길이 제한 초과", () -> { + var res = restExchange.apply(Map.of("name", "012345678901234567891")); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }) + ); + } + +} diff --git a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java new file mode 100644 index 00000000..4ba7eed8 --- /dev/null +++ b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java @@ -0,0 +1,92 @@ +package vook.server.api.web.routes.vocabulary; + +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 vook.server.api.app.vocabulary.VocabularyService; +import vook.server.api.app.vocabulary.exception.VocabularyLimitExceededException; +import vook.server.api.model.user.User; +import vook.server.api.model.vocabulary.Vocabulary; +import vook.server.api.testhelper.IntegrationTestBase; +import vook.server.api.testhelper.creator.TestUserCreator; +import vook.server.api.testhelper.creator.TestVocabularyCreator; +import vook.server.api.web.auth.data.VookLoginUser; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyCreateRequest; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Transactional +class VocabularyWebServiceTest extends IntegrationTestBase { + + @Autowired + VocabularyWebService vocabularyWebService; + + @Autowired + TestUserCreator testUserCreator; + @Autowired + TestVocabularyCreator testVocabularyCreator; + @Autowired + VocabularyService vocabularyService; + + @Test + @DisplayName("단어장 조회 - 정상") + void vocabularies() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + Vocabulary vocabulary = testVocabularyCreator.createVocabulary(user); + + // when + List vocabularies = vocabularyWebService.vocabularies(vookLoginUser); + + // then + assertThat(vocabularies).hasSize(1); + assertThat(vocabularies.getFirst().getUid()).isEqualTo(vocabulary.getUid()); + assertThat(vocabularies.getFirst().getName()).isEqualTo(vocabulary.getName()); + assertThat(vocabularies.getFirst().getTermCount()).isEqualTo(vocabulary.termCount()); + assertThat(vocabularies.getFirst().getCreatedAt()).isEqualTo(vocabulary.getCreatedAt()); + } + + @Test + @DisplayName("단어장 생성 - 정상") + void createVocabulary() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + var request = new VocabularyCreateRequest(); + request.setName("testVocabulary"); + + // when + vocabularyWebService.createVocabulary(vookLoginUser, request); + + // then + List vocabularies = vocabularyService.findAllBy(user); + assertThat(vocabularies).hasSize(1); + assertThat(vocabularies.getFirst().getName()).isEqualTo(request.getName()); + } + + @Test + @DisplayName("단어장 생성 - 실패; 단어장이 이미 3개 존재하는 경우") + void createVocabularyError1() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + + // 단어장 3개 생성 + testVocabularyCreator.createVocabulary(user); + testVocabularyCreator.createVocabulary(user); + testVocabularyCreator.createVocabulary(user); + + var request = new VocabularyCreateRequest(); + request.setName("testVocabulary"); + + // when + assertThatThrownBy(() -> vocabularyWebService.createVocabulary(vookLoginUser, request)) + .isInstanceOf(VocabularyLimitExceededException.class); + } +} From a61f59a7c501144f7a4d74b88332a5a0a71f4a47 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 11:11:04 +0900 Subject: [PATCH 132/273] chore: dagger version up --- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/deploy-stag.yml | 2 +- cicd/dagger.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index ce183eb7..d61c6c9d 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -24,7 +24,7 @@ jobs: SSH_DEST: ${{ secrets.DEV_SSH_DEST }} SSH_KEY: ${{ secrets.DEV_SSH_KEY }} with: - version: 0.11.6 + version: 0.11.7 verb: call module: ./cicd args: >- diff --git a/.github/workflows/deploy-stag.yml b/.github/workflows/deploy-stag.yml index ffad63f9..991c969f 100644 --- a/.github/workflows/deploy-stag.yml +++ b/.github/workflows/deploy-stag.yml @@ -24,7 +24,7 @@ jobs: SSH_DEST: ${{ secrets.STAG_SSH_DEST }} SSH_KEY: ${{ secrets.STAG_SSH_KEY }} with: - version: 0.11.6 + version: 0.11.7 verb: call module: ./cicd args: >- diff --git a/cicd/dagger.json b/cicd/dagger.json index 12deb9a4..00e33ed0 100644 --- a/cicd/dagger.json +++ b/cicd/dagger.json @@ -24,5 +24,5 @@ } ], "source": "dagger", - "engineVersion": "v0.11.6" + "engineVersion": "v0.11.7" } From b26ae6b4ecc8033e8f43c1964ed19a9645a055ba Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 17:11:50 +0900 Subject: [PATCH 133/273] =?UTF-8?q?fix:=20test=EC=9A=A9=20user=EB=A5=BC=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=EC=84=9C=202=EB=B2=88=20?= =?UTF-8?q?=EC=9D=B4=EC=83=81=20=ED=98=B8=EC=B6=9C=20=ED=95=A0=20=EC=88=98?= =?UTF-8?q?=20=EC=97=86=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vook/server/api/testhelper/creator/TestUserCreator.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java b/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java index 4c26c083..3fb7c5fc 100644 --- a/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java +++ b/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java @@ -14,6 +14,8 @@ import vook.server.api.web.auth.app.TokenService; import vook.server.api.web.auth.data.GeneratedToken; +import java.util.concurrent.atomic.AtomicInteger; + @Component @Transactional @RequiredArgsConstructor @@ -21,10 +23,11 @@ public class TestUserCreator { private final UserService userService; private final TokenService tokenService; + private final AtomicInteger userCounter = new AtomicInteger(0); public User createUnregisteredUser() { SocialUser user = userService.signUpFromSocial( - SignUpFromSocialCommand.of("testProvider", "testProviderUserId", "testEmail@test.com") + SignUpFromSocialCommand.of("testProvider", "testProviderUserId" + userCounter.getAndIncrement(), "testEmail" + userCounter.getAndIncrement() + "@test.com") ); return user.getUser(); } From 589828d90820b51982902f0af198fc24f61989a0 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 17:12:09 +0900 Subject: [PATCH 134/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20#81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/app/vocabulary/VocabularyService.java | 11 ++++ .../data/VocabularyUpdateCommand.java | 20 +++++++ .../VocabularyNotFoundException.java | 10 ++++ .../vocabulary/repo/VocabularyRepository.java | 3 + .../api/model/vocabulary/Vocabulary.java | 8 +++ .../web/routes/vocabulary/VocabularyApi.java | 29 +++++++++ .../vocabulary/VocabularyRestController.java | 12 ++++ .../vocabulary/VocabularyWebService.java | 17 +++++- .../reqres/VocabularyUpdateRequest.java | 13 ++++ .../VocabularyRestControllerTest.java | 50 ++++++++++++++++ .../vocabulary/VocabularyWebServiceTest.java | 60 ++++++++++++++++++- 11 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyUpdateCommand.java create mode 100644 api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyNotFoundException.java create mode 100644 api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyUpdateRequest.java diff --git a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java b/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java index 81e89171..5c92638e 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java +++ b/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java @@ -3,7 +3,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.app.vocabulary.data.VocabularyUpdateCommand; import vook.server.api.app.vocabulary.exception.VocabularyLimitExceededException; +import vook.server.api.app.vocabulary.exception.VocabularyNotFoundException; import vook.server.api.app.vocabulary.repo.VocabularyRepository; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; @@ -28,4 +30,13 @@ public Vocabulary create(VocabularyCreateCommand command) { return repository.save(Vocabulary.forCreateOf(command.getName(), user)); } + + public void update(VocabularyUpdateCommand command) { + Vocabulary vocabulary = repository.findByUid(command.getVocabularyUid()).orElseThrow(VocabularyNotFoundException::new); + if (!vocabulary.isValidOwner(command.getUser())) { + throw new VocabularyNotFoundException(); + } + + vocabulary.update(command.getName()); + } } diff --git a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyUpdateCommand.java b/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyUpdateCommand.java new file mode 100644 index 00000000..0aa7b32c --- /dev/null +++ b/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyUpdateCommand.java @@ -0,0 +1,20 @@ +package vook.server.api.app.vocabulary.data; + +import lombok.Getter; +import vook.server.api.model.user.User; + +@Getter +public class VocabularyUpdateCommand { + + private String vocabularyUid; + private String name; + private User user; + + public static VocabularyUpdateCommand of(String vocabularyUid, String name, User user) { + VocabularyUpdateCommand command = new VocabularyUpdateCommand(); + command.vocabularyUid = vocabularyUid; + command.name = name; + command.user = user; + return command; + } +} diff --git a/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyNotFoundException.java b/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyNotFoundException.java new file mode 100644 index 00000000..8556ac1d --- /dev/null +++ b/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyNotFoundException.java @@ -0,0 +1,10 @@ +package vook.server.api.app.vocabulary.exception; + +import vook.server.api.app.common.AppException; + +public class VocabularyNotFoundException extends AppException { + @Override + public String contents() { + return "VocabularyNotFound"; + } +} diff --git a/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java b/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java index 5ed69461..98acc4d3 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java +++ b/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java @@ -5,7 +5,10 @@ import vook.server.api.model.vocabulary.Vocabulary; import java.util.List; +import java.util.Optional; public interface VocabularyRepository extends JpaRepository { List findAllByUser(User user); + + Optional findByUid(String vocabularyUid); } diff --git a/api/src/main/java/vook/server/api/model/vocabulary/Vocabulary.java b/api/src/main/java/vook/server/api/model/vocabulary/Vocabulary.java index 58a94637..975e7a3d 100644 --- a/api/src/main/java/vook/server/api/model/vocabulary/Vocabulary.java +++ b/api/src/main/java/vook/server/api/model/vocabulary/Vocabulary.java @@ -51,4 +51,12 @@ public void addTerm(Term term) { public int termCount() { return this.terms.size(); } + + public boolean isValidOwner(User user) { + return this.user.equals(user); + } + + public void update(String name) { + this.name = name; + } } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java index cb605d86..5913a34a 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java @@ -12,6 +12,7 @@ import vook.server.api.web.common.CommonApiResponse; import vook.server.api.web.routes.vocabulary.reqres.VocabularyCreateRequest; import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyUpdateRequest; import vook.server.api.web.swagger.ComponentRefConsts; import java.util.List; @@ -62,4 +63,32 @@ class VocabularyApiVocabulariesResponse extends CommonApiResponse createVocabulary(VookLoginUser user, VocabularyCreateRequest request); + + @Operation( + summary = "용어집 수정", + security = { + @SecurityRequirement(name = "AccessToken") + }, + description = """ + 비즈니스 규칙 위반 내용 + - VocabularyNotFound: 사용자의 용어집 중 해당 ID의 용어집이 존재하지 않는 경우""" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "400", + content = @Content( + mediaType = "application/json", + schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), + examples = { + @ExampleObject(name = "유효하지 않은 파라미터", ref = ComponentRefConsts.Example.INVALID_PARAMETER), + @ExampleObject(name = "비즈니스 규칙 위반", ref = ComponentRefConsts.Example.VIOLATION_BUSINESS_RULE) + } + ) + ), + }) + CommonApiResponse updateVocabulary( + VookLoginUser user, + String vocabularyUid, + VocabularyUpdateRequest request + ); } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java index 15750e26..b93ea461 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java @@ -8,6 +8,7 @@ import vook.server.api.web.common.CommonApiResponse; import vook.server.api.web.routes.vocabulary.reqres.VocabularyCreateRequest; import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyUpdateRequest; import java.util.List; @@ -36,4 +37,15 @@ public CommonApiResponse createVocabulary( service.createVocabulary(user, request); return CommonApiResponse.ok(); } + + @Override + @PutMapping("/{vocabularyUid}") + public CommonApiResponse updateVocabulary( + @AuthenticationPrincipal VookLoginUser user, + @PathVariable String vocabularyUid, + @Validated @RequestBody VocabularyUpdateRequest request + ) { + service.updateVocabulary(user, vocabularyUid, request); + return CommonApiResponse.ok(); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java index b5f77b3b..4bdf1059 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java @@ -6,11 +6,13 @@ import vook.server.api.app.user.UserService; import vook.server.api.app.vocabulary.VocabularyService; import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.app.vocabulary.data.VocabularyUpdateCommand; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.routes.vocabulary.reqres.VocabularyCreateRequest; import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyUpdateRequest; import java.util.List; @@ -20,16 +22,25 @@ public class VocabularyWebService { private final UserService userService; - private final VocabularyService service; + private final VocabularyService vocabularyService; public List vocabularies(VookLoginUser loginUser) { User user = userService.findByUid(loginUser.getUid()).orElseThrow(); - List vocabularies = service.findAllBy(user); + List vocabularies = vocabularyService.findAllBy(user); return VocabularyResponse.from(vocabularies); } public void createVocabulary(VookLoginUser loginUser, VocabularyCreateRequest request) { User user = userService.findByUid(loginUser.getUid()).orElseThrow(); - service.create(VocabularyCreateCommand.of(request.getName(), user)); + vocabularyService.create(VocabularyCreateCommand.of(request.getName(), user)); + } + + public void updateVocabulary( + VookLoginUser loginUser, + String vocabularyUid, + VocabularyUpdateRequest request + ) { + User user = userService.findByUid(loginUser.getUid()).orElseThrow(); + vocabularyService.update(VocabularyUpdateCommand.of(vocabularyUid, request.getName(), user)); } } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyUpdateRequest.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyUpdateRequest.java new file mode 100644 index 00000000..ac1db83c --- /dev/null +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/reqres/VocabularyUpdateRequest.java @@ -0,0 +1,13 @@ +package vook.server.api.web.routes.vocabulary.reqres; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class VocabularyUpdateRequest { + + @NotBlank + @Size(min = 1, max = 20) + private String name; +} diff --git a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyRestControllerTest.java b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyRestControllerTest.java index d21a3b91..09a8e12d 100644 --- a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyRestControllerTest.java +++ b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyRestControllerTest.java @@ -83,4 +83,54 @@ Collection createVocabularyFail() { ); } + @Test + @DisplayName("단어장 수정 - 정상") + void updateVocabulary() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + GeneratedToken token = testUserCreator.createToken(user); + + // when + var res = rest.exchange( + "/vocabularies/1", + HttpMethod.PUT, + new HttpEntityBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .body(Map.of("name", "단어장 이름")) + .build(), + VocabularyApi.VocabularyApiVocabulariesResponse.class + ); + + // then + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @TestFactory + @DisplayName("단어장 수정 - 실패") + Collection updateVocabularyFail() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + GeneratedToken token = testUserCreator.createToken(user); + + Function, ResponseEntity> restExchange = body -> rest.exchange( + "/vocabularies/1", + HttpMethod.PUT, + new HttpEntityBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .body(body) + .build(), + String.class + ); + + return List.of( + DynamicTest.dynamicTest("단어장 이름 누락", () -> { + var res = restExchange.apply(Map.of()); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }), + DynamicTest.dynamicTest("단어장 이름 길이 제한 초과", () -> { + var res = restExchange.apply(Map.of("name", "012345678901234567891")); + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + }) + ); + } } diff --git a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java index 4ba7eed8..9b471646 100644 --- a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java @@ -4,8 +4,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.vocabulary.VocabularyService; import vook.server.api.app.vocabulary.exception.VocabularyLimitExceededException; +import vook.server.api.app.vocabulary.exception.VocabularyNotFoundException; +import vook.server.api.app.vocabulary.repo.VocabularyRepository; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; import vook.server.api.testhelper.IntegrationTestBase; @@ -14,6 +15,7 @@ import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.routes.vocabulary.reqres.VocabularyCreateRequest; import vook.server.api.web.routes.vocabulary.reqres.VocabularyResponse; +import vook.server.api.web.routes.vocabulary.reqres.VocabularyUpdateRequest; import java.util.List; @@ -31,7 +33,7 @@ class VocabularyWebServiceTest extends IntegrationTestBase { @Autowired TestVocabularyCreator testVocabularyCreator; @Autowired - VocabularyService vocabularyService; + VocabularyRepository vocabularyRepository; @Test @DisplayName("단어장 조회 - 정상") @@ -65,7 +67,7 @@ void createVocabulary() { vocabularyWebService.createVocabulary(vookLoginUser, request); // then - List vocabularies = vocabularyService.findAllBy(user); + List vocabularies = vocabularyRepository.findAllByUser(user); assertThat(vocabularies).hasSize(1); assertThat(vocabularies.getFirst().getName()).isEqualTo(request.getName()); } @@ -89,4 +91,56 @@ void createVocabularyError1() { assertThatThrownBy(() -> vocabularyWebService.createVocabulary(vookLoginUser, request)) .isInstanceOf(VocabularyLimitExceededException.class); } + + @Test + @DisplayName("단어장 수정 - 정상") + void updateVocabulary() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + Vocabulary vocabulary = testVocabularyCreator.createVocabulary(user); + + var request = new VocabularyUpdateRequest(); + request.setName("updatedName"); + + // when + vocabularyWebService.updateVocabulary(vookLoginUser, vocabulary.getUid(), request); + + // then + Vocabulary updatedVocabulary = vocabularyRepository.findByUid(vocabulary.getUid()).orElseThrow(); + assertThat(updatedVocabulary.getName()).isEqualTo(request.getName()); + } + + @Test + @DisplayName("단어장 수정 - 실패; 해당 단어장이 존재하지 않는 경우") + void updateVocabularyError1() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + testVocabularyCreator.createVocabulary(user); + + var request = new VocabularyUpdateRequest(); + request.setName("updatedName"); + + // when + assertThatThrownBy(() -> vocabularyWebService.updateVocabulary(vookLoginUser, "nonExistentUid", request)) + .isInstanceOf(VocabularyNotFoundException.class); + } + + @Test + @DisplayName("단어장 수정 - 실패; 해당 단어장이 다른 사용자의 것인 경우") + void updateVocabularyError2() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + User otherUser = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + Vocabulary vocabulary = testVocabularyCreator.createVocabulary(otherUser); + + var request = new VocabularyUpdateRequest(); + request.setName("updatedName"); + + // when + assertThatThrownBy(() -> vocabularyWebService.updateVocabulary(vookLoginUser, vocabulary.getUid(), request)) + .isInstanceOf(VocabularyNotFoundException.class); + } } From e3124469d8a9789aeac56b0a54af7d5c8754728b Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 17:24:36 +0900 Subject: [PATCH 135/273] =?UTF-8?q?feat:=20=EC=9A=A9=EC=96=B4=EC=A7=91=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20API=20#81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/app/vocabulary/VocabularyService.java | 18 ++++++-- .../data/VocabularyDeleteCommand.java | 18 ++++++++ .../web/routes/vocabulary/VocabularyApi.java | 23 ++++++++++ .../vocabulary/VocabularyRestController.java | 10 +++++ .../vocabulary/VocabularyWebService.java | 6 +++ .../vocabulary/VocabularyWebServiceTest.java | 42 +++++++++++++++++++ 6 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyDeleteCommand.java diff --git a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java b/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java index 5c92638e..8a8b1ce9 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java +++ b/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.app.vocabulary.data.VocabularyDeleteCommand; import vook.server.api.app.vocabulary.data.VocabularyUpdateCommand; import vook.server.api.app.vocabulary.exception.VocabularyLimitExceededException; import vook.server.api.app.vocabulary.exception.VocabularyNotFoundException; @@ -32,11 +33,20 @@ public Vocabulary create(VocabularyCreateCommand command) { } public void update(VocabularyUpdateCommand command) { - Vocabulary vocabulary = repository.findByUid(command.getVocabularyUid()).orElseThrow(VocabularyNotFoundException::new); - if (!vocabulary.isValidOwner(command.getUser())) { + Vocabulary vocabulary = validateAndGetVocabulary(command.getVocabularyUid(), command.getUser()); + vocabulary.update(command.getName()); + } + + public void delete(VocabularyDeleteCommand command) { + Vocabulary vocabulary = validateAndGetVocabulary(command.getVocabularyUid(), command.getUser()); + repository.delete(vocabulary); + } + + private Vocabulary validateAndGetVocabulary(String vocabularyUid, User user) { + Vocabulary vocabulary = repository.findByUid(vocabularyUid).orElseThrow(VocabularyNotFoundException::new); + if (!vocabulary.isValidOwner(user)) { throw new VocabularyNotFoundException(); } - - vocabulary.update(command.getName()); + return vocabulary; } } diff --git a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyDeleteCommand.java b/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyDeleteCommand.java new file mode 100644 index 00000000..7a3902a7 --- /dev/null +++ b/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyDeleteCommand.java @@ -0,0 +1,18 @@ +package vook.server.api.app.vocabulary.data; + +import lombok.Getter; +import vook.server.api.model.user.User; + +@Getter +public class VocabularyDeleteCommand { + + private String vocabularyUid; + private User user; + + public static VocabularyDeleteCommand of(String vocabularyUid, User user) { + VocabularyDeleteCommand command = new VocabularyDeleteCommand(); + command.vocabularyUid = vocabularyUid; + command.user = user; + return command; + } +} diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java index 5913a34a..e99b4806 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java @@ -91,4 +91,27 @@ CommonApiResponse updateVocabulary( String vocabularyUid, VocabularyUpdateRequest request ); + + @Operation( + summary = "용어집 삭제", + security = { + @SecurityRequirement(name = "AccessToken") + }, + description = """ + 비즈니스 규칙 위반 내용 + - VocabularyNotFound: 사용자의 용어집 중 해당 ID의 용어집이 존재하지 않는 경우""" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "400", + content = @Content( + mediaType = "application/json", + schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), + examples = { + @ExampleObject(name = "비즈니스 규칙 위반", ref = ComponentRefConsts.Example.VIOLATION_BUSINESS_RULE) + } + ) + ), + }) + CommonApiResponse deleteVocabulary(VookLoginUser user, String vocabularyUid); } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java index b93ea461..20094a14 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyRestController.java @@ -48,4 +48,14 @@ public CommonApiResponse updateVocabulary( service.updateVocabulary(user, vocabularyUid, request); return CommonApiResponse.ok(); } + + @Override + @DeleteMapping("/{vocabularyUid}") + public CommonApiResponse deleteVocabulary( + @AuthenticationPrincipal VookLoginUser user, + @PathVariable String vocabularyUid + ) { + service.deleteVocabulary(user, vocabularyUid); + return CommonApiResponse.ok(); + } } diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java index 4bdf1059..400c7710 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java @@ -6,6 +6,7 @@ import vook.server.api.app.user.UserService; import vook.server.api.app.vocabulary.VocabularyService; import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.app.vocabulary.data.VocabularyDeleteCommand; import vook.server.api.app.vocabulary.data.VocabularyUpdateCommand; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; @@ -43,4 +44,9 @@ public void updateVocabulary( User user = userService.findByUid(loginUser.getUid()).orElseThrow(); vocabularyService.update(VocabularyUpdateCommand.of(vocabularyUid, request.getName(), user)); } + + public void deleteVocabulary(VookLoginUser loginUser, String vocabularyUid) { + User user = userService.findByUid(loginUser.getUid()).orElseThrow(); + vocabularyService.delete(VocabularyDeleteCommand.of(vocabularyUid, user)); + } } diff --git a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java index 9b471646..a66fb9a4 100644 --- a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java @@ -143,4 +143,46 @@ void updateVocabularyError2() { assertThatThrownBy(() -> vocabularyWebService.updateVocabulary(vookLoginUser, vocabulary.getUid(), request)) .isInstanceOf(VocabularyNotFoundException.class); } + + @Test + @DisplayName("단어장 삭제 - 정상") + void deleteVocabulary() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + Vocabulary vocabulary = testVocabularyCreator.createVocabulary(user); + + // when + vocabularyWebService.deleteVocabulary(vookLoginUser, vocabulary.getUid()); + + // then + assertThat(vocabularyRepository.findByUid(vocabulary.getUid())).isEmpty(); + } + + @Test + @DisplayName("단어장 삭제 - 실패; 해당 단어장이 존재하지 않는 경우") + void deleteVocabularyError1() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + testVocabularyCreator.createVocabulary(user); + + // when + assertThatThrownBy(() -> vocabularyWebService.deleteVocabulary(vookLoginUser, "nonExistentUid")) + .isInstanceOf(VocabularyNotFoundException.class); + } + + @Test + @DisplayName("단어장 삭제 - 실패; 해당 단어장이 다른 사용자의 것인 경우") + void deleteVocabularyError2() { + // given + User user = testUserCreator.createCompletedOnboardingUser(); + User otherUser = testUserCreator.createCompletedOnboardingUser(); + VookLoginUser vookLoginUser = VookLoginUser.of(user.getUid()); + Vocabulary vocabulary = testVocabularyCreator.createVocabulary(otherUser); + + // when + assertThatThrownBy(() -> vocabularyWebService.deleteVocabulary(vookLoginUser, vocabulary.getUid())) + .isInstanceOf(VocabularyNotFoundException.class); + } } From 875be5cba3e9f4a102ddcd6efbc01084a13c517f Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 21:55:19 +0900 Subject: [PATCH 136/273] =?UTF-8?q?chore:=20github=20actions=EC=97=90=20se?= =?UTF-8?q?tup-go=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dagger를 이용한 배포에서는 go가 따로 필요하지 않음 --- .github/workflows/deploy-dev.yml | 3 --- .github/workflows/deploy-stag.yml | 3 --- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index d61c6c9d..b1cc03aa 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -15,9 +15,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: '1.22.2' - name: Deploy uses: dagger/dagger-for-github@v5 env: diff --git a/.github/workflows/deploy-stag.yml b/.github/workflows/deploy-stag.yml index 991c969f..c87a200a 100644 --- a/.github/workflows/deploy-stag.yml +++ b/.github/workflows/deploy-stag.yml @@ -15,9 +15,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: '1.22.2' - name: Deploy uses: dagger/dagger-for-github@v5 env: From 9f238ad6db601a38f5d4dd00ffdbcfe6cec6f1bf Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 22:39:30 +0900 Subject: [PATCH 137/273] =?UTF-8?q?chore:=20scp,=20ssh=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=97=90=20IP?= =?UTF-8?q?=EA=B0=80=20=EB=B3=B4=EC=9D=B4=EC=A7=80=20=EC=95=8A=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정된 scp, ssh dagger module로 버전 교체 --- cicd/dagger.json | 4 +- cicd/dagger/go.mod | 39 ++++++++++--------- cicd/dagger/go.sum | 95 +++++++++++++++++++++++++--------------------- 3 files changed, 75 insertions(+), 63 deletions(-) diff --git a/cicd/dagger.json b/cicd/dagger.json index 00e33ed0..d5b9b7ab 100644 --- a/cicd/dagger.json +++ b/cicd/dagger.json @@ -16,11 +16,11 @@ }, { "name": "scp", - "source": "github.com/seungyeop-lee/daggerverse/scp@63e721cf63323ede8b1f11aeff00d41960699920" + "source": "github.com/seungyeop-lee/daggerverse/scp@63f0f2d385768aa435474a9eec552750500899f2" }, { "name": "ssh", - "source": "github.com/seungyeop-lee/daggerverse/ssh@63e721cf63323ede8b1f11aeff00d41960699920" + "source": "github.com/seungyeop-lee/daggerverse/ssh@63f0f2d385768aa435474a9eec552750500899f2" } ], "source": "dagger", diff --git a/cicd/dagger/go.mod b/cicd/dagger/go.mod index 7ede703c..58c8d0c4 100644 --- a/cicd/dagger/go.mod +++ b/cicd/dagger/go.mod @@ -6,32 +6,35 @@ require ( github.com/99designs/gqlgen v0.17.44 github.com/Khan/genqlient v0.7.0 github.com/vektah/gqlparser/v2 v2.5.11 - go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel v1.26.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 - go.opentelemetry.io/otel/sdk v1.24.0 - go.opentelemetry.io/otel/trace v1.24.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 + go.opentelemetry.io/otel/sdk v1.26.0 + go.opentelemetry.io/otel/trace v1.26.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa - golang.org/x/sync v0.6.0 - google.golang.org/grpc v1.62.1 + golang.org/x/sync v0.7.0 + google.golang.org/grpc v1.64.0 ) require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sosodev/duration v1.2.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.1.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect - google.golang.org/protobuf v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect + go.opentelemetry.io/otel/log v0.2.0-alpha + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.2.0-alpha + go.opentelemetry.io/proto/otlp v1.2.0 + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/protobuf v1.34.1 // indirect ) diff --git a/cicd/dagger/go.sum b/cicd/dagger/go.sum index 55462ae8..246c7479 100644 --- a/cicd/dagger/go.sum +++ b/cicd/dagger/go.sum @@ -6,6 +6,7 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -13,66 +14,74 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us= github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 h1:oM0GTNKGlc5qHctWeIGTVyda4iFFalOzMZ3Ehj5rwB4= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88/go.mod h1:JGG8ebaMO5nXOPnvKEl+DiA4MGwFjCbjsxT1WHIEBPY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha h1:z2s6Zba+OUyayRv5m1AXWNUTGh57K1iMhy6emU5QT5Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha/go.mod h1:paOXXyUgPW6jYxYkP0pB47H2zHE1fPvMJ4E4G9LHOi0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38= +go.opentelemetry.io/otel/log v0.2.0-alpha h1:ixOPvMzserpqA07SENHvRzkZOsnG0XbPr74hv1AQ+n0= +go.opentelemetry.io/otel/log v0.2.0-alpha/go.mod h1:vbFZc65yq4c4ssvXY43y/nIqkNJLxORrqw0L85P59LA= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= +go.opentelemetry.io/otel/sdk/log v0.2.0-alpha h1:jGTkL/jroJ31jnP6jDl34N/mDOfRGGYZHcHsCM+5kWA= +go.opentelemetry.io/otel/sdk/log v0.2.0-alpha/go.mod h1:Hd8Lw9FPGUM3pfY7iGMRvFaC2Nyau4Ajb5WnQ9OdIho= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= -google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= -google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 h1:4HZJ3Xv1cmrJ+0aFo304Zn79ur1HMxptAE7aCPNLSqc= +google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 469ef8a5e333383cf935141e66c7aea5c2a43dba Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 12 Jun 2024 22:51:02 +0900 Subject: [PATCH 138/273] =?UTF-8?q?fix:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=8B=9C,=20=EC=9A=A9=EC=96=B4?= =?UTF-8?q?=EC=A7=91=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/vook/server/api/devhelper/app/InitService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/src/main/java/vook/server/api/devhelper/app/InitService.java b/api/src/main/java/vook/server/api/devhelper/app/InitService.java index 9105e2d9..b89eb7a9 100644 --- a/api/src/main/java/vook/server/api/devhelper/app/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/app/InitService.java @@ -8,6 +8,7 @@ import vook.server.api.app.user.repo.SocialUserRepository; import vook.server.api.app.user.repo.UserInfoRepository; import vook.server.api.app.user.repo.UserRepository; +import vook.server.api.app.vocabulary.repo.VocabularyRepository; import vook.server.api.model.demo.DemoTerm; import vook.server.api.outbound.search.demo.DemoTermSearchService; @@ -20,6 +21,7 @@ public class InitService { private final DemoTermRepository demoTermRepository; private final DemoTermSynonymRepository demoTermSynonymRepository; + private final VocabularyRepository vocabularyRepository; private final UserInfoRepository userInfoRepository; private final SocialUserRepository socialUserRepository; private final UserRepository userRepository; @@ -41,6 +43,9 @@ private void deleteAll() { demoTermSynonymRepository.deleteAllInBatch(); demoTermRepository.deleteAllInBatch(); + // 용어집 + vocabularyRepository.deleteAllInBatch(); + // 사용자 userInfoRepository.deleteAllInBatch(); socialUserRepository.deleteAllInBatch(); From d163173c6e06e06741288a657d04d7c5e03a5706 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 14 Jun 2024 10:38:43 +0900 Subject: [PATCH 139/273] =?UTF-8?q?refactor:=20app.domain=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../demo/repo/DemoTermRepository.java | 2 +- .../demo/repo/DemoTermSynonymRepository.java | 2 +- .../api/app/{ => domain}/user/UserService.java | 16 ++++++++-------- .../user/data/OnboardingCommand.java | 2 +- .../{ => domain}/user/data/RegisterCommand.java | 2 +- .../user/data/SignUpFromSocialCommand.java | 2 +- .../exception/AlreadyOnboardingException.java | 2 +- .../exception/AlreadyRegisteredException.java | 2 +- .../exception/NotReadyToOnboardingException.java | 4 ++-- .../user/exception/NotRegisteredException.java | 2 +- .../user/exception/WithdrawnUserException.java | 2 +- .../user/repo/SocialUserRepository.java | 2 +- .../user/repo/UserInfoRepository.java | 2 +- .../{ => domain}/user/repo/UserRepository.java | 2 +- .../vocabulary/VocabularyService.java | 14 +++++++------- .../vocabulary/data/VocabularyCreateCommand.java | 2 +- .../vocabulary/data/VocabularyDeleteCommand.java | 2 +- .../vocabulary/data/VocabularyUpdateCommand.java | 2 +- .../VocabularyLimitExceededException.java | 2 +- .../exception/VocabularyNotFoundException.java | 2 +- .../vocabulary/repo/VocabularyRepository.java | 2 +- .../server/api/devhelper/app/InitService.java | 12 ++++++------ .../server/api/devhelper/init/LocalInit.java | 2 +- .../web/auth/oauth2/VookOAuth2UserService.java | 4 ++-- .../api/web/routes/user/UserWebService.java | 2 +- .../user/reqres/UserOnboardingRequest.java | 2 +- .../routes/user/reqres/UserRegisterRequest.java | 2 +- .../routes/vocabulary/VocabularyWebService.java | 10 +++++----- .../api/testhelper/creator/TestUserCreator.java | 8 ++++---- .../creator/TestVocabularyCreator.java | 4 ++-- .../api/web/routes/demo/DemoWebServiceTest.java | 2 +- .../api/web/routes/user/UserWebServiceTest.java | 4 ++-- .../vocabulary/VocabularyWebServiceTest.java | 6 +++--- 33 files changed, 64 insertions(+), 64 deletions(-) rename api/src/main/java/vook/server/api/app/{ => domain}/demo/repo/DemoTermRepository.java (80%) rename api/src/main/java/vook/server/api/app/{ => domain}/demo/repo/DemoTermSynonymRepository.java (81%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/UserService.java (84%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/data/OnboardingCommand.java (92%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/data/RegisterCommand.java (91%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/data/SignUpFromSocialCommand.java (94%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/exception/AlreadyOnboardingException.java (79%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/exception/AlreadyRegisteredException.java (79%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/exception/NotReadyToOnboardingException.java (80%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/exception/NotRegisteredException.java (79%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/exception/WithdrawnUserException.java (79%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/repo/SocialUserRepository.java (87%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/repo/UserInfoRepository.java (80%) rename api/src/main/java/vook/server/api/app/{ => domain}/user/repo/UserRepository.java (90%) rename api/src/main/java/vook/server/api/app/{ => domain}/vocabulary/VocabularyService.java (74%) rename api/src/main/java/vook/server/api/app/{ => domain}/vocabulary/data/VocabularyCreateCommand.java (91%) rename api/src/main/java/vook/server/api/app/{ => domain}/vocabulary/data/VocabularyDeleteCommand.java (89%) rename api/src/main/java/vook/server/api/app/{ => domain}/vocabulary/data/VocabularyUpdateCommand.java (90%) rename api/src/main/java/vook/server/api/app/{ => domain}/vocabulary/exception/VocabularyLimitExceededException.java (78%) rename api/src/main/java/vook/server/api/app/{ => domain}/vocabulary/exception/VocabularyNotFoundException.java (78%) rename api/src/main/java/vook/server/api/app/{ => domain}/vocabulary/repo/VocabularyRepository.java (88%) diff --git a/api/src/main/java/vook/server/api/app/demo/repo/DemoTermRepository.java b/api/src/main/java/vook/server/api/app/domain/demo/repo/DemoTermRepository.java similarity index 80% rename from api/src/main/java/vook/server/api/app/demo/repo/DemoTermRepository.java rename to api/src/main/java/vook/server/api/app/domain/demo/repo/DemoTermRepository.java index dab1dd12..3978f70d 100644 --- a/api/src/main/java/vook/server/api/app/demo/repo/DemoTermRepository.java +++ b/api/src/main/java/vook/server/api/app/domain/demo/repo/DemoTermRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.demo.repo; +package vook.server.api.app.domain.demo.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.demo.DemoTerm; diff --git a/api/src/main/java/vook/server/api/app/demo/repo/DemoTermSynonymRepository.java b/api/src/main/java/vook/server/api/app/domain/demo/repo/DemoTermSynonymRepository.java similarity index 81% rename from api/src/main/java/vook/server/api/app/demo/repo/DemoTermSynonymRepository.java rename to api/src/main/java/vook/server/api/app/domain/demo/repo/DemoTermSynonymRepository.java index 46cef6df..d17ae655 100644 --- a/api/src/main/java/vook/server/api/app/demo/repo/DemoTermSynonymRepository.java +++ b/api/src/main/java/vook/server/api/app/domain/demo/repo/DemoTermSynonymRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.demo.repo; +package vook.server.api.app.domain.demo.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.demo.DemoTermSynonym; diff --git a/api/src/main/java/vook/server/api/app/user/UserService.java b/api/src/main/java/vook/server/api/app/domain/user/UserService.java similarity index 84% rename from api/src/main/java/vook/server/api/app/user/UserService.java rename to api/src/main/java/vook/server/api/app/domain/user/UserService.java index 366513c1..0fd30bef 100644 --- a/api/src/main/java/vook/server/api/app/user/UserService.java +++ b/api/src/main/java/vook/server/api/app/domain/user/UserService.java @@ -1,14 +1,14 @@ -package vook.server.api.app.user; +package vook.server.api.app.domain.user; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import vook.server.api.app.user.data.OnboardingCommand; -import vook.server.api.app.user.data.RegisterCommand; -import vook.server.api.app.user.data.SignUpFromSocialCommand; -import vook.server.api.app.user.exception.*; -import vook.server.api.app.user.repo.SocialUserRepository; -import vook.server.api.app.user.repo.UserInfoRepository; -import vook.server.api.app.user.repo.UserRepository; +import vook.server.api.app.domain.user.data.OnboardingCommand; +import vook.server.api.app.domain.user.data.RegisterCommand; +import vook.server.api.app.domain.user.data.SignUpFromSocialCommand; +import vook.server.api.app.domain.user.exception.*; +import vook.server.api.app.domain.user.repo.SocialUserRepository; +import vook.server.api.app.domain.user.repo.UserInfoRepository; +import vook.server.api.app.domain.user.repo.UserRepository; import vook.server.api.model.user.SocialUser; import vook.server.api.model.user.User; import vook.server.api.model.user.UserInfo; diff --git a/api/src/main/java/vook/server/api/app/user/data/OnboardingCommand.java b/api/src/main/java/vook/server/api/app/domain/user/data/OnboardingCommand.java similarity index 92% rename from api/src/main/java/vook/server/api/app/user/data/OnboardingCommand.java rename to api/src/main/java/vook/server/api/app/domain/user/data/OnboardingCommand.java index a0d4813c..eba9bee1 100644 --- a/api/src/main/java/vook/server/api/app/user/data/OnboardingCommand.java +++ b/api/src/main/java/vook/server/api/app/domain/user/data/OnboardingCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.data; +package vook.server.api.app.domain.user.data; import lombok.Getter; import vook.server.api.model.user.Funnel; diff --git a/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java b/api/src/main/java/vook/server/api/app/domain/user/data/RegisterCommand.java similarity index 91% rename from api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java rename to api/src/main/java/vook/server/api/app/domain/user/data/RegisterCommand.java index c6acee84..63989bc8 100644 --- a/api/src/main/java/vook/server/api/app/user/data/RegisterCommand.java +++ b/api/src/main/java/vook/server/api/app/domain/user/data/RegisterCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.data; +package vook.server.api.app.domain.user.data; import lombok.Getter; diff --git a/api/src/main/java/vook/server/api/app/user/data/SignUpFromSocialCommand.java b/api/src/main/java/vook/server/api/app/domain/user/data/SignUpFromSocialCommand.java similarity index 94% rename from api/src/main/java/vook/server/api/app/user/data/SignUpFromSocialCommand.java rename to api/src/main/java/vook/server/api/app/domain/user/data/SignUpFromSocialCommand.java index bc7ece14..401313e8 100644 --- a/api/src/main/java/vook/server/api/app/user/data/SignUpFromSocialCommand.java +++ b/api/src/main/java/vook/server/api/app/domain/user/data/SignUpFromSocialCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.data; +package vook.server.api.app.domain.user.data; import lombok.Getter; import vook.server.api.model.user.SocialUser; diff --git a/api/src/main/java/vook/server/api/app/user/exception/AlreadyOnboardingException.java b/api/src/main/java/vook/server/api/app/domain/user/exception/AlreadyOnboardingException.java similarity index 79% rename from api/src/main/java/vook/server/api/app/user/exception/AlreadyOnboardingException.java rename to api/src/main/java/vook/server/api/app/domain/user/exception/AlreadyOnboardingException.java index 068e2b9b..2bf33928 100644 --- a/api/src/main/java/vook/server/api/app/user/exception/AlreadyOnboardingException.java +++ b/api/src/main/java/vook/server/api/app/domain/user/exception/AlreadyOnboardingException.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.exception; +package vook.server.api.app.domain.user.exception; import vook.server.api.app.common.AppException; diff --git a/api/src/main/java/vook/server/api/app/user/exception/AlreadyRegisteredException.java b/api/src/main/java/vook/server/api/app/domain/user/exception/AlreadyRegisteredException.java similarity index 79% rename from api/src/main/java/vook/server/api/app/user/exception/AlreadyRegisteredException.java rename to api/src/main/java/vook/server/api/app/domain/user/exception/AlreadyRegisteredException.java index a7f2d1c0..22bcd4dd 100644 --- a/api/src/main/java/vook/server/api/app/user/exception/AlreadyRegisteredException.java +++ b/api/src/main/java/vook/server/api/app/domain/user/exception/AlreadyRegisteredException.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.exception; +package vook.server.api.app.domain.user.exception; import vook.server.api.app.common.AppException; diff --git a/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java b/api/src/main/java/vook/server/api/app/domain/user/exception/NotReadyToOnboardingException.java similarity index 80% rename from api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java rename to api/src/main/java/vook/server/api/app/domain/user/exception/NotReadyToOnboardingException.java index 0cba64e1..6d7b07ee 100644 --- a/api/src/main/java/vook/server/api/app/user/exception/NotReadyToOnboardingException.java +++ b/api/src/main/java/vook/server/api/app/domain/user/exception/NotReadyToOnboardingException.java @@ -1,9 +1,9 @@ -package vook.server.api.app.user.exception; +package vook.server.api.app.domain.user.exception; import vook.server.api.app.common.AppException; public class NotReadyToOnboardingException extends AppException { - + @Override public String contents() { return "NotReadyToOnboarding"; diff --git a/api/src/main/java/vook/server/api/app/user/exception/NotRegisteredException.java b/api/src/main/java/vook/server/api/app/domain/user/exception/NotRegisteredException.java similarity index 79% rename from api/src/main/java/vook/server/api/app/user/exception/NotRegisteredException.java rename to api/src/main/java/vook/server/api/app/domain/user/exception/NotRegisteredException.java index 0ff2eefe..fdb7f313 100644 --- a/api/src/main/java/vook/server/api/app/user/exception/NotRegisteredException.java +++ b/api/src/main/java/vook/server/api/app/domain/user/exception/NotRegisteredException.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.exception; +package vook.server.api.app.domain.user.exception; import vook.server.api.app.common.AppException; diff --git a/api/src/main/java/vook/server/api/app/user/exception/WithdrawnUserException.java b/api/src/main/java/vook/server/api/app/domain/user/exception/WithdrawnUserException.java similarity index 79% rename from api/src/main/java/vook/server/api/app/user/exception/WithdrawnUserException.java rename to api/src/main/java/vook/server/api/app/domain/user/exception/WithdrawnUserException.java index f91b80e5..696f6d06 100644 --- a/api/src/main/java/vook/server/api/app/user/exception/WithdrawnUserException.java +++ b/api/src/main/java/vook/server/api/app/domain/user/exception/WithdrawnUserException.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.exception; +package vook.server.api.app.domain.user.exception; import vook.server.api.app.common.AppException; diff --git a/api/src/main/java/vook/server/api/app/user/repo/SocialUserRepository.java b/api/src/main/java/vook/server/api/app/domain/user/repo/SocialUserRepository.java similarity index 87% rename from api/src/main/java/vook/server/api/app/user/repo/SocialUserRepository.java rename to api/src/main/java/vook/server/api/app/domain/user/repo/SocialUserRepository.java index 2e858bc4..f97b10e1 100644 --- a/api/src/main/java/vook/server/api/app/user/repo/SocialUserRepository.java +++ b/api/src/main/java/vook/server/api/app/domain/user/repo/SocialUserRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.repo; +package vook.server.api.app.domain.user.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.user.SocialUser; diff --git a/api/src/main/java/vook/server/api/app/user/repo/UserInfoRepository.java b/api/src/main/java/vook/server/api/app/domain/user/repo/UserInfoRepository.java similarity index 80% rename from api/src/main/java/vook/server/api/app/user/repo/UserInfoRepository.java rename to api/src/main/java/vook/server/api/app/domain/user/repo/UserInfoRepository.java index 47c18f25..4bc6b125 100644 --- a/api/src/main/java/vook/server/api/app/user/repo/UserInfoRepository.java +++ b/api/src/main/java/vook/server/api/app/domain/user/repo/UserInfoRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.repo; +package vook.server.api.app.domain.user.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.user.UserInfo; diff --git a/api/src/main/java/vook/server/api/app/user/repo/UserRepository.java b/api/src/main/java/vook/server/api/app/domain/user/repo/UserRepository.java similarity index 90% rename from api/src/main/java/vook/server/api/app/user/repo/UserRepository.java rename to api/src/main/java/vook/server/api/app/domain/user/repo/UserRepository.java index bdbca4ee..a6bf7640 100644 --- a/api/src/main/java/vook/server/api/app/user/repo/UserRepository.java +++ b/api/src/main/java/vook/server/api/app/domain/user/repo/UserRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.user.repo; +package vook.server.api.app.domain.user.repo; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java b/api/src/main/java/vook/server/api/app/domain/vocabulary/VocabularyService.java similarity index 74% rename from api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java rename to api/src/main/java/vook/server/api/app/domain/vocabulary/VocabularyService.java index 8a8b1ce9..ec349165 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/VocabularyService.java +++ b/api/src/main/java/vook/server/api/app/domain/vocabulary/VocabularyService.java @@ -1,13 +1,13 @@ -package vook.server.api.app.vocabulary; +package vook.server.api.app.domain.vocabulary; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; -import vook.server.api.app.vocabulary.data.VocabularyDeleteCommand; -import vook.server.api.app.vocabulary.data.VocabularyUpdateCommand; -import vook.server.api.app.vocabulary.exception.VocabularyLimitExceededException; -import vook.server.api.app.vocabulary.exception.VocabularyNotFoundException; -import vook.server.api.app.vocabulary.repo.VocabularyRepository; +import vook.server.api.app.domain.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.app.domain.vocabulary.data.VocabularyDeleteCommand; +import vook.server.api.app.domain.vocabulary.data.VocabularyUpdateCommand; +import vook.server.api.app.domain.vocabulary.exception.VocabularyLimitExceededException; +import vook.server.api.app.domain.vocabulary.exception.VocabularyNotFoundException; +import vook.server.api.app.domain.vocabulary.repo.VocabularyRepository; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; diff --git a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyCreateCommand.java b/api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyCreateCommand.java similarity index 91% rename from api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyCreateCommand.java rename to api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyCreateCommand.java index e178acb0..b5c7d9fa 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyCreateCommand.java +++ b/api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyCreateCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.vocabulary.data; +package vook.server.api.app.domain.vocabulary.data; import lombok.Getter; import vook.server.api.model.user.User; diff --git a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyDeleteCommand.java b/api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyDeleteCommand.java similarity index 89% rename from api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyDeleteCommand.java rename to api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyDeleteCommand.java index 7a3902a7..a847ec9d 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyDeleteCommand.java +++ b/api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyDeleteCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.vocabulary.data; +package vook.server.api.app.domain.vocabulary.data; import lombok.Getter; import vook.server.api.model.user.User; diff --git a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyUpdateCommand.java b/api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyUpdateCommand.java similarity index 90% rename from api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyUpdateCommand.java rename to api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyUpdateCommand.java index 0aa7b32c..77dc8ead 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/data/VocabularyUpdateCommand.java +++ b/api/src/main/java/vook/server/api/app/domain/vocabulary/data/VocabularyUpdateCommand.java @@ -1,4 +1,4 @@ -package vook.server.api.app.vocabulary.data; +package vook.server.api.app.domain.vocabulary.data; import lombok.Getter; import vook.server.api.model.user.User; diff --git a/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyLimitExceededException.java b/api/src/main/java/vook/server/api/app/domain/vocabulary/exception/VocabularyLimitExceededException.java similarity index 78% rename from api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyLimitExceededException.java rename to api/src/main/java/vook/server/api/app/domain/vocabulary/exception/VocabularyLimitExceededException.java index 80b5e421..da4f1d99 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyLimitExceededException.java +++ b/api/src/main/java/vook/server/api/app/domain/vocabulary/exception/VocabularyLimitExceededException.java @@ -1,4 +1,4 @@ -package vook.server.api.app.vocabulary.exception; +package vook.server.api.app.domain.vocabulary.exception; import vook.server.api.app.common.AppException; diff --git a/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyNotFoundException.java b/api/src/main/java/vook/server/api/app/domain/vocabulary/exception/VocabularyNotFoundException.java similarity index 78% rename from api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyNotFoundException.java rename to api/src/main/java/vook/server/api/app/domain/vocabulary/exception/VocabularyNotFoundException.java index 8556ac1d..47c85d18 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/exception/VocabularyNotFoundException.java +++ b/api/src/main/java/vook/server/api/app/domain/vocabulary/exception/VocabularyNotFoundException.java @@ -1,4 +1,4 @@ -package vook.server.api.app.vocabulary.exception; +package vook.server.api.app.domain.vocabulary.exception; import vook.server.api.app.common.AppException; diff --git a/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java b/api/src/main/java/vook/server/api/app/domain/vocabulary/repo/VocabularyRepository.java similarity index 88% rename from api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java rename to api/src/main/java/vook/server/api/app/domain/vocabulary/repo/VocabularyRepository.java index 98acc4d3..2533428d 100644 --- a/api/src/main/java/vook/server/api/app/vocabulary/repo/VocabularyRepository.java +++ b/api/src/main/java/vook/server/api/app/domain/vocabulary/repo/VocabularyRepository.java @@ -1,4 +1,4 @@ -package vook.server.api.app.vocabulary.repo; +package vook.server.api.app.domain.vocabulary.repo; import org.springframework.data.jpa.repository.JpaRepository; import vook.server.api.model.user.User; diff --git a/api/src/main/java/vook/server/api/devhelper/app/InitService.java b/api/src/main/java/vook/server/api/devhelper/app/InitService.java index b89eb7a9..1412a7f8 100644 --- a/api/src/main/java/vook/server/api/devhelper/app/InitService.java +++ b/api/src/main/java/vook/server/api/devhelper/app/InitService.java @@ -3,12 +3,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.demo.repo.DemoTermRepository; -import vook.server.api.app.demo.repo.DemoTermSynonymRepository; -import vook.server.api.app.user.repo.SocialUserRepository; -import vook.server.api.app.user.repo.UserInfoRepository; -import vook.server.api.app.user.repo.UserRepository; -import vook.server.api.app.vocabulary.repo.VocabularyRepository; +import vook.server.api.app.domain.demo.repo.DemoTermRepository; +import vook.server.api.app.domain.demo.repo.DemoTermSynonymRepository; +import vook.server.api.app.domain.user.repo.SocialUserRepository; +import vook.server.api.app.domain.user.repo.UserInfoRepository; +import vook.server.api.app.domain.user.repo.UserRepository; +import vook.server.api.app.domain.vocabulary.repo.VocabularyRepository; import vook.server.api.model.demo.DemoTerm; import vook.server.api.outbound.search.demo.DemoTermSearchService; diff --git a/api/src/main/java/vook/server/api/devhelper/init/LocalInit.java b/api/src/main/java/vook/server/api/devhelper/init/LocalInit.java index 07c9c9f5..8f865605 100644 --- a/api/src/main/java/vook/server/api/devhelper/init/LocalInit.java +++ b/api/src/main/java/vook/server/api/devhelper/init/LocalInit.java @@ -5,7 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import vook.server.api.app.demo.repo.DemoTermRepository; +import vook.server.api.app.domain.demo.repo.DemoTermRepository; import vook.server.api.devhelper.app.InitService; @Slf4j diff --git a/api/src/main/java/vook/server/api/web/auth/oauth2/VookOAuth2UserService.java b/api/src/main/java/vook/server/api/web/auth/oauth2/VookOAuth2UserService.java index 202c3b8e..597a76ab 100644 --- a/api/src/main/java/vook/server/api/web/auth/oauth2/VookOAuth2UserService.java +++ b/api/src/main/java/vook/server/api/web/auth/oauth2/VookOAuth2UserService.java @@ -8,8 +8,8 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.user.UserService; -import vook.server.api.app.user.data.SignUpFromSocialCommand; +import vook.server.api.app.domain.user.UserService; +import vook.server.api.app.domain.user.data.SignUpFromSocialCommand; import vook.server.api.model.user.SocialUser; import vook.server.api.web.auth.data.VookLoginUser; diff --git a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java index 26ffa6ed..04712572 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/user/UserWebService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.user.UserService; +import vook.server.api.app.domain.user.UserService; import vook.server.api.model.user.User; import vook.server.api.web.auth.data.VookLoginUser; import vook.server.api.web.routes.user.reqres.UserInfoResponse; diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingRequest.java index 4ce5344c..448867c2 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserOnboardingRequest.java @@ -1,7 +1,7 @@ package vook.server.api.web.routes.user.reqres; import lombok.Data; -import vook.server.api.app.user.data.OnboardingCommand; +import vook.server.api.app.domain.user.data.OnboardingCommand; import vook.server.api.model.user.Funnel; import vook.server.api.model.user.Job; diff --git a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java index 16b27fa8..25913c73 100644 --- a/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java +++ b/api/src/main/java/vook/server/api/web/routes/user/reqres/UserRegisterRequest.java @@ -6,7 +6,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; -import vook.server.api.app.user.data.RegisterCommand; +import vook.server.api.app.domain.user.data.RegisterCommand; @Data public class UserRegisterRequest { diff --git a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java index 400c7710..85267aca 100644 --- a/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java +++ b/api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyWebService.java @@ -3,11 +3,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.user.UserService; -import vook.server.api.app.vocabulary.VocabularyService; -import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; -import vook.server.api.app.vocabulary.data.VocabularyDeleteCommand; -import vook.server.api.app.vocabulary.data.VocabularyUpdateCommand; +import vook.server.api.app.domain.user.UserService; +import vook.server.api.app.domain.vocabulary.VocabularyService; +import vook.server.api.app.domain.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.app.domain.vocabulary.data.VocabularyDeleteCommand; +import vook.server.api.app.domain.vocabulary.data.VocabularyUpdateCommand; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; import vook.server.api.web.auth.data.VookLoginUser; diff --git a/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java b/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java index 3fb7c5fc..b3123c9e 100644 --- a/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java +++ b/api/src/test/java/vook/server/api/testhelper/creator/TestUserCreator.java @@ -3,10 +3,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.user.UserService; -import vook.server.api.app.user.data.OnboardingCommand; -import vook.server.api.app.user.data.RegisterCommand; -import vook.server.api.app.user.data.SignUpFromSocialCommand; +import vook.server.api.app.domain.user.UserService; +import vook.server.api.app.domain.user.data.OnboardingCommand; +import vook.server.api.app.domain.user.data.RegisterCommand; +import vook.server.api.app.domain.user.data.SignUpFromSocialCommand; import vook.server.api.model.user.Funnel; import vook.server.api.model.user.Job; import vook.server.api.model.user.SocialUser; diff --git a/api/src/test/java/vook/server/api/testhelper/creator/TestVocabularyCreator.java b/api/src/test/java/vook/server/api/testhelper/creator/TestVocabularyCreator.java index 028ccb12..22040d26 100644 --- a/api/src/test/java/vook/server/api/testhelper/creator/TestVocabularyCreator.java +++ b/api/src/test/java/vook/server/api/testhelper/creator/TestVocabularyCreator.java @@ -3,8 +3,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.vocabulary.VocabularyService; -import vook.server.api.app.vocabulary.data.VocabularyCreateCommand; +import vook.server.api.app.domain.vocabulary.VocabularyService; +import vook.server.api.app.domain.vocabulary.data.VocabularyCreateCommand; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; diff --git a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java index 9671ee16..4091c044 100644 --- a/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/demo/DemoWebServiceTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.demo.repo.DemoTermRepository; +import vook.server.api.app.domain.demo.repo.DemoTermRepository; import vook.server.api.devhelper.app.TestTermsLoader; import vook.server.api.model.demo.DemoTerm; import vook.server.api.outbound.search.demo.DemoTermSearchService; diff --git a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java index b9ad2108..2043d309 100644 --- a/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/user/UserWebServiceTest.java @@ -4,8 +4,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.user.UserService; -import vook.server.api.app.user.exception.*; +import vook.server.api.app.domain.user.UserService; +import vook.server.api.app.domain.user.exception.*; import vook.server.api.model.user.Funnel; import vook.server.api.model.user.Job; import vook.server.api.model.user.User; diff --git a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java index a66fb9a4..9829182b 100644 --- a/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java +++ b/api/src/test/java/vook/server/api/web/routes/vocabulary/VocabularyWebServiceTest.java @@ -4,9 +4,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; -import vook.server.api.app.vocabulary.exception.VocabularyLimitExceededException; -import vook.server.api.app.vocabulary.exception.VocabularyNotFoundException; -import vook.server.api.app.vocabulary.repo.VocabularyRepository; +import vook.server.api.app.domain.vocabulary.exception.VocabularyLimitExceededException; +import vook.server.api.app.domain.vocabulary.exception.VocabularyNotFoundException; +import vook.server.api.app.domain.vocabulary.repo.VocabularyRepository; import vook.server.api.model.user.User; import vook.server.api.model.vocabulary.Vocabulary; import vook.server.api.testhelper.IntegrationTestBase; From cf222dd9f2e240bb3eff04e768c004da7b255c84 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 14 Jun 2024 11:16:38 +0900 Subject: [PATCH 140/273] =?UTF-8?q?docs:=20db=20schema=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- docs/schema/README.md | 92 +++++++++++++++++++++++++++ docs/schema/demo_term.md | 66 ++++++++++++++++++++ docs/schema/demo_term_synonym.md | 66 ++++++++++++++++++++ docs/schema/schema.json | 1 + docs/schema/social_user.md | 77 +++++++++++++++++++++++ docs/schema/term.md | 82 ++++++++++++++++++++++++ docs/schema/term_synonym.md | 67 ++++++++++++++++++++ docs/schema/user_info.md | 81 ++++++++++++++++++++++++ docs/schema/users.md | 104 +++++++++++++++++++++++++++++++ docs/schema/vocabulary.md | 89 ++++++++++++++++++++++++++ 11 files changed, 728 insertions(+), 1 deletion(-) create mode 100644 docs/schema/README.md create mode 100644 docs/schema/demo_term.md create mode 100644 docs/schema/demo_term_synonym.md create mode 100644 docs/schema/schema.json create mode 100644 docs/schema/social_user.md create mode 100644 docs/schema/term.md create mode 100644 docs/schema/term_synonym.md create mode 100644 docs/schema/user_info.md create mode 100644 docs/schema/users.md create mode 100644 docs/schema/vocabulary.md diff --git a/README.md b/README.md index daf55198..b4248dcc 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# vook-server \ No newline at end of file +# vook-server + +- [DB Schema](docs/schema/README.md) diff --git a/docs/schema/README.md b/docs/schema/README.md new file mode 100644 index 00000000..68a428fe --- /dev/null +++ b/docs/schema/README.md @@ -0,0 +1,92 @@ +# vook + +## Tables + +| Name | Columns | Comment | Type | +| ---- | ------- | ------- | ---- | +| [demo_term](demo_term.md) | 5 | | BASE TABLE | +| [demo_term_synonym](demo_term_synonym.md) | 3 | | BASE TABLE | +| [social_user](social_user.md) | 5 | | BASE TABLE | +| [term](term.md) | 6 | | BASE TABLE | +| [term_synonym](term_synonym.md) | 3 | | BASE TABLE | +| [users](users.md) | 10 | | BASE TABLE | +| [user_info](user_info.md) | 6 | | BASE TABLE | +| [vocabulary](vocabulary.md) | 6 | | BASE TABLE | + +## Relations + +```mermaid +erDiagram + +"demo_term_synonym" }o--|| "demo_term" : "FOREIGN KEY (demo_term_id) REFERENCES demo_term (id)" +"social_user" }o--o| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" +"term" }o--|| "vocabulary" : "FOREIGN KEY (vocabulary_id) REFERENCES vocabulary (id)" +"term_synonym" }o--|| "term" : "FOREIGN KEY (term_id) REFERENCES term (id)" +"user_info" |o--o| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" +"vocabulary" }o--|| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" + +"demo_term" { + bigint_20_ id PK + datetime_6_ created_at + datetime_6_ updated_at + varchar_2000_ meaning + varchar_100_ term +} +"demo_term_synonym" { + bigint_20_ id PK + varchar_100_ synonym + bigint_20_ demo_term_id FK +} +"social_user" { + bigint_20_ id PK + datetime_6_ created_at + varchar_255_ provider + varchar_255_ provider_user_id + bigint_20_ user_id FK +} +"term" { + bigint_20_ id PK + varchar_2000_ meaning + varchar_100_ term + bigint_20_ vocabulary_id FK + datetime_6_ created_at + datetime_6_ updated_at +} +"term_synonym" { + bigint_20_ id PK + varchar_100_ synonym + bigint_20_ term_id FK +} +"users" { + bigint_20_ id PK + datetime_6_ deleted_at + varchar_255_ email + datetime_6_ last_updated_at + bit_1_ onboarding_completed + datetime_6_ onboarding_completed_at + datetime_6_ registered_at + enum__SOCIAL_LOGIN_COMPLETED___REGISTERED___WITHDRAWN__ status + varchar_255_ uid + datetime_6_ withdrawn_at +} +"user_info" { + bigint_20_ id PK + enum__X___FACEBOOK___LINKEDIN___INSTAGRAM___NAVER_BLOG___RECOMMENDATION___OTHER__ funnel + enum__PLANNER___DESIGNER___DEVELOPER___MARKETER___CEO___HR___OTHER__ job + bit_1_ marketing_email_opt_in + varchar_255_ nickname + bigint_20_ user_id FK +} +"vocabulary" { + bigint_20_ id PK + varchar_100_ name + datetime_6_ created_at + datetime_6_ updated_at + varchar_255_ uid + bigint_20_ user_id FK +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/schema/demo_term.md b/docs/schema/demo_term.md new file mode 100644 index 00000000..21e84785 --- /dev/null +++ b/docs/schema/demo_term.md @@ -0,0 +1,66 @@ +# demo_term + +## Description + +
+Table Definition + +```sql +CREATE TABLE `demo_term` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) DEFAULT NULL, + `updated_at` datetime(6) DEFAULT NULL, + `meaning` varchar(2000) NOT NULL, + `term` varchar(100) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Extra Definition | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | ---------------- | -------- | ------- | ------- | +| id | bigint(20) | | false | auto_increment | [demo_term_synonym](demo_term_synonym.md) | | | +| created_at | datetime(6) | NULL | true | | | | | +| updated_at | datetime(6) | NULL | true | | | | | +| meaning | varchar(2000) | | false | | | | | +| term | varchar(100) | | false | | | | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| PRIMARY | PRIMARY KEY | PRIMARY KEY (id) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| PRIMARY | PRIMARY KEY (id) USING BTREE | + +## Relations + +```mermaid +erDiagram + +"demo_term_synonym" }o--|| "demo_term" : "FOREIGN KEY (demo_term_id) REFERENCES demo_term (id)" + +"demo_term" { + bigint_20_ id PK + datetime_6_ created_at + datetime_6_ updated_at + varchar_2000_ meaning + varchar_100_ term +} +"demo_term_synonym" { + bigint_20_ id PK + varchar_100_ synonym + bigint_20_ demo_term_id FK +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/schema/demo_term_synonym.md b/docs/schema/demo_term_synonym.md new file mode 100644 index 00000000..9c273a59 --- /dev/null +++ b/docs/schema/demo_term_synonym.md @@ -0,0 +1,66 @@ +# demo_term_synonym + +## Description + +
+Table Definition + +```sql +CREATE TABLE `demo_term_synonym` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `synonym` varchar(100) NOT NULL, + `demo_term_id` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `FKu0pc6dmrckuupfj3y7n9vxs0` (`demo_term_id`), + CONSTRAINT `FKu0pc6dmrckuupfj3y7n9vxs0` FOREIGN KEY (`demo_term_id`) REFERENCES `demo_term` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Extra Definition | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | ---------------- | -------- | ------- | ------- | +| id | bigint(20) | | false | auto_increment | | | | +| synonym | varchar(100) | | false | | | | | +| demo_term_id | bigint(20) | | false | | | [demo_term](demo_term.md) | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| FKu0pc6dmrckuupfj3y7n9vxs0 | FOREIGN KEY | FOREIGN KEY (demo_term_id) REFERENCES demo_term (id) | +| PRIMARY | PRIMARY KEY | PRIMARY KEY (id) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| FKu0pc6dmrckuupfj3y7n9vxs0 | KEY FKu0pc6dmrckuupfj3y7n9vxs0 (demo_term_id) USING BTREE | +| PRIMARY | PRIMARY KEY (id) USING BTREE | + +## Relations + +```mermaid +erDiagram + +"demo_term_synonym" }o--|| "demo_term" : "FOREIGN KEY (demo_term_id) REFERENCES demo_term (id)" + +"demo_term_synonym" { + bigint_20_ id PK + varchar_100_ synonym + bigint_20_ demo_term_id FK +} +"demo_term" { + bigint_20_ id PK + datetime_6_ created_at + datetime_6_ updated_at + varchar_2000_ meaning + varchar_100_ term +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/schema/schema.json b/docs/schema/schema.json new file mode 100644 index 00000000..ed2bb339 --- /dev/null +++ b/docs/schema/schema.json @@ -0,0 +1 @@ +{"name":"vook","desc":"","tables":[{"name":"demo_term","type":"BASE TABLE","comment":"","columns":[{"name":"id","type":"bigint(20)","nullable":false,"default":null,"comment":"","extra_def":"auto_increment"},{"name":"created_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"updated_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"meaning","type":"varchar(2000)","nullable":false,"default":null,"comment":""},{"name":"term","type":"varchar(100)","nullable":false,"default":null,"comment":""}],"indexes":[{"name":"PRIMARY","def":"PRIMARY KEY (id) USING BTREE","table":"demo_term","columns":["id"],"comment":""}],"constraints":[{"name":"PRIMARY","type":"PRIMARY KEY","def":"PRIMARY KEY (id)","table":"demo_term","referenced_table":null,"columns":["id"],"referenced_columns":null,"comment":""}],"triggers":[],"def":"CREATE TABLE `demo_term` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `created_at` datetime(6) DEFAULT NULL,\n `updated_at` datetime(6) DEFAULT NULL,\n `meaning` varchar(2000) NOT NULL,\n `term` varchar(100) NOT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"},{"name":"demo_term_synonym","type":"BASE TABLE","comment":"","columns":[{"name":"id","type":"bigint(20)","nullable":false,"default":null,"comment":"","extra_def":"auto_increment"},{"name":"synonym","type":"varchar(100)","nullable":false,"default":null,"comment":""},{"name":"demo_term_id","type":"bigint(20)","nullable":false,"default":null,"comment":""}],"indexes":[{"name":"FKu0pc6dmrckuupfj3y7n9vxs0","def":"KEY FKu0pc6dmrckuupfj3y7n9vxs0 (demo_term_id) USING BTREE","table":"demo_term_synonym","columns":["demo_term_id"],"comment":""},{"name":"PRIMARY","def":"PRIMARY KEY (id) USING BTREE","table":"demo_term_synonym","columns":["id"],"comment":""}],"constraints":[{"name":"FKu0pc6dmrckuupfj3y7n9vxs0","type":"FOREIGN KEY","def":"FOREIGN KEY (demo_term_id) REFERENCES demo_term (id)","table":"demo_term_synonym","referenced_table":"demo_term","columns":["demo_term_id"],"referenced_columns":["id"],"comment":""},{"name":"PRIMARY","type":"PRIMARY KEY","def":"PRIMARY KEY (id)","table":"demo_term_synonym","referenced_table":null,"columns":["id"],"referenced_columns":null,"comment":""}],"triggers":[],"def":"CREATE TABLE `demo_term_synonym` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `synonym` varchar(100) NOT NULL,\n `demo_term_id` bigint(20) NOT NULL,\n PRIMARY KEY (`id`),\n KEY `FKu0pc6dmrckuupfj3y7n9vxs0` (`demo_term_id`),\n CONSTRAINT `FKu0pc6dmrckuupfj3y7n9vxs0` FOREIGN KEY (`demo_term_id`) REFERENCES `demo_term` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"},{"name":"social_user","type":"BASE TABLE","comment":"","columns":[{"name":"id","type":"bigint(20)","nullable":false,"default":null,"comment":"","extra_def":"auto_increment"},{"name":"created_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"provider","type":"varchar(255)","nullable":true,"default":"NULL","comment":""},{"name":"provider_user_id","type":"varchar(255)","nullable":true,"default":"NULL","comment":""},{"name":"user_id","type":"bigint(20)","nullable":true,"default":"NULL","comment":""}],"indexes":[{"name":"FKcuirus7h2n9g0fp6rxtrkuiix","def":"KEY FKcuirus7h2n9g0fp6rxtrkuiix (user_id) USING BTREE","table":"social_user","columns":["user_id"],"comment":""},{"name":"PRIMARY","def":"PRIMARY KEY (id) USING BTREE","table":"social_user","columns":["id"],"comment":""}],"constraints":[{"name":"FKcuirus7h2n9g0fp6rxtrkuiix","type":"FOREIGN KEY","def":"FOREIGN KEY (user_id) REFERENCES users (id)","table":"social_user","referenced_table":"users","columns":["user_id"],"referenced_columns":["id"],"comment":""},{"name":"PRIMARY","type":"PRIMARY KEY","def":"PRIMARY KEY (id)","table":"social_user","referenced_table":null,"columns":["id"],"referenced_columns":null,"comment":""}],"triggers":[],"def":"CREATE TABLE `social_user` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `created_at` datetime(6) DEFAULT NULL,\n `provider` varchar(255) DEFAULT NULL,\n `provider_user_id` varchar(255) DEFAULT NULL,\n `user_id` bigint(20) DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `FKcuirus7h2n9g0fp6rxtrkuiix` (`user_id`),\n CONSTRAINT `FKcuirus7h2n9g0fp6rxtrkuiix` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"},{"name":"term","type":"BASE TABLE","comment":"","columns":[{"name":"id","type":"bigint(20)","nullable":false,"default":null,"comment":"","extra_def":"auto_increment"},{"name":"meaning","type":"varchar(2000)","nullable":false,"default":null,"comment":""},{"name":"term","type":"varchar(100)","nullable":false,"default":null,"comment":""},{"name":"vocabulary_id","type":"bigint(20)","nullable":false,"default":null,"comment":""},{"name":"created_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"updated_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""}],"indexes":[{"name":"FKk0u34gq7hfjfe9fnaqk90729c","def":"KEY FKk0u34gq7hfjfe9fnaqk90729c (vocabulary_id) USING BTREE","table":"term","columns":["vocabulary_id"],"comment":""},{"name":"PRIMARY","def":"PRIMARY KEY (id) USING BTREE","table":"term","columns":["id"],"comment":""}],"constraints":[{"name":"FKk0u34gq7hfjfe9fnaqk90729c","type":"FOREIGN KEY","def":"FOREIGN KEY (vocabulary_id) REFERENCES vocabulary (id)","table":"term","referenced_table":"vocabulary","columns":["vocabulary_id"],"referenced_columns":["id"],"comment":""},{"name":"PRIMARY","type":"PRIMARY KEY","def":"PRIMARY KEY (id)","table":"term","referenced_table":null,"columns":["id"],"referenced_columns":null,"comment":""}],"triggers":[],"def":"CREATE TABLE `term` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `meaning` varchar(2000) NOT NULL,\n `term` varchar(100) NOT NULL,\n `vocabulary_id` bigint(20) NOT NULL,\n `created_at` datetime(6) DEFAULT NULL,\n `updated_at` datetime(6) DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `FKk0u34gq7hfjfe9fnaqk90729c` (`vocabulary_id`),\n CONSTRAINT `FKk0u34gq7hfjfe9fnaqk90729c` FOREIGN KEY (`vocabulary_id`) REFERENCES `vocabulary` (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"},{"name":"term_synonym","type":"BASE TABLE","comment":"","columns":[{"name":"id","type":"bigint(20)","nullable":false,"default":null,"comment":"","extra_def":"auto_increment"},{"name":"synonym","type":"varchar(100)","nullable":false,"default":null,"comment":""},{"name":"term_id","type":"bigint(20)","nullable":false,"default":null,"comment":""}],"indexes":[{"name":"FKs40pkmn91x9xglfvf2cmnh2as","def":"KEY FKs40pkmn91x9xglfvf2cmnh2as (term_id) USING BTREE","table":"term_synonym","columns":["term_id"],"comment":""},{"name":"PRIMARY","def":"PRIMARY KEY (id) USING BTREE","table":"term_synonym","columns":["id"],"comment":""}],"constraints":[{"name":"FKs40pkmn91x9xglfvf2cmnh2as","type":"FOREIGN KEY","def":"FOREIGN KEY (term_id) REFERENCES term (id)","table":"term_synonym","referenced_table":"term","columns":["term_id"],"referenced_columns":["id"],"comment":""},{"name":"PRIMARY","type":"PRIMARY KEY","def":"PRIMARY KEY (id)","table":"term_synonym","referenced_table":null,"columns":["id"],"referenced_columns":null,"comment":""}],"triggers":[],"def":"CREATE TABLE `term_synonym` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `synonym` varchar(100) NOT NULL,\n `term_id` bigint(20) NOT NULL,\n PRIMARY KEY (`id`),\n KEY `FKs40pkmn91x9xglfvf2cmnh2as` (`term_id`),\n CONSTRAINT `FKs40pkmn91x9xglfvf2cmnh2as` FOREIGN KEY (`term_id`) REFERENCES `term` (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"},{"name":"users","type":"BASE TABLE","comment":"","columns":[{"name":"id","type":"bigint(20)","nullable":false,"default":null,"comment":"","extra_def":"auto_increment"},{"name":"deleted_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"email","type":"varchar(255)","nullable":true,"default":"NULL","comment":""},{"name":"last_updated_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"onboarding_completed","type":"bit(1)","nullable":false,"default":null,"comment":""},{"name":"onboarding_completed_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"registered_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"status","type":"enum('SOCIAL_LOGIN_COMPLETED','REGISTERED','WITHDRAWN')","nullable":true,"default":"NULL","comment":""},{"name":"uid","type":"varchar(255)","nullable":true,"default":"NULL","comment":""},{"name":"withdrawn_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""}],"indexes":[{"name":"PRIMARY","def":"PRIMARY KEY (id) USING BTREE","table":"users","columns":["id"],"comment":""},{"name":"UK_6dotkott2kjsp8vw4d0m25fb7","def":"UNIQUE KEY UK_6dotkott2kjsp8vw4d0m25fb7 (email) USING BTREE","table":"users","columns":["email"],"comment":""}],"constraints":[{"name":"PRIMARY","type":"PRIMARY KEY","def":"PRIMARY KEY (id)","table":"users","referenced_table":null,"columns":["id"],"referenced_columns":null,"comment":""},{"name":"UK_6dotkott2kjsp8vw4d0m25fb7","type":"UNIQUE","def":"UNIQUE KEY UK_6dotkott2kjsp8vw4d0m25fb7 (email)","table":"users","referenced_table":null,"columns":["email"],"referenced_columns":null,"comment":""}],"triggers":[],"def":"CREATE TABLE `users` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `deleted_at` datetime(6) DEFAULT NULL,\n `email` varchar(255) DEFAULT NULL,\n `last_updated_at` datetime(6) DEFAULT NULL,\n `onboarding_completed` bit(1) NOT NULL,\n `onboarding_completed_at` datetime(6) DEFAULT NULL,\n `registered_at` datetime(6) DEFAULT NULL,\n `status` enum('SOCIAL_LOGIN_COMPLETED','REGISTERED','WITHDRAWN') DEFAULT NULL,\n `uid` varchar(255) DEFAULT NULL,\n `withdrawn_at` datetime(6) DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `UK_6dotkott2kjsp8vw4d0m25fb7` (`email`)\n) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"},{"name":"user_info","type":"BASE TABLE","comment":"","columns":[{"name":"id","type":"bigint(20)","nullable":false,"default":null,"comment":"","extra_def":"auto_increment"},{"name":"funnel","type":"enum('X','FACEBOOK','LINKEDIN','INSTAGRAM','NAVER_BLOG','RECOMMENDATION','OTHER')","nullable":true,"default":"NULL","comment":""},{"name":"job","type":"enum('PLANNER','DESIGNER','DEVELOPER','MARKETER','CEO','HR','OTHER')","nullable":true,"default":"NULL","comment":""},{"name":"marketing_email_opt_in","type":"bit(1)","nullable":true,"default":"NULL","comment":""},{"name":"nickname","type":"varchar(255)","nullable":true,"default":"NULL","comment":""},{"name":"user_id","type":"bigint(20)","nullable":true,"default":"NULL","comment":""}],"indexes":[{"name":"PRIMARY","def":"PRIMARY KEY (id) USING BTREE","table":"user_info","columns":["id"],"comment":""},{"name":"UK_hixwjgx0ynne0cq4tqvoawoda","def":"UNIQUE KEY UK_hixwjgx0ynne0cq4tqvoawoda (user_id) USING BTREE","table":"user_info","columns":["user_id"],"comment":""}],"constraints":[{"name":"FKr1b96ca4asuvrhwoqkdmbo7nj","type":"FOREIGN KEY","def":"FOREIGN KEY (user_id) REFERENCES users (id)","table":"user_info","referenced_table":"users","columns":["user_id"],"referenced_columns":["id"],"comment":""},{"name":"PRIMARY","type":"PRIMARY KEY","def":"PRIMARY KEY (id)","table":"user_info","referenced_table":null,"columns":["id"],"referenced_columns":null,"comment":""},{"name":"UK_hixwjgx0ynne0cq4tqvoawoda","type":"UNIQUE","def":"UNIQUE KEY UK_hixwjgx0ynne0cq4tqvoawoda (user_id)","table":"user_info","referenced_table":null,"columns":["user_id"],"referenced_columns":null,"comment":""}],"triggers":[],"def":"CREATE TABLE `user_info` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `funnel` enum('X','FACEBOOK','LINKEDIN','INSTAGRAM','NAVER_BLOG','RECOMMENDATION','OTHER') DEFAULT NULL,\n `job` enum('PLANNER','DESIGNER','DEVELOPER','MARKETER','CEO','HR','OTHER') DEFAULT NULL,\n `marketing_email_opt_in` bit(1) DEFAULT NULL,\n `nickname` varchar(255) DEFAULT NULL,\n `user_id` bigint(20) DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `UK_hixwjgx0ynne0cq4tqvoawoda` (`user_id`),\n CONSTRAINT `FKr1b96ca4asuvrhwoqkdmbo7nj` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"},{"name":"vocabulary","type":"BASE TABLE","comment":"","columns":[{"name":"id","type":"bigint(20)","nullable":false,"default":null,"comment":"","extra_def":"auto_increment"},{"name":"name","type":"varchar(100)","nullable":false,"default":null,"comment":""},{"name":"created_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"updated_at","type":"datetime(6)","nullable":true,"default":"NULL","comment":""},{"name":"uid","type":"varchar(255)","nullable":true,"default":"NULL","comment":""},{"name":"user_id","type":"bigint(20)","nullable":false,"default":null,"comment":""}],"indexes":[{"name":"FK50fel7tsib4tpnuqa7irn7f5c","def":"KEY FK50fel7tsib4tpnuqa7irn7f5c (user_id) USING BTREE","table":"vocabulary","columns":["user_id"],"comment":""},{"name":"PRIMARY","def":"PRIMARY KEY (id) USING BTREE","table":"vocabulary","columns":["id"],"comment":""}],"constraints":[{"name":"FK50fel7tsib4tpnuqa7irn7f5c","type":"FOREIGN KEY","def":"FOREIGN KEY (user_id) REFERENCES users (id)","table":"vocabulary","referenced_table":"users","columns":["user_id"],"referenced_columns":["id"],"comment":""},{"name":"PRIMARY","type":"PRIMARY KEY","def":"PRIMARY KEY (id)","table":"vocabulary","referenced_table":null,"columns":["id"],"referenced_columns":null,"comment":""}],"triggers":[],"def":"CREATE TABLE `vocabulary` (\n `id` bigint(20) NOT NULL AUTO_INCREMENT,\n `name` varchar(100) NOT NULL,\n `created_at` datetime(6) DEFAULT NULL,\n `updated_at` datetime(6) DEFAULT NULL,\n `uid` varchar(255) DEFAULT NULL,\n `user_id` bigint(20) NOT NULL,\n PRIMARY KEY (`id`),\n KEY `FK50fel7tsib4tpnuqa7irn7f5c` (`user_id`),\n CONSTRAINT `FK50fel7tsib4tpnuqa7irn7f5c` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"}],"relations":[{"table":"demo_term_synonym","columns":["demo_term_id"],"cardinality":"Zero or more","parent_table":"demo_term","parent_columns":["id"],"parent_cardinality":"Exactly one","def":"FOREIGN KEY (demo_term_id) REFERENCES demo_term (id)","virtual":false},{"table":"social_user","columns":["user_id"],"cardinality":"Zero or more","parent_table":"users","parent_columns":["id"],"parent_cardinality":"Zero or one","def":"FOREIGN KEY (user_id) REFERENCES users (id)","virtual":false},{"table":"term","columns":["vocabulary_id"],"cardinality":"Zero or more","parent_table":"vocabulary","parent_columns":["id"],"parent_cardinality":"Exactly one","def":"FOREIGN KEY (vocabulary_id) REFERENCES vocabulary (id)","virtual":false},{"table":"term_synonym","columns":["term_id"],"cardinality":"Zero or more","parent_table":"term","parent_columns":["id"],"parent_cardinality":"Exactly one","def":"FOREIGN KEY (term_id) REFERENCES term (id)","virtual":false},{"table":"user_info","columns":["user_id"],"cardinality":"Zero or one","parent_table":"users","parent_columns":["id"],"parent_cardinality":"Zero or one","def":"FOREIGN KEY (user_id) REFERENCES users (id)","virtual":false},{"table":"vocabulary","columns":["user_id"],"cardinality":"Zero or more","parent_table":"users","parent_columns":["id"],"parent_cardinality":"Exactly one","def":"FOREIGN KEY (user_id) REFERENCES users (id)","virtual":false}],"functions":[],"driver":{"name":"mariadb","database_version":"10.11.7-MariaDB-1:10.11.7+maria~ubu2204","meta":{"dict":{"Functions":"Stored procedures and functions"}}}} diff --git a/docs/schema/social_user.md b/docs/schema/social_user.md new file mode 100644 index 00000000..f9e7077a --- /dev/null +++ b/docs/schema/social_user.md @@ -0,0 +1,77 @@ +# social_user + +## Description + +
+Table Definition + +```sql +CREATE TABLE `social_user` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `created_at` datetime(6) DEFAULT NULL, + `provider` varchar(255) DEFAULT NULL, + `provider_user_id` varchar(255) DEFAULT NULL, + `user_id` bigint(20) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FKcuirus7h2n9g0fp6rxtrkuiix` (`user_id`), + CONSTRAINT `FKcuirus7h2n9g0fp6rxtrkuiix` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Extra Definition | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | ---------------- | -------- | ------- | ------- | +| id | bigint(20) | | false | auto_increment | | | | +| created_at | datetime(6) | NULL | true | | | | | +| provider | varchar(255) | NULL | true | | | | | +| provider_user_id | varchar(255) | NULL | true | | | | | +| user_id | bigint(20) | NULL | true | | | [users](users.md) | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| FKcuirus7h2n9g0fp6rxtrkuiix | FOREIGN KEY | FOREIGN KEY (user_id) REFERENCES users (id) | +| PRIMARY | PRIMARY KEY | PRIMARY KEY (id) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| FKcuirus7h2n9g0fp6rxtrkuiix | KEY FKcuirus7h2n9g0fp6rxtrkuiix (user_id) USING BTREE | +| PRIMARY | PRIMARY KEY (id) USING BTREE | + +## Relations + +```mermaid +erDiagram + +"social_user" }o--o| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" + +"social_user" { + bigint_20_ id PK + datetime_6_ created_at + varchar_255_ provider + varchar_255_ provider_user_id + bigint_20_ user_id FK +} +"users" { + bigint_20_ id PK + datetime_6_ deleted_at + varchar_255_ email + datetime_6_ last_updated_at + bit_1_ onboarding_completed + datetime_6_ onboarding_completed_at + datetime_6_ registered_at + enum__SOCIAL_LOGIN_COMPLETED___REGISTERED___WITHDRAWN__ status + varchar_255_ uid + datetime_6_ withdrawn_at +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/schema/term.md b/docs/schema/term.md new file mode 100644 index 00000000..95305f66 --- /dev/null +++ b/docs/schema/term.md @@ -0,0 +1,82 @@ +# term + +## Description + +
+Table Definition + +```sql +CREATE TABLE `term` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `meaning` varchar(2000) NOT NULL, + `term` varchar(100) NOT NULL, + `vocabulary_id` bigint(20) NOT NULL, + `created_at` datetime(6) DEFAULT NULL, + `updated_at` datetime(6) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FKk0u34gq7hfjfe9fnaqk90729c` (`vocabulary_id`), + CONSTRAINT `FKk0u34gq7hfjfe9fnaqk90729c` FOREIGN KEY (`vocabulary_id`) REFERENCES `vocabulary` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Extra Definition | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | ---------------- | -------- | ------- | ------- | +| id | bigint(20) | | false | auto_increment | [term_synonym](term_synonym.md) | | | +| meaning | varchar(2000) | | false | | | | | +| term | varchar(100) | | false | | | | | +| vocabulary_id | bigint(20) | | false | | | [vocabulary](vocabulary.md) | | +| created_at | datetime(6) | NULL | true | | | | | +| updated_at | datetime(6) | NULL | true | | | | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| FKk0u34gq7hfjfe9fnaqk90729c | FOREIGN KEY | FOREIGN KEY (vocabulary_id) REFERENCES vocabulary (id) | +| PRIMARY | PRIMARY KEY | PRIMARY KEY (id) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| FKk0u34gq7hfjfe9fnaqk90729c | KEY FKk0u34gq7hfjfe9fnaqk90729c (vocabulary_id) USING BTREE | +| PRIMARY | PRIMARY KEY (id) USING BTREE | + +## Relations + +```mermaid +erDiagram + +"term_synonym" }o--|| "term" : "FOREIGN KEY (term_id) REFERENCES term (id)" +"term" }o--|| "vocabulary" : "FOREIGN KEY (vocabulary_id) REFERENCES vocabulary (id)" + +"term" { + bigint_20_ id PK + varchar_2000_ meaning + varchar_100_ term + bigint_20_ vocabulary_id FK + datetime_6_ created_at + datetime_6_ updated_at +} +"term_synonym" { + bigint_20_ id PK + varchar_100_ synonym + bigint_20_ term_id FK +} +"vocabulary" { + bigint_20_ id PK + varchar_100_ name + datetime_6_ created_at + datetime_6_ updated_at + varchar_255_ uid + bigint_20_ user_id FK +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/schema/term_synonym.md b/docs/schema/term_synonym.md new file mode 100644 index 00000000..b4000f85 --- /dev/null +++ b/docs/schema/term_synonym.md @@ -0,0 +1,67 @@ +# term_synonym + +## Description + +
+Table Definition + +```sql +CREATE TABLE `term_synonym` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `synonym` varchar(100) NOT NULL, + `term_id` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `FKs40pkmn91x9xglfvf2cmnh2as` (`term_id`), + CONSTRAINT `FKs40pkmn91x9xglfvf2cmnh2as` FOREIGN KEY (`term_id`) REFERENCES `term` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Extra Definition | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | ---------------- | -------- | ------- | ------- | +| id | bigint(20) | | false | auto_increment | | | | +| synonym | varchar(100) | | false | | | | | +| term_id | bigint(20) | | false | | | [term](term.md) | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| FKs40pkmn91x9xglfvf2cmnh2as | FOREIGN KEY | FOREIGN KEY (term_id) REFERENCES term (id) | +| PRIMARY | PRIMARY KEY | PRIMARY KEY (id) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| FKs40pkmn91x9xglfvf2cmnh2as | KEY FKs40pkmn91x9xglfvf2cmnh2as (term_id) USING BTREE | +| PRIMARY | PRIMARY KEY (id) USING BTREE | + +## Relations + +```mermaid +erDiagram + +"term_synonym" }o--|| "term" : "FOREIGN KEY (term_id) REFERENCES term (id)" + +"term_synonym" { + bigint_20_ id PK + varchar_100_ synonym + bigint_20_ term_id FK +} +"term" { + bigint_20_ id PK + varchar_2000_ meaning + varchar_100_ term + bigint_20_ vocabulary_id FK + datetime_6_ created_at + datetime_6_ updated_at +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/schema/user_info.md b/docs/schema/user_info.md new file mode 100644 index 00000000..adb39fa3 --- /dev/null +++ b/docs/schema/user_info.md @@ -0,0 +1,81 @@ +# user_info + +## Description + +
+Table Definition + +```sql +CREATE TABLE `user_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `funnel` enum('X','FACEBOOK','LINKEDIN','INSTAGRAM','NAVER_BLOG','RECOMMENDATION','OTHER') DEFAULT NULL, + `job` enum('PLANNER','DESIGNER','DEVELOPER','MARKETER','CEO','HR','OTHER') DEFAULT NULL, + `marketing_email_opt_in` bit(1) DEFAULT NULL, + `nickname` varchar(255) DEFAULT NULL, + `user_id` bigint(20) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UK_hixwjgx0ynne0cq4tqvoawoda` (`user_id`), + CONSTRAINT `FKr1b96ca4asuvrhwoqkdmbo7nj` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Extra Definition | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | ---------------- | -------- | ------- | ------- | +| id | bigint(20) | | false | auto_increment | | | | +| funnel | enum('X','FACEBOOK','LINKEDIN','INSTAGRAM','NAVER_BLOG','RECOMMENDATION','OTHER') | NULL | true | | | | | +| job | enum('PLANNER','DESIGNER','DEVELOPER','MARKETER','CEO','HR','OTHER') | NULL | true | | | | | +| marketing_email_opt_in | bit(1) | NULL | true | | | | | +| nickname | varchar(255) | NULL | true | | | | | +| user_id | bigint(20) | NULL | true | | | [users](users.md) | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| FKr1b96ca4asuvrhwoqkdmbo7nj | FOREIGN KEY | FOREIGN KEY (user_id) REFERENCES users (id) | +| PRIMARY | PRIMARY KEY | PRIMARY KEY (id) | +| UK_hixwjgx0ynne0cq4tqvoawoda | UNIQUE | UNIQUE KEY UK_hixwjgx0ynne0cq4tqvoawoda (user_id) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| PRIMARY | PRIMARY KEY (id) USING BTREE | +| UK_hixwjgx0ynne0cq4tqvoawoda | UNIQUE KEY UK_hixwjgx0ynne0cq4tqvoawoda (user_id) USING BTREE | + +## Relations + +```mermaid +erDiagram + +"user_info" |o--o| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" + +"user_info" { + bigint_20_ id PK + enum__X___FACEBOOK___LINKEDIN___INSTAGRAM___NAVER_BLOG___RECOMMENDATION___OTHER__ funnel + enum__PLANNER___DESIGNER___DEVELOPER___MARKETER___CEO___HR___OTHER__ job + bit_1_ marketing_email_opt_in + varchar_255_ nickname + bigint_20_ user_id FK +} +"users" { + bigint_20_ id PK + datetime_6_ deleted_at + varchar_255_ email + datetime_6_ last_updated_at + bit_1_ onboarding_completed + datetime_6_ onboarding_completed_at + datetime_6_ registered_at + enum__SOCIAL_LOGIN_COMPLETED___REGISTERED___WITHDRAWN__ status + varchar_255_ uid + datetime_6_ withdrawn_at +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/schema/users.md b/docs/schema/users.md new file mode 100644 index 00000000..f7ada3ec --- /dev/null +++ b/docs/schema/users.md @@ -0,0 +1,104 @@ +# users + +## Description + +
+Table Definition + +```sql +CREATE TABLE `users` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `deleted_at` datetime(6) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `last_updated_at` datetime(6) DEFAULT NULL, + `onboarding_completed` bit(1) NOT NULL, + `onboarding_completed_at` datetime(6) DEFAULT NULL, + `registered_at` datetime(6) DEFAULT NULL, + `status` enum('SOCIAL_LOGIN_COMPLETED','REGISTERED','WITHDRAWN') DEFAULT NULL, + `uid` varchar(255) DEFAULT NULL, + `withdrawn_at` datetime(6) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UK_6dotkott2kjsp8vw4d0m25fb7` (`email`) +) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Extra Definition | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | ---------------- | -------- | ------- | ------- | +| id | bigint(20) | | false | auto_increment | [social_user](social_user.md) [user_info](user_info.md) [vocabulary](vocabulary.md) | | | +| deleted_at | datetime(6) | NULL | true | | | | | +| email | varchar(255) | NULL | true | | | | | +| last_updated_at | datetime(6) | NULL | true | | | | | +| onboarding_completed | bit(1) | | false | | | | | +| onboarding_completed_at | datetime(6) | NULL | true | | | | | +| registered_at | datetime(6) | NULL | true | | | | | +| status | enum('SOCIAL_LOGIN_COMPLETED','REGISTERED','WITHDRAWN') | NULL | true | | | | | +| uid | varchar(255) | NULL | true | | | | | +| withdrawn_at | datetime(6) | NULL | true | | | | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| PRIMARY | PRIMARY KEY | PRIMARY KEY (id) | +| UK_6dotkott2kjsp8vw4d0m25fb7 | UNIQUE | UNIQUE KEY UK_6dotkott2kjsp8vw4d0m25fb7 (email) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| PRIMARY | PRIMARY KEY (id) USING BTREE | +| UK_6dotkott2kjsp8vw4d0m25fb7 | UNIQUE KEY UK_6dotkott2kjsp8vw4d0m25fb7 (email) USING BTREE | + +## Relations + +```mermaid +erDiagram + +"social_user" }o--o| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" +"user_info" |o--o| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" +"vocabulary" }o--|| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" + +"users" { + bigint_20_ id PK + datetime_6_ deleted_at + varchar_255_ email + datetime_6_ last_updated_at + bit_1_ onboarding_completed + datetime_6_ onboarding_completed_at + datetime_6_ registered_at + enum__SOCIAL_LOGIN_COMPLETED___REGISTERED___WITHDRAWN__ status + varchar_255_ uid + datetime_6_ withdrawn_at +} +"social_user" { + bigint_20_ id PK + datetime_6_ created_at + varchar_255_ provider + varchar_255_ provider_user_id + bigint_20_ user_id FK +} +"user_info" { + bigint_20_ id PK + enum__X___FACEBOOK___LINKEDIN___INSTAGRAM___NAVER_BLOG___RECOMMENDATION___OTHER__ funnel + enum__PLANNER___DESIGNER___DEVELOPER___MARKETER___CEO___HR___OTHER__ job + bit_1_ marketing_email_opt_in + varchar_255_ nickname + bigint_20_ user_id FK +} +"vocabulary" { + bigint_20_ id PK + varchar_100_ name + datetime_6_ created_at + datetime_6_ updated_at + varchar_255_ uid + bigint_20_ user_id FK +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/schema/vocabulary.md b/docs/schema/vocabulary.md new file mode 100644 index 00000000..24c549fa --- /dev/null +++ b/docs/schema/vocabulary.md @@ -0,0 +1,89 @@ +# vocabulary + +## Description + +
+Table Definition + +```sql +CREATE TABLE `vocabulary` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `created_at` datetime(6) DEFAULT NULL, + `updated_at` datetime(6) DEFAULT NULL, + `uid` varchar(255) DEFAULT NULL, + `user_id` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `FK50fel7tsib4tpnuqa7irn7f5c` (`user_id`), + CONSTRAINT `FK50fel7tsib4tpnuqa7irn7f5c` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=[Redacted by tbls] DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Extra Definition | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | ---------------- | -------- | ------- | ------- | +| id | bigint(20) | | false | auto_increment | [term](term.md) | | | +| name | varchar(100) | | false | | | | | +| created_at | datetime(6) | NULL | true | | | | | +| updated_at | datetime(6) | NULL | true | | | | | +| uid | varchar(255) | NULL | true | | | | | +| user_id | bigint(20) | | false | | | [users](users.md) | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| FK50fel7tsib4tpnuqa7irn7f5c | FOREIGN KEY | FOREIGN KEY (user_id) REFERENCES users (id) | +| PRIMARY | PRIMARY KEY | PRIMARY KEY (id) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| FK50fel7tsib4tpnuqa7irn7f5c | KEY FK50fel7tsib4tpnuqa7irn7f5c (user_id) USING BTREE | +| PRIMARY | PRIMARY KEY (id) USING BTREE | + +## Relations + +```mermaid +erDiagram + +"term" }o--|| "vocabulary" : "FOREIGN KEY (vocabulary_id) REFERENCES vocabulary (id)" +"vocabulary" }o--|| "users" : "FOREIGN KEY (user_id) REFERENCES users (id)" + +"vocabulary" { + bigint_20_ id PK + varchar_100_ name + datetime_6_ created_at + datetime_6_ updated_at + varchar_255_ uid + bigint_20_ user_id FK +} +"term" { + bigint_20_ id PK + varchar_2000_ meaning + varchar_100_ term + bigint_20_ vocabulary_id FK + datetime_6_ created_at + datetime_6_ updated_at +} +"users" { + bigint_20_ id PK + datetime_6_ deleted_at + varchar_255_ email + datetime_6_ last_updated_at + bit_1_ onboarding_completed + datetime_6_ onboarding_completed_at + datetime_6_ registered_at + enum__SOCIAL_LOGIN_COMPLETED___REGISTERED___WITHDRAWN__ status + varchar_255_ uid + datetime_6_ withdrawn_at +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) From df0a53c138f5ab0bf9265eec589b30ae04f69af8 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 19 Jun 2024 20:21:32 +0900 Subject: [PATCH 141/273] chore: dagger version up #87 --- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/deploy-stag.yml | 2 +- cicd/dagger.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index b1cc03aa..6e49c2ad 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -21,7 +21,7 @@ jobs: SSH_DEST: ${{ secrets.DEV_SSH_DEST }} SSH_KEY: ${{ secrets.DEV_SSH_KEY }} with: - version: 0.11.7 + version: 0.11.8 verb: call module: ./cicd args: >- diff --git a/.github/workflows/deploy-stag.yml b/.github/workflows/deploy-stag.yml index c87a200a..c0414d18 100644 --- a/.github/workflows/deploy-stag.yml +++ b/.github/workflows/deploy-stag.yml @@ -21,7 +21,7 @@ jobs: SSH_DEST: ${{ secrets.STAG_SSH_DEST }} SSH_KEY: ${{ secrets.STAG_SSH_KEY }} with: - version: 0.11.7 + version: 0.11.8 verb: call module: ./cicd args: >- diff --git a/cicd/dagger.json b/cicd/dagger.json index d5b9b7ab..294df112 100644 --- a/cicd/dagger.json +++ b/cicd/dagger.json @@ -24,5 +24,5 @@ } ], "source": "dagger", - "engineVersion": "v0.11.7" + "engineVersion": "v0.11.8" } From 9542b2884de2de03c7bef5a4eca4cc853cb14436 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 19 Jun 2024 20:23:42 +0900 Subject: [PATCH 142/273] chore: spring boot version up #87 3.2.5 -> 3.2.6 --- api/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 66ed837b..b1007806 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.2.5' - id 'io.spring.dependency-management' version '1.1.4' + id 'org.springframework.boot' version '3.2.6' + id 'io.spring.dependency-management' version '1.1.5' } group = 'vook.server' From 868618484da01759226d95dee0d18fdff053c574 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 19 Jun 2024 20:27:29 +0900 Subject: [PATCH 143/273] =?UTF-8?q?chore:=20mariadb=20=EC=84=B8=EB=B6=80?= =?UTF-8?q?=20version=20=EA=B3=A0=EC=A0=95=20#87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10.11 -> 10.11.8 --- .../java/vook/server/api/testhelper/IntegrationTestBase.java | 2 +- devenv/mariadb.Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java index f5162dc2..aa543452 100644 --- a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java +++ b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java @@ -19,7 +19,7 @@ public abstract class IntegrationTestBase { @ServiceConnection - protected static final MariaDBContainer mariaDBContainer = new MariaDBContainer<>("mariadb:10.11") + protected static final MariaDBContainer mariaDBContainer = new MariaDBContainer<>("mariadb:10.11.8") .withDatabaseName("example") .withUsername("user") .withPassword("userPw") diff --git a/devenv/mariadb.Dockerfile b/devenv/mariadb.Dockerfile index 2b6b74ef..0c53909c 100644 --- a/devenv/mariadb.Dockerfile +++ b/devenv/mariadb.Dockerfile @@ -1,4 +1,4 @@ -FROM mariadb:10.11 +FROM mariadb:10.11.8 # windows에서 volume mount 할 경우, 파일 권한이 777로 변경되는 문제가 있어서 아래와 같은 작업을 추가 함 COPY db/conf.d/my.cnf /etc/mysql/conf.d/my.cnf From 172c88dac2e0a4ca36db29082445da8d8bae05b8 Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Wed, 19 Jun 2024 20:28:31 +0900 Subject: [PATCH 144/273] chore: meilisearch version up #87 v1.8.0 -> v1.8.3 --- .../java/vook/server/api/testhelper/IntegrationTestBase.java | 2 +- devenv/compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java index aa543452..eb17f7fe 100644 --- a/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java +++ b/api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java @@ -26,7 +26,7 @@ public abstract class IntegrationTestBase { .withConfigurationOverride("db/conf") .withTmpFs(Map.of("/var/lib/mysql", "rw")); - protected static final MeilisearchContainer meilisearchContainer = new MeilisearchContainer("getmeili/meilisearch:v1.8.0"); + protected static final MeilisearchContainer meilisearchContainer = new MeilisearchContainer("getmeili/meilisearch:v1.8.3"); static { mariaDBContainer.start(); diff --git a/devenv/compose.yml b/devenv/compose.yml index 0f2ca078..28232d29 100644 --- a/devenv/compose.yml +++ b/devenv/compose.yml @@ -18,7 +18,7 @@ services: mem_limit: 300m restart: unless-stopped meilisearch: - image: getmeili/meilisearch:v1.8.0 + image: getmeili/meilisearch:v1.8.3 volumes: - meili_data:/meili_data ports: From c8577691ed9fe5abe2e2a33c4e22ff74c848b62c Mon Sep 17 00:00:00 2001 From: seungyeop-lee Date: Fri, 21 Jun 2024 04:45:41 +0900 Subject: [PATCH 145/273] =?UTF-8?q?docs:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=86=8C=EA=B0=9C=20=EB=AC=B8=EC=84=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20#89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/introduction/240622/README.md | 208 +++++++++++++++ docs/introduction/240622/README.pdf | Bin 0 -> 2932717 bytes docs/introduction/240622/assets/dagger.png | Bin 0 -> 1049653 bytes docs/introduction/240622/assets/dep.png | Bin 0 -> 540911 bytes .../240622/assets/drag-and-drop.png | Bin 0 -> 151335 bytes docs/introduction/240622/assets/layout.png | Bin 0 -> 418061 bytes docs/introduction/240622/assets/logo.svg | 8 + docs/introduction/240622/diagram.drawio | 241 ++++++++++++++++++ 9 files changed, 458 insertions(+) create mode 100644 docs/introduction/240622/README.md create mode 100644 docs/introduction/240622/README.pdf create mode 100644 docs/introduction/240622/assets/dagger.png create mode 100644 docs/introduction/240622/assets/dep.png create mode 100644 docs/introduction/240622/assets/drag-and-drop.png create mode 100644 docs/introduction/240622/assets/layout.png create mode 100644 docs/introduction/240622/assets/logo.svg create mode 100644 docs/introduction/240622/diagram.drawio diff --git a/README.md b/README.md index b4248dcc..140b485d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # vook-server +- [Introduction](docs/introduction/240622/README.md) - [DB Schema](docs/schema/README.md) diff --git a/docs/introduction/240622/README.md b/docs/introduction/240622/README.md new file mode 100644 index 00000000..6211646c --- /dev/null +++ b/docs/introduction/240622/README.md @@ -0,0 +1,208 @@ +--- +theme: uncover +class: + - lead +marp: true +style: | + section { + font-size: 25px; + } + section strong { + background-color: yellow; + padding: 2px; + } + section h2 { + margin: 15px 0 15px; + } +--- + +![bg center:50% 50%](./assets/logo.svg) + +--- + +# 용어의 의미를 가장 쉽고 빠르게 찾는 방법 + +--- + +### Target + +용어집을 만들고 사용하길 원하는 +개인을 위한 Utility 서비스 + +### Main Problem + +원하는 결과를 찾기 어려움 + +### How to Solve + +**드래그앤드롭**으로 쉽게 찾기 + +![bg right:58% 90%](./assets/drag-and-drop.png) + +--- + +# 프로젝트 구조 - 레이아웃 + +![w:850](./assets/layout.png) + +--- + +# 프로젝트 구조 - root (tree) + +``` +. +├── api # Spring API 서버 +├── cicd # dagger 모듈 +├── devenv # 개발에 필요한 외부 의존성 (docker) +└── docs # 관련 문서 +``` + +--- + +# 프로젝트 구조 - api main package (tree) + +``` +. +└── vook + └── server + └── api + ├── app # 앱 파트 + │   ├── common # 앱 파트 공통 + │   ├── config # 앱 파트 설정 (@Configuration) + │   └── domain # 도메인 별 비즈니스 로직 + ├── devhelper # 개발 편의성에 사용되는 로직 + ├── helper # 헬퍼 함수 + ├── model # 엔티티 모델 + ├── outbound # 외부 연동 어뎁터 + └── web # 웹 파트 + ├── auth # 인증, 인가 + ├── common # 웹 파트 공통 + ├── config # 웹 파트 설정 (@Configuration) + ├── routes # 웹 라우터 (@RestController) + └── swagger # Swagger 공통 설정 +``` + +--- + +# 프로젝트 구조 - api main package (layout) + +![w:850](./assets/dep.png) + +--- + +## 도입 기술 - Swagger Schema 재사용 + +```java +// api/src/main/java/vook/server/api/web/swagger/GlobalOpenApiCustomizerImpl.java +public class GlobalOpenApiCustomizerImpl implements GlobalOpenApiCustomizer { + @Override + public void customise(OpenAPI openApi) { + openApi.getComponents() + .addSchemas(getKey(ComponentRefConsts.Schema.COMMON_API_RESPONSE), new Schema>() + .addProperty("code", new StringSchema().description("결과 코드")).addRequiredItem("code") + .addProperty("result", new Schema<>()) + ) + ... + } +} +``` + +```java +// api/src/main/java/vook/server/api/web/routes/vocabulary/VocabularyApi.java +public interface VocabularyApi { + ... + @ApiResponses(value = { + @ApiResponse( + responseCode = "400", + content = @Content( + mediaType = "application/json", + schema = @Schema(ref = ComponentRefConsts.Schema.COMMON_API_RESPONSE), + ... + ) + ), + }) + CommonApiResponse createVocabulary(VookLoginUser user, VocabularyCreateRequest request); +} +``` + +--- + +## 도입 기술 - Swagger Annotation 분리 + +```java +// api/src/main/java/vook/server/api/web/routes/health/HealthApi.java +@Tag(name = "health") +public interface HealthApi { + + @Operation( + summary = "서버 상태 확인", + description = """ + - 서버의 상태를 체크하는 API입니다.""") + @ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "text/plain", + examples = @ExampleObject(name = "성공", value = "OK") + ) + ) + String health(); +} +``` + +```java +// api/src/main/java/vook/server/api/web/routes/health/HealthController.java +@RestController +@RequestMapping("/health") +public class HealthController implements HealthApi { + + @GetMapping + public String health() { + return "OK"; + } +} +``` + +--- + +## 도입 기술 - Testcontainer + +```java +// api/src/test/java/vook/server/api/testhelper/IntegrationTestBase.java +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public abstract class IntegrationTestBase { + + @ServiceConnection + protected static final MariaDBContainer mariaDBContainer = new MariaDBContainer<>("mariadb:10.11.8") + .withDatabaseName("example") + .withUsername("user") + .withPassword("userPw") + .withConfigurationOverride("db/conf") + .withTmpFs(Map.of("/var/lib/mysql", "rw")); + + protected static final MeilisearchContainer meilisearchContainer = new MeilisearchContainer("getmeili/meilisearch:v1.8.3"); + + static { + mariaDBContainer.start(); + meilisearchContainer.start(); + } + + ... + + @Configuration + public static class TestConfig { + + @Bean + public MeilisearchProperties meilisearchProperties() { + MeilisearchProperties meilisearchProperties = new MeilisearchProperties(); + meilisearchProperties.setHost(meilisearchContainer.getHostUrl()); + meilisearchProperties.setApiKey(meilisearchContainer.getMasterKey()); + return meilisearchProperties; + } + } +} +``` +--- + +## 도입 기술 - Dagger + +![w:750](./assets/dagger.png) diff --git a/docs/introduction/240622/README.pdf b/docs/introduction/240622/README.pdf new file mode 100644 index 0000000000000000000000000000000000000000..25bc94ecb21895af4b5906304bf404b118703d40 GIT binary patch literal 2932717 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 jjGEP5%;LEmY&fi@skF7&29hQ%8zNM_huJ}G> zYSM1o7Bqr0(p=1Oj1ArEUycMhTyZ^HUf{spZ6#l7MA0$kjqQKlspyG(j4Bv(3CRu? zMf1;oaJ#TqMO$1io+ouYl>LzcnF?>iSpV_VyM3>R@8!oNgqq=GqE&mmHAUa3O=Hj}EQ+`Bq{sn{{VlZ29b9)nC6I6h4O_oA^=RDFo%cwVc79nETu+p9hXAZO3gXf{Bxsul@0Ny;es< zhvYxMMuCJ`ji$3R^Tiu=GAzB|{Lofe1X*L<;YO(pz%N~#?0!~i?W3I&cDTw-Uxb9` zysH9yB%Ody!nLdVDuC`V&&d%{Jn$X~5=YK${d9k1|Y%kE&*) zWhRgUV6f8KufNEqsiGG~eg)Iy*|^})h?r)1S_<86(LY<(p^IdeImvL~Vr2(itOi-QaH3wYUVS78n^{)jG zvpXmCGhA@7_S1Fs?npSn+65;Om0k z@-($0mOKSG6rFq85qp0btqMPpO;e7nRKJBIH2EweMJa;C)A3p7g>Gmu;LFKr#Ji6C zIR!32yFT7@e;yJJI&CRCFA5sP%M&z5V}+b>9h3}&vL*{gIg0>vmPdKO4qP%US54kmtzZI?hgr1a3tM%{mR5i?1!D$D8B@6W#Kb2l@pIV z{nx+sm}Y1BhNP*O*;f3y+bBt?JvURRS!lvPbadoBT6;=L_q6$waV^JjV}eB#VR zdPy@mPwVQKw%GLuvgxfAW;u8%GqFst)=F5L1BGIlyGtPe{X~gCft0t%7pVM7Hz1=Lu1neYj&)8sB zlSXzW+AmXd4B6yUU_9`Nbo;&woNKB@n`Dg{XJS~Dv9;|&!4D

as4iUFdG`$b3r1 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
Tu6Wq)mqoQrFD}@c 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