diff --git a/.gitignore b/.gitignore index 9da7b18..e4107c6 100644 --- a/.gitignore +++ b/.gitignore @@ -256,5 +256,11 @@ out/ **/src/main/resources/application-s3-local.yml **/src/main/resources/application-s3-prod.yml +**/src/main/resources/application-nice-local.yml +**/src/main/resources/application-nice-prod.yml + +**/src/main/resources/application-auth-local.yml +**/src/main/resources/application-auth-prod.yml + # 깃허브 서브모듈 관련 폴더 ignore backend-submodule/ \ No newline at end of file diff --git a/api/api-auth/src/main/java/com/drinkhere/apiauth/ApiAuthApplication.java b/api/api-auth/src/main/java/com/drinkhere/apiauth/ApiAuthApplication.java index 09bb947..8ac51b0 100644 --- a/api/api-auth/src/main/java/com/drinkhere/apiauth/ApiAuthApplication.java +++ b/api/api-auth/src/main/java/com/drinkhere/apiauth/ApiAuthApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication +@ComponentScan(basePackages = "com.drinkhere") public class ApiAuthApplication { public static void main(String[] args) { diff --git a/backend-submodule b/backend-submodule index 22542c3..fc0315d 160000 --- a/backend-submodule +++ b/backend-submodule @@ -1 +1 @@ -Subproject commit 22542c3a451b8a4e5e61d519b1bf0b0ffed83240 +Subproject commit fc0315d2e2cd6e47fb0d5580b3c3894c86f027ea diff --git a/build.gradle b/build.gradle index 33c6c64..039603c 100644 --- a/build.gradle +++ b/build.gradle @@ -79,4 +79,16 @@ task copyPrivate(type: Copy) { include "application-s3-prod.yml" into 'infra/infra-s3/src/main/resources' } + + copy { + from './backend-submodule' + include "application-nice-prod.yml" + into 'client/client-nice/src/main/resources' + } + + copy { + from './backend-submodule' + include "application-auth-prod.yml" + into 'api/api-auth/src/main/resources' + } } \ No newline at end of file diff --git a/client/client-nice/.gitattributes b/client/client-nice/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/client/client-nice/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/client/client-nice/.gitignore b/client/client-nice/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/client/client-nice/.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/client/client-nice/build.gradle b/client/client-nice/build.gradle new file mode 100644 index 0000000..ebdc330 --- /dev/null +++ b/client/client-nice/build.gradle @@ -0,0 +1,15 @@ +bootJar { + enabled = true +} +jar { + enabled = false +} + +dependencies { + implementation project(':domain:domain-rds') + implementation project(':infra:infra-redis') + implementation project(':api:api-auth') + implementation 'org.springframework.boot:spring-boot-configuration-processor' + // WebClient + implementation 'org.springframework.boot:spring-boot-starter-webflux' +} diff --git a/client/client-nice/gradle/wrapper/gradle-wrapper.jar b/client/client-nice/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/client/client-nice/gradle/wrapper/gradle-wrapper.jar differ diff --git a/client/client-nice/gradle/wrapper/gradle-wrapper.properties b/client/client-nice/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e2847c8 --- /dev/null +++ b/client/client-nice/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/client/client-nice/gradlew b/client/client-nice/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/client/client-nice/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/client/client-nice/gradlew.bat b/client/client-nice/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/client/client-nice/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/client/client-nice/settings.gradle b/client/client-nice/settings.gradle new file mode 100644 index 0000000..d65b29d --- /dev/null +++ b/client/client-nice/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'client-nice' diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/ClientNiceApplication.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/ClientNiceApplication.java new file mode 100644 index 0000000..69209ee --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/ClientNiceApplication.java @@ -0,0 +1,19 @@ +package com.drinkhere.clientnice; + +import com.drinkhere.clientnice.webclient.config.NiceProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ConfigurationPropertiesScan +@ComponentScan(basePackages = "com.drinkhere") +public class ClientNiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ClientNiceApplication.class, args); + } + +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceCryptoData.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceCryptoData.java new file mode 100644 index 0000000..f4bf63a --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceCryptoData.java @@ -0,0 +1,12 @@ +package com.drinkhere.clientnice.dto; + +public record NiceCryptoData( + String key, + String iv, + String hmacKey +) { + // 팩토리 메서드 + public static NiceCryptoData of(String key, String iv, String hmacKey) { + return new NiceCryptoData(key, iv, hmacKey); + } +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceDecryptedData.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceDecryptedData.java new file mode 100644 index 0000000..f2f2415 --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceDecryptedData.java @@ -0,0 +1,59 @@ +package com.drinkhere.clientnice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record NiceDecryptedData( + + @JsonProperty("resultcode") + String resultCode, + + @JsonProperty("requestno") + String requestNo, + + @JsonProperty("enctime") + String encTime, + + @JsonProperty("sitecode") + String siteCode, + + @JsonProperty("responseno") + String responseNo, + + @JsonProperty("authtype") + String authType, + + @JsonProperty("name") + String name, + + @JsonProperty("utf8_name") + String utf8Name, + + @JsonProperty("birthdate") + String birthDate, + + @JsonProperty("gender") + String gender, + + @JsonProperty("nationalinfo") + String nationalInfo, + + @JsonProperty("mobileco") + String mobileCo, + + @JsonProperty("mobileno") + String mobileNo, + + @JsonProperty("ci") + String ci, + + @JsonProperty("di") + String di, + + @JsonProperty("businessno") + String businessNo, + + @JsonProperty("receivedata") + String receiveData + +) { +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceRequestData.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceRequestData.java new file mode 100644 index 0000000..77b0bb5 --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/NiceRequestData.java @@ -0,0 +1,11 @@ +package com.drinkhere.clientnice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record NiceRequestData( + @JsonProperty("requestno") String requestNo, + @JsonProperty("returnurl") String returnUrl, + @JsonProperty("sitecode") String siteCode, + @JsonProperty("popupyn") String popupYn +) { +} \ No newline at end of file diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/request/GetCryptoTokenRequest.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/request/GetCryptoTokenRequest.java new file mode 100644 index 0000000..0486169 --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/request/GetCryptoTokenRequest.java @@ -0,0 +1,18 @@ +package com.drinkhere.clientnice.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GetCryptoTokenRequest( + @JsonProperty("dataHeader") DataHeader dataHeader, + @JsonProperty("dataBody") DataBody dataBody +) { + public static record DataHeader( + @JsonProperty("CNTY_CD") String countryCd + ) {} + + public static record DataBody( + @JsonProperty("req_dtim") String requestDateTime, + @JsonProperty("req_no") String requestNo, + @JsonProperty("enc_mode") String encryptionMode + ) {} +} \ No newline at end of file diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/response/CreateNiceApiRequestDataDto.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/response/CreateNiceApiRequestDataDto.java new file mode 100644 index 0000000..5ae356b --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/response/CreateNiceApiRequestDataDto.java @@ -0,0 +1,10 @@ +package com.drinkhere.clientnice.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record CreateNiceApiRequestDataDto( + @JsonProperty("token_version_id") String tokenVersionId, + @JsonProperty("enc_data") String encData, + @JsonProperty("integrity_value") String integrityValue +) { +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/response/GetCryptoTokenResponse.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/response/GetCryptoTokenResponse.java new file mode 100644 index 0000000..f3fc8e7 --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/dto/response/GetCryptoTokenResponse.java @@ -0,0 +1,25 @@ +package com.drinkhere.clientnice.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GetCryptoTokenResponse( + @JsonProperty("dataHeader") DataHeader dataHeader, + @JsonProperty("dataBody") DataBody dataBody +) { + public record DataHeader( + @JsonProperty("GW_RSLT_CD") String gwRsltCd, + @JsonProperty("GW_RSLT_MSG") String gwRsltMsg + ) { + } + + public record DataBody( + @JsonProperty("rsp_cd") String rspCd, + @JsonProperty("res_msg") String resMsg, + @JsonProperty("result_cd") String resultCd, + @JsonProperty("site_code") String siteCode, + @JsonProperty("token_version_id") String tokenVersionId, + @JsonProperty("token_val") String tokenVal, + @JsonProperty("period") Integer period + ) { + } +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/presentation/NiceController.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/presentation/NiceController.java new file mode 100644 index 0000000..70237d8 --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/presentation/NiceController.java @@ -0,0 +1,38 @@ +package com.drinkhere.clientnice.presentation; + +import com.drinkhere.clientnice.dto.response.CreateNiceApiRequestDataDto; +import com.drinkhere.clientnice.service.InitializeNiceUseCase; +import com.drinkhere.clientnice.service.NiceCallBackUseCase; +import com.drinkhere.common.response.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.drinkhere.clientnice.response.NiceSuccessStatus.INIT_NICE_API_SUCCESS; +import static com.drinkhere.clientnice.response.NiceSuccessStatus.PROCESS_CALLBACK_SUCCESS; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/nice") +public class NiceController { + private final InitializeNiceUseCase initializeNiceUseCase; + private final NiceCallBackUseCase niceCallBackUseCase; + @GetMapping("/{mid}") + public ResponseEntity> initNiceApi( + @PathVariable("mid") Long memberId + ){ + return ApiResponse.success(INIT_NICE_API_SUCCESS, initializeNiceUseCase.initializeNiceApi(memberId)); + } + + @GetMapping("/call-back") + public ResponseEntity> handleNiceCallBack( + @RequestParam("mid") Long memberId, + @RequestParam("token_version_id") String tokenVersionId, + @RequestParam("enc_data") String encData, + @RequestParam("integrity_value") String integrityValue + ) { + niceCallBackUseCase.processCallback(memberId, encData); + return ApiResponse.success(PROCESS_CALLBACK_SUCCESS); + } + +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/response/NiceSuccessStatus.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/response/NiceSuccessStatus.java new file mode 100644 index 0000000..c4e9ebe --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/response/NiceSuccessStatus.java @@ -0,0 +1,22 @@ +package com.drinkhere.clientnice.response; + +import com.drinkhere.common.response.BaseSuccessStatus; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum NiceSuccessStatus implements BaseSuccessStatus { + INIT_NICE_API_SUCCESS(HttpStatus.CREATED, "NICE API 표준창 요청 위한 초기화 성공"), + PROCESS_CALLBACK_SUCCESS(HttpStatus.OK, "NICE API 인증 결과 콜백 처리 성공") + ; + + private final HttpStatus httpStatus; + private final String message; + + @Override + public int getStatusCode() { + return this.httpStatus.value(); + } +} \ No newline at end of file diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/service/InitializeNiceUseCase.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/service/InitializeNiceUseCase.java new file mode 100644 index 0000000..428af68 --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/service/InitializeNiceUseCase.java @@ -0,0 +1,206 @@ +package com.drinkhere.clientnice.service; + +import com.drinkhere.clientnice.dto.NiceCryptoData; +import com.drinkhere.clientnice.dto.NiceRequestData; +import com.drinkhere.clientnice.dto.response.CreateNiceApiRequestDataDto; +import com.drinkhere.clientnice.dto.response.GetCryptoTokenResponse; +import com.drinkhere.clientnice.webclient.NiceClient; +import com.drinkhere.clientnice.webclient.config.NiceProperties; +import com.drinkhere.common.exception.clientnice.NiceException; +import com.drinkhere.infraredis.util.RedisUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.UUID; + +import static com.drinkhere.common.exception.clientnice.NiceErrorCode.*; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + +@Service +@RequiredArgsConstructor +public class InitializeNiceUseCase { + private static final String REDIS_CRYPTO_KEY = "cryptoData"; + private static final String REDIS_CRYPTO_TOKEN_KEY = "cryptoToken"; + private static final Long REDIS_CRYPTO_TOKEN_EXPIRATION = 3300L; + private static final Long REDIS_REQUEST_NO_EXPIRATION = 30L; + private static final String HASH_ALGORITHM_SHA256 = "SHA-256"; // 단방향 해시 알고리즘 + private static final String SYMMETRIC_ENCRYPTION_ALGORITHM_AES = "AES"; // 대칭키 암호화 알고리즘 + private static final String MAC_ALGORITHM_HMAC_SHA256 = "HmacSHA256"; + private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; + private static final String RETURN_URL = "/api/v1/nice/call-back?mid="; + private static final String REDIS_REQUEST_NO_KEY_TEMPLATE = "memberId:%d:requestNo"; + + private final NiceClient niceClient; + private final RedisUtil redisUtil; + private final ObjectMapper objectMapper; + private final NiceProperties niceProperties; + public CreateNiceApiRequestDataDto initializeNiceApi(Long memberId) { + String key = null; + String iv = null; + String hmacKey = null; + GetCryptoTokenResponse cryptoToken; + + String storedCryptoToken = (String) redisUtil.get(REDIS_CRYPTO_TOKEN_KEY); + if (storedCryptoToken == null) { // 암호화 토큰 만료로 새로 발급 + String reqDtim = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String reqNo = UUID.randomUUID().toString().substring(0, 30); + + // 암호화 토큰 호출 + cryptoToken = niceClient.requestCryptoToken(reqDtim, reqNo); + + String resultVal = generateResultVal(reqDtim, reqNo, cryptoToken.dataBody().tokenVal()); + // 대칭키 생성 + key = resultVal.substring(0, 16); + iv = resultVal.substring(resultVal.length() - 16); + + // 무결성키 생성 + hmacKey = resultVal.substring(0, 32); + + // 새로 생성한 암호화 토큰, 대칭키, 무결성키 저장 + saveCryptoTokenToRedis(cryptoToken); + saveCryptoDataToRedis(key, iv, hmacKey); + } else { // 기존 암호화 토큰, 대칭키, 무결성키 재사용 + cryptoToken = deserialization(storedCryptoToken); // 역직렬화 + NiceCryptoData niceCryptoData = getCryptoDataFromRedis(); + key = niceCryptoData.key(); + iv = niceCryptoData.iv(); + hmacKey = niceCryptoData.hmacKey(); + } + + // 요청 데이터 생성 + String reqData = createReqDataJson(cryptoToken.dataBody().siteCode(), memberId); + + // 요청 데이터 암호화 + String encData = encryptReqData(key, iv, reqData); + + // Hmac 무결성 체크값 생성 + String integrityValue = generateIntegrityValue(hmacKey.getBytes(), encData.getBytes()); + + return new CreateNiceApiRequestDataDto( + cryptoToken.dataBody().tokenVersionId(), + encData, + integrityValue + ); + } + + /**----------------------------------------------------------------------------------------------------**/ + // 암호화 토큰 조회 + private GetCryptoTokenResponse deserialization(String serializaedCryptoToken) { + try { + return objectMapper.readValue(serializaedCryptoToken, GetCryptoTokenResponse.class); // Deserialization + } catch (Exception e) { + throw new NiceException(DESERIALIZATION_FAILED); + } + } + + // 대칭키 및 무결성키 조회 + private NiceCryptoData getCryptoDataFromRedis() { + String storedCryptoData = (String) redisUtil.get(REDIS_CRYPTO_KEY); + if (storedCryptoData != null) { + try { + return objectMapper.readValue(storedCryptoData, NiceCryptoData.class); // Deserialization + } catch (Exception e) { + throw new NiceException(DESERIALIZATION_FAILED); + } + } else { + throw new NiceException(CRYPTO_DATA_NOT_FOUND); + } + } + + // 암호화 토큰 저장 + private void saveCryptoTokenToRedis(GetCryptoTokenResponse cryptoToken) { + try { + String jsonValue = objectMapper.writeValueAsString(cryptoToken); + redisUtil.saveAsValue(REDIS_CRYPTO_TOKEN_KEY, jsonValue, REDIS_CRYPTO_TOKEN_EXPIRATION, SECONDS); + } catch (JsonProcessingException e) { + throw new NiceException(SERIALIZATION_FAILED); + } + } + + // 대칭키, 무결성키 생성 위한 초기 값 생성 + private String generateResultVal(String reqDtim, String reqNo, String tokenVal) { + try { + String value = reqDtim.trim() + reqNo.trim() + tokenVal.trim(); + MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM_SHA256); + md.update(value.getBytes()); + byte[] arrHashValue = md.digest(); + return Base64.getEncoder().encodeToString(arrHashValue); + } catch (NoSuchAlgorithmException e) { + throw new NiceException(HASH_ALGORITHM_NOT_FOUND); + } + } + + // 대칭키, 무결성키 저장 + private void saveCryptoDataToRedis(String key, String iv, String hmacKey) { + try { + NiceCryptoData niceCryptoData = NiceCryptoData.of(key, iv, hmacKey); + String cryptoDataJson = objectMapper.writeValueAsString(niceCryptoData); + redisUtil.saveWithoutExpiration(REDIS_CRYPTO_KEY, cryptoDataJson); + } catch (Exception e) { + throw new NiceException(SERIALIZATION_FAILED); + } + } + + // 요청 데이터 생성 + private String createReqDataJson(String siteCode, Long memberId) { + try { + String requestNo = UUID.randomUUID().toString().substring(0, 30); + NiceRequestData niceRequestData = new NiceRequestData( + requestNo, + niceProperties.getCallbackUrl() + RETURN_URL + memberId, + siteCode, + "Y" + ); + + String requestNoKey = String.format(REDIS_REQUEST_NO_KEY_TEMPLATE, memberId); + redisUtil.saveAsValue(requestNoKey, requestNo, REDIS_REQUEST_NO_EXPIRATION, MINUTES); + + return objectMapper.writeValueAsString(niceRequestData); + } catch (JsonProcessingException e) { + throw new NiceException(SERIALIZATION_FAILED); + } + } + + // 요청 데이터 암호화 + public String encryptReqData(String key, String iv, String reqData) { + try { + SecretKey secretKey = new SecretKeySpec(key.getBytes(), SYMMETRIC_ENCRYPTION_ALGORITHM_AES); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv.getBytes())); + byte[] encrypted = cipher.doFinal(reqData.trim().getBytes()); + return Base64.getEncoder().encodeToString(encrypted); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new NiceException(INVALID_CIPHER_ALGORITHM); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new NiceException(INVALID_CIPHER_PARAMETERS); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new NiceException(CIPHER_DECRYPTION_FAILED); + } + } + + // 무결성 체크값 생성 + private String generateIntegrityValue(byte[] secretKey, byte[] encData) { + try { + Mac mac = Mac.getInstance(MAC_ALGORITHM_HMAC_SHA256); + SecretKeySpec sks = new SecretKeySpec(secretKey, MAC_ALGORITHM_HMAC_SHA256); + mac.init(sks); + byte[] hmac256 = mac.doFinal(encData); + return Base64.getEncoder().encodeToString(hmac256); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new NiceException(CREATE_INTEGRITY_VALUE_FAILED); + } + } +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/service/NiceCallBackUseCase.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/service/NiceCallBackUseCase.java new file mode 100644 index 0000000..69b0ab7 --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/service/NiceCallBackUseCase.java @@ -0,0 +1,164 @@ +package com.drinkhere.clientnice.service; + +import com.drinkhere.clientnice.dto.NiceCryptoData; +import com.drinkhere.clientnice.dto.NiceDecryptedData; +import com.drinkhere.common.annotation.ApplicationService; +import com.drinkhere.common.exception.clientnice.NiceException; +import com.drinkhere.domainrds.member.entity.Member; +import com.drinkhere.domainrds.member.enums.Gender; +import com.drinkhere.domainrds.member.enums.MobileCo; +import com.drinkhere.domainrds.member.enums.NationalInfo; +import com.drinkhere.domainrds.member.service.MemberCommandService; +import com.drinkhere.domainrds.member.service.MemberQueryService; +import com.drinkhere.infraredis.util.RedisUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Base64; + +import static com.drinkhere.common.exception.clientnice.NiceErrorCode.*; + +@ApplicationService +@RequiredArgsConstructor +public class NiceCallBackUseCase { + private final RedisUtil redisUtil; + private final ObjectMapper objectMapper; + private final MemberCommandService memberCommandService; + private final MemberQueryService memberQueryService; + private static final String REDIS_CRYPTO_KEY = "cryptoData"; + private static final String REDIS_REQUEST_NO_KEY_TEMPLATE = "memberId:%d:requestNo"; + private static final String SYMMETRIC_ENCRYPTION_ALGORITHM_AES = "AES"; + private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; + + public void processCallback(Long memberId, String encData) + { + // 대칭키 조회 + NiceCryptoData niceCryptoData = getCryptoDataFromRedisAndValidate(); + + // encData 복호화 + NiceDecryptedData niceDecryptedData = decryptAndParseData(encData, niceCryptoData.key(), niceCryptoData.iv()); + + // Redis에서 requestno 조회 후 복호화 결과의 requestno 비교 + getRequestNoFromRedisAndValidate(memberId, niceDecryptedData.requestNo()); + + // 성인 인증 및 DI 값으로 중복 계정 체크 + validateAdult(niceDecryptedData.birthDate()); + checkDuplicateAccountByDI(niceDecryptedData.di()); + + String decodedName = decodingName(niceDecryptedData.utf8Name()); + + // 복호화 결과 저장 + Member member = Member.builder() + .name(decodedName) + .birthDate(niceDecryptedData.birthDate()) + .gender(Gender.fromValue(Integer.parseInt(niceDecryptedData.gender()))) // Gender Enum 변환 + .nationalInfo(NationalInfo.fromValue(Integer.parseInt(niceDecryptedData.nationalInfo()))) // NationalInfo Enum 변환 + .mobileCo(MobileCo.fromValue(Integer.parseInt(niceDecryptedData.mobileCo()))) // MobileCo Enum 변환 + .mobileNo(niceDecryptedData.mobileNo()) + .di(niceDecryptedData.di()) + .build(); + + memberCommandService.save(member); + } + /**--------------------------------------------------------------------------------------------**/ + // Redis에서 대칭키 조회 후 역직렬화 + private NiceCryptoData getCryptoDataFromRedisAndValidate() { + String cryptoDataJson = (String) redisUtil.get(REDIS_CRYPTO_KEY); + + if (cryptoDataJson == null) { + throw new NiceException(CRYPTO_DATA_NOT_FOUND); + } + + try { + return objectMapper.readValue(cryptoDataJson, NiceCryptoData.class); // Deserialization + } catch (JsonProcessingException e) { + throw new NiceException(DESERIALIZATION_FAILED); + } + } + + // 복호화 후 객체 반환 + private NiceDecryptedData decryptAndParseData(String encData, String key, String iv) { + try { + SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), SYMMETRIC_ENCRYPTION_ALGORITHM_AES ); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8))); + + byte[] decodedData = Base64.getDecoder().decode(encData); + byte[] decryptedData = cipher.doFinal(decodedData); + + String decryptedString = new String(decryptedData, StandardCharsets.UTF_8); + + return objectMapper.readValue(decryptedString, NiceDecryptedData.class);// Deserialization + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new NiceException(INVALID_CIPHER_ALGORITHM); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new NiceException(INVALID_CIPHER_PARAMETERS); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new NiceException(CIPHER_DECRYPTION_FAILED); + } catch (JsonProcessingException e) { + throw new NiceException(DESERIALIZATION_FAILED); + } + + } + + // requestno 조회 및 검증 + private String getRequestNoFromRedisAndValidate(Long memberId, String niceDecryptedRequestNo) { + String requestNoKey = String.format(REDIS_REQUEST_NO_KEY_TEMPLATE, memberId); + String requestNoFromRedis = (String) redisUtil.get(requestNoKey); + if (requestNoFromRedis != null) { // 레디스에 해당 요청번호가 존재하는지 + if (!niceDecryptedRequestNo.equals(requestNoFromRedis)) { // 요청 번호가 일치하는지 + throw new NiceException(REQUEST_NO_MISMATCH); + } + } else { + throw new NiceException(REQUEST_NO_NOT_FOUND); + } + + return requestNoFromRedis; + } + + private void validateAdult(String birthDate) { + // 생년월일 파싱 (yyyyMMdd 형식) + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + LocalDate birthDateLocal = LocalDate.parse(birthDate, formatter); + + // 만 19년이 되는 해의 1월 1일 + LocalDate adultStartDate = birthDateLocal.plusYears(19).withDayOfYear(1); + + // 현재 날짜 + LocalDate today = LocalDate.now(); + + // 만 19년이 되는 해의 1월 1일이 오늘 날짜 이후라면 예외 발생 + if (today.isBefore(adultStartDate)) { + throw new NiceException(NOT_AN_ADULT); + } + } + + private void checkDuplicateAccountByDI(String di) { + // MemberCommandService에서 DI 값으로 회원 조회 + boolean isDuplicate = memberQueryService.existsByDi(di); + + if (isDuplicate) { + throw new NiceException(DUPLICATE_ACCOUNT); + } + } + + private String decodingName(String encodedName) { + try { + return URLDecoder.decode(encodedName, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new NiceException(NAME_DECODING_FAILED); + } + } +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/NiceClient.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/NiceClient.java new file mode 100644 index 0000000..dbebeec --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/NiceClient.java @@ -0,0 +1,63 @@ +package com.drinkhere.clientnice.webclient; + +import com.drinkhere.clientnice.dto.request.GetCryptoTokenRequest; +import com.drinkhere.clientnice.dto.response.GetCryptoTokenResponse; +import com.drinkhere.clientnice.webclient.config.NiceProperties; +import com.drinkhere.common.exception.clientnice.NiceException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static com.drinkhere.common.exception.clientnice.NiceErrorCode.CRYPTO_TOKEN_REQUEST_FAILED; + +@Component +@RequiredArgsConstructor +public class NiceClient { + private final WebClient niceWebClient; + private final NiceProperties niceProperties; + + private static final String NICE_API_URL = "/digital/niceid/api/v1.0/common/crypto/token"; + private static final String BEARER_PREFIX = "bearer "; + private static final String VALID_RESULT_CODE = "1200"; + + public GetCryptoTokenResponse requestCryptoToken(String reqDtim, String reqNo) { + + String authorization = generateAuthorizationHeader(); + + GetCryptoTokenRequest request = new GetCryptoTokenRequest( + new GetCryptoTokenRequest.DataHeader("ko"), + new GetCryptoTokenRequest.DataBody(reqDtim, reqNo, "1") + ); + + GetCryptoTokenResponse response + = niceWebClient.post() + .uri(NICE_API_URL) + .header("Authorization", authorization) + .header("ProductID", niceProperties.getProductId()) + .bodyValue(request) + .retrieve() + .bodyToMono(GetCryptoTokenResponse.class) + .block(); + + validateNiceClientResponse(response.dataHeader().gwRsltCd()); + + return response; + } + + /**----------------------------------------------------------------------------------------------------**/ + // Authorization 헤더 생성 + private String generateAuthorizationHeader() { + long currentTimestamp = System.currentTimeMillis() / 1000; + String rawAuth = String.format("%s:%d:%s", niceProperties.getOrganizationToken(), currentTimestamp, niceProperties.getClientId()); + return BEARER_PREFIX + Base64.getEncoder().encodeToString(rawAuth.getBytes(StandardCharsets.UTF_8)); + } + + private static void validateNiceClientResponse(String resultCode) { + if (!VALID_RESULT_CODE.equals(resultCode)) { + throw new NiceException(CRYPTO_TOKEN_REQUEST_FAILED); + } + } +} diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/config/NiceProperties.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/config/NiceProperties.java new file mode 100644 index 0000000..45c3ec0 --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/config/NiceProperties.java @@ -0,0 +1,20 @@ +package com.drinkhere.clientnice.webclient.config; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@ConfigurationProperties(prefix = "nice-api") +public class NiceProperties { + private String organizationToken; + private String clientId; + private String clientSecret; + private String productId; + private String callbackUrl; +} \ No newline at end of file diff --git a/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/config/NiceWebClientConfig.java b/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/config/NiceWebClientConfig.java new file mode 100644 index 0000000..af160ba --- /dev/null +++ b/client/client-nice/src/main/java/com/drinkhere/clientnice/webclient/config/NiceWebClientConfig.java @@ -0,0 +1,13 @@ +package com.drinkhere.clientnice.webclient.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class NiceWebClientConfig { + @Bean + public WebClient niceWebClient(WebClient.Builder builder) { + return builder.baseUrl("https://svc.niceapi.co.kr:22001").build(); + } +} \ No newline at end of file diff --git a/client/client-nice/src/test/java/com/drinkhere/clientnice/ClientNiceApplicationTests.java b/client/client-nice/src/test/java/com/drinkhere/clientnice/ClientNiceApplicationTests.java new file mode 100644 index 0000000..900191f --- /dev/null +++ b/client/client-nice/src/test/java/com/drinkhere/clientnice/ClientNiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.drinkhere.clientnice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ClientNiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/common/src/main/java/com/drinkhere/common/exception/ErrorResponse.java b/common/src/main/java/com/drinkhere/common/exception/ErrorResponse.java index d60e6f5..fb715f1 100644 --- a/common/src/main/java/com/drinkhere/common/exception/ErrorResponse.java +++ b/common/src/main/java/com/drinkhere/common/exception/ErrorResponse.java @@ -12,6 +12,6 @@ private ErrorResponse(BaseErrorCode errorCode) { } public static ErrorResponse from(CustomException exception) { - return new ErrorResponse(exception.getBaseErrorCode()); + return new ErrorResponse(exception.getErrorCode()); } } diff --git a/common/src/main/java/com/drinkhere/common/exception/GlobalExceptionHandler.java b/common/src/main/java/com/drinkhere/common/exception/GlobalExceptionHandler.java index d8160dc..74b8657 100644 --- a/common/src/main/java/com/drinkhere/common/exception/GlobalExceptionHandler.java +++ b/common/src/main/java/com/drinkhere/common/exception/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package com.drinkhere.common.exception; +import com.drinkhere.common.exception.clientnice.NiceException; import com.drinkhere.common.exception.oauth.AuthException; import com.drinkhere.common.exception.oauth.NotExistTokenException; import com.drinkhere.common.exception.oauth.OAuthException; @@ -72,4 +73,9 @@ protected ResponseEntity> handleInvalidTokenException(InvalidT protected ResponseEntity> handleTokenException(TokenException e) { return ApiResponse.fail(e.getErrorCode().getHttpStatus(), e.getErrorCode().getMessage()); } + + @ExceptionHandler(NiceException.class) + protected ResponseEntity> handleNiceException(NiceException e) { + return ApiResponse.fail(e.getErrorCode().getHttpStatus(), e.getErrorCode().getMessage()); + } } diff --git a/common/src/main/java/com/drinkhere/common/exception/clientnice/NiceErrorCode.java b/common/src/main/java/com/drinkhere/common/exception/clientnice/NiceErrorCode.java new file mode 100644 index 0000000..91a4af7 --- /dev/null +++ b/common/src/main/java/com/drinkhere/common/exception/clientnice/NiceErrorCode.java @@ -0,0 +1,48 @@ +package com.drinkhere.common.exception.clientnice; + +import com.drinkhere.common.exception.BaseErrorCode; +import com.drinkhere.common.response.ApiResponse; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@Getter +@AllArgsConstructor +public enum NiceErrorCode implements BaseErrorCode { + + // 인증 관련 오류 + CRYPTO_TOKEN_REQUEST_FAILED("NICE API 암호화 토큰 요청에 실패했습니다. 다시 시도해주세요.", 1001, HttpStatus.BAD_REQUEST), + NOT_AN_ADULT("성인 인증에 실패했습니다. 민 19세 미만입니다.", 1002, HttpStatus.FORBIDDEN), + DUPLICATE_ACCOUNT("이미 가입한 회원입니다. 가입된 계정으로 로그인해주세요.", 1003, HttpStatus.FORBIDDEN), + NAME_DECODING_FAILED("이름 디코딩에 실패해습니다.", 1004, HttpStatus.INTERNAL_SERVER_ERROR), + + // 암호화/복호화 관련 오류 + HASH_ALGORITHM_NOT_FOUND("요청하신 해시 알고리즘을 찾을 수 없습니다.", 1006, HttpStatus.BAD_REQUEST), + + INVALID_CIPHER_ALGORITHM("지원되지 않는 암호화 알고리즘 또는 패딩 방식입니다.", 1019, HttpStatus.BAD_REQUEST), + INVALID_CIPHER_PARAMETERS("복호화 키 또는 초기화 벡터가 유효하지 않습니다.", 1018, HttpStatus.BAD_REQUEST), + CIPHER_DECRYPTION_FAILED("복호화에 실패했습니다. 패딩 오류 또는 블록 크기 오류.", 1016, HttpStatus.BAD_REQUEST), + + CREATE_INTEGRITY_VALUE_FAILED("무결성 체크값 생성에 실패했습니다: HMAC 알고리즘 초기화 실패 또는 키 유효성 오류", 1020, HttpStatus.BAD_REQUEST), + + // Redis 관련 오류 + REDIS_SAVE_FAILED("Redis에 데이터를 저장하는 데 실패했습니다. 다시 시도해주세요.", 1005, HttpStatus.INTERNAL_SERVER_ERROR), + + CRYPTO_DATA_NOT_FOUND("Redis에 CryptoData를 찾을 수 없습니다. 본인 인증을 다시 진행해주세요.", 1002, HttpStatus.NOT_FOUND), + REQUEST_NO_NOT_FOUND("Redis에 RequestNo를 찾을 수 없습니다. 본인 인증을 다시 진행해주세요.", 1001, HttpStatus.NOT_FOUND), + REQUEST_NO_MISMATCH("RequestNo가 일치하지 않습니다. 본인 인증을 다시 진행해주세요.", 1002, HttpStatus.BAD_REQUEST), + + // 직렬화 / 역직렬화 처리 관련 오류 + SERIALIZATION_FAILED("직렬화(객체 to 문자열)에 실패했습니다.", 1007, HttpStatus.INTERNAL_SERVER_ERROR), + DESERIALIZATION_FAILED("역직렬화(문자열 to 객체)에 실패했습니다.", 1006, HttpStatus.INTERNAL_SERVER_ERROR); + + private final String message; + private final int errorCode; + private final HttpStatus httpStatus; + + @Override + public ResponseEntity> toResponseEntity() { + return ApiResponse.fail(httpStatus, message); + } +} diff --git a/common/src/main/java/com/drinkhere/common/exception/clientnice/NiceException.java b/common/src/main/java/com/drinkhere/common/exception/clientnice/NiceException.java new file mode 100644 index 0000000..17993c0 --- /dev/null +++ b/common/src/main/java/com/drinkhere/common/exception/clientnice/NiceException.java @@ -0,0 +1,10 @@ +package com.drinkhere.common.exception.clientnice; + +import com.drinkhere.common.exception.BaseErrorCode; +import com.drinkhere.common.exception.CustomException; + +public class NiceException extends CustomException { + public NiceException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auditing/BaseEntity.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auditing/BaseEntity.java new file mode 100644 index 0000000..e3bb84c --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auditing/BaseEntity.java @@ -0,0 +1,21 @@ +package com.drinkhere.domainrds.auditing; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +@Getter +public class BaseEntity extends BaseTimeEntity { + @CreatedBy + @Column(updatable = false) + private String createdBy; + + @LastModifiedBy + private String lastModifiedBy; +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auditing/BaseTimeEntity.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auditing/BaseTimeEntity.java new file mode 100644 index 0000000..9fdf9d5 --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auditing/BaseTimeEntity.java @@ -0,0 +1,23 @@ +package com.drinkhere.domainrds.auditing; + +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 +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +public class BaseTimeEntity { + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime lastModifiedDate; +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auth/consts/IgnoredPathConsts.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auth/consts/IgnoredPathConsts.java index 9216c0c..b9dde2d 100644 --- a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auth/consts/IgnoredPathConsts.java +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/auth/consts/IgnoredPathConsts.java @@ -18,7 +18,10 @@ public class IgnoredPathConsts { "/jwt", HttpMethod.GET, "/reissue",HttpMethod.POST, "/actuator/**", HttpMethod.GET, - "/user/landing", HttpMethod.POST + "/user/landing", HttpMethod.POST, + "/api/v1/nice/**" + + "" + + "", HttpMethod.GET ); } diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/entity/Member.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/entity/Member.java new file mode 100644 index 0000000..7cf5767 --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/entity/Member.java @@ -0,0 +1,60 @@ +package com.drinkhere.domainrds.member.entity; + +import com.drinkhere.domainrds.auditing.BaseTimeEntity; +import com.drinkhere.domainrds.member.enums.Gender; +import com.drinkhere.domainrds.member.enums.MobileCo; +import com.drinkhere.domainrds.member.enums.NationalInfo; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long memberId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String birthDate; + + @Enumerated(EnumType.STRING) + @Column(name = "gender", nullable = false) + private Gender gender; + + @Enumerated(EnumType.STRING) + @Column(name = "national_info", nullable = false) + private NationalInfo nationalInfo; + + @Enumerated(EnumType.STRING) + @Column(name = "mobile_co", nullable = false) + private MobileCo mobileCo; + + @Column(nullable = false) + private String mobileNo; + + @Column(nullable = false) + private String di; + + @Builder + public Member(String name, String birthDate, Gender gender, NationalInfo nationalInfo, MobileCo mobileCo, String mobileNo, String di) { + this.name = name; + this.birthDate = birthDate; + this.gender = gender; + this.nationalInfo = nationalInfo; + this.mobileCo = mobileCo; + this.mobileNo = mobileNo; + this.di = di; + } +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/Gender.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/Gender.java new file mode 100644 index 0000000..ffe5cde --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/Gender.java @@ -0,0 +1,34 @@ +package com.drinkhere.domainrds.member.enums; + +import lombok.RequiredArgsConstructor; + +/** + * 성별 + */ +@RequiredArgsConstructor +public enum Gender { + FEMALE(0, "여성"), // 여성(0) + MALE(1, "남성"); // 남성(1) + + private final int value; + private final String description; + + public int getValue() { + return value; + } + + public String getDescription() { + return description; + } + + // 주어진 정수값에 맞는 Gender를 반환하는 정적 메소드 + public static Gender fromValue(int value) { + // 정수값이 0이면 MALE, 1이면 FEMALE을 반환 + switch (value) { + case 0: return FEMALE; + case 1: return MALE; + } + return MALE; + } + +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/MobileCo.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/MobileCo.java new file mode 100644 index 0000000..414e494 --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/MobileCo.java @@ -0,0 +1,42 @@ +package com.drinkhere.domainrds.member.enums; + +import lombok.RequiredArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +/** + * 통신사 정보 + */ +@RequiredArgsConstructor +public enum MobileCo { + SK_TELECOM(1, "SK텔레콤"), + KT(2, "KT"), + LG_UPLUS(3, "LGU+"), + SK_TELECOM_MVNO(5, "SK텔레콤 알뜰폰"), + KT_MVNO(6, "KT 알뜰폰"), + LG_UPLUS_MVNO(7, "LGU+ 알뜰폰"); + + private final int value; + private final String description; + private static final Map mobileCoMap = new HashMap<>(); + + static { + for (MobileCo mobileCo : values()) { + mobileCoMap.put(mobileCo.value, mobileCo); + } + } + + public int value() { + return value; + } + + public String getDescription() { + return description; + } + + public static MobileCo fromValue(int value) { + MobileCo mobileCo = mobileCoMap.get(value); + return mobileCo; + } +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/NationalInfo.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/NationalInfo.java new file mode 100644 index 0000000..64eeed7 --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/enums/NationalInfo.java @@ -0,0 +1,31 @@ +package com.drinkhere.domainrds.member.enums; + +import lombok.RequiredArgsConstructor; + +/** + * 내국인/외국인 + */ +@RequiredArgsConstructor +public enum NationalInfo { + DOMESTIC(0, "내국인"), + FOREIGN(1, "외국인"); + + private final int value; + private final String description; + + public int getValue() { + return value; + } + + public String getDescription() { + return description; + } + + public static NationalInfo fromValue(int value) { + switch (value) { + case 0: return DOMESTIC; + case 1: return FOREIGN; + } + return DOMESTIC; + } +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/repository/MemberRepository.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/repository/MemberRepository.java new file mode 100644 index 0000000..24945d6 --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/repository/MemberRepository.java @@ -0,0 +1,8 @@ +package com.drinkhere.domainrds.member.repository; + +import com.drinkhere.domainrds.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { + boolean existsByDi(String di); +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/service/MemberCommandService.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/service/MemberCommandService.java new file mode 100644 index 0000000..9f4c9be --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/service/MemberCommandService.java @@ -0,0 +1,15 @@ +package com.drinkhere.domainrds.member.service; + +import com.drinkhere.domainrds.member.entity.Member; +import com.drinkhere.domainrds.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class MemberCommandService { + private final MemberRepository memberRepository; + public void save(final Member member) {memberRepository.save(member);} +} diff --git a/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/service/MemberQueryService.java b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/service/MemberQueryService.java new file mode 100644 index 0000000..a07817c --- /dev/null +++ b/domain/domain-rds/src/main/java/com/drinkhere/domainrds/member/service/MemberQueryService.java @@ -0,0 +1,17 @@ +package com.drinkhere.domainrds.member.service; + +import com.drinkhere.domainrds.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class MemberQueryService { + private final MemberRepository memberRepository; + + public boolean existsByDi(String di) { + return memberRepository.existsByDi(di); + } +} diff --git a/execute/build.gradle b/execute/build.gradle index fabc5be..ddfd55f 100644 --- a/execute/build.gradle +++ b/execute/build.gradle @@ -7,7 +7,8 @@ jar { dependencies { implementation project(':api:api-health-check') - implementation project(':api:api-auth') +// implementation project(':api:api-auth') + implementation project(':client:client-nice') // WebClient implementation 'org.springframework.boot:spring-boot-starter-webflux' diff --git a/execute/src/main/resources/application.yml b/execute/src/main/resources/application.yml index f3a8340..f05883e 100644 --- a/execute/src/main/resources/application.yml +++ b/execute/src/main/resources/application.yml @@ -19,7 +19,8 @@ spring: - application-redis-local.yml - application-s3-local.yml - application-hc-local.yml - - application-auth.yml + - application-nice-local.yml + - application-auth-local.yml logging: level: @@ -38,7 +39,8 @@ spring: - application-redis-prod.yml - application-s3-prod.yml - application-hc-prod.yml - - application-auth.yml + - application-auth-prod.yml + - application-nice-prod.yml logging: level: diff --git a/infra/infra-redis/src/main/java/com/drinkhere/infraredis/util/RedisUtil.java b/infra/infra-redis/src/main/java/com/drinkhere/infraredis/util/RedisUtil.java index 6a6b8f3..b7f7c8b 100644 --- a/infra/infra-redis/src/main/java/com/drinkhere/infraredis/util/RedisUtil.java +++ b/infra/infra-redis/src/main/java/com/drinkhere/infraredis/util/RedisUtil.java @@ -17,6 +17,10 @@ public void saveAsValue(String key, Object val, Long time, TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, val, time, timeUnit); } + public void saveWithoutExpiration(String key, Object value) { + redisTemplate.opsForValue().set(key, value); + } + public boolean saveAsValueIfAbsent(String key, Object value, long time, TimeUnit timeUnit) { Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit); return Boolean.TRUE.equals(result); diff --git a/settings.gradle b/settings.gradle index 9ce535e..d0f1a1c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,9 +15,11 @@ include 'infra:infra-s3' // API 모듈 include 'api:api-health-check' +include 'api:api-auth' + +// Client 모듈 +include 'client:client-nice' +//include 'client:client-tosspayments' // 실행 모듈 include 'execute' -include 'api:api-auth' -findProject(':api:api-auth')?.name = 'api-auth' -