From 13a6c6f0b1b7be0f40810decd90097a57b4c8c47 Mon Sep 17 00:00:00 2001 From: kimjaeyeol Date: Tue, 9 Nov 2021 09:30:19 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Add=20=EB=B3=B4=EC=95=88?= =?UTF-8?q?=EC=A0=90=EA=B2=80=EC=97=90=20=EA=B3=B5=ED=86=B5=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/module-common/build.gradle | 94 ++++++++ backend/module-common/gradlew | 185 +++++++++++++++ backend/module-common/gradlew.bat | 89 ++++++++ backend/module-common/settings.gradle | 2 + .../common/config/ApiControllerAdvice.java | 38 ++++ .../cloud/common/config/GlobalConstant.java | 32 +++ .../common/config/LeaveaTraceConfig.java | 34 +++ .../common/config/MessageSourceConfig.java | 63 ++++++ .../common/config/OpenApiDocsConfig.java | 36 +++ .../egovframe/cloud/common/domain/Role.java | 47 ++++ .../common/dto/AttachmentEntityMessage.java | 21 ++ .../cloud/common/dto/RequestDto.java | 30 +++ .../common/exception/BusinessException.java | 70 ++++++ .../exception/BusinessMessageException.java | 33 +++ .../exception/EntityNotFoundException.java | 28 +++ .../exception/InvalidValueException.java | 32 +++ .../cloud/common/exception/dto/ErrorCode.java | 65 ++++++ .../common/exception/dto/ErrorResponse.java | 172 ++++++++++++++ .../cloud/common/service/AbstractService.java | 69 ++++++ .../egovframe/cloud/common/util/LogUtil.java | 84 +++++++ .../cloud/common/util/MessageUtil.java | 66 ++++++ .../config/AuthenticationConverter.java | 99 ++++++++ .../cloud/reactive/config/R2dbcConfig.java | 47 ++++ .../cloud/reactive/config/SecurityConfig.java | 82 +++++++ .../cloud/reactive/config/UserAuditAware.java | 20 ++ .../cloud/reactive/domain/BaseEntity.java | 32 +++ .../cloud/reactive/domain/BaseTimeEntity.java | 34 +++ .../exception/ExceptionHandlerAdvice.java | 209 +++++++++++++++++ .../service/ReactiveAbstractService.java | 19 ++ .../servlet/config/AuthenticationFilter.java | 101 +++++++++ .../cloud/servlet/config/JpaConfig.java | 44 ++++ .../cloud/servlet/config/UserAuditAware.java | 48 ++++ .../cloud/servlet/config/WebMvcConfig.java | 66 ++++++ .../cloud/servlet/domain/BaseEntity.java | 41 ++++ .../cloud/servlet/domain/BaseTimeEntity.java | 39 ++++ .../cloud/servlet/domain/log/ApiLog.java | 60 +++++ .../servlet/domain/log/ApiLogRepository.java | 23 ++ .../exception/ExceptionHandlerAdvice.java | 213 ++++++++++++++++++ .../interceptor/ApiLogInterceptor.java | 38 ++++ .../cloud/servlet/service/ApiLogService.java | 63 ++++++ 40 files changed, 2568 insertions(+) create mode 100644 backend/module-common/build.gradle create mode 100755 backend/module-common/gradlew create mode 100644 backend/module-common/gradlew.bat create mode 100644 backend/module-common/settings.gradle create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/config/ApiControllerAdvice.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/config/GlobalConstant.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/config/LeaveaTraceConfig.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/config/MessageSourceConfig.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/config/OpenApiDocsConfig.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/domain/Role.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/dto/AttachmentEntityMessage.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/dto/RequestDto.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/exception/BusinessException.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/exception/BusinessMessageException.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/exception/EntityNotFoundException.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/exception/InvalidValueException.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/exception/dto/ErrorCode.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/exception/dto/ErrorResponse.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/service/AbstractService.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/util/LogUtil.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/common/util/MessageUtil.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/AuthenticationConverter.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/R2dbcConfig.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/SecurityConfig.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/UserAuditAware.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/reactive/domain/BaseEntity.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/reactive/domain/BaseTimeEntity.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/reactive/exception/ExceptionHandlerAdvice.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/reactive/service/ReactiveAbstractService.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/AuthenticationFilter.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/JpaConfig.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/UserAuditAware.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/WebMvcConfig.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/BaseEntity.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/BaseTimeEntity.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/log/ApiLog.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/log/ApiLogRepository.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/exception/ExceptionHandlerAdvice.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/interceptor/ApiLogInterceptor.java create mode 100644 backend/module-common/src/main/java/org/egovframe/cloud/servlet/service/ApiLogService.java diff --git a/backend/module-common/build.gradle b/backend/module-common/build.gradle new file mode 100644 index 0000000..10cc97d --- /dev/null +++ b/backend/module-common/build.gradle @@ -0,0 +1,94 @@ +plugins { + id 'org.springframework.boot' version '2.4.5' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + // querydsl + id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10' + id 'java' +} + +group 'org.egovframe.cloud' +version '0.1' + +repositories { + mavenCentral() + maven { url "http://www.egovframe.go.kr/maven/" } // egovframe maven 원격 저장소 TODO https +} + +bootJar { + enabled(false) +} +jar { + enabled(true) +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { + // EgovAbstractServiceImpl + implementation('org.egovframe.rte:org.egovframe.rte.fdl.cmmn:4.0.0') { + exclude group: 'org.egovframe.rte', module: 'org.egovframe.rte.fdl.logging' + } + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' // LocalValidatorFactoryBean + implementation 'io.jsonwebtoken:jjwt:0.9.1' + // querydsl + implementation 'com.querydsl:querydsl-jpa' + + //openapi docs + implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' + implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' + implementation 'org.springdoc:springdoc-openapi-webmvc-core:1.5.8' + implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.5.8' + + //reactive + implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + //messaging + implementation 'org.springframework.cloud:spring-cloud-stream' + implementation 'org.springframework.cloud:spring-cloud-stream-binder-rabbit' + + // lombok + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +// querydsl 추가 시작 +def querydslDir = "$buildDir/generated/querydsl" +querydsl { + jpa = true + querydslSourcesDir = querydslDir +} +sourceSets { + main.java.srcDir querydslDir +} +configurations { + querydsl.extendsFrom compileClasspath +} +compileQuerydsl { + options.annotationProcessorPath = configurations.querydsl +} +// querydsl 추가 끝 diff --git a/backend/module-common/gradlew b/backend/module-common/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/module-common/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/backend/module-common/gradlew.bat b/backend/module-common/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/backend/module-common/gradlew.bat @@ -0,0 +1,89 @@ +@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=. +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%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/module-common/settings.gradle b/backend/module-common/settings.gradle new file mode 100644 index 0000000..0359454 --- /dev/null +++ b/backend/module-common/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'module-common' + diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/config/ApiControllerAdvice.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/ApiControllerAdvice.java new file mode 100644 index 0000000..5529405 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/ApiControllerAdvice.java @@ -0,0 +1,38 @@ +package org.egovframe.cloud.common.config; + +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.InitBinder; + +/** + * org.egovframe.cloud.common.config.WebControllerAdvice + * + * 모든 컨트롤러에 적용되는 컨트롤러 어드바이스 클래스 + * 예외 처리 (@ExceptionHandler), 바인딩 설정(@InitBinder), 모델 객체(@ModelAttributes) + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/12 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jooho       최초 생성
+ * 
+ */ +@ControllerAdvice +public class ApiControllerAdvice { + + /** + * 모든 컨트롤러로 들어오는 요청 초기화 + * + * @param binder 웹 데이터 바인더 + */ + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.initDirectFieldAccess(); // Setter 구현 없이 DTO 클래스 필드에 접근 + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/config/GlobalConstant.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/GlobalConstant.java new file mode 100644 index 0000000..4893568 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/GlobalConstant.java @@ -0,0 +1,32 @@ +package org.egovframe.cloud.common.config; + +/** + * org.egovframe.cloud.common.config.Constants + * + * 공통 전역 상수 정의 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/19 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/19    jaeyeolkim  최초 생성
+ * 
+ */ +public interface GlobalConstant { + final String HEADER_SITE_ID = "X-Site-Id"; // header에 어떤 사이트에서 보내는 요청인지 구분하기 위한 정보 + final String AUTHORIZATION_URI = "/api/v1/authorizations/check"; + final String REFRESH_TOKEN_URI = "/api/v1/users/token/refresh"; + final String MESSAGES_URI = "/api/v1/messages/**"; + final String LOGIN_URI = "/login"; + final String[] SECURITY_PERMITALL_ANTPATTERNS = {AUTHORIZATION_URI, REFRESH_TOKEN_URI, MESSAGES_URI, LOGIN_URI, "/actuator/**", "/v3/api-docs/**", "/api/v1/images/**", "/swagger-ui.html"}; + final String USER_SERVICE_URI = "/user-service"; + //예약 신청 후 재고 변경 성공여부 exchange name + final String SUCCESS_OR_NOT_EX_NAME = "success-or-not.direct"; + // 첨부파일 저장 후 entity 정보 update binding name + final String ATTACHMENT_ENTITY_BINDING_NAME = "attachmentEntity-out-0"; +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/config/LeaveaTraceConfig.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/LeaveaTraceConfig.java new file mode 100644 index 0000000..fcf7ae8 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/LeaveaTraceConfig.java @@ -0,0 +1,34 @@ +package org.egovframe.cloud.common.config; + +import lombok.extern.slf4j.Slf4j; +import org.egovframe.rte.fdl.cmmn.trace.LeaveaTrace; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * org.egovframe.cloud.common.config.LeaveaTraceConfig + *

+ * LeaveaTrace Bean 설정 + * EgovAbstractServiceImpl 클래스가 의존한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/24 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/24    jaeyeolkim  최초 생성
+ * 
+ */ +@Configuration +public class LeaveaTraceConfig { + + @Bean + public LeaveaTrace leaveaTrace() { + return new LeaveaTrace(); + } + +} \ No newline at end of file diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/config/MessageSourceConfig.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/MessageSourceConfig.java new file mode 100644 index 0000000..fef988f --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/MessageSourceConfig.java @@ -0,0 +1,63 @@ +package org.egovframe.cloud.common.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.util.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * org.egovframe.cloud.common.config.MessageSourceConfig + *

+ * Spring MessageSource 설정 + * Message Domain 이 있는 portal-service 에서 messages.properties 를 외부 위치에 생성한다. + * 각 서비스에서 해당 파일을 통해 다국어를 지원하도록 한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/09    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@Configuration +public class MessageSourceConfig { + + @Value("${messages.directory}") + private String messagesDirectory; + + @Value("${spring.profiles.active:default}") + private String profile; + + @Bean + public MessageSource messageSource() { + ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); + final String MESSAGES = "/messages"; + if ("default".equals(profile)) { + Path fileStorageLocation = Paths.get(messagesDirectory).toAbsolutePath().normalize(); + String dbMessages = StringUtils.cleanPath("file://" + fileStorageLocation + MESSAGES); + messageSource.setBasenames(dbMessages); + } else { + messageSource.setBasenames(messagesDirectory + MESSAGES); + } + messageSource.getBasenameSet().forEach(s -> log.info("messageSource getBasenameSet={}", s)); + + messageSource.setCacheSeconds(60); // 메세지 파일 변경 감지 간격 + messageSource.setUseCodeAsDefaultMessage(true); // 메세지가 없으면 코드를 메세지로 한다 + messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name()); + return messageSource; + } + +} \ No newline at end of file diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/config/OpenApiDocsConfig.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/OpenApiDocsConfig.java new file mode 100644 index 0000000..c403206 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/config/OpenApiDocsConfig.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.common.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.servers.Server; + +@Configuration +public class OpenApiDocsConfig { + + @Value("${spring.application.name}") + private String appName; + + /** + * @TODO + * api info update 필요 + * + */ + @Bean + public OpenAPI customOpenAPI() { + Server server = new Server(); + server.url("/"+appName); + List servers = new ArrayList<>(); + servers.add(server); + return new OpenAPI() + .components(new Components()) + .servers(servers) + .info(new io.swagger.v3.oas.models.info.Info().title(appName+" API")); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/domain/Role.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/domain/Role.java new file mode 100644 index 0000000..754931c --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/domain/Role.java @@ -0,0 +1,47 @@ +package org.egovframe.cloud.common.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * org.egovframe.cloud.common.domain.Role + *

+ * 사용자 권한 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@RequiredArgsConstructor +public enum Role { + + // 스프링 시큐리티에서는 권한 코드에 항상 ROLE_ 이 앞에 있어야 한다. + ANONYMOUS("ROLE_ANONYMOUS", "손님"), + USER("ROLE_USER", "일반 사용자"), + EMPLOYEE("ROLE_EMPLOYEE", "내부 사용자"), + ADMIN("ROLE_ADMIN", "시스템 관리자"); + + private final String key; + private final String title; + + /** + * 권한 id로 상수 검색 + * + * @param key 권한 id + * @return Role 권한 상수 + */ + public static Role findByKey(String key) { + return Arrays.stream(Role.values()).filter(c -> c.getKey().equals(key)).findAny().orElse(null); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/dto/AttachmentEntityMessage.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/dto/AttachmentEntityMessage.java new file mode 100644 index 0000000..17b2036 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/dto/AttachmentEntityMessage.java @@ -0,0 +1,21 @@ +package org.egovframe.cloud.common.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class AttachmentEntityMessage { + + private String attachmentCode; + private String entityName; + private String entityId; + + @Builder + public AttachmentEntityMessage(String attachmentCode, String entityName, String entityId) { + this.attachmentCode = attachmentCode; + this.entityName = entityName; + this.entityId = entityId; + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/dto/RequestDto.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/dto/RequestDto.java new file mode 100644 index 0000000..2a9388c --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/dto/RequestDto.java @@ -0,0 +1,30 @@ +package org.egovframe.cloud.common.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * org.egovframe.cloud.common.dto.RequestDto + *

+ * 공통 조회조건 요청 파라미터 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/15    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@SuperBuilder +public class RequestDto { + private String keywordType; // 검색조건 + private String keyword; // 검색어 +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/BusinessException.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/BusinessException.java new file mode 100644 index 0000000..663c091 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/BusinessException.java @@ -0,0 +1,70 @@ +package org.egovframe.cloud.common.exception; + +import org.egovframe.cloud.common.exception.dto.ErrorCode; + +/** + * org.egovframe.cloud.common.exception.BusinessException + *

+ * 런타임시 비즈니스 로직상 사용자에게 알려줄 오류 메시지를 만들어 던지는 처리를 담당한다 + * 이 클래스를 상속하여 다양한 형태의 business exception 을 만들 수 있고, + * 그것들은 모두 ExceptionHandlerAdvice BusinessException 처리 메소드에서 잡아낸다. + * 상황에 맞게 에러 코드를 추가하고 이 클래스를 상속하여 사용할 수 있다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/16    jaeyeolkim  최초 생성
+ * 
+ */ +public class BusinessException extends RuntimeException { + + private String customMessage; + private ErrorCode errorCode; + + /** + * 사용자 정의 메시지를 받아 처리하는 경우 + * + * @param errorCode 400 에러 + * @param customMessage 사용자에게 표시할 메시지 + */ + public BusinessException(ErrorCode errorCode, String customMessage) { + super(customMessage); + this.errorCode = errorCode; + this.customMessage = customMessage; + } + + /** + * 사전 정의된 에러코드 객체를 넘기는 경우 + * + * @param message 서버에 남길 메시지 + * @param errorCode 사전 정의된 에러코드 + */ + public BusinessException(String message, ErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + /** + * 사전 정의된 에러코드의 메시지를 서버에 남기고 에러코드 객체를 리턴한다 + * @param errorCode 사전 정의된 에러코드 + */ + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public String getCustomMessage() { + return customMessage; + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/BusinessMessageException.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/BusinessMessageException.java new file mode 100644 index 0000000..d3b8831 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/BusinessMessageException.java @@ -0,0 +1,33 @@ +package org.egovframe.cloud.common.exception; + +import org.egovframe.cloud.common.exception.dto.ErrorCode; + +/** + * org.egovframe.cloud.common.exception.BusinessMessageException + *

+ * 런타임시 비즈니스 로직상 사용자에게 알려줄 오류 메시지를 만들어 던지는 처리를 담당한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jaeyeolkim  최초 생성
+ * 
+ */ +public class BusinessMessageException extends BusinessException { + + /** + * 사용자에게 표시될 메시지와 상태코드 400 을 넘긴다 + * + * @param customMessage + */ + public BusinessMessageException(String customMessage) { + super(ErrorCode.BUSINESS_CUSTOM_MESSAGE, customMessage); + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/EntityNotFoundException.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/EntityNotFoundException.java new file mode 100644 index 0000000..0be0cf4 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/EntityNotFoundException.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.common.exception; + +import org.egovframe.cloud.common.exception.dto.ErrorCode; + +/** + * org.egovframe.cloud.common.exception.EntityNotFoundException + *

+ * 요청한 엔티티를 찾을 수 없을 경우 사용자에게 알려준다. + * ExceptionHandlerAdvice BusinessException 처리 메소드에서 잡아낸다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/16    jaeyeolkim  최초 생성
+ * 
+ */ +public class EntityNotFoundException extends BusinessException { + + public EntityNotFoundException(String message) { + super(message, ErrorCode.ENTITY_NOT_FOUND); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/InvalidValueException.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/InvalidValueException.java new file mode 100644 index 0000000..0b8a277 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/InvalidValueException.java @@ -0,0 +1,32 @@ +package org.egovframe.cloud.common.exception; + +import org.egovframe.cloud.common.exception.dto.ErrorCode; + +/** + * org.egovframe.cloud.common.exception.InvalidValueException + *

+ * 입력 받은 값이 잘못된 경우 사용자에게 알려준다. + * ExceptionHandlerAdvice BusinessException 처리 메소드에서 잡아낸다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/16    jaeyeolkim  최초 생성
+ * 
+ */ +public class InvalidValueException extends BusinessException { + + public InvalidValueException(String value) { + super(value, ErrorCode.INVALID_INPUT_VALUE); + } + + public InvalidValueException(String value, ErrorCode errorCode) { + super(value, errorCode); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/dto/ErrorCode.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/dto/ErrorCode.java new file mode 100644 index 0000000..ba4dd79 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/dto/ErrorCode.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.common.exception.dto; + +/** + * org.egovframe.cloud.common.exception.dto.ErrorCode + *

+ * REST API 요청에 대한 오류 반환값을 정의 + * ErrorResponse 클래스에서 status, code, message 세 가지 속성을 의존한다 + * message 는 MessageSource 의 키 값을 정의하여 다국어 처리를 지원한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/16    jaeyeolkim  최초 생성
+ * 
+ */ +public enum ErrorCode { + + INVALID_INPUT_VALUE(400, "E001", "err.invalid.input.value"), // Bad Request + INVALID_TYPE_VALUE(400, "E002", "err.invalid.type.value"), // Bad Request + ENTITY_NOT_FOUND(400, "E003", "err.entity.not.found"), // Bad Request + UNAUTHORIZED(401, "E004", "err.unauthorized"), // The request requires an user authentication + JWT_EXPIRED(401, "E005", "err.unauthorized"), // The request requires an user authentication + ACCESS_DENIED(403, "E006", "err.access.denied"), // Forbidden, Access is Denied + NOT_FOUND(404, "E010", "err.page.not.found"), // Not found + METHOD_NOT_ALLOWED(405, "E011", "err.method.not.allowed"), // 요청 방법이 서버에 의해 알려졌으나, 사용 불가능한 상태 + REQUIRE_USER_JOIN(412, "E012", "err.user.notexists"), // Server Error + UNPROCESSABLE_ENTITY(422, "E020", "err.unprocessable.entity"), // Unprocessable Entity + INTERNAL_SERVER_ERROR(500, "E999", "err.internal.server"), // Server Error + + // business error code + BUSINESS_CUSTOM_MESSAGE(400, "B001", ""), // 사용자 정의 메시지를 넘기는 business exception + DUPLICATE_INPUT_INVALID(400, "B002", "err.duplicate.input.value"), // 중복된 값을 입력하였습니다 + DB_CONSTRAINT_DELETE(400, "B003", "err.duplicate.input.value") // 참조하는 데이터가 있어서 삭제할 수 없습니다 + ; + + + private final int status; + private final String code; + private final String message; + + ErrorCode(final int status, final String code, final String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public int getStatus() { + return status; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/dto/ErrorResponse.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/dto/ErrorResponse.java new file mode 100644 index 0000000..d058f2a --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/exception/dto/ErrorResponse.java @@ -0,0 +1,172 @@ +package org.egovframe.cloud.common.exception.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.validation.BindingResult; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static lombok.AccessLevel.PROTECTED; + +/** + * org.egovframe.cloud.common.exception.ErrorResponse + *

+ * 일관된 예외처리를 제공하는 클래스 + * https://github.com/cheese10yun/spring-guide + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/16    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor(access = PROTECTED) +public class ErrorResponse { + private LocalDateTime timestamp; + private String message; + private int status; + private String code; + private List errors; + + private static final String DEFAULT_ERROR_MESSAGE = "ERROR"; + + private ErrorResponse(final ErrorCode code, final List errors, MessageSource messageSource) { + this.timestamp = LocalDateTime.now(); + this.message = messageSource.getMessage(code.getMessage(), new Object[]{}, DEFAULT_ERROR_MESSAGE, LocaleContextHolder.getLocale()); + this.status = code.getStatus(); + this.code = code.getCode(); + this.errors = errors; + } + + private ErrorResponse(final ErrorCode code, MessageSource messageSource) { + this.timestamp = LocalDateTime.now(); + this.message = messageSource.getMessage(code.getMessage(), new Object[]{}, DEFAULT_ERROR_MESSAGE, LocaleContextHolder.getLocale()); + this.status = code.getStatus(); + this.code = code.getCode(); + this.errors = new ArrayList<>(); + } + + private ErrorResponse(final ErrorCode code, String customMessage) { + this.timestamp = LocalDateTime.now(); + this.message = customMessage; + this.status = code.getStatus(); + this.code = code.getCode(); + this.errors = new ArrayList<>(); + } + + /** + * 사용자 정의 메시지를 받아 넘기는 경우 + * + * @param code + * @param customMessage + * @return + */ + public static ErrorResponse of(final ErrorCode code, final String customMessage) { + return new ErrorResponse(code, customMessage); + } + + /** + * ErrorResponse 는 protected 를 선언하여 new 생성할 수 없도록 막아두고 static 메소드를 통해 생성할 수 있도록 하였다 + * ExceptionHandlerAdvice 에서 인자를 받아 ErrorResponse 객체를 생성한다 + * + * @param code + * @param bindingResult + * @param messageSource + * @return + */ + public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult, MessageSource messageSource) { + return new ErrorResponse(code, FieldError.of(bindingResult), messageSource); + } + + /** + * ErrorResponse 는 protected 를 선언하여 new 생성할 수 없도록 막아두고 static 메소드를 통해 생성할 수 있도록 하였다 + * ExceptionHandlerAdvice 에서 인자를 받아 ErrorResponse 객체를 생성한다 + * + * @param code + * @param messageSource + * @return + */ + public static ErrorResponse of(final ErrorCode code, MessageSource messageSource) { + return new ErrorResponse(code, messageSource); + } + + /** + * ErrorResponse 는 protected 를 선언하여 new 생성할 수 없도록 막아두고 static 메소드를 통해 생성할 수 있도록 하였다 + * ExceptionHandlerAdvice 에서 인자를 받아 ErrorResponse 객체를 생성한다 + * + * @param code + * @param errors + * @param messageSource + * @return + */ + public static ErrorResponse of(final ErrorCode code, final List errors, MessageSource messageSource) { + return new ErrorResponse(code, errors, messageSource); + } + + /** + * ErrorResponse 는 protected 를 선언하여 new 생성할 수 없도록 막아두고 static 메소드를 통해 생성할 수 있도록 하였다 + * ExceptionHandlerAdvice 에서 인자를 받아 ErrorResponse 객체를 생성한다 + * java validator 에러 발생 시 에러 정보 중 필요한 내용만 FieldError 로 반환한다 + * + * @param e + * @return + */ + public static ErrorResponse of(MethodArgumentTypeMismatchException e, MessageSource messageSource) { + final String value = e.getValue() == null ? "" : e.getValue().toString(); + final List errors = ErrorResponse.FieldError.of(e.getName(), value, e.getErrorCode()); + return new ErrorResponse(ErrorCode.INVALID_TYPE_VALUE, errors, messageSource); + } + + /** + * java validator 에러 발생 시 에러 정보 중 필요한 내용만 반환한다 + */ + @Getter + @NoArgsConstructor(access = PROTECTED) + public static class FieldError { + private String message; + private String field; + private String rejectedValue; + + private FieldError(final String field, final String rejectedValue, final String message) { + this.field = field; + this.rejectedValue = rejectedValue; + this.message = message; + } + + public static List of(final String field, final String rejectedValue, final String message) { + List fieldErrors = new ArrayList<>(); + fieldErrors.add(new FieldError(field, rejectedValue, message)); + return fieldErrors; + } + + /** + * BindingResult to FieldError + * + * @param bindingResult + * @return + */ + private static List of(final BindingResult bindingResult) { + final List fieldErrors = bindingResult.getFieldErrors(); + return fieldErrors.stream() + .map(error -> new FieldError( + error.getField(), + error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(), + error.getDefaultMessage())) + .collect(Collectors.toList()); + } + } + +} \ No newline at end of file diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/service/AbstractService.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/service/AbstractService.java new file mode 100644 index 0000000..8f7c979 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/service/AbstractService.java @@ -0,0 +1,69 @@ +package org.egovframe.cloud.common.service; + +import static org.egovframe.cloud.common.config.GlobalConstant.*; + +import org.egovframe.cloud.common.dto.AttachmentEntityMessage; +import org.egovframe.cloud.common.util.MessageUtil; +import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.messaging.support.MessageBuilder; + +import javax.annotation.Resource; + +/** + * org.egovframe.cloud.common.service.AbstractService + *

+ * 표준프레임워크 EgovAbstractServiceImpl 을 상속하는 공통 추상 클래스이다. + * 각 @Service 클래스는 이 클래스를 반드시 상속하여야 한다.(표준프레임워크 준수사항) + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jaeyeolkim  최초 생성
+ * 
+ * @since 2021/07/28 + */ +public abstract class AbstractService extends EgovAbstractServiceImpl { + + @Resource(name = "messageUtil") + protected MessageUtil messageUtil; + + /** + * messageSource 에 코드값을 넘겨 메시지를 찾아 리턴한다. + * + * @param code + * @return + */ + protected String getMessage(String code) { + return messageUtil.getMessage(code); + } + + /** + * messageSource 에 코드값과 인자를 넘겨 메시지를 찾아 리턴한다. + * + * @param code + * @param args + * @return + */ + protected String getMessage(String code, Object[] args) { + return messageUtil.getMessage(code, args); + } + + /** + * 게시물 저장 후 해당 정보를 첨부파일 entity에 입력하기 위해 + * 이벤트 메세지 발행 + * + * @param entityMessage + */ + protected void sendAttachmentEntityInfo(StreamBridge streamBridge, AttachmentEntityMessage entityMessage) { + streamBridge.send(ATTACHMENT_ENTITY_BINDING_NAME, + MessageBuilder.withPayload(entityMessage).build()); + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/util/LogUtil.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/util/LogUtil.java new file mode 100644 index 0000000..4986fee --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/util/LogUtil.java @@ -0,0 +1,84 @@ +package org.egovframe.cloud.common.util; + +import org.egovframe.cloud.common.config.GlobalConstant; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + + +/** + * org.egovframe.cloud.common.util.LogUtil + *

+ * 로그인, 접속 로그 입력 시 필요한 정보 제공 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/02 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/02    jaeyeolkim  최초 생성
+ * 
+ */ +public class LogUtil { + + /** + * 클라이언트 사용자의 IP 가져오기 + * + * @return + */ + public static String getUserIp() { + + String ip = null; + HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest(); + + ip = request.getHeader("X-Forwarded-For"); + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-RealIP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("REMOTE_ADDR"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + return ip; + } + + /** + * 접속 사이트 정보를 넘긴다. + * + * @param request + * @return + */ + public static Long getSiteId(HttpServletRequest request) { + String header = request.getHeader(GlobalConstant.HEADER_SITE_ID); + if (!StringUtils.hasLength(header)) { + return null; + } + return Long.valueOf(header); + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/common/util/MessageUtil.java b/backend/module-common/src/main/java/org/egovframe/cloud/common/util/MessageUtil.java new file mode 100644 index 0000000..77d21b2 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/common/util/MessageUtil.java @@ -0,0 +1,66 @@ +package org.egovframe.cloud.common.util; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Locale; + +/** + * org.egovframe.cloud.common.util.MessageUtil + *

+ * MessageSource 값을 읽을 수 있는 메소드를 제공한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/08    jaeyeolkim  최초 생성
+ * 
+ */ +@Component +public class MessageUtil { + + @Resource(name = "messageSource") + private MessageSource messageSource; + + /** + * messageSource 에 코드값을 넘겨 메시지를 찾아 리턴한다. + * + * @param code + * @return + */ + public String getMessage(String code) { + return this.getMessage(code, new Object[]{}); + } + + /** + * messageSource 에 코드값과 인자를 넘겨 메시지를 찾아 리턴한다. + * + * @param code + * @param args + * @return + */ + public String getMessage(String code, Object[] args) { + return this.getMessage(code, args, LocaleContextHolder.getLocale()); + } + + /** + * messageSource 에 코드값, 인자, 지역정보를 넘겨 메시지를 찾아 리턴한다. + * + * @param code + * @param args + * @param locale + * @return + */ + public String getMessage(String code, Object[] args, Locale locale) { + return messageSource.getMessage(code, args, locale); + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/AuthenticationConverter.java b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/AuthenticationConverter.java new file mode 100644 index 0000000..0f5d29d --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/AuthenticationConverter.java @@ -0,0 +1,99 @@ +package org.egovframe.cloud.reactive.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; + + +/** + * org.egovframe.cloud.reserveitemservice.config.AuthenticationConverter + * + * 요청을 authentiation으로 변환하는 클래스 + * AuthenticationWebFilter에서 호출됨. + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@Component +public class AuthenticationConverter implements ServerAuthenticationConverter { + @Value("${token.secret}") + private String TOKEN_SECRET; + final String TOKEN_CLAIM_NAME = "authorities"; + + /** + * 요청에 담긴 토큰을 조회하여 Authentication 정보를 설정한다. + * + * @param exchange + * @return + */ + @Override + public Mono convert(ServerWebExchange exchange) { + return Mono.justOrEmpty(exchange) + .flatMap(e -> Mono.justOrEmpty(e.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION))) + .flatMap(auth -> { + if (auth == null) { + return Mono.empty(); + } + + String token = auth.get(0); + if (!StringUtils.hasText(token) || "undefined".equals(token)) { + return Mono.empty(); + } + + Claims claims = getClaimsFromToken(token); + String authorities = claims.get(TOKEN_CLAIM_NAME, String.class); + List roleList = new ArrayList<>(); + roleList.add(new SimpleGrantedAuthority(authorities)); + + String username = claims.getSubject(); + + if (username == null) { + ReactiveSecurityContextHolder.withAuthentication(null); + return Mono.empty(); + } + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(username, null, roleList); + + ReactiveSecurityContextHolder.withAuthentication(authenticationToken); + return Mono.just(authenticationToken); + }); + + } + + /** + * AuthenticationFilter.doFilter 메소드에서 UsernamePasswordAuthenticationToken 정보를 세팅할 때 호출된다. + * + * @param token + * @return + */ + public Claims getClaimsFromToken(String token) { + return Jwts.parser() + .setSigningKey(TOKEN_SECRET) + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/R2dbcConfig.java b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/R2dbcConfig.java new file mode 100644 index 0000000..25fe5b6 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/R2dbcConfig.java @@ -0,0 +1,47 @@ +package org.egovframe.cloud.reactive.config; + +import io.r2dbc.spi.ConnectionFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.r2dbc.config.EnableR2dbcAuditing; +import org.springframework.r2dbc.connection.init.CompositeDatabasePopulator; +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; + +/** + * org.egovframe.cloud.reserveitemservice.config.R2dbcConfig + * + * R2DBC configuration class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj       최초 생성
+ * 
+ */ +@Profile("!test") +@Configuration +@EnableR2dbcAuditing(auditorAwareRef = "userAuditAware") //auditing +public class R2dbcConfig { + + @Bean + public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); + initializer.setConnectionFactory(connectionFactory); + + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); + populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("schema.sql"))); + initializer.setDatabasePopulator(populator); + + return initializer; + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/SecurityConfig.java b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/SecurityConfig.java new file mode 100644 index 0000000..cdc0ad0 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/SecurityConfig.java @@ -0,0 +1,82 @@ +package org.egovframe.cloud.reactive.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reserveitemservice.config.SecurityConfig + * + * Spring Security Config 클래스 + * AuthenticationFilter 를 추가하고 토큰으로 setAuthentication 인증처리를 한다 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + + private final AuthenticationConverter authenticationConverter; + + /** + * Reactive Security 설정 + * + * @param http + * @return + * @throws Exception + */ + @Bean + public SecurityWebFilterChain configure(ServerHttpSecurity http) throws Exception { + return http + .csrf().disable() + .headers().frameOptions().disable() + .and() + .formLogin().disable() + .httpBasic().disable() + .logout().disable() + .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION) + .build(); + } + + /** + * AuthenticationManager + * Api Gateway에서 인증되기 때문에 따로 확인하지 않는다. + * + * @return + */ + @Bean + public ReactiveAuthenticationManager authenticationManager() { + return authentication -> Mono.just(authentication); + } + + /** + * 인증 요청 필터 + * AuthenticationConverter 를 적용하여 Authentication 정보를 설정한다. + * + * @return + * @throws Exception + */ + public AuthenticationWebFilter authenticationWebFilter() throws Exception { + AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager()); + filter.setServerAuthenticationConverter(authenticationConverter); + return filter; + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/UserAuditAware.java b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/UserAuditAware.java new file mode 100644 index 0000000..2e6c13f --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/config/UserAuditAware.java @@ -0,0 +1,20 @@ +package org.egovframe.cloud.reactive.config; + +import org.springframework.data.domain.ReactiveAuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class UserAuditAware implements ReactiveAuditorAware { + @Override + public Mono getCurrentAuditor() { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getPrincipal) + .map(String.class::cast); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/reactive/domain/BaseEntity.java b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/domain/BaseEntity.java new file mode 100644 index 0000000..845efab --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/domain/BaseEntity.java @@ -0,0 +1,32 @@ +package org.egovframe.cloud.reactive.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; + +/** + * org.egovframe.cloud.servlet.domain.BaseEntity + *

+ * JPA Entity 클래스들이 BaseEntity 를 상속할 경우 createdBy, lastModifiedBy 필드들과 + * BaseTimeEntity 필드들(createdDate, modifiedDate)까지 컬럼으로 인식된다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +public abstract class BaseEntity extends BaseTimeEntity{ + @CreatedBy + protected String createdBy; + + @LastModifiedBy + protected String lastModifiedBy; +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/reactive/domain/BaseTimeEntity.java b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/domain/BaseTimeEntity.java new file mode 100644 index 0000000..e5bdd5f --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/domain/BaseTimeEntity.java @@ -0,0 +1,34 @@ +package org.egovframe.cloud.reactive.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.servlet.domain.BaseTimeEntity + *

+ * JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들(createdDate, modifiedDate)도 컬럼으로 인식된다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +public abstract class BaseTimeEntity { + @CreatedDate + protected LocalDateTime createDate; + + @LastModifiedDate + protected LocalDateTime modifiedDate; + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/reactive/exception/ExceptionHandlerAdvice.java b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/exception/ExceptionHandlerAdvice.java new file mode 100644 index 0000000..38c6594 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/exception/ExceptionHandlerAdvice.java @@ -0,0 +1,209 @@ +package org.egovframe.cloud.reactive.exception; + +import io.jsonwebtoken.ExpiredJwtException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.exception.BusinessException; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.exception.dto.ErrorCode; +import org.egovframe.cloud.common.exception.dto.ErrorResponse; +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.webjars.NotFoundException; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +@RestControllerAdvice +public class ExceptionHandlerAdvice { + + private final MessageSource messageSource; + + /** + * javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다. + * HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할경우 발생 + * 주로 @RequestBody, @RequestPart 어노테이션에서 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected Mono handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("handleMethodArgumentNotValidException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource); + return Mono.just(response); + } + + /** + * 바인딩 객체 @ModelAttribute 으로 binding error 발생시 BindException 발생한다. + * ref https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-modelattrib-method-args + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected Mono handleBindException(BindException e) { + log.error("handleBindException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource); + return Mono.just(response); + } + + /** + * 요청은 잘 만들어졌지만, 문법 오류로 인하여 따를 수 없습니다 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(HttpClientErrorException.UnprocessableEntity.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected Mono handleUnprocessableEntityException(HttpClientErrorException.UnprocessableEntity e) { + log.error("handleUnprocessableEntityException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.UNPROCESSABLE_ENTITY, messageSource); + return Mono.just(response); + } + + /** + * enum type 일치하지 않아 binding 못할 경우 발생 + * 주로 @RequestParam enum으로 binding 못했을 경우 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected Mono handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + log.error("handleMethodArgumentTypeMismatchException", e); + final ErrorResponse response = ErrorResponse.of(e, messageSource); + return Mono.just(response); + } + + /** + * 요청한 페이지가 존재하지 않는 경우 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + protected Mono handleNotFoundException(NotFoundException e) { + log.error("handleNotFoundException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.NOT_FOUND, messageSource); + return Mono.just(response); + } + + /** + * Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(AccessDeniedException.class) + protected Mono> handleAccessDeniedException(AccessDeniedException e) { + log.error("handleAccessDeniedException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.ACCESS_DENIED, messageSource); + return Mono.just(ResponseEntity.status(HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus())) + .body(response)); + } + + /** + * 사용자 인증되지 않은 경우 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(HttpClientErrorException.Unauthorized.class) + protected Mono> handleUnauthorizedException(HttpClientErrorException.Unauthorized e) { + log.error("handleUnauthorizedException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.UNAUTHORIZED, messageSource); + return Mono.just(ResponseEntity.status(HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus())) + .body(response)); + } + + /** + * JWT 인증 만료 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(ExpiredJwtException.class) + protected Mono> handleExpiredJwtException(ExpiredJwtException e) { + log.error("handleExpiredJwtException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.JWT_EXPIRED, messageSource); + return Mono.just(ResponseEntity.status(HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus())) + .body(response)); + } + + /** + * 사용자에게 표시할 다양한 메시지를 직접 정의하여 처리하는 Business RuntimeException Handler + * 개발자가 만들어 던지는 런타임 오류를 처리 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(BusinessMessageException.class) + protected Mono> handleBusinessMessageException(BusinessMessageException e) { + log.error("handleBusinessMessageException", e); + final ErrorCode errorCode = e.getErrorCode(); + final String customMessage = e.getCustomMessage(); + final ErrorResponse response = ErrorResponse.of(errorCode, customMessage); + return Mono.just(ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())) + .body(response)); + } + + /** + * 개발자 정의 ErrorCode 를 처리하는 Business RuntimeException Handler + * 개발자가 만들어 던지는 런타임 오류를 처리 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(BusinessException.class) + protected Mono> handleBusinessException(BusinessException e) { + log.error("handleBusinessException", e); + final ErrorCode errorCode = e.getErrorCode(); + final ErrorResponse response = ErrorResponse.of(errorCode, messageSource); + return Mono.just(ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())) + .body(response)); + } + + /** + * default exception + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + protected Mono handleException(Exception e) { + log.error("handleException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, messageSource); + return Mono.just(response); + } + + /** + * javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다. + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(WebExchangeBindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected Mono handleWebExchangeBindException(WebExchangeBindException e) { + log.error("handleWebExchangeBindException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource); + return Mono.just(response); + + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/reactive/service/ReactiveAbstractService.java b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/service/ReactiveAbstractService.java new file mode 100644 index 0000000..797d37d --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/reactive/service/ReactiveAbstractService.java @@ -0,0 +1,19 @@ +package org.egovframe.cloud.reactive.service; + +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import reactor.core.publisher.Mono; + +public class ReactiveAbstractService extends AbstractService { + + /** + * mono error entity not found exception + * + * @param id + * @param + * @return + */ + protected Mono monoResponseStatusEntityNotFoundException(Object id) { + return Mono.error( new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + String.valueOf(id))); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/AuthenticationFilter.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/AuthenticationFilter.java new file mode 100644 index 0000000..4010e22 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/AuthenticationFilter.java @@ -0,0 +1,101 @@ +package org.egovframe.cloud.servlet.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * org.egovframe.cloud.servlet.config.AuthenticationFilter + *

+ * Spring Security AuthenticationFilter 처리 + * 로그인 인증정보를 받아 토큰을 발급한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private final String TOKEN_SECRET; + + final String TOKEN_CLAIM_NAME = "authorities"; + + public AuthenticationFilter(AuthenticationManager authenticationManager, String tokenSecret) { + super.setAuthenticationManager(authenticationManager); + this.TOKEN_SECRET = tokenSecret; + } + + /** + * AuthenticationFilter.doFilter 메소드에서 UsernamePasswordAuthenticationToken 정보를 세팅할 때 호출된다. + * + * @param token + * @return + */ + public Claims getClaimsFromToken(String token) { + return Jwts.parser() + .setSigningKey(TOKEN_SECRET) + .parseClaimsJws(token) + .getBody(); + } + + + + /** + * 로그인 요청 뿐만 아니라 모든 요청시마다 호출된다. + * 토큰에 담긴 정보로 Authentication 정보를 설정한다. + * 이 처리를 하지 않으면 AnonymousAuthenticationToken 으로 처리된다. + * + * @param request + * @param response + * @param chain + * @throws IOException + * @throws ServletException + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + String token = httpRequest.getHeader(HttpHeaders.AUTHORIZATION); + if (token == null || "undefined".equals(token) || "".equals(token)) { + super.doFilter(request, response, chain); + } else { + Claims claims = getClaimsFromToken(token); + String authorities = claims.get(TOKEN_CLAIM_NAME, String.class); + List roleList = new ArrayList<>(); + roleList.add(new SimpleGrantedAuthority(authorities)); + + String username = claims.getSubject(); + if (username != null) { + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, null, roleList)); + chain.doFilter(request, response); + } else { + SecurityContextHolder.getContext().setAuthentication(null); + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + } + } + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/JpaConfig.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/JpaConfig.java new file mode 100644 index 0000000..8b33f75 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/JpaConfig.java @@ -0,0 +1,44 @@ +package org.egovframe.cloud.servlet.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +import javax.persistence.EntityManager; + +/** + * org.egovframe.cloud.servlet.config.JpaConfig + * + * JPA 설정 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/07 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    jooho       최초 생성
+ * 
+ */ +@Configuration +@EnableJpaAuditing(auditorAwareRef = "userAuditAware") // JPA Auditing 활성화 +@EnableJpaRepositories(basePackages = "org.egovframe.cloud.*.domain") +public class JpaConfig { + + /** + * JpaQueryFactory 빈 등록 + * + * @param entityManager 엔티티 매니저 + * @return JPAQueryFactory 쿼리 및 DML 절 생성을 위한 팩토리 클래스 + */ + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) { + return new JPAQueryFactory(entityManager); + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/UserAuditAware.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/UserAuditAware.java new file mode 100644 index 0000000..84fd848 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/UserAuditAware.java @@ -0,0 +1,48 @@ +package org.egovframe.cloud.servlet.config; + +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * org.egovframe.cloud.servlet.config.UserAuditAware + *

+ * JPA Entity 생성자/수정자 정보를 자동 입력한다. + * AuthenticationFilter.doFilter 메소드에서 UsernamePasswordAuthenticationToken 정보를 세팅해주기 때문에 + * Authentication 에서 userId 값을 받아올 수 있게 된다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jaeyeolkim  최초 생성
+ * 
+ */ +@Component +public class UserAuditAware implements AuditorAware { + + /** + * Auditing 기능이 활성화된 엔티티에 변경이 감지되면 호출되어 생성자/수정자 정보를 반환한다. + * + * @return + */ + @Override + public Optional getCurrentAuditor() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) { + return Optional.empty(); + } + + String userId = authentication.getPrincipal() == null ? null : authentication.getPrincipal().toString(); + return Optional.of(userId); + } +} \ No newline at end of file diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/WebMvcConfig.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/WebMvcConfig.java new file mode 100644 index 0000000..fc4173a --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/config/WebMvcConfig.java @@ -0,0 +1,66 @@ +package org.egovframe.cloud.servlet.config; + +import org.egovframe.cloud.servlet.interceptor.ApiLogInterceptor; +import org.egovframe.cloud.servlet.service.ApiLogService; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * org.egovframe.cloud.userservice.domain.BaseTimeEntity + *

+ * WebMvc 관련 Configuration 클래스 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/05 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/05    jaeyeolkim  최초 생성
+ * 
+ */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final ApiLogService apiLogService; + + public WebMvcConfig(ApiLogService apiLogService) { + this.apiLogService = apiLogService; + } + + /** + * LocalValidatorFactoryBean 에 messageSource Bean 주입하여 validtor 메시지를 지원한다. + * + * @param messageSource messages.properties + * @return LocalValidatorFactoryBean + */ + @Bean + public LocalValidatorFactoryBean validator(MessageSource messageSource) { + LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); + bean.setValidationMessageSource(messageSource); + return bean; + } + + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry + .addInterceptor(new ApiLogInterceptor(apiLogService)) + .excludePathPatterns( + "/", + "/error", + "/favicon.ico", + "/api/v1/authorizations/check", + "/api/v1/users/token/refresh", + "/api/v1/menu-roles/**", + "/api/v1/messages/**" + ); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/BaseEntity.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/BaseEntity.java new file mode 100644 index 0000000..7102d80 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/BaseEntity.java @@ -0,0 +1,41 @@ +package org.egovframe.cloud.servlet.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; + +/** + * org.egovframe.cloud.servlet.domain.BaseTimeEntity + *

+ * JPA Entity 클래스들이 BaseEntity 를 상속할 경우 createdBy, lastModifiedBy 필드들과 + * BaseTimeEntity 필드들(createdDate, modifiedDate)까지 컬럼으로 인식된다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함 +public abstract class BaseEntity extends BaseTimeEntity { + + @CreatedBy + @Column(updatable = false) + private String createdBy; + + @LastModifiedBy + private String lastModifiedBy; +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/BaseTimeEntity.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/BaseTimeEntity.java new file mode 100644 index 0000000..794af7a --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/BaseTimeEntity.java @@ -0,0 +1,39 @@ +package org.egovframe.cloud.servlet.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.servlet.domain.BaseTimeEntity + *

+ * JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들(createdDate, modifiedDate)도 컬럼으로 인식된다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함 +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime modifiedDate; +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/log/ApiLog.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/log/ApiLog.java new file mode 100644 index 0000000..9d9e15f --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/log/ApiLog.java @@ -0,0 +1,60 @@ +package org.egovframe.cloud.servlet.domain.log; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseTimeEntity; + +import javax.persistence.*; + +import static javax.persistence.GenerationType.IDENTITY; + +/** + * org.egovframe.cloud.servlet.domain.log.LoginLog + *

+ * 로그인 로그 엔티티 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/01 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/01    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +public class ApiLog extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "log_id") + private Long id; + + private Long siteId; + + private String userId; + + @Column(length = 10) + private String httpMethod; + + @Column(length = 500) + private String requestUrl; + + @Column(name = "ip_addr", length = 100) + private String remoteIp; + + @Builder + public ApiLog(Long siteId, String userId, String httpMethod, String requestUrl, String remoteIp) { + this.siteId = siteId; + this.userId = userId; + this.httpMethod = httpMethod; + this.requestUrl = requestUrl; + this.remoteIp = remoteIp; + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/log/ApiLogRepository.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/log/ApiLogRepository.java new file mode 100644 index 0000000..3715d22 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/domain/log/ApiLogRepository.java @@ -0,0 +1,23 @@ +package org.egovframe.cloud.servlet.domain.log; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.servlet.domain.log.ApiLogRepository + *

+ * Spring Data JPA 에서 제공되는 JpaRepository 를 상속하는 인터페이스 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/01 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/01    jaeyeolkim  최초 생성
+ * 
+ */ +public interface ApiLogRepository extends JpaRepository { +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/exception/ExceptionHandlerAdvice.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/exception/ExceptionHandlerAdvice.java new file mode 100644 index 0000000..1ed912c --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/exception/ExceptionHandlerAdvice.java @@ -0,0 +1,213 @@ +package org.egovframe.cloud.servlet.exception; + +import io.jsonwebtoken.ExpiredJwtException; +import javassist.NotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.exception.BusinessException; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.exception.dto.ErrorCode; +import org.egovframe.cloud.common.exception.dto.ErrorResponse; +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +/** + * org.egovframe.cloud.common.exception.WebControllerAdvice + *

+ * 모든 컨트롤러에 적용되는 컨트롤러 어드바이스 클래스 + * 예외 처리 (@ExceptionHandler), 바인딩 설정(@InitBinder), 모델 객체(@ModelAttributes) + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/15    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class ExceptionHandlerAdvice { + + protected final MessageSource messageSource; + + /** + * javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다. + * HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할경우 발생 + * 주로 @RequestBody, @RequestPart 어노테이션에서 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("handleMethodArgumentNotValidException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + /** + * 바인딩 객체 @ModelAttribute 으로 binding error 발생시 BindException 발생한다. + * ref https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-modelattrib-method-args + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(BindException.class) + protected ResponseEntity handleBindException(BindException e) { + log.error("handleBindException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult(), messageSource); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + /** + * 요청은 잘 만들어졌지만, 문법 오류로 인하여 따를 수 없습니다 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(HttpClientErrorException.UnprocessableEntity.class) + protected ResponseEntity handleUnprocessableEntityException(HttpClientErrorException.UnprocessableEntity e) { + log.error("handleUnprocessableEntityException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.UNPROCESSABLE_ENTITY, messageSource); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + /** + * 지원하지 않은 HTTP method 호출 할 경우 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + protected ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + log.error("handleHttpRequestMethodNotSupportedException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED, messageSource); + return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED); + } + + /** + * enum type 일치하지 않아 binding 못할 경우 발생 + * 주로 @RequestParam enum으로 binding 못했을 경우 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + protected ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + log.error("handleMethodArgumentTypeMismatchException", e); + final ErrorResponse response = ErrorResponse.of(e, messageSource); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + /** + * 요청한 페이지가 존재하지 않는 경우 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(NotFoundException.class) + protected ResponseEntity handleNotFoundException(NotFoundException e) { + log.error("handleNotFoundException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.NOT_FOUND, messageSource); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + + /** + * Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(AccessDeniedException.class) + protected ResponseEntity handleAccessDeniedException(AccessDeniedException e) { + log.error("handleAccessDeniedException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.ACCESS_DENIED, messageSource); + return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus())); + } + + /** + * 사용자 인증되지 않은 경우 발생 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(HttpClientErrorException.Unauthorized.class) + protected ResponseEntity handleUnauthorizedException(HttpClientErrorException.Unauthorized e) { + log.error("handleUnauthorizedException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.UNAUTHORIZED, messageSource); + return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus())); + } + + /** + * JWT 인증 만료 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(ExpiredJwtException.class) + protected ResponseEntity handleExpiredJwtException(ExpiredJwtException e) { + log.error("handleExpiredJwtException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.JWT_EXPIRED, messageSource); + return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.ACCESS_DENIED.getStatus())); + } + + /** + * 사용자에게 표시할 다양한 메시지를 직접 정의하여 처리하는 Business RuntimeException Handler + * 개발자가 만들어 던지는 런타임 오류를 처리 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(BusinessMessageException.class) + protected ResponseEntity handleBusinessMessageException(BusinessMessageException e) { + log.error("handleBusinessMessageException", e); + final ErrorCode errorCode = e.getErrorCode(); + final String customMessage = e.getCustomMessage(); + final ErrorResponse response = ErrorResponse.of(errorCode, customMessage); + return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus())); + } + + /** + * 개발자 정의 ErrorCode 를 처리하는 Business RuntimeException Handler + * 개발자가 만들어 던지는 런타임 오류를 처리 + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(BusinessException.class) + protected ResponseEntity handleBusinessException(BusinessException e) { + log.error("handleBusinessException", e); + final ErrorCode errorCode = e.getErrorCode(); + final ErrorResponse response = ErrorResponse.of(errorCode, messageSource); + return new ResponseEntity<>(response, HttpStatus.valueOf(errorCode.getStatus())); + } + + /** + * default exception + * + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(Exception.class) + protected ResponseEntity handleException(Exception e) { + log.error("handleException", e); + final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, messageSource); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/interceptor/ApiLogInterceptor.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/interceptor/ApiLogInterceptor.java new file mode 100644 index 0000000..57c3899 --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/interceptor/ApiLogInterceptor.java @@ -0,0 +1,38 @@ +package org.egovframe.cloud.servlet.interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.servlet.service.ApiLogService; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Slf4j +public class ApiLogInterceptor implements HandlerInterceptor { + + private final ApiLogService apiLogService; + + public ApiLogInterceptor(ApiLogService apiLogService) { + this.apiLogService = apiLogService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 접근 로그 입력 + apiLogService.saveApiLog(request); + log.info("[ApiLogInterceptor preHandle] {}, {}, {}", request.getMethod(), request.getRequestURI(), response.getStatus()); + return HandlerInterceptor.super.preHandle(request, response, handler); + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + log.info("[ApiLogInterceptor postHandle] {}, {}, {}", request.getMethod(), request.getRequestURI(), response.getStatus()); + HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + HandlerInterceptor.super.afterCompletion(request, response, handler, ex); + } +} diff --git a/backend/module-common/src/main/java/org/egovframe/cloud/servlet/service/ApiLogService.java b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/service/ApiLogService.java new file mode 100644 index 0000000..4be4c8d --- /dev/null +++ b/backend/module-common/src/main/java/org/egovframe/cloud/servlet/service/ApiLogService.java @@ -0,0 +1,63 @@ +package org.egovframe.cloud.servlet.service; + +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.servlet.domain.log.ApiLog; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.servlet.domain.log.ApiLogRepository; +import org.egovframe.cloud.common.util.LogUtil; +import org.egovframe.cloud.servlet.interceptor.ApiLogInterceptor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletRequest; + +/** + * org.egovframe.cloud.servlet.service.ApiLogService + *

+ * API Log 처리 서비스 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/01 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/01    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@Service +public class ApiLogService extends AbstractService { + + private final ApiLogRepository apiLogRepository; + + public ApiLogService(ApiLogRepository apiLogRepository) { + this.apiLogRepository = apiLogRepository; + } + + /** + * API log 입력 + * LogInterceptor 에서 호출된다 + * + * @param request + * @see ApiLogInterceptor + */ + @Transactional + public void saveApiLog(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + apiLogRepository.save( + ApiLog.builder() + .siteId(LogUtil.getSiteId(request)) + .httpMethod(request.getMethod()) + .requestUrl(request.getRequestURI()) + .userId(authentication.getName()) + .remoteIp(LogUtil.getUserIp()) + .build() + ); + } +}