diff --git a/backend/apigateway/Dockerfile b/backend/apigateway/Dockerfile new file mode 100644 index 0000000..459796a --- /dev/null +++ b/backend/apigateway/Dockerfile @@ -0,0 +1,15 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine + +# config server uri: dockder run --e 로 변경 가능 +ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar apigateway.jar +# application port +EXPOSE 8000 +# 실행 (application-cf.yml 프로필이 기본값) +CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "apigateway.jar"] diff --git a/backend/apigateway/build.gradle b/backend/apigateway/build.gradle new file mode 100644 index 0000000..8731f3f --- /dev/null +++ b/backend/apigateway/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'org.springframework.boot' version '2.4.5' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'org.egovframe.cloud' +version = '0.1' +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.cloud:spring-cloud-starter-gateway' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-config' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'net.logstash.logback:logstash-logback-encoder:6.6' // logstash logback + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + + implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' + implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +test { + useJUnitPlatform() +} diff --git a/backend/apigateway/gradlew b/backend/apigateway/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/apigateway/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/apigateway/gradlew.bat b/backend/apigateway/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/backend/apigateway/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/apigateway/manifest.yml b/backend/apigateway/manifest.yml new file mode 100644 index 0000000..eaa0d95 --- /dev/null +++ b/backend/apigateway/manifest.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: egov-apigateway # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-apigateway # host 명으로 유일해야 함 + path: build/libs/apigateway-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + services: + - egov-discovery-provided-service # discovery service binding + env: + spring_profiles_active: cf + spring_cloud_config_uri: https://egov-config.paas-ta.org + app_name: egov-apigateway # logstash custom app name + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/backend/apigateway/settings.gradle b/backend/apigateway/settings.gradle new file mode 100644 index 0000000..ed34aee --- /dev/null +++ b/backend/apigateway/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'apigateway' diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/ApigatewayApplication.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/ApigatewayApplication.java new file mode 100644 index 0000000..d6d315e --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/ApigatewayApplication.java @@ -0,0 +1,33 @@ +package org.egovframe.cloud.apigateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * org.egovframe.cloud.apigateway.ApigatewayApplication + *

+ * 게이트웨이 어플리케이션 클래스 + * Eureka Client 로 설정했기 때문에 Eureka Server 가 먼저 기동되어야 한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@EnableDiscoveryClient +@SpringBootApplication +public class ApigatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(ApigatewayApplication.class, args); + } + +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/MessageSourceApiController.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/MessageSourceApiController.java new file mode 100644 index 0000000..3689551 --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/MessageSourceApiController.java @@ -0,0 +1,43 @@ +package org.egovframe.cloud.apigateway.api; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Locale; + +/** + * org.egovframe.cloud.apigateway.api.MessageSourceApiController + *

+ * MessageSource 정상 확인을 위한 컨트롤러 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/10 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/10    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class MessageSourceApiController { + private final MessageSource messageSource; + + @GetMapping("/api/v1/messages/{code}/{lang}") + public String getMessage(@PathVariable String code, @PathVariable String lang) { + Locale locale = "en".equals(lang)? Locale.ENGLISH : Locale.KOREAN; + String message = messageSource.getMessage(code, null, locale); + log.info("code={}, lang={}, message={}", code, lang, message); + return message; + } +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/SwaggerResourcesController.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/SwaggerResourcesController.java new file mode 100644 index 0000000..1f46a81 --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/api/SwaggerResourcesController.java @@ -0,0 +1,70 @@ +package org.egovframe.cloud.apigateway.api; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; +import springfox.documentation.swagger.web.*; + +import java.util.Optional; + +/** + * org.egovframe.cloud.apigateway.api.SwaggerResourcesController + * + * Swagger resource 들을 모으는 controller class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/07 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    shinmj       최초 생성
+ * 
+ */ +@RestController +@RequestMapping("/swagger-resources") +public class SwaggerResourcesController { + + @Autowired(required = false) + private SecurityConfiguration securityConfiguration; + + @Autowired(required = false) + private UiConfiguration uiConfiguration; + + private final SwaggerResourcesProvider swaggerResources; + + @Autowired + public SwaggerResourcesController(SwaggerResourcesProvider swaggerResources) { + this.swaggerResources = swaggerResources; + } + + @GetMapping("/configuration/security") + public Mono> securityConfiguration() { + return Mono.just(new ResponseEntity<>( + Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), + HttpStatus.OK + )); + } + + @GetMapping("/configuration/ui") + public Mono> uiConfiguration() { + return Mono.just(new ResponseEntity<>( + Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), + HttpStatus.OK + )); + } + + @GetMapping("") + public Mono swaggerResources() { + return Mono.just(new ResponseEntity( + swaggerResources.get(), HttpStatus.OK + )); + } +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/MessageSourceConfig.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/MessageSourceConfig.java new file mode 100644 index 0000000..b070d64 --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/MessageSourceConfig.java @@ -0,0 +1,64 @@ +package org.egovframe.cloud.apigateway.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.apigateway.config.MessageSourceConfig + *

+ * Spring MessageSource 설정 + * Message Domain 이 있는 portal-service 에서 messages.properties 를 공유 가능한 외부 위치에 생성한다. + * 각 서비스에서 해당 파일을 통해 다국어를 지원하도록 한다. + * module-common.jar 를 포함하지 않는 서비스에서는 이 configuration을 추가해주어야 한다. + * + * @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); + log.info("DB MessageSource location = {}", dbMessages); + 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; + } +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorization.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorization.java new file mode 100644 index 0000000..56e5cbe --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorization.java @@ -0,0 +1,127 @@ +package org.egovframe.cloud.apigateway.config; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.RequestPath; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.access.AuthorizationServiceException; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.authorization.AuthorizationContext; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + + +/** + * org.egovframe.cloud.apigateway.config.ReactiveAuthorization + *

+ * Spring Security 에 의해 요청 url에 대한 사용자 인가 서비스를 수행하는 클래스 + * 요청에 대한 사용자의 권한여부 체크하여 true/false 리턴한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/19 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/19    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Component +public class ReactiveAuthorization implements ReactiveAuthorizationManager { + + @Value("${apigateway.host:http://localhost:8000}") + private String APIGATEWAY_HOST; + + @Value("${token.secret}") + private String TOKEN_SECRET; + + // org.egovframe.cloud.common.config.GlobalConstant 값도 같이 변경해주어야 한다. + public static final String AUTHORIZATION_URI = "/user-service" + "/api/v1/authorizations/check"; + public static final String REFRESH_TOKEN_URI = "/user-service" + "/api/v1/users/token/refresh"; + + /** + * 요청에 대한 사용자의 권한여부 체크하여 true/false 리턴한다 + * 헤더에 토큰이 있으면 유효성을 체크한다. + * + * @param authentication + * @param context + * @return + * @see WebFluxSecurityConfig + */ + @Override + public Mono check(Mono authentication, AuthorizationContext context) { + ServerHttpRequest request = context.getExchange().getRequest(); + RequestPath requestPath = request.getPath(); + HttpMethod httpMethod = request.getMethod(); + + String baseUrl = APIGATEWAY_HOST + AUTHORIZATION_URI + "?httpMethod=" + httpMethod + "&requestPath=" + requestPath; + log.info("baseUrl={}", baseUrl); + + String authorizationHeader = ""; + if (request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION) + && StringUtils.hasLength( + request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0)) + && !"undefined".equals(request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0)) + ) { + try { + authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0); + String jwt = authorizationHeader.replace("Bearer", ""); + String subject = Jwts.parser().setSigningKey(TOKEN_SECRET) + .parseClaimsJws(jwt) + .getBody() + .getSubject(); + + // refresh token 요청 시 토큰 검증만 하고 인가 처리 한다. + if (REFRESH_TOKEN_URI.equals(requestPath + "")) { + return Mono.just(new AuthorizationDecision(true)); + } + if (subject == null || subject.isEmpty()) { + log.error("토큰 인증 오류"); + throw new AuthorizationServiceException("토큰 인증 오류"); + } + } catch (IllegalArgumentException e) { + log.error("토큰 헤더 오류 : {}", e.getMessage()); + throw new AuthorizationServiceException("토큰 인증 오류"); + } catch (ExpiredJwtException e) { + log.error("토큰 유효기간이 만료되었습니다. : {}", e.getMessage()); + throw new AuthorizationServiceException("토큰 유효기간 만료"); + } catch (Exception e) { + log.error("토큰 인증 오류 Exception : {}", e.getMessage()); + throw new AuthorizationServiceException("토큰 인증 오류"); + } + } + + Boolean granted = false; + try { + String token = authorizationHeader; // Variable used in lambda expression should be final or effectively final + Mono body = WebClient.create(baseUrl) + .get() + .headers(httpHeaders -> { + httpHeaders.add(HttpHeaders.AUTHORIZATION, token); + }) + .retrieve().bodyToMono(Boolean.class); + granted = body.block(); + log.info("Security AuthorizationDecision granted={}", granted); + } catch (Exception e) { + log.error("인가 서버에 요청 중 오류 : {}", e.getMessage()); + throw new AuthorizationServiceException("인가 요청시 오류 발생"); + } + + return Mono.just(new AuthorizationDecision(granted)); + } + +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/SwaggerProvider.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/SwaggerProvider.java new file mode 100644 index 0000000..4efcb60 --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/SwaggerProvider.java @@ -0,0 +1,78 @@ +package org.egovframe.cloud.apigateway.config; + +import lombok.AllArgsConstructor; +import org.springframework.cloud.gateway.config.GatewayProperties; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.support.NameUtils; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; +import springfox.documentation.swagger.web.SwaggerResource; +import springfox.documentation.swagger.web.SwaggerResourcesProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * org.egovframe.cloud.apigateway.config.SwaggerProvider + * + * Swagger API Doc aggregator class + * Swagger Resource인 api-docs를 가져오는 provider + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/07 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    shinmj       최초 생성
+ * 
+ */ +@AllArgsConstructor +@Component +@Primary +public class SwaggerProvider implements SwaggerResourcesProvider { + + public static final String API_URL = "/v2/api-docs"; + public static final String WEBFLUX_API_URL = "/v3/api-docs"; + private final RouteLocator routeLocator; + private final GatewayProperties gatewayProperties; + + @Override + public List get() { + List resources = new ArrayList<>(); + List routes = new ArrayList<>(); + + routeLocator.getRoutes().subscribe(route -> routes.add(route.getId())); + + gatewayProperties.getRoutes().stream() + .filter(routeDefinition -> routes.contains(routeDefinition.getId())) + .forEach(routeDefinition -> routeDefinition.getPredicates().stream() + .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName())) + .forEach(predicateDefinition -> + resources.add( + swaggerResource(routeDefinition.getId(), + predicateDefinition. + getArgs(). + get(NameUtils.GENERATED_NAME_PREFIX+"0"). + replace("/**", API_URL)))) + ); + + return resources; + } + + private SwaggerResource swaggerResource(String name, String location) { + SwaggerResource swaggerResource = new SwaggerResource(); + swaggerResource.setName(name); + if (name.contains("reserve")) { + swaggerResource.setLocation(location.replace(API_URL, WEBFLUX_API_URL)); + }else { + swaggerResource.setLocation(location); + } + + swaggerResource.setSwaggerVersion("2.0"); + return swaggerResource; + } +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java new file mode 100644 index 0000000..5a78209 --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/config/WebFluxSecurityConfig.java @@ -0,0 +1,67 @@ +package org.egovframe.cloud.apigateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; +import org.springframework.security.web.server.authorization.AuthorizationContext; + +/** + * org.egovframe.cloud.apigateway.config.WebFluxSecurityConfig + *

+ * Spring Security Config 클래스 + * ReactiveAuthorizationManager 구현체 ReactiveAuthorization 클래스를 통해 인증/인가 처리를 구현한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@EnableWebFluxSecurity // Spring Security 설정들을 활성화시켜 준다 +public class WebFluxSecurityConfig { + + private final static String[] PERMITALL_ANTPATTERNS = { + ReactiveAuthorization.AUTHORIZATION_URI, "/", "/csrf", + "/user-service/login", "/?*-service/api/v1/messages/**", "/api/v1/messages/**", + "/?*-service/actuator/?*", "/actuator/?*", + "/?*-service/v2/api-docs", "/?*-service/v3/api-docs", "**/configuration/*", "/swagger*/**", "/webjars/**" + }; + private final static String USER_JOIN_ANTPATTERNS = "/user-service/api/v1/users"; + + /** + * WebFlux 스프링 시큐리티 설정 + * + * @see ReactiveAuthorization + * @param http + * @param check check(Mono authentication, AuthorizationContext context) + * @return + * @throws Exception + */ + @Bean + public SecurityWebFilterChain configure(ServerHttpSecurity http, ReactiveAuthorizationManager check) throws Exception { + http + .csrf().disable() + .headers().frameOptions().disable() + .and() + .formLogin().disable() + .httpBasic().authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)) // login dialog disabled & 401 HttpStatus return + .and() + .authorizeExchange() + .pathMatchers(PERMITALL_ANTPATTERNS).permitAll() + .pathMatchers(HttpMethod.POST, USER_JOIN_ANTPATTERNS).permitAll() + .anyExchange().access(check); + return http.build(); + } + +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/ReactiveExceptionHandlerConfig.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/ReactiveExceptionHandlerConfig.java new file mode 100644 index 0000000..4df9b1a --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/ReactiveExceptionHandlerConfig.java @@ -0,0 +1,98 @@ +package org.egovframe.cloud.apigateway.exception; + +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.apigateway.exception.dto.ErrorCode; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.web.reactive.function.server.ServerRequest; + +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * org.egovframe.cloud.apigateway.exception.ReactiveExceptionHandlerConfig + *

+ * 에러 발생 시 에러 정보 중 필요한 내용만 반환한다 + * ErrorCode 에서 status, code, message 세 가지 속성을 의존한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/16    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +//@Configuration +public class ReactiveExceptionHandlerConfig { + + private final MessageSource messageSource; + + public ReactiveExceptionHandlerConfig(MessageSource messageSource) { + this.messageSource = messageSource; + } + + /** + * 에러 발생 시 에러 정보 중 필요한 내용만 반환한다 + * + * @return + */ +// @Bean + public ErrorAttributes errorAttributes() { + return new DefaultErrorAttributes() { + @Override + public Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { + Map defaultMap = super.getErrorAttributes(request, options); + Map errorAttributes = new LinkedHashMap<>(); + + int status = (int) defaultMap.get("status"); + ErrorCode errorCode = getErrorCode(status); + String message = messageSource.getMessage(errorCode.getMessage(), null, LocaleContextHolder.getLocale()); + errorAttributes.put("timestamp", LocalDateTime.now()); + errorAttributes.put("message", message); + errorAttributes.put("status", status); + errorAttributes.put("code", errorCode.getCode()); + // API Gateway 에서 FieldError는 처리하지 않는다. + + log.error("getErrorAttributes()={}", defaultMap); + return errorAttributes; + } + }; + } + + /** + * 상태코드로부터 ErrorCode 를 매핑하여 리턴한다. + * + * @param status + * @return + */ + private ErrorCode getErrorCode(int status) { + switch (status) { + case 400: + return ErrorCode.ENTITY_NOT_FOUND; + case 401: + return ErrorCode.UNAUTHORIZED; + case 403: + return ErrorCode.ACCESS_DENIED; + case 404: + return ErrorCode.NOT_FOUND; + case 405: + return ErrorCode.METHOD_NOT_ALLOWED; + case 422: + return ErrorCode.UNPROCESSABLE_ENTITY; + default: + return ErrorCode.INTERNAL_SERVER_ERROR; + } + } +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/dto/ErrorCode.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/dto/ErrorCode.java new file mode 100644 index 0000000..4da3263 --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/exception/dto/ErrorCode.java @@ -0,0 +1,59 @@ +package org.egovframe.cloud.apigateway.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 + ACCESS_DENIED(403, "E005", "err.access.denied"), // Forbidden, Access is Denied + NOT_FOUND(404, "E007", "err.not.found"), // Not found + METHOD_NOT_ALLOWED(405, "E008", "err.method.not.allowed"), // 요청 방법이 서버에 의해 알려졌으나, 사용 불가능한 상태 + UNPROCESSABLE_ENTITY(422, "E009", "err.unprocessable.entity"), // Unprocessable Entity + INTERNAL_SERVER_ERROR(500, "E999", "err.internal.server"), // Server Error + SERVICE_UNAVAILABLE(503, "E010", "err.service.unavailable") // Service Unavailable + ; + + + 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/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java new file mode 100644 index 0000000..62dfd1c --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/GlobalFilter.java @@ -0,0 +1,49 @@ +package org.egovframe.cloud.apigateway.filter; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Slf4j +@Component +public class GlobalFilter extends AbstractGatewayFilterFactory { + + public GlobalFilter() { + super(Config.class); + } + + @Override + public GatewayFilter apply(Config config) { + // Pre filter + return ((exchange, chain) -> { + // Netty 비동기 방식 서버 사용시에는 ServerHttpRequest 를 사용해야 한다. + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); + + if (config.isPreLogger()) { + log.info("[GlobalFilter Start] request ID: {}, method: {}, path: {}", request.getId(), request.getMethod(), request.getPath()); + } + + // Post Filter + // 비동기 방식의 단일값 전달시 Mono 사용(Webflux) + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + if (config.isPostLogger()) { + log.info("[GlobalFilter End ] request ID: {}, method: {}, path: {}, statusCode: {}", request.getId(), request.getMethod(), request.getPath(), response.getStatusCode()); + } + })); + }); + } + + @Data + public static class Config { + // put the configure + private String baseMessage; + private boolean preLogger; + private boolean postLogger; + } +} diff --git a/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/SwaggerHeaderFilter.java b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/SwaggerHeaderFilter.java new file mode 100644 index 0000000..cb6bc9e --- /dev/null +++ b/backend/apigateway/src/main/java/org/egovframe/cloud/apigateway/filter/SwaggerHeaderFilter.java @@ -0,0 +1,49 @@ +package org.egovframe.cloud.apigateway.filter; + +import org.egovframe.cloud.apigateway.config.SwaggerProvider; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; + +/** + * org.egovframe.cloud.apigateway.filter.SwaggerHeaderFilter + * + * Swagger header filter class + * 각 서비스 명을 붙여서 호출 할 수 있도록 filter를 추가 한다. + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/07 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    shinmj       최초 생성
+ * 
+ */ +@Component +public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory { + private static final String HEADER_NAME = "X-Forwarded-Prefix"; + + @Override + public GatewayFilter apply(Object config) { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + String path = request.getURI().getPath(); + if (!StringUtils.endsWithIgnoreCase(path, SwaggerProvider.API_URL)) { + return chain.filter(exchange); + } + + String basePath = path.substring(0, path.lastIndexOf(SwaggerProvider.API_URL)); + ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build(); + ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); + + return chain.filter(newExchange); + }; + } +} diff --git a/backend/apigateway/src/main/resources/application.yml b/backend/apigateway/src/main/resources/application.yml new file mode 100644 index 0000000..342185a --- /dev/null +++ b/backend/apigateway/src/main/resources/application.yml @@ -0,0 +1,67 @@ +server: + port: 8000 + +spring: + application: + name: apigateway + cloud: + gateway: + routes: + - id: user-service + uri: lb://USER-SERVICE + predicates: + - Path=/user-service/** + filters: + - RemoveRequestHeader=Cookie + - RewritePath=/user-service/(?.*), /$\{segment} + - SwaggerHeaderFilter + - id: portal-service + uri: lb://PORTAL-SERVICE + predicates: + - Path=/portal-service/** + filters: + - RewritePath=/portal-service/(?.*), /$\{segment} + - SwaggerHeaderFilter + - id: board-service + uri: lb://BOARD-SERVICE + predicates: + - Path=/board-service/** + filters: + - RewritePath=/board-service/(?.*), /$\{segment} + - SwaggerHeaderFilter + - id: reserve-item-service + uri: lb://RESERVE-ITEM-SERVICE + predicates: + - Path=/reserve-item-service/** + filters: + - RewritePath=/reserve-item-service/(?.*), /$\{segment} + - SwaggerHeaderFilter + - id: reserve-check-service + uri: lb://RESERVE-CHECK-SERVICE + predicates: + - Path=/reserve-check-service/** + filters: + - RewritePath=/reserve-check-service/(?.*), /$\{segment} + - SwaggerHeaderFilter + - id: reserve-request-service + uri: lb://RESERVE-REQUEST-SERVICE + predicates: + - Path=/reserve-request-service/** + filters: + - RewritePath=/reserve-request-service/(?.*), /$\{segment} + - SwaggerHeaderFilter + default-filters: + - name: GlobalFilter + args: + preLogger: true + postLogger: true + discovery: + locator: + enabled: true + +# config server actuator +management: + endpoints: + web: + exposure: + include: refresh, health, beans diff --git a/backend/apigateway/src/main/resources/bootstrap.yml b/backend/apigateway/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..6bf9cc8 --- /dev/null +++ b/backend/apigateway/src/main/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: apigateway diff --git a/backend/apigateway/src/main/resources/logback-spring.xml b/backend/apigateway/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..03aa0f4 --- /dev/null +++ b/backend/apigateway/src/main/resources/logback-spring.xml @@ -0,0 +1,34 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n + + + + + + + + + + + + + + + + + ${destination} + + {"app.name":"${app_name}"} + + + + + + + + + \ No newline at end of file diff --git a/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/ApigatewayApplicationTests.java b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/ApigatewayApplicationTests.java new file mode 100644 index 0000000..a211bac --- /dev/null +++ b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/ApigatewayApplicationTests.java @@ -0,0 +1,13 @@ +package org.egovframe.cloud.apigateway; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApigatewayApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/MessageSourceConfigTest.java b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/MessageSourceConfigTest.java new file mode 100644 index 0000000..b95c6e9 --- /dev/null +++ b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/MessageSourceConfigTest.java @@ -0,0 +1,25 @@ +package org.egovframe.cloud.apigateway.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +class MessageSourceConfigTest { + + @Autowired + TestRestTemplate restTemplate; + + @Test + public void 메세지를_외부위치에서_읽어온다() throws Exception { + // when + String message = restTemplate.getForObject("http://localhost:8000/api/v1/messages/common.login/ko", String.class); + + // then + assertThat(message).isEqualTo("로그인"); + } +} \ No newline at end of file diff --git a/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorizationTest.java b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorizationTest.java new file mode 100644 index 0000000..e2be571 --- /dev/null +++ b/backend/apigateway/src/test/java/org/egovframe/cloud/apigateway/config/ReactiveAuthorizationTest.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.apigateway.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest +class ReactiveAuthorizationTest { + + @Value("${server.port}") + private String PORT; + + @Test + public void API요청시_토큰인증_만료된다() throws Exception { + // given + String baseUrl = "http://localhost:" + PORT; + String notValidToken = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI2NWEwMGY2NS04NDYwLTQ5YWYtOThlYy0wNDI5NzdlNTZmNGIiLCJhdXRob3JpdGllcyI6IlJPTEVfVVNFUiIsImV4cCI6MTYyNjc4MjQ0N30.qiScvtr1m88SHPLpHqcJiklXFyIQ7WBJdiFcdcb2B8YSWC59QcdRRgMtXDGSZnjBgF194W-GRBpHUta6VCkrfQ"; + + // when, then + WebTestClient.bindToServer().baseUrl(baseUrl).defaultHeader(HttpHeaders.AUTHORIZATION, notValidToken).build() + .get() + .uri("/user-service/api/v1/users") + .exchange().expectStatus().isUnauthorized() + ; + } +} \ No newline at end of file diff --git a/backend/board-service/Dockerfile b/backend/board-service/Dockerfile new file mode 100644 index 0000000..bd86ccf --- /dev/null +++ b/backend/board-service/Dockerfile @@ -0,0 +1,15 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine + +# config server uri: dockder run --e 로 변경 가능 +ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar app.jar +# application port +#EXPOSE 8000 +# 실행 (application-cf.yml 프로필이 기본값) +CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "app.jar"] diff --git a/backend/board-service/build.gradle b/backend/board-service/build.gradle new file mode 100644 index 0000000..09f9bf7 --- /dev/null +++ b/backend/board-service/build.gradle @@ -0,0 +1,98 @@ +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' +sourceCompatibility = '1.8' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() + maven { url "https://maven.egovframe.go.kr/maven/" } // egovframe maven 원격 저장소 +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { +// implementation files('../../module-common/build/libs/module-common-0.1.jar') // 공통 모듈, @ComponentScan(basePackages={"org.egovframe.cloud"}) 추가해야 적용된다 + implementation 'org.egovframe.cloud:module-common:0.1' + 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-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-config' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'org.springframework.cloud:spring-cloud-starter-sleuth' + implementation 'org.springframework.cloud:spring-cloud-sleuth-zipkin' + implementation 'net.logstash.logback:logstash-logback-encoder:6.6' // logstash logback + implementation 'mysql:mysql-connector-java' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + // querydsl + implementation 'com.querydsl:querydsl-jpa' + implementation 'com.querydsl:querydsl-sql:4.4.0' + implementation 'com.querydsl:querydsl-sql-spring:4.4.0' + + //messaging + implementation 'org.springframework.cloud:spring-cloud-stream' + implementation 'org.springframework.cloud:spring-cloud-stream-binder-rabbit' + + // swagger + implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' + implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + // lombok + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + testImplementation 'com.h2database:h2' + testImplementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7' // 테스트시에만 출력 + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +test { + useJUnitPlatform() +} + +// 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/board-service/gradlew b/backend/board-service/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/board-service/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/board-service/gradlew.bat b/backend/board-service/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/backend/board-service/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/board-service/manifest.yml b/backend/board-service/manifest.yml new file mode 100644 index 0000000..9897f1d --- /dev/null +++ b/backend/board-service/manifest.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: egov-board-service # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-board-service # host 명으로 유일해야 함 + path: build/libs/board-service-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + services: + - egov-discovery-provided-service # discovery service binding + env: + spring_profiles_active: cf + spring_cloud_config_uri: https://egov-config.paas-ta.org + app_name: egov-board-service # logstash custom app name + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/backend/board-service/settings.gradle b/backend/board-service/settings.gradle new file mode 100644 index 0000000..850e63b --- /dev/null +++ b/backend/board-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'board-service' diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/BoardServiceApplication.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/BoardServiceApplication.java new file mode 100644 index 0000000..5a5fcc6 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/BoardServiceApplication.java @@ -0,0 +1,39 @@ +package org.egovframe.cloud.boardservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; + +/** + * org.egovframe.cloud.boardservice.BoardServiceApplication + *

+ * 게시판 서비스 어플리케이션 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +@ComponentScan(basePackages={"org.egovframe.cloud.common", "org.egovframe.cloud.servlet", "org.egovframe.cloud.boardservice"}) // org.egovframe.cloud.common package 포함하기 위해 +@EntityScan({"org.egovframe.cloud.servlet.domain", "org.egovframe.cloud.boardservice.domain"}) +@SpringBootApplication +public class BoardServiceApplication { + + /** + * 메인 + * + * @param args 매개변수 + */ + public static void main(String[] args) { + SpringApplication.run(BoardServiceApplication.class, args); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/BoardApiController.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/BoardApiController.java new file mode 100644 index 0000000..078cbbf --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/BoardApiController.java @@ -0,0 +1,101 @@ +package org.egovframe.cloud.boardservice.api.board; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardSaveRequestDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardUpdateRequestDto; +import org.egovframe.cloud.boardservice.service.board.BoardService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * org.egovframe.cloud.boardservice.api.board.BoardApiController + *

+ * 게시판 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class BoardApiController { + + /** + * 게시판 서비스 + */ + private final BoardService boardService; + + /** + * 게시판 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시판 목록 응답 DTO + */ + @GetMapping("/api/v1/boards") + public Page findPage(RequestDto requestDto, + @PageableDefault(sort = "board_no", direction = Sort.Direction.DESC) Pageable pageable) { + return boardService.findPage(requestDto, pageable); + } + + /** + * 게시판 단건 조회 + * + * @param boardNo 게시판 번호 + * @return BoardResponseDto 게시판 상세 응답 DTO + */ + @GetMapping("/api/v1/boards/{boardNo}") + public BoardResponseDto findById(@PathVariable Integer boardNo) { + return boardService.findById(boardNo); + } + + /** + * 게시판 등록 + * + * @param requestDto 게시판 등록 요청 DTO + * @return BoardResponseDto 게시판 상세 응답 DTO + */ + @PostMapping("/api/v1/boards") + public BoardResponseDto save(@RequestBody @Valid BoardSaveRequestDto requestDto) { + return boardService.save(requestDto); + } + + /** + * 게시판 수정 + * + * @param boardNo 게시판 번호 + * @param requestDto 게시판 수정 요청 DTO + * @return BoardResponseDto 게시판 상세 응답 DTO + */ + @PutMapping("/api/v1/boards/{boardNo}") + public BoardResponseDto update(@PathVariable Integer boardNo, @RequestBody @Valid BoardUpdateRequestDto requestDto) { + return boardService.update(boardNo, requestDto); + } + + /** + * 게시판 삭제 + * + * @param boardNo 게시판 번호 + */ + @DeleteMapping("/api/v1/boards/{boardNo}") + public void delete(@PathVariable Integer boardNo) { + boardService.delete(boardNo); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardListResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardListResponseDto.java new file mode 100644 index 0000000..164bbdf --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardListResponseDto.java @@ -0,0 +1,93 @@ +package org.egovframe.cloud.boardservice.api.board.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto + *

+ * 게시판 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BoardListResponseDto implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = 115181553377528728L; + + /** + * 게시판 번호 + */ + private Integer boardNo; + + /** + * 게시판 명 + */ + private String boardName; + + /** + * 스킨 유형 코드 + */ + private String skinTypeCode; + + /** + * 스킨 유형 코드 명 + */ + private String skinTypeCodeName; + + /** + * 생성 일시 + */ + private LocalDateTime createdDate; + + /** + * 게시판 목록 응답 DTO 생성자 + * + * @param boardNo 게시판 번호 + * @param boardName 게시판 명 + * @param skinTypeCode 스킨 유형 코드 + */ + @QueryProjection + public BoardListResponseDto(Integer boardNo, String boardName, String skinTypeCode) { + this.boardNo = boardNo; + this.boardName = boardName; + this.skinTypeCode = skinTypeCode; + } + + /** + * 게시판 목록 응답 DTO 생성자 + * + * @param boardNo 게시판 번호 + * @param boardName 게시판 명 + * @param skinTypeCode 스킨 유형 코드 + * @param skinTypeCodeName 스킨 유형 코드 명 + * @param createdDate 생성 일시 + */ + @QueryProjection + public BoardListResponseDto(Integer boardNo, String boardName, String skinTypeCode, String skinTypeCodeName, LocalDateTime createdDate) { + this.boardNo = boardNo; + this.boardName = boardName; + this.skinTypeCode = skinTypeCode; + this.skinTypeCodeName = skinTypeCodeName; + this.createdDate = createdDate; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardResponseDto.java new file mode 100644 index 0000000..a7746b9 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardResponseDto.java @@ -0,0 +1,199 @@ +package org.egovframe.cloud.boardservice.api.board.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto; +import org.egovframe.cloud.boardservice.domain.board.Board; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto + *

+ * 게시판 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BoardResponseDto implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = -7139346671431363426L; + + /** + * 게시판 번호 + */ + private Integer boardNo; + + /** + * 게시판 제목 + */ + private String boardName; + + /** + * 스킨 유형 코드 + */ + private String skinTypeCode; + + /** + * 제목 표시 길이 + */ + private Integer titleDisplayLength; + + /** + * 게시물 표시 수 + */ + private Integer postDisplayCount; + + /** + * 페이지 표시 수 + */ + private Integer pageDisplayCount; + + /** + * 표시 신규 수 + */ + private Integer newDisplayDayCount; + + /** + * 에디터 사용 여부 + */ + private Boolean editorUseAt; + + /** + * 사용자 작성 여부 + */ + private Boolean userWriteAt; + + /** + * 댓글 사용 여부 + */ + private Boolean commentUseAt; + + /** + * 업로드 사용 여부 + */ + private Boolean uploadUseAt; + + /** + * 업로드 제한 수 + */ + private Integer uploadLimitCount; + + /** + * 업로드 제한 크기 + */ + private BigDecimal uploadLimitSize; + + /** + * 게시물 목록 + */ + private List posts; + + /** + * 게시판 상세 응답 DTO 생성자 + * + * @param boardNo 게시판 번호 + * @param boardName 게시판 명 + * @param skinTypeCode 스킨 유형 코드 + * @param titleDisplayLength 제목 표시 길이 + * @param postDisplayCount 게시물 표시 수 + * @param pageDisplayCount 페이지 표시 수 + * @param newDisplayDayCount 신규 표시 일 수 + * @param editorUseAt 에디터 사용 여부 + * @param userWriteAt 사용자 작성 여부 + * @param commentUseAt 댓글 사용 여부 + * @param uploadUseAt 업로드 사용 여부 + * @param uploadLimitCount 업로드 제한 수 + * @param uploadLimitSize 업로드 제한 크기 + */ + @QueryProjection + public BoardResponseDto(Integer boardNo, String boardName, String skinTypeCode, Integer titleDisplayLength, + Integer postDisplayCount, Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt, + Boolean userWriteAt, Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount, + BigDecimal uploadLimitSize) { + this.boardNo = boardNo; + this.boardName = boardName; + this.skinTypeCode = skinTypeCode; + this.titleDisplayLength = titleDisplayLength; + this.postDisplayCount = postDisplayCount; + this.pageDisplayCount = pageDisplayCount; + this.newDisplayDayCount = newDisplayDayCount; + this.editorUseAt = editorUseAt; + this.userWriteAt = userWriteAt; + this.commentUseAt = commentUseAt; + this.uploadUseAt = uploadUseAt; + this.uploadLimitCount = uploadLimitCount; + this.uploadLimitSize = uploadLimitSize; + } + + /** + * 게시판 엔티티를 생성자로 주입 받아서 게시판 상세 응답 DTO 속성 값 세팅 + * + * @param entity 게시판 엔티티 + */ + public BoardResponseDto(Board entity) { + this.boardNo = entity.getBoardNo(); + this.boardName = entity.getBoardName(); + this.skinTypeCode = entity.getSkinTypeCode(); + this.titleDisplayLength = entity.getTitleDisplayLength(); + this.postDisplayCount = entity.getPostDisplayCount(); + this.pageDisplayCount = entity.getPageDisplayCount(); + this.newDisplayDayCount = entity.getNewDisplayDayCount(); + this.editorUseAt = entity.getEditorUseAt(); + this.userWriteAt = entity.getUserWriteAt(); + this.commentUseAt = entity.getCommentUseAt(); + this.uploadUseAt = entity.getUploadUseAt(); + this.uploadLimitCount = entity.getUploadLimitCount(); + this.uploadLimitSize = entity.getUploadLimitSize(); + } + + /** + * 게시판 상세 응답 DTO 속성 값으로 게시판 엔티티 빌더를 사용하여 객체 생성 + * + * @return Board 게시판 엔티티 + */ + public Board toEntity() { + return Board.builder() + .boardNo(boardNo) + .boardName(boardName) + .skinTypeCode(skinTypeCode) + .titleDisplayLength(titleDisplayLength) + .postDisplayCount(postDisplayCount) + .pageDisplayCount(pageDisplayCount) + .newDisplayDayCount(newDisplayDayCount) + .editorUseAt(editorUseAt) + .userWriteAt(userWriteAt) + .commentUseAt(commentUseAt) + .uploadUseAt(uploadUseAt) + .uploadLimitCount(uploadLimitCount) + .uploadLimitSize(uploadLimitSize) + .build(); + } + + /** + * 최근 게시물 목록 세팅 + * + * @param posts 게시물 목록 + */ + public void setNewestPosts(List posts) { + this.posts = posts; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardSaveRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardSaveRequestDto.java new file mode 100644 index 0000000..afe45ab --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardSaveRequestDto.java @@ -0,0 +1,160 @@ +package org.egovframe.cloud.boardservice.api.board.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.board.Board; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.BoardSaveRequestDto + *

+ * 게시판 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BoardSaveRequestDto { + + /** + * 게시판 제목 + */ + @NotBlank(message = "{board.board_name} {err.required}") + private String boardName; + + /** + * 스킨 유형 코드 + */ + @NotBlank(message = "{board.skin_type_code} {err.required}") + private String skinTypeCode; + + /** + * 제목 표시 길이 + */ + @NotNull(message = "{board.title_display_length} {err.required}") + private Integer titleDisplayLength; + + /** + * 게시물 표시 수 + */ + @NotNull(message = "{board.post_display_count} {err.required}") + private Integer postDisplayCount; + + /** + * 페이지 표시 수 + */ + @NotNull(message = "{board.page_display_count} {err.required}") + private Integer pageDisplayCount; + + /** + * 표시 신규 수 + */ + @NotNull(message = "{board.new_display_day_count} {err.required}") + private Integer newDisplayDayCount; + + /** + * 에디터 사용 여부 + */ + @NotNull(message = "{board.editor_use_at} {err.required}") + private Boolean editorUseAt; + + /** + * 사용자 작성 여부 + */ + @NotNull(message = "{board.user_write_at} {err.required}") + private Boolean userWriteAt; + + /** + * 댓글 사용 여부 + */ + @NotNull(message = "{board.comment_use_at} {err.required}") + private Boolean commentUseAt; + + /** + * 업로드 사용 여부 + */ + @NotNull(message = "{board.upload_use_at} {err.required}") + private Boolean uploadUseAt; + + /** + * 업로드 제한 수 + */ + private Integer uploadLimitCount; + + /** + * 업로드 제한 크기 + */ + private BigDecimal uploadLimitSize; + + /** + * 게시판 등록 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param boardName 게시판 명 + * @param skinTypeCode 스킨 유형 코드 + * @param titleDisplayLength 제목 표시 길이 + * @param postDisplayCount 게시물 표시 수 + * @param pageDisplayCount 페이지 표시 수 + * @param newDisplayDayCount 신규 표시 일 수 + * @param editorUseAt 에디터 사용 여부 + * @param userWriteAt 사용자 작성 여부 + * @param commentUseAt 댓글 사용 여부 + * @param uploadUseAt 업로드 사용 여부 + * @param uploadLimitCount 업로드 제한 수 + * @param uploadLimitSize 업로드 제한 크기 + */ + @Builder + public BoardSaveRequestDto(String boardName, String skinTypeCode, Integer titleDisplayLength, Integer postDisplayCount, + Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt, Boolean userWriteAt, + Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount, BigDecimal uploadLimitSize) { + this.boardName = boardName; + this.skinTypeCode = skinTypeCode; + this.titleDisplayLength = titleDisplayLength; + this.postDisplayCount = postDisplayCount; + this.pageDisplayCount = pageDisplayCount; + this.newDisplayDayCount = newDisplayDayCount; + this.editorUseAt = editorUseAt; + this.userWriteAt = userWriteAt; + this.commentUseAt = commentUseAt; + this.uploadUseAt = uploadUseAt; + this.uploadLimitCount = uploadLimitCount; + this.uploadLimitSize = uploadLimitSize; + } + + /** + * 게시판 등록 요청 DTO 속성 값으로 게시판 엔티티 빌더를 사용하여 객체 생성 + * + * @return Board 게시판 엔티티 + */ + public Board toEntity() { + return Board.builder() + .boardName(boardName) + .skinTypeCode(skinTypeCode) + .titleDisplayLength(titleDisplayLength) + .postDisplayCount(postDisplayCount) + .pageDisplayCount(pageDisplayCount) + .newDisplayDayCount(newDisplayDayCount) + .editorUseAt(editorUseAt) + .userWriteAt(userWriteAt) + .commentUseAt(commentUseAt) + .uploadUseAt(uploadUseAt) + .uploadLimitCount(uploadLimitCount) + .uploadLimitSize(uploadLimitSize) + .build(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardUpdateRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardUpdateRequestDto.java new file mode 100644 index 0000000..c75420c --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/board/dto/BoardUpdateRequestDto.java @@ -0,0 +1,160 @@ +package org.egovframe.cloud.boardservice.api.board.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.board.Board; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.BoardUpdateRequestDto + *

+ * 게시판 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BoardUpdateRequestDto { + + /** + * 게시판 제목 + */ + @NotBlank(message = "{board.board_name} {err.required}") + private String boardName; + + /** + * 스킨 유형 코드 + */ + @NotBlank(message = "{board.skin_type_code} {err.required}") + private String skinTypeCode; + + /** + * 제목 표시 길이 + */ + @NotNull(message = "{board.title_display_length} {err.required}") + private Integer titleDisplayLength; + + /** + * 게시물 표시 수 + */ + @NotNull(message = "{board.post_display_count} {err.required}") + private Integer postDisplayCount; + + /** + * 페이지 표시 수 + */ + @NotNull(message = "{board.page_display_count} {err.required}") + private Integer pageDisplayCount; + + /** + * 표시 신규 수 + */ + @NotNull(message = "{board.new_display_day_count} {err.required}") + private Integer newDisplayDayCount; + + /** + * 에디터 사용 여부 + */ + @NotNull(message = "{board.editor_use_at} {err.required}") + private Boolean editorUseAt; + + /** + * 사용자 작성 여부 + */ + @NotNull(message = "{board.user_write_at} {err.required}") + private Boolean userWriteAt; + + /** + * 댓글 사용 여부 + */ + @NotNull(message = "{board.comment_use_at} {err.required}") + private Boolean commentUseAt; + + /** + * 업로드 사용 여부 + */ + @NotNull(message = "{board.upload_use_at} {err.required}") + private Boolean uploadUseAt; + + /** + * 업로드 제한 수 + */ + private Integer uploadLimitCount; + + /** + * 업로드 제한 크기 + */ + private BigDecimal uploadLimitSize; + + /** + * 게시판 등록 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param boardName 게시판 명 + * @param skinTypeCode 스킨 유형 코드 + * @param titleDisplayLength 제목 표시 길이 + * @param postDisplayCount 게시물 표시 수 + * @param pageDisplayCount 페이지 표시 수 + * @param newDisplayDayCount 신규 표시 일 수 + * @param editorUseAt 에디터 사용 여부 + * @param userWriteAt 사용자 작성 여부 + * @param commentUseAt 댓글 사용 여부 + * @param uploadUseAt 업로드 사용 여부 + * @param uploadLimitCount 업로드 제한 수 + * @param uploadLimitSize 업로드 제한 크기 + */ + @Builder + public BoardUpdateRequestDto(String boardName, String skinTypeCode, Integer titleDisplayLength, Integer postDisplayCount, + Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt, Boolean userWriteAt, + Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount, BigDecimal uploadLimitSize) { + this.boardName = boardName; + this.skinTypeCode = skinTypeCode; + this.titleDisplayLength = titleDisplayLength; + this.postDisplayCount = postDisplayCount; + this.pageDisplayCount = pageDisplayCount; + this.newDisplayDayCount = newDisplayDayCount; + this.editorUseAt = editorUseAt; + this.userWriteAt = userWriteAt; + this.commentUseAt = commentUseAt; + this.uploadUseAt = uploadUseAt; + this.uploadLimitCount = uploadLimitCount; + this.uploadLimitSize = uploadLimitSize; + } + + /** + * 게시판 등록 요청 DTO 속성 값으로 게시판 엔티티 빌더를 사용하여 객체 생성 + * + * @return Board 게시판 엔티티 + */ + public Board toEntity() { + return Board.builder() + .boardName(boardName) + .skinTypeCode(skinTypeCode) + .titleDisplayLength(titleDisplayLength) + .postDisplayCount(postDisplayCount) + .pageDisplayCount(pageDisplayCount) + .newDisplayDayCount(newDisplayDayCount) + .editorUseAt(editorUseAt) + .userWriteAt(userWriteAt) + .commentUseAt(commentUseAt) + .uploadUseAt(uploadUseAt) + .uploadLimitCount(uploadLimitCount) + .uploadLimitSize(uploadLimitSize) + .build(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/CommentApiController.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/CommentApiController.java new file mode 100644 index 0000000..6fc89f8 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/CommentApiController.java @@ -0,0 +1,150 @@ +package org.egovframe.cloud.boardservice.api.comment; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentResponseDto; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentSaveRequestDto; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentUpdateRequestDto; +import org.egovframe.cloud.boardservice.service.comment.CommentService; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.Map; + +/** + * org.egovframe.cloud.commentservice.api.comment.CommentApiController + *

+ * 댓글 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class CommentApiController { + + /** + * 댓글 서비스 + */ + private final CommentService commentService; + + /** + * 게시글의 전체 댓글 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Map 페이지 댓글 목록 응답 DTO + */ + @GetMapping("/api/v1/comments/total/{boardNo}/{postsNo}") + public Map findTotal(@PathVariable Integer boardNo, @PathVariable Integer postsNo) { + return commentService.findAll(boardNo, postsNo, null); + } + + /** + * 게시글의 전체 미삭제 댓글 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Map 페이지 댓글 목록 응답 DTO + */ + @GetMapping("/api/v1/comments/all/{boardNo}/{postsNo}") + public Map findAll(@PathVariable Integer boardNo, @PathVariable Integer postsNo) { + return commentService.findAll(boardNo, postsNo, 0); + } + + /** + * 게시글의 댓글 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param pageable 페이지 정보 + * @return Map 페이지 댓글 목록 응답 DTO + */ + @GetMapping("/api/v1/comments/{boardNo}/{postsNo}") + public Map findPage(@PathVariable Integer boardNo, @PathVariable Integer postsNo, Pageable pageable) { + return commentService.findPage(boardNo, postsNo, null, pageable); + } + + /** + * 게시글의 미삭제 댓글 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param pageable 페이지 정보 + * @return Map 페이지 댓글 목록 응답 DTO + */ + @GetMapping("/api/v1/comments/list/{boardNo}/{postsNo}") + public Map findListPage(@PathVariable Integer boardNo, @PathVariable Integer postsNo, Pageable pageable) { + return commentService.findPage(boardNo, postsNo, 0, pageable); + } + + /** + * 댓글 등록 + * + * @param requestDto 댓글 등록 요청 DTO + */ + @PostMapping("/api/v1/comments") + public CommentResponseDto save(@RequestBody @Valid CommentSaveRequestDto requestDto) { + return commentService.save(requestDto); + } + + /** + * 댓글 수정(작성자 체크) + * + * @param requestDto 게시물 수정 요청 DTO + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + @PutMapping("/api/v1/comments/update") + public CommentResponseDto updateByCreator(@RequestBody @Valid CommentUpdateRequestDto requestDto) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + return commentService.update(requestDto, userId); + } + + /** + * 댓글 삭제(작성자 체크) + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + */ + @DeleteMapping("/api/v1/comments/delete/{boardNo}/{postsNo}/{commentNo}") + public void deleteByCreator(@PathVariable Integer boardNo, @PathVariable Integer postsNo, @PathVariable Integer commentNo) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + commentService.delete(boardNo, postsNo, commentNo, userId); + } + + /** + * 댓글 수정 + * + * @param requestDto 댓글 수정 요청 DTO + */ + @PutMapping("/api/v1/comments") + public CommentResponseDto update(@RequestBody @Valid CommentUpdateRequestDto requestDto) { + return commentService.update(requestDto); + } + + /** + * 댓글 삭제 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + */ + @DeleteMapping("/api/v1/comments/{boardNo}/{postsNo}/{commentNo}") + public void delete(@PathVariable Integer boardNo, @PathVariable Integer postsNo, @PathVariable Integer commentNo) { + commentService.delete(boardNo, postsNo, commentNo); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentListResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentListResponseDto.java new file mode 100644 index 0000000..2bf301f --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentListResponseDto.java @@ -0,0 +1,130 @@ +package org.egovframe.cloud.boardservice.api.comment.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.boardservice.api.comment.dto.CommentListResponseDto + *

+ * 댓글 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CommentListResponseDto implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = -8163130888886378482L; + + /** + * 게시판 번호 + */ + private Integer boardNo; + + /** + * 게시물 번호 + */ + private Integer postsNo; + + /** + * 댓글 번호 + */ + private Integer commentNo; + + /** + * 댓글 내용 + */ + private String commentContent; + + /** + * 그룹 번호 + */ + private Integer groupNo; + + /** + * 부모 댓글 번호 + */ + private Integer parentCommentNo; + + /** + * 깊이 순서 + */ + private Integer depthSeq; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * 삭제 여부 + */ + private Integer deleteAt; + + /** + * 생성자 id + */ + private String createdBy; + + /** + * 생성자 명 + */ + private String createdName; + + /** + * 생성 일시 + */ + private LocalDateTime createdDate; + + /** + * 댓글 목록 응답 DTO 생성자 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + * @param commentContent 댓글 내용 + * @param parentCommentNo 부모 댓글 번호 + * @param depthSeq 깊이 순서 + * @param sortSeq 정렬 순서 + * @param deleteAt 삭제 여부 + * @param createdBy 생성자 id + * @param createdName 생성자 명 + * @param createdDate 생성 일시 + */ + @QueryProjection + public CommentListResponseDto(Integer boardNo, Integer postsNo, Integer commentNo, + String commentContent, Integer groupNo, Integer parentCommentNo, + Integer depthSeq, Integer sortSeq, Integer deleteAt, + String createdBy, String createdName, LocalDateTime createdDate) { + this.boardNo = boardNo; + this.postsNo = postsNo; + this.commentNo = commentNo; + this.commentContent = commentContent; + this.groupNo = groupNo; + this.parentCommentNo = parentCommentNo; + this.depthSeq = depthSeq; + this.sortSeq = sortSeq; + this.deleteAt = deleteAt; + this.createdBy = createdBy; + this.createdName = createdName; + this.createdDate = createdDate; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentResponseDto.java new file mode 100644 index 0000000..d06ab66 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentResponseDto.java @@ -0,0 +1,116 @@ +package org.egovframe.cloud.boardservice.api.comment.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.comment.Comment; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.boardservice.api.comment.dto.CommentResponseDto + * + * 댓글 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/11 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/11    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CommentResponseDto implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = -3087424580328463654L; + + /** + * 게시판 번호 + */ + private Integer boardNo; + + /** + * 게시물 번호 + */ + private Integer postsNo; + + /** + * 댓글 번호 + */ + private Integer commentNo; + + /** + * 댓글 내용 + */ + private String commentContent; + + /** + * 그룹 번호 + */ + private Integer groupNo; + + /** + * 부모 댓글 번호 + */ + private Integer parentCommentNo; + + /** + * 깊이 순서 + */ + private Integer depthSeq; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * 삭제 여부 + */ + private Integer deleteAt; + + /** + * 생성자 id + */ + private String createdBy; + + /** + * 생성자 명 + */ + private String createdName; + + /** + * 생성 일시 + */ + private LocalDateTime createdDate; + + /** + * 댓글 엔티티를 생성자로 주입 받아서 댓글 상세 응답 DTO 속성 값 세팅 + * + * @param entity 댓글 엔티티 + */ + public CommentResponseDto(Comment entity) { + this.postsNo = entity.getCommentId().getPostsId().getPostsNo(); + this.boardNo = entity.getCommentId().getPostsId().getBoardNo(); + this.commentNo = entity.getCommentId().getCommentNo(); + this.commentContent = entity.getCommentContent(); + this.groupNo = entity.getGroupNo(); + this.parentCommentNo = entity.getParentCommentNo(); + this.depthSeq = entity.getDepthSeq(); + this.sortSeq = entity.getSortSeq(); + this.deleteAt = entity.getDeleteAt(); + this.createdBy = entity.getCreatedBy(); + this.createdName = entity.getCreator() != null ? entity.getCreator().getUserName() : null; + this.createdDate = entity.getCreatedDate(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentSaveRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentSaveRequestDto.java new file mode 100644 index 0000000..31b469f --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentSaveRequestDto.java @@ -0,0 +1,137 @@ +package org.egovframe.cloud.boardservice.api.comment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.comment.Comment; +import org.egovframe.cloud.boardservice.domain.comment.CommentId; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.boardservice.api.comment.dto.CommentSaveRequestDto + *

+ * 댓글 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CommentSaveRequestDto { + + /** + * 게시판 번호 + */ + @NotNull(message = "{board.board_no} {err.required}") + private Integer boardNo; + + /** + * 게시물 번호 + */ + @NotNull(message = "{posts.posts_no} {err.required}") + private Integer postsNo; + + /** + * 댓글 내용 + */ + @NotBlank(message = "{comment.comment_content} {err.required}") + private String commentContent; + + /** + * 그룹 번호 + */ + private Integer groupNo; + + /** + * 부모 댓글 번호 + */ + private Integer parentCommentNo; + + /** + * 깊이 순서 + */ + private Integer depthSeq; + + /** + * 댓글 등록 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentContent 댓글 내용 + * @param groupNo 그룹 번호 + * @param parentCommentNo 부모 댓글 번호 + * @param depthSeq 깊이 순서 + */ + @Builder + public CommentSaveRequestDto(Integer boardNo, Integer postsNo, String commentContent, Integer groupNo, Integer parentCommentNo, Integer depthSeq) { + this.boardNo = boardNo; + this.postsNo = postsNo; + this.commentContent = commentContent; + this.groupNo = groupNo; + this.parentCommentNo = parentCommentNo; + this.depthSeq = depthSeq; + } + + /** + * 댓글 등록 요청 DTO 속성 값으로 댓글 엔티티 빌더를 사용하여 객체 생성 + * + * @param commentNo 댓글 번호 + * @param sortSeq 정렬 순서 + * @return Comment 댓글 엔티티 + */ + public Comment toEntity(Integer commentNo, Integer sortSeq) { + return Comment.builder() + .commentId(CommentId.builder() + .postsId(PostsId.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .build()) + .commentNo(commentNo) + .build()) + .commentContent(commentContent) + .groupNo(groupNo) + .parentCommentNo(parentCommentNo) + .depthSeq(depthSeq) + .sortSeq(sortSeq) + .build(); + } + + /** + * 댓글 등록 요청 DTO 속성 값으로 댓글 엔티티 빌더를 사용하여 객체 생성 + * + * @param posts 게시물 엔티티 + * @param commentNo 댓글 번호 + * @param groupNo 그룹 번호 + * @param sortSeq 정렬 순서 + * @return Comment 댓글 엔티티 + */ + public Comment toEntity(Posts posts, Integer commentNo, Integer groupNo, Integer sortSeq) { + return Comment.builder() + .posts(posts) + .commentId(CommentId.builder() + .postsId(posts.getPostsId()) + .commentNo(commentNo) + .build()) + .commentContent(commentContent) + .groupNo(groupNo) + .parentCommentNo(parentCommentNo) + .depthSeq(depthSeq) + .sortSeq(sortSeq) + .build(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentUpdateRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentUpdateRequestDto.java new file mode 100644 index 0000000..eaf0e34 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/comment/dto/CommentUpdateRequestDto.java @@ -0,0 +1,90 @@ +package org.egovframe.cloud.boardservice.api.comment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.comment.Comment; +import org.egovframe.cloud.boardservice.domain.comment.CommentId; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.boardservice.api.comment.dto.CommentSaveRequestDto + * + * 댓글 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CommentUpdateRequestDto { + + /** + * 게시판 번호 + */ + @NotNull(message = "{board.board_no} {err.required}") + private Integer boardNo; + + /** + * 게시물 번호 + */ + @NotNull(message = "{posts.posts_no} {err.required}") + private Integer postsNo; + + /** + * 댓글 번호 + */ + @NotNull(message = "{comment.comment_no} {err.required}") + private Integer commentNo; + + /** + * 댓글 내용 + */ + @NotBlank(message = "{comment.comment_content} {err.required}") + private String commentContent; + + /** + * 댓글 수정 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param commentContent 댓글 내용 + */ + @Builder + public CommentUpdateRequestDto(Integer boardNo, Integer postsNo, Integer commentNo, String commentContent) { + this.boardNo = boardNo; + this.postsNo = postsNo; + this.commentNo = commentNo; + this.commentContent = commentContent; + } + + /** + * 댓글 수정 요청 DTO 속성 값으로 댓글 엔티티 빌더를 사용하여 객체 생성 + * + * @return Comment 댓글 엔티티 + */ + public Comment toEntity() { + return Comment.builder() + .commentId(CommentId.builder() + .postsId(PostsId.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .build()) + .commentNo(commentNo) + .build()) + .commentContent(commentContent) + .build(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/PostsApiController.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/PostsApiController.java new file mode 100644 index 0000000..01ffb0b --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/PostsApiController.java @@ -0,0 +1,229 @@ +package org.egovframe.cloud.boardservice.api.posts; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.*; +import org.egovframe.cloud.boardservice.service.posts.PostsService; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.util.LogUtil; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.SortDefault; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +/** + * org.egovframe.cloud.postservice.api.posts.PostsApiController + *

+ * 게시물 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class PostsApiController { + + /** + * 게시물 서비스 + */ + private final PostsService postsService; + + /** + * 게시물(삭제 포함) 페이지 목록 조회 + * + * @param boardNo 게시판 번호 + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시물 목록 응답 DTO + */ + @GetMapping("/api/v1/posts/{boardNo}") + public Page findPage(@PathVariable Integer boardNo, + RequestDto requestDto, + @SortDefault.SortDefaults({ + @SortDefault(sort = "notice_at", direction = Sort.Direction.DESC), + @SortDefault(sort = "board_no", direction = Sort.Direction.ASC), + @SortDefault(sort = "posts_no", direction = Sort.Direction.DESC) + }) Pageable pageable) { + return postsService.findPage(boardNo, null, requestDto, pageable); + } + + /** + * 게시물(삭제 제외) 페이지 목록 조회 + * + * @param boardNo 게시판 번호 + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시물 목록 응답 DTO + */ + @GetMapping("/api/v1/posts/list/{boardNo}") + public Page findListPage(@PathVariable Integer boardNo, + RequestDto requestDto, + @SortDefault.SortDefaults({ + @SortDefault(sort = "notice_at", direction = Sort.Direction.DESC), + @SortDefault(sort = "board_no", direction = Sort.Direction.ASC), + @SortDefault(sort = "posts_no", direction = Sort.Direction.DESC) + }) Pageable pageable) { + return postsService.findPage(boardNo, 0, requestDto, pageable); + } + + /** + * 최근 게시물이 포함된 게시판 목록 조회 + * + * @param boardNos 게시판 번호 목록 + * @param postsCount 게시물 수 + * @return Map 최근 게시물이 포함된 게시판 상세 응답 DTO Map + */ + @GetMapping("/api/v1/posts/newest/{boardNos}/{postsCount}") + public Map findNewest(@PathVariable List boardNos, @PathVariable Integer postsCount) { + return postsService.findNewest(boardNos, postsCount); + } + + /** + * 게시물(삭제 포함) 단건 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + @GetMapping("/api/v1/posts/{boardNo}/{postsNo}") + public PostsResponseDto findById(@PathVariable Integer boardNo, @PathVariable Integer postsNo) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + userId = "anonymousUser".equals(userId) ? null : userId; + + return postsService.findById(boardNo, postsNo, null, userId, LogUtil.getUserIp()); + } + + /** + * 게시물(삭제 제외) 단건 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + @GetMapping("/api/v1/posts/view/{boardNo}/{postsNo}") + public PostsResponseDto findViewById(@PathVariable Integer boardNo, @PathVariable Integer postsNo, RequestDto requestDto) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + userId = "anonymousUser".equals(userId) ? null : userId; + final Integer deleteAt = 0; // 미삭제 게시물만 조회 + + return postsService.findById(boardNo, postsNo, deleteAt, userId, LogUtil.getUserIp(), requestDto); + } + + /** + * 게시물 등록(작성자 체크) + * + * @param boardNo 게시판 번호 + * @param requestDto 게시물 등록 요청 DTO + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + @PostMapping("/api/v1/posts/save/{boardNo}") + public PostsResponseDto saveByCreator(@PathVariable Integer boardNo, @RequestBody @Valid PostsSimpleSaveRequestDto requestDto) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + return postsService.save(boardNo, requestDto, userId); + } + + /** + * 게시물 수정(작성자 체크) + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param requestDto 게시물 수정 요청 DTO + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + @PutMapping("/api/v1/posts/update/{boardNo}/{postsNo}") + public PostsResponseDto updateByCreator(@PathVariable Integer boardNo, @PathVariable Integer postsNo, @RequestBody @Valid PostsSimpleSaveRequestDto requestDto) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + return postsService.update(boardNo, postsNo, requestDto, userId); + } + + /** + * 게시물 삭제(작성자 체크) + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + */ + @DeleteMapping("/api/v1/posts/remove/{boardNo}/{postsNo}") + public void deleteByCreator(@PathVariable Integer boardNo, @PathVariable Integer postsNo) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + postsService.remove(boardNo, postsNo, userId); + } + + /** + * 게시물 등록 + * + * @param boardNo 게시판 번호 + * @param requestDto 게시물 등록 요청 DTO + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + @PostMapping("/api/v1/posts/{boardNo}") + public PostsResponseDto save(@PathVariable Integer boardNo, @RequestBody @Valid PostsSaveRequestDto requestDto) { + return postsService.save(boardNo, requestDto); + } + + /** + * 게시물 수정 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param requestDto 게시물 수정 요청 DTO + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + @PutMapping("/api/v1/posts/{boardNo}/{postsNo}") + public PostsResponseDto update(@PathVariable Integer boardNo, @PathVariable Integer postsNo, @RequestBody @Valid PostsUpdateRequestDto requestDto) { + return postsService.update(boardNo, postsNo, requestDto); + } + + /** + * 게시물 다건 삭제 + * + * @param requestDtoList 게시물 삭제 요청 DTO List + * @return Long 삭제 건수 + */ + @PutMapping("/api/v1/posts/remove") + public Long remove(@RequestBody @Valid List requestDtoList) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + return postsService.remove(requestDtoList, userId); + } + + /** + * 게시물 다건 복원 + * + * @param requestDtoList 게시물 삭제 요청 DTO List + * @return Long 복원 건수 + */ + @PutMapping("/api/v1/posts/restore") + public Long restore(@RequestBody @Valid List requestDtoList) { + String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + return postsService.restore(requestDtoList, userId); + } + + /** + * 게시물 다건 완전 삭제 + * + * @param requestDtoList 게시물 삭제 요청 DTO List + */ + @PutMapping("/api/v1/posts/delete") + public void delete(@RequestBody @Valid List requestDtoList) { + postsService.delete(requestDtoList); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsDeleteRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsDeleteRequestDto.java new file mode 100644 index 0000000..5b21800 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsDeleteRequestDto.java @@ -0,0 +1,55 @@ +package org.egovframe.cloud.boardservice.api.posts.dto; + +import lombok.Getter; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.boardservice.api.posts.dto.PostsDeleteRequestDto + *

+ * 게시물 삭제 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/29 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/29    jooho       최초 생성
+ * 
+ */ +@Getter +public class PostsDeleteRequestDto { + + /** + * 게시판 번호 + */ + @NotBlank(message = "{board.board_no} {err.required}") + private Integer boardNo; + + /** + * 게시물 번호 + */ + @NotBlank(message = "{posts.posts_no} {err.required}") + private Integer postsNo; + + /** + * 게시물 삭제 요청 DTO 속성 값으로 게시물 엔티티 빌더를 사용하여 객체 생성 + * + * @return Posts 게시물 엔티티 + */ + public Posts toEntity() { + return Posts.builder() + .postsId(PostsId.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .build()) + .build(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsListResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsListResponseDto.java new file mode 100644 index 0000000..be7e416 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsListResponseDto.java @@ -0,0 +1,137 @@ +package org.egovframe.cloud.boardservice.api.posts.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.PostsListResponseDto + *

+ * 게시물 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PostsListResponseDto implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = 3316086575500238046L; + + /** + * 게시판 번호 + */ + private Integer boardNo; + + /** + * 게시물 번호 + */ + private Integer postsNo; + + /** + * 게시물 제목 + */ + private String postsTitle; + + /** + * 게시물 내용 + */ + private String postsContent; + + /** + * 게시물 답변 내용 + */ + private String postsAnswerContent; + + /** + * 조회 수 + */ + private Integer readCount; + + /** + * 공지 여부 + */ + private Boolean noticeAt; + + /** + * 삭제 여부 + */ + private Integer deleteAt; + + /** + * 생성자 id + */ + private String createdBy; + + /** + * 생성자 명 + */ + private String createdName; + + /** + * 생성 일시 + */ + private LocalDateTime createdDate; + + /** + * 신규 여부 + */ + private Boolean isNew; + + /** + * 댓글 수 + */ + private Long commentCount; + + /** + * 게시물 목록 응답 DTO 생성자 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param postsAnswerContent 게시물 답변 내용 + * @param readCount 조회 수 + * @param noticeAt 공지 여부 + * @param deleteAt 삭제 여부 + * @param createdBy 생성자 id + * @param createdName 생성자 명 + * @param createdDate 생성 일시 + * @param commentCount 댓글 수 + */ + @QueryProjection + public PostsListResponseDto(Integer boardNo, Integer postsNo, String postsTitle, String postsContent, + String postsAnswerContent, Integer readCount, Boolean noticeAt, Integer deleteAt, + String createdBy, String createdName, LocalDateTime createdDate, Integer newDisplayDayCount, + Long commentCount) { + this.boardNo = boardNo; + this.postsNo = postsNo; + this.postsTitle = postsTitle; + this.postsContent = postsContent; + this.postsAnswerContent = postsAnswerContent; + this.readCount = readCount; + this.noticeAt = noticeAt; + this.deleteAt = deleteAt; + this.createdBy = createdBy; + this.createdName = createdName; + this.createdDate = createdDate; + this.isNew = createdDate.plusDays(newDisplayDayCount).compareTo(LocalDateTime.now()) >= 0; + this.commentCount = commentCount; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsResponseDto.java new file mode 100644 index 0000000..90facef --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsResponseDto.java @@ -0,0 +1,215 @@ +package org.egovframe.cloud.boardservice.api.posts.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.boardservice.domain.posts.Posts; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.PostsResponseDto + *

+ * 게시물 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PostsResponseDto implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = 8644170429040511387L; + + /** + * 게시판 번호 + */ + private Integer boardNo; + + /** + * 게시물 번호 + */ + private Integer postsNo; + + /** + * 게시물 제목 + */ + private String postsTitle; + + /** + * 게시물 내용 + */ + private String postsContent; + + /** + * 게시물 답변 내용 + */ + private String postsAnswerContent; + + /** + * 첨부파일 코드 + */ + private String attachmentCode; + + /** + * 조회 수 + */ + private Integer readCount; + + /** + * 공지 여부 + */ + private Boolean noticeAt; + + /** + * 삭제 여부 + */ + private Integer deleteAt; + + /** + * 생성자 + */ + private String createdBy; + + /** + * 생성자 명 + */ + private String createdName; + + /** + * 생성 일시 + */ + private LocalDateTime createdDate; + + /** + * 게시판 응답 DTO + */ + private BoardResponseDto board; + + /** + * 신규 여부 + */ + private Boolean isNew; + + /** + * 유저 게시물 조회 수 + */ + private Long userPostsReadCount; + + /** + * 이전 게시물 응답 DTO List + */ + private List prevPosts; + + /** + * 다음 게시물 응답 DTO List + */ + private List nextPosts; + + /** + * 게시물 상세 응답 DTO 생성자 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param postsAnswerContent 게시물 답변 내용 + * @param attachmentCode 첨부파일 코드 + * @param readCount 조회 수 + * @param noticeAt 공지 여부 + * @param deleteAt 삭제 여부 + * @param createdBy 생성자 id + * @param createdName 생성자 명 + * @param createdDate 생성 일시 + * @param board 게시판 + * @param userPostsReadCount 유저 게시물 조회 수 + */ + @QueryProjection + public PostsResponseDto(Integer boardNo, Integer postsNo, String postsTitle, String postsContent, + String postsAnswerContent, String attachmentCode, Integer readCount, Boolean noticeAt, + Integer deleteAt, String createdBy, String createdName, LocalDateTime createdDate, + BoardResponseDto board, Long userPostsReadCount) { + this.boardNo = boardNo; + this.postsNo = postsNo; + this.postsTitle = postsTitle; + this.postsContent = postsContent; + this.postsAnswerContent = postsAnswerContent; + this.attachmentCode = attachmentCode; + this.readCount = readCount; + this.noticeAt = noticeAt; + this.deleteAt = deleteAt; + this.createdBy = createdBy; + this.createdName = createdName; + this.createdDate = createdDate; + this.board = board; + if (this.board.getNewDisplayDayCount() != null && this.board.getNewDisplayDayCount() > 0) { + this.isNew = createdDate.plusDays(this.board.getNewDisplayDayCount()).compareTo(LocalDateTime.now()) <= 0; + } else { + this.isNew = false; + } + this.userPostsReadCount = userPostsReadCount; + } + + /** + * 게시물 엔티티를 생성자로 주입 받아서 게시물 상세 응답 DTO 속성 값 세팅 + * + * @param entity 게시물 엔티티 + */ + public PostsResponseDto(Posts entity) { + this.postsNo = entity.getPostsId().getPostsNo(); + this.boardNo = entity.getPostsId().getBoardNo(); + this.postsTitle = entity.getPostsTitle(); + this.postsContent = entity.getPostsContent(); + this.postsAnswerContent = entity.getPostsAnswerContent(); + this.attachmentCode = entity.getAttachmentCode(); + this.readCount = entity.getReadCount(); + this.noticeAt = entity.getNoticeAt(); + this.deleteAt = entity.getDeleteAt(); + this.createdBy = entity.getCreatedBy(); + this.createdName = entity.getCreator() != null ? entity.getCreator().getUserName() : null; + this.createdDate = entity.getCreatedDate(); + this.board = new BoardResponseDto(entity.getBoard()); + if (this.board.getNewDisplayDayCount() != null) { + this.isNew = createdDate.plusDays(this.board.getNewDisplayDayCount()).compareTo(LocalDateTime.now()) <= 0; + } else { + this.isNew = false; + } + } + + /** + * 조회 수 증가 + */ + public void increaseReadCount() { + this.readCount = this.readCount + 1; + } + + /** + * 이전 게시물 + */ + public void setPrevPosts(List prevPosts) { + this.prevPosts = prevPosts; + } + + /** + * 다음 게시물 + */ + public void setNextPosts(List nextPosts) { + this.nextPosts = nextPosts; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSaveRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSaveRequestDto.java new file mode 100644 index 0000000..84ca615 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSaveRequestDto.java @@ -0,0 +1,104 @@ +package org.egovframe.cloud.boardservice.api.posts.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.board.Board; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.PostsSaveRequestDto + *

+ * 게시물 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PostsSaveRequestDto { + + /** + * 게시물 제목 + */ + @NotBlank(message = "{posts.posts_title} {err.required}") + private String postsTitle; + + /** + * 게시물 내용 + */ + @NotBlank(message = "{posts.posts_content} {err.required}") + private String postsContent; + + /** + * 게시물 답변 내용 + */ + private String postsAnswerContent; + + /** + * 첨부파일 코드 + */ + private String attachmentCode; + + /** + * 공지 여부 + */ + @NotNull(message = "{posts.notice_at} {err.required}") + private Boolean noticeAt; + + /** + * 게시물 등록 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param attachmentCode 첨부파일 코드 + * @param noticeAt 공지 여부 + */ + @Builder + public PostsSaveRequestDto(String postsTitle, String postsContent, String postsAnswerContent, String attachmentCode, Boolean noticeAt) { + this.postsTitle = postsTitle; + this.postsContent = postsContent; + this.postsAnswerContent = postsAnswerContent; + this.attachmentCode = attachmentCode; + this.noticeAt = noticeAt; + } + + /** + * 게시물 등록 요청 DTO 속성 값으로 게시물 엔티티 빌더를 사용하여 객체 생성 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Posts 게시물 엔티티 + */ + public Posts toEntity(Integer boardNo, Integer postsNo) { + return Posts.builder() + .board(Board.builder() + .boardNo(boardNo) + .build()) + .postsId(PostsId.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .build()) + .postsTitle(postsTitle) + .postsContent(postsContent) + .postsAnswerContent(postsAnswerContent) + .attachmentCode(attachmentCode) + .noticeAt(noticeAt) + .build(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleResponseDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleResponseDto.java new file mode 100644 index 0000000..f56e986 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleResponseDto.java @@ -0,0 +1,104 @@ +package org.egovframe.cloud.boardservice.api.posts.dto; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; + +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto + *

+ * 게시물 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/03 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/03    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PostsSimpleResponseDto implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = 6916914915364711614L; + + /** + * 게시판 번호 + */ + private Integer boardNo; + + /** + * 게시물 번호 + */ + private Integer postsNo; + + /** + * 게시물 제목 + */ + private String postsTitle; + + /** + * 게시물 내용 + */ + private String postsContent; + + /** + * 생성 일시 + */ + private LocalDateTime createdDate; + + /** + * 신규 여부 + */ + private Boolean isNew; + + /** + * 게시물 응답 DTO 생성자 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param createdDate 생성 일시 + */ + @QueryProjection + public PostsSimpleResponseDto(Integer boardNo, Integer postsNo, + String postsTitle, String postsContent, LocalDateTime createdDate) { + this.boardNo = boardNo; + this.postsNo = postsNo; + this.postsTitle = postsTitle; + // this.postsContent = HtmlUtils.htmlEscape(HtmlUtils.htmlUnescape(postsContent)); // frontend 에서 처리 + this.postsContent = postsContent; + this.createdDate = createdDate; + } + + /** + * 신규 여부 계산 + * + * @param boardResponseDto 게시판 상세 응답 DTO + * @return PostsSimpleResponseDto 게시물 응답 DTO + */ + public PostsSimpleResponseDto setIsNew(BoardResponseDto boardResponseDto) { + if (boardResponseDto.getNewDisplayDayCount() != null) { + this.isNew = createdDate.plusDays(boardResponseDto.getNewDisplayDayCount()).compareTo(LocalDateTime.now()) <= 0; + } else { + this.isNew = false; + } + return this; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleSaveRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleSaveRequestDto.java new file mode 100644 index 0000000..7ae2d3f --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsSimpleSaveRequestDto.java @@ -0,0 +1,87 @@ +package org.egovframe.cloud.boardservice.api.posts.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.board.Board; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleSaveRequestDto + * + * 게시물 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/10 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/10    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PostsSimpleSaveRequestDto { + + /** + * 게시물 제목 + */ + @NotBlank(message = "{posts.posts_title} {err.required}") + private String postsTitle; + + /** + * 게시물 내용 + */ + @NotBlank(message = "{posts.posts_content} {err.required}") + private String postsContent; + + /** + * 첨부파일 코드 + */ + private String attachmentCode; + + /** + * 게시물 등록 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param attachmentCode 첨부파일 코드 + */ + @Builder + public PostsSimpleSaveRequestDto(String postsTitle, String postsContent, String attachmentCode) { + this.postsTitle = postsTitle; + this.postsContent = postsContent; + this.attachmentCode = attachmentCode; + } + + /** + * 게시물 등록 요청 DTO 속성 값으로 게시물 엔티티 빌더를 사용하여 객체 생성 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Posts 게시물 엔티티 + */ + public Posts toEntity(Integer boardNo, Integer postsNo) { + return Posts.builder() + .board(Board.builder() + .boardNo(boardNo) + .build()) + .postsId(PostsId.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .build()) + .postsTitle(postsTitle) + .postsContent(postsContent) + .attachmentCode(attachmentCode) + .build(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsUpdateRequestDto.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsUpdateRequestDto.java new file mode 100644 index 0000000..b9cfe53 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/api/posts/dto/PostsUpdateRequestDto.java @@ -0,0 +1,94 @@ +package org.egovframe.cloud.boardservice.api.posts.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.posts.Posts; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.PostsUpdateRequestDto + *

+ * 게시물 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PostsUpdateRequestDto { + + /** + * 게시물 제목 + */ + @NotBlank(message = "{posts.posts_title} {err.required}") + private String postsTitle; + + /** + * 게시물 내용 + */ + @NotBlank(message = "{posts.posts_content} {err.required}") + private String postsContent; + + /** + * 게시물 답변 내용 + */ + private String postsAnswerContent; + + /** + * 첨부파일 코드 + */ + private String attachmentCode; + + /** + * 공지 여부 + */ + @NotNull(message = "{posts.notice_at} {err.required}") + private Boolean noticeAt; + + /** + * 게시물 등록 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param postsAnswerContent 게시물 답변 내용 + * @param attachmentCode 첨부파일 코드 + * @param noticeAt 공지 여부 + */ + @Builder + public PostsUpdateRequestDto(String postsTitle, String postsContent, String postsAnswerContent, String attachmentCode, Boolean noticeAt) { + this.postsTitle = postsTitle; + this.postsContent = postsContent; + this.postsAnswerContent = postsAnswerContent; + this.attachmentCode = attachmentCode; + this.noticeAt = noticeAt; + } + + /** + * 게시물 등록 요청 DTO 속성 값으로 게시물 엔티티 빌더를 사용하여 객체 생성 + * + * @return Posts 게시물 엔티티 + */ + public Posts toEntity() { + return Posts.builder() + .postsTitle(postsTitle) + .postsContent(postsContent) + .postsAnswerContent(postsAnswerContent) + .attachmentCode(attachmentCode) + .noticeAt(noticeAt) + .build(); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SecurityConfig.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SecurityConfig.java new file mode 100644 index 0000000..76cc942 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SecurityConfig.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.boardservice.config; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.servlet.config.AuthenticationFilter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; + +/** + * org.egovframe.cloud.boardservice.config.SecurityConfig + *

+ * Spring Security Config 클래스 + * AuthenticationFilter 를 추가하고 토큰으로 setAuthentication 인증처리를 한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@EnableWebSecurity // Spring Security 설정들을 활성화시켜 준다 +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Value("${token.secret}") + private String TOKEN_SECRET; + + /** + * 스프링 시큐리티 설정 + * + * @param http + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .headers().frameOptions().disable() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 사용하기 때문에 세션은 비활성화 + .and() + .addFilter(getAuthenticationFilter()); + } + + /** + * 토큰에 담긴 정보로 Authentication 정보를 설정하여 jpa audit 처리에 사용된다. + * 이 처리를 하지 않으면 AnonymousAuthenticationToken 으로 처리된다. + * + * @return + * @throws Exception + */ + private AuthenticationFilter getAuthenticationFilter() throws Exception { + return new AuthenticationFilter(authenticationManager(), TOKEN_SECRET); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SqlQueryConfig.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SqlQueryConfig.java new file mode 100644 index 0000000..e396c38 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/config/SqlQueryConfig.java @@ -0,0 +1,56 @@ +package org.egovframe.cloud.boardservice.config; + +import com.querydsl.sql.MySQLTemplates; +import com.querydsl.sql.SQLQueryFactory; +import com.querydsl.sql.SQLTemplates; +import com.querydsl.sql.spring.SpringConnectionProvider; +import com.querydsl.sql.spring.SpringExceptionTranslator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +/** + * org.egovframe.cloud.common.config.SqlQueryConfig + * + * Native SQL 설정 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/07 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    jooho       최초 생성
+ * 
+ */ +@Configuration +public class SqlQueryConfig { + + /** + * SQLQueryFactory 빈 등록 + * + * @param dataSource 데이터 소스 + * @return SQLQueryFactory 쿼리 및 DML 절 생성을 위한 팩토리 클래스 + */ + @Bean + public SQLQueryFactory queryFactory(DataSource dataSource) { + return new SQLQueryFactory(querydslConfiguration(), new SpringConnectionProvider(dataSource)); + } + + /** + * querydsl 설정 + * + * @return Configuration 설정 + */ + public com.querydsl.sql.Configuration querydslConfiguration() { + SQLTemplates templates = MySQLTemplates.builder().build(); // MySQL + com.querydsl.sql.Configuration configuration = new com.querydsl.sql.Configuration(templates); + configuration.setExceptionTranslator(new SpringExceptionTranslator()); + return configuration; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/Board.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/Board.java new file mode 100644 index 0000000..5819021 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/Board.java @@ -0,0 +1,193 @@ +package org.egovframe.cloud.boardservice.domain.board; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.*; +import java.math.BigDecimal; + +/** + * org.egovframe.cloud.boardservice.domain.board.Board + *

+ * 게시판 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +public class Board extends BaseEntity { + + /** + * 게시판 번호 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(columnDefinition = "int(9)") + private Integer boardNo; + + /** + * 게시판 제목 + */ + @Column(nullable = false, length = 100) + private String boardName; + + /** + * 스킨 유형 코드 + */ + @Column(nullable = false, length = 20) + private String skinTypeCode; + + /** + * 제목 표시 길이 + */ + @Column(nullable = false, columnDefinition = "mediumint(5) default '20'") + private Integer titleDisplayLength; + + /** + * 게시물 표시 수 + */ + @Column(nullable = false, columnDefinition = "mediumint(5) default '10'") + private Integer postDisplayCount; + + /** + * 페이지 표시 수 + */ + @Column(nullable = false, columnDefinition = "mediumint(5) default '10'") + private Integer pageDisplayCount; + + /** + * 신규 표시 일 수 + */ + @Column(nullable = false, columnDefinition = "mediumint(5) default '3'") + private Integer newDisplayDayCount; + + /** + * 에디터 사용 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Boolean editorUseAt; + + /** + * 사용자 작성 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Boolean userWriteAt; + + /** + * 댓글 사용 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Boolean commentUseAt; + + /** + * 업로드 사용 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Boolean uploadUseAt; + + /** + * 업로드 제한 수 + */ + @Column(columnDefinition = "mediumint(5)") + private Integer uploadLimitCount; + + /** + * 업로드 제한 크기 + */ + @Column(columnDefinition = "bigint(20)") + private BigDecimal uploadLimitSize; + + /** + * 게시물 엔티티 + */ + /*@OneToMany(mappedBy = "board", fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + private List posts;*/ + + /** + * 빌더 패턴 클래스 생성자 + * + * @param boardNo 게시판 번호 + * @param boardName 게시판 명 + * @param skinTypeCode 스킨 유형 코드 + * @param titleDisplayLength 제목 표시 길이 + * @param postDisplayCount 게시물 표시 수 + * @param pageDisplayCount 페이지 표시 수 + * @param newDisplayDayCount 신규 표시 일 수 + * @param editorUseAt 에디터 사용 여부 + * @param userWriteAt 사용자 작성 여부 + * @param commentUseAt 댓글 사용 여부 + * @param uploadUseAt 업로드 사용 여부 + * @param uploadLimitCount 업로드 제한 수 + * @param uploadLimitSize 업로드 제한 크기 + */ + @Builder + public Board(Integer boardNo, String boardName, String skinTypeCode, Integer titleDisplayLength, + Integer postDisplayCount, Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt, + Boolean userWriteAt, Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount, + BigDecimal uploadLimitSize) { + this.boardNo = boardNo; + this.boardName = boardName; + this.skinTypeCode = skinTypeCode; + this.titleDisplayLength = titleDisplayLength; + this.postDisplayCount = postDisplayCount; + this.pageDisplayCount = pageDisplayCount; + this.newDisplayDayCount = newDisplayDayCount; + this.editorUseAt = editorUseAt; + this.userWriteAt = userWriteAt; + this.commentUseAt = commentUseAt; + this.uploadUseAt = uploadUseAt; + this.uploadLimitCount = uploadLimitCount; + this.uploadLimitSize = uploadLimitSize; + } + + /** + * 게시판 속성 값 수정 + * + * @param boardName 게시판 명 + * @param skinTypeCode 스킨 유형 코드 + * @param titleDisplayLength 제목 표시 길이 + * @param postDisplayCount 게시물 표시 수 + * @param pageDisplayCount 페이지 표시 수 + * @param newDisplayDayCount 신규 표시 일 수 + * @param userWriteAt 사용자 작성 여부 + * @param editorUseAt 에디터 사용 여부 + * @param commentUseAt 댓글 사용 여부 + * @param uploadUseAt 업로드 사용 여부 + * @param uploadLimitCount 업로드 제한 수 + * @param uploadLimitSize 업로드 제한 크기 + * @return Board 게시판 엔티티 + */ + public Board update(String boardName, String skinTypeCode, Integer titleDisplayLength, Integer postDisplayCount, + Integer pageDisplayCount, Integer newDisplayDayCount, Boolean editorUseAt, Boolean userWriteAt, + Boolean commentUseAt, Boolean uploadUseAt, Integer uploadLimitCount, BigDecimal uploadLimitSize) { + this.boardName = boardName; + this.skinTypeCode = skinTypeCode; + this.titleDisplayLength = titleDisplayLength; + this.postDisplayCount = postDisplayCount; + this.pageDisplayCount = pageDisplayCount; + this.newDisplayDayCount = newDisplayDayCount; + this.editorUseAt = editorUseAt; + this.userWriteAt = userWriteAt; + this.commentUseAt = commentUseAt; + this.uploadUseAt = uploadUseAt; + this.uploadLimitCount = uploadLimitCount; + this.uploadLimitSize = uploadLimitSize; + + return this; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepository.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepository.java new file mode 100644 index 0000000..c7e76e6 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepository.java @@ -0,0 +1,24 @@ +package org.egovframe.cloud.boardservice.domain.board; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.boardservice.domain.board.BoardRepository + *

+ * 게시판 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +public interface BoardRepository extends JpaRepository, BoardRepositoryCustom { + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryCustom.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryCustom.java new file mode 100644 index 0000000..db24b0b --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryCustom.java @@ -0,0 +1,47 @@ +package org.egovframe.cloud.boardservice.domain.board; + +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * org.egovframe.cloud.boardservice.domain.board.BoardRepositoryCustom + *

+ * 게시판 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +public interface BoardRepositoryCustom { + + /** + * 게시판 페이지 목록 조회 + * + * @param requestDto 게시판 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시판 목록 응답 DTO + */ + Page findPage(RequestDto requestDto, Pageable pageable); + + /** + * 게시판 목록 조회 + * + * @param boardNos 게시판 번호 목록 + * @return List 게시판 상세 응답 DTO List + */ + List findAllByBoardNoIn(List boardNos); + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryImpl.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryImpl.java new file mode 100644 index 0000000..9905843 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/board/BoardRepositoryImpl.java @@ -0,0 +1,143 @@ +package org.egovframe.cloud.boardservice.domain.board; + +import java.util.List; + +import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.boardservice.api.board.dto.QBoardListResponseDto; +import org.egovframe.cloud.boardservice.api.board.dto.QBoardResponseDto; +import org.egovframe.cloud.boardservice.domain.code.QCode; +import org.egovframe.cloud.common.dto.RequestDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.google.common.base.CaseFormat; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.boardservice.domain.board.BoardRepositoryImpl + *

+ * 게시판 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class BoardRepositoryImpl implements BoardRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 게시판 페이지 목록 조회 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시판 목록 응답 DTO + */ + @Override + public Page findPage(RequestDto requestDto, Pageable pageable) { + JPQLQuery query = jpaQueryFactory + .select(new QBoardListResponseDto( + QBoard.board.boardNo, + QBoard.board.boardName, + QBoard.board.skinTypeCode, + Expressions.as(QCode.code.codeName, "skinTypeCodeName"), + QBoard.board.createdDate + )) + .from(QBoard.board) + .leftJoin(QCode.code).on(QBoard.board.skinTypeCode.eq(QCode.code.codeId).and(QCode.code.parentCodeId.eq("skin_type_code"))) + .fetchJoin() + .where(getBooleanExpressionKeyword(requestDto)); + + //정렬 + pageable.getSort().stream().forEach(sort -> { + Order order = sort.isAscending() ? Order.ASC : Order.DESC; + String property = sort.getProperty(); + + Path target = Expressions.path(Object.class, QBoard.board, CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, property)); + @SuppressWarnings({ "rawtypes", "unchecked" }) + OrderSpecifier orderSpecifier = new OrderSpecifier(order, target); + query.orderBy(orderSpecifier); + }); + + QueryResults result = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) //페이징 + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 게시판 목록 조회 + * + * @param boardNos 게시판 번호 목록 + * @return List 게시판 상세 응답 DTO List + */ + @Override + public List findAllByBoardNoIn(List boardNos) { + return jpaQueryFactory + .select(new QBoardResponseDto( + QBoard.board.boardNo, + QBoard.board.boardName, + QBoard.board.skinTypeCode, + QBoard.board.titleDisplayLength, + QBoard.board.postDisplayCount, + QBoard.board.pageDisplayCount, + QBoard.board.newDisplayDayCount, + QBoard.board.editorUseAt, + QBoard.board.userWriteAt, + QBoard.board.commentUseAt, + QBoard.board.uploadUseAt, + QBoard.board.uploadLimitCount, + QBoard.board.uploadLimitSize + )) + .from(QBoard.board) + .leftJoin(QCode.code).on(QBoard.board.skinTypeCode.eq(QCode.code.codeId).and(QCode.code.parentCodeId.eq("skin_type_code"))) + .fetchJoin() + .where(QBoard.board.boardNo.in(boardNos)) + .orderBy(QBoard.board.boardNo.asc()) + .fetchResults().getResults(); + } + + /** + * 요청 DTO로 동적 검색 표현식 리턴 + * + * @param requestDto 요청 DTO + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpressionKeyword(RequestDto requestDto) { + if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null; + + switch (requestDto.getKeywordType()) { + case "boardName": // 게시판 명 + return QBoard.board.boardName.containsIgnoreCase(requestDto.getKeyword()); + default: + return null; + } + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/code/Code.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/code/Code.java new file mode 100644 index 0000000..36dc1a1 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/code/Code.java @@ -0,0 +1,41 @@ +package org.egovframe.cloud.boardservice.domain.code; + +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * org.egovframe.cloud.boardservice.domain.code.Code + *

+ * 공통코드 엔티티 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@NoArgsConstructor +@Entity +public class Code extends BaseEntity { + + @Id + @Column(insertable = false, updatable = false) + private String codeId; // 코드ID + + @Column(insertable = false, updatable = false) + private String parentCodeId; // 상위 코드ID + + @Column(insertable = false, updatable = false) + private String codeName; // 코드 명 + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/Comment.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/Comment.java new file mode 100644 index 0000000..f6f69c4 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/Comment.java @@ -0,0 +1,149 @@ +package org.egovframe.cloud.boardservice.domain.comment; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.user.User; +import org.egovframe.cloud.servlet.domain.BaseEntity; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import javax.persistence.*; + +/** + * org.egovframe.cloud.boardservice.domain.comment.Comment + *

+ * 댓글 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@DynamicInsert +@DynamicUpdate +@Entity +public class Comment extends BaseEntity { + + /** + * 댓글 복합키 + */ + @EmbeddedId + private CommentId commentId; + + /** + * 댓글 내용 + */ + @Column(nullable = false, length = 2000) + private String commentContent; + + /** + * 그룹 번호 + */ + @Column(columnDefinition = "int(9)") + private Integer groupNo; + + /** + * 부모 댓글 번호 + */ + @Column(columnDefinition = "int(9)") + private Integer parentCommentNo; + + /** + * 깊이 순서 + */ + @Column(nullable = false, columnDefinition = "smallint(3)") + private Integer depthSeq; + + /** + * 정렬 순서 + */ + @Column(nullable = false, columnDefinition = "int(9)") + private Integer sortSeq; + + /** + * 삭제 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Integer deleteAt; // 0:미삭제, 1:작성자삭제, 2:관리자삭제 + + /** + * 생성자 엔티티 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "createdBy", referencedColumnName = "userId", insertable = false, updatable = false) + private User creator; + + /** + * 게시물 엔티티 + */ + @MapsId("postsId") // CommentId.postsId 매핑 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumns({ + @JoinColumn(name = "board_no"), + @JoinColumn(name = "posts_no") + }) + private Posts posts; + + /** + * 빌더 패턴 클래스 생성자 + * + * @param posts 게시물 엔티티 + * @param commentId 댓글 복합키 + * @param commentContent 게시물 내용 + * @param groupNo 그룹 번호 + * @param parentCommentNo 부모 댓글 번호 + * @param depthSeq 깊이 순서 + * @param sortSeq 정렬 순서 + * @param deleteAt 삭제 여부 + */ + @Builder + public Comment(Posts posts, CommentId commentId, String commentContent, + Integer groupNo, Integer parentCommentNo, Integer depthSeq, + Integer sortSeq, Integer deleteAt, User creator) { + this.posts = posts; + this.commentId = commentId; + this.commentContent = commentContent; + this.groupNo = groupNo; + this.parentCommentNo = parentCommentNo; + this.depthSeq = depthSeq; + this.sortSeq = sortSeq; + this.deleteAt = deleteAt; + this.creator = creator; + } + + /** + * 댓글 내용 수정 + * + * @param commentContent 게시물 내용 + * @return Comment 댓글 엔티티 + */ + public Comment update(String commentContent) { + this.commentContent = commentContent; + + return this; + } + + /** + * 댓글 삭제 여부 수정 + * + * @param deleteAt 삭제 여부 + * @return Comment 댓글 엔티티 + */ + public Comment updateDeleteAt(Integer deleteAt) { + this.deleteAt = deleteAt; + + return this; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentId.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentId.java new file mode 100644 index 0000000..539dbe0 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentId.java @@ -0,0 +1,89 @@ +package org.egovframe.cloud.boardservice.domain.comment; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +/** + * org.egovframe.cloud.boardservice.domain.comment.CommentId + *

+ * 댓글 엔티티 복합키 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Embeddable +public class CommentId implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -8263227281325691911L; + + /** + * 게시물 복합키 + */ + private PostsId postsId; // @MapsId("postsId")로 매핑 + + /** + * 댓글 번호 + */ + @Column(columnDefinition = "int(9)") + private Integer commentNo; + + /** + * 빌드 패턴 클래스 생성자 + * + * @param postsId 게시물 복합키 + * @param commentNo 댓글 번호 + */ + @Builder + public CommentId(PostsId postsId, Integer commentNo) { + this.postsId = postsId; + this.commentNo = commentNo; + } + + /** + * Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap. + * + * @return int a hash code value for this object. + */ + @Override + public int hashCode() { + return Objects.hash(postsId.getBoardNo(), postsId.getPostsNo(), commentNo); + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param object the reference object with which to compare. + * @return {@code true} if this object is the same as the obj + */ + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof CommentId)) return false; + CommentId that = (CommentId) object; + return Objects.equals(postsId.getBoardNo(), that.getPostsId().getBoardNo()) && + Objects.equals(postsId.getPostsNo(), that.getPostsId().getPostsNo()) && + Objects.equals(commentNo, that.getCommentNo()); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepository.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepository.java new file mode 100644 index 0000000..cc53f10 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepository.java @@ -0,0 +1,24 @@ +package org.egovframe.cloud.boardservice.domain.comment; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.boardservice.domain.comment.CommentRepository + * + * 댓글 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +public interface CommentRepository extends JpaRepository, CommentRepositoryCustom { + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryCustom.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryCustom.java new file mode 100644 index 0000000..9e0dac2 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryCustom.java @@ -0,0 +1,112 @@ +package org.egovframe.cloud.boardservice.domain.comment; + +import org.egovframe.cloud.boardservice.api.comment.dto.CommentListResponseDto; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Map; + +/** + * org.egovframe.cloud.boardservice.domain.comment.CommentRepositoryCustom + *

+ * 댓글 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +public interface CommentRepositoryCustom { + + /** + * 댓글 전체 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param deleteAt 삭제 여부 + * @return List 댓글 목록 응답 DTO + */ + List findAll(Integer boardNo, Integer postsNo, Integer deleteAt); + + /** + * 댓글 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param deleteAt 삭제 여부 + * @param pageable 페이지 정보 + * @return Map 페이지 댓글 목록 응답 DTO + */ + Map findPage(Integer boardNo, Integer postsNo, Integer deleteAt, Pageable pageable); + + /** + * 다음 댓글 번호 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Integer 다음 댓글 번호 + */ + Integer findNextCommentNo(Integer boardNo, Integer postsNo); + + /** + * 다음 정렬 순서 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Integer 다음 댓글 번호 + */ + Integer findNextSortSeq(Integer boardNo, Integer postsNo); + + /** + * 대댓글의 정렬 마지막 순서 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param groupNo 그룹 번호 + * @return Integer 다음 댓글 번호 + */ + Integer findLastSortSeq(Integer boardNo, Integer postsNo, Integer groupNo); + + /** + * 대댓글의 정렬 순서 조회 + * 댓글그룹내에 부모 댓글 보다 정렬 순서가 크고 깊이가 크거나 같은 가장 작은 순서 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param groupNo 그룹 번호 + * @param parentCommentNo 부모 게시물 번호 + * @param depthSeq 깊이 순서 + * @return Integer 다음 댓글 번호 + */ + Integer findNextSortSeq(Integer boardNo, Integer postsNo, Integer groupNo, Integer parentCommentNo, Integer depthSeq); + + /** + * 댓글 삭제 여부 수정 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + * @param deleteAt 삭제 여부 + * @return Integer 처리 건수 + */ + Long updateDeleteAt(Integer boardNo, Integer postsNo, Integer commentNo, Integer deleteAt); + + /** + * 댓글 정렬 순서 수정 + * + * @param groupNo 그룹 번호 + * @param startSortSeq 시작 정렬 순서 + * @param endSortSeq 종료 정렬 순서 + * @param increaseSortSeq 증가 정렬 순서 + * @return Long 처리 건수 + */ + Long updateSortSeq(Integer groupNo, Integer startSortSeq, Integer endSortSeq, int increaseSortSeq); + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryImpl.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryImpl.java new file mode 100644 index 0000000..0dac4d8 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/comment/CommentRepositoryImpl.java @@ -0,0 +1,362 @@ +package org.egovframe.cloud.boardservice.domain.comment; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.SubQueryExpression; +import com.querydsl.core.types.dsl.*; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.querydsl.sql.SQLQueryFactory; +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentListResponseDto; +import org.egovframe.cloud.boardservice.api.comment.dto.QCommentListResponseDto; +import org.egovframe.cloud.boardservice.domain.user.QUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.egovframe.cloud.boardservice.domain.comment.QComment.comment; +import static org.egovframe.cloud.boardservice.domain.user.QUser.user; + +/** + * org.egovframe.cloud.boardservice.domain.comment.CommentRepositoryImpl + *

+ * 댓글 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class CommentRepositoryImpl implements CommentRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 쿼리 및 DML 절 생성을 위한 팩토리 클래스 + */ + private final SQLQueryFactory sqlQueryFactory; + + + /** + * 댓글 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param deleteAt 삭제 여부 + * @return List 댓글 목록 응답 DTO + */ + public List findAll(Integer boardNo, Integer postsNo, Integer deleteAt) { + return jpaQueryFactory + .select(new QCommentListResponseDto( + comment.commentId.postsId.boardNo, + comment.commentId.postsId.postsNo, + comment.commentId.commentNo, + comment.commentContent, + comment.groupNo, + comment.parentCommentNo, + comment.depthSeq, + comment.sortSeq, + comment.deleteAt, + comment.createdBy, + QUser.user.userName.as("createdName"), + comment.createdDate)) + .from(comment) + .leftJoin(user).on(comment.createdBy.eq(user.userId)) + .fetchJoin() + .where(comment.commentId.postsId.boardNo.eq(boardNo) + .and(comment.commentId.postsId.postsNo.eq(postsNo)) + .and(isEqualsDeleteAt(deleteAt))) + .orderBy(comment.commentId.postsId.boardNo.asc(), comment.commentId.postsId.postsNo.asc(), comment.groupNo.asc(), comment.sortSeq.asc()) + .fetch(); + } + + /** + * 댓글 목록 조회 + *

+ * JPQL 은 from 절에서 서브쿼리를 사용할 수 없어서 SQLQueryFactory 를 사용해 Native SQL 로 조회 + *

+ * Native SQL 을 사용하지 않고 서브쿼리를 먼저 조회한 후 JPQL 만을 사용해서 조회 가능 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param deleteAt 삭제 여부 + * @param pageable 페이지 정보 + * @return Map 페이지 댓글 목록 응답 DTO + */ + public Map findPage(Integer boardNo, Integer postsNo, Integer deleteAt, Pageable pageable) { + // 전체 댓글 수, 최상위 댓글 수(페이징 기준) 조회 + String totalElementsKey = "totalElements"; + Tuple countInfo = jpaQueryFactory + .select(Expressions.asNumber(1).count().as(totalElementsKey), + new CaseBuilder() + .when(comment.parentCommentNo.isNull()) + .then(1) + .otherwise(0) + .sum() + .coalesce(0) + .as("count")) + .from(comment) + .where(comment.commentId.postsId.boardNo.eq(boardNo) + .and(comment.commentId.postsId.postsNo.eq(postsNo)) + .and(isEqualsDeleteAt(deleteAt))) + .fetchOne(); + + Long totalElements = 0L; + Integer groupElements = 0; + if (countInfo != null) { + totalElements = countInfo.get(Expressions.numberPath(Long.class, totalElementsKey)); // 전체 댓글 수 + groupElements = countInfo.get(Expressions.numberPath(Integer.class, "count")); // 최상위 댓글 수 + } + + // path 정의 + Path commentPath = Expressions.path(Comment.class, "comment"); + NumberPath boardNoPath = Expressions.numberPath(Integer.class, commentPath, "board_no"); + NumberPath postsNoPath = Expressions.numberPath(Integer.class, commentPath, "posts_no"); + NumberPath commentNoPath = Expressions.numberPath(Integer.class, commentPath, "comment_no"); + NumberPath groupNoPath = Expressions.numberPath(Integer.class, commentPath, "group_no"); + NumberPath parentCommentNoPath = Expressions.numberPath(Integer.class, commentPath, "parent_comment_no"); + NumberPath sortSeqPath = Expressions.numberPath(Integer.class, commentPath, "sort_seq"); + NumberPath deleteAtPath = Expressions.numberPath(Integer.class, commentPath, "delete_at"); + + StringPath userPath = Expressions.stringPath("user"); + + BooleanExpression deleteAtExpression = null; + if (deleteAt != null) { + deleteAtExpression = deleteAt == 0 ? deleteAtPath.eq(0) : deleteAtPath.ne(0); + } + + // 댓글 그룹 조회 + Path groupCommentPath = Expressions.path(Comment.class, "groupComment"); + SubQueryExpression groupComment = JPAExpressions.select(boardNoPath, + postsNoPath, + commentNoPath) + .from(comment) + .where(boardNoPath.eq(boardNo) + .and(postsNoPath.eq(postsNo)) + .and(parentCommentNoPath.isNull()) + .and(deleteAtExpression)) + .orderBy(boardNoPath.asc(), postsNoPath.asc(), commentNoPath.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()); + + // 댓글 조회 + List comments = sqlQueryFactory + .select(new QCommentListResponseDto( + boardNoPath, + postsNoPath, + commentNoPath, + Expressions.stringPath(commentPath, "comment_content"), + groupNoPath, + parentCommentNoPath, + Expressions.numberPath(Integer.class, commentPath, "depth_seq"), + Expressions.numberPath(Integer.class, commentPath, "sort_seq"), + Expressions.numberPath(Integer.class, commentPath, "delete_at"), + Expressions.stringPath(commentPath, "created_by"), + Expressions.stringPath(userPath, "user_name"), + Expressions.datePath(LocalDateTime.class, commentPath, "created_date"))) + .from(comment) + .innerJoin(groupComment, groupCommentPath).on(Expressions.numberPath(Integer.class, groupCommentPath, "board_no").eq(boardNoPath) + .and(Expressions.numberPath(Integer.class, groupCommentPath, "posts_no").eq(postsNoPath)) + .and(Expressions.numberPath(Integer.class, groupCommentPath, "comment_no").eq(groupNoPath))) + .leftJoin(user).on(Expressions.stringPath(commentPath, "created_by").eq(Expressions.stringPath(userPath, "user_id"))) + .where(boardNoPath.eq(boardNo) + .and(postsNoPath.eq(postsNo)) + .and(deleteAtExpression)) + .orderBy(boardNoPath.asc(), postsNoPath.asc(), groupNoPath.asc(), sortSeqPath.asc()) + .fetch(); + + Page page = new PageImpl<>(comments, pageable, groupElements == null ? 0 : groupElements); + + // 페이지 인터페이스와 동일한 속성의 맵 리턴 + Map result = new HashMap<>(); + + result.put("content", comments); + result.put("empty", page.isEmpty()); + result.put("first", page.isFirst()); + result.put("last", page.isLast()); + result.put("number", page.getNumber()); + result.put("numberOfElements", comments.size()); + result.put("size", page.getSize()); + result.put(totalElementsKey, totalElements); + result.put("groupElements", groupElements); + result.put("totalPages", page.getTotalPages()); + + return result; + } + + /** + * 다음 댓글 번호 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Integer 다음 게시물 번호 + */ + public Integer findNextCommentNo(Integer boardNo, Integer postsNo) { + return jpaQueryFactory + .select(comment.commentId.commentNo.max().add(1).coalesce(1)) + .from(comment) + .where(comment.commentId.postsId.boardNo.eq(boardNo) + .and(comment.commentId.postsId.postsNo.eq(postsNo))) + .fetchOne(); + } + + /** + * 다음 정렬 순서 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Integer 다음 게시물 번호 + */ + public Integer findNextSortSeq(Integer boardNo, Integer postsNo) { + return jpaQueryFactory + .select(comment.sortSeq.max().add(1).coalesce(1)) + .from(comment) + .where(comment.commentId.postsId.boardNo.eq(boardNo) + .and(comment.commentId.postsId.postsNo.eq(postsNo)) + .and(comment.parentCommentNo.isNull())) + .fetchOne(); + } + + /** + * 대댓글의 정렬 마지막 순서 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param groupNo 그룹 번호 + * @return Integer 다음 게시물 번호 + */ + public Integer findLastSortSeq(Integer boardNo, Integer postsNo, Integer groupNo) { + return jpaQueryFactory + .select(comment.sortSeq.max().add(1).coalesce(1)) + .from(comment) + .where(comment.commentId.postsId.boardNo.eq(boardNo) + .and(comment.commentId.postsId.postsNo.eq(postsNo)) + .and(comment.groupNo.eq(groupNo))) + .fetchOne(); + } + + /** + * 대댓글의 정렬 순서 조회 + * 댓글그룹내에 부모 댓글 보다 정렬 순서가 크고 깊이가 크거나 같은 가장 작은 순서 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param groupNo 그룹 번호 + * @param parentCommentNo 부모 게시물 번호 + * @param depthSeq 깊이 순서 + * @return Integer 다음 게시물 번호 + */ + public Integer findNextSortSeq(Integer boardNo, Integer postsNo, Integer groupNo, Integer parentCommentNo, Integer depthSeq) { + return jpaQueryFactory + .select(comment.sortSeq.min()) + .from(comment) + .where(comment.commentId.postsId.boardNo.eq(boardNo) + .and(comment.commentId.postsId.postsNo.eq(postsNo)) + .and(comment.groupNo.eq(groupNo)) + .and(comment.sortSeq.gt(JPAExpressions.select(comment.sortSeq) + .from(comment) + .where(comment.commentId.postsId.boardNo.eq(boardNo) + .and(comment.commentId.postsId.postsNo.eq(postsNo)) + .and(comment.commentId.commentNo.eq(parentCommentNo))))) + .and(comment.depthSeq.lt(depthSeq))) + .fetchOne(); + } + + /** + * 댓글 정렬 순서 수정 + * + * @param groupNo 그룹 번호 + * @param startSortSeq 시작 정렬 순서 + * @param endSortSeq 종료 정렬 순서 + * @param increaseSortSeq 증가 정렬 순서 + * @return Long 수정 건수 + */ + public Long updateSortSeq(Integer groupNo, Integer startSortSeq, Integer endSortSeq, int increaseSortSeq) { + return jpaQueryFactory.update(comment) + .set(comment.sortSeq, comment.sortSeq.add(increaseSortSeq)) + .where(isEqualsGroupNo(groupNo), + isGoeSortSeq(startSortSeq), + isLoeSortSeq(endSortSeq)) + .execute(); + } + + /** + * 댓글 삭제 여부 수정 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + * @param deleteAt 삭제 여부 + * @return Integer 처리 건수 + */ + public Long updateDeleteAt(Integer boardNo, Integer postsNo, Integer commentNo, Integer deleteAt) { + return jpaQueryFactory.update(comment) + .set(comment.deleteAt, deleteAt) + .set(comment.modifiedDate, LocalDateTime.now()) + .where(comment.commentId.postsId.boardNo.eq(boardNo) + .and(comment.commentId.postsId.postsNo.eq(postsNo)) + .and(comment.commentId.commentNo.eq(commentNo))) + .execute(); + } + + /** + * 삭제여부 검색 표현식 + * + * @param deleteAt 삭제 여부 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression isEqualsDeleteAt(Integer deleteAt) { + if (deleteAt == null) return null; + + if (deleteAt == 0) return comment.deleteAt.eq(deleteAt); + else return comment.deleteAt.ne(0); + } + + /** + * 그룹 번호 동일 검색 표현식 + * + * @param groupNo 그룹 번호 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression isEqualsGroupNo(Integer groupNo) { + return groupNo == null ? null : comment.groupNo.eq(groupNo); + } + + /** + * 정렬 순서 이하 검색 표현식 + * + * @param sortSeq 정렬 순서 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression isLoeSortSeq(Integer sortSeq) { + return sortSeq == null ? null : comment.sortSeq.loe(sortSeq); + } + + /** + * 정렬 순서 이상 검색 표현식 + * + * @param sortSeq 정렬 순서 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression isGoeSortSeq(Integer sortSeq) { + return sortSeq == null ? null : comment.sortSeq.goe(sortSeq); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/Posts.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/Posts.java new file mode 100644 index 0000000..a7627ae --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/Posts.java @@ -0,0 +1,202 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.boardservice.domain.board.Board; +import org.egovframe.cloud.boardservice.domain.comment.Comment; +import org.egovframe.cloud.boardservice.domain.user.User; +import org.egovframe.cloud.servlet.domain.BaseEntity; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; +import java.util.List; + +/** + * org.egovframe.cloud.boardservice.domain.posts.Posts + *

+ * 게시물 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@DynamicInsert +@DynamicUpdate +@Entity +public class Posts extends BaseEntity { + + /** + * 게시물 복합키 + */ + @EmbeddedId + private PostsId postsId; + + /** + * 게시판 엔티티 + */ + @MapsId("boardNo") // PostsId.boardNo 매핑 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_no") + private Board board; + + /** + * 게시물 제목 + */ + @Column(nullable = false, length = 100) + private String postsTitle; + + /** + * 게시물 내용 + */ + @Column(nullable = false, columnDefinition = "longtext") + private String postsContent; + + /** + * 게시물 내용 + */ + @Column(columnDefinition = "longtext") + private String postsAnswerContent; + + /** + * 첨부파일 코드 + */ + @Column + private String attachmentCode; + + /** + * 조회 수 + */ + @Column(columnDefinition = "int(9) default '0'") + private Integer readCount; + + /** + * 공지 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Boolean noticeAt; + + /** + * 삭제 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Integer deleteAt; + + /** + * 생성자 엔티티 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "createdBy", referencedColumnName = "userId", insertable = false, updatable = false) + private User creator; + + /** + * 댓글 엔티티 + */ + @OneToMany(mappedBy = "posts", fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + private List comments; + + /** + * 빌더 패턴 클래스 생성자 + * + * @param board 게시판 엔티티 + * @param postsId 게시물 복합키 + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param postsAnswerContent 게시물 답변 내용 + * @param attachmentCode 첨부파일 코드 + * @param readCount 조회 수 + * @param noticeAt 공지 여부 + * @param deleteAt 삭제 여부 + */ + @Builder + public Posts(Board board, PostsId postsId, String postsTitle, + String postsContent, String postsAnswerContent, String attachmentCode, + Integer readCount, Boolean noticeAt, Integer deleteAt, + User creator, List comments) { + this.board = board; + this.postsId = postsId; + this.postsTitle = postsTitle; + this.postsContent = postsContent; + this.postsAnswerContent = postsAnswerContent; + this.attachmentCode = attachmentCode; + this.readCount = readCount; + this.noticeAt = noticeAt; + this.deleteAt = deleteAt; + this.creator = creator; + this.comments = comments; + } + + /** + * 게시물 속성 값 수정 + * + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param attachmentCode 첨부파일 코드 + * @return Posts 게시물 엔티티 + */ + public Posts update(String postsTitle, String postsContent, String attachmentCode) { + this.postsTitle = postsTitle; + this.postsContent = postsContent; + this.attachmentCode = attachmentCode; + + return this; + } + + /** + * 게시물 속성 값 수정 + * + * @param postsTitle 게시물 제목 + * @param postsContent 게시물 내용 + * @param postsAnswerContent 게시물 답변 내용 + * @param attachmentCode 첨부파일 코드 + * @param noticeAt 공지 여부 + * @return Posts 게시물 엔티티 + */ + public Posts update(String postsTitle, String postsContent, String postsAnswerContent, String attachmentCode, Boolean noticeAt) { + this.postsTitle = postsTitle; + this.postsContent = postsContent; + this.postsAnswerContent = postsAnswerContent; + this.attachmentCode = attachmentCode; + this.noticeAt = noticeAt; + + return this; + } + + /** + * 게시물 삭제 여부 수정 + * + * @param deleteAt 삭제 여부 + * @return Posts 게시물 엔티티 + */ + public Posts updateDeleteAt(Integer deleteAt) { + this.deleteAt = deleteAt; + + return this; + } + + /** + * 조회 수 증가 + * + * @return Posts 게시물 엔티티 + */ + public Posts updateReadCount() { + this.readCount += 1; + + return this; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsId.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsId.java new file mode 100644 index 0000000..13cc5f1 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsId.java @@ -0,0 +1,92 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsId + *

+ * 게시물 엔티티 복합키 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/30    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Embeddable +public class PostsId implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = 2286680637185590124L; + + /** + * 게시판 번호 + */ + @Column(columnDefinition = "int(9)") + private Integer boardNo; // @MapsId("boardNo")로 매핑 + + /** + * 게시물 번호 + */ + @Column(columnDefinition = "int(9)") + private Integer postsNo; + + /** + * 빌드 패턴 클래스 생성자 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + */ + @Builder + public PostsId(Integer boardNo, Integer postsNo) { + this.boardNo = boardNo; + this.postsNo = postsNo; + } + + /** + * Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap. + * + * @return int a hash code value for this object. + */ + @Override + public int hashCode() { + return Objects.hash(boardNo, postsNo); + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param object the reference object with which to compare. + * @return {@code true} if this object is the same as the obj + */ + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof PostsId)) return false; + PostsId that = (PostsId) object; + return Objects.equals(boardNo, that.getBoardNo()) && + Objects.equals(postsNo, that.getPostsNo()); + } + + @Override + public String toString() { + return this.boardNo+"_"+this.postsNo; + } +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRead.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRead.java new file mode 100644 index 0000000..d8c38f9 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRead.java @@ -0,0 +1,83 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsRead + *

+ * 게시물 조회 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/02 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/02    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함 +public class PostsRead { + + /** + * 게시물 조회 복합키 + */ + @EmbeddedId + private PostsReadId postsReadId; + + /** + * 사용자 id + */ + private String userId; + + /** + * ip 주소 + */ + @Column(nullable = false, columnDefinition = "varchar(50)") + private String ipAddr; + + /** + * 생성 일시 + */ + @CreatedDate + @Column + private LocalDateTime createdDate; + + /** + * 빌더 패턴 클래스 생성자 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param readNo 조회 번호 + * @param userId 사용자 id + * @param tokenId 토큰 id + * @param ipAddr ip 주소 + */ + @Builder + public PostsRead(Integer boardNo, Integer postsNo, Integer readNo, String userId, String tokenId, String ipAddr) { + this.postsReadId = PostsReadId.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .readNo(readNo) + .build(); + this.userId = userId; + this.ipAddr = ipAddr; + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadId.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadId.java new file mode 100644 index 0000000..c25f093 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadId.java @@ -0,0 +1,97 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsReadId + *

+ * 게시판 조회 엔티티 복합키 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/02 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/02    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Embeddable +public class PostsReadId implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -6710005976442877773L; + + /** + * 게시판 번호 + */ + @Column(columnDefinition = "int(9)") + private Integer boardNo; + + /** + * 게시물 번호 + */ + @Column(columnDefinition = "int(9)") + private Integer postsNo; + + /** + * 조회 번호 + */ + @Column(columnDefinition = "int(9)") + private Integer readNo; + + /** + * 빌드 패턴 클래스 생성자 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param readNo 조회 번호 + */ + @Builder + public PostsReadId(Integer boardNo, Integer postsNo, Integer readNo) { + this.boardNo = boardNo; + this.postsNo = postsNo; + this.readNo = readNo; + } + + /** + * Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap. + * + * @return int a hash code value for this object. + */ + @Override + public int hashCode() { + return Objects.hash(boardNo, postsNo, readNo); + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param object the reference object with which to compare. + * @return {@code true} if this object is the same as the obj + */ + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof PostsReadId)) return false; + PostsReadId that = (PostsReadId) object; + return Objects.equals(boardNo, that.getBoardNo()) && + Objects.equals(postsNo, that.getPostsNo()) && + Objects.equals(readNo, that.getReadNo()); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepository.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepository.java new file mode 100644 index 0000000..de5a432 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepository.java @@ -0,0 +1,24 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsReadRepository + *

+ * 게시물 조회 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/02 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/02    jooho       최초 생성
+ * 
+ */ +public interface PostsReadRepository extends JpaRepository, PostsReadRepositoryCustom { + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryCustom.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryCustom.java new file mode 100644 index 0000000..0e7cfd3 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryCustom.java @@ -0,0 +1,42 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsReadRepositoryCustom + *

+ * 게시물 조회 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/02 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/02    jooho       최초 생성
+ * 
+ */ +public interface PostsReadRepositoryCustom { + + /** + * 게시물 조회 데이터 수 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param userId 사용자 id + * @param ipAddr ip 주소 + * @return Long 데이터 수 + */ + Long countByBoardNoAndPostsNoAndUserId(Integer boardNo, Integer postsNo, String userId, String ipAddr); + + /** + * 다음 게시물 조회 번호 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Integer 다음 게시물 조회 번호 + */ + Integer findNextReadNo(Integer boardNo, Integer postsNo); + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryImpl.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryImpl.java new file mode 100644 index 0000000..a589efd --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsReadRepositoryImpl.java @@ -0,0 +1,86 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsReadRepositoryImpl + *

+ * 게시물 조회 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/02 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/02    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class PostsReadRepositoryImpl implements PostsReadRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 게시물 조회 데이터 수 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param userId 사용자 id + * @param ipAddr ip 주소 + * @return Long 데이터 수 + */ + public Long countByBoardNoAndPostsNoAndUserId(Integer boardNo, Integer postsNo, String userId, String ipAddr) { + return jpaQueryFactory + .selectFrom(QPostsRead.postsRead) + .where(QPostsRead.postsRead.postsReadId.boardNo.eq(boardNo) + .and(QPostsRead.postsRead.postsReadId.postsNo.eq(postsNo)) + .and(getBooleanExpression("userId", userId)) + .and(getBooleanExpression("ipAddr", ipAddr))) + .fetchCount(); + } + + /** + * 다음 게시물 조회 번호 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Integer 다음 게시물 조회 번호 + */ + public Integer findNextReadNo(Integer boardNo, Integer postsNo) { + return jpaQueryFactory.select(QPostsRead.postsRead.postsReadId.readNo.max().add(1).coalesce(1)) + .from(QPostsRead.postsRead) + .where(QPostsRead.postsRead.postsReadId.boardNo.eq(boardNo) + .and(QPostsRead.postsRead.postsReadId.postsNo.eq(postsNo))) + .fetchOne(); + } + + /** + * 엔티티 속성별 동적 검색 표현식 리턴 + * + * @param attributeName 속성 명 + * @param attributeValue 속성 값 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpression(String attributeName, Object attributeValue) { + if (attributeValue == null || "".equals(attributeValue.toString())) return null; + + switch (attributeName) { + case "userId": // 사용자 id + return QPostsRead.postsRead.userId.eq((String) attributeValue); + case "ipAddr": // ip 주소 + return QPostsRead.postsRead.ipAddr.eq((String) attributeValue); + default: + return null; + } + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepository.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepository.java new file mode 100644 index 0000000..0ed68b2 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepository.java @@ -0,0 +1,24 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsRepository + *

+ * 게시물 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +public interface PostsRepository extends JpaRepository, PostsRepositoryCustom { + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryCustom.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryCustom.java new file mode 100644 index 0000000..ad27f49 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryCustom.java @@ -0,0 +1,102 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import org.egovframe.cloud.boardservice.api.posts.dto.PostsListResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto; +import org.egovframe.cloud.common.dto.RequestDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Map; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsRepositoryCustom + *

+ * 게시물 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +public interface PostsRepositoryCustom { + + /** + * 게시물 페이지 목록 조회 + * + * @param boardNo 게시판 번호 + * @param deleteAt 삭제 여부 + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시물 목록 응답 DTO + */ + Page findPage(Integer boardNo, Integer deleteAt, RequestDto requestDto, Pageable pageable); + + /** + * 게시판별 최근 게시물 목록 조회 + * + * @param boardNos 게시판 번호 목록 + * @param postsCount 게시물 수 + * @return List 게시물 응답 DTO List + */ + List findAllByBoardNosLimitCount(List boardNos, Integer postsCount); + + /** + * 게시물 상세 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param userId 사용자 id + * @param ipAddr ip 주소 + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + PostsResponseDto findById(Integer boardNo, Integer postsNo, String userId, String ipAddr); + + /** + * 근처 게시물 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param gap 차이 -1: 이전, 1: 이후 + * @param deleteAt 삭제 여부 + * @param requestDto 요청 DTO + * @return List 게시물 상세 응답 DTO List + */ + List findNearPost(Integer boardNo, Integer postsNo, long gap, Integer deleteAt, RequestDto requestDto); + + /** + * 다음 게시물 번호 조회 + * + * @param boardNo 게시판 번호 + * @return Integer 다음 게시물 번호 + */ + Integer findNextPostsNo(Integer boardNo); + + /** + * 게시물 조회 수 증가 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Long 처리 건수 + */ + Long updateReadCount(Integer boardNo, Integer postsNo); + + /** + * 게시물 삭제 여부 수정 + * + * @param posts 게시물 정보(게시판번호, 게시물번호배열) + * @param deleteAt 삭제 여부 + * @param userId 사용자 id + * @return Long 처리 건수 + */ + Long updateDeleteAt(Map> posts, Integer deleteAt, String userId); + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryImpl.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryImpl.java new file mode 100644 index 0000000..e525ed8 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/posts/PostsRepositoryImpl.java @@ -0,0 +1,406 @@ +package org.egovframe.cloud.boardservice.domain.posts; + +import java.time.LocalDateTime; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.egovframe.cloud.boardservice.api.board.dto.QBoardResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsListResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.QPostsListResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.QPostsResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.QPostsSimpleResponseDto; +import org.egovframe.cloud.boardservice.domain.board.QBoard; +import org.egovframe.cloud.boardservice.domain.comment.QComment; +import org.egovframe.cloud.boardservice.domain.user.QUser; +import org.egovframe.cloud.common.dto.RequestDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.google.common.base.CaseFormat; +import com.querydsl.core.QueryResults; +import com.querydsl.core.Tuple; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.SimpleExpression; +import com.querydsl.core.types.dsl.StringPath; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.querydsl.sql.SQLExpressions; +import com.querydsl.sql.SQLQuery; +import com.querydsl.sql.SQLQueryFactory; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.boardservice.domain.posts.PostsRepositoryImpl + *

+ * 게시물 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class PostsRepositoryImpl implements PostsRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 쿼리 및 DML 절 생성을 위한 팩토리 클래스 + */ + private final SQLQueryFactory sqlQueryFactory; + + /** + * 게시물 페이지 목록 조회 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param boardNo 게시판 번호 + * @param deleteAt 삭제 여부 + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시물 목록 응답 DTO + */ + @Override + public Page findPage(Integer boardNo, Integer deleteAt, RequestDto requestDto, Pageable pageable) { + JPQLQuery query = jpaQueryFactory + .select(new QPostsListResponseDto( + QPosts.posts.postsId.boardNo, + QPosts.posts.postsId.postsNo, + QPosts.posts.postsTitle, + new CaseBuilder() + .when(QBoard.board.skinTypeCode.in("faq", "qna")) + .then(QPosts.posts.postsContent) + .otherwise(""), + new CaseBuilder() + .when(QBoard.board.skinTypeCode.in("faq", "qna")) + .then(QPosts.posts.postsAnswerContent) + .otherwise(""), + QPosts.posts.readCount, + QPosts.posts.noticeAt, + QPosts.posts.deleteAt, + QPosts.posts.createdBy, + QUser.user.userName.as("createdName"), + QPosts.posts.createdDate, + QBoard.board.newDisplayDayCount, + getCommentCountExpression(deleteAt))) + .from(QPosts.posts) + .innerJoin(QBoard.board).on(QPosts.posts.postsId.boardNo.eq(QBoard.board.boardNo)) + .fetchJoin() + .leftJoin(QUser.user).on(QPosts.posts.createdBy.eq(QUser.user.userId)) + .fetchJoin() + .where(QPosts.posts.postsId.boardNo.eq(boardNo) + .and(getBooleanExpression(requestDto.getKeywordType(), requestDto.getKeyword())) + .and(getBooleanExpression("deleteAt", deleteAt))); + + //정렬 + pageable.getSort().stream().forEach(sort -> { + Order order = sort.isAscending() ? Order.ASC : Order.DESC; + String property = sort.getProperty(); + Path parent; + if ("board_no".equals(property) || "posts_no".equals(property)) parent = QPosts.posts.postsId; + else parent = QPosts.posts; + + Path target = Expressions.path(Object.class, parent, CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, property)); + @SuppressWarnings({ "unchecked", "rawtypes" }) + OrderSpecifier orderSpecifier = new OrderSpecifier(order, target); + query.orderBy(orderSpecifier); + }); + + QueryResults result = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) //페이징 + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 게시판별 최근 게시물 목록 조회 + *

+ * JPQL 은 from 절에서 서브쿼리를 사용할 수 없어서 SQLQueryFactory 를 사용해 Native SQL 로 조회 + * MySQL8 부터는 ROW_NUMBER, RANK 함수를 지원, 탬플릿에서는 MySQL5.7 로 개발해서 mysql 변수를 사용하는 방법으로 조회 + * MySQL 문법이 포함되어있어서 다른 DBMS 를 사용하는 경우 수정 필요 + *

+ * 인프런 김영한 강사는 추천하지 않는다. + * SqlQueryFactory는 저는 권장하지 않습니다. DB에서 메타데이터를 다 뽑아내서 생성해야 하는데... 너무 복잡하고 기능에 한계도 많습니다. + * 따라서 JPA와 JPA용 Querydsl을 최대한 사용하고, 그래도 잘 안되는 부분은 네이티브 쿼리를 사용하는 것이 더 좋다 생각합니다. + *

+ * 게시판별로 반복하여 게시물 조회하는 방법 등으로 JPQL 만을 사용해서 조회 가능 + * + * @param boardNos 게시판 번호 목록 + * @param postsCount 게시물 수 + * @return List 게시물 응답 DTO List + */ + @Override + public List findAllByBoardNosLimitCount(List boardNos, Integer postsCount) { + // path 정의 + Path postsPath = Expressions.path(Posts.class, "posts"); + NumberPath boardNoPath = Expressions.numberPath(Integer.class, postsPath, "board_no"); + NumberPath postsNoPath = Expressions.numberPath(Integer.class, postsPath, "posts_no"); + + // 게시판번호, 로우넘 변수 + StringPath varPath = Expressions.stringPath("v"); + SQLQuery varSql = SQLExpressions.select( + Expressions.stringTemplate("@boardNo := 0"), + Expressions.stringTemplate("@rn := 0")); + + // 게시물 조회 + SQLQuery rownumSql = SQLExpressions + .select(boardNoPath, + postsNoPath, + Expressions.stringPath(postsPath, "posts_title"), + Expressions.stringPath(postsPath, "posts_content"), + Expressions.datePath(LocalDateTime.class, postsPath, "created_date"), + Expressions.stringTemplate("(CASE @boardNo WHEN posts.board_no THEN @rn := @rn + 1 ELSE @rn := 1 END)").as("rn"), + Expressions.stringTemplate("(@boardNo := posts.board_no)").as("boardNo")) + .from(QPosts.posts, postsPath) + .innerJoin(varSql, varPath) + .where(boardNoPath.in(boardNos) + .and(Expressions.numberPath(Integer.class, postsPath, "delete_at").eq(0))) + .orderBy(boardNoPath.asc(), postsNoPath.desc()); + + // 최근 게시물 조회 + return sqlQueryFactory + .select(new QPostsSimpleResponseDto(boardNoPath, + postsNoPath, + Expressions.stringPath(postsPath, "posts_title"), + Expressions.stringPath(postsPath, "posts_content"), + Expressions.datePath(LocalDateTime.class, postsPath, "created_date"))) + .from(rownumSql, postsPath) + .where(Expressions.numberPath(Integer.class, postsPath, "rn").loe(postsCount)) + .fetch(); + } + + /** + * 게시물 상세 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param userId 사용자 id + * @param ipAddr ip 주소 + * @return PostsResponseDto 게시물 상세 응답 DTO + */ + @Override + public PostsResponseDto findById(Integer boardNo, Integer postsNo, String userId, String ipAddr) { + return jpaQueryFactory + .select( + // 게시물 + new QPostsResponseDto( + QPosts.posts.postsId.boardNo, + QPosts.posts.postsId.postsNo, + QPosts.posts.postsTitle, + QPosts.posts.postsContent, + QPosts.posts.postsAnswerContent, + QPosts.posts.attachmentCode, + QPosts.posts.readCount, + QPosts.posts.noticeAt, + QPosts.posts.deleteAt, + QPosts.posts.createdBy, + QUser.user.userName.as("createdName"), + QPosts.posts.createdDate, + // 게시판 + new QBoardResponseDto(QBoard.board.boardNo, + QBoard.board.boardName, + QBoard.board.skinTypeCode, + QBoard.board.titleDisplayLength, + QBoard.board.postDisplayCount, + QBoard.board.pageDisplayCount, + QBoard.board.newDisplayDayCount, + QBoard.board.editorUseAt, + QBoard.board.userWriteAt, + QBoard.board.commentUseAt, + QBoard.board.uploadUseAt, + QBoard.board.uploadLimitCount, + QBoard.board.uploadLimitSize), + // 댓글 수 + // getCommentCountExpression(), + // 조회 사용자의 게시물 조회 수(조회 수 증가 확인 용) + ExpressionUtils.as( + JPAExpressions.select(ExpressionUtils.count(QPostsRead.postsRead.userId)) + .from(QPostsRead.postsRead) + .where(QPostsRead.postsRead.postsReadId.boardNo.eq(QPosts.posts.postsId.boardNo) + .and(QPostsRead.postsRead.postsReadId.postsNo.eq(QPosts.posts.postsId.postsNo)) + .and(getBooleanExpression("userId", userId)) + .and(getBooleanExpression("ipAddr", ipAddr))), + "userPostsReadCount"))) + .from(QPosts.posts) // 게시물 + .innerJoin(QBoard.board).on(QPosts.posts.postsId.boardNo.eq(QBoard.board.boardNo)) // 게시판 + .leftJoin(QUser.user).on(QPosts.posts.createdBy.eq(QUser.user.userId)) // 생성자 + .fetchJoin() + .where(QPosts.posts.postsId.boardNo.eq(boardNo) + .and(QPosts.posts.postsId.postsNo.eq(postsNo))) + .fetchOne(); + } + + /** + * 이전 게시물 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param gap 차이 -1: 이전, 1: 이후 + * @param deleteAt 삭제 여부 + * @param requestDto 요청 DTO + * @return List 게시물 상세 응답 DTO List + */ + @Override + public List findNearPost(Integer boardNo, Integer postsNo, long gap, Integer deleteAt, RequestDto requestDto) { + return jpaQueryFactory + .select(new QPostsSimpleResponseDto( + QPosts.posts.postsId.boardNo, + QPosts.posts.postsId.postsNo, + QPosts.posts.postsTitle, + QPosts.posts.postsContent, + QPosts.posts.createdDate)) + .from(QPosts.posts) + .where(QPosts.posts.postsId.boardNo.eq(boardNo) + .and(getBooleanExpression(requestDto.getKeywordType(), requestDto.getKeyword())) + .and(getBooleanExpression("deleteAt", deleteAt)) + .and(getBooleanExpression(gap < 0 ? "postsNoLt" : "postsNoGt", postsNo))) + .orderBy(gap < 0 ? QPosts.posts.noticeAt.asc() : QPosts.posts.noticeAt.desc(), + QPosts.posts.postsId.boardNo.asc(), + gap < 0 ? QPosts.posts.postsId.postsNo.desc() : QPosts.posts.postsId.postsNo.asc()) + .limit((gap < 0 ? -1 : 1) * gap) + // .fetchFirst() // 단건 리턴 + .fetch(); + } + + /** + * 다음 게시물 번호 조회 + * + * @param boardNo 게시판 번호 + * @return Integer 다음 게시물 번호 + */ + @Override + public Integer findNextPostsNo(Integer boardNo) { + return jpaQueryFactory + .select(QPosts.posts.postsId.postsNo.max().add(1).coalesce(1)) + .from(QPosts.posts) + .where(QPosts.posts.postsId.boardNo.eq(boardNo)) + .fetchOne(); + } + + /** + * 게시물 조회 수 증가 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Long 처리 건수 + */ + @Override + public Long updateReadCount(Integer boardNo, Integer postsNo) { + return jpaQueryFactory.update(QPosts.posts) + .set(QPosts.posts.readCount, QPosts.posts.readCount.add(1)) + .where(QPosts.posts.postsId.boardNo.eq(boardNo) + .and(QPosts.posts.postsId.postsNo.eq(postsNo))) + .execute(); + } + + /** + * 게시물 삭제 여부 수정 + * + * @param posts 게시물 정보(게시판번호, 게시물번호배열) + * @param deleteAt 삭제 여부 + * @param userId 사용자 id + * @return Long 수정 건수 + */ + @Override + public Long updateDeleteAt(Map> posts, Integer deleteAt, String userId) { + long updateCount = 0L; + + Iterator iterator = posts.keySet().iterator(); + while (iterator.hasNext()) { + Integer boardNo = iterator.next(); + + List postsNoList = posts.get(boardNo); + + updateCount += jpaQueryFactory.update(QPosts.posts) + .set(QPosts.posts.deleteAt, deleteAt) + .set(QPosts.posts.lastModifiedBy, userId) + .set(QPosts.posts.modifiedDate, LocalDateTime.now()) + .where(QPosts.posts.postsId.boardNo.eq(boardNo) + .and(QPosts.posts.postsId.postsNo.in(postsNoList))) + .execute(); + } + + return updateCount; + } + + /** + * 댓글 수 표현식 + * + * @param deleteAt 삭제 여부 + * @return SimpleExpression 댓글 수 표현식 + */ + private SimpleExpression getCommentCountExpression(Integer deleteAt) { + BooleanExpression deleteAtExpression = null; + if (deleteAt != null) { + deleteAtExpression = deleteAt == 0 ? QComment.comment.deleteAt.eq(0) : QComment.comment.deleteAt.ne(0); + } + + return Expressions.as(new CaseBuilder() + .when(QBoard.board.commentUseAt.eq(true)) + .then(JPAExpressions.select(ExpressionUtils.count(QComment.comment.commentId.commentNo)) + .from(QComment.comment) + .where(QComment.comment.commentId.postsId.boardNo.eq(QPosts.posts.postsId.boardNo) + .and(QComment.comment.commentId.postsId.postsNo.eq(QPosts.posts.postsId.postsNo)) + .and(QComment.comment.commentId.postsId.postsNo.eq(QPosts.posts.postsId.postsNo)) + .and(deleteAtExpression))) + .otherwise(0L) + , "commentCount"); + } + + /** + * 엔티티 속성별 동적 검색 표현식 리턴 + * + * @param attributeName 속성 명 + * @param attributeValue 속성 값 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpression(String attributeName, Object attributeValue) { + if (attributeValue == null || "".equals(attributeValue.toString())) return null; + + switch (attributeName) { + case "userId": // 사용자 id + return QPostsRead.postsRead.userId.eq((String) attributeValue); + case "ipAddr": // ip 주소 + return QPostsRead.postsRead.ipAddr.eq((String) attributeValue); + case "deleteAt": // 삭제 여부 + return QPosts.posts.deleteAt.eq((Integer) attributeValue); + case "postsData": // 게시물 제목 + 내용 + return QPosts.posts.postsTitle.containsIgnoreCase((String) attributeValue).or(QPosts.posts.postsContent.containsIgnoreCase((String) attributeValue)); + case "postsTitle": // 게시물 제목 + return QPosts.posts.postsTitle.containsIgnoreCase((String) attributeValue); + case "postsContent": // 게시물 내용 + return QPosts.posts.postsContent.containsIgnoreCase((String) attributeValue); + case "postsNoLt": // 게시물 번호 + return QPosts.posts.postsId.postsNo.lt((Integer) attributeValue); + case "postsNoGt": // 게시물 번호 + return QPosts.posts.postsId.postsNo.gt((Integer) attributeValue); + default: + return null; + } + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/user/User.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/user/User.java new file mode 100644 index 0000000..fa4a3b5 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/domain/user/User.java @@ -0,0 +1,49 @@ +package org.egovframe.cloud.boardservice.domain.user; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import java.io.Serializable; + +/** + * org.egovframe.cloud.boardservice.domain.user.User + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@NoArgsConstructor +@Entity +@Getter +public class User extends BaseEntity implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = 8328774953218952698L; + + @Id + @Column(name = "user_no", insertable = false, updatable = false) + private Long id; + + @Column(insertable = false, updatable = false) + private String userId; + + @Column(insertable = false, updatable = false) + private String userName; + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/board/BoardService.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/board/BoardService.java new file mode 100644 index 0000000..2b0637f --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/board/BoardService.java @@ -0,0 +1,136 @@ +package org.egovframe.cloud.boardservice.service.board; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardSaveRequestDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardUpdateRequestDto; +import org.egovframe.cloud.boardservice.domain.board.Board; +import org.egovframe.cloud.boardservice.domain.board.BoardRepository; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * org.egovframe.cloud.boardservice.service.board.BoardService + *

+ * 게시판 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class BoardService extends AbstractService { + + /** + * 게시판 레파지토리 인터페이스 + */ + private final BoardRepository boardRepository; + + /** + * 조회 조건에 일치하는 게시판 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시판 목록 응답 DTO + */ + public Page findPage(RequestDto requestDto, Pageable pageable) { + return boardRepository.findPage(requestDto, pageable); + } + + /** + * 게시판 목록 조회 + * + * @param boardNos 게시판 번호 목록 + * @return List 게시판 상세 응답 DTO List + */ + public List findAllByBoardNos(List boardNos) { + return boardRepository.findAllByBoardNoIn(boardNos); + } + + /** + * 게시판 단건 조회 + * + * @param boardNo 게시판 번호 + * @return BoardResponseDto 게시판 응답 DTO + */ + public BoardResponseDto findById(Integer boardNo) { + Board entity = findBoard(boardNo); + + return new BoardResponseDto(entity); + } + + /** + * 게시판 등록 + * + * @param requestDto 게시판 등록 요청 DTO + * @return BoardResponseDto 게시판 응답 DTO + */ + @Transactional + public BoardResponseDto save(BoardSaveRequestDto requestDto) { + Board entity = boardRepository.save(requestDto.toEntity()); + + return new BoardResponseDto(entity); + } + + /** + * 게시판 수정 + * + * @param boardNo 게시판 번호 + * @param requestDto 게시판 수정 요청 DTO + * @return BoardResponseDto 게시판 응답 DTO + */ + @Transactional + public BoardResponseDto update(Integer boardNo, BoardUpdateRequestDto requestDto) { + Board entity = findBoard(boardNo); + + // 수정 + entity.update(requestDto.getBoardName(), requestDto.getSkinTypeCode(), requestDto.getTitleDisplayLength(), requestDto.getPostDisplayCount(), + requestDto.getPageDisplayCount(), requestDto.getNewDisplayDayCount(), requestDto.getEditorUseAt(), requestDto.getUserWriteAt(), + requestDto.getCommentUseAt(), requestDto.getUploadUseAt(), requestDto.getUploadLimitCount(), requestDto.getUploadLimitSize()); + + return new BoardResponseDto(entity); + } + + /** + * 게시판 삭제 + * + * @param boardNo 게시판 번호 + */ + @Transactional + public void delete(Integer boardNo) { + Board entity = findBoard(boardNo); + + // 삭제 + boardRepository.delete(entity); + } + + /** + * 게시판 번호로 게시판 엔티티 조회 + * + * @param boardNo 게시판 번호 + * @return Board 게시판 엔티티 + */ + private Board findBoard(Integer boardNo) { + return boardRepository.findById(boardNo) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("board")}))); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/comment/CommentService.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/comment/CommentService.java new file mode 100644 index 0000000..9a69710 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/comment/CommentService.java @@ -0,0 +1,288 @@ +package org.egovframe.cloud.boardservice.service.comment; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentListResponseDto; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentResponseDto; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentSaveRequestDto; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentUpdateRequestDto; +import org.egovframe.cloud.boardservice.domain.board.Board; +import org.egovframe.cloud.boardservice.domain.comment.Comment; +import org.egovframe.cloud.boardservice.domain.comment.CommentId; +import org.egovframe.cloud.boardservice.domain.comment.CommentRepository; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; +import org.egovframe.cloud.boardservice.service.posts.PostsService; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.exception.InvalidValueException; +import org.egovframe.cloud.common.service.AbstractService; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * org.egovframe.cloud.boardservice.service.comment.CommentService + *

+ * 댓글 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/04 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/04    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class CommentService extends AbstractService { + + /** + * 게시물 레파지토리 인터페이스 + */ + private final CommentRepository commentRepository; + + /** + * 게시물 서비스 + */ + private final PostsService postsService; + + /** + * 게시글의 댓글 전체 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Map 페이지 댓글 목록 응답 DTO + */ + public Map findAll(Integer boardNo, Integer postsNo) { + return findAll(boardNo, postsNo, null); + } + + /** + * 게시글의 댓글 전체 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param deleteAt 삭제 여부 + * @return Map 페이지 댓글 목록 응답 DTO + */ + public Map findAll(Integer boardNo, Integer postsNo, Integer deleteAt) { + List comments = commentRepository.findAll(boardNo, postsNo, deleteAt); + + // 페이지 인터페이스와 동일한 속성의 맵 리턴 + Map result = new HashMap<>(); + + result.put("content", comments); + result.put("empty", comments.isEmpty()); + result.put("first", true); + result.put("last", true); + result.put("number", 0); + result.put("numberOfElements", comments.size()); + result.put("size", comments.size()); + result.put("totalElements", comments.size()); + result.put("totalPages", 1); + + return result; + } + + /** + * 게시글의 댓글 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param pageable 페이지 정보 + * @return Map 페이지 댓글 목록 응답 DTO + */ + public Map findPage(Integer boardNo, Integer postsNo, Pageable pageable) { + return commentRepository.findPage(boardNo, postsNo, null, pageable); + } + + /** + * 게시글의 댓글 목록 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param deleteAt 삭제 여부 + * @param pageable 페이지 정보 + * @return Map 페이지 댓글 목록 응답 DTO + */ + public Map findPage(Integer boardNo, Integer postsNo, Integer deleteAt, Pageable pageable) { + return commentRepository.findPage(boardNo, postsNo, deleteAt, pageable); + } + + /** + * 댓글 등록 + * + * @param requestDto 댓글 등록 요청 DTO + */ + @Transactional + public CommentResponseDto save(CommentSaveRequestDto requestDto) { + if (requestDto.getBoardNo() == null || requestDto.getPostsNo() == null) { + throw new InvalidValueException(getMessage("err.invalid.input.value")); + } + + Posts posts = postsService.findPosts(requestDto.getBoardNo(), requestDto.getPostsNo()); + checkEditableComment(posts); // 저장 가능 여부 확인 + + Integer sortSeq; + if (requestDto.getParentCommentNo() != null) { // 대댓글 + sortSeq = commentRepository.findNextSortSeq(requestDto.getBoardNo(), requestDto.getPostsNo(), requestDto.getGroupNo(), requestDto.getParentCommentNo(), requestDto.getDepthSeq()); + if (sortSeq != null) { + commentRepository.updateSortSeq(requestDto.getGroupNo(), sortSeq, null, 1); // 들어갈 위치와 같거나 큰 대댓글 정렬 순서 +1 + } else { + sortSeq = commentRepository.findLastSortSeq(requestDto.getBoardNo(), requestDto.getPostsNo(), requestDto.getGroupNo()); // 들어갈 위치가 검색되지 않으면 max + } + } else { + sortSeq = 1; + } + + Integer commentNo = commentRepository.findNextCommentNo(requestDto.getBoardNo(), requestDto.getPostsNo()); // 댓글 번호 채번 + Integer groupNo; // 댓글 그룹 번호 + if (requestDto.getGroupNo() != null) groupNo = requestDto.getGroupNo(); + else groupNo = commentNo; // 최상위 댓글 + + Comment entity = commentRepository.save(requestDto.toEntity(posts, commentNo, groupNo, sortSeq)); + + return new CommentResponseDto(entity); + } + + /** + * 댓글 수정(작성자 체크) + * + * @param requestDto 댓글 수정 요청 DTO + */ + @Transactional + public CommentResponseDto update(CommentUpdateRequestDto requestDto, String userId) { + Comment entity = findCommentByCreatedBy(requestDto.getBoardNo(), requestDto.getPostsNo(), requestDto.getCommentNo(), userId); + + checkEditableComment(entity.getPosts()); // 저장 가능 여부 확인 + + // 수정 + entity.update(requestDto.getCommentContent()); + + return new CommentResponseDto(entity); + } + + /** + * 댓글 삭제(작성자 체크) + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + */ + @Transactional + public void delete(Integer boardNo, Integer postsNo, Integer commentNo, String userId) { + Comment entity = findCommentByCreatedBy(boardNo, postsNo, commentNo, userId); + + checkEditableComment(entity.getPosts()); // 변경 가능 여부 확인 + + entity.updateDeleteAt(1); // 작성자 삭제 + } + + /** + * 댓글 수정 + * + * @param requestDto 댓글 수정 요청 DTO + */ + @Transactional + public CommentResponseDto update(CommentUpdateRequestDto requestDto) { + Comment entity = findComment(requestDto.getBoardNo(), requestDto.getPostsNo(), requestDto.getCommentNo()); + + // 수정 + entity.update(requestDto.getCommentContent()); + + return new CommentResponseDto(entity); + } + + /** + * 댓글 삭제 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + */ + @Transactional + public void delete(Integer boardNo, Integer postsNo, Integer commentNo) { + Comment entity = findComment(boardNo, postsNo, commentNo); + + entity.updateDeleteAt(2); // 관리자 삭제 - 관리자가 본인 댓글 지울 경우 관리자 삭제로 처리 + } + + /** + * 댓글 기본키로 댓글 엔티티 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + * @return Comment 댓글 엔티티 + */ + private Comment findComment(Integer boardNo, Integer postsNo, Integer commentNo) { + if (boardNo == null || postsNo == null || commentNo == null) { + throw new InvalidValueException(getMessage("err.invalid.input.value")); + } + + CommentId id = CommentId.builder() + .postsId(PostsId.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .build()) + .commentNo(commentNo) + .build(); + + return commentRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("comment")}))); // 게시물이(가) 없습니다. + } + + /** + * 댓글 기본키로 댓글 엔티티 조회 + * 작성자 체크하여 본인 댓글이 아닌 경우 예외 발생 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + * @param userId 사용자 id + * @return Comment 댓글 엔티티 + */ + private Comment findCommentByCreatedBy(Integer boardNo, Integer postsNo, Integer commentNo, String userId) { + if (userId == null) { + throw new BusinessMessageException(getMessage("err.required.login")); // 로그인 후 다시 시도해주세요. + } + + Comment entity = findComment(boardNo, postsNo, commentNo); + + if (!userId.equals(entity.getCreatedBy())) { + throw new BusinessMessageException(getMessage("err.unauthorized")); // 권한이 불충분합니다 + } + + return entity; + } + + /** + * 댓글 등록/수정/삭제 가능 여부 확인 + * 댓글 사용 여부, 게시물 삭제 여부 체크해서 예외 발생 + * + * @param posts 게시물 엔티티 + */ + private void checkEditableComment(Posts posts) { + Board board = posts.getBoard(); + if (board == null) { + throw new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("board")})); // 게시판이(가) 없습니다. + } + if (Boolean.FALSE.equals(board.getCommentUseAt())) { + throw new BusinessMessageException(getMessage("err.board.not_use_comment")); // 댓글 사용이 금지된 게시판입니다. + } + if (posts.getDeleteAt().compareTo(0) > 0) { + throw new BusinessMessageException(getMessage("err.posts.deleted")); // 삭제된 게시물입니다. + } + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/posts/PostsService.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/posts/PostsService.java new file mode 100644 index 0000000..80cb705 --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/service/posts/PostsService.java @@ -0,0 +1,411 @@ +package org.egovframe.cloud.boardservice.service.posts; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsDeleteRequestDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsListResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsSaveRequestDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsSimpleSaveRequestDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsUpdateRequestDto; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; +import org.egovframe.cloud.boardservice.domain.posts.PostsRead; +import org.egovframe.cloud.boardservice.domain.posts.PostsReadRepository; +import org.egovframe.cloud.boardservice.domain.posts.PostsRepository; +import org.egovframe.cloud.boardservice.service.board.BoardService; +import org.egovframe.cloud.common.dto.AttachmentEntityMessage; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.exception.InvalidValueException; +import org.egovframe.cloud.common.service.AbstractService; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.postsservice.service.posts.PostsService + *

+ * 게시물 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/28 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/28    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class PostsService extends AbstractService { + + /** + * 게시물 레파지토리 인터페이스 + */ + private final PostsRepository postsRepository; + + /** + * 게시물 조회 레파지토리 인터페이스 + */ + private final PostsReadRepository postsReadRepository; + + /** + * 게시판 서비스 클래스 + */ + private final BoardService boardService; + + /** + * 이벤트 메시지 발행하기 위한 spring cloud stream 유틸리티 클래스 + */ + private final StreamBridge streamBridge; + + /** + * 조회 조건에 일치하는 게시물 페이지 목록 조회 + * + * @param boardNo 게시판 번호 + * @param deleteAt 삭제 여부 + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 게시물 목록 응답 DTO + */ + public Page findPage(Integer boardNo, Integer deleteAt, RequestDto requestDto, Pageable pageable) { + if (boardNo == null || boardNo <= 0) throw new InvalidValueException(getMessage("err.invalid.input.value")); + return postsRepository.findPage(boardNo, deleteAt, requestDto, pageable); + } + + /** + * 최근 게시물이 포함된 게시판 목록 조회 + * + * @param boardNos 게시판 번호 목록 + * @param postsCount 게시물 수 + * @return Map 최근 게시물이 포함된 게시판 상세 응답 DTO Map + */ + public Map findNewest(List boardNos, Integer postsCount) { + if (boardNos == null || boardNos.isEmpty()) + throw new InvalidValueException(getMessage("err.invalid.input.value")); + + List boards = boardService.findAllByBoardNos(boardNos); + + List allPosts = postsRepository.findAllByBoardNosLimitCount(boardNos, postsCount); + Map> postsGroup = allPosts.stream().collect(Collectors.groupingBy(PostsSimpleResponseDto::getBoardNo, Collectors.toList())); + + Map data = new HashMap<>(); // 요청한 게시판 순서로 리턴하기 위해서 map 리턴 + for (BoardResponseDto board : boards) { + List posts = postsGroup.get(board.getBoardNo()) + .stream().map(post -> post.setIsNew(board)) + .collect(Collectors.toList()); + board.setNewestPosts(posts); + data.put(board.getBoardNo(), board); + } + + return data; + } + + /** + * 게시물 단건 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param userId 사용자 id + * @param ipAddr ip 주소 + * @return PostsResponseDto 게시물 응답 DTO + */ + @Transactional + public PostsResponseDto findById(Integer boardNo, Integer postsNo, Integer deleteAt, String userId, String ipAddr) { + return findById(boardNo, postsNo, deleteAt, userId, ipAddr, null); + } + + /** + * 게시물 단건 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param deleteAt 삭제 여부 + * @param userId 사용자 id + * @param ipAddr ip 주소 + * @param requestDto 요청 DTO + * @return PostsResponseDto 게시물 응답 DTO + */ + @Transactional + public PostsResponseDto findById(Integer boardNo, Integer postsNo, Integer deleteAt, String userId, String ipAddr, RequestDto requestDto) { + PostsResponseDto dto = postsRepository.findById(boardNo, postsNo, userId, ipAddr); + + if (dto == null) { + throw new EntityNotFoundException("not found posts : "+ boardNo + ", " + postsNo + ", " + userId + ", " + ipAddr); + } + + // 삭제 여부 확인 + if (deleteAt != null && deleteAt.intValue() != dto.getDeleteAt().intValue()) { + throw new BusinessMessageException(getMessage("err.posts.deleted")); + } + + if (dto.getUserPostsReadCount() == 0) { + // 게시판 조회 등록 + Integer readNo = postsReadRepository.findNextReadNo(boardNo, postsNo); + PostsRead postsRead = PostsRead.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .readNo(readNo) + .userId(userId) + .ipAddr(ipAddr) + .build(); + + postsReadRepository.save(postsRead); + + // 조회 수 증가 + postsRepository.updateReadCount(boardNo, postsNo); + + // dto 조회 수 증가 + dto.increaseReadCount(); + } + + // 이전글, 다음글 조회 + if (requestDto != null) { + List prevPosts = postsRepository.findNearPost(boardNo, postsNo, -1, deleteAt, requestDto); + dto.setPrevPosts(prevPosts); + List nextPosts = postsRepository.findNearPost(boardNo, postsNo, 1, deleteAt, requestDto); + dto.setNextPosts(nextPosts); + } + + return dto; + } + + /** + * 게시물 등록 + * + * @param boardNo 게시판 번호 + * @param requestDto 게시물 등록 요청 DTO + * @return PostsResponseDto 게시물 응답 DTO + */ + @Transactional + public PostsResponseDto save(Integer boardNo, PostsSaveRequestDto requestDto) { + Integer postsNo = postsRepository.findNextPostsNo(boardNo); + + Posts entity = postsRepository.save(requestDto.toEntity(boardNo, postsNo)); + + /** + * 첨부파일 entity 정보 저장 이벤트 발생 + */ + sendAttachmentEvent(entity); + + return new PostsResponseDto(entity); + } + + /** + * 게시물 수정(권한 체크 안함) + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param requestDto 게시물 수정 요청 DTO + * @return PostsResponseDto 게시물 응답 DTO + */ + @Transactional + public PostsResponseDto update(Integer boardNo, Integer postsNo, PostsUpdateRequestDto requestDto) { + Posts entity = findPosts(boardNo, postsNo); + + // 수정 + entity.update(requestDto.getPostsTitle(), requestDto.getPostsContent(), requestDto.getPostsAnswerContent(), requestDto.getAttachmentCode(), requestDto.getNoticeAt()); + + return new PostsResponseDto(entity); + } + + /** + * 게시물 다건 삭제(권한 체크 안함) + * + * @param requestDtoList 게시물 삭제 요청 DTO List + * @param userId 사용자 id + * @return Long 삭제 건수 + */ + @Transactional + public Long remove(List requestDtoList, String userId) { + Map> data = requestDtoList.stream() + .collect(Collectors.groupingBy(PostsDeleteRequestDto::getBoardNo, + Collectors.mapping(PostsDeleteRequestDto::getPostsNo, Collectors.toList()))); + + return postsRepository.updateDeleteAt(data, 2, userId); + } + + /** + * 게시물 다건 복원(권한 체크 안함) + * + * @param requestDtoList 게시물 삭제 요청 DTO List + * @param userId 사용자 id + * @return Long 복원 건수 + */ + @Transactional + public Long restore(List requestDtoList, String userId) { + Map> data = requestDtoList.stream() + .collect(Collectors.groupingBy(PostsDeleteRequestDto::getBoardNo, + Collectors.mapping(PostsDeleteRequestDto::getPostsNo, Collectors.toList()))); + + return postsRepository.updateDeleteAt(data, 0, userId); + } + + /** + * 게시물 다건 완전 삭제(권한 체크 안함) + * + * @param requestDtoList 게시물 삭제 요청 DTO List + */ + @Transactional + public void delete(List requestDtoList) { + List deleteEntityList = requestDtoList.stream() + .map(PostsDeleteRequestDto::toEntity) + .collect(Collectors.toList()); + + // 일괄 처리 + postsRepository.deleteAll(deleteEntityList); + } + + /** + * 게시물 등록(작성자 체크) + * + * @param boardNo 게시판 번호 + * @param requestDto 게시물 등록 요청 DTO + * @return PostsResponseDto 게시물 응답 DTO + */ + @Transactional + public PostsResponseDto save(Integer boardNo, PostsSimpleSaveRequestDto requestDto, String userId) { + checkUserWritable(boardNo); + + Integer postsNo = postsRepository.findNextPostsNo(boardNo); + + Posts entity = postsRepository.save(requestDto.toEntity(boardNo, postsNo)); + + /** + * 첨부파일 entity 정보 저장 이벤트 발생 + */ + sendAttachmentEvent(entity); + + return new PostsResponseDto(entity); + } + + /** + * 게시물 수정(작성자 체크) + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param requestDto 게시물 수정 요청 DTO + * @param userId 사용자 id + * @return PostsResponseDto 게시물 응답 DTO + */ + @Transactional + public PostsResponseDto update(Integer boardNo, Integer postsNo, PostsSimpleSaveRequestDto requestDto, String userId) { + Posts entity = findPostsByCreatedBy(boardNo, postsNo, userId); + + // 수정 + entity.update(requestDto.getPostsTitle(), requestDto.getPostsContent(), requestDto.getAttachmentCode()); + + return new PostsResponseDto(entity); + } + + /** + * 게시물 삭제(작성자 체크) + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param userId 사용자 id + */ + @Transactional + public void remove(Integer boardNo, Integer postsNo, String userId) { + Posts entity = findPostsByCreatedBy(boardNo, postsNo, userId); + + // 삭제 여부 수정 + entity.updateDeleteAt(1); + } + + /** + * 게시물 번호로 게시물 엔티티 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Posts 게시물 엔티티 + * @throws InvalidValueException 입력값 예외 + * @throws EntityNotFoundException 엔티티 예외 + */ + public Posts findPosts(Integer boardNo, Integer postsNo) throws InvalidValueException, EntityNotFoundException { + if (boardNo == null || postsNo == null) { + throw new InvalidValueException(getMessage("err.invalid.input.value")); + } + + PostsId id = PostsId.builder().boardNo(boardNo).postsNo(postsNo).build(); + + return postsRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("posts")}))); + } + + /** + * 게시물 번호, 작성자로 게시물 엔티티 조회 + * 로그인 확인 + * 로그인 사용자가 작성자인지 확인 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param userId 사용자 id + * @return Posts 게시물 엔티티 + * @throws BusinessMessageException 비지니스 예외 + */ + private Posts findPostsByCreatedBy(Integer boardNo, Integer postsNo, String userId) throws BusinessMessageException { + if (userId == null || "".equals(userId)) { + throw new BusinessMessageException(getMessage("err.required.login")); // 로그인 후 다시 시도해주세요. + } + + Posts entity = findPosts(boardNo, postsNo); + + if (!userId.equals(entity.getCreatedBy())) { + throw new BusinessMessageException(getMessage("err.unauthorized")); // 권한이 불충분합니다 + } + + checkUserWritable(boardNo); + + return entity; + } + + /** + * 게시판 사용자 작성 여부 확인 + * + * @param boardNo 게시판 번호 + * @throws BusinessMessageException 비지니스 예외 + */ + private void checkUserWritable(Integer boardNo) throws BusinessMessageException { + BoardResponseDto board = boardService.findById(boardNo); + if (!Boolean.TRUE.equals(board.getUserWriteAt())) { + // 게시판 작성 가능 여부를 알려줄 필요는 없다. + throw new BusinessMessageException(getMessage("err.unauthorized")); // 권한이 불충분합니다. + } + } + + /** + * 첨부파일 entity 정보 업데이트 하기 위해 이벤트 메세지 발행 + * + * @param entity + */ + private void sendAttachmentEvent(Posts entity) { + if (!StringUtils.hasText(entity.getAttachmentCode())) { + return; + } + sendAttachmentEntityInfo(streamBridge, + AttachmentEntityMessage.builder() + .attachmentCode(entity.getAttachmentCode()) + .entityName(entity.getClass().getName()) + .entityId(entity.getPostsId().toString()) + .build()); + } + +} diff --git a/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/util/HttpUtil.java b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/util/HttpUtil.java new file mode 100644 index 0000000..b30c1ae --- /dev/null +++ b/backend/board-service/src/main/java/org/egovframe/cloud/boardservice/util/HttpUtil.java @@ -0,0 +1,42 @@ +package org.egovframe.cloud.boardservice.util; + +import org.egovframe.cloud.common.domain.Role; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * org.egovframe.cloud.boardservice.util.HttpUtil + *

+ * HTTP 관련 유틸 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/09    jooho       최초 생성
+ * 
+ */ +public class HttpUtil { + + /** + * static method 만으로 구성된 유틸리티 클래스 + * 객체 생성 금지 + */ + private HttpUtil() { + throw new IllegalStateException("Http Utility Class"); + } + + /*** + * 관리자 권한 확인 + * @return boolean 관리자 여부 + */ + public static boolean isAdmin() { + return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream() + .anyMatch(r -> r.toString().equals(Role.ADMIN.getKey())); + } + +} diff --git a/backend/board-service/src/main/resources/application.yml b/backend/board-service/src/main/resources/application.yml new file mode 100644 index 0000000..50ced93 --- /dev/null +++ b/backend/board-service/src/main/resources/application.yml @@ -0,0 +1,28 @@ +server: + port: 0 # random port + +spring: + application: + name: board-service + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL57Dialect + storage_engine: innodb + format_sql: true + default_batch_fetch_size: 1000 + show-sql: true + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 50MB + +# config server actuator +management: + endpoints: + web: + exposure: + include: refresh, health, beans diff --git a/backend/board-service/src/main/resources/bootstrap.yml b/backend/board-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..be89492 --- /dev/null +++ b/backend/board-service/src/main/resources/bootstrap.yml @@ -0,0 +1,8 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: board-service # board-service.yml이 있으면 불러오게 된다 +# name: config-service # config-service의 application.yml 을 불러오게 된다 +# profiles: +# active: prod # application-prod.yml \ No newline at end of file diff --git a/backend/board-service/src/main/resources/logback-spring.xml b/backend/board-service/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..03aa0f4 --- /dev/null +++ b/backend/board-service/src/main/resources/logback-spring.xml @@ -0,0 +1,34 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n + + + + + + + + + + + + + + + + + ${destination} + + {"app.name":"${app_name}"} + + + + + + + + + \ No newline at end of file diff --git a/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/board/BoardApiControllerTest.java b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/board/BoardApiControllerTest.java new file mode 100644 index 0000000..78a0e02 --- /dev/null +++ b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/board/BoardApiControllerTest.java @@ -0,0 +1,440 @@ +package org.egovframe.cloud.boardservice.api.board; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.assertj.core.api.Condition; +import org.egovframe.cloud.boardservice.api.board.dto.BoardListResponseDto; +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.boardservice.domain.board.Board; +import org.egovframe.cloud.boardservice.domain.board.BoardRepository; +import org.egovframe.cloud.boardservice.util.RestResponsePage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import lombok.extern.slf4j.Slf4j; + +/** + * org.egovframe.cloud.boardservice.api.board.BoardApiControllerTest + *

+ * 게시판 Rest API 컨트롤러 테스트 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@Slf4j +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +public class BoardApiControllerTest { + + /** + * test rest template + */ + @Autowired + TestRestTemplate restTemplate; + + /** + * 게시판 레파지토리 인터페이스 + */ + @Autowired + BoardRepository boardRepository; + + /** + * 게시판 API 경로 + */ + private static final String URL = "/api/v1/boards"; + + /** + * 테스트 데이터 등록 횟수 + */ + private final Integer GIVEN_DATA_COUNT = 10; + + /** + * 테스트 데이터 + */ + private final String BOARD_NAME_PREFIX = "게시판 명"; + private final String SKIN_TYPE_CODE_PREFIX = "000"; + + private final Integer BOARD_NO = GIVEN_DATA_COUNT + 1; + private final String INSERT_BOARD_NAME = BOARD_NAME_PREFIX + "_" + BOARD_NO; + private final String INSERT_SKIN_TYPE_CODE = SKIN_TYPE_CODE_PREFIX + "_" + BOARD_NO; + private final Integer INSERT_TITLE_DISPLAY_LENGTH = 50; + private final Integer INSERT_POST_DISPLAY_COUNT = 10; + private final Integer INSERT_PAGE_DISPLAY_COUNT = 10; + private final Integer INSERT_NEW_DISPLAY_COUNT = 3; + private final Boolean INSERT_EDITOR_USE_AT = true; + private final Boolean INSERT_UPLOAD_USE_AT = true; + private final Boolean INSERT_USER_WRITE_AT = true; + private final Boolean INSERT_COMMENT_USE_AT = true; + private final Integer INSERT_UPLOAD_FILE_COUNT = 5; + private final BigDecimal INSERT_UPLOAD_LIMIT_SIZE = new BigDecimal("104857600"); + + private final String UPDATE_BOARD_NAME = BOARD_NAME_PREFIX + "_" + (BOARD_NO + 1); + private final String UPDATE_SKIN_TYPE_CODE = SKIN_TYPE_CODE_PREFIX + "_" + (BOARD_NO + 1); + private final Integer UPDATE_TITLE_DISPLAY_LENGTH = 50 + 1; + private final Integer UPDATE_POST_DISPLAY_COUNT = 10 + 1; + private final Integer UPDATE_PAGE_DISPLAY_COUNT = 10 + 1; + private final Integer UPDATE_NEW_DISPLAY_COUNT = 3 + 1; + private final Boolean UPDATE_EDITOR_USE_AT = false; + private final Boolean UPDATE_UPLOAD_USE_AT = false; + private final Boolean UPDATE_USER_WRITE_AT = false; + private final Boolean UPDATE_COMMENT_USE_AT = false; + private final Integer UPDATE_UPLOAD_FILE_COUNT = 5 + 1; + private final BigDecimal UPDATE_UPLOAD_LIMIT_SIZE = new BigDecimal("209715200"); + + /** + * 테스트 시작 전 수행 + */ + @BeforeEach + void setUp() { + log.info("###setUp"); + } + + /** + * 테스트 종료 후 수행 + */ + @AfterEach + void tearDown() { + log.info("###tearDown"); + + //게시판 삭제 + boardRepository.deleteAll(); + } + + /** + * 게시판 페이지 목록 조회 테스트 + */ + @Test + void 게시판_페이지_목록_조회() { + log.info("###게시판_페이지_목록_조회"); + + // given + insertBoards(); + + String queryString = "?keywordType=boardName&keyword=" + BOARD_NAME_PREFIX; // 검색 조건 + queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보 + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + URL + queryString, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + RestResponsePage page = responseEntity.getBody(); + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT); + assertThat(page.getContent()) + .isNotEmpty() + .has(new Condition<>(l -> (BOARD_NAME_PREFIX + "_10").equals(l.get(0).getBoardName()), "BoardApiControllerTest.findPage contains " + BOARD_NAME_PREFIX + "_10")) + .has(new Condition<>(l -> (BOARD_NAME_PREFIX + "_9").equals(l.get(1).getBoardName()), "BoardApiControllerTest.findPage contains " + BOARD_NAME_PREFIX + "_9")); + } + + /** + * 게시판 상세 조회 테스트 + */ + @Test + void 게시판_상세_조회() { + log.info("###게시판_상세_조회"); + + // given + Board entity = insertBoard(); + + final Integer boardNo = entity.getBoardNo(); + + String url = URL + "/" + boardNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + BoardResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + assertThat(dto.getBoardNo()).isEqualTo(boardNo); + assertThat(dto.getBoardName()).isEqualTo(INSERT_BOARD_NAME); + assertThat(dto.getSkinTypeCode()).isEqualTo(INSERT_SKIN_TYPE_CODE); + assertThat(dto.getTitleDisplayLength()).isEqualTo(INSERT_TITLE_DISPLAY_LENGTH); + assertThat(dto.getPostDisplayCount()).isEqualTo(INSERT_POST_DISPLAY_COUNT); + assertThat(dto.getPageDisplayCount()).isEqualTo(INSERT_PAGE_DISPLAY_COUNT); + assertThat(dto.getNewDisplayDayCount()).isEqualTo(INSERT_NEW_DISPLAY_COUNT); + assertThat(dto.getEditorUseAt()).isEqualTo(INSERT_EDITOR_USE_AT); + assertThat(dto.getUploadUseAt()).isEqualTo(INSERT_UPLOAD_USE_AT); + assertThat(dto.getUserWriteAt()).isEqualTo(INSERT_USER_WRITE_AT); + assertThat(dto.getCommentUseAt()).isEqualTo(INSERT_COMMENT_USE_AT); + assertThat(dto.getUploadLimitCount()).isEqualTo(INSERT_UPLOAD_FILE_COUNT); + assertThat(dto.getUploadLimitSize()).isEqualTo(INSERT_UPLOAD_LIMIT_SIZE); + + deleteBoard(boardNo); + } + + /** + * 게시판 등록 테스트 + */ + @Test + void 게시판_등록() { + log.info("###게시판_등록"); + + // given + Map params = new HashMap<>(); + params.put("boardName", INSERT_BOARD_NAME); + params.put("skinTypeCode", INSERT_SKIN_TYPE_CODE); + params.put("titleDisplayLength", INSERT_TITLE_DISPLAY_LENGTH); + params.put("postDisplayCount", INSERT_POST_DISPLAY_COUNT); + params.put("pageDisplayCount", INSERT_PAGE_DISPLAY_COUNT); + params.put("newDisplayDayCount", INSERT_NEW_DISPLAY_COUNT); + params.put("editorUseAt", INSERT_EDITOR_USE_AT); + params.put("uploadUseAt", INSERT_UPLOAD_USE_AT); + params.put("userWriteAt", INSERT_USER_WRITE_AT); + params.put("commentUseAt", INSERT_COMMENT_USE_AT); + params.put("uploadLimitCount", INSERT_UPLOAD_FILE_COUNT); + params.put("uploadLimitSize", INSERT_UPLOAD_LIMIT_SIZE); + HttpEntity> httpEntity = new HttpEntity<>(params); + + // when + //ResponseEntity responseEntity = restTemplate.postForEntity(URL, requestDto, PostsResponseDto.class); + ResponseEntity responseEntity = restTemplate.exchange( + URL, + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + BoardResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + final Integer boardNo = dto.getBoardNo(); + + Optional board = selectData(boardNo); + assertThat(board).isPresent(); + + Board entity = board.get(); + assertThat(entity.getBoardNo()).isEqualTo(boardNo); + assertThat(entity.getBoardName()).isEqualTo(INSERT_BOARD_NAME); + assertThat(entity.getSkinTypeCode()).isEqualTo(INSERT_SKIN_TYPE_CODE); + assertThat(entity.getTitleDisplayLength()).isEqualTo(INSERT_TITLE_DISPLAY_LENGTH); + assertThat(entity.getPostDisplayCount()).isEqualTo(INSERT_POST_DISPLAY_COUNT); + assertThat(entity.getPageDisplayCount()).isEqualTo(INSERT_PAGE_DISPLAY_COUNT); + assertThat(entity.getNewDisplayDayCount()).isEqualTo(INSERT_NEW_DISPLAY_COUNT); + assertThat(entity.getEditorUseAt()).isEqualTo(INSERT_EDITOR_USE_AT); + assertThat(entity.getUploadUseAt()).isEqualTo(INSERT_UPLOAD_USE_AT); + assertThat(entity.getUserWriteAt()).isEqualTo(INSERT_USER_WRITE_AT); + assertThat(entity.getCommentUseAt()).isEqualTo(INSERT_COMMENT_USE_AT); + assertThat(entity.getUploadLimitCount()).isEqualTo(INSERT_UPLOAD_FILE_COUNT); + assertThat(entity.getUploadLimitSize()).isEqualTo(INSERT_UPLOAD_LIMIT_SIZE); + + deleteBoard(boardNo); + } + + /** + * 게시판 수정 테스트 + */ + @Test + void 게시판_수정() { + log.info("###게시판_수정"); + + // given + Board entity = insertBoard(); + + final Integer boardNo = entity.getBoardNo(); + + Map params = new HashMap<>(); + params.put("boardName", UPDATE_BOARD_NAME); + params.put("skinTypeCode", UPDATE_SKIN_TYPE_CODE); + params.put("titleDisplayLength", UPDATE_TITLE_DISPLAY_LENGTH); + params.put("postDisplayCount", UPDATE_POST_DISPLAY_COUNT); + params.put("pageDisplayCount", UPDATE_PAGE_DISPLAY_COUNT); + params.put("newDisplayDayCount", UPDATE_NEW_DISPLAY_COUNT); + params.put("editorUseAt", UPDATE_EDITOR_USE_AT); + params.put("uploadUseAt", UPDATE_UPLOAD_USE_AT); + params.put("userWriteAt", UPDATE_USER_WRITE_AT); + params.put("commentUseAt", UPDATE_COMMENT_USE_AT); + params.put("uploadLimitCount", UPDATE_UPLOAD_FILE_COUNT); + params.put("uploadLimitSize", UPDATE_UPLOAD_LIMIT_SIZE); + HttpEntity> httpEntity = new HttpEntity<>(params); + + String url = URL + "/" + boardNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + BoardResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + Optional board = selectData(boardNo); + assertThat(board).isPresent(); + + Board updatedBoard = board.get(); + assertThat(updatedBoard.getBoardNo()).isEqualTo(boardNo); + assertThat(updatedBoard.getBoardName()).isEqualTo(UPDATE_BOARD_NAME); + assertThat(updatedBoard.getSkinTypeCode()).isEqualTo(UPDATE_SKIN_TYPE_CODE); + assertThat(updatedBoard.getTitleDisplayLength()).isEqualTo(UPDATE_TITLE_DISPLAY_LENGTH); + assertThat(updatedBoard.getPostDisplayCount()).isEqualTo(UPDATE_POST_DISPLAY_COUNT); + assertThat(updatedBoard.getPageDisplayCount()).isEqualTo(UPDATE_PAGE_DISPLAY_COUNT); + assertThat(updatedBoard.getNewDisplayDayCount()).isEqualTo(UPDATE_NEW_DISPLAY_COUNT); + assertThat(updatedBoard.getEditorUseAt()).isEqualTo(UPDATE_EDITOR_USE_AT); + assertThat(updatedBoard.getUploadUseAt()).isEqualTo(UPDATE_UPLOAD_USE_AT); + assertThat(updatedBoard.getUserWriteAt()).isEqualTo(UPDATE_USER_WRITE_AT); + assertThat(updatedBoard.getCommentUseAt()).isEqualTo(UPDATE_COMMENT_USE_AT); + assertThat(updatedBoard.getUploadLimitCount()).isEqualTo(UPDATE_UPLOAD_FILE_COUNT); + assertThat(updatedBoard.getUploadLimitSize()).isEqualTo(UPDATE_UPLOAD_LIMIT_SIZE); + + deleteBoard(boardNo); + } + + /** + * 게시판 삭제 테스트 + */ + @Test + void 게시판_삭제() { + log.info("###게시판_삭제"); + + // given + Board entity = insertBoard(); + + final Integer boardNo = entity.getBoardNo(); + + String url = URL + "/" + boardNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.DELETE, + null, + BoardResponseDto.class + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Optional board = selectData(boardNo); + assertThat(board).isNotPresent(); + } + + /** + * 테스트 데이터 등록 + */ + private void insertBoards() { + log.info("###테스트 데이터 등록"); + + List list = new ArrayList<>(); + for (int i = 1; i <= GIVEN_DATA_COUNT; i++) { + list.add(Board.builder() + .boardName(BOARD_NAME_PREFIX + "_" + i) + .skinTypeCode(SKIN_TYPE_CODE_PREFIX + "_" + i) + .titleDisplayLength(INSERT_TITLE_DISPLAY_LENGTH + i) + .postDisplayCount(INSERT_POST_DISPLAY_COUNT + i) + .pageDisplayCount(INSERT_PAGE_DISPLAY_COUNT + i) + .newDisplayDayCount(INSERT_NEW_DISPLAY_COUNT + i) + .editorUseAt(i % 2 == 1) + .uploadUseAt(i % 3 == 0) + .userWriteAt(i % 3 == 0) + .commentUseAt(i % 2 == 0) + .uploadLimitCount(INSERT_UPLOAD_FILE_COUNT + i) + .uploadLimitSize(INSERT_UPLOAD_LIMIT_SIZE.add(new BigDecimal(String.valueOf(i)))) + .build()); + } + + boardRepository.saveAll(list); + } + + /** + * 테스트 데이터 단건 등록 + * + * @return Board 게시판 엔티티 + */ + private Board insertBoard() { + log.info("###테스트 데이터 단건 등록"); + + return boardRepository.save(Board.builder() + .boardName(INSERT_BOARD_NAME) + .skinTypeCode(INSERT_SKIN_TYPE_CODE) + .titleDisplayLength(INSERT_TITLE_DISPLAY_LENGTH) + .postDisplayCount(INSERT_POST_DISPLAY_COUNT) + .pageDisplayCount(INSERT_PAGE_DISPLAY_COUNT) + .newDisplayDayCount(INSERT_NEW_DISPLAY_COUNT) + .editorUseAt(INSERT_EDITOR_USE_AT) + .uploadUseAt(INSERT_UPLOAD_USE_AT) + .userWriteAt(INSERT_USER_WRITE_AT) + .commentUseAt(INSERT_COMMENT_USE_AT) + .uploadLimitCount(INSERT_UPLOAD_FILE_COUNT) + .uploadLimitSize(INSERT_UPLOAD_LIMIT_SIZE) + .build()); + } + + /** + * 테스트 데이터 단건 삭제 + */ + private void deleteBoard(Integer boardNo) { + log.info("###테스트 데이터 단건 삭제"); + + boardRepository.deleteById(boardNo); + } + + /** + * 테스트 데이터 단건 조회 + * + * @param boardNo 게시판 번호 + * @return Optional 게시판 엔티티 + */ + private Optional selectData(Integer boardNo) { + log.info("###테스트 데이터 단건 조회"); + + return boardRepository.findById(boardNo); + } + +} diff --git a/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/comment/CommentApiControllerTest.java b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/comment/CommentApiControllerTest.java new file mode 100644 index 0000000..fd00614 --- /dev/null +++ b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/comment/CommentApiControllerTest.java @@ -0,0 +1,585 @@ +package org.egovframe.cloud.boardservice.api.comment; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto; +import org.egovframe.cloud.boardservice.api.comment.dto.CommentResponseDto; +import org.egovframe.cloud.boardservice.domain.board.Board; +import org.egovframe.cloud.boardservice.domain.board.BoardRepository; +import org.egovframe.cloud.boardservice.domain.comment.Comment; +import org.egovframe.cloud.boardservice.domain.comment.CommentId; +import org.egovframe.cloud.boardservice.domain.comment.CommentRepository; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; +import org.egovframe.cloud.boardservice.domain.posts.PostsRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import lombok.extern.slf4j.Slf4j; + +/** + * org.egovframe.cloud.boardservice.api.posts.CommentApiControllerTest + *

+ * 댓글 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/11 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *  수정일        수정자       수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/11    jooho       최초 생성
+ * 
+ */ +@Slf4j +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class CommentApiControllerTest { + + /** + * test rest template + */ + @Autowired + TestRestTemplate restTemplate; + + /** + * 게시판 레파지토리 인터페이스 + */ + @Autowired + BoardRepository boardRepository; + + /** + * 게시물 레파지토리 인터페이스 + */ + @Autowired + PostsRepository postsRepository; + + /** + * 댓글 레파지토리 인터페이스 + */ + @Autowired + CommentRepository commentRepository; + + /** + * 게시판 API 경로 + */ + private static final String URL = "/api/v1/comments"; + + /** + * 테스트 데이터 등록 횟수 + */ + private final Integer GIVEN_DATA_COUNT = 10; + + /** + * 테스트 데이터 + */ + private Board board; + private Posts posts; + + private final Integer INSERT_COMMENT_NO = 1; + private final String INSERT_COMMENT_CONTENT = "댓글 내용"; + + private final String UPDATE_COMMENT_CONTENT = "댓글 내용 수정"; + + /** + * 테스트 시작 전 수행 + */ + @BeforeEach + void setUp() { + log.info("###setUp"); + + // 게시판 등록 + board = boardRepository.save(Board.builder() + .boardName("일반게시판1") + .skinTypeCode("normal") + .titleDisplayLength(50) + .postDisplayCount(50) + .pageDisplayCount(50) + .newDisplayDayCount(50) + .editorUseAt(true) + .uploadUseAt(true) + .uploadLimitCount(50) + .uploadLimitSize(new BigDecimal("52428800")) + .userWriteAt(true) + .commentUseAt(true) + .build()); + + // 게시물 등록 + posts = postsRepository.save(Posts.builder() + .board(board) + .postsId(PostsId.builder() + .boardNo(board.getBoardNo()) + .postsNo(1) + .build()) + .postsTitle("게시물 1") + .postsContent("게시물 내용 1") + .postsAnswerContent("게시물 답변 내용 1") + .attachmentCode("0000000001") + .readCount(0) + .noticeAt(false) + .deleteAt(0) + .build()); + } + + /** + * 테스트 종료 후 수행 + */ + @AfterEach + void tearDown() { + log.info("###tearDown"); + + // 댓글 삭제 + commentRepository.deleteAll(); + + // 게시물 삭제 + postsRepository.deleteAll(); + + // 게시판 삭제 + boardRepository.deleteAll(); + } + + /** + * 게시글의 전체 댓글 목록 조회 + */ + @Test + void 게시글의_전체_댓글_목록_조회() { + log.info("###게시글의_전체_댓글_목록_조회"); + + // given + insertComments(); + + final String url = URL + "/total/" + posts.getPostsId().getBoardNo() + "/" + posts.getPostsId().getPostsNo(); + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Map data = responseEntity.getBody(); + + assertThat(data).isNotNull(); + assertThat(data.get("numberOfElements")).isEqualTo(GIVEN_DATA_COUNT); + assertThat(data.get("totalElements")).isEqualTo(GIVEN_DATA_COUNT); + assertThat(data.get("totalPages")).isEqualTo(1); + } + + /** + * 게시글의 전체 미삭제 댓글 목록 조회 + */ + @Test + void 게시글의_전체_미삭제_댓글_목록_조회() { + log.info("###게시글의_전체_미삭제_댓글_목록_조회"); + + // given + insertComments(); + + final String url = URL + "/all/" + posts.getPostsId().getBoardNo() + "/" + posts.getPostsId().getPostsNo(); + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Map data = responseEntity.getBody(); + + assertThat(data).isNotNull(); + assertThat(data.get("numberOfElements")).isEqualTo(GIVEN_DATA_COUNT / 3); + assertThat(data.get("totalElements")).isEqualTo(GIVEN_DATA_COUNT / 3); + assertThat(data.get("totalPages")).isEqualTo(1); + } + + /** + * 게시글의 댓글 목록 조회 + */ + @Test + void 게시글의_댓글_목록_조회() { + log.info("###게시글의_댓글_목록_조회"); + + // given + insertComments(); + + final Integer page = 0; + final Integer size = 5; + final String url = URL + "/" + posts.getPostsId().getBoardNo() + "/" + posts.getPostsId().getPostsNo() + "?page=" + page + "&size=" + size; + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Map data = responseEntity.getBody(); + + assertThat(data).isNotNull(); + assertThat(data.get("numberOfElements")).isEqualTo(size); + assertThat(data.get("totalElements")).isEqualTo(GIVEN_DATA_COUNT); + assertThat(data.get("totalPages")).isEqualTo(GIVEN_DATA_COUNT / size); + } + + /** + * 게시글의 미삭제 댓글 목록 조회 + */ + @Test + void 게시글의_미삭제_댓글_목록_조회() { + log.info("###게시글의_미삭제_댓글_목록_조회"); + + // given + insertComments(); + + final Integer page = 0; + final Integer size = 5; + final String url = URL + "/list/" + posts.getPostsId().getBoardNo() + "/" + posts.getPostsId().getPostsNo() + "?page=" + page + "&size=" + size; + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Map data = responseEntity.getBody(); + + assertThat(data).isNotNull(); + assertThat(data.get("numberOfElements")).isEqualTo(GIVEN_DATA_COUNT / 3); + assertThat(data.get("totalElements")).isEqualTo(GIVEN_DATA_COUNT / 3); + assertThat(data.get("totalPages")).isEqualTo(1); + } + + /** + * 댓글 등록 + */ + @Test + void 댓글_등록() { + log.info("###댓글_등록"); + + // given + Map params = new HashMap<>(); + params.put("boardNo", posts.getPostsId().getBoardNo()); + params.put("postsNo", posts.getPostsId().getPostsNo()); + params.put("commentContent", INSERT_COMMENT_CONTENT); + params.put("depthSeq", 0); + params.put("parentCommentNo", null); + HttpEntity> httpEntity = new HttpEntity<>(params); + + // when + ResponseEntity responseEntity = restTemplate.exchange( + URL, + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + CommentResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + final Integer boardNo = dto.getBoardNo(); + final Integer postsNo = dto.getPostsNo(); + final Integer commentNo = dto.getCommentNo(); + + Optional comment = selectData(boardNo, postsNo, commentNo); + assertThat(comment).isPresent(); + + Comment entity = comment.get(); + assertThat(entity.getCommentId().getPostsId().getBoardNo()).isEqualTo(boardNo); + assertThat(entity.getCommentId().getPostsId().getPostsNo()).isEqualTo(postsNo); + assertThat(entity.getCommentId().getCommentNo()).isEqualTo(commentNo); + assertThat(entity.getCommentContent()).isEqualTo(INSERT_COMMENT_CONTENT); + assertThat(entity.getParentCommentNo()).isNull(); + assertThat(entity.getDeleteAt()).isZero(); + } + + /** + * 댓글 수정 작성자체크 + */ + @Test + void 댓글_수정_작성자체크() { + log.info("###댓글_수정 작성자체크"); + + // given + Comment entity = insertComment(); + + final Integer boardNo = entity.getCommentId().getPostsId().getBoardNo(); + final Integer postsNo = entity.getCommentId().getPostsId().getPostsNo(); + final Integer commentNo = entity.getCommentId().getCommentNo(); + + Map params = new HashMap<>(); + params.put("boardNo", boardNo); + params.put("postsNo", postsNo); + params.put("commentNo", commentNo); + params.put("commentContent", UPDATE_COMMENT_CONTENT); + HttpEntity> httpEntity = new HttpEntity<>(params); + + String url = URL + "/update"; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 본인글 아닌 경우 예외 발생 + } + + /** + * 댓글 삭제 작성자체크 + */ + @Test + void 댓글_삭제_작성자체크() { + log.info("###댓글_삭제 작성자체크"); + + // given + Comment entity = insertComment(); + + final Integer boardNo = entity.getCommentId().getPostsId().getBoardNo(); + final Integer postsNo = entity.getCommentId().getPostsId().getPostsNo(); + final Integer commentNo = entity.getCommentId().getCommentNo(); + + String url = URL + "/delete/" + boardNo + "/" + postsNo + "/" + commentNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.DELETE, + null, + BoardResponseDto.class + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 관리자권한 or 본인글 아닌 경우 예외 발생 + } + + /** + * 댓글 수정 + */ + @Test + void 댓글_수정() { + log.info("###댓글_수정"); + + // given + Comment entity = insertComment(); + + final Integer boardNo = entity.getCommentId().getPostsId().getBoardNo(); + final Integer postsNo = entity.getCommentId().getPostsId().getPostsNo(); + final Integer commentNo = entity.getCommentId().getCommentNo(); + + Map params = new HashMap<>(); + params.put("boardNo", boardNo); + params.put("postsNo", postsNo); + params.put("commentNo", commentNo); + params.put("commentContent", UPDATE_COMMENT_CONTENT); + HttpEntity> httpEntity = new HttpEntity<>(params); + + // when + ResponseEntity responseEntity = restTemplate.exchange( + URL, + HttpMethod.PUT, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + CommentResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + Optional comment = selectData(boardNo, postsNo, commentNo); + assertThat(comment).isPresent(); + + Comment updatedComment = comment.get(); + assertThat(updatedComment.getCommentId().getPostsId().getBoardNo()).isEqualTo(boardNo); + assertThat(updatedComment.getCommentId().getPostsId().getPostsNo()).isEqualTo(postsNo); + assertThat(updatedComment.getCommentId().getCommentNo()).isEqualTo(commentNo); + assertThat(updatedComment.getCommentContent()).isEqualTo(UPDATE_COMMENT_CONTENT); + } + + /** + * 댓글 삭제 + */ + @Test + void 댓글_삭제() { + log.info("###댓글_삭제"); + + // given + Comment entity = insertComment(); + + final Integer boardNo = entity.getCommentId().getPostsId().getBoardNo(); + final Integer postsNo = entity.getCommentId().getPostsId().getPostsNo(); + final Integer commentNo = entity.getCommentId().getCommentNo(); + + String url = URL + "/" + boardNo + "/" + postsNo + "/" + commentNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.DELETE, + null, + BoardResponseDto.class + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Optional comment = selectData(boardNo, postsNo, commentNo); + assertThat(comment).isPresent(); + + Comment deletedComment = comment.get(); + assertThat(deletedComment.getCommentId().getPostsId().getBoardNo()).isEqualTo(boardNo); + assertThat(deletedComment.getCommentId().getPostsId().getPostsNo()).isEqualTo(postsNo); + assertThat(deletedComment.getCommentId().getCommentNo()).isEqualTo(commentNo); + assertThat(deletedComment.getDeleteAt()).isNotZero(); + } + + /** + * 테스트 데이터 등록 + */ + private void insertComments() { + log.info("###테스트 데이터 등록"); + + // 댓글 등록 + List list = new ArrayList<>(); + for (int i = 1; i <= GIVEN_DATA_COUNT; i++) { + list.add(Comment.builder() + .posts(posts) + .commentId(CommentId.builder() + .postsId(PostsId.builder() + .boardNo(posts.getPostsId().getBoardNo()) + .postsNo(posts.getPostsId().getPostsNo()) + .build()) + .commentNo(i) + .build()) + .commentContent(INSERT_COMMENT_CONTENT + i) + .groupNo(i) + .depthSeq(0) + .sortSeq(i) + .deleteAt(i % 3) + .build()); + } + + commentRepository.saveAll(list); + } + + /** + * 테스트 데이터 단건 등록 + * + * @return Comment 댓글 엔티티 + */ + private Comment insertComment() { + log.info("###테스트 데이터 단건 등록"); + + return commentRepository.save(Comment.builder() + .posts(posts) + .commentId(CommentId.builder() + .postsId(PostsId.builder() + .boardNo(posts.getPostsId().getBoardNo()) + .postsNo(posts.getPostsId().getPostsNo()) + .build()) + .commentNo(INSERT_COMMENT_NO) + .build()) + .commentContent(INSERT_COMMENT_CONTENT) + .groupNo(INSERT_COMMENT_NO) + .depthSeq(0) + .sortSeq(INSERT_COMMENT_NO) + .deleteAt(0) + .build()); + } + + /** + * 테스트 데이터 단건 삭제 + */ + /*private void deleteComment(Comment comment) { + log.info("###테스트 데이터 단건 삭제"); + + commentRepository.deleteById(comment.getCommentId()); + }*/ + + /** + * 테스트 데이터 삭제 + */ + /*private void deleteComments() { + log.info("###테스트 데이터 삭제"); + + commentRepository.deleteAll(comments); + + comments.clear(); + }*/ + + /** + * 테스트 데이터 단건 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @param commentNo 댓글 번호 + * @return Optional 댓글 엔티티 + */ + private Optional selectData(Integer boardNo, Integer postsNo, Integer commentNo) { + log.info("###테스트 데이터 단건 조회"); + + return commentRepository.findById(CommentId.builder() + .postsId(PostsId.builder() + .boardNo(boardNo) + .postsNo(postsNo) + .build()) + .commentNo(commentNo) + .build()); + } + +} \ No newline at end of file diff --git a/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/posts/PostsApiControllerTest.java b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/posts/PostsApiControllerTest.java new file mode 100644 index 0000000..a404ede --- /dev/null +++ b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/api/posts/PostsApiControllerTest.java @@ -0,0 +1,730 @@ +package org.egovframe.cloud.boardservice.api.posts; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.lang.StringUtils; +import org.assertj.core.api.Condition; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsListResponseDto; +import org.egovframe.cloud.boardservice.api.posts.dto.PostsResponseDto; +import org.egovframe.cloud.boardservice.domain.board.Board; +import org.egovframe.cloud.boardservice.domain.board.BoardRepository; +import org.egovframe.cloud.boardservice.domain.posts.Posts; +import org.egovframe.cloud.boardservice.domain.posts.PostsId; +import org.egovframe.cloud.boardservice.domain.posts.PostsRepository; +import org.egovframe.cloud.boardservice.util.RestResponsePage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; + +/** + * org.egovframe.cloud.boardservice.api.posts.PostsApiControllerTest + *

+ * 게시물 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/10 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/10    jooho       최초 생성
+ * 
+ */ +@Slf4j +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class PostsApiControllerTest { + + /** + * test rest template + */ + @Autowired + TestRestTemplate restTemplate; + + /** + * 게시판 레파지토리 인터페이스 + */ + @Autowired + BoardRepository boardRepository; + + /** + * 게시물 레파지토리 인터페이스 + */ + @Autowired + PostsRepository postsRepository; + + /** + * 게시물 API 경로 + */ + private static final String URL = "/api/v1/posts"; + + /** + * 테스트 데이터 등록 횟수 + */ + private final Integer GIVEN_DATA_COUNT = 10; + + /** + * 테스트 데이터 + */ + private Board board; + private List posts = new ArrayList<>(); + + private final Integer INSERT_POSTS_NO = 1; + private final String INSERT_POSTS_TITLE = "게시물 제목"; + private final String INSERT_POSTS_CONTENT = "게시물 내용"; + private final String INSERT_POSTS_ANSWER_CONTENT = "게시물 답변 내용"; + private final String INSERT_ATTACHMENT_CODE = "0000000001"; + private final Integer INSERT_READ_COUNT = 0; + private final Boolean INSERT_NOTICE_AT = true; + + private final String UPDATE_POSTS_TITLE = "게시물 제목 수정"; + private final String UPDATE_POSTS_CONTENT = "게시물 내용 수정"; + private final String UPDATE_POSTS_ANSWER_CONTENT = "게시물 답변 내용 수정"; + private final String UPDATE_ATTACHMENT_CODE = "0000000002"; + private final Boolean UPDATE_NOTICE_AT = false; + + /** + * 테스트 시작 전 수행 + */ + @BeforeEach + void setUp() { + log.info("###setUp"); + + // 게시판 등록 + board = boardRepository.save(Board.builder() + .boardName("일반게시판1") + .skinTypeCode("normal") + .titleDisplayLength(50) + .postDisplayCount(50) + .pageDisplayCount(50) + .newDisplayDayCount(50) + .editorUseAt(true) + .uploadUseAt(true) + .uploadLimitCount(50) + .uploadLimitSize(new BigDecimal("52428800")) + .userWriteAt(false) + .commentUseAt(true) + .build()); + } + + /** + * 테스트 종료 후 수행 + */ + @AfterEach + void tearDown() { + log.info("###tearDown"); + + postsRepository.deleteAll(); + + // 게시판 삭제 + boardRepository.deleteAll(); + } + + /** + * 최근 게시물이 포함된 게시판 목록 조회 + * @throws JsonProcessingException json exception + * @throws JsonMappingException json exception + */ + @Test + void 최근_게시물이_포함된_게시판_목록_조회() throws JsonMappingException, JsonProcessingException { + log.info("###최근_게시물이_포함된_게시판_목록_조회"); + + // given + insertPosts(null); + + final Integer postsCount = 3; + final String url = URL + "/newest/" + board.getBoardNo() + "/" + postsCount; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode data = mapper.readTree(responseEntity.getBody()); + JsonNode boardData = data.get(board.getBoardNo().toString()); + + assertThat(data).isNotNull(); + assertThat(boardData.get("boardNo").asInt()).isEqualTo(board.getBoardNo()); + assertThat(boardData.get("posts").isArray()).isTrue(); + // assertThat(boardData.get("posts").size()).isEqualTo(postsCount); // h2에서 rownum을 계산하지 못해서 3건, 5건 조회될때가 있음. + } + + /** + * 게시물 삭제포함 페이지 목록 조회 + */ + @Test + void 게시물_삭제포함_페이지_목록_조회() { + log.info("###게시물_삭제포함_페이지_목록_조회"); + + // given + insertPosts(null); + + String url = URL + "/" + board.getBoardNo(); + String queryString = "?keywordType=postsTitle&keyword=" + INSERT_POSTS_TITLE; // 검색 조건 + queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보 + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + url + queryString, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + RestResponsePage page = responseEntity.getBody(); + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT); + assertThat(page.getContent()) + .isNotEmpty() + .has(new Condition<>(l -> (INSERT_POSTS_TITLE + "9").equals(l.get(0).getPostsTitle()) && l.get(0).getNoticeAt(), "PostsApiControllerTest.findPage contains [notice] " + INSERT_POSTS_TITLE + "9")) + .has(new Condition<>(l -> (INSERT_POSTS_TITLE + "6").equals(l.get(1).getPostsTitle()) && l.get(0).getNoticeAt(), "PostsApiControllerTest.findPage contains [notice] " + INSERT_POSTS_TITLE + "6")); + } + + /** + * 게시물 삭제제외 페이지 목록 조회 + */ + @Test + void 게시물_삭제제외_페이지_목록_조회() { + log.info("###게시물_삭제제외_페이지_목록_조회"); + + // given + insertPosts(null); + + String url = URL + "/list/" + board.getBoardNo(); + String queryString = "?keywordType=postsTitle&keyword=" + INSERT_POSTS_TITLE; // 검색 조건 + queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보 + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + url + queryString, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + RestResponsePage page = responseEntity.getBody(); + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT / 2); + assertThat(page.getContent()) + .isNotEmpty() + .has(new Condition<>(l -> (INSERT_POSTS_TITLE + "6").equals(l.get(0).getPostsTitle()) && l.get(0).getNoticeAt(), "PostsApiControllerTest.findPage contains [notice] " + INSERT_POSTS_TITLE + "6")) + .has(new Condition<>(l -> (INSERT_POSTS_TITLE + "10").equals(l.get(1).getPostsTitle()) && l.get(0).getNoticeAt(), "PostsApiControllerTest.findPage contains [notice] " + INSERT_POSTS_TITLE + "10")); + } + + /** + * 게시물 삭제포함 단건 조회 + */ + @Test + void 게시물_삭제포함_단건_조회() { + log.info("###게시물_삭제포함_단건_조회"); + + // given + final Posts post = insertPost(1); + + String url = URL + "/" + post.getPostsId().getBoardNo() + "/" + post.getPostsId().getPostsNo(); + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + PostsResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + assertThat(dto.getBoardNo()).isEqualTo(post.getPostsId().getBoardNo()); + assertThat(dto.getPostsNo()).isEqualTo(post.getPostsId().getPostsNo()); + assertThat(dto.getPostsTitle()).isEqualTo(post.getPostsTitle()); + assertThat(dto.getPostsContent()).isEqualTo(post.getPostsContent()); + assertThat(dto.getPostsAnswerContent()).isEqualTo(post.getPostsAnswerContent()); + assertThat(dto.getAttachmentCode()).isEqualTo(post.getAttachmentCode()); + assertThat(dto.getReadCount()).isEqualTo(post.getReadCount() + 1); + assertThat(dto.getNoticeAt()).isEqualTo(post.getNoticeAt()); + assertThat(dto.getDeleteAt()).isEqualTo(post.getDeleteAt()); + } + + /** + * 게시물 삭제제외 단건 조회 + */ + @Test + void 게시물_삭제제외_단건_조회() { + log.info("###게시물_삭제제외_단건_조회"); + + // given + final Posts post = insertPost(1); + + String url = URL + "/view/" + post.getPostsId().getBoardNo() + "/" + post.getPostsId().getPostsNo(); + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 삭제 게시물 조회 불가 + } + + /** + * 게시물 등록 작성자체크 + */ + @Test + void 게시물_등록_작성자체크() { + log.info("###게시물_등록_작성자체크"); + + // given + String url = URL + "/save/" + board.getBoardNo(); + + Map params = new HashMap<>(); + params.put("postsTitle", INSERT_POSTS_TITLE); + params.put("postsContent", INSERT_POSTS_CONTENT); + params.put("postsAnswerContent", INSERT_POSTS_ANSWER_CONTENT); + params.put("attachmentCode", INSERT_ATTACHMENT_CODE); + params.put("noticeAt", INSERT_NOTICE_AT); + HttpEntity> httpEntity = new HttpEntity<>(params); + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 게시판에 사용자 글쓰기 허용 X + } + + /** + * 게시물 수정 작성자체크 + */ + @Test + void 게시물_수정_작성자체크() { + log.info("###게시물_수정 작성자체크"); + + // given + Posts entity = insertPost(0); + + final Integer boardNo = entity.getPostsId().getBoardNo(); + final Integer postsNo = entity.getPostsId().getPostsNo(); + + Map params = new HashMap<>(); + params.put("postsTitle", UPDATE_POSTS_TITLE); + params.put("postsContent", UPDATE_POSTS_CONTENT); + params.put("postsAnswerContent", UPDATE_POSTS_ANSWER_CONTENT); + params.put("attachmentCode", UPDATE_ATTACHMENT_CODE); + params.put("noticeAt", UPDATE_NOTICE_AT); + HttpEntity> httpEntity = new HttpEntity<>(params); + + String url = URL + "/update/" + boardNo + "/" + postsNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 본인글 아닌 경우 예외 발생 + } + + /** + * 게시물 삭제 작성자체크 + */ + @Test + void 게시물_삭제_작성자체크() { + log.info("###게시물_삭제 작성자체크"); + + // given + Posts entity = insertPost(0); + + final Integer boardNo = entity.getPostsId().getBoardNo(); + final Integer postsNo = entity.getPostsId().getPostsNo(); + + String url = URL + "/remove/" + boardNo + "/" + postsNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.DELETE, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 본인글 아닌 경우 예외 발생 + } + + /** + * 게시물 등록 + */ + @Test + void 게시물_등록() { + log.info("###게시물_등록"); + + // given + String url = URL + "/" + board.getBoardNo(); + + Map params = new HashMap<>(); + params.put("postsTitle", INSERT_POSTS_TITLE); + params.put("postsContent", INSERT_POSTS_CONTENT); + params.put("postsAnswerContent", INSERT_POSTS_ANSWER_CONTENT); + params.put("attachmentCode", INSERT_ATTACHMENT_CODE); + params.put("noticeAt", INSERT_NOTICE_AT); + HttpEntity> httpEntity = new HttpEntity<>(params); + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + PostsResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + final Integer boardNo = dto.getBoardNo(); + final Integer postsNo = dto.getPostsNo(); + + Optional posts = selectData(boardNo, postsNo); + assertThat(posts).isPresent(); + + Posts entity = posts.get(); + assertThat(entity.getPostsId().getBoardNo()).isEqualTo(boardNo); + assertThat(entity.getPostsId().getPostsNo()).isEqualTo(postsNo); + assertThat(entity.getPostsTitle()).isEqualTo(INSERT_POSTS_TITLE); + assertThat(entity.getPostsContent()).isEqualTo(INSERT_POSTS_CONTENT); + assertThat(entity.getPostsAnswerContent()).isEqualTo(INSERT_POSTS_ANSWER_CONTENT); + assertThat(entity.getAttachmentCode()).isEqualTo(INSERT_ATTACHMENT_CODE); + assertThat(entity.getReadCount()).isZero(); + assertThat(entity.getNoticeAt()).isEqualTo(INSERT_NOTICE_AT); + assertThat(entity.getDeleteAt()).isZero(); + } + + /** + * 게시물 수정 + */ + @Test + void 게시물_수정() { + log.info("###게시물_수정"); + + // given + Posts entity = insertPost(0); + + final Integer boardNo = entity.getPostsId().getBoardNo(); + final Integer postsNo = entity.getPostsId().getPostsNo(); + + Map params = new HashMap<>(); + params.put("postsTitle", UPDATE_POSTS_TITLE); + params.put("postsContent", UPDATE_POSTS_CONTENT); + params.put("postsAnswerContent", UPDATE_POSTS_ANSWER_CONTENT); + params.put("attachmentCode", UPDATE_ATTACHMENT_CODE); + params.put("noticeAt", UPDATE_NOTICE_AT); + HttpEntity> httpEntity = new HttpEntity<>(params); + + String url = URL + "/" + boardNo + "/" + postsNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + PostsResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + Optional posts = selectData(boardNo, postsNo); + assertThat(posts).isPresent(); + + Posts updatedPosts = posts.get(); + assertThat(updatedPosts.getPostsId().getBoardNo()).isEqualTo(boardNo); + assertThat(updatedPosts.getPostsId().getPostsNo()).isEqualTo(postsNo); + assertThat(updatedPosts.getPostsTitle()).isEqualTo(UPDATE_POSTS_TITLE); + assertThat(updatedPosts.getPostsContent()).isEqualTo(UPDATE_POSTS_CONTENT); + assertThat(updatedPosts.getPostsAnswerContent()).isEqualTo(UPDATE_POSTS_ANSWER_CONTENT); + assertThat(updatedPosts.getAttachmentCode()).isEqualTo(UPDATE_ATTACHMENT_CODE); + assertThat(updatedPosts.getNoticeAt()).isEqualTo(UPDATE_NOTICE_AT); + } + + /** + * 게시물 다건 삭제 + */ + @Test + void 게시물_다건_삭제() { + log.info("###게시물_다건_삭제"); + + // given + insertPosts(false); + + List> params = new ArrayList<>(); + for (Posts post : posts) { + Map param = new HashMap<>(); + param.put("boardNo", post.getPostsId().getBoardNo()); + param.put("postsNo", post.getPostsId().getPostsNo()); + + params.add(param); + } + + HttpEntity>> httpEntity = new HttpEntity<>(params); + + String url = URL + "/remove"; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + Long.class + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).isEqualTo(posts.size()); + + List list = postsRepository.findAll(); + + assertThat(list).isNotNull(); + assertThat(list.size()).isEqualTo(posts.size()); + + for (Posts post : list) { + assertThat(post).isNotNull(); + assertThat(post.getDeleteAt()).isNotZero(); + } + } + + /** + * 게시물 다건 복원 + */ + @Test + void 게시물_다건_복원() { + log.info("###게시물_다건_복원"); + + // given + insertPosts(true); + + List> params = new ArrayList<>(); + for (Posts post : posts) { + Map param = new HashMap<>(); + param.put("boardNo", post.getPostsId().getBoardNo()); + param.put("postsNo", post.getPostsId().getPostsNo()); + + params.add(param); + } + + HttpEntity>> httpEntity = new HttpEntity<>(params); + + String url = URL + "/restore"; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + Long.class + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).isEqualTo(posts.size()); + + List list = postsRepository.findAll(); + + assertThat(list).isNotNull(); + assertThat(list.size()).isEqualTo(posts.size()); + + for (Posts post : list) { + assertThat(post).isNotNull(); + assertThat(post.getDeleteAt()).isZero(); + } + } + + /** + * 게시물 다건 완전 삭제 + */ + @Test + void 게시물_다건_완전_삭제() { + log.info("###게시물_다건_완전_삭제"); + + // given + insertPosts(null); + + List> params = new ArrayList<>(); + for (Posts post : posts) { + Map param = new HashMap<>(); + param.put("boardNo", post.getPostsId().getBoardNo()); + param.put("postsNo", post.getPostsId().getPostsNo()); + + params.add(param); + } + + HttpEntity>> httpEntity = new HttpEntity<>(params); + + String url = URL + "/delete"; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + Long.class + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + List list = postsRepository.findAll(); + + assertThat(list).isNotNull(); + assertThat(list.size()).isZero(); + } + + /** + * 테스트 데이터 등록 + */ + private void insertPosts(Boolean deleteAt) { + log.info("###테스트 데이터 등록"); + + // 게시물 등록 + List list = new ArrayList<>(); + for (int i = 1; i <= GIVEN_DATA_COUNT; i++) { + list.add(Posts.builder() + .board(board) + .postsId(PostsId.builder() + .boardNo(board.getBoardNo()) + .postsNo(i) + .build()) + .postsTitle(INSERT_POSTS_TITLE + i) + .postsContent(INSERT_POSTS_CONTENT + i) + .postsAnswerContent(INSERT_POSTS_ANSWER_CONTENT + i) + .attachmentCode(StringUtils.leftPad(String.valueOf(i), 10, '0')) + .readCount(0) + .noticeAt(i % 3 == 0) + .deleteAt(i % 2) + .build()); + } + + posts = postsRepository.saveAll(list); + } + + /** + * 테스트 데이터 단건 등록 + * + * @return Posts 게시물 엔티티 + */ + private Posts insertPost(Integer deleteAt) { + log.info("###테스트 데이터 단건 등록"); + + return postsRepository.save(Posts.builder() + .board(board) + .postsId(PostsId.builder() + .boardNo(board.getBoardNo()) + .postsNo(INSERT_POSTS_NO) + .build()) + .postsTitle(INSERT_POSTS_TITLE + 1) + .postsContent(INSERT_POSTS_CONTENT + 1) + .postsAnswerContent(INSERT_POSTS_ANSWER_CONTENT + 1) + .attachmentCode(INSERT_ATTACHMENT_CODE) + .readCount(INSERT_READ_COUNT) + .noticeAt(INSERT_NOTICE_AT) + .deleteAt(deleteAt) + .build()); + } + + /** + * 테스트 데이터 단건 삭제 + */ + /*private void deletePost(Posts post) { + postsRepository.deleteById(post.getPostsId()); + }*/ + + /** + * 테스트 데이터 삭제 + */ + /*private void deletePosts() { + postsRepository.deleteAll(posts); + + posts.clear(); + }*/ + + /** + * 테스트 데이터 단건 조회 + * + * @param boardNo 게시판 번호 + * @param postsNo 게시물 번호 + * @return Optional 게시물 엔티티 + */ + private Optional selectData(Integer boardNo, Integer postsNo) { + return postsRepository.findById(PostsId.builder().boardNo(boardNo).postsNo(postsNo).build()); + } + +} \ No newline at end of file diff --git a/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/util/RestResponsePage.java b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/util/RestResponsePage.java new file mode 100644 index 0000000..bd7a793 --- /dev/null +++ b/backend/board-service/src/test/java/org/egovframe/cloud/boardservice/util/RestResponsePage.java @@ -0,0 +1,93 @@ +package org.egovframe.cloud.boardservice.util; + +import java.util.Collections; +import java.util.List; + +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * org.egovframe.cloud.boardservice.util.RestResponsePage + *

+ * 페이지 API 조회 시 JSON 형식의 응답 데이터를 페이지 객체를 구현하여 마이그레이션 해주는 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +public class RestResponsePage extends PageImpl { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -4377617807230211193L; + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + * @param number 페이지 번호 + * @param size 조회할 데이터 수 + * @param totalElements 총 데이터 수 + * @param pageable 페이지 정보 + * @param last 마지막 + * @param totalPages 총 페이지 + * @param sort 정렬 + * @param first 처음 + * @param numberOfElements 조회된 데이터 수 + */ + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public RestResponsePage(@JsonProperty("content") List content, + @JsonProperty("number") int number, + @JsonProperty("size") int size, + @JsonProperty("totalElements") Long totalElements, + @JsonProperty("pageable") JsonNode pageable, + @JsonProperty("last") boolean last, + @JsonProperty("totalPages") int totalPages, + @JsonProperty("sort") JsonNode sort, + @JsonProperty("first") boolean first, + @JsonProperty("numberOfElements") int numberOfElements) { + super(content, PageRequest.of(number, size), totalElements); + } + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + * @param pageable 페이지 정보 + * @param total 총 데이터 수 + */ + public RestResponsePage(List content, Pageable pageable, long total) { + super(content, pageable, total); + } + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + */ + public RestResponsePage(List content) { + super(content); + } + + /** + * Rest 응답 페이지 생성자 + */ + public RestResponsePage() { + super(Collections.emptyList()); + } + +} diff --git a/backend/board-service/src/test/resources/application-test.yml b/backend/board-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..d94e2b5 --- /dev/null +++ b/backend/board-service/src/test/resources/application-test.yml @@ -0,0 +1,46 @@ +spring: + application: + name: board-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: +# database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + hibernate: + generate-ddl: true + ddl-auto: create-drop +# dialect: org.hibernate.dialect.MySQL5InnoDBDialect + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/backend/config/Dockerfile b/backend/config/Dockerfile new file mode 100644 index 0000000..4a98744 --- /dev/null +++ b/backend/config/Dockerfile @@ -0,0 +1,13 @@ +# docker run --name egov-config -d -p 8888:8888 -e profile=prod -e ENCRYPT_KEY=??? jaeyeolkim/egov-config +# base image - openjdk8 +FROM openjdk:8-jre-alpine +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar config.jar +# application port +EXPOSE 8888 +# 실행 (ARG 아니고 ENV 값이다) +CMD ["java", "-Dspring.profiles.active=${profile:prod}", "-jar", "config.jar"] diff --git a/backend/config/build.gradle b/backend/config/build.gradle new file mode 100644 index 0000000..c4a2df1 --- /dev/null +++ b/backend/config/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'org.springframework.boot' version '2.4.5' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'org.egovframe.cloud' +version = '0.1' +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { + implementation 'org.springframework.cloud:spring-cloud-config-server' + implementation 'org.springframework.cloud:spring-cloud-config-monitor' // webhook + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'org.springframework.boot:spring-boot-starter-actuator' // bus + implementation 'net.logstash.logback:logstash-logback-encoder:6.6' // logstash logback + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +test { + useJUnitPlatform() +} diff --git a/backend/config/gradlew b/backend/config/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/config/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/config/gradlew.bat b/backend/config/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/backend/config/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/config/manifest.yml b/backend/config/manifest.yml new file mode 100644 index 0000000..2658a55 --- /dev/null +++ b/backend/config/manifest.yml @@ -0,0 +1,12 @@ +--- +applications: + - name: egov-config # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-config # host 명으로 유일해야 함 + path: build/libs/config-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + env: + spring_profiles_active: prod + app_name: egov-config # logstash custom app name + TZ: Asia/Seoul diff --git a/backend/config/settings.gradle b/backend/config/settings.gradle new file mode 100644 index 0000000..6bbfae7 --- /dev/null +++ b/backend/config/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'config' diff --git a/backend/config/src/main/java/org/egovframe/cloud/config/ConfigApplication.java b/backend/config/src/main/java/org/egovframe/cloud/config/ConfigApplication.java new file mode 100644 index 0000000..1145c9a --- /dev/null +++ b/backend/config/src/main/java/org/egovframe/cloud/config/ConfigApplication.java @@ -0,0 +1,15 @@ +package org.egovframe.cloud.config; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.config.server.EnableConfigServer; + +@EnableConfigServer +@SpringBootApplication +public class ConfigApplication { + + public static void main(String[] args) { + SpringApplication.run(ConfigApplication.class, args); + } + +} diff --git a/backend/config/src/main/resources/application.yml b/backend/config/src/main/resources/application.yml new file mode 100644 index 0000000..d493d6b --- /dev/null +++ b/backend/config/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + port: 8888 + +spring: + application: + name: config-service + profiles: + active: native # native file repository + cloud: + config: + server: + native: + search-locations: file://${user.home}/workspace/egovframe-msa-edu/config + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + +# config server actuator +management: + endpoints: + web: + exposure: + include: busrefresh diff --git a/backend/config/src/main/resources/logback-spring.xml b/backend/config/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..3e0847e --- /dev/null +++ b/backend/config/src/main/resources/logback-spring.xml @@ -0,0 +1,35 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n + + + + + + + + + + + + + + + + + ${destination} + + {"app.name":"${app_name}"} + + + + + + + + + + \ No newline at end of file diff --git a/backend/config/src/test/java/org/egovframe/cloud/config/ConfigApplicationTests.java b/backend/config/src/test/java/org/egovframe/cloud/config/ConfigApplicationTests.java new file mode 100644 index 0000000..d1db984 --- /dev/null +++ b/backend/config/src/test/java/org/egovframe/cloud/config/ConfigApplicationTests.java @@ -0,0 +1,13 @@ +package org.egovframe.cloud.config; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ConfigApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/discovery/Dockerfile b/backend/discovery/Dockerfile new file mode 100644 index 0000000..15053df --- /dev/null +++ b/backend/discovery/Dockerfile @@ -0,0 +1,12 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar discovery.jar +# application port +EXPOSE 8761 +# 실행 +CMD ["java", "-jar", "discovery.jar"] diff --git a/backend/discovery/build.gradle b/backend/discovery/build.gradle new file mode 100644 index 0000000..a547611 --- /dev/null +++ b/backend/discovery/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.springframework.boot' version '2.4.5' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'org.egovframe.cloud' +version = '0.1' +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server' + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +test { + useJUnitPlatform() +} diff --git a/backend/discovery/gradlew b/backend/discovery/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/discovery/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/discovery/gradlew.bat b/backend/discovery/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/backend/discovery/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/discovery/manifest.yml b/backend/discovery/manifest.yml new file mode 100644 index 0000000..923636a --- /dev/null +++ b/backend/discovery/manifest.yml @@ -0,0 +1,15 @@ +--- +applications: + - name: egov-discovery # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-discovery # host 명으로 유일해야 함 + path: build/libs/discovery-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + env: + spring_profiles_active: cf + spring_cloud_config_uri: https://egov-config.paas-ta.org + eureka_server_enable_self_preservation: true + server_port: 80 + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/backend/discovery/settings.gradle b/backend/discovery/settings.gradle new file mode 100644 index 0000000..ca40650 --- /dev/null +++ b/backend/discovery/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'discovery' diff --git a/backend/discovery/src/main/java/org/egovframe/cloud/discovery/DiscoveryApplication.java b/backend/discovery/src/main/java/org/egovframe/cloud/discovery/DiscoveryApplication.java new file mode 100644 index 0000000..0b8c2b0 --- /dev/null +++ b/backend/discovery/src/main/java/org/egovframe/cloud/discovery/DiscoveryApplication.java @@ -0,0 +1,15 @@ +package org.egovframe.cloud.discovery; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@EnableEurekaServer +@SpringBootApplication +public class DiscoveryApplication { + + public static void main(String[] args) { + SpringApplication.run(DiscoveryApplication.class, args); + } + +} diff --git a/backend/discovery/src/main/java/org/egovframe/cloud/discovery/SecurityConfig.java b/backend/discovery/src/main/java/org/egovframe/cloud/discovery/SecurityConfig.java new file mode 100644 index 0000000..b632d87 --- /dev/null +++ b/backend/discovery/src/main/java/org/egovframe/cloud/discovery/SecurityConfig.java @@ -0,0 +1,46 @@ +package org.egovframe.cloud.discovery; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * org.egovframe.cloud.discovery.SecurityConfig + *

+ * Spring Security Config 클래스 + * Eureka Server 접속 보안 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@EnableWebSecurity // Spring Security 설정들을 활성화시켜 준다 +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + /** + * 스프링 시큐리티 설정 + * + * @param http + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .headers().frameOptions().disable() + .and() + .authorizeRequests() + .anyRequest().authenticated() + .and() + .httpBasic(); + } + +} diff --git a/backend/discovery/src/main/resources/application.yml b/backend/discovery/src/main/resources/application.yml new file mode 100644 index 0000000..180b3ea --- /dev/null +++ b/backend/discovery/src/main/resources/application.yml @@ -0,0 +1,18 @@ +server: + port: 8761 + +spring: + application: + name: discovery + security: + user: + name: admin + password: admin + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false + server: + peer-node-read-timeout-ms: 10000 \ No newline at end of file diff --git a/backend/discovery/src/test/java/org/egovframe/cloud/discovery/DiscoveryApplicationTests.java b/backend/discovery/src/test/java/org/egovframe/cloud/discovery/DiscoveryApplicationTests.java new file mode 100644 index 0000000..6e7514b --- /dev/null +++ b/backend/discovery/src/test/java/org/egovframe/cloud/discovery/DiscoveryApplicationTests.java @@ -0,0 +1,13 @@ +package org.egovframe.cloud.discovery; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DiscoveryApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/portal-service/Dockerfile b/backend/portal-service/Dockerfile new file mode 100644 index 0000000..bd86ccf --- /dev/null +++ b/backend/portal-service/Dockerfile @@ -0,0 +1,15 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine + +# config server uri: dockder run --e 로 변경 가능 +ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar app.jar +# application port +#EXPOSE 8000 +# 실행 (application-cf.yml 프로필이 기본값) +CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "app.jar"] diff --git a/backend/portal-service/build.gradle b/backend/portal-service/build.gradle new file mode 100644 index 0000000..ef9959f --- /dev/null +++ b/backend/portal-service/build.gradle @@ -0,0 +1,100 @@ +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' +sourceCompatibility = '1.8' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() + maven { url "https://maven.egovframe.go.kr/maven/" } // egovframe maven 원격 저장소 +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { +// implementation files('../../module-common/build/libs/module-common-0.1.jar') // 공통 모듈, @ComponentScan(basePackages={"org.egovframe.cloud"}) 추가해야 적용된다 + implementation 'org.egovframe.cloud:module-common:0.1' + 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-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-config' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'org.springframework.cloud:spring-cloud-starter-sleuth' + implementation 'org.springframework.cloud:spring-cloud-sleuth-zipkin' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' + implementation 'net.logstash.logback:logstash-logback-encoder:6.6' // logstash logback + implementation 'commons-net:commons-net:3.8.0' // FTPClient + implementation 'mysql:mysql-connector-java' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + + //messaging + implementation 'org.springframework.cloud:spring-cloud-stream' + implementation 'org.springframework.cloud:spring-cloud-stream-binder-rabbit' + + // querydsl + implementation 'com.querydsl:querydsl-jpa' + + // swagger + implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' + implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + // lombok + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + testImplementation 'com.h2database:h2' + testImplementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7' // 테스트시에만 출력 + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +test { + useJUnitPlatform() +} + +// 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 추가 끝 \ No newline at end of file diff --git a/backend/portal-service/gradlew b/backend/portal-service/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/backend/portal-service/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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 "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/portal-service/gradlew.bat b/backend/portal-service/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/backend/portal-service/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/portal-service/manifest.yml b/backend/portal-service/manifest.yml new file mode 100644 index 0000000..efd2c12 --- /dev/null +++ b/backend/portal-service/manifest.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: egov-portal-service # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-portal-service # host 명으로 유일해야 함 + path: build/libs/portal-service-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + services: + - egov-discovery-provided-service # discovery service binding + env: + spring_profiles_active: cf + spring_cloud_config_uri: https://egov-config.paas-ta.org + app_name: egov-portal-service # logstash custom app name + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/backend/portal-service/settings.gradle b/backend/portal-service/settings.gradle new file mode 100644 index 0000000..47ed47a --- /dev/null +++ b/backend/portal-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'portal-service' diff --git a/backend/portal-service/src/main/java/META-INF/persistence.xml b/backend/portal-service/src/main/java/META-INF/persistence.xml new file mode 100644 index 0000000..957d4ce --- /dev/null +++ b/backend/portal-service/src/main/java/META-INF/persistence.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/PortalServiceApplication.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/PortalServiceApplication.java new file mode 100644 index 0000000..61162a5 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/PortalServiceApplication.java @@ -0,0 +1,21 @@ +package org.egovframe.cloud.portalservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan(basePackages={"org.egovframe.cloud.common", "org.egovframe.cloud.servlet", "org.egovframe.cloud.portalservice"}) // org.egovframe.cloud.common package 포함하기 위해 +@EntityScan({"org.egovframe.cloud.servlet.domain", "org.egovframe.cloud.portalservice.domain"}) +@EnableFeignClients +@EnableDiscoveryClient +@SpringBootApplication +public class PortalServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(PortalServiceApplication.class, args); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/PortalApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/PortalApiController.java new file mode 100644 index 0000000..40437ce --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/PortalApiController.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.portalservice.api; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * org.egovframe.cloud.portalservice.api.PortalApiController + *

+ * 상태 확인 요청을 처리하는 REST API Controller + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class PortalApiController { + private final Environment env; + + /** + * 포털 서비스 상태 확인 + * + * @return + */ + @GetMapping("/actuator/health-portal") + public String status() { + return String.format("GET Portal Service on" + + "\n local.server.port :" + env.getProperty("local.server.port") + + "\n token expiration time :" + env.getProperty("token.expiration_time") + + "\n egov.server.ip :" + env.getProperty("egov.server.ip") + + "\n spring.datasource.username :" + env.getProperty("spring.datasource.username") + + "\n spring.profiles.active :" + env.getProperty("spring.profiles.active") + + "\n spring.cloud.config.label :" + env.getProperty("spring.cloud.config.label") + + "\n spring.cloud.config.uri :" + env.getProperty("spring.cloud.config.uri") + + "\n egov.message :" + env.getProperty("egov.message") + ); + } + + /** + * 포털 서비스 상태 확인 + * + * @return + */ + @PostMapping("/actuator/health-portal") + public String poststatus() { + return String.format("POST Portal Service on" + + "\n local.server.port :" + env.getProperty("local.server.port") + + "\n token expiration time :" + env.getProperty("token.expiration_time") + + "\n egov.server.ip :" + env.getProperty("egov.server.ip") + + "\n spring.datasource.username :" + env.getProperty("spring.datasource.username") + + "\n egov.message :" + env.getProperty("egov.message") + ); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/AttachmentApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/AttachmentApiController.java new file mode 100644 index 0000000..bd3267a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/AttachmentApiController.java @@ -0,0 +1,309 @@ +package org.egovframe.cloud.portalservice.api.attachment; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.portalservice.api.attachment.dto.*; +import org.egovframe.cloud.portalservice.service.attachment.AttachmentService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.api.attachment.AttachmentApiController + *

+ * 첨부파일 API controller class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class AttachmentApiController { + + private final AttachmentService attachmentService; + + + /** + * 첨부파일 업로드 - 단건 + * 물리적 파일에 대해 업로드만 진행 (.temp) + * 추후 저장 필요 + * + * @param file + * @return + * @throws Exception + */ + @PostMapping(value = "/api/v1/upload") + public AttachmentFileResponseDto upload(@RequestParam("file") MultipartFile file) { + return attachmentService.uploadFile(file); + } + + /** + * 첨부파일 업로드 - 여러 건 + * 물리적 파일에 대해 업로드만 진행 (.temp) + * 추후 저장 필요 + * + * @param files + * @return + */ + @PostMapping(value = "/api/v1/upload/multi") + public List uploadMulti(@RequestParam("files") List files) { + return attachmentService.uploadFiles(files); + } + + /** + * 에디터에서 파일 업로드 + * 현재 이미지만 적용 + * + * @param editorRequestDto + * @return + */ + @PostMapping(value = "/api/v1/upload/editor") + public AttachmentEditorResponseDto uploadEditor(@RequestBody AttachmentBase64RequestDto editorRequestDto) { + return attachmentService.uploadEditor(editorRequestDto); + } + + /** + * 에디터에서 파일 경로(명) 이미지 load + * + * @param imagename + * @return + * @throws IOException + */ + @GetMapping(value = "/api/v1/images/editor/{imagename}") + public ResponseEntity loadImages(@PathVariable("imagename") String imagename) { + AttachmentImageResponseDto image = attachmentService.loadImage(imagename); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(image.getMimeType())) + .body(image.getData()); + } + + /** + * unique id로 이미지 태그에서 이미지 load + * + * @param uniqueId + * @return + */ + @GetMapping(value = "/api/v1/images/{uniqueId}") + public ResponseEntity loadImagesByUniqueId(@PathVariable String uniqueId) { + AttachmentImageResponseDto image = attachmentService.loadImageByUniqueId(uniqueId); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(image.getMimeType())) + .body(image.getData()); + + } + + /** + * 첨부파일 다운로드 + * + * @param uniqueId + * @return + * @throws IOException + */ + @GetMapping(value = "/api/v1/download/{uniqueId}") + public ResponseEntity downloadFile(@PathVariable String uniqueId) { + + AttachmentDownloadResponseDto downloadFile = attachmentService.downloadFile(uniqueId); + + String mimeType = null; + try { + // get mime type + URLConnection connection = new URL(downloadFile.getFile().getURL().toString()).openConnection(); + mimeType = connection.getContentType(); + } catch (IOException ex) { + log.error("download fail", ex); + throw new BusinessMessageException("Sorry. download fail... \uD83D\uDE3F"); + } + + if (mimeType == null) { + mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE; + } + + ContentDisposition contentDisposition = ContentDisposition.builder("attachment") + .filename(downloadFile.getOriginalFileName(), StandardCharsets.UTF_8) + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, mimeType); + headers.setContentDisposition(contentDisposition); + + return ResponseEntity.ok() + .headers(headers) + .body(downloadFile.getFile()); + } + + /** + * 첨부파일 코드로 첨부파일 목록 조회 + * + * @param attachmentCode + * @return + */ + @GetMapping(value = "/api/v1/attachments/{attachmentCode}") + public List findByCode(@PathVariable String attachmentCode) { + return attachmentService.findByCode(attachmentCode); + } + + /** + * 첨부파일 저장 - 물리적 파일은 .temp로 저장 된 후 호출되어야 함 + * 새롭게 attachment code를 생성해야 하는 경우 + * + * @param saveRequestDtoList + * @return 새로 생성한 첨부파일 code + */ + @PostMapping(value = "/api/v1/attachments/file") + public String save(@RequestBody List saveRequestDtoList) { + return attachmentService.save(saveRequestDtoList); + } + + /** + * 첨부파일 저장 - 물리적 파일은 .temp로 저장 된 후 호출되어야 함 + * 이미 attachment code 가 있는 경우 seq만 새로 생성해서 저장 + * or + * isDelete = true 인 경우 삭제 여부 Y + * + * @param saveRequestDtoList + * @return + */ + @PutMapping(value = "/api/v1/attachments/file/{attachmentCode}") + public String saveByCode(@PathVariable String attachmentCode, @RequestBody List saveRequestDtoList) { + return attachmentService.saveByCode(attachmentCode, saveRequestDtoList); + } + + /** + * 관리자 - 전체 첨부파일 목록 조회 + * + * @param searchRequestDto + * @param pageable + * @return + */ + @GetMapping(value = "/api/v1/attachments") + public Page search(RequestDto searchRequestDto, Pageable pageable) { + return attachmentService.search(searchRequestDto, pageable); + } + + /** + * 관리자 - 삭제여부 토글 + * + * @param uniqueId + * @param isDelete + * @return + */ + @PutMapping(value = "/api/v1/attachments/{uniqueId}/{isDelete}") + public String toggleDelete(@PathVariable String uniqueId, @PathVariable boolean isDelete) { + return attachmentService.toggleDelete(uniqueId, isDelete); + } + + + /** + * 관리자 - 하나의 파일 삭제 + * 물리적 파일 삭제 + * + * @param uniqueId + */ + @DeleteMapping(value = "/api/v1/attachments/{uniqueId}") + public void delete(@PathVariable String uniqueId) { + attachmentService.delete(uniqueId); + } + + /** + * 첨부파일 저장 + * 새롭게 attachment code를 생성해야 하는 경우 + * + * @param files + * @param uploadRequestDto + * @return + */ + @PostMapping(value = "/api/v1/attachments/upload", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public String uploadAndSave(@RequestPart(value = "files", required = true) List files, + @RequestPart(value = "info", required = false) AttachmentUploadRequestDto uploadRequestDto) { + return attachmentService.uploadAndSave(files, uploadRequestDto); + } + + /** + * 첨부파일 저장 + * 이미 attachment code 가 있는 경우 이므로 seq만 새로 생성해서 저장 + * or + * isDelete = true 인 경우 삭제 여부 Y + * + * @param files + * @param attachmentCode + * @param uploadRequestDto + * @param saveRequestDtoList + * @return + */ + @PutMapping(value = "/api/v1/attachments/upload/{attachmentCode}", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public String uploadAndUpdate(@PathVariable String attachmentCode, + @RequestPart(value = "files", required = true) List files, + @RequestPart(value = "info", required = true) AttachmentUploadRequestDto uploadRequestDto, + @RequestPart(value = "list", required = false) List saveRequestDtoList) { + return attachmentService.uploadAndUpdate(files, attachmentCode, uploadRequestDto, saveRequestDtoList); + } + + /** + * 첨부파일 저장 - 업로드 없이 기존 파일만 삭제 + * isDelete = true 인 경우 삭제 여부 Y + * + * @param attachmentCode + * @param uploadRequestDto + * @param updateRequestDtoList + * @return + */ + @PutMapping(value = "/api/v1/attachments/{attachmentCode}", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public String update(@PathVariable String attachmentCode, + @RequestPart(value = "info") AttachmentUploadRequestDto uploadRequestDto, + @RequestPart(value = "list") List updateRequestDtoList){ + + return attachmentService.uploadAndUpdate(null, attachmentCode, + uploadRequestDto, updateRequestDtoList); + } + + /** + * attachmentCode로 해당하는 모든 첨부파일의 entity 정보 업데이트 + * 신규 entity의 경우 entity가 저장 된 후 entity id가 생성되므로 + * entity 저장 후 해당 api 호출하여 entity 정보를 업데이트 해준다. + * + * @param attachmentCode + * @param uploadRequestDto + * @return + */ + @PutMapping("/api/v1/attachments/{attachmentCode}/info") + public String updateEntity(@PathVariable String attachmentCode, + @RequestBody AttachmentUploadRequestDto uploadRequestDto) { + return attachmentService.updateEntity(attachmentCode, uploadRequestDto); + } + + /** + * 첨부파일 저장 후 기능 저장 시 오류 날 경우 + * 해당 첨부파일 코드에 조회되는 첨부파일 목록 전부 삭제 + * rollback + * + * @param attachmentCode + */ + @DeleteMapping("/api/v1/attachments/{attachmentCode}/children") + public void deleteAllEmptyEntity(@PathVariable String attachmentCode) { + attachmentService.deleteAllEmptyEntity(attachmentCode); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentBase64RequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentBase64RequestDto.java new file mode 100644 index 0000000..78d5f3a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentBase64RequestDto.java @@ -0,0 +1,45 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentBase64RequestDto + *

+ * 첨부파일 Base64 업로드 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class AttachmentBase64RequestDto { + + private String fieldName; + private String fileType; + private String fileBase64; + private String originalName; + private Long size; + + @Builder + public AttachmentBase64RequestDto(String fieldName, String fileType, String fileBase64, String originalName, Long size) { + this.fieldName = fieldName; + this.fileType = fileType; + this.fileBase64 = fileBase64; + this.originalName = originalName; + this.size = size; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentDownloadResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentDownloadResponseDto.java new file mode 100644 index 0000000..9eb9004 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentDownloadResponseDto.java @@ -0,0 +1,35 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.core.io.Resource; +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentDownloadResponseDto + *

+ * 첨부파일 다운로드 시 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class AttachmentDownloadResponseDto { + private String originalFileName; + private Resource file; + + @Builder + public AttachmentDownloadResponseDto(String originalFileName, Resource file) { + this.originalFileName = originalFileName; + this.file = file; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentEditorResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentEditorResponseDto.java new file mode 100644 index 0000000..8375fde --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentEditorResponseDto.java @@ -0,0 +1,42 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentEditorResponseDto + *

+ * 첨부파일 에디터 업로드 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class AttachmentEditorResponseDto extends AttachmentUploadResponseDto { + private int uploaded; + private String url; + + + @Builder + public AttachmentEditorResponseDto(String originalFileName, String message, String fileType, long size, int uploaded, String url) { + this.originalFileName = originalFileName; + this.message = message; + this.fileType = fileType; + this.size = size; + this.uploaded = uploaded; + this.url = url; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentFileResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentFileResponseDto.java new file mode 100644 index 0000000..29fe854 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentFileResponseDto.java @@ -0,0 +1,39 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentFileResponseDto + *

+ * 첨부파일 업로드 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class AttachmentFileResponseDto extends AttachmentUploadResponseDto { + private String physicalFileName; + + @Builder + public AttachmentFileResponseDto(String originalFileName, String message, String fileType, long size, String physicalFileName) { + this.originalFileName = originalFileName; + this.message = message; + this.fileType = fileType; + this.size = size; + this.physicalFileName = physicalFileName; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentImageResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentImageResponseDto.java new file mode 100644 index 0000000..20666e1 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentImageResponseDto.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentImageResponseDto + *

+ * 이미지태그에 대한 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class AttachmentImageResponseDto { + private String mimeType; + private byte[] data; + + @Builder + public AttachmentImageResponseDto(String mimeType, byte[] data) { + this.mimeType = mimeType; + this.data = data; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentResponseDto.java new file mode 100644 index 0000000..08676d7 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentResponseDto.java @@ -0,0 +1,61 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.domain.attachment.Attachment; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentResponseDto + *

+ * 첨부파일 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class AttachmentResponseDto { + private String code; + private Long seq; + private String id; + private String physicalFileName; + private String originalFileName; + private Long size; + private String fileType; + private Long downloadCnt; + private Boolean isDelete; + private String entityName; + private String entityId; + private LocalDateTime createDate; + + @Builder + public AttachmentResponseDto(Attachment attachment) { + this.code = attachment.getAttachmentId().getCode(); + this.seq = attachment.getAttachmentId().getSeq(); + this.id = attachment.getUniqueId(); + this.physicalFileName = attachment.getPhysicalFileName(); + this.originalFileName = attachment.getOriginalFileName(); + this.size = attachment.getSize(); + this.fileType = attachment.getFileType(); + this.downloadCnt = attachment.getDownloadCnt(); + this.isDelete = attachment.getIsDelete(); + this.entityName = attachment.getEntityName(); + this.entityId = attachment.getEntityId(); + this.createDate = attachment.getCreatedDate(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentTempSaveRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentTempSaveRequestDto.java new file mode 100644 index 0000000..fb4cea0 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentTempSaveRequestDto.java @@ -0,0 +1,52 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentSaveRequestDto + *

+ * 첨부파일 도메인 저장에대한 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class AttachmentTempSaveRequestDto { + private String uniqueId; + private String physicalFileName; + private String originalName; + private Long size; + private String fileType; + private String entityName; + private String entityId; + private boolean isDelete; + + @Builder + public AttachmentTempSaveRequestDto(String uniqueId, String physicalFileName, + String originalName, Long size, + String fileType, String entityName, + String entityId, boolean isDelete) { + this.uniqueId = uniqueId; + this.physicalFileName = physicalFileName; + this.originalName = originalName; + this.size = size; + this.fileType = fileType; + this.entityName = entityName; + this.entityId = entityId; + this.isDelete = isDelete; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUpdateRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUpdateRequestDto.java new file mode 100644 index 0000000..9a07a4c --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUpdateRequestDto.java @@ -0,0 +1,35 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + +import lombok.*; + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentUpdateRequestDto + *

+ * 첨부파일 수정 저장 시 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class AttachmentUpdateRequestDto { + + private String uniqueId; + private Boolean isDelete; + + @Builder + public AttachmentUpdateRequestDto(String uniqueId, Boolean isDelete) { + this.uniqueId = uniqueId; + this.isDelete = isDelete; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUploadRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUploadRequestDto.java new file mode 100644 index 0000000..81c4f68 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUploadRequestDto.java @@ -0,0 +1,38 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentUploadRequestDto + *

+ * 첨부파일 업로드 저장 시 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class AttachmentUploadRequestDto { + + private String entityName; + private String entityId; + + @Builder + public AttachmentUploadRequestDto(String entityName, String entityId) { + this.entityName = entityName; + this.entityId = entityId; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUploadResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUploadResponseDto.java new file mode 100644 index 0000000..a7e8c49 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/attachment/dto/AttachmentUploadResponseDto.java @@ -0,0 +1,32 @@ +package org.egovframe.cloud.portalservice.api.attachment.dto; + + +import lombok.*; + +/** + * org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentUploadResponseDto + *

+ * 첨부파일 업로드에 대한 응답 dto 추상 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class AttachmentUploadResponseDto { + protected String originalFileName; + protected String message; + protected String fileType; + protected long size; + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/BannerApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/BannerApiController.java new file mode 100644 index 0000000..e824e0a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/BannerApiController.java @@ -0,0 +1,148 @@ +package org.egovframe.cloud.portalservice.api.banner; + +import java.util.List; +import java.util.Map; + +import javax.validation.Valid; + +import org.egovframe.cloud.portalservice.api.banner.dto.BannerImageResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerListResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerRequestDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerSaveRequestDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerUpdateRequestDto; +import org.egovframe.cloud.portalservice.service.banner.BannerService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.api.banner.BannerApiController + *

+ * 배너 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class BannerApiController { + + /** + * 배너 서비스 + */ + private final BannerService bannerService; + + /** + * 배너 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 배너 목록 응답 DTO + */ + @GetMapping("/api/v1/banners") + public Page findPage(BannerRequestDto requestDto, + @PageableDefault(sort = "banner_no", direction = Sort.Direction.DESC) Pageable pageable) { + return bannerService.findPage(requestDto, pageable); + } + + /** + * 유형별 배너 목록 조회 + * + * @param siteId 사이트 ID + * @param bannerTypeCodes 배너 유형 코드 목록 + * @param bannerCount 배너 수 + * @return Map> 배너 유형 코드별 배너 이미지 응답 DTO Map + */ + @GetMapping("/api/v1/{siteId}/banners/{bannerTypeCodes}/{bannerCount}") + public Map> findUseList(@PathVariable Long siteId, @PathVariable List bannerTypeCodes, @PathVariable Integer bannerCount) { + return bannerService.findList(bannerTypeCodes, bannerCount, true, siteId); + } + + /** + * 배너 단건 조회 + * + * @param bannerNo 배너 번호 + * @return BannerResponseDto 배너 상세 응답 DTO + */ + @GetMapping("/api/v1/banners/{bannerNo}") + public BannerResponseDto findById(@PathVariable Integer bannerNo) { + return bannerService.findById(bannerNo); + } + + /** + * 배너 다음 정렬 순서 조회 + * + * @param siteId siteId + * @return Integer 다음 정렬 순서 + */ + @GetMapping("/api/v1/banners/{siteId}/sort-seq/next") + public Integer findNextSortSeq(@PathVariable Long siteId) { + return bannerService.findNextSortSeq(siteId); + } + + /** + * 배너 등록 + * + * @param requestDto 배너 등록 요청 DTO + * @return BannerResponseDto 배너 상세 응답 DTO + */ + @PostMapping("/api/v1/banners") + public BannerResponseDto save(@RequestBody @Valid BannerSaveRequestDto requestDto) { + return bannerService.save(requestDto); + } + + /** + * 배너 수정 + * + * @param bannerNo 배너 번호 + * @param requestDto 배너 수정 요청 DTO + * @return BannerResponseDto 배너 상세 응답 DTO + */ + @PutMapping("/api/v1/banners/{bannerNo}") + public BannerResponseDto update(@PathVariable Integer bannerNo, @RequestBody @Valid BannerUpdateRequestDto requestDto) { + return bannerService.update(bannerNo, requestDto); + } + + /** + * 배너 사용 여부 수정 + * + * @param bannerNo 배너 번호 + * @param useAt 사용 여부 + * @return BannerResponseDto 배너 상세 응답 DTO + */ + @PutMapping("/api/v1/banners/{bannerNo}/{useAt}") + public BannerResponseDto updateUseAt(@PathVariable Integer bannerNo, @PathVariable Boolean useAt) { + return bannerService.updateUseAt(bannerNo, useAt); + } + + /** + * 배너 삭제 + * + * @param bannerNo 배너 번호 + */ + @DeleteMapping("/api/v1/banners/{bannerNo}") + public void delete(@PathVariable Integer bannerNo) { + bannerService.delete(bannerNo); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerImageResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerImageResponseDto.java new file mode 100644 index 0000000..0a2b9ae --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerImageResponseDto.java @@ -0,0 +1,96 @@ +package org.egovframe.cloud.portalservice.api.banner.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * org.egovframe.cloud.portalservice.api.banner.dto.BannerImageResponseDto + *

+ * 배너 이미지 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/03 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/03    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BannerImageResponseDto implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -5701020612682455280L; + + /** + * 배너 번호 + */ + private Integer bannerNo; + + /** + * 배너 구분 코드 + */ + private String bannerTypeCode; + + /** + * 배너 제목 + */ + private String bannerTitle; + + /** + * 첨부파일 코드 + */ + private String attachmentCode; + + private String uniqueId; + + /** + * url 주소 + */ + private String urlAddr; + + /** + * 새 창 여부 + */ + private Boolean newWindowAt; + + /** + * 배너 내용 + */ + private String bannerContent; + + /** + * 배너 목록 응답 DTO 생성자 + * + * @param bannerNo 배너 번호 + * @param bannerTypeCode 배너 구분 코드 + * @param bannerTitle 배너 제목 + * @param attachmentCode 첨부파일 코드 + * @param uniqueId 첨부파일 unique 코드 + * @param urlAddr url 주소 + * @param newWindowAt 새 창 여부 + * @param bannerContent 배너 내용 + */ + @QueryProjection + public BannerImageResponseDto(Integer bannerNo, String bannerTypeCode, String bannerTitle, String attachmentCode, String uniqueId, String urlAddr, Boolean newWindowAt, String bannerContent) { + this.bannerNo = bannerNo; + this.bannerTypeCode = bannerTypeCode; + this.bannerTitle = bannerTitle; + this.attachmentCode = attachmentCode; + this.uniqueId = uniqueId; + this.urlAddr = urlAddr; + this.newWindowAt = newWindowAt; + this.bannerContent = bannerContent; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerListResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerListResponseDto.java new file mode 100644 index 0000000..13a3ebf --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerListResponseDto.java @@ -0,0 +1,93 @@ +package org.egovframe.cloud.portalservice.api.banner.dto; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.api.content.dto.BannerListResponseDto + *

+ * 배너 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BannerListResponseDto implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -2944698753371861587L; + + /** + * 배너 번호 + */ + private Integer bannerNo; + + /** + * 배너 구분 코드 + */ + private String bannerTypeCode; + + /** + * 배너 구분 코드 명 + */ + private String bannerTypeCodeName; + + /** + * 배너 제목 + */ + private String bannerTitle; + + /** + * 사용 여부 + */ + private Boolean useAt; + + /** + * 수정 일시 + */ + private LocalDateTime createdDate; + + /** + * site 명 + */ + private String siteName; + + /** + * 배너 목록 응답 DTO 생성자 + * + * @param bannerNo 배너 번호 + * @param bannerTypeCode 배너 구분 코드 + * @param bannerTypeCodeName 배너 구분 코드 명 + * @param bannerTitle 배너 제목 + * @param useAt 사용 여부 + * @param createdDate 생성 일시 + */ + @QueryProjection + public BannerListResponseDto(Integer bannerNo, String bannerTypeCode, String bannerTypeCodeName, String bannerTitle, Boolean useAt, LocalDateTime createdDate, String siteName) { + this.bannerNo = bannerNo; + this.bannerTypeCode = bannerTypeCode; + this.bannerTypeCodeName = bannerTypeCodeName; + this.bannerTitle = bannerTitle; + this.useAt = useAt; + this.createdDate = createdDate; + this.siteName = siteName; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerRequestDto.java new file mode 100644 index 0000000..9de01c6 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerRequestDto.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.portalservice.api.banner.dto; + +import org.egovframe.cloud.common.dto.RequestDto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +/** + * org.egovframe.cloud.portalservice.api.content.dto.BannerRequestDto + *

+ * 배너 목록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/10/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/10/12    shinmj       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BannerRequestDto extends RequestDto { + private Long siteId; +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerResponseDto.java new file mode 100644 index 0000000..286d0ab --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerResponseDto.java @@ -0,0 +1,108 @@ +package org.egovframe.cloud.portalservice.api.banner.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.banner.Banner; + +/** + * org.egovframe.cloud.portalservice.api.content.dto.BannerResponseDto + *

+ * 배너 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BannerResponseDto { + + /** + * 배너 번호 + */ + private Integer bannerNo; + + /** + * 배너 구분 코드 + */ + private String bannerTypeCode; + + /** + * 배너 제목 + */ + private String bannerTitle; + + /** + * 첨부파일 코드 + */ + private String attachmentCode; + + /** + * url 주소 + */ + private String urlAddr; + + /** + * 새 창 여부 + */ + private Boolean newWindowAt; + + /** + * 배너 내용 + */ + private String bannerContent; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * site Id + */ + private Long siteId; + + /** + * 배너 엔티티를 생성자로 주입 받아서 배너 상세 응답 DTO 속성 값 세팅 + * + * @param entity 배너 엔티티 + */ + public BannerResponseDto(Banner entity) { + this.bannerNo = entity.getBannerNo(); + this.bannerTypeCode = entity.getBannerTypeCode(); + this.bannerTitle = entity.getBannerTitle(); + this.attachmentCode = entity.getAttachmentCode(); + this.urlAddr = entity.getUrlAddr(); + this.newWindowAt = entity.getNewWindowAt(); + this.bannerContent = entity.getBannerContent(); + this.sortSeq = entity.getSortSeq(); + this.siteId = entity.getSite().getId(); + } + + /** + * 배너 상세 응답 DTO 속성 값으로 배너 엔티티 빌더를 사용하여 객체 생성 + * + * @return Banner 배너 엔티티 + */ + public Banner toEntity() { + return Banner.builder() + .bannerNo(bannerNo) + .bannerTypeCode(bannerTypeCode) + .bannerTitle(bannerTitle) + .attachmentCode(attachmentCode) + .urlAddr(urlAddr) + .newWindowAt(newWindowAt) + .bannerContent(bannerContent) + .sortSeq(sortSeq) + .build(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerSaveRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerSaveRequestDto.java new file mode 100644 index 0000000..aa295ff --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerSaveRequestDto.java @@ -0,0 +1,91 @@ +package org.egovframe.cloud.portalservice.api.banner.dto; + +import javax.validation.constraints.NotBlank; + +import org.egovframe.cloud.portalservice.domain.banner.Banner; +import org.egovframe.cloud.portalservice.domain.menu.Site; + +import lombok.Getter; + +/** + * org.egovframe.cloud.portalservice.api.content.dto.BannerSaveRequestDto + *

+ * 배너 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +@Getter +public class BannerSaveRequestDto { + + /** + * 배너 구분 코드 + */ + @NotBlank(message = "{banner.banner_type_code} {err.required}") + private String bannerTypeCode; + + /** + * 배너 제목 + */ + @NotBlank(message = "{banner.banner_title} {err.required}") + private String bannerTitle; + + /** + * 첨부파일 코드 + */ + @NotBlank(message = "{banner.attachment_code} {err.required}") + private String attachmentCode; + + /** + * url 주소 + */ + private String urlAddr; + + /** + * 새 창 여부 + */ + private Boolean newWindowAt; + + /** + * 배너 내용 + */ + private String bannerContent; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * siteId + */ + private Long siteId; + + /** + * 배너 등록 요청 DTO 속성 값으로 배너 엔티티 빌더를 사용하여 객체 생성 + * + * @return Banner 배너 엔티티 + */ + public Banner toEntity(Site site) { + return Banner.builder() + .bannerTypeCode(bannerTypeCode) + .bannerTitle(bannerTitle) + .attachmentCode(attachmentCode) + .urlAddr(urlAddr) + .newWindowAt(newWindowAt) + .bannerContent(bannerContent) + .sortSeq(sortSeq) + .site(site) + .build(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerUpdateRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerUpdateRequestDto.java new file mode 100644 index 0000000..b8e48ba --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/banner/dto/BannerUpdateRequestDto.java @@ -0,0 +1,70 @@ +package org.egovframe.cloud.portalservice.api.banner.dto; + +import lombok.Getter; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.portalservice.api.content.dto.BannerUpdateRequestDto + *

+ * 배너 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +public class BannerUpdateRequestDto { + + /** + * 배너 구분 코드 + */ + @NotBlank(message = "{banner.banner_type_code} {err.required}") + private String bannerTypeCode; + + /** + * 배너 제목 + */ + @NotBlank(message = "{banner.banner_title} {err.required}") + private String bannerTitle; + + /** + * 첨부파일 코드 + */ + @NotBlank(message = "{banner.attachment_code} {err.required}") + private String attachmentCode; + + /** + * url 주소 + */ + private String urlAddr; + + /** + * 새 창 여부 + */ + private Boolean newWindowAt; + + /** + * 배너 내용 + */ + private String bannerContent; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * site Id + */ + private Long siteId; + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/CodeApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/CodeApiController.java new file mode 100644 index 0000000..e297ef3 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/CodeApiController.java @@ -0,0 +1,120 @@ +package org.egovframe.cloud.portalservice.api.code; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeListResponseDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeResponseDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeSaveRequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeUpdateRequestDto; +import org.egovframe.cloud.portalservice.domain.code.CodeRepository; +import org.egovframe.cloud.portalservice.service.code.CodeService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.api.code.CodeApiController + *

+ * 공통코드 CRUD 요청을 처리하는 REST API Controller + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor // final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성하여, 빈을 생성자로 주입받게 한다. +@RestController +public class CodeApiController { + + private final CodeService codeService; + private final CodeRepository codeRepository; + + /** + * 공통코드 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @GetMapping("/api/v1/codes") + public Page findAllByKeyword(RequestDto requestDto, Pageable pageable) { + return codeRepository.findAllByKeyword(requestDto, pageable); + } + + /** + * 공통코드 목록 - parentCodeId 가 없는 상위공통코드 + * + * @return + */ + @GetMapping("/api/v1/codes-parent") + public List findAllParent() { + return codeRepository.findAllParent(); + } + + /** + * 공통코드 단건 조회 + * + * @param codeId + * @return + */ + @GetMapping("/api/v1/codes/{codeId}") + public CodeResponseDto findByCodeId(@PathVariable String codeId) { + return codeService.findByCodeId(codeId); + } + + /** + * 공통코드 등록 + * + * @param requestDto + * @return + */ + @PostMapping("/api/v1/codes") + public String save(@RequestBody @Valid CodeSaveRequestDto requestDto) { + return codeService.save(requestDto); + } + + /** + * 공통코드 수정 + * + * @param codeId + * @param requestDto + * @return + */ + @PutMapping("/api/v1/codes/{codeId}") + public String update(@PathVariable String codeId, @RequestBody CodeUpdateRequestDto requestDto) { + return codeService.update(codeId, requestDto); + } + + /** + * 사용여부 toggle + * + * @param codeId + * @param useAt + * @return + */ + @PutMapping("/api/v1/codes/{codeId}/toggle-use") + public String updateUseAt(@PathVariable String codeId, @RequestParam boolean useAt) { + return codeService.updateUseAt(codeId, useAt); + } + + /** + * 공통코드 삭제 + * + * @param codeId + */ + @DeleteMapping("/api/v1/codes/{codeId}") + public void delete(@PathVariable String codeId) { + codeService.delete(codeId); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/CodeDetailApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/CodeDetailApiController.java new file mode 100644 index 0000000..126bb3a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/CodeDetailApiController.java @@ -0,0 +1,141 @@ +package org.egovframe.cloud.portalservice.api.code; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.portalservice.api.code.dto.*; +import org.egovframe.cloud.portalservice.domain.code.CodeRepository; +import org.egovframe.cloud.portalservice.service.code.CodeDetailService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.api.code.CodeApiController + *

+ * 공통코드 CRUD 요청을 처리하는 REST API Controller + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor // final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성하여, 빈을 생성자로 주입받게 한다. +@RestController +public class CodeDetailApiController { + + private final CodeDetailService codeDetailService; + private final CodeRepository codeRepository; + + /** + * 공통코드 상세 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @GetMapping("/api/v1/code-details") + public Page findAllByKeyword(CodeDetailRequestDto requestDto, Pageable pageable) { + return codeRepository.findAllDetailByKeyword(requestDto, pageable); + } + + /** + * 부모공통코드의 상세 목록 조회 + * 사용중인 공통코드만 조회한다 + * + * @param parentCodeId + * @return + */ + @GetMapping("/api/v1/code-details/{parentCodeId}/codes") + public List findDetailsByParentCodeIdUseAt(@PathVariable String parentCodeId) { + return codeRepository.findDetailsByParentCodeIdUseAt(parentCodeId); + } + + /** + * 부모공통코드의 상세 목록 조회 + * 사용여부가 false 로 변경된 경우에도 인자로 받은 공통코드를 목록에 포함되도록 한다 + * + * @param parentCodeId + * @return + */ + @GetMapping("/api/v1/code-details/{parentCodeId}/codes/{codeId}") + public List findDetailsUnionCodeIdByParentCodeId(@PathVariable String parentCodeId, @PathVariable String codeId) { + return codeRepository.findDetailsUnionCodeIdByParentCodeId(parentCodeId, codeId); + } + + /** + * 공통코드 상세 단건 조회 + * + * @param codeId + * @return + */ + @GetMapping("/api/v1/code-details/{codeId}") + public CodeDetailResponseDto findByCodeId(@PathVariable String codeId) { + return codeDetailService.findByCodeId(codeId); + } + + /** + * 공통코드 부모코드 단건 조회 + * + * @param codeId + * @return + */ + @GetMapping("/api/v1/code-details/{codeId}/parent") + public CodeResponseDto findParentByCodeId(@PathVariable String codeId) { + return codeRepository.findParentByCodeId(codeId); + } + + /** + * 공통코드 상세 등록 + * + * @param requestDto + * @return + */ + @PostMapping("/api/v1/code-details") + public String save(@RequestBody @Valid CodeDetailSaveRequestDto requestDto) { + return codeDetailService.save(requestDto); + } + + /** + * 공통코드 상세 수정 + * + * @param codeId + * @param requestDto + * @return + */ + @PutMapping("/api/v1/code-details/{codeId}") + public String update(@PathVariable String codeId, @RequestBody CodeDetailUpdateRequestDto requestDto) { + return codeDetailService.update(codeId, requestDto); + } + + /** + * 사용여부 toggle + * + * @param codeId + * @param useAt + * @return + */ + @PutMapping("/api/v1/code-details/{codeId}/toggle-use") + public String updateUseAt(@PathVariable String codeId, @RequestParam boolean useAt) { + return codeDetailService.updateUseAt(codeId, useAt); + } + + /** + * 공통코드 상세 삭제 + * + * @param codeId + */ + @DeleteMapping("/api/v1/code-details/{codeId}") + public void delete(@PathVariable String codeId) { + codeDetailService.delete(codeId); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailListResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailListResponseDto.java new file mode 100644 index 0000000..b80a772 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailListResponseDto.java @@ -0,0 +1,32 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeDetailListResponseDto + *

+ * 공통코드 상세 목록 조회 응답 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CodeDetailListResponseDto { + private String parentCodeId; // 상위 코드ID + private String codeId; // 코드ID + private String codeName; // 코드 명 + private Integer sortSeq; // 정렬 순서 + private Boolean useAt; // 사용 여부 + private Boolean readonly; // 수정하면 안되는 읽기전용 공통코드 +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailRequestDto.java new file mode 100644 index 0000000..680fe87 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailRequestDto.java @@ -0,0 +1,30 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.egovframe.cloud.common.dto.RequestDto; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeDetailRequestDto + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@SuperBuilder +public class CodeDetailRequestDto extends RequestDto { + private String parentCodeId; // 상위 공통코드 ID +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailResponseDto.java new file mode 100644 index 0000000..9cd247d --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailResponseDto.java @@ -0,0 +1,51 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.code.Code; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeDetailResponseDto + *

+ * 공통코드 상세 조회 응답 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CodeDetailResponseDto { + private String codeId; // 코드ID + private String parentCodeId; // 상위 코드ID + private String codeName; // 코드 명 + private String codeDescription; // 코드 설명 + private Integer sortSeq; // 정렬 순서 + private Boolean useAt; // 사용 여부 + private Boolean readonly; // 수정하면 안되는 읽기전용 공통코드 + + /** + * Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣는다. + * 굳이 모든 필드를 가진 생성자가 필요하지 않다. + * + * @param entity + */ + public CodeDetailResponseDto(Code entity) { + this.codeId = entity.getCodeId(); + this.parentCodeId = entity.getParentCodeId(); + this.codeName = entity.getCodeName(); + this.codeDescription = entity.getCodeDescription(); + this.sortSeq = entity.getSortSeq(); + this.useAt = entity.getUseAt(); + this.readonly = entity.getReadonly(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailSaveRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailSaveRequestDto.java new file mode 100644 index 0000000..7dd95b4 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailSaveRequestDto.java @@ -0,0 +1,72 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.code.Code; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeDetailSaveRequestDto + *

+ * 공통코드 상세 등록 요청 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CodeDetailSaveRequestDto { + @NotBlank(message = "{code.parent_code_id}{valid.required}") + private String parentCodeId; // 상위 코드ID + + @NotBlank(message = "{code.code_id}{valid.required}") + private String codeId; // 코드ID + + @NotBlank(message = "{code.code_name}{valid.required}") + private String codeName; // 코드 명 + + private String codeDescription; // 코드 설명 + private Integer sortSeq; // 정렬 순서 + private Boolean useAt; // 사용 여부 + private Boolean readonly; // 수정하면 안되는 읽기전용 공통코드 + + @Builder + public CodeDetailSaveRequestDto(String parentCodeId, String codeId, String codeName, String codeDescription, Integer sortSeq, Boolean useAt, Boolean readonly) { + this.parentCodeId = parentCodeId; + this.codeId = codeId; + this.codeName = codeName; + this.codeDescription = codeDescription; + this.sortSeq = sortSeq; + this.useAt = useAt; + this.readonly = readonly; + } + + /** + * SaveRequestDto 의 필드 값을 Entity 빌더를 사용하여 주입 후 Entity 를 리턴한다. + * SaveRequestDto 가 가지고 있는 Entity 의 필드만 세팅할 수 있게 된다. + * + * @return + */ + public Code toEntity() { + return Code.builder() + .parentCodeId(parentCodeId) + .codeId(codeId) + .codeName(codeName) + .codeDescription(codeDescription) + .sortSeq(sortSeq) + .useAt(useAt) + .readonly(readonly != null && readonly) // readonly 값이 없으면 기본값은 false 로 설정한다 + .build(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailUpdateRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailUpdateRequestDto.java new file mode 100644 index 0000000..31d7e32 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeDetailUpdateRequestDto.java @@ -0,0 +1,64 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.code.Code; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeDetailSaveRequestDto + *

+ * 공통코드 상세 수정 요청 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CodeDetailUpdateRequestDto { + @NotBlank(message = "{code.parent_code_id}{valid.required}") + private String parentCodeId; // 상위 코드ID + + @NotBlank(message = "{code.code_name}{valid.required}") + private String codeName; // 코드 명 + + private String codeDescription; // 코드 설명 + private Integer sortSeq; // 정렬 순서 + private Boolean useAt; // 사용 여부 + + @Builder + public CodeDetailUpdateRequestDto(String parentCodeId, String codeName, String codeDescription, Integer sortSeq, Boolean useAt) { + this.parentCodeId = parentCodeId; + this.codeName = codeName; + this.codeDescription = codeDescription; + this.sortSeq = sortSeq; + this.useAt = useAt; + } + + /** + * SaveRequestDto 의 필드 값을 Entity 빌더를 사용하여 주입 후 Entity 를 리턴한다. + * SaveRequestDto 가 가지고 있는 Entity 의 필드만 세팅할 수 있게 된다. + * + * @return + */ + public Code toEntity() { + return Code.builder() + .parentCodeId(parentCodeId) + .codeName(codeName) + .codeDescription(codeDescription) + .sortSeq(sortSeq) + .useAt(useAt) + .build(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeListResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeListResponseDto.java new file mode 100644 index 0000000..c2f2dc5 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeListResponseDto.java @@ -0,0 +1,33 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.code.Code; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeListResponseDto + *

+ * 공통코드 목록 조회 응답 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CodeListResponseDto { + private String codeId; // 코드ID + private String codeName; // 코드 명 + private String codeDescription; // 코드 설명 + private Boolean useAt; // 사용 여부 + private Boolean readonly; // 수정하면 안되는 읽기전용 공통코드 + private Long codeDetailCount; // 코드상세수 +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeResponseDto.java new file mode 100644 index 0000000..145d1e0 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeResponseDto.java @@ -0,0 +1,48 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.code.Code; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeResponseDto + *

+ * 공통코드 조회 응답 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CodeResponseDto { + private String codeId; // 코드ID + private String codeName; // 코드 명 + private String codeDescription; // 코드 설명 + private Integer sortSeq; // 정렬 순서 + private Boolean useAt; // 사용 여부 + private Boolean readonly; // 수정하면 안되는 읽기전용 공통코드 + + /** + * Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣는다. 굳이 모든 필드를 가진 생성자가 필요하지 않다. + * + * @param entity + */ + public CodeResponseDto(Code entity) { + this.codeId = entity.getCodeId(); + this.codeName = entity.getCodeName(); + this.codeDescription = entity.getCodeDescription(); + this.sortSeq = entity.getSortSeq(); + this.useAt = entity.getUseAt(); + this.readonly = entity.getReadonly(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeSaveRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeSaveRequestDto.java new file mode 100644 index 0000000..aaafbc0 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeSaveRequestDto.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.code.Code; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeSaveRequestDto + *

+ * 공통코드 등록 요청 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CodeSaveRequestDto { + @NotBlank(message = "{code.code_id}{valid.required}") + private String codeId; // 코드ID + @NotBlank(message = "{code.code_name}{valid.required}") + private String codeName; // 코드 명 + private String codeDescription; // 코드 설명 + private Integer sortSeq; // 정렬 순서 + private Boolean useAt; // 사용 여부 + private Boolean readonly; // 수정하면 안되는 읽기전용 공통코드 + + @Builder + public CodeSaveRequestDto(String codeId, String codeName, String codeDescription, Integer sortSeq, Boolean useAt, Boolean readonly) { + this.codeId = codeId; + this.codeName = codeName; + this.codeDescription = codeDescription; + this.sortSeq = sortSeq; + this.useAt = useAt; + this.readonly = readonly; + } + + /** + * SaveRequestDto 의 필드 값을 Entity 빌더를 사용하여 주입 후 Entity 를 리턴한다. + * SaveRequestDto 가 가지고 있는 Entity 의 필드만 세팅할 수 있게 된다. + * + * @return + */ + public Code toEntity() { + return Code.builder() + .codeId(codeId) + .codeName(codeName) + .codeDescription(codeDescription) + .useAt(useAt) + .sortSeq(sortSeq) + .readonly(readonly != null && readonly) // readonly 값이 없으면 기본값은 false 로 설정한다 + .build(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeUpdateRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeUpdateRequestDto.java new file mode 100644 index 0000000..faa76f5 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/code/dto/CodeUpdateRequestDto.java @@ -0,0 +1,63 @@ +package org.egovframe.cloud.portalservice.api.code.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.code.Code; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.portalservice.api.code.dto.CodeUpdateRequestDto + *

+ * 공통코드 수정 요청 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class CodeUpdateRequestDto { + @NotBlank(message = "{code.code_id}{valid.required}") + private String codeId; // 코드ID + + @NotBlank(message = "{code.code_name}{valid.required}") + private String codeName; // 코드 명 + + private String codeDescription; // 코드 설명 + private Integer sortSeq; // 정렬 순서 + private Boolean useAt; // 사용 여부 + + @Builder + public CodeUpdateRequestDto(String codeId, String codeName, String codeDescription, Integer sortSeq, Boolean useAt) { + this.codeId = codeId; + this.codeName = codeName; + this.codeDescription = codeDescription; + this.sortSeq = sortSeq; + this.useAt = useAt; + } + + /** + * SaveRequestDto 의 필드 값을 Entity 빌더를 사용하여 주입 후 Entity 를 리턴한다. + * SaveRequestDto 가 가지고 있는 Entity 의 필드만 세팅할 수 있게 된다. + * + * @return + */ + public Code toEntity() { + return Code.builder() + .codeId(codeId) + .codeName(codeName) + .codeDescription(codeDescription) + .useAt(useAt) + .build(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/ContentApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/ContentApiController.java new file mode 100644 index 0000000..0f288f3 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/ContentApiController.java @@ -0,0 +1,108 @@ +package org.egovframe.cloud.portalservice.api.content; + +import javax.validation.Valid; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentListResponseDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentResponseDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentSaveRequestDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentUpdateRequestDto; +import org.egovframe.cloud.portalservice.service.content.ContentService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.api.content.ContentApiController + *

+ * 컨텐츠 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class ContentApiController { + + /** + * 컨텐츠 서비스 + */ + private final ContentService contentService; + + /** + * 컨텐츠 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 컨텐츠 목록 응답 DTO + */ + @GetMapping("/api/v1/contents") + public Page findPage(RequestDto requestDto, + @PageableDefault(sort = "content_no", direction = Sort.Direction.DESC) Pageable pageable) { + return contentService.findPage(requestDto, pageable); + } + + /** + * 컨텐츠 단건 조회 + * + * @param contentNo 컨텐츠 번호 + * @return ContentResponseDto 컨텐츠 상세 응답 DTO + */ + @GetMapping("/api/v1/contents/{contentNo}") + public ContentResponseDto findById(@PathVariable Integer contentNo) { + return contentService.findById(contentNo); + } + + /** + * 컨텐츠 등록 + * + * @param requestDto 컨텐츠 등록 요청 DTO + * @return ContentResponseDto 컨텐츠 상세 응답 DTO + */ + @PostMapping("/api/v1/contents") + public ContentResponseDto save(@RequestBody @Valid ContentSaveRequestDto requestDto) { + return contentService.save(requestDto); + } + + /** + * 컨텐츠 수정 + * + * @param contentNo 컨텐츠 번호 + * @param requestDto 컨텐츠 수정 요청 DTO + * @return ContentResponseDto 컨텐츠 상세 응답 DTO + */ + @PutMapping("/api/v1/contents/{contentNo}") + public ContentResponseDto update(@PathVariable Integer contentNo, @RequestBody @Valid ContentUpdateRequestDto requestDto) { + return contentService.update(contentNo, requestDto); + } + + /** + * 컨텐츠 삭제 + * + * @param contentNo 컨텐츠 번호 + */ + @DeleteMapping("/api/v1/contents/{contentNo}") + public void delete(@PathVariable Integer contentNo) { + contentService.delete(contentNo); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentListResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentListResponseDto.java new file mode 100644 index 0000000..8f63e5c --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentListResponseDto.java @@ -0,0 +1,72 @@ +package org.egovframe.cloud.portalservice.api.content.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.portalservice.api.content.dto.ContentListResponseDto + *

+ * 컨텐츠 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class ContentListResponseDto implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -1902534539945283321L; + + /** + * 컨텐츠 번호 + */ + private Integer contentNo; + + /** + * 컨텐츠 명 + */ + private String contentName; + + /** + * 수정자 + */ + private String lastModifiedBy; + + /** + * 수정 일시 + */ + private LocalDateTime modifiedDate; + + /** + * 컨텐츠 목록 응답 DTO 생성자 + * + * @param contentNo 컨텐츠 번호 + * @param contentName 컨텐츠 명 + * @param lastModifiedBy 수정자 + * @param modifiedDate 수정 일시 + */ + @QueryProjection + public ContentListResponseDto(Integer contentNo, String contentName, String lastModifiedBy, LocalDateTime modifiedDate) { + this.contentNo = contentNo; + this.contentName = contentName; + this.lastModifiedBy = lastModifiedBy; + this.modifiedDate = modifiedDate; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentResponseDto.java new file mode 100644 index 0000000..50d7f88 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentResponseDto.java @@ -0,0 +1,74 @@ +package org.egovframe.cloud.portalservice.api.content.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.content.Content; + +/** + * org.egovframe.cloud.portalservice.api.content.dto.ContentResponseDto + *

+ * 컨텐츠 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class ContentResponseDto { + + /** + * 컨텐츠 번호 + */ + private Integer contentNo; + + /** + * 컨텐츠 명 + */ + private String contentName; + + /** + * 컨텐츠 비고 + */ + private String contentRemark; + + /** + * 컨텐츠 값 + */ + private String contentValue; + + /** + * 컨텐츠 엔티티를 생성자로 주입 받아서 컨텐츠 상세 응답 DTO 속성 값 세팅 + * + * @param entity 컨텐츠 엔티티 + */ + public ContentResponseDto(Content entity) { + this.contentNo = entity.getContentNo(); + this.contentName = entity.getContentName(); + this.contentRemark = entity.getContentRemark(); + this.contentValue = entity.getContentValue(); + } + + /** + * 컨텐츠 상세 응답 DTO 속성 값으로 컨텐츠 엔티티 빌더를 사용하여 객체 생성 + * + * @return Content 컨텐츠 엔티티 + */ + public Content toEntity() { + return Content.builder() + .contentNo(contentNo) + .contentName(contentName) + .contentRemark(contentRemark) + .contentValue(contentValue) + .build(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentSaveRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentSaveRequestDto.java new file mode 100644 index 0000000..a6855a1 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentSaveRequestDto.java @@ -0,0 +1,58 @@ +package org.egovframe.cloud.portalservice.api.content.dto; + +import lombok.Getter; +import org.egovframe.cloud.portalservice.domain.content.Content; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.portalservice.api.content.dto.ContentSaveRequestDto + *

+ * 컨텐츠 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@Getter +public class ContentSaveRequestDto { + + /** + * 컨텐츠 명 + */ + @NotBlank(message = "{content.content_name} {err.required}") + private String contentName; + + /** + * 컨텐츠 비고 + */ + private String contentRemark; + + /** + * 컨텐츠 값 + */ + @NotBlank(message = "{content.content_value} {err.required}") + private String contentValue; + + /** + * 컨텐츠 등록 요청 DTO 속성 값으로 컨텐츠 엔티티 빌더를 사용하여 객체 생성 + * + * @return Content 컨텐츠 엔티티 + */ + public Content toEntity() { + return Content.builder() + .contentName(contentName) + .contentRemark(contentRemark) + .contentValue(contentValue) + .build(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentUpdateRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentUpdateRequestDto.java new file mode 100644 index 0000000..70f1ae2 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/content/dto/ContentUpdateRequestDto.java @@ -0,0 +1,44 @@ +package org.egovframe.cloud.portalservice.api.content.dto; + +import lombok.Getter; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.portalservice.api.content.dto.ContentUpdateRequestDto + *

+ * 컨텐츠 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +public class ContentUpdateRequestDto { + + /** + * 컨텐츠 명 + */ + @NotBlank(message = "{content.content_name} {err.required}") + private String contentName; + + /** + * 컨텐츠 비고 + */ + private String contentRemark; + + /** + * 컨텐츠 값 + */ + @NotBlank(message = "{content.content_value} {err.required}") + private String contentValue; + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/MenuApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/MenuApiController.java new file mode 100644 index 0000000..bb560bd --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/MenuApiController.java @@ -0,0 +1,128 @@ +package org.egovframe.cloud.portalservice.api.menu; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.portalservice.api.menu.dto.*; +import org.egovframe.cloud.portalservice.domain.menu.SiteRepository; +import org.egovframe.cloud.portalservice.service.menu.MenuService; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.api.menu.MenuApiController + *

+ * 메뉴관리 api controller 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor // final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성하여, 빈을 생성자로 주입받게 한다. +@RestController +public class MenuApiController { + + private final MenuService menuService; + + private final SiteRepository siteRepository; + + /** + * 사이트 목록 조회 + * + * @return + */ + @GetMapping("/api/v1/sites") + public List findAllSites() { + return siteRepository.findAllByIsUseTrueOrderBySortSeq(); + } + + /** + * 관리자 메뉴 트리 목록 조회 + * + * @param siteId + * @return + */ + @GetMapping("/api/v1/menus/{siteId}/tree") + public List findTreeBySiteId(@PathVariable Long siteId) { + return menuService.findTreeBySiteId(siteId); + } + + /** + * 메뉴 상세 정보 한건 조회 + * + * @param menuId + * @return + */ + @GetMapping("/api/v1/menus/{menuId}") + public MenuResponseDto findById(@PathVariable Long menuId) { + return menuService.findById(menuId); + } + + /** + * 트리 메뉴 한건 추가 + * + * @param menuTreeRequestDto + * @return + */ + @PostMapping(value = "/api/v1/menus") + public MenuTreeResponseDto save(@RequestBody @Valid MenuTreeRequestDto menuTreeRequestDto) { + return menuService.save(menuTreeRequestDto); + } + + /** + * 트리 드래그 앤드 드랍 저장 + * + * @param siteId + * @param menuDnDRequestDtoList + * @return + */ + @PutMapping(value = "/api/v1/menus/{siteId}/tree") + public Long saveDnD(@PathVariable Long siteId, @RequestBody List menuDnDRequestDtoList) { + return menuService.updateDnD(siteId, menuDnDRequestDtoList); + } + + /** + * 트리에서 메뉴명 변경 + * + * @param menuId + * @param name + * @return + */ + @PutMapping(value = "/api/v1/menus/{menuId}/{name}") + public MenuTreeResponseDto updateName(@PathVariable Long menuId, @PathVariable String name) { + return menuService.updateName(menuId, name); + } + + /** + * 메뉴 상세 정보 변경 + * + * @param menuId + * @param updateRequestDto + * @return + */ + @PutMapping(value = "/api/v1/menus/{menuId}") + public MenuResponseDto update(@PathVariable Long menuId, @RequestBody MenuUpdateRequestDto updateRequestDto) { + return menuService.update(menuId, updateRequestDto); + } + + /** + * 메뉴 삭제 + * + * @param menuId + */ + @DeleteMapping(value = "/api/v1/menus/{menuId}") + public void delete(@PathVariable Long menuId) { + menuService.delete(menuId); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/MenuRoleApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/MenuRoleApiController.java new file mode 100644 index 0000000..00abf31 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/MenuRoleApiController.java @@ -0,0 +1,72 @@ +package org.egovframe.cloud.portalservice.api.menu; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleRequestDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuSideResponseDto; +import org.egovframe.cloud.portalservice.domain.user.User; +import org.egovframe.cloud.portalservice.service.menu.MenuRoleService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.api.menu.MenuRoleApiController + *

+ * 권한별 메뉴관리 api controller 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/17    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor // final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성하여, 빈을 생성자로 주입받게 한다. +@RestController +public class MenuRoleApiController { + + private final MenuRoleService menuRoleService; + + /** + * 권한별 메뉴관리 트리 목록 조회 + * + * @param roleId + * @param siteId + * @return + */ + @GetMapping("/api/v1/menu-roles/{roleId}/{siteId}") + public List findTree(@PathVariable String roleId, @PathVariable Long siteId, User user) { + return menuRoleService.fineTree(roleId.toUpperCase(), siteId); + } + + /** + * 권한별 메뉴 저장 + * + * @param menuRoleRequestDtoList + * @return + */ + @PostMapping("/api/v1/menu-roles") + public String save(@RequestBody List menuRoleRequestDtoList) { + return menuRoleService.save(menuRoleRequestDtoList); + } + + /** + * 로그인 후 사용자 권한에 따른 메뉴 조회 + * + * @param siteId + * @return + */ + @GetMapping("/api/v1/menu-roles/{siteId}") + public List findMenus(@PathVariable Long siteId) { + return menuRoleService.findMenus(siteId); + + } + } diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuDnDRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuDnDRequestDto.java new file mode 100644 index 0000000..af298f4 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuDnDRequestDto.java @@ -0,0 +1,53 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.MenuDnDRequestDto + *

+ * 메뉴관리 Tree Drag and Drop 저장 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class MenuDnDRequestDto { + + private Long menuId; + private String name; + private Integer sortSeq; + private Long parentId; + private Integer level; + private String icon; + private List children = new ArrayList<>(); + + + @Builder + public MenuDnDRequestDto(Long menuId, String name, Integer sortSeq, Long parentId, Integer level, String icon, List children) { + this.menuId = menuId; + this.name = name; + this.sortSeq = sortSeq; + this.parentId = parentId; + this.level = level; + this.icon = icon; + this.children = children; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuResponseDto.java new file mode 100644 index 0000000..45094d9 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuResponseDto.java @@ -0,0 +1,59 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.domain.menu.Menu; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.MenuResponseDto + *

+ * 메뉴관리 상세 정보 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class MenuResponseDto { + private Long menuId; + private String menuKorName; + private String menuEngName; + private String menuType; + private Integer connectId; + private String connectName; + private String urlPath; + private Boolean isUse; + private Boolean isShow; + private Boolean isBlank; + private String subName; + private String description; + private String icon; + + @Builder + public MenuResponseDto(Menu entity) { + this.menuId = entity.getId(); + this.menuKorName = entity.getMenuKorName(); + this.menuEngName = entity.getMenuEngName(); + this.menuType = entity.getMenuType(); + this.connectId = entity.getConnectId(); + this.urlPath = entity.getUrlPath(); + this.isUse = entity.getIsUse(); + this.isShow = entity.getIsShow(); + this.isBlank = entity.getIsBlank(); + this.subName = entity.getSubName(); + this.description = entity.getDescription(); + this.icon = entity.getIcon(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuRoleRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuRoleRequestDto.java new file mode 100644 index 0000000..1ba8637 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuRoleRequestDto.java @@ -0,0 +1,58 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleRequestDto + *

+ * 권한별 메뉴관리 저장 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/13    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class MenuRoleRequestDto { + private Long menuRoleId; + private String roleId; + private Boolean isChecked; + private Long id; + private String korName; + private String engName; + private Long parentId; + private Integer sortSeq; + private String icon; + private Integer level; + private List children; + + @Builder + public MenuRoleRequestDto(Long menuRoleId, String roleId, Boolean isChecked, Long id, String korName, String engName, Long parentId, Integer sortSeq, String icon, Integer level, List children) { + this.menuRoleId = menuRoleId; + this.roleId = roleId; + this.isChecked = isChecked; + this.id = id; + this.korName = korName; + this.engName = engName; + this.parentId = parentId; + this.sortSeq = sortSeq; + this.icon = icon; + this.level = level; + this.children = children; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuRoleResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuRoleResponseDto.java new file mode 100644 index 0000000..7521df7 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuRoleResponseDto.java @@ -0,0 +1,82 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.domain.menu.Menu; +import org.egovframe.cloud.portalservice.domain.menu.MenuRole; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleResponseDto + *

+ * 권한별 메뉴관리 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/13    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class MenuRoleResponseDto { + private Long menuRoleId; + private String roleId; + private Boolean isChecked; + private Long id; + private String korName; + private String engName; + private Long parentId; + private Integer sortSeq; + private String icon; + private Integer level; + @ToString.Exclude + private List children; + + + /** + * 생성자 + * querydsl 사용시 Menu Entity로 생성 + * roleId에 해당하는 권한별 메뉴 데이터만 조회 + * + * @param menu + * @param roleId + */ + public MenuRoleResponseDto (Menu menu, String roleId) { + + MenuRole menuRole = menu.getMenuRole(roleId); + if (menuRole == null) { + this.isChecked = false; + this.roleId = roleId; + }else { + this.menuRoleId = menuRole.getId(); + this.roleId = menuRole.getRoleId(); + this.isChecked = true; + } + + this.id = menu.getId(); + this.korName = menu.getMenuKorName(); + this.engName = menu.getMenuEngName(); + if (menu.getParent() != null) { + this.parentId = menu.getParent().getId(); + } + + this.sortSeq = menu.getSortSeq(); + this.icon = menu.getIcon(); + this.level = menu.getLevel(); + this.children = menu.getChildren().stream() + .map(children -> new MenuRoleResponseDto(children, roleId)) + .collect(Collectors.toList()); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuSideResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuSideResponseDto.java new file mode 100644 index 0000000..af29ef8 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuSideResponseDto.java @@ -0,0 +1,82 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.domain.menu.Menu; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.MenuSideResponseDto + *

+ * 로그인 후 사용자 권한별 메뉴 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/13    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class MenuSideResponseDto { + private Long id; + private String korName; + private String engName; + private Long parentId; + private Integer sortSeq; + private String icon; + private Integer level; + private String urlPath; + private Integer connectId; + private String menuType; + private Boolean isShow; + @ToString.Exclude + private List children; + + /** + * 생성자 + * 계층구조를 만들기 위해 querydsl에서 사용 + * + * @param menu + */ + public MenuSideResponseDto (Menu menu, String roleId) { + this.id = menu.getId(); + this.korName = menu.getMenuKorName(); + this.engName = menu.getMenuEngName(); + if (menu.getParent() != null) { + this.parentId = menu.getParent().getId(); + } + this.sortSeq = menu.getSortSeq(); + this.icon = menu.getIcon(); + this.level = menu.getLevel(); + this.menuType = menu.getMenuType(); + this.connectId = menu.getConnectId(); + this.urlPath = menu.getUrlPath(); + this.isShow = menu.getIsShow(); + + this.children = menu.getChildren().stream() + .filter(children -> children.getIsUse()) + .filter(children -> children.getMenuRole(roleId) != null) + .map(children -> new MenuSideResponseDto(children, roleId)) + .collect(Collectors.toList()); + } + + /** + * 메뉴유형이 게시판이나 컨텐츠인 경우 urlPath 지정 + * + * @param urlPath + */ + public void setUrlPath(String urlPath) { + this.urlPath = urlPath; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuTreeRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuTreeRequestDto.java new file mode 100644 index 0000000..0a6709b --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuTreeRequestDto.java @@ -0,0 +1,51 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.domain.menu.Menu; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.MenuTreeRequestDto + *

+ * 메뉴관리 tree 추가 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class MenuTreeRequestDto { + private Long siteId; + private String name; + private Integer sortSeq; + private Long parentId; + private Integer level; + private Boolean isShow; + private Boolean isUse; + + @Builder + public MenuTreeRequestDto(Long siteId, String name, Integer sortSeq, Long parentId, Integer level, Boolean isShow, Boolean isUse) { + this.siteId = siteId; + this.name = name; + this.sortSeq = sortSeq; + this.parentId = parentId; + this.level = level; + this.isShow = isShow; + this.isUse = isUse; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuTreeResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuTreeResponseDto.java new file mode 100644 index 0000000..f4b7f94 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuTreeResponseDto.java @@ -0,0 +1,57 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.domain.menu.Menu; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.MenuTreeResponseDto + *

+ * 메뉴관리 tree 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class MenuTreeResponseDto { + private Long menuId; + private String name; + private Long parentId; + private Integer sortSeq; + private String icon; + @ToString.Exclude + private List children; + private Integer level; + + @Builder + public MenuTreeResponseDto(Menu entity) { + this.menuId = entity.getId(); + this.name = entity.getMenuKorName(); + if (entity.getParent() != null) { + this.parentId = entity.getParent().getId(); + } + + this.sortSeq = entity.getSortSeq(); + this.icon = entity.getIcon(); + this.level = entity.getLevel(); + this.children = entity.getChildren().stream() + .map(children -> new MenuTreeResponseDto(children)) + .collect(Collectors.toList()); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuUpdateRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuUpdateRequestDto.java new file mode 100644 index 0000000..793c1fc --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/MenuUpdateRequestDto.java @@ -0,0 +1,61 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.MenuUpdateRequestDto + *

+ * 메뉴관리 상세정보 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class MenuUpdateRequestDto { + @NotBlank(message = "{menu.name}{valid.required}") + private String menuKorName; + @NotBlank(message = "{menu.eng_name}{valid.required}") + private String menuEngName; + private String menuType; + private String menuTypeName; + private Integer connectId; + private String urlPath; + private Boolean isUse; + private Boolean isShow; + private Boolean isBlank; + private String subName; + private String description; + private String icon; + + @Builder + public MenuUpdateRequestDto(String menuKorName, String menuEngName, String menuType, String menuTypeName, Integer connectId, String urlPath, Boolean isUse, Boolean isShow, Boolean isBlank, String subName, String description, String icon) { + this.menuKorName = menuKorName; + this.menuEngName = menuEngName; + this.menuType = menuType; + this.menuTypeName = menuTypeName; + this.connectId = connectId; + this.urlPath = urlPath; + this.isUse = isUse; + this.isShow = isShow; + this.isBlank = isBlank; + this.subName = subName; + this.description = description; + this.icon = icon; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/SiteResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/SiteResponseDto.java new file mode 100644 index 0000000..6980492 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/menu/dto/SiteResponseDto.java @@ -0,0 +1,40 @@ +package org.egovframe.cloud.portalservice.api.menu.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.domain.menu.Site; + +/** + * org.egovframe.cloud.portalservice.api.menu.dto.SiteResponseDto + *

+ * 메뉴관리 사이트 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class SiteResponseDto { + private Long id; + private String name; + private Boolean isUse; + + @Builder + public SiteResponseDto(Site entity) { + this.id = entity.getId(); + this.name = entity.getName(); + this.isUse = entity.getIsUse(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/message/MessageApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/message/MessageApiController.java new file mode 100644 index 0000000..a3d79eb --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/message/MessageApiController.java @@ -0,0 +1,74 @@ +package org.egovframe.cloud.portalservice.api.message; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.portalservice.api.message.dto.MessageListResponseDto; +import org.egovframe.cloud.portalservice.config.MessageSourceFiles; +import org.egovframe.cloud.portalservice.domain.message.MessageRepository; +import org.springframework.context.MessageSource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * org.egovframe.cloud.portalservice.api.message.MessageApiController + *

+ * Message 요청을 처리하는 REST API Controller + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor // final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성하여, 빈을 생성자로 주입받게 한다. +@RestController +public class MessageApiController { + + private final MessageRepository messageRepository; + private final MessageSource messageSource; + private final MessageSourceFiles messageSourceFiles; + + /** + * Message 목록 조회 + * + * @param lang ko/en + * @return + * @deprecated Map 형태 반환을 기본으로 한다. 이 API는 사용하지 않는다. + */ + @GetMapping("/api/v1/messages/{lang}/list-type") + public List findAllMessages(@PathVariable String lang) { + return messageRepository.findAllMessages(lang); + } + + /** + * Message 목록 조회하여 Map 형태로 변환하여 반환한다 + * + * @param lang ko/en + * @return + */ + @GetMapping("/api/v1/messages/{lang}") + public Map findAllMessagesMap(@PathVariable String lang) { + return messageRepository.findAllMessagesMap(lang); + } + + @GetMapping("/api/v1/messages/{code}/{lang}") + public String getMessage(@PathVariable String code, @PathVariable String lang) { + Locale locale = "en".equals(lang)? Locale.ENGLISH : Locale.KOREAN; + return messageSource.getMessage(code, null, locale); + } + + @GetMapping("/api/v1/messages/refresh") + public int refresh() { + return messageSourceFiles.create(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/message/dto/MessageListResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/message/dto/MessageListResponseDto.java new file mode 100644 index 0000000..8251037 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/message/dto/MessageListResponseDto.java @@ -0,0 +1,30 @@ +package org.egovframe.cloud.portalservice.api.message.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; + +/** + * org.egovframe.cloud.portalservice.api.message.dto.MessageListResponseDto + *

+ * Message 목록 조회 응답 dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class MessageListResponseDto { + private String messageId; + private String messageName; +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/PolicyApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/PolicyApiController.java new file mode 100644 index 0000000..2a0a178 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/PolicyApiController.java @@ -0,0 +1,119 @@ +package org.egovframe.cloud.portalservice.api.policy; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyResponseDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicySaveRequestDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyUpdateRequestDto; +import org.egovframe.cloud.portalservice.service.policy.PolicyService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +/** + * org.egovframe.cloud.portalservice.api.policy.PolicyApiController + *

+ * 이용약관/개인정보수집동의(Policy) API class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/06    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class PolicyApiController { + + private final PolicyService policyService; + + /** + * 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @GetMapping("/api/v1/policies") + public Page search(RequestDto requestDto, Pageable pageable) { + return policyService.search(requestDto, pageable); + } + + /** + * 단건 조회 + * + * @param id + * @return + */ + @GetMapping("/api/v1/policies/{id}") + public PolicyResponseDto findById(@PathVariable Long id) { + return policyService.findById(id); + } + + /** + * 회원가입 시 가장 최근등록 된 자료 단건 조회 + * + * @param type + * @return + */ + @GetMapping("/api/v1/policies/latest/{type}") + public PolicyResponseDto searchOne(@PathVariable String type) { + return policyService.searchOne(type); + } + + /** + * 등록 + * + * @param saveRequestDto + * @return + */ + @PostMapping("/api/v1/policies") + public Long save(@RequestBody PolicySaveRequestDto saveRequestDto) { + System.out.println(saveRequestDto.toString()); + return policyService.save(saveRequestDto); + } + + /** + * 수정 + * + * @param id + * @param updateRequestDto + * @return + */ + @PutMapping("/api/v1/policies/{id}") + public Long update(@PathVariable Long id, @RequestBody PolicyUpdateRequestDto updateRequestDto) { + return policyService.update(id, updateRequestDto); + } + + /** + * 사용여부 toggle + * + * @param id + * @param isUse + * @return + */ + @PutMapping("/api/v1/policies/{id}/{isUse}") + public Long updateIsUse(@PathVariable Long id, @PathVariable boolean isUse) { + return policyService.updateIsUse(id, isUse); + } + + /** + * 삭제 + * + * @param id + */ + @DeleteMapping("/api/v1/policies/{id}") + public void delete(@PathVariable Long id) { + policyService.delete(id); + } + + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicyResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicyResponseDto.java new file mode 100644 index 0000000..0230d1b --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicyResponseDto.java @@ -0,0 +1,50 @@ +package org.egovframe.cloud.portalservice.api.policy.dto; + + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.domain.policy.Policy; + +import java.time.ZonedDateTime; + +/** + * org.egovframe.cloud.portalservice.api.policy.dto.PolicyResponseDto + *

+ * 이용약관/개인정보수집동의(Policy) 응답 dto + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/06    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class PolicyResponseDto { + private Long id; + private String type; + private String title; + private Boolean isUse; + private ZonedDateTime regDate; + private String contents; + + @Builder + public PolicyResponseDto(Policy policy){ + this.id = policy.getId(); + this.type = policy.getType(); + this.title = policy.getTitle(); + this.isUse = policy.getIsUse(); + this.regDate = policy.getRegDate(); + this.contents = policy.getContents(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicySaveRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicySaveRequestDto.java new file mode 100644 index 0000000..4c672ac --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicySaveRequestDto.java @@ -0,0 +1,62 @@ +package org.egovframe.cloud.portalservice.api.policy.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.policy.Policy; + +import javax.validation.constraints.NotBlank; +import java.time.ZonedDateTime; + +/** + * org.egovframe.cloud.portalservice.api.policy.dto.PolicySaveRequestDto + *

+ * 이용약관/개인정보수집동의(Policy) 등록 시 요청 dto + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/06    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PolicySaveRequestDto { + @NotBlank(message = "{common.type}{valid.required}") + private String type; + @NotBlank(message = "{policy.title}{valid.required}") + private String title; + private Boolean isUse; + private ZonedDateTime regDate; + private String contents; + + @Builder + public PolicySaveRequestDto(String type, String title, Boolean isUse, ZonedDateTime regDate, String contents){ + this.type = type; + this.title = title; + this.isUse = isUse; + this.regDate = regDate; + this.contents = contents; + } + + /** + * 저장 요청 dto -> 이용약관 entity + * + * @return + */ + public Policy toEntity(){ + return Policy.builder() + .type(this.type) + .title(this.title) + .isUse(this.isUse) + .regDate(this.regDate) + .contents(this.contents) + .build(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicyUpdateRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicyUpdateRequestDto.java new file mode 100644 index 0000000..fa9bd6d --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/policy/dto/PolicyUpdateRequestDto.java @@ -0,0 +1,38 @@ +package org.egovframe.cloud.portalservice.api.policy.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.api.policy.dto.PolicyUpdateRequestDto + *

+ * 이용약관/개인정보수집동의(Policy) 수정 시 요청 dto + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/06    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PolicyUpdateRequestDto { + + private String title; + private Boolean isUse; + private String contents; + + @Builder + public PolicyUpdateRequestDto(String title, Boolean isUse, String contents){ + this.title = title; + this.isUse = isUse; + this.contents = contents; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/PrivacyApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/PrivacyApiController.java new file mode 100644 index 0000000..b5e278f --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/PrivacyApiController.java @@ -0,0 +1,132 @@ +package org.egovframe.cloud.portalservice.api.privacy; + +import java.util.List; + +import javax.validation.Valid; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyListResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacySaveRequestDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyUpdateRequestDto; +import org.egovframe.cloud.portalservice.service.privacy.PrivacyService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.api.privacy.PrivacyApiController + *

+ * 개인정보처리방침 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class PrivacyApiController { + + /** + * 개인정보처리방침 서비스 + */ + private final PrivacyService privacyService; + + /** + * 개인정보처리방침 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 개인정보처리방침 목록 응답 DTO + */ + @GetMapping("/api/v1/privacies") + public Page findPage(RequestDto requestDto, + @PageableDefault(sort = "privacy_no", direction = Sort.Direction.DESC) Pageable pageable) { + return privacyService.findPage(requestDto, pageable); + } + + /** + * 개인정보처리방침 사용중인 내림차순 전체 목록 조회 + * + * @return List 개인정보처리방침 상세 응답 DTO List + */ + @GetMapping("/api/v1/privacies/all/use") + public List findAllByUse() { + return privacyService.findAllByUseAt(true); + } + + /** + * 개인정보처리방침 단건 조회 + * + * @param privacyNo 개인정보처리방침 번호 + * @return PrivacyResponseDto 개인정보처리방침 상세 응답 DTO + */ + @GetMapping("/api/v1/privacies/{privacyNo}") + public PrivacyResponseDto findById(@PathVariable Integer privacyNo) { + return privacyService.findById(privacyNo); + } + + /** + * 개인정보처리방침 등록 + * + * @param requestDto 개인정보처리방침 등록 요청 DTO + * @return PrivacyResponseDto 개인정보처리방침 상세 응답 DTO + */ + @PostMapping("/api/v1/privacies") + public PrivacyResponseDto save(@RequestBody @Valid PrivacySaveRequestDto requestDto) { + return privacyService.save(requestDto); + } + + /** + * 개인정보처리방침 수정 + * + * @param privacyNo 개인정보처리방침 번호 + * @param requestDto 개인정보처리방침 수정 요청 DTO + * @return PrivacyResponseDto 개인정보처리방침 상세 응답 DTO + */ + @PutMapping("/api/v1/privacies/{privacyNo}") + public PrivacyResponseDto update(@PathVariable Integer privacyNo, @RequestBody @Valid PrivacyUpdateRequestDto requestDto) { + return privacyService.update(privacyNo, requestDto); + } + + /** + * 개인정보처리방침 사용 여부 수정 + * + * @param privacyNo 개인정보처리방침 번호 + * @param useAt 사용 여부 + * @return PrivacyResponseDto 개인정보처리방침 상세 응답 DTO + */ + @PutMapping("/api/v1/privacies/{privacyNo}/{useAt}") + public PrivacyResponseDto updateUseAt(@PathVariable Integer privacyNo, @PathVariable Boolean useAt) { + return privacyService.updateUseAt(privacyNo, useAt); + } + + /** + * 개인정보처리방침 삭제 + * + * @param privacyNo 개인정보처리방침 번호 + */ + @DeleteMapping("/api/v1/privacies/{privacyNo}") + public void delete(@PathVariable Integer privacyNo) { + privacyService.delete(privacyNo); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyListResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyListResponseDto.java new file mode 100644 index 0000000..1c06f5e --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyListResponseDto.java @@ -0,0 +1,72 @@ +package org.egovframe.cloud.portalservice.api.privacy.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyListResponseDto + *

+ * 개인정보처리방침 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PrivacyListResponseDto implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = -6649931248096772439L; + + /** + * 개인정보처리방침 번호 + */ + private Integer privacyNo; + + /** + * 개인정보처리방침 제목 + */ + private String privacyTitle; + + /** + * 사용 여부 + */ + private Boolean useAt; + + /** + * 생성 일시 + */ + private LocalDateTime createdDate; + + /** + * 개인정보처리방침 목록 응답 DTO 생성자 + * + * @param privacyNo 개인정보처리방침 번호 + * @param privacyTitle 개인정보처리방침 제목 + * @param useAt 사용 여부 + * @param createdDate 생성 일시 + */ + @QueryProjection + public PrivacyListResponseDto(Integer privacyNo, String privacyTitle, Boolean useAt, LocalDateTime createdDate) { + this.privacyNo = privacyNo; + this.privacyTitle = privacyTitle; + this.useAt = useAt; + this.createdDate = createdDate; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyResponseDto.java new file mode 100644 index 0000000..9809718 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyResponseDto.java @@ -0,0 +1,91 @@ +package org.egovframe.cloud.portalservice.api.privacy.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.portalservice.domain.privacy.Privacy; + +/** + * org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyResponseDto + *

+ * 개인정보처리방침 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class PrivacyResponseDto { + + /** + * 개인정보처리방침 번호 + */ + private Integer privacyNo; + + /** + * 개인정보처리방침 제목 + */ + private String privacyTitle; + + /** + * 개인정보처리방침 내용 + */ + private String privacyContent; + + /** + * 사용 여부 + */ + private Boolean useAt; + + /** + * 개인정보처리방침 목록 응답 DTO 생성자 + * + * @param privacyNo 개인정보처리방침 번호 + * @param privacyTitle 개인정보처리방침 제목 + * @param privacyContent 개인정보처리방침 내용 + * @param useAt 사용 여부 + */ + @QueryProjection + public PrivacyResponseDto(Integer privacyNo, String privacyTitle, String privacyContent, Boolean useAt) { + this.privacyNo = privacyNo; + this.privacyTitle = privacyTitle; + this.privacyContent = privacyContent; + this.useAt = useAt; + } + + /** + * 개인정보처리방침 엔티티를 생성자로 주입 받아서 개인정보처리방침 상세 응답 DTO 속성 값 세팅 + * + * @param entity 개인정보처리방침 엔티티 + */ + public PrivacyResponseDto(Privacy entity) { + this.privacyNo = entity.getPrivacyNo(); + this.privacyTitle = entity.getPrivacyTitle(); + this.privacyContent = entity.getPrivacyContent(); + this.useAt = entity.getUseAt(); + } + + /** + * 개인정보처리방침 상세 응답 DTO 속성 값으로 개인정보처리방침 엔티티 빌더를 사용하여 객체 생성 + * + * @return Privacy 개인정보처리방침 엔티티 + */ + public Privacy toEntity() { + return Privacy.builder() + .privacyNo(privacyNo) + .privacyTitle(privacyTitle) + .privacyContent(privacyContent) + .useAt(useAt) + .build(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacySaveRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacySaveRequestDto.java new file mode 100644 index 0000000..4b552ba --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacySaveRequestDto.java @@ -0,0 +1,60 @@ +package org.egovframe.cloud.portalservice.api.privacy.dto; + +import lombok.Getter; +import org.egovframe.cloud.portalservice.domain.privacy.Privacy; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.portalservice.api.privacy.dto.PrivacySaveRequestDto + *

+ * 개인정보처리방침 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +@Getter +public class PrivacySaveRequestDto { + + /** + * 개인정보처리방침 제목 + */ + @NotBlank(message = "{privacy.privacy_title} {err.required}") + private String privacyTitle; + + /** + * 개인정보처리방침 내용 + */ + @NotBlank(message = "{privacy.privacy_content} {err.required}") + private String privacyContent; + + /** + * 사용 여부 + */ + @NotNull(message = "{common.use_at} {err.required}") + private Boolean useAt; + + /** + * 개인정보처리방침 등록 요청 DTO 속성 값으로 개인정보처리방침 엔티티 빌더를 사용하여 객체 생성 + * + * @return Privacy 개인정보처리방침 엔티티 + */ + public Privacy toEntity() { + return Privacy.builder() + .privacyTitle(privacyTitle) + .privacyContent(privacyContent) + .useAt(useAt) + .build(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyUpdateRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyUpdateRequestDto.java new file mode 100644 index 0000000..df0fdce --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/privacy/dto/PrivacyUpdateRequestDto.java @@ -0,0 +1,46 @@ +package org.egovframe.cloud.portalservice.api.privacy.dto; + +import lombok.Getter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyUpdateRequestDto + *

+ * 개인정보처리방침 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +public class PrivacyUpdateRequestDto { + + /** + * 개인정보처리방침 제목 + */ + @NotBlank(message = "{privacy.privacy_title} {err.required}") + private String privacyTitle; + + /** + * 개인정보처리방침 내용 + */ + @NotBlank(message = "{privacy.privacy_content} {err.required}") + private String privacyContent; + + /** + * 사용 여부 + */ + @NotNull(message = "{common.use_at} {err.required}") + private Boolean useAt; + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/StatisticsApiController.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/StatisticsApiController.java new file mode 100644 index 0000000..d798a27 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/StatisticsApiController.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.portalservice.api.statistics; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsResponseDto; +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsYMRequestDto; +import org.egovframe.cloud.portalservice.service.statistics.StatisticsService; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +/** + * org.egovframe.cloud.portalservice.api.statistics.StatisticsApiController + *

+ * 통계 api controller class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/07    shinmj      최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class StatisticsApiController { + private final StatisticsService statisticsService; + + /** + * 접속 통계 월별 + * + * @param siteId + * @return + */ + @GetMapping("/api/v1/statistics/monthly/{siteId}") + public List findMonthlyBySiteId(@PathVariable Long siteId) { + return statisticsService.findMonthlyBySiteId(siteId); + } + + /** + * 접속 통계 일별 + * + * @param siteId + * @return + */ + @GetMapping("/api/v1/statistics/daily/{siteId}") + public List findDailyBySiteId(@PathVariable Long siteId, StatisticsYMRequestDto requestDto) { + return statisticsService.findDailyBySiteId(siteId, requestDto); + } + + /** + * 접속통계 등록 + * + * @param statisticsId + * @param request + */ + @PostMapping("/api/v1/statistics/{statisticsId}") + public void save(@PathVariable String statisticsId, HttpServletRequest request) { + statisticsService.save(request, statisticsId); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/dto/StatisticsResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/dto/StatisticsResponseDto.java new file mode 100644 index 0000000..62ea949 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/dto/StatisticsResponseDto.java @@ -0,0 +1,57 @@ +package org.egovframe.cloud.portalservice.api.statistics.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsResponseDto + *

+ * 통계 차트 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/07    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class StatisticsResponseDto { + private Integer year; + private Integer month; + private Integer day; + private String x; + private Long y; + + @Builder + public StatisticsResponseDto(Integer year, Integer month, Integer day, Integer x, Long y) { + this.year = year; + this.month = month; + this.day = day; + this.x = convertLabel(x); + this.y = y; + } + + /** + * 일/월 에 대한 라벨 설정 + * 01,02....11...31 + * + * @param x + * @return + */ + private String convertLabel(Integer x) { + if (x < 10) { + return "0"+x; + } + return ""+x; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/dto/StatisticsYMRequestDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/dto/StatisticsYMRequestDto.java new file mode 100644 index 0000000..eb75437 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/api/statistics/dto/StatisticsYMRequestDto.java @@ -0,0 +1,20 @@ +package org.egovframe.cloud.portalservice.api.statistics.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@ToString +public class StatisticsYMRequestDto { + private Integer year; + private Integer month; + + @Builder + public StatisticsYMRequestDto(Integer year, Integer month) { + this.year = year; + this.month = month; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/BoardServiceClient.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/BoardServiceClient.java new file mode 100644 index 0000000..94b5f58 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/BoardServiceClient.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.portalservice.client; + +import org.egovframe.cloud.portalservice.client.dto.BoardResponseDto; +import org.egovframe.cloud.portalservice.config.CustomFeignConfiguration; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +/** + * org.egovframe.cloud.portalservice.client.BoardServiceClient + *

+ * 게시판 서비스와 통신하는 feign client interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/19 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/19    shinmj  최초 생성
+ * 
+ */ +@FeignClient(value = "board-service", configuration = CustomFeignConfiguration.class) +public interface BoardServiceClient { + /** + * 게시판 한건 조회 + * + * @param boardNo + * @return + */ + @GetMapping("/api/v1/boards/{boardNo}") + BoardResponseDto findById(@PathVariable("boardNo") Integer boardNo); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/decoder/CustomErrorDecoder.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/decoder/CustomErrorDecoder.java new file mode 100644 index 0000000..4749e35 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/decoder/CustomErrorDecoder.java @@ -0,0 +1,51 @@ +package org.egovframe.cloud.portalservice.client.decoder; + +import feign.Response; +import feign.codec.ErrorDecoder; +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; + +/** + * org.egovframe.cloud.portalservice.client.decoder.CustomErrorDecoder + *

+ * feign client custom 에러 핸들링 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/23    shinmj  최초 생성
+ * 
+ */ +@Slf4j +public class CustomErrorDecoder implements ErrorDecoder { + @Override + public Exception decode(String methodKey, Response response) { + log.info("%s 요청이 성공하지 못했습니다. status : %s, body : %s", + methodKey, response.status(), response.body()); + + switch (response.status()) { + case 400: + return new BusinessMessageException(response.body().toString()); + case 401: + return new BusinessException(ErrorCode.UNAUTHORIZED); + case 403: + return new BusinessException(ErrorCode.JWT_EXPIRED); + case 404: + return new BusinessException(ErrorCode.NOT_FOUND); + case 405: + return new BusinessException(ErrorCode.METHOD_NOT_ALLOWED); + case 422: + return new BusinessException(ErrorCode.UNPROCESSABLE_ENTITY); + default: + return new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/dto/BoardResponseDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/dto/BoardResponseDto.java new file mode 100644 index 0000000..e365a28 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/client/dto/BoardResponseDto.java @@ -0,0 +1,94 @@ +package org.egovframe.cloud.portalservice.client.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * org.egovframe.cloud.boardservice.api.board.dto.BoardResponseDto + *

+ * 게시판 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class BoardResponseDto implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = -7139346671431363426L; + + /** + * 게시판 번호 + */ + private Integer boardNo; + + /** + * 게시판 제목 + */ + private String boardName; + + /** + * 스킨 유형 코드 + */ + private String skinTypeCode; + + /** + * 제목 표시 길이 + */ + private Integer titleDisplayLength; + + /** + * 게시물 표시 수 + */ + private Integer postDisplayCount; + + /** + * 페이지 표시 수 + */ + private Integer pageDisplayCount; + + /** + * 표시 신규 수 + */ + private Integer newDisplayDayCount; + + /** + * 에디터 사용 여부 + */ + private Boolean editorUseAt; + + /** + * 댓글 사용 여부 + */ + private Boolean commentUseAt; + + /** + * 업로드 사용 여부 + */ + private Boolean uploadUseAt; + + /** + * 업로드 제한 수 + */ + private Integer uploadLimitCount; + + /** + * 업로드 제한 크기 + */ + private Integer uploadLimitSize; + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/CustomFeignConfiguration.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/CustomFeignConfiguration.java new file mode 100644 index 0000000..412572d --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/CustomFeignConfiguration.java @@ -0,0 +1,58 @@ +package org.egovframe.cloud.portalservice.config; + +import feign.Logger; +import feign.Retryer; +import feign.codec.ErrorDecoder; +import org.egovframe.cloud.portalservice.client.decoder.CustomErrorDecoder; +import org.springframework.context.annotation.Bean; + +/** + * org.egovframe.cloud.portalservice.config.CustomFeignConfiguration + *

+ * feign client custom 설정 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/23    shinmj  최초 생성
+ * 
+ */ +public class CustomFeignConfiguration { + + /** + * log level 설정 + * + * @return + */ + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.BASIC; + } + + /** + * 에러 핸들링 + * + * @return + */ + @Bean + public ErrorDecoder errorDecoder() { + return new CustomErrorDecoder(); + } + + /** + * retryer 설정 + * + * @return + */ + @Bean + public Retryer retryer() { + return new Retryer.Default(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/EventStreamConfig.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/EventStreamConfig.java new file mode 100644 index 0000000..fb07f5c --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/EventStreamConfig.java @@ -0,0 +1,30 @@ +package org.egovframe.cloud.portalservice.config; + +import java.util.function.Consumer; + +import org.egovframe.cloud.common.dto.AttachmentEntityMessage; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentUploadRequestDto; +import org.egovframe.cloud.portalservice.service.attachment.AttachmentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +public class EventStreamConfig { + + @Autowired + private AttachmentService attachmentService; + + @Bean + public Consumer attachmentEntity() { + return attachmentEntityMessage -> attachmentService.updateEntity( + attachmentEntityMessage.getAttachmentCode(), + AttachmentUploadRequestDto.builder() + .entityName(attachmentEntityMessage.getEntityName()) + .entityId(attachmentEntityMessage.getEntityId()) + .build()); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/MessageSourceFiles.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/MessageSourceFiles.java new file mode 100644 index 0000000..c732342 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/MessageSourceFiles.java @@ -0,0 +1,120 @@ +package org.egovframe.cloud.portalservice.config; + +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.portalservice.domain.message.Message; +import org.egovframe.cloud.portalservice.domain.message.MessageRepository; +import org.egovframe.cloud.portalservice.utils.FileStorageUtils; +import org.egovframe.cloud.portalservice.utils.FtpClientDto; +import org.egovframe.cloud.portalservice.utils.StorageUtils; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * org.egovframe.cloud.portalservice.config.MessageSourceFileCreate + *

+ * 서비스 기동시 호출되어 messages/messages{lang}.properties 를 jar 실행되는 위치에 생성한다. + * 각 서비스에서 해당 파일을 통해 다국어를 지원하도록 한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/09    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@RefreshScope +@Component +public class MessageSourceFiles { + + private final MessageRepository messageRepository; + private final Environment environment; + private final StorageUtils storageUtils; + + public MessageSourceFiles(MessageRepository messageRepository, Environment environment, StorageUtils storageUtils) { + this.messageRepository = messageRepository; + this.environment = environment; + this.storageUtils = storageUtils; + } + + @PostConstruct + public int create() { + // db 에서 messages 를 조회한다. + List messages = messageRepository.findAll(); + log.info("messages size = {}", messages.size()); + if (messages.size() == 0) { + return 0; + } + + // 기본 properties 파일과 언어별 properties 파일을 생성한다. + String[] langs = new String[]{"", "_ko", "_en"}; + List files = new ArrayList<>(); + + // 메시지 폴더 경로 + final String fileMessagesDirectory = StringUtils.cleanPath(environment.getProperty("file.directory") + "/messages"); + try { + Files.createDirectory(Paths.get(fileMessagesDirectory).toAbsolutePath().normalize()); + } catch (FileAlreadyExistsException e) { + log.info("메시지 폴더 경로에 파일이나 디렉토리가 이미 존재, {}", e.getMessage()); + } catch (IOException e) { + log.error("메시지 폴더 생성 오류", e); + } + + for (String lang : langs) { + Properties prop = new Properties(); + + // Properties 에 조회한 messages set + if ("_en".equals(lang)) { + for (Message message : messages) { + String name = StringUtils.hasLength(message.getMessageEnName()) ? message.getMessageEnName() : message.getMessageKoName(); + prop.setProperty(message.getMessageId(), name); + } + } else { + for (Message message : messages) { + prop.setProperty(message.getMessageId(), message.getMessageKoName()); + } + } + + File propFile = new File(StringUtils.cleanPath(fileMessagesDirectory + "/messages" + lang + ".properties")); + log.info("messages properties path={}", propFile.getPath()); + propFile.setReadable(true); + propFile.setWritable(true, true); + + try (FileOutputStream out = new FileOutputStream(propFile)) { + prop.store(out, "messages"); + } catch (IOException e) { + log.error("Messages FileOutputStream IOException = {}, {}", e.getMessage(), e.getCause()); + } + + // files + files.add(propFile); + } + + String ftpEnabled = environment.getProperty("ftp.enabled"); + // files 있는 경우 ftp 서버에 올린다. + if ((StringUtils.hasLength(ftpEnabled) || "true".equals(ftpEnabled)) && !files.isEmpty()) { + storageUtils.storeFiles(files, "messages"); + } + + return messages.size(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/Resilience4JConfig.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/Resilience4JConfig.java new file mode 100644 index 0000000..8f27736 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/Resilience4JConfig.java @@ -0,0 +1,49 @@ +package org.egovframe.cloud.portalservice.config; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory; +import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder; +import org.springframework.cloud.client.circuitbreaker.Customizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +/** + * org.egovframe.cloud.portalservice.config.Resilience4JConfig + *

+ * Resilience4J Configuration + * 기본 설정값으로 운영되어도 무방하다. 이 클래스는 필수는 아니다. + * retry 기본값은 최대 3회이고, fallback 이 없는 경우에만 동작하므로 설정하지 않았다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/31 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/31    jaeyeolkim  최초 생성
+ * 
+ */ +@Configuration +public class Resilience4JConfig { + + @Bean + public Customizer resilience4JCircuitBreakerFactoryCustomizer() { + CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() + .failureRateThreshold(50) // Circuit 열지 말지 결정하는 실패 threshold 퍼센테이지 + .waitDurationInOpenState(Duration.ofSeconds(5)) // (half closed 전에) circuitBreaker가 open 되기 전에 기다리는 기간 + .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) // circuit breaker count 기반 처리 + .slidingWindowSize(10) // 통계 대상 건수 -> N건의 요청중.. + .build(); + + return circuitBreakerFactory -> circuitBreakerFactory.configureDefault( + id -> new Resilience4JConfigBuilder(id) + .circuitBreakerConfig(circuitBreakerConfig) + .build() + ); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/SecurityConfig.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/SecurityConfig.java new file mode 100644 index 0000000..8b23166 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/SecurityConfig.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.portalservice.config; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.servlet.config.AuthenticationFilter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; + +/** + * org.egovframe.cloud.portalservice.config.SecurityConfig + *

+ * Spring Security Config 클래스 + * AuthenticationFilter 를 추가하고 토큰으로 setAuthentication 인증처리를 한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@EnableWebSecurity // Spring Security 설정들을 활성화시켜 준다 +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Value("${token.secret}") + private String TOKEN_SECRET; + + /** + * 스프링 시큐리티 설정 + * + * @param http + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .headers().frameOptions().disable() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 사용하기 때문에 세션은 비활성화 + .and() + .addFilter(getAuthenticationFilter()); + } + + /** + * 토큰에 담긴 정보로 Authentication 정보를 설정하여 jpa audit 처리에 사용된다. + * 이 처리를 하지 않으면 AnonymousAuthenticationToken 으로 처리된다. + * + * @return + * @throws Exception + */ + private AuthenticationFilter getAuthenticationFilter() throws Exception { + return new AuthenticationFilter(authenticationManager(), TOKEN_SECRET); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/StorageConfig.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/StorageConfig.java new file mode 100644 index 0000000..7d5701f --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/config/StorageConfig.java @@ -0,0 +1,45 @@ +package org.egovframe.cloud.portalservice.config; + +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.util.MessageUtil; +import org.egovframe.cloud.portalservice.utils.FileStorageUtils; +import org.egovframe.cloud.portalservice.utils.FtpStorageUtils; +import org.egovframe.cloud.portalservice.utils.StorageUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +/** + * org.egovframe.cloud.portalservice.config.StorageConfig + *

+ * StorageConfig Config 클래스 + * ftp 서버 사용 여부에 따라 StorageUtils 에 주입하는 빈이 달라진다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/08    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@Configuration +public class StorageConfig { + + @Bean + public StorageUtils storageUtils(Environment environment, MessageUtil messageUtil) { + String ftpEnabled = environment.getProperty("ftp.enabled"); + if (StringUtils.hasLength(ftpEnabled) && "true".equals(ftpEnabled)) { + log.info("ftpEnabled: {} StorageUtils -> FtpStorageUtils", ftpEnabled); + return new FtpStorageUtils(environment, messageUtil); + } + log.info("ftpEnabled: {} StorageUtils -> FileStorageUtils", ftpEnabled); + return new FileStorageUtils(environment, messageUtil); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/Attachment.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/Attachment.java new file mode 100644 index 0000000..cb77129 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/Attachment.java @@ -0,0 +1,129 @@ +package org.egovframe.cloud.portalservice.domain.attachment; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.Column; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; + +/** + * org.egovframe.cloud.portalservice.domain.attachment.Attachment + *

+ * 첨부파일 엔티티 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ + +/** + * @TODO + * 관련 엔티티를 어떻게 처리하는지 에 따라 + * 컬럼명이 변경되거나 삭제될 수 있음. + * 아직 용어사전에 넣지 않았으므로 기능 완료되면 용어사전에도 fix된 컬럼명을 넣어야 함. + * 2021/07/26 shinmj!! + */ +@Getter +@NoArgsConstructor +@ToString +@Entity +public class Attachment extends BaseEntity { + + @EmbeddedId + private AttachmentId attachmentId; + + /** + * 복합키를 HTTP URI에 표현하기 힘드므로 대체키를 추가한다. + */ + @Column(name = "attachment_id", length = 50, nullable = false, unique = true) + private String uniqueId; + + @Column(nullable = false, length = 200) + private String physicalFileName; + + @Column(nullable = false, length = 200) + private String originalFileName; + + @Column(name = "attachment_size", length = 20) + private Long size; + + @Column(name = "file_type_value", length = 100) + private String fileType; + + @Column(name = "download_count", length = 15) + private Long downloadCnt; + + @Column(name = "delete_at", columnDefinition = "boolean default false") + private Boolean isDelete; + + @Column(length = 200) + private String entityName; + + @Column(length = 50) + private String entityId; + + @Builder + public Attachment(AttachmentId attachmentId, String uniqueId, + String physicalFileName, String originalFileName, + Long size, String fileType, + String entityName, String entityId) { + this.attachmentId = attachmentId; + this.uniqueId = uniqueId; + this.physicalFileName = physicalFileName; + this.originalFileName = originalFileName; + this.size = size; + this.fileType = fileType; + this.entityName = entityName; + this.entityId = entityId; + this.isDelete = false; + this.downloadCnt = 0L; + } + + /** + * 삭제 여부 토글 + * + * @param isDelete + * @return + */ + public Attachment updateIsDelete(Boolean isDelete) { + this.isDelete = isDelete; + return this; + } + + /** + * 첨부파일 다운로드 할 때 마다 Download 횟수 + 1 + * + * @return + */ + public Attachment updateDownloadCnt() { + this.downloadCnt = getDownloadCnt() == null ? 1 : getDownloadCnt() + 1; + return this; + } + + /** + * entity 정보 update + * + * @param entityName + * @param entityId + * @return + */ + public Attachment updateEntity(String entityName, String entityId) { + this.entityName = entityName; + this.entityId = entityId; + return this; + } + + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentId.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentId.java new file mode 100644 index 0000000..27d99b8 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentId.java @@ -0,0 +1,48 @@ +package org.egovframe.cloud.portalservice.domain.attachment; + +import lombok.Builder; +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.UUID; + +/** + * org.egovframe.cloud.portalservice.domain.attachment.AttachmentId + *

+ * 첨부파일 엔티티 복합키 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Data +@Embeddable +public class AttachmentId implements Serializable { + @Column(name = "attachment_code", length = 20) + private String code; + + @Column(name = "attachment_seq", length = 15) + private Long seq; + + @Builder + public AttachmentId (String code, Long seq) { + this.code = code; + this.seq = seq; + } + + public AttachmentId () { + this.code = UUID.randomUUID().toString(); + this.seq = 1L; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepository.java new file mode 100644 index 0000000..af8809d --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepository.java @@ -0,0 +1,27 @@ +package org.egovframe.cloud.portalservice.domain.attachment; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +/** + * org.egovframe.cloud.portalservice.domain.attachment.AttachmentRepository + *

+ * 첨부파일 repository interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +public interface AttachmentRepository extends JpaRepository, AttachmentRepositoryCustom { + Optional findAllByUniqueId(String uniqueId); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryCustom.java new file mode 100644 index 0000000..21223fe --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryCustom.java @@ -0,0 +1,31 @@ +package org.egovframe.cloud.portalservice.domain.attachment; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.attachment.AttachmentRepositoryCustom + *

+ * 첨부파일 querydsl interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +public interface AttachmentRepositoryCustom { + List findByCode(String attachmentCode); + AttachmentId getId(String attachmentCode); + Page search(RequestDto searchRequestDto, Pageable pageable); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryImpl.java new file mode 100644 index 0000000..4c1c2b8 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryImpl.java @@ -0,0 +1,130 @@ +package org.egovframe.cloud.portalservice.domain.attachment; + +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +import static com.querydsl.core.types.Projections.constructor; +import static org.egovframe.cloud.portalservice.domain.attachment.QAttachment.attachment; +import static org.springframework.util.StringUtils.hasLength; + +/** + * org.egovframe.cloud.portalservice.domain.attachment.AttachmentRepositoryImpl + *

+ * 첨부파일 querydsl 구현 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +public class AttachmentRepositoryImpl implements AttachmentRepositoryCustom{ + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory queryFactory; + + /** + * code로 첨부파일 검색 + * + * @param attachmentCode + * @return + */ + @Override + public List findByCode(String attachmentCode) { + return queryFactory.selectFrom(attachment) + .where(attachment.attachmentId.code.eq(attachmentCode), attachment.isDelete.eq(false)) + .orderBy(attachment.attachmentId.seq.asc()) + .fetch(); + } + + /** + * 첨부파일 복합키 seq 조회하여 생성 + * + * @param attachmentCode + * @return + */ + @Override + public AttachmentId getId(String attachmentCode) { + Long seq = queryFactory.select( + attachment.attachmentId.seq.max() + ) + .from(attachment) + .where(attachment.attachmentId.code.eq(attachmentCode)) + .fetchOne(); + + return AttachmentId.builder() + .code(attachmentCode) + .seq(seq+1L) + .build(); + + } + + /** + * 관리자 - 첨부파일 목록 조회 + * + * @param searchRequestDto + * @param pageable + * @return + */ + @Override + public Page search(RequestDto searchRequestDto, Pageable pageable) { + + QueryResults results = + queryFactory.select(constructor(AttachmentResponseDto.class, attachment)) + .from(attachment) + .where( + searchTextLike(searchRequestDto) + ) + .orderBy( + attachment.createdDate.desc(), + attachment.attachmentId.code.asc(), + attachment.attachmentId.seq.asc() + ) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetchResults(); + + return new PageImpl<>(results.getResults(), pageable, results.getTotal()); + } + + /** + * dynamic query binding + * + * @param requestDto + * @return + */ + private BooleanExpression searchTextLike(RequestDto requestDto) { + final String type = requestDto.getKeywordType(); + final String value = requestDto.getKeyword(); + if (!hasLength(type) || !hasLength(value)) { + return null; + } + + if ("id".equals(type)) { + return attachment.attachmentId.code.containsIgnoreCase(value); + } else if ("name".equals(type)) { + return attachment.originalFileName.containsIgnoreCase(value); + } + return null; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/Banner.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/Banner.java new file mode 100644 index 0000000..c15e547 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/Banner.java @@ -0,0 +1,164 @@ +package org.egovframe.cloud.portalservice.domain.banner; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import org.egovframe.cloud.portalservice.domain.menu.Site; +import org.egovframe.cloud.servlet.domain.BaseEntity; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import javax.persistence.*; + +/** + * org.egovframe.cloud.portalservice.domain.banner.Banner + *

+ * 배너 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@DynamicInsert +@DynamicUpdate +@Entity +public class Banner extends BaseEntity { + + /** + * 배너 번호 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer bannerNo; + + /** + * 배너 구분 코드 + */ + @Column(nullable = false, length = 20) + private String bannerTypeCode; + + /** + * 배너 제목 + */ + @Column(nullable = false, length = 100) + private String bannerTitle; + + /** + * 첨부파일 코드 + */ + @Column(nullable = false, length = 255) + private String attachmentCode; + + /** + * url 주소 + */ + @Column(length = 500) + private String urlAddr; + + /** + * 새 창 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Boolean newWindowAt; + + /** + * 배너 내용 + */ + @Column(length = 2000) + private String bannerContent; + + /** + * 정렬 순서 + */ + @Column(nullable = false, columnDefinition = "mediumint(5) default '99999'") + private Integer sortSeq; + + /** + * 사용 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '1'") + private Boolean useAt; + + /** + * 사이트 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_id") + private Site site; + + /** + * 빌더 패턴 클래스 생성자 + * + * @param bannerNo 배너 번호 + * @param bannerTypeCode 배너 구분 코드 + * @param bannerTitle 배너 제목 + * @param attachmentCode 첨부파일 코드 + * @param urlAddr url 주소 + * @param newWindowAt 새 창 여부 + * @param bannerContent 배너 내용 + * @param sortSeq 정렬 순서 + * @param useAt 사용 여부 + */ + @Builder + public Banner(Integer bannerNo, String bannerTypeCode, String bannerTitle, String attachmentCode, + String urlAddr, Boolean newWindowAt, String bannerContent, Integer sortSeq, Boolean useAt, Site site) { + this.bannerNo = bannerNo; + this.bannerTypeCode = bannerTypeCode; + this.bannerTitle = bannerTitle; + this.attachmentCode = attachmentCode; + this.urlAddr = urlAddr; + this.newWindowAt = newWindowAt; + this.bannerContent = bannerContent; + this.sortSeq = sortSeq; + this.useAt = useAt; + this.site = site; + } + + /** + * 배너 속성 값 수정 + * + * @param bannerTypeCode 배너 구분 코드 + * @param bannerTitle 배너 제목 + * @param attachmentCode 첨부파일 코드 + * @param urlAddr url 주소 + * @param bannerContent 배너 내용 + * @return Banner 배너 엔티티 + */ + public Banner update(String bannerTypeCode, String bannerTitle, String attachmentCode, + String urlAddr, Boolean newWindowAt, String bannerContent, Integer sortSeq, Site site) { + this.bannerTypeCode = bannerTypeCode; + this.bannerTitle = bannerTitle; + this.attachmentCode = attachmentCode; + this.urlAddr = urlAddr; + this.newWindowAt = newWindowAt; + this.bannerContent = bannerContent; + this.sortSeq = sortSeq; + this.site = site; + + return this; + } + + /** + * 배너 사용 여부 수정 + * + * @param useAt 사용 여부 + * @return Banner 배너 엔티티 + */ + public Banner updateUseAt(Boolean useAt) { + this.useAt = useAt; + + return this; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepository.java new file mode 100644 index 0000000..3085915 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepository.java @@ -0,0 +1,24 @@ +package org.egovframe.cloud.portalservice.domain.banner; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.portalservice.domain.banner.BannerRepository + *

+ * 배너 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +public interface BannerRepository extends JpaRepository, BannerRepositoryCustom { + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepositoryCustom.java new file mode 100644 index 0000000..41fd85a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepositoryCustom.java @@ -0,0 +1,75 @@ +package org.egovframe.cloud.portalservice.domain.banner; + +import java.util.List; +import java.util.Optional; + +import org.egovframe.cloud.portalservice.api.banner.dto.BannerImageResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerListResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerRequestDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * org.egovframe.cloud.portalservice.domain.banner.BannerRepositoryCustom + *

+ * 배너 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +public interface BannerRepositoryCustom { + + /** + * 배너 페이지 목록 조회 + * + * @param requestDto 배너 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 배너 목록 응답 DTO + */ + Page findPage(BannerRequestDto requestDto, Pageable pageable); + + /** + * 배너 목록 조회 + * + * @param bannerTypeCode 배너 유형 코드 + * @param bannerCount 배너 수 + * @param useAt 사용 여부 + * @return List 배너 이미지 응답 DTO List + */ + List findList(String bannerTypeCode, Integer bannerCount, Boolean useAt, Long siteId); + + /** + * 배너 다음 정렬 순서 조회 + * + * @return Integer 다음 정렬 순서 + */ + Integer findNextSortSeq(Long siteId); + + /** + * 배너 정렬 순서 수정 + * + * @param startSortSeq 시작 정렬 순서 + * @param endSortSeq 종료 정렬 순서 + * @param increaseSortSeq 증가 정렬 순서 + * @return Long 처리 건수 + */ + Long updateSortSeq(Integer startSortSeq, Integer endSortSeq, int increaseSortSeq, Long siteId); + + /** + * 정렬 순서로 배너 단건 조회 + * + * @param sortSeq 정렬 순서 + * @return Banner 배너 엔티티 + */ + Optional findBySortSeqAndSiteId(Integer sortSeq, Long siteId); + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepositoryImpl.java new file mode 100644 index 0000000..07ee127 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/banner/BannerRepositoryImpl.java @@ -0,0 +1,221 @@ +package org.egovframe.cloud.portalservice.domain.banner; + +import static com.querydsl.core.types.Projections.*; + +import java.util.List; +import java.util.Optional; + +import org.egovframe.cloud.portalservice.api.banner.dto.BannerImageResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerListResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerRequestDto; +import org.egovframe.cloud.portalservice.domain.attachment.QAttachment; +import org.egovframe.cloud.portalservice.domain.code.QCode; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.domain.banner.BannerRepositoryImpl + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class BannerRepositoryImpl implements BannerRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 배너 페이지 목록 조회 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 배너 목록 응답 DTO + */ + public Page findPage(BannerRequestDto requestDto, Pageable pageable) { + QueryResults result = jpaQueryFactory + .select(fields(BannerListResponseDto.class, + QBanner.banner.bannerNo, + QBanner.banner.bannerTypeCode, + Expressions.as(QCode.code.codeName, "bannerTypeCodeName"), + QBanner.banner.bannerTitle, + QBanner.banner.useAt, + QBanner.banner.createdDate, + QBanner.banner.site.name.as("siteName") + )) + .from(QBanner.banner) + .leftJoin(QCode.code).on(QBanner.banner.bannerTypeCode.eq(QCode.code.codeId).and(QCode.code.parentCodeId.eq("banner_type_code"))) + .fetchJoin() + .where(getBooleanExpressionKeyword(requestDto), getEqualsBooleanExpression("siteId", requestDto.getSiteId())) + .orderBy(QBanner.banner.site.sortSeq.asc(), QBanner.banner.sortSeq.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) //페이징 + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 배너 목록 조회 + * + * @param bannerTypeCode 배너 유형 코드 + * @param bannerCount 배너 수 + * @param useAt 사용 여부 + * @return List 배너 이미지 응답 DTO List + */ + public List findList(String bannerTypeCode, Integer bannerCount, Boolean useAt, Long siteId) { + JPQLQuery query = jpaQueryFactory + .select(constructor(BannerImageResponseDto.class, + QBanner.banner.bannerNo, + QBanner.banner.bannerTypeCode, + QBanner.banner.bannerTitle, + QBanner.banner.attachmentCode, + JPAExpressions.select(QAttachment.attachment.uniqueId) + .from(QAttachment.attachment) + .where(QAttachment.attachment.attachmentId.code.eq(QBanner.banner.attachmentCode) + .and(QAttachment.attachment.isDelete.eq(Boolean.FALSE)) + .and(QAttachment.attachment.attachmentId.seq.eq( + JPAExpressions.select(QAttachment.attachment.attachmentId.seq.max()) + .from(QAttachment.attachment) + .where(QAttachment.attachment.attachmentId.code.eq(QBanner.banner.attachmentCode) + .and(QAttachment.attachment.isDelete.eq(Boolean.FALSE)))))), + QBanner.banner.urlAddr, + QBanner.banner.newWindowAt, + QBanner.banner.bannerContent + )) + .from(QBanner.banner) + .where(QBanner.banner.site.id.eq(siteId), + getEqualsBooleanExpression("bannerTypeCode", bannerTypeCode), + getEqualsBooleanExpression("useAt", useAt)) + .orderBy(QBanner.banner.sortSeq.asc()); + + if (bannerCount != null && bannerCount > 0) { + query.limit(bannerCount); + } + + return query.fetch(); + } + + /** + * 배너 다음 정렬 순서 조회 + * + * @param siteId siteId + * @return Integer 다음 정렬 순서 + */ + public Integer findNextSortSeq(Long siteId) { + return jpaQueryFactory + .select(QBanner.banner.sortSeq.max().add(1).coalesce(1)) + .from(QBanner.banner) + .where(QBanner.banner.site.id.eq(siteId)) + .fetchOne(); + } + + /** + * 배너 정렬 순서 수정 + * + * @param startSortSeq 시작 정렬 순서 + * @param endSortSeq 종료 정렬 순서 + * @param increaseSortSeq 증가 정렬 순서 + * @param siteId siteId + * @return Long 수정 건수 + */ + public Long updateSortSeq(Integer startSortSeq, Integer endSortSeq, int increaseSortSeq, Long siteId) { + return jpaQueryFactory.update(QBanner.banner) + .set(QBanner.banner.sortSeq, QBanner.banner.sortSeq.add(increaseSortSeq)) + .where(QBanner.banner.site.id.eq(siteId), + isGoeSortSeq(startSortSeq), + isLoeSortSeq(endSortSeq)) + .execute(); + } + + @Override + public Optional findBySortSeqAndSiteId(Integer sortSeq, Long siteId) { + return Optional.ofNullable(jpaQueryFactory.selectFrom(QBanner.banner) + .where(QBanner.banner.site.id.eq(siteId), QBanner.banner.sortSeq.eq(sortSeq)).fetchOne()); + + } + + /** + * 엔티티 속성별 동적 검색 표현식 리턴 + * + * @param attributeName 속성 명 + * @param attributeValue 속성 값 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getEqualsBooleanExpression(String attributeName, Object attributeValue) { + if (attributeValue == null || "".equals(attributeValue.toString())) return null; + + switch (attributeName) { + case "bannerTypeCode": // 배너 유형 코드 + return QBanner.banner.bannerTypeCode.eq((String) attributeValue); + case "useAt": // 사용 여부 + return QBanner.banner.useAt.eq((Boolean) attributeValue); + case "siteId": + return QBanner.banner.site.id.eq((Long) attributeValue); + default: + return null; + } + } + + /** + * 요청 DTO로 동적 검색 표현식 리턴 + * + * @param requestDto 요청 DTO + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpressionKeyword(BannerRequestDto requestDto) { + if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null; + + switch (requestDto.getKeywordType()) { + case "bannerTitle": // 배너 제목 + return QBanner.banner.bannerTitle.containsIgnoreCase(requestDto.getKeyword()); + case "bannerContent": // 배너 내용 + return QBanner.banner.bannerContent.containsIgnoreCase(requestDto.getKeyword()); + default: + return null; + } + } + + /** + * 정렬 순서 이하 검색 표현식 + * + * @param sortSeq 정렬 순서 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression isLoeSortSeq(Integer sortSeq) { + return sortSeq == null ? null : QBanner.banner.sortSeq.loe(sortSeq); + } + + /** + * 정렬 순서 이상 검색 표현식 + * + * @param sortSeq 정렬 순서 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression isGoeSortSeq(Integer sortSeq) { + return sortSeq == null ? null : QBanner.banner.sortSeq.goe(sortSeq); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/board/Board.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/board/Board.java new file mode 100644 index 0000000..335671c --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/board/Board.java @@ -0,0 +1,51 @@ +package org.egovframe.cloud.portalservice.domain.board; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.*; + +/** + * org.egovframe.cloud.boardservice.domain.board.Board + *

+ * 게시판 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/26 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/26    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +public class Board extends BaseEntity { + + /** + * 게시판 번호 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer boardNo; + + /** + * 게시판 제목 + */ + @Column(nullable = false, length = 100) + private String boardName; + + /** + * 스킨 유형 코드 + */ + @Column(nullable = false, length = 20) + private String skinTypeCode; + + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/Code.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/Code.java new file mode 100644 index 0000000..79c67a0 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/Code.java @@ -0,0 +1,115 @@ +package org.egovframe.cloud.portalservice.domain.code; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * org.egovframe.cloud.portalservice.domain.code.Code + *

+ * 공통코드 엔티티 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +public class Code extends BaseEntity { + + @Id + @Column(length = 20) + private String codeId; // 코드ID + + @Column(length = 20) + private String parentCodeId; // 상위 코드ID + + @Column(nullable = false, length = 500) + private String codeName; // 코드 명 + + @Column(length = 500) + private String codeDescription; // 코드 설명 + + @Column(columnDefinition = "SMALLINT(3)") + private Integer sortSeq; // 정렬 순서 + + @Column(nullable = false) + private Boolean useAt; // 사용 여부 + + @Column(nullable = false, name = "readonly_at") + private Boolean readonly; // 수정하면 안되는 읽기전용 공통코드 + + @Builder + public Code(String codeId, String parentCodeId, String codeName, String codeDescription, Integer sortSeq, Boolean useAt, Boolean readonly) { + this.codeId = codeId; + this.parentCodeId = parentCodeId; + this.codeName = codeName; + this.codeDescription = codeDescription; + this.sortSeq = sortSeq; + this.useAt = useAt; + this.readonly = readonly; + } + + /** + * 공통코드 정보를 수정한다. + * + * @param codeName + * @param codeDescription + * @param sortSeq + * @param useAt + * @return + */ + public Code update(String codeName, String codeDescription, Integer sortSeq, Boolean useAt) { + this.codeName = codeName; + this.codeDescription = codeDescription; + this.sortSeq = sortSeq; + this.useAt = useAt; + + return this; + } + + /** + * 상세공통코드 정보를 수정한다. + * + * @param codeName + * @param codeDescription + * @param sortSeq + * @param useAt + * @return + */ + public Code updateDetail(String parentCodeId, String codeName, String codeDescription, Integer sortSeq, Boolean useAt) { + this.parentCodeId = parentCodeId; + this.codeName = codeName; + this.codeDescription = codeDescription; + this.sortSeq = sortSeq; + this.useAt = useAt; + + return this; + } + + /** + * 공통코드 사용여부를 수정한다. + * + * @param useAt + * @return + */ + public Code updateUseAt(boolean useAt) { + this.useAt = useAt; + + return this; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepository.java new file mode 100644 index 0000000..3a43c00 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepository.java @@ -0,0 +1,27 @@ +package org.egovframe.cloud.portalservice.domain.code; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +/** + * org.egovframe.cloud.portalservice.domain.code.CodeRepository + *

+ * 공통코드 엔티티를 위한 Repository + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +public interface CodeRepository extends JpaRepository, CodeRepositoryCustom { + Optional findByCodeId(String codeId); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryCustom.java new file mode 100644 index 0000000..1eea752 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryCustom.java @@ -0,0 +1,86 @@ +package org.egovframe.cloud.portalservice.domain.code; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.code.CodeRepositoryCustom + *

+ * 공통코드 Querydsl interface + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +public interface CodeRepositoryCustom { + + /** + * 공통코드 목록 + * + * @param requestDto + * @param pageable + * @return + */ + Page findAllByKeyword(RequestDto requestDto, Pageable pageable); + + /** + * 공통코드 상세 목록 + * + * @param requestDto + * @param pageable + * @return + */ + Page findAllDetailByKeyword(CodeDetailRequestDto requestDto, Pageable pageable); + + /** + * 공통코드 목록 - parentCodeId 가 없는 상위공통코드 + * + * @return + */ + List findAllParent(); + + /** + * 공통코드 상세 목록 - parentCodeId 에 해당하는 사용중인 공통코드 목록 + * + * @param parentCodeId + * @return + */ + List findDetailsByParentCodeIdUseAt(String parentCodeId); + + /** + * 공통코드 상세 목록 - parentCodeId 에 해당하는 사용중인 공통코드 목록 + * 사용여부가 false 로 변경된 경우에도 인자로 받은 공통코드를 목록에 포함되도록 한다 + * + * @param parentCodeId + * @param codeId + * @return + */ + List findDetailsUnionCodeIdByParentCodeId(String parentCodeId, String codeId); + + /** + * 부모 공통 코드 단건 조회 + * + * @param codeId + * @return + */ + CodeResponseDto findParentByCodeId(String codeId); + + /** + * 공통코드 parentCodeId 에 해당코드가 존재하는지 여부를 알기 위해 건수를 카운트한다 + * @param codeId + * @return + */ + long countByParentCodeId(String codeId); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryImpl.java new file mode 100644 index 0000000..0c0599e --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryImpl.java @@ -0,0 +1,274 @@ +package org.egovframe.cloud.portalservice.domain.code; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; + +import javax.persistence.EntityManager; +import java.util.List; + +import static com.querydsl.core.types.Projections.fields; +import static org.egovframe.cloud.portalservice.domain.code.QCode.code; +import static org.springframework.util.StringUtils.hasLength; + +/** + * org.egovframe.cloud.portalservice.domain.code.CodeRepositoryImpl + *

+ * 공통코드 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class CodeRepositoryImpl implements CodeRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + /** + * 공통코드 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Override + public Page findAllByKeyword(RequestDto requestDto, Pageable pageable) { + QCode childCode = new QCode("childCode"); + List content = + queryFactory + .select(fields(CodeListResponseDto.class, + code.codeId, + code.codeName, + code.codeDescription, + code.useAt, + code.readonly, + childCode.codeId.count().as("codeDetailCount") + )) + .from(code) + .leftJoin(childCode).on(code.parentCodeId.eq(childCode.codeId)) + .where( + code.parentCodeId.isNull(), + keyword(requestDto.getKeywordType(), requestDto.getKeyword()) + ) + .groupBy(code.codeId, + code.codeName, + code.codeDescription, + code.useAt, + code.readonly) + .orderBy(code.sortSeq.asc(), code.createdDate.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery query = queryFactory + .selectFrom(code) + .where( + code.parentCodeId.isNull(), + keyword(requestDto.getKeywordType(), requestDto.getKeyword()) + ); + + return PageableExecutionUtils.getPage(content, pageable, query::fetchCount); + } + + /** + * 공통코드 상세 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Override + public Page findAllDetailByKeyword(CodeDetailRequestDto requestDto, Pageable pageable) { + List content = + queryFactory + .select(fields(CodeDetailListResponseDto.class, + code.parentCodeId, + code.codeId, + code.codeName, + code.sortSeq, + code.useAt, + code.readonly + )) + .from(code) + .where( + code.parentCodeId.isNotNull(), + parentCodeIdEq(requestDto.getParentCodeId()), + keyword(requestDto.getKeywordType(), requestDto.getKeyword()) + ) + .groupBy(code.codeId, + code.codeName, + code.sortSeq, + code.useAt, + code.readonly) + .orderBy(code.parentCodeId.asc(), code.sortSeq.asc(), code.createdDate.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery query = queryFactory + .selectFrom(code) + .where( + code.parentCodeId.isNotNull(), + parentCodeIdEq(requestDto.getParentCodeId()), + keyword(requestDto.getKeywordType(), requestDto.getKeyword()) + ); + + return PageableExecutionUtils.getPage(content, pageable, query::fetchCount); + } + + /** + * 공통코드 상세 목록 - parentCodeId 에 해당하는 사용중인 공통코드 목록 + * + * @param parentCodeId + * @return + */ + @Override + public List findDetailsByParentCodeIdUseAt(String parentCodeId) { + return queryFactory + .select(fields(CodeDetailResponseDto.class, + code.parentCodeId, + code.codeId, + code.codeName, + code.sortSeq, + code.useAt, + code.readonly + )) + .from(code) + .where( + code.parentCodeId.eq(parentCodeId) + .and(code.useAt.eq(true)) + ) + .orderBy(code.sortSeq.asc()) + .fetch(); + } + + /** + * 공통코드 상세 목록 - parentCodeId 에 해당하는 사용중인 공통코드 목록 + * 사용여부가 false 로 변경된 경우에도 인자로 받은 공통코드를 목록에 포함되도록 한다 + * + * @param parentCodeId + * @param codeId + * @return + */ + @Override + public List findDetailsUnionCodeIdByParentCodeId(String parentCodeId, String codeId) { + return queryFactory + .select(fields(CodeDetailResponseDto.class, + code.parentCodeId, + code.codeId, + code.codeName, + code.sortSeq, + code.useAt, + code.readonly + )) + .from(code) + .where( + code.parentCodeId.eq(parentCodeId) + .and(code.useAt.eq(true).or(code.codeId.eq(codeId))) + ) + .orderBy(code.sortSeq.asc()) + .fetch(); + } + + /** + * 공통코드 목록 - parentCodeId 가 없는 상위공통코드 + * + * @return + */ + @Override + public List findAllParent() { + return queryFactory + .select(fields(CodeResponseDto.class, + code.codeId, + code.codeName, + code.sortSeq, + code.useAt + )) + .from(code) + .where(code.parentCodeId.isNull()) + .orderBy(code.sortSeq.asc()) + .fetch(); + } + + /** + * 부모 공통코드 단건 조회 + * + * @param codeId + * @return + */ + @Override + public CodeResponseDto findParentByCodeId(String codeId) { + QCode parent = new QCode("parent"); + return queryFactory + .select(fields(CodeResponseDto.class, + parent.codeId, + parent.codeName, + parent.useAt + )) + .from(code) + .join(parent) + .on(code.parentCodeId.eq(parent.codeId)) + .where(code.codeId.eq(codeId)) + .fetchOne(); + } + + /** + * 공통코드 parentCodeId 에 해당코드가 존재하는지 여부를 알기 위해 건수를 카운트한다 + * + * @param codeId + * @return + */ + @Override + public long countByParentCodeId(String codeId) { + return queryFactory + .selectFrom(code) + .where(code.parentCodeId.eq(codeId)) + .fetchCount(); + } + + /** + * 공통코드 조회조건 + * + * @param keywordType + * @param keyword + * @return + */ + private BooleanExpression keyword(String keywordType, String keyword) { + if (!hasLength(keywordType) || !hasLength(keyword)) { + return null; + } + + if ("codeId".equals(keywordType)) { + return code.codeId.containsIgnoreCase(keyword); + } else if ("codeName".equals(keywordType)) { + return code.codeName.containsIgnoreCase(keyword); + } + return null; + } + + /** + * 공통코드 상세 목록 추가 조회조건 - 상위코드 + * + * @param parentCodeId + * @return + */ + private BooleanExpression parentCodeIdEq(String parentCodeId) { + return hasLength(parentCodeId) ? code.parentCodeId.eq(parentCodeId) : null; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/Content.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/Content.java new file mode 100644 index 0000000..4e8fffc --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/Content.java @@ -0,0 +1,93 @@ +package org.egovframe.cloud.portalservice.domain.content; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import javax.persistence.*; + +/** + * org.egovframe.cloud.portalservice.domain.content.Content + *

+ * 컨텐츠 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +@DynamicInsert +@DynamicUpdate +public class Content extends BaseEntity { + + /** + * 컨텐츠 번호 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer contentNo; + + /** + * 컨텐츠 명 + */ + @Column(nullable = false, length = 100) + private String contentName; + + /** + * 컨텐츠 비고 + */ + @Column(length = 200) + private String contentRemark; + + /** + * 컨텐츠 값 + */ + @Column(nullable = false, columnDefinition = "longtext") + private String contentValue; + + /** + * 빌더 패턴 클래스 생성자 + * + * @param contentNo 컨텐츠 번호 + * @param contentName 컨텐츠 명 + * @param contentRemark 컨텐츠 비고 + * @param contentValue 컨텐츠 값 + */ + @Builder + public Content(Integer contentNo, String contentName, String contentRemark, String contentValue) { + this.contentNo = contentNo; + this.contentName = contentName; + this.contentRemark = contentRemark; + this.contentValue = contentValue; + } + + /** + * 컨텐츠 속성 값 수정 + * + * @param contentName 컨텐츠 명 + * @param contentRemark 컨텐츠 비고 + * @param contentValue 컨텐츠 값 + * @return Content 컨텐츠 엔티티 + */ + public Content update(String contentName, String contentRemark, String contentValue) { + this.contentName = contentName; + this.contentRemark = contentRemark; + this.contentValue = contentValue; + + return this; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepository.java new file mode 100644 index 0000000..a635a55 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepository.java @@ -0,0 +1,24 @@ +package org.egovframe.cloud.portalservice.domain.content; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.portalservice.domain.content.ContentRepository + *

+ * 컨텐츠 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +public interface ContentRepository extends JpaRepository, ContentRepositoryCustom { + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepositoryCustom.java new file mode 100644 index 0000000..a47ef13 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepositoryCustom.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.portalservice.domain.content; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * org.egovframe.cloud.portalservice.domain.content.ContentRepositoryCustom + *

+ * 컨텐츠 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +public interface ContentRepositoryCustom { + + /** + * 컨텐츠 페이지 목록 조회 + * + * @param requestDto 컨텐츠 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 컨텐츠 목록 응답 DTO + */ + Page findPage(RequestDto requestDto, Pageable pageable); + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepositoryImpl.java new file mode 100644 index 0000000..5a2ed9c --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/content/ContentRepositoryImpl.java @@ -0,0 +1,110 @@ +package org.egovframe.cloud.portalservice.domain.content; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentListResponseDto; +import org.egovframe.cloud.portalservice.api.content.dto.QContentListResponseDto; +import org.egovframe.cloud.portalservice.domain.user.QUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.google.common.base.CaseFormat; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.domain.content.ContentRepositoryImpl + *

+ * 컨텐츠 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class ContentRepositoryImpl implements ContentRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 컨텐츠 페이지 목록 조회 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 컨텐츠 목록 응답 DTO + */ + @Override + public Page findPage(RequestDto requestDto, Pageable pageable) { + JPQLQuery query = jpaQueryFactory + .select(new QContentListResponseDto( + QContent.content.contentNo, + QContent.content.contentName, + Expressions.as(QUser.user.userName, "lastModifiedBy"), + QContent.content.modifiedDate + )) + .from(QContent.content) + .leftJoin(QUser.user).on(QContent.content.lastModifiedBy.eq(QUser.user.userId)) + .fetchJoin() + .where(getBooleanExpressionKeyword(requestDto)); + + //정렬 + pageable.getSort().stream().forEach(sort -> { + Order order = sort.isAscending() ? Order.ASC : Order.DESC; + String property = sort.getProperty(); + + Path target = Expressions.path(Object.class, QContent.content, CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, property)); + @SuppressWarnings({ "rawtypes", "unchecked" }) + OrderSpecifier orderSpecifier = new OrderSpecifier(order, target); + query.orderBy(orderSpecifier); + }); + + QueryResults result = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) //페이징 + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 요청 DTO로 동적 검색 표현식 리턴 + * + * @param requestDto 요청 DTO + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpressionKeyword(RequestDto requestDto) { + if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null; + + switch (requestDto.getKeywordType()) { + case "contentName": // 컨텐츠 명 + return QContent.content.contentName.containsIgnoreCase(requestDto.getKeyword()); + case "contentRemark": // 컨텐츠 비고 + return QContent.content.contentRemark.containsIgnoreCase(requestDto.getKeyword()); + case "contentValue": // 컨텐츠 값 + return QContent.content.contentValue.containsIgnoreCase(requestDto.getKeyword()); + default: + return null; + } + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/Menu.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/Menu.java new file mode 100644 index 0000000..bfd9fdb --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/Menu.java @@ -0,0 +1,203 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuUpdateRequestDto; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.menu.Menu + *

+ * 메뉴관리 > Menu 도메인 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +@ToString +public class Menu extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "menu_id") + private Long id; // id + + @Column( length = 100) + private String menuKorName; // 메뉴명 kor + + @Column( length = 100) + private String menuEngName; // 메뉴명 eng + + @Column(name = "type_id", length = 20) + private String menuType; // 메뉴 유형 공통 코드 id + + @Column + private Integer connectId; // 연결된 컨텐츠 or 게시판 id + + @Column(length = 200) + private String urlPath; // 링크 url + + @Column(name = "use_at", columnDefinition = "boolean default true") + private Boolean isUse; // 사용 여부 + + @Column(name = "show_at", columnDefinition = "boolean default true") + private Boolean isShow; // 출력 여부 + + @Column(name = "blank_at", columnDefinition = "boolean default false") + private Boolean isBlank; // 연결 형태 (새창/현재창) + + @Column(name = "sub_name", length = 200) + private String subName; // 메뉴 서브명 + + @Column(name = "menu_description", length = 500) + private String description; // 메뉴 설명 + + @Column(columnDefinition = "SMALLINT(3)") + private Integer sortSeq; // 정렬 순서 + + @Column(name = "icon_name", length = 100) + private String icon; // 아이콘 class + + @Column(name = "level_no", length = 15) + private Integer level; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_menu_id") + private Menu parent; + + @ToString.Exclude + @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) + private List children = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "site_id") + private Site site; + + @ToString.Exclude + @OneToMany(fetch = FetchType.LAZY, mappedBy = "menu", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) + private List menuRoles = new ArrayList<>(); + + + @Builder + public Menu(String menuKorName, Menu parent, Integer sortSeq, Site site, Integer level, Boolean isUse, Boolean isShow) { + this.menuKorName = menuKorName; + this.parent = parent; + this.sortSeq = sortSeq; + this.site = site; + this.level = level; + this.isShow = isShow; + this.isUse = isUse; + } + + /** + * 메뉴명 변경 + * + * @param menuKorName + * @return + */ + public Menu updateName(String menuKorName) { + this.menuKorName = menuKorName; + return this; + } + + /** + * 드래그앤드랍으로 변경 시 상위메뉴, 정렬순서 수정 + * + * @param parent + * @param sortSeq + * @return + */ + public Menu updateDnD(Menu parent, Integer sortSeq, Integer level) { + this.sortSeq = sortSeq; + this.level = level; + if (parent == null) { + Menu oldParent = this.getParent(); + + if (oldParent == null) { + return this; + } + + Menu old = oldParent.getChildren().stream().filter(item -> item.getId().equals(this.id)).findAny().orElse(null); + if (old != null) { + oldParent.getChildren().remove(old); + } + this.parent = null; + + return this; + } + + if (parent.equals(this.parent)) { + return this; + } + + this.parent = parent; + parent.getChildren().add(this); + + return this; + } + + + /** + * 메뉴 기본 설정 저장 + * + * @param updateRequestDto + * @return + */ + public Menu updateDetail(MenuUpdateRequestDto updateRequestDto) { + this.menuKorName = updateRequestDto.getMenuKorName(); + this.menuEngName = updateRequestDto.getMenuEngName(); + this.menuType = updateRequestDto.getMenuType(); + this.connectId = updateRequestDto.getConnectId(); + this.urlPath = updateRequestDto.getUrlPath(); + this.isUse = updateRequestDto.getIsUse(); + this.isShow = updateRequestDto.getIsShow(); + this.isBlank = updateRequestDto.getIsBlank(); + this.subName = updateRequestDto.getSubName(); + this.description = updateRequestDto.getDescription(); + this.icon = updateRequestDto.getIcon(); + + return this; + } + + /** + * 부모 메뉴 설정 및 양방향 관계 처리 + * + * @param parent + */ + public void setParentMenu(Menu parent) { + this.parent = parent; + parent.getChildren().add(this); + } + + /** + * 연관관계 데이터(권한별 메뉴) 조회 + * + * @param roleId + * @return + */ + public MenuRole getMenuRole(String roleId) { + return this.getMenuRoles() + .stream() + .filter(menuRole -> + menuRole.getRoleId().equals(roleId)) + .findAny().orElse(null); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepository.java new file mode 100644 index 0000000..61dd86a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepository.java @@ -0,0 +1,23 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.portalservice.domain.menu.MenuRepository + *

+ * 메뉴관리 > Menu 엔티티를 위한 Repository + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +public interface MenuRepository extends JpaRepository, MenuRepositoryCustom { +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryCustom.java new file mode 100644 index 0000000..81854d2 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryCustom.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import org.egovframe.cloud.portalservice.api.menu.dto.MenuResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuTreeResponseDto; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.menu.MenuRepositoryCustom + *

+ * 메뉴관리 > Menu querydsl interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +public interface MenuRepositoryCustom { + List findTreeBySiteId(Long siteId); + MenuResponseDto findByIdWithConnectName(Long menuId); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryImpl.java new file mode 100644 index 0000000..1e5072e --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryImpl.java @@ -0,0 +1,108 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuTreeResponseDto; +import org.egovframe.cloud.portalservice.domain.board.QBoard; +import org.egovframe.cloud.portalservice.domain.content.QContent; +import org.egovframe.cloud.portalservice.domain.user.QUser; + +import java.util.List; + +import static com.querydsl.core.types.Projections.constructor; +import static com.querydsl.core.types.Projections.fields; +import static org.egovframe.cloud.portalservice.domain.board.QBoard.board; +import static org.egovframe.cloud.portalservice.domain.content.QContent.content; +import static org.egovframe.cloud.portalservice.domain.menu.QMenu.menu; +import static org.egovframe.cloud.portalservice.domain.message.QMessage.message; + +/** + * org.egovframe.cloud.portalservice.domain.menu.MenuRepositoryImpl + *

+ * 메뉴관리 > Menu querydsl 구현체 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class MenuRepositoryImpl implements MenuRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + /** + * 메뉴관리 tree 조회 + * + * @param siteId + * @return + */ + @Override + public List findTreeBySiteId(Long siteId) { + return jpaQueryFactory.select( + constructor(MenuTreeResponseDto.class, menu)) + .from(menu) + .where(menu.site.id.eq(siteId), menu.parent.isNull()) + .orderBy(menu.sortSeq.asc()) + .fetch(); + } + + /** + * 메뉴 상세 정보 조회 + * + * @param menuId + * @return + */ + @Override + public MenuResponseDto findByIdWithConnectName(Long menuId) { + + return jpaQueryFactory.select( + fields(MenuResponseDto.class, + menu.id.as("menuId"), + menu.menuKorName, + menu.menuEngName, + menu.menuType, + menu.connectId, + new CaseBuilder() + .when( + menu.menuType.eq("contents") + ).then( + JPAExpressions.select(content.contentName) + .from(content) + .where(content.contentNo.eq(menu.connectId))) + .when( + menu.menuType.eq("board") + ).then( + JPAExpressions.select(board.boardName) + .from(board) + .where(board.boardNo.eq(menu.connectId))) + .otherwise("").as("connectName"), + menu.urlPath, + menu.isUse, + menu.isShow, + menu.isBlank, + menu.subName, + menu.description, + menu.icon + ) + ) + .from(menu) + .where(menu.id.eq(menuId)) + .fetchOne(); + + } + + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRole.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRole.java new file mode 100644 index 0000000..323256b --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRole.java @@ -0,0 +1,64 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.*; + +/** + * org.egovframe.cloud.portalservice.domain.menu.MenuRole + *

+ * 메뉴관리 > 권한별 메뉴 도메인 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/12    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@Entity +public class MenuRole extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "menu_role_id") + private Long id; // id + + @Column(nullable = false, length = 20) + private String roleId; //role id + + @ToString.Exclude + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "menu_id") + private Menu menu; //menu + + @Builder + public MenuRole(Long id, String roleId, Menu menu) { + this.id = id; + this.roleId = roleId; + this.menu = menu; + } + + /** + * 연관관계 설정 + * + * @param menu + */ + public void setMenu(Menu menu) { + this.menu = menu; + menu.getMenuRoles().add(this); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepository.java new file mode 100644 index 0000000..4940b4c --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepository.java @@ -0,0 +1,23 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.portalservice.domain.menu.MenuRoleRepository + *

+ * 메뉴관리 > 권한별 메뉴 Repository interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/12    shinmj  최초 생성
+ * 
+ */ +public interface MenuRoleRepository extends JpaRepository, MenuRoleRepositoryCustom { +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryCustom.java new file mode 100644 index 0000000..8dd7498 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryCustom.java @@ -0,0 +1,29 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuSideResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuTreeResponseDto; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.menu.MenuRoleRepositoryCustom + *

+ * 메뉴관리 > 권한별 메뉴 querydsl interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/12    shinmj  최초 생성
+ * 
+ */ +public interface MenuRoleRepositoryCustom { + List findTree(String roleId, Long siteId); + List findMenu(String roleId, Long siteId); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryImpl.java new file mode 100644 index 0000000..ebc428a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryImpl.java @@ -0,0 +1,67 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuSideResponseDto; + +import java.util.List; + +import static org.egovframe.cloud.portalservice.domain.menu.QMenu.menu; + +/** + * org.egovframe.cloud.portalservice.domain.menu.MenuRoleRepositoryImpl + *

+ * 메뉴관리 > 권한별 메뉴 querydsl 구현체 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/12    shinmj  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class MenuRoleRepositoryImpl implements MenuRoleRepositoryCustom{ + + private final JPAQueryFactory jpaQueryFactory; + + /** + * 권한별 메뉴 트리 조회 + * + * @param roleId + * @param siteId + * @return + */ + @Override + public List findTree(String roleId, Long siteId) { + return jpaQueryFactory.select( + Projections.constructor(MenuRoleResponseDto.class, menu, Expressions.constant(roleId))) + .from(menu) + .where(menu.site.id.eq(siteId), menu.parent.isNull()) + .orderBy(menu.sortSeq.asc()) + .fetch(); + } + + @Override + public List findMenu(String roleId, Long siteId) { + return jpaQueryFactory.select( + Projections.constructor(MenuSideResponseDto.class, menu, Expressions.constant(roleId))) + .from(menu) + .where(menu.site.id.eq(siteId), + menu.parent.isNull(), + menu.menuRoles.any().roleId.eq(roleId), + menu.isUse.isTrue()) + .orderBy(menu.sortSeq.asc()) + + .fetch(); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/Site.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/Site.java new file mode 100644 index 0000000..6423523 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/Site.java @@ -0,0 +1,54 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.*; + +/** + * org.egovframe.cloud.portalservice.domain.menu.Site + *

+ * 메뉴관리 > 사이트 도메인 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@Entity +public class Site extends BaseEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "site_id") + private Long id; + + @Column(name = "site_name", length = 50) + private String name; + + @Column(name = "use_at", columnDefinition = "boolean default true") + private Boolean isUse; + + @Column(columnDefinition = "SMALLINT(3)") + private Integer sortSeq; + + @Builder + public Site(String name, Boolean isUse, Integer sortSeq) { + this.name = name; + this.isUse = isUse; + this.sortSeq = sortSeq; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/SiteRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/SiteRepository.java new file mode 100644 index 0000000..0975549 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/menu/SiteRepository.java @@ -0,0 +1,27 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import org.egovframe.cloud.portalservice.api.menu.dto.SiteResponseDto; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.menu.SiteRepository + *

+ * 메뉴관리 > Site 도메인 Repository + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +public interface SiteRepository extends JpaRepository { + List findAllByIsUseTrueOrderBySortSeq(); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/Message.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/Message.java new file mode 100644 index 0000000..0298e97 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/Message.java @@ -0,0 +1,54 @@ +package org.egovframe.cloud.portalservice.domain.message; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * org.egovframe.cloud.portalservice.domain.message.Message + *

+ * 메시지 엔티티 + * Spring MessageSouce 데이터 관리 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +public class Message extends BaseEntity { + + @Id + private String messageId; + + @Column(nullable = false, length = 2000) + private String messageKoName; // 한글명 + + @Column(length = 2000) + private String messageEnName; // 영문명 + + @Column(length = 500) + private String messageDescription; // 메시지 설명 + + @Builder + public Message(String messageId, String messageKoName, String messageEnName, String messageDescription) { + this.messageId = messageId; + this.messageKoName = messageKoName; + this.messageEnName = messageEnName; + this.messageDescription = messageDescription; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepository.java new file mode 100644 index 0000000..d66db3f --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepository.java @@ -0,0 +1,23 @@ +package org.egovframe.cloud.portalservice.domain.message; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.portalservice.domain.message.CodeRepositoryCustom + *

+ * Message 엔티티를 위한 Repository + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jaeyeolkim  최초 생성
+ * 
+ */ +public interface MessageRepository extends JpaRepository, MessageRepositoryCustom { +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryCustom.java new file mode 100644 index 0000000..082d87f --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryCustom.java @@ -0,0 +1,29 @@ +package org.egovframe.cloud.portalservice.domain.message; + +import com.querydsl.core.Tuple; +import org.egovframe.cloud.portalservice.api.message.dto.MessageListResponseDto; + +import java.util.List; +import java.util.Map; + +/** + * org.egovframe.cloud.portalservice.domain.message.CodeRepositoryCustom + *

+ * Message Querydsl interface + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jaeyeolkim  최초 생성
+ * 
+ */ +public interface MessageRepositoryCustom { + List findAllMessages(String lang); + Map findAllMessagesMap(String lang); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryImpl.java new file mode 100644 index 0000000..7d37145 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryImpl.java @@ -0,0 +1,101 @@ +package org.egovframe.cloud.portalservice.domain.message; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.portalservice.api.message.dto.MessageListResponseDto; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.querydsl.core.types.Projections.fields; +import static org.egovframe.cloud.portalservice.domain.message.QMessage.message; +import static org.springframework.util.StringUtils.hasLength; + +/** + * org.egovframe.cloud.portalservice.domain.code.CodeRepositoryImpl + *

+ * 공통코드 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class MessageRepositoryImpl implements MessageRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + /** + * Message 목록 조회 + * 영문 요청시 값이 없는 레코드는 한글로 대체된다. + * + * @param lang + * @return + */ + @Override + public List findAllMessages(String lang) { + + return queryFactory + .select(fields(MessageListResponseDto.class, + message.messageId, + new CaseBuilder() + .when( + langEnEq(lang) + .and(message.messageEnName.isNotNull()) + ).then(message.messageEnName) + .otherwise(message.messageKoName).as("messageName") + )) + .from(message) + .orderBy(message.messageId.asc()) + .fetch(); + } + + /** + * Message 목록 조회 + * 영문 요청시 값이 없는 레코드는 한글로 대체된다. + * + * @param lang + * @return + */ + @Override + public Map findAllMessagesMap(String lang) { + List messages = queryFactory + .select(message.messageId, + new CaseBuilder() + .when( + langEnEq(lang) + .and(message.messageEnName.isNotNull()) + ).then(message.messageEnName) + .otherwise(message.messageKoName).as(message.messageKoName) + ) + .from(message) + .orderBy(message.messageId.asc()) + .fetch(); + + return messages.stream() + .collect(Collectors.toMap(tuple -> tuple.get(message.messageId), tuple -> tuple.get(message.messageKoName), (a, b) -> b)); + } + + /** + * 영문 요청인지 여부를 리턴한다 + * + * @param lang + * @return + */ + private BooleanExpression langEnEq(String lang) { + boolean en = hasLength(lang) && lang.equals("en"); + return Expressions.asBoolean(en).isTrue(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/Policy.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/Policy.java new file mode 100644 index 0000000..c277aff --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/Policy.java @@ -0,0 +1,88 @@ +package org.egovframe.cloud.portalservice.domain.policy; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.*; +import java.time.ZonedDateTime; + +/** + * org.egovframe.cloud.portalservice.domain.policy.Policy + *

+ * 이용약관/개인정보수집동의(Policy) 도메인 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/06    shinmj  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@Entity +public class Policy extends BaseEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "policy_id") + private Long id; + + @Column(nullable = false, name = "type_id", length = 20) + private String type; + + @Column(name = "policy_title", length = 200) + private String title; + + @Column(name = "use_at", columnDefinition = "boolean default true") + private Boolean isUse; + + @Column(name = "reg_timestamp") + private ZonedDateTime regDate; + + @Column(name = "policy_content", columnDefinition = "LONGTEXT") + private String contents; + + @Builder + public Policy(String type, String title, Boolean isUse, ZonedDateTime regDate, String contents) { + this.type = type; + this.title = title; + this.isUse = isUse; + this.regDate = regDate; + this.contents = contents; + } + + /** + * 내용 수정 + * + * @param title + * @param isUse + * @param contents + * @return this + */ + public Policy update(String title, Boolean isUse, String contents) { + this.title = title; + this.isUse = isUse; + this.contents = contents; + return this; + } + + /** + * 사용여부 변경 + * + * @param isUse + * @return this + */ + public Policy updateIsUSe(Boolean isUse) { + this.isUse = isUse; + return this; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepository.java new file mode 100644 index 0000000..c130893 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepository.java @@ -0,0 +1,6 @@ +package org.egovframe.cloud.portalservice.domain.policy; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PolicyRepository extends JpaRepository, PolicyRepositoryCustom { +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryCustom.java new file mode 100644 index 0000000..4000828 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryCustom.java @@ -0,0 +1,11 @@ +package org.egovframe.cloud.portalservice.domain.policy; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface PolicyRepositoryCustom { + Page search(RequestDto requestDto, Pageable pageable); + PolicyResponseDto searchOne(String type); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryImpl.java new file mode 100644 index 0000000..7625702 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryImpl.java @@ -0,0 +1,110 @@ +package org.egovframe.cloud.portalservice.domain.policy; + +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import javax.persistence.EntityManager; + +import static com.querydsl.core.types.Projections.*; +import static com.querydsl.core.types.Projections.fields; +import static org.egovframe.cloud.portalservice.domain.policy.QPolicy.policy; +import static org.springframework.util.StringUtils.hasLength; + +/** + * org.egovframe.cloud.portalservice.domain.policy.PolicyRepositoryImpl + *

+ * 이용약관/개인정보수집동의(Policy) querydsl 사용 확장 repository class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/06    shinmj  최초 생성
+ * 
+ */ +@Slf4j +public class PolicyRepositoryImpl implements PolicyRepositoryCustom{ + private final JPAQueryFactory queryFactory; + + public PolicyRepositoryImpl(EntityManager em) { + queryFactory = new JPAQueryFactory(em); + } + + /** + * 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Override + public Page search(RequestDto requestDto, Pageable pageable) { + + QueryResults results = queryFactory.select( + constructor(PolicyResponseDto.class, policy) + ) + .from(policy) + .where( + searchTextLike(requestDto) + ) + .orderBy( policy.regDate.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetchResults(); + + return new PageImpl<>(results.getResults(), pageable, results.getTotal()); + + } + + /** + * 회원가입 시 사용여부=true인 최근 등록된 자료 조회 + * + * @param type + * @return + */ + @Override + public PolicyResponseDto searchOne(String type) { + return queryFactory.select(constructor(PolicyResponseDto.class, policy)) + .from(policy) + .where( + policy.isUse.eq(true), + policy.type.eq(type) + ) + .orderBy(policy.regDate.desc()) + .limit(1) + .fetchOne(); + } + + /** + * dynamic query binding + * + * @param requestDto + * @return + */ + private BooleanExpression searchTextLike(RequestDto requestDto) { + final String searchType = requestDto.getKeywordType(); + final String value = requestDto.getKeyword(); + if (!hasLength(searchType) || !hasLength(value)) { + return null; + } + + if ("title".equals(searchType)) { + return policy.title.containsIgnoreCase(value); + } else if ("contents".equals(searchType)) { + return policy.contents.containsIgnoreCase(value); + } + return null; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/Privacy.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/Privacy.java new file mode 100644 index 0000000..b68c8c1 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/Privacy.java @@ -0,0 +1,101 @@ +package org.egovframe.cloud.portalservice.domain.privacy; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.*; + +/** + * org.egovframe.cloud.portalservice.domain.privacy.Privacy + *

+ * 개인정보처리방침 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +public class Privacy extends BaseEntity { + + /** + * 개인정보처리방침 번호 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer privacyNo; + + /** + * 개인정보처리방침 제목 + */ + @Column(nullable = false, length = 100) + private String privacyTitle; + + /** + * 개인정보처리방침 내용 + */ + @Column(nullable = false, columnDefinition = "longtext") + private String privacyContent; + + /** + * 사용 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '1'") + private Boolean useAt; + + /** + * 빌더 패턴 클래스 생성자 + * + * @param privacyNo 개인정보처리방침 번호 + * @param privacyTitle 개인정보처리방침 제목 + * @param privacyContent 개인정보처리방침 내용 + * @param useAt 사용 여부 + */ + @Builder + public Privacy(Integer privacyNo, String privacyTitle, String privacyContent, Boolean useAt) { + this.privacyNo = privacyNo; + this.privacyTitle = privacyTitle; + this.privacyContent = privacyContent; + this.useAt = useAt; + } + + /** + * 개인정보처리방침 속성 값 수정 + * + * @param privacyTitle 개인정보처리방침 제목 + * @param privacyContent 개인정보처리방침 내용 + * @param useAt 사용 여부 + * @return Privacy 개인정보처리방침 엔티티 + */ + public Privacy update(String privacyTitle, String privacyContent, Boolean useAt) { + this.privacyTitle = privacyTitle; + this.privacyContent = privacyContent; + this.useAt = useAt; + + return this; + } + + /** + * 개인정보처리방침 사용 여부 수정 + * + * @param useAt 사용 여부 + * @return Privacy 개인정보처리방침 엔티티 + */ + public Privacy updateUseAt(Boolean useAt) { + this.useAt = useAt; + + return this; + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepository.java new file mode 100644 index 0000000..17f8c58 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepository.java @@ -0,0 +1,24 @@ +package org.egovframe.cloud.portalservice.domain.privacy; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.portalservice.domain.privacy.PrivacyRepository + *

+ * 개인정보처리방침 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +public interface PrivacyRepository extends JpaRepository, PrivacyRepositoryCustom { + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepositoryCustom.java new file mode 100644 index 0000000..08d9cac --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepositoryCustom.java @@ -0,0 +1,47 @@ +package org.egovframe.cloud.portalservice.domain.privacy; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyListResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.privacy.PrivacyRepositoryCustom + *

+ * 개인정보처리방침 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +public interface PrivacyRepositoryCustom { + + /** + * 개인정보처리방침 페이지 목록 조회 + * + * @param requestDto 개인정보처리방침 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 개인정보처리방침 목록 응답 DTO + */ + Page findPage(RequestDto requestDto, Pageable pageable); + + /** + * 사용여부로 개인정보처리방침 내림차순 전체 목록 조회 + * + * @param useAt 사용 여부 + * @return List 개인정보처리방침 상세 응답 DTO + */ + List findAllByUseAt(Boolean useAt); + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepositoryImpl.java new file mode 100644 index 0000000..1fd466e --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/privacy/PrivacyRepositoryImpl.java @@ -0,0 +1,138 @@ +package org.egovframe.cloud.portalservice.domain.privacy; + +import java.util.List; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyListResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.QPrivacyListResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.QPrivacyResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.google.common.base.CaseFormat; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.domain.privacy.PrivacyRepositoryImpl + * + * 개인정보처리방침 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class PrivacyRepositoryImpl { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 개인정보처리방침 페이지 목록 조회 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 개인정보처리방침 목록 응답 DTO + */ + public Page findPage(RequestDto requestDto, Pageable pageable) { + JPQLQuery query = jpaQueryFactory + .select(new QPrivacyListResponseDto( + QPrivacy.privacy.privacyNo, + QPrivacy.privacy.privacyTitle, + QPrivacy.privacy.useAt, + QPrivacy.privacy.createdDate + )) + .from(QPrivacy.privacy) + .where(getBooleanExpressionKeyword(requestDto)); + + //정렬 + pageable.getSort().stream().forEach(sort -> { + Order order = sort.isAscending() ? Order.ASC : Order.DESC; + String property = sort.getProperty(); + + Path target = Expressions.path(Object.class, QPrivacy.privacy, CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, property)); + @SuppressWarnings({ "rawtypes", "unchecked" }) + OrderSpecifier orderSpecifier = new OrderSpecifier(order, target); + query.orderBy(orderSpecifier); + }); + + QueryResults result = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) //페이징 + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 사용여부로 개인정보처리방침 내림차순 전체 목록 조회 + * + * @param useAt 사용 여부 + * @return List 개인정보처리방침 상세 응답 DTO List + */ + public List findAllByUseAt(Boolean useAt) { + return jpaQueryFactory + .select(new QPrivacyResponseDto( + QPrivacy.privacy.privacyNo, + QPrivacy.privacy.privacyTitle, + QPrivacy.privacy.privacyContent, + QPrivacy.privacy.useAt + )) + .from(QPrivacy.privacy) + .where(getBooleanExpressionUseAt(useAt)) + .orderBy(QPrivacy.privacy.privacyNo.desc()) + .fetch(); + } + + /** + * 사용 여부 동적 검색 표현식 리턴 + * + * @param useAt 사용 여부 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpressionUseAt(Boolean useAt) { + return useAt == null ? null : QPrivacy.privacy.useAt.eq(useAt); + } + + /** + * 요청 DTO로 동적 검색 표현식 리턴 + * + * @param requestDto 요청 DTO + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpressionKeyword(RequestDto requestDto) { + if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null; + + switch (requestDto.getKeywordType()) { + case "privacyTitle": // 개인정보처리방침 제목 + return QPrivacy.privacy.privacyTitle.containsIgnoreCase(requestDto.getKeyword()); + case "privacyContent": // 개인정보처리방침 내용 + return QPrivacy.privacy.privacyContent.containsIgnoreCase(requestDto.getKeyword()); + default: + return null; + } + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/Statistics.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/Statistics.java new file mode 100644 index 0000000..131e709 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/Statistics.java @@ -0,0 +1,55 @@ +package org.egovframe.cloud.portalservice.domain.statistics; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseTimeEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +import static javax.persistence.GenerationType.IDENTITY; + +/** + * org.egovframe.cloud.portalservice.domain.statistics.Statistics + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/01    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +public class Statistics extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "log_id") + private Long id; + + private Long siteId; + + private String statisticsId; + + @Column(name = "ip_addr", length = 100) + private String remoteIp; + + @Builder + public Statistics(Long siteId, String statisticsId, String remoteIp) { + this.siteId = siteId; + this.statisticsId = statisticsId; + this.remoteIp = remoteIp; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepository.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepository.java new file mode 100644 index 0000000..7de8cce --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepository.java @@ -0,0 +1,27 @@ +package org.egovframe.cloud.portalservice.domain.statistics; + +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsResponseDto; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.statistics.StatisticsRepository + *

+ * 통계 entity를 위한 repository class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/07    shinmj      최초 생성
+ * 
+ */ +public interface StatisticsRepository extends JpaRepository, StatisticsRepositoryCustom { + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryCustom.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryCustom.java new file mode 100644 index 0000000..7ce1c83 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryCustom.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.portalservice.domain.statistics; + +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsResponseDto; +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsYMRequestDto; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.domain.statistics.StatisticsRepositoryCustom + *

+ * 접속통계 Querydsl interface + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/07    jaeyeolkim  최초 생성
+ * 
+ */ +public interface StatisticsRepositoryCustom { + List findMonthBySiteId(Long siteId); + List findDayBySiteId(Long siteId, StatisticsYMRequestDto requestDto); +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryImpl.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryImpl.java new file mode 100644 index 0000000..0cad2c9 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryImpl.java @@ -0,0 +1,119 @@ +package org.egovframe.cloud.portalservice.domain.statistics; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsResponseDto; +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsYMRequestDto; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.Month; +import java.time.YearMonth; +import java.util.List; +import java.util.stream.Collectors; + +import static org.egovframe.cloud.portalservice.domain.statistics.QStatistics.statistics; + +/** + * org.egovframe.cloud.portalservice.domain.statistics.StatisticsRepositoryImpl + *

+ * 통계 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/07    shinmj      최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@Repository +public class StatisticsRepositoryImpl implements StatisticsRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + /** + * 현재 년도의 접속통계 월별 조회 + * + * @param siteId + * @return + */ + @Override + public List findMonthBySiteId(Long siteId) { + LocalDateTime now = LocalDateTime.now(); + + List tuples = jpaQueryFactory.select( + Expressions.constant(now.getYear()), + statistics.createdDate.month().as("month"), + Expressions.constant(0), + statistics.createdDate.month().as("x"), + statistics.id.count()) + .from(statistics) + .where( + statistics.siteId.eq(siteId), + statistics.createdDate.between( + LocalDateTime.of(now.getYear(), Month.JANUARY, 1, 0, 0), + LocalDateTime.of(now.getYear(), Month.DECEMBER, 31, 23, 59))) + .groupBy(statistics.createdDate.month()) + .orderBy(statistics.createdDate.month().asc()) + .fetch(); + return tuples.stream() + .map(this::convertDto) + .collect(Collectors.toList()); + } + + /** + * 접속통계 일별 조회 + * + * @param siteId + * @param requestDto + * @return + */ + @Override + public List findDayBySiteId(Long siteId, StatisticsYMRequestDto requestDto) { + YearMonth yearMonth = YearMonth.of(requestDto.getYear(), requestDto.getMonth()); + + List tuples = jpaQueryFactory.select( + Expressions.constant(yearMonth.getYear()), + Expressions.constant(yearMonth.getMonth().getValue()), + statistics.createdDate.dayOfMonth().as("day"), + statistics.createdDate.dayOfMonth().as("x"), + statistics.id.count()) + .from(statistics) + .where( + statistics.siteId.eq(siteId), + statistics.createdDate.between( + LocalDateTime.of(yearMonth.getYear(), yearMonth.getMonth(), 1, 0, 0), + LocalDateTime.of(yearMonth.getYear(), yearMonth.getMonth(), yearMonth.lengthOfMonth(), 23, 59))) + .groupBy(statistics.createdDate.dayOfMonth()) + .orderBy(statistics.createdDate.dayOfMonth().asc()) + .fetch(); + + return tuples.stream() + .map(this::convertDto) + .collect(Collectors.toList()); + } + + /** + * 조회된 tuple을 응답 dto 형태로 변환 + * + * @param tuple + * @return + */ + private StatisticsResponseDto convertDto(Tuple tuple) { + return StatisticsResponseDto.builder() + .year(tuple.get(0, Integer.class)) + .month(tuple.get(1, Integer.class)) + .day(tuple.get(2, Integer.class)) + .x(tuple.get(3, Integer.class)) + .y(tuple.get(4, Long.class)) + .build(); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/user/Role.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/user/Role.java new file mode 100644 index 0000000..1be2d2a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/user/Role.java @@ -0,0 +1,35 @@ +package org.egovframe.cloud.portalservice.domain.user; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.userservice.domain.user.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; +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/user/User.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/user/User.java new file mode 100644 index 0000000..2d15028 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/domain/user/User.java @@ -0,0 +1,41 @@ +package org.egovframe.cloud.portalservice.domain.user; + +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * org.egovframe.cloud.portalservice.domain.user.User + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@NoArgsConstructor +@Entity +public class User extends BaseEntity { + + @Id + @Column(name = "user_no", insertable = false, updatable = false) + private Long id; + + @Column(insertable = false, updatable = false) + private String userId; + + @Column(insertable = false, updatable = false) + private String userName; + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/attachment/AttachmentService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/attachment/AttachmentService.java new file mode 100644 index 0000000..b35c6d0 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/attachment/AttachmentService.java @@ -0,0 +1,470 @@ +package org.egovframe.cloud.portalservice.service.attachment; + +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.RandomStringUtils; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.portalservice.api.attachment.dto.*; +import org.egovframe.cloud.portalservice.domain.attachment.Attachment; +import org.egovframe.cloud.portalservice.domain.attachment.AttachmentId; +import org.egovframe.cloud.portalservice.domain.attachment.AttachmentRepository; +import org.egovframe.cloud.portalservice.utils.StorageUtils; +import org.springframework.core.io.Resource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URL; +import java.net.URLConnection; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.portalservice.service.attachment.AttachmentService + *

+ * 첨부파일 서비스 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/13    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class AttachmentService extends AbstractService { + + private final AttachmentRepository attachmentRepository; + private final StorageUtils storageUtils; + + /** + * 첨부파일 업로드 + * .temp 파일 생성 + * 추후 저장로직에서 rename + * + * @param file + * @return + */ + public AttachmentFileResponseDto uploadFile(MultipartFile file) { + + String basePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM")); + + return upload(file, basePath, true); + } + + /** + * 첨부파일 업로드 + * .temp 파일 생성 + * 추후 저장로직에서 rename + * + * @param file + * @param basePath + * @return + */ + private AttachmentFileResponseDto upload(MultipartFile file, String basePath, boolean isTemp) { + String storeFile = storageUtils.storeFile(file, basePath, isTemp); + return AttachmentFileResponseDto.builder() + .originalFileName(file.getOriginalFilename()) + .physicalFileName(StringUtils.cleanPath(basePath + "/" + storeFile)) + .message("Success") + .size(file.getSize()) + .fileType(file.getContentType()) + .build(); + } + + + /** + * 여러 첨부파일 업로드 + * .temp 파일 생성 + * 추후 저장로직에서 rename + * + * @param files + * @return + */ + public List uploadFiles(List files) { + String basePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM")); + List responseDtoList = new ArrayList<>(); + + for (MultipartFile file : files) { + responseDtoList.add(upload(file, basePath, true)); + } + return responseDtoList; + } + + /** + * 에디터 파일 업로드 + * + * @param editorRequestDto + * @return + */ + public AttachmentEditorResponseDto uploadEditor(AttachmentBase64RequestDto editorRequestDto) { + String fileBase64 = editorRequestDto.getFileBase64(); + + if (fileBase64 == null || fileBase64.equals("")) { + // 업로드할 파일이 없습니다. + throw new BusinessMessageException("valid.file.not_exists"); + } + + if (fileBase64.length() > 400000) { + //파일 용량이 너무 큽니다. + throw new BusinessMessageException(getMessage("valid.file.too_big")); + } + + String basePath = "editor/" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM")); + String storeFile = storageUtils.storeBase64File(editorRequestDto, basePath); + + return AttachmentEditorResponseDto.builder() + .uploaded(1) + .url(basePath.replaceAll("/", "-") + "-" + storeFile) + .originalFileName(editorRequestDto.getOriginalName()) + .size(editorRequestDto.getSize()) + .fileType(editorRequestDto.getFileType()) + .message("Success") + .build(); + } + + /** + * 에디터에서 호출 시 byte[] 형태의 값으로 incoding + * + * @param imagename + * @return + */ + @Transactional(readOnly = true) + public AttachmentImageResponseDto loadImage(String imagename) { + imagename = imagename.replaceAll("-", "/"); + return storageUtils.loadImage(imagename); + } + + /** + * img 태그에서 호출 시 byte[] 형태의 값으로 incoding + * + * @param uniqueId + * @return + */ + @Transactional(readOnly = true) + public AttachmentImageResponseDto loadImageByUniqueId(String uniqueId) { + Attachment attachment = attachmentRepository.findAllByUniqueId(uniqueId) + // 파일을 찾을 수 없습니다. + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.file.not_found") + " ID= " + uniqueId)); + + return storageUtils.loadImage(attachment.getPhysicalFileName()); + } + + /** + * 첨부파일 다운로드 + * + * @param uniqueId + * @return + */ + public AttachmentDownloadResponseDto downloadFile(String uniqueId) { + Attachment attachment = attachmentRepository.findAllByUniqueId(uniqueId) + // 파일을 찾을 수 없습니다. + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.file.not_found") + " ID= " + uniqueId)); + + Resource resource = storageUtils.downloadFile(attachment.getPhysicalFileName()); + + // 첨부파일 다운로드 할 때 마다 Download 횟수 + 1 + attachment.updateDownloadCnt(); + + return AttachmentDownloadResponseDto.builder() + .file(resource) + .originalFileName(attachment.getOriginalFileName()) + .build(); + } + + /** + * 첨부파일 코드로 첨부파일 목록 조회 + * + * @param attachmentCode + * @return + */ + @Transactional(readOnly = true) + public List findByCode(String attachmentCode) { + List attachmentList = attachmentRepository.findByCode(attachmentCode); + return attachmentList.stream() + .map(attachment -> AttachmentResponseDto.builder().attachment(attachment).build()) + .collect(Collectors.toList()); + } + + /** + * 첨부파일 저장 + * + * @param saveRequestDtoList + * @return 생성된 첨부파일 코드 + */ + public String save(List saveRequestDtoList) { + String attachmentCode = RandomStringUtils.randomAlphanumeric(20); + for (int i = 0; i < saveRequestDtoList.size(); i++) { + AttachmentTempSaveRequestDto requestDto = saveRequestDtoList.get(i); + AttachmentId attachmentId = AttachmentId.builder() + .code(attachmentCode) + .seq(i + 1L) + .build(); + + // 첨부파일 .temp 제거 + String renameTemp = storageUtils.renameTemp(requestDto.getPhysicalFileName()); + + attachmentRepository.save( + Attachment.builder() + .attachmentId(attachmentId) + .uniqueId(UUID.randomUUID().toString()) + .physicalFileName(renameTemp) + .originalFileName(requestDto.getOriginalName()) + .size(requestDto.getSize()) + .fileType(requestDto.getFileType()) + .entityName(requestDto.getEntityName()) + .entityId(requestDto.getEntityId()) + .build() + ); + } + return attachmentCode; + } + + /** + * 첨부파일 저장 + * 이미 attachment code 가 있는 경우 seq만 새로 생성해서 저장 + * or + * isUserDelete = true 인 경우 삭제 여부 Y + * + * @param attachmentCode + * @param saveRequestDtoList + * @return + */ + public String saveByCode(String attachmentCode, List saveRequestDtoList) { + for (AttachmentTempSaveRequestDto saveRequestDto : saveRequestDtoList) { + // 사용자 삭제인 경우 삭제여부 Y + if (saveRequestDto.isDelete()) { + Attachment attachment = attachmentRepository.findAllByUniqueId(saveRequestDto.getUniqueId()) + // 파일을 찾을 수 없습니다. + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.file.not_found") + " ID= " + saveRequestDto.getUniqueId())); + attachment.updateIsDelete(saveRequestDto.isDelete()); + } else if (saveRequestDto.getUniqueId() == null || saveRequestDto.getUniqueId().equals("")) { + // 해당 attachment에 seq 조회해서 attachmentid 생성 + AttachmentId attachmentId = attachmentRepository.getId(attachmentCode); + //새로운 첨부파일 저장 (물리적 파일 .temp 제거) + String renameTemp = storageUtils.renameTemp(saveRequestDto.getPhysicalFileName()); + attachmentRepository.save( + Attachment.builder() + .attachmentId(attachmentId) + .uniqueId(UUID.randomUUID().toString()) + .originalFileName(saveRequestDto.getOriginalName()) + .physicalFileName(renameTemp) + .size(saveRequestDto.getSize()) + .fileType(saveRequestDto.getFileType()) + .entityName(saveRequestDto.getEntityName()) + .entityId(saveRequestDto.getEntityId()) + .build() + ); + } + } + + return attachmentCode; + } + + /** + * 관리자 - 전체 첨부파일 목록 조회 + * + * @param searchRequestDto + * @param pageable + * @return + */ + @Transactional(readOnly = true) + public Page search(RequestDto searchRequestDto, Pageable pageable) { + return attachmentRepository.search(searchRequestDto, pageable); + } + + /** + * 관리자 - 삭제여부 토글 + * + * @param uniqueId + * @param isDelete + * @return + */ + public String toggleDelete(String uniqueId, boolean isDelete) { + Attachment attachment = attachmentRepository.findAllByUniqueId(uniqueId) + // 파일을 찾을 수 없습니다. + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.file.not_found") + " ID= " + uniqueId)); + + attachment.updateIsDelete(isDelete); + return uniqueId; + } + + /** + * 관리자 - 첨부파일 한건 완전 삭제 + * + * @param uniqueId + */ + public void delete(String uniqueId) { + Attachment attachment = attachmentRepository.findAllByUniqueId(uniqueId) + // 파일을 찾을 수 없습니다. + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.file.not_found") + " ID= " + uniqueId)); + // 물리적 파일 삭제 + boolean deleted = storageUtils.deleteFile(attachment.getPhysicalFileName()); + if (deleted) { + attachmentRepository.delete(attachment); + } else { + throw new BusinessMessageException(getMessage("valid.file.not_deleted")); + } + } + + /** + * 첨부파일 업로드 및 저장 + * + * @param files + * @param uploadRequestDto + */ + public String uploadAndSave(List files, AttachmentUploadRequestDto uploadRequestDto) { + String basePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM")); + String attachmentCode = RandomStringUtils.randomAlphanumeric(20); + + for (int i = 0; i < files.size(); i++) { + AttachmentId attachmentId = AttachmentId.builder() + .code(attachmentCode) + .seq(i + 1L) + .build(); + + // 물리적 파일 생성 + AttachmentFileResponseDto fileResponseDto = upload(files.get(i), basePath, false); + + attachmentRepository.save( + Attachment.builder() + .attachmentId(attachmentId) + .uniqueId(UUID.randomUUID().toString()) + .physicalFileName(fileResponseDto.getPhysicalFileName()) + .originalFileName(fileResponseDto.getOriginalFileName()) + .size(fileResponseDto.getSize()) + .fileType(fileResponseDto.getFileType()) + .entityName(uploadRequestDto.getEntityName()) + .entityId(uploadRequestDto.getEntityId()) + .build() + ); + } + + return attachmentCode; + } + + /** + * 첨부파일 저장 + * 이미 attachment code 가 있는 경우 이므로 seq만 새로 생성해서 저장 + * or + * isUserDelete = true 인 경우 삭제 여부 Y + * + * @param files + * @param attachmentCode + * @param uploadRequestDto + * @param updateRequestDtoList + * @return + */ + public String uploadAndUpdate(List files, + String attachmentCode, + AttachmentUploadRequestDto uploadRequestDto, + List updateRequestDtoList) { + String basePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM")); + + // 기존 파일 삭제 처리 + if (updateRequestDtoList != null) { + for (AttachmentUpdateRequestDto saveRequestDto : updateRequestDtoList) { + if (saveRequestDto.getIsDelete()) { + Attachment attachment = attachmentRepository.findAllByUniqueId(saveRequestDto.getUniqueId()) + // 파일을 찾을 수 없습니다. + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.file.not_found") + " ID= " + saveRequestDto.getUniqueId())); + attachment.updateIsDelete(saveRequestDto.getIsDelete()); + } + } + } + + if (files != null) { + //새로운 파일 저장 처리 + for (int i = 0; i < files.size(); i++) { + // 해당 attachment에 seq 조회해서 attachmentid 생성 + AttachmentId attachmentId = attachmentRepository.getId(attachmentCode); + + // 물리적 파일 생성 + AttachmentFileResponseDto fileResponseDto = upload(files.get(i), basePath, false); + + attachmentRepository.save( + Attachment.builder() + .attachmentId(attachmentId) + .uniqueId(UUID.randomUUID().toString()) + .physicalFileName(fileResponseDto.getPhysicalFileName()) + .originalFileName(fileResponseDto.getOriginalFileName()) + .size(fileResponseDto.getSize()) + .fileType(fileResponseDto.getFileType()) + .entityName(uploadRequestDto.getEntityName()) + .entityId(uploadRequestDto.getEntityId()) + .build() + ); + } + } + + + return attachmentCode; + } + + /** + * entity 정보 update + * + * @param attachmentCode + * @param uploadRequestDto + * @return + */ + public String updateEntity(String attachmentCode, AttachmentUploadRequestDto uploadRequestDto) { + System.out.println(" ====attachmentCode : " + attachmentCode); + System.out.println(" ====uploadRequestDto : " + uploadRequestDto); + List attachments = attachmentRepository.findByCode(attachmentCode); + for (Attachment attachment : attachments) { + attachment.updateEntity(uploadRequestDto.getEntityName(), uploadRequestDto.getEntityId()); + } + + return attachmentCode; + } + + /** + * 첨부파일 저장 후 기능 저장 시 오류 날 경우 + * 조회되는 첨부파일 목록 전부 삭제 + * rollback + * + * @param attachmentCode + */ + public void deleteAllEmptyEntity(String attachmentCode) { + List attachmentList = attachmentRepository.findByCode(attachmentCode); + + if (attachmentList == null || attachmentList.size() <= 0) { + throw new EntityNotFoundException(getMessage("valid.file.not_found") + " ID= " + attachmentCode); + } + + for (Attachment attachment: attachmentList) { + // 첨부파일 저장 후 기능 저장 시 오류 날 경우에만 첨부파일 전체 삭제를 하므로 + // entity 정보가 있는 경우에는 삭제하지 못하도록 한다. + if (attachment.getEntityId() != null || StringUtils.hasText(attachment.getEntityId())) { + throw new BusinessMessageException(getMessage("valid.file.not_deleted")); + } + // 물리적 파일 삭제 + boolean deleted = storageUtils.deleteFile(attachment.getPhysicalFileName()); + if (deleted) { + attachmentRepository.delete(attachment); + } else { + throw new BusinessMessageException(getMessage("valid.file.not_deleted")); + } + } + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/banner/BannerService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/banner/BannerService.java new file mode 100644 index 0000000..664cb55 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/banner/BannerService.java @@ -0,0 +1,216 @@ +package org.egovframe.cloud.portalservice.service.banner; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.egovframe.cloud.common.dto.AttachmentEntityMessage; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerImageResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerListResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerRequestDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerSaveRequestDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerUpdateRequestDto; +import org.egovframe.cloud.portalservice.domain.banner.Banner; +import org.egovframe.cloud.portalservice.domain.banner.BannerRepository; +import org.egovframe.cloud.portalservice.domain.menu.Site; +import org.egovframe.cloud.portalservice.domain.menu.SiteRepository; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.portalservice.service.banner.BannerService + *

+ * 배너 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class BannerService extends AbstractService { + + /** + * 배너 레파지토리 인터페이스 + */ + private final BannerRepository bannerRepository; + private final SiteRepository siteRepository; + + /** + * 이벤트 메시지 발행하기 위한 spring cloud stream 유틸리티 클래스 + */ + private final StreamBridge streamBridge; + + /** + * 조회 조건에 일치하는 배너 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 배너 목록 응답 DTO + */ + public Page findPage(BannerRequestDto requestDto, Pageable pageable) { + return bannerRepository.findPage(requestDto, pageable); + } + + /** + * 유형별 배너 목록 조회 + * + * @param bannerTypeCodes 배너 유형 코드 목록 + * @param bannerCount 배너 수 + * @param useAt 사용 여부 + * @return Map> 배너 유형 코드별 배너 이미지 응답 DTO Map + */ + public Map> findList(List bannerTypeCodes, Integer bannerCount, Boolean useAt, Long siteId) { + Map> bannerMap = new HashMap<>(); + + for (String bannerTypeCode : bannerTypeCodes) { + bannerMap.put(bannerTypeCode, bannerRepository.findList(bannerTypeCode, bannerCount, useAt, siteId)); + } + + return bannerMap; + } + + /** + * 배너 단건 조회 + * + * @param bannerNo 배너 번호 + * @return BannerResponseDto 배너 응답 DTO + */ + public BannerResponseDto findById(Integer bannerNo) { + Banner entity = findBanner(bannerNo); + + return new BannerResponseDto(entity); + } + + /** + * 배너 다음 정렬 순서 조회 + * + * @param siteId siteId + * @return Integer 다음 정렬 순서 + */ + public Integer findNextSortSeq(Long siteId) { + return bannerRepository.findNextSortSeq(siteId); + } + + /** + * 배너 등록 + * + * @param requestDto 배너 등록 요청 DTO + * @return BannerResponseDto 배너 응답 DTO + */ + @Transactional + public BannerResponseDto save(BannerSaveRequestDto requestDto) { + System.out.println("@@@@@@@@requestDto:"+requestDto); + //site 정보 조회 + Site site = siteRepository.findById(requestDto.getSiteId()) + .orElseThrow(() -> + new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu.site")}) + " ID= " + requestDto.getSiteId())); + + // 동일한 정렬 순서가 존재할 경우 +1 + Optional authorization = bannerRepository.findBySortSeqAndSiteId(requestDto.getSortSeq(), requestDto.getSiteId()); + if (authorization.isPresent()) { + bannerRepository.updateSortSeq(requestDto.getSortSeq(), null, 1, requestDto.getSiteId()); + } + + Banner entity = bannerRepository.save(requestDto.toEntity(site)); + + //첨부파일 entity 정보 업데이트 하기 위해 이벤트 메세지 발행 + sendAttachmentEntityInfo(streamBridge, + AttachmentEntityMessage.builder() + .attachmentCode(entity.getAttachmentCode()) + .entityName(entity.getClass().getName()) + .entityId(String.valueOf(entity.getBannerNo())) + .build()); + + return new BannerResponseDto(entity); + } + + /** + * 배너 수정 + * + * @param bannerNo 배너 번호 + * @param requestDto 배너 수정 요청 DTO + * @return BannerResponseDto 배너 응답 DTO + */ + @Transactional + public BannerResponseDto update(Integer bannerNo, BannerUpdateRequestDto requestDto) { + Banner entity = findBanner(bannerNo); + + //site 정보 조회 + Site site = siteRepository.findById(requestDto.getSiteId()) + .orElseThrow(() -> + new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu.site")}) + " ID= " + requestDto.getSiteId())); + + // 동일한 정렬 순서가 존재할 경우 +1 + Optional authorization = bannerRepository.findBySortSeqAndSiteId(requestDto.getSortSeq(), requestDto.getSiteId()); + if (authorization.isPresent()) { + bannerRepository.updateSortSeq(requestDto.getSortSeq(), null, 1, requestDto.getSiteId()); + } + + // 수정 + entity.update(requestDto.getBannerTypeCode(), requestDto.getBannerTitle(), requestDto.getAttachmentCode(), + requestDto.getUrlAddr(), requestDto.getNewWindowAt(), requestDto.getBannerContent(), requestDto.getSortSeq(), site); + + return new BannerResponseDto(entity); + } + + /** + * 배너 사용 여부 수정 + * + * @param bannerNo 배너 번호 + * @param useAt 사용 여부 + * @return BannerResponseDto 배너 응답 DTO + */ + @Transactional + public BannerResponseDto updateUseAt(Integer bannerNo, Boolean useAt) { + Banner entity = findBanner(bannerNo); + + // 수정 + entity.updateUseAt(useAt); + + return new BannerResponseDto(entity); + } + + /** + * 배너 삭제 + * + * @param bannerNo 배너 번호 + */ + @Transactional + public void delete(Integer bannerNo) { + Banner entity = findBanner(bannerNo); + + // 삭제 + bannerRepository.delete(entity); + } + + /** + * 배너 번호로 배너 엔티티 조회 + * + * @param bannerNo 배너 번호 + * @return Banner 배너 엔티티 + */ + private Banner findBanner(Integer bannerNo) { + return bannerRepository.findById(bannerNo) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("banner")}))); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/code/CodeDetailService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/code/CodeDetailService.java new file mode 100644 index 0000000..722e521 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/code/CodeDetailService.java @@ -0,0 +1,109 @@ +package org.egovframe.cloud.portalservice.service.code; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.portalservice.api.code.dto.CodeDetailListResponseDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeDetailResponseDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeDetailSaveRequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeDetailUpdateRequestDto; +import org.egovframe.cloud.portalservice.domain.code.Code; +import org.egovframe.cloud.portalservice.domain.code.CodeRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.portalservice.service.code.CodeDetailService + *

+ * 공통코드 상세 CRUD 요청을 처리하는 Service + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    jaeyeolkim  최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class CodeDetailService { + private final CodeRepository codeRepository; + + /** + * 단건 조회 + * + * @param codeId + * @return + */ + public CodeDetailResponseDto findByCodeId(String codeId) { + Code code = codeRepository.findByCodeId(codeId) + .orElseThrow(() -> new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + codeId)); + return new CodeDetailResponseDto(code); + } + + /** + * 등록 + * + * @param saveRequestDto + * @return + */ + @Transactional + public String save(CodeDetailSaveRequestDto saveRequestDto) { + return codeRepository.save(saveRequestDto.toEntity()).getCodeId(); + } + + /** + * 수정 + * + * @param codeId + * @param requestDto + * @return + */ + @Transactional + public String update(String codeId, CodeDetailUpdateRequestDto requestDto) { + Code code = codeRepository.findByCodeId(codeId) + .orElseThrow(() -> new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + codeId)); + + code.updateDetail(requestDto.getParentCodeId(), requestDto.getCodeName(), requestDto.getCodeDescription(), requestDto.getSortSeq(), requestDto.getUseAt()); + + return codeId; + } + + /** + * 삭제 + * + * @param codeId + */ + @Transactional + public void delete(String codeId) { + Code code = codeRepository.findByCodeId(codeId) + .orElseThrow(() -> new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + codeId)); + codeRepository.delete(code); + } + + /** + * 사용여부 toggle + * + * @param codeId + * @param useAt + * @return + */ + @Transactional + public String updateUseAt(String codeId, boolean useAt) { + Code code = codeRepository.findByCodeId(codeId) + .orElseThrow(() -> new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + codeId)); + + code.updateUseAt(useAt); + + return codeId; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/code/CodeService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/code/CodeService.java new file mode 100644 index 0000000..302c344 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/code/CodeService.java @@ -0,0 +1,122 @@ +package org.egovframe.cloud.portalservice.service.code; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.exception.BusinessException; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.exception.dto.ErrorCode; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.portalservice.api.code.dto.CodeResponseDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeSaveRequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeUpdateRequestDto; +import org.egovframe.cloud.portalservice.domain.code.Code; +import org.egovframe.cloud.portalservice.domain.code.CodeRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * org.egovframe.cloud.portalservice.service.code.CodeService + *

+ * 공통코드 CRUD 요청을 처리하는 Service + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class CodeService extends AbstractService { + private final CodeRepository codeRepository; + + /** + * 단건 조회 + * + * @param codeId + * @return + */ + public CodeResponseDto findByCodeId(String codeId) { + Code code = codeRepository.findByCodeId(codeId) + .orElseThrow(() -> new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + codeId)); + return new CodeResponseDto(code); + } + + /** + * 등록 + * + * @param saveRequestDto + * @return + */ + @Transactional + public String save(CodeSaveRequestDto saveRequestDto) { + Optional byCodeId = codeRepository.findByCodeId(saveRequestDto.getCodeId()); + if (byCodeId.isPresent()) { + throw new BusinessException("코드ID 중복 : " + byCodeId, ErrorCode.DUPLICATE_INPUT_INVALID); + } + return codeRepository.save(saveRequestDto.toEntity()).getCodeId(); + } + + /** + * 수정 + * + * @param codeId + * @param requestDto + * @return + */ + @Transactional + public String update(String codeId, CodeUpdateRequestDto requestDto) { + Code code = codeRepository.findByCodeId(codeId) + .orElseThrow(() -> new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + codeId)); + + code.update(requestDto.getCodeName(), requestDto.getCodeDescription(), requestDto.getSortSeq(), requestDto.getUseAt()); + + return codeId; + } + + /** + * 삭제 - parentCodeId 에서 참조하지 않는 경우에만 삭제할 수 있다 + * + * @param codeId + */ + @Transactional + public void delete(String codeId) { + Code code = codeRepository.findByCodeId(codeId) + .orElseThrow(() -> new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + codeId)); + + long parentCount = codeRepository.countByParentCodeId(codeId); + if (parentCount > 0) { + // 참조하는 데이터가 있어 삭제할 수 없습니다 + throw new BusinessMessageException(getMessage("err.db.constraint.delete")); + } + + codeRepository.delete(code); + } + + /** + * 사용여부 toggle + * + * @param codeId + * @param useAt + * @return + */ + @Transactional + public String updateUseAt(String codeId, boolean useAt) { + Code code = codeRepository.findByCodeId(codeId) + .orElseThrow(() -> new EntityNotFoundException("해당 데이터가 존재하지 않습니다. ID =" + codeId)); + + code.updateUseAt(useAt); + + return codeId; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/content/ContentService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/content/ContentService.java new file mode 100644 index 0000000..a8d7a7e --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/content/ContentService.java @@ -0,0 +1,122 @@ +package org.egovframe.cloud.portalservice.service.content; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.portalservice.api.content.dto.ContentListResponseDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentResponseDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentSaveRequestDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentUpdateRequestDto; +import org.egovframe.cloud.portalservice.domain.content.Content; +import org.egovframe.cloud.portalservice.domain.content.ContentRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * org.egovframe.cloud.portalservice.service.content.ContentService + *

+ * 컨텐츠 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class ContentService extends AbstractService { + + /** + * 컨텐츠 레파지토리 인터페이스 + */ + private final ContentRepository contentRepository; + + /** + * 조회 조건에 일치하는 컨텐츠 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 컨텐츠 목록 응답 DTO + */ + public Page findPage(RequestDto requestDto, Pageable pageable) { + return contentRepository.findPage(requestDto, pageable); + } + + /** + * 컨텐츠 단건 조회 + * + * @param contentNo 컨텐츠 번호 + * @return ContentResponseDto 컨텐츠 응답 DTO + */ + public ContentResponseDto findById(Integer contentNo) { + Content entity = findContent(contentNo); + + return new ContentResponseDto(entity); + } + + /** + * 컨텐츠 등록 + * + * @param requestDto 컨텐츠 등록 요청 DTO + * @return ContentResponseDto 컨텐츠 응답 DTO + */ + @Transactional + public ContentResponseDto save(ContentSaveRequestDto requestDto) { + Content entity = contentRepository.save(requestDto.toEntity()); + + return new ContentResponseDto(entity); + } + + /** + * 컨텐츠 수정 + * + * @param contentNo 컨텐츠 번호 + * @param requestDto 컨텐츠 수정 요청 DTO + * @return ContentResponseDto 컨텐츠 응답 DTO + */ + @Transactional + public ContentResponseDto update(Integer contentNo, ContentUpdateRequestDto requestDto) { + Content entity = findContent(contentNo); + + // 수정 + entity.update(requestDto.getContentName(), requestDto.getContentRemark(), requestDto.getContentValue()); + + return new ContentResponseDto(entity); + } + + /** + * 컨텐츠 삭제 + * + * @param contentNo 컨텐츠 번호 + */ + @Transactional + public void delete(Integer contentNo) { + Content entity = findContent(contentNo); + + // 삭제 + contentRepository.delete(entity); + } + + /** + * 컨텐츠 번호로 컨텐츠 엔티티 조회 + * + * @param contentNo 컨텐츠 번호 + * @return Content 컨텐츠 엔티티 + */ + private Content findContent(Integer contentNo) { + return contentRepository.findById(contentNo) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("content")}))); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/menu/MenuRoleService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/menu/MenuRoleService.java new file mode 100644 index 0000000..cc89b2d --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/menu/MenuRoleService.java @@ -0,0 +1,194 @@ +package org.egovframe.cloud.portalservice.service.menu; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleRequestDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuSideResponseDto; +import org.egovframe.cloud.portalservice.client.BoardServiceClient; +import org.egovframe.cloud.portalservice.client.dto.BoardResponseDto; +import org.egovframe.cloud.portalservice.domain.menu.*; +import org.egovframe.cloud.portalservice.domain.user.Role; +import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; +import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.portalservice.service.menu.MenuRoleService + *

+ * 권한별 메뉴 관리 서비스 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/08/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/17    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class MenuRoleService extends AbstractService { + + private final MenuRoleRepository menuRoleRepository; + private final MenuRepository menuRepository; + private final BoardServiceClient boardServiceClient; + private final CircuitBreakerFactory circuitBreakerFactory; + + /** + * 권한별 메뉴 트리 조회 + * + * @param roleId + * @param siteId + * @return + */ + public List fineTree(String roleId, Long siteId) { + return menuRoleRepository.findTree(roleId, siteId); + } + + /** + * 권한별 메뉴 저장 + * children 데이터 재귀 호출 + * checked 인 경우 저장 + * unchecked 인 경우 삭제 + * + * @param menuRoleRequestDto + */ + private void recursiveSave( MenuRoleRequestDto menuRoleRequestDto) { + if (menuRoleRequestDto.getIsChecked()) { + // checked 인 경우 menuRole 저장 + + Menu menu = menuRepository.findById(menuRoleRequestDto.getId()) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu")}) + " ID= " + menuRoleRequestDto.getId())); + + if (menuRoleRequestDto.getMenuRoleId() == null) { + MenuRole menuRole = MenuRole.builder() + .roleId(menuRoleRequestDto.getRoleId()) + .menu(menu) + .build(); + menuRole.setMenu(menu); + menuRoleRepository.save(menuRole); + }else { + MenuRole menuRole = menuRoleRepository.findById(menuRoleRequestDto.getMenuRoleId()).orElse(null); + + menuRole.setMenu(menu); + } + } else { + //unchecked 인 경우 menurole 삭제 + if (menuRoleRequestDto.getMenuRoleId() != null) { + MenuRole menuRole = menuRoleRepository.findById(menuRoleRequestDto.getMenuRoleId()).orElse(null); + if (menuRole != null) { + menuRoleRepository.delete(menuRole); + } + } + } + + if (menuRoleRequestDto.getChildren() == null || menuRoleRequestDto.getChildren().size() <= 0) { + return; + } + + for (int i = 0; i < menuRoleRequestDto.getChildren().size(); i++) { + MenuRoleRequestDto child = menuRoleRequestDto.getChildren().get(i); + recursiveSave( child); + } + } + + /** + * 권한별 메뉴 저장 + * + * @param menuRoleRequestDtoList + * @return + */ + @Transactional + public String save(List menuRoleRequestDtoList) { + + for (MenuRoleRequestDto menuRoleRequestDto: menuRoleRequestDtoList) { + recursiveSave( menuRoleRequestDto); + } + + return "Success"; + } + + /** + * 계층구조 메뉴 조회 + * 로그인 사용자의 권한으로 조회하고 + * 로그인 사용자가 없는 경우 손님(ROLE_ANONYMOUS) 로 조회한다. + * + * @param siteId + * @return + */ + private List findMenu(Long siteId) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) { + return menuRoleRepository.findMenu(Role.ANONYMOUS.getKey(), siteId); + } + String role = authentication.getAuthorities().stream().map(GrantedAuthority::toString).collect(Collectors.toList()).get(0); + return menuRoleRepository.findMenu(role, siteId); + } + + /** + * 메뉴 유형이 게시판인 경우 + * 해당 게시판의 스킨타입으로 url을 만들어 준다. + * + * @param menuSideResponseDto + */ + private void recursiveSetUrlPath(MenuSideResponseDto menuSideResponseDto) { + if (menuSideResponseDto.getConnectId() != null) { + if ("board".equals(menuSideResponseDto.getMenuType())) { + //connectid 로 board 조회 + //board 의 skinTypeCode로 url 지정 +// BoardResponseDto board = boardServiceClient.findById(menuSideResponseDto.getConnectId()); + CircuitBreaker circuitBreaker = circuitBreakerFactory.create("board"); + BoardResponseDto board = circuitBreaker.run(() -> + boardServiceClient.findById(menuSideResponseDto.getConnectId()), + throwable -> new BoardResponseDto()); + + menuSideResponseDto.setUrlPath("/board/"+board.getSkinTypeCode()+"/"+menuSideResponseDto.getConnectId()); + } else if ("contents".equals(menuSideResponseDto.getMenuType())) { + menuSideResponseDto.setUrlPath("/content/"+menuSideResponseDto.getConnectId()); + } + } + + if (menuSideResponseDto.getChildren() == null || menuSideResponseDto.getChildren().size() <= 0) { + return; + } + + for (int i = 0; i < menuSideResponseDto.getChildren().size(); i++) { + MenuSideResponseDto child = menuSideResponseDto.getChildren().get(i); + recursiveSetUrlPath(child); + } + } + + /** + * 로그인한 사용자의 권한에 맞는 메뉴 조회 + * + * @param siteId + * @return + */ + public List findMenus(Long siteId) { + List menuSideResponseDtoList = findMenu(siteId); + + for (MenuSideResponseDto menuSideResponseDto: menuSideResponseDtoList) { + recursiveSetUrlPath(menuSideResponseDto); + } + + return menuSideResponseDtoList; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/menu/MenuService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/menu/MenuService.java new file mode 100644 index 0000000..fd0de08 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/menu/MenuService.java @@ -0,0 +1,201 @@ +package org.egovframe.cloud.portalservice.service.menu; + +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.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.portalservice.api.menu.dto.*; +import org.egovframe.cloud.portalservice.domain.menu.Menu; +import org.egovframe.cloud.portalservice.domain.menu.MenuRepository; +import org.egovframe.cloud.portalservice.domain.menu.Site; +import org.egovframe.cloud.portalservice.domain.menu.SiteRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.service.menu.MenuService + *

+ * 메뉴관리 서비스 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/21 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class MenuService extends AbstractService { + + private final MenuRepository menuRepository; + private final SiteRepository siteRepository; + + /** + * 메뉴 트리 목록 조회 + * + * @param siteId + * @return + */ + public List findTreeBySiteId(Long siteId) { + return menuRepository.findTreeBySiteId(siteId); + } + + /** + * 메뉴 한건 조회 + * + * @param menuId + * @return + */ + public MenuResponseDto findById(Long menuId) { + return menuRepository.findByIdWithConnectName(menuId); + } + + /** + * 메뉴 트리 한건 추가 + * + * @param menuTreeRequestDto + * @return + */ + @Transactional + public MenuTreeResponseDto save(MenuTreeRequestDto menuTreeRequestDto) { + Site site = siteRepository.findById(menuTreeRequestDto.getSiteId()) + .orElseThrow(() -> + new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu.site")}) + " ID= " + menuTreeRequestDto.getSiteId())); + + Menu parent = null; + + if (menuTreeRequestDto.getParentId() != null) { + parent = menuRepository.findById(menuTreeRequestDto.getParentId()) + .orElseThrow(() -> + new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu")}) + " ID= " + menuTreeRequestDto.getParentId())); + } + + Menu menu = menuRepository.save(Menu.builder() + .parent(parent) + .site(site) + .menuKorName(menuTreeRequestDto.getName()) + .sortSeq(menuTreeRequestDto.getSortSeq()) + .level(menuTreeRequestDto.getLevel()) + .isShow(menuTreeRequestDto.getIsShow()) + .isUse(menuTreeRequestDto.getIsUse()) + .build()); + return MenuTreeResponseDto.builder() + .entity(menu).build(); + } + + /** + * 메뉴 명 변경 + * + * @param menuId + * @param name + * @return + */ + @Transactional + public MenuTreeResponseDto updateName(Long menuId, String name) { + Menu menu = menuRepository.findById(menuId) + .orElseThrow(() -> + new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu")}) + " ID= " + menuId)); + + menu.updateName(name); + + return MenuTreeResponseDto.builder() + .entity(menu).build(); + } + + /** + * 메뉴 상세 정보 변경 + * + * @param menuId + * @param updateRequestDto + * @return + */ + @Transactional + public MenuResponseDto update(Long menuId, MenuUpdateRequestDto updateRequestDto) { + Menu menu = menuRepository.findById(menuId) + .orElseThrow(() -> + new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu")}) + " ID= " + menuId)); + + //컨텐츠 or 게시판인 경우 connectId 필수 + if ("contents".equals(updateRequestDto.getMenuType()) || "board".equals(updateRequestDto.getMenuType())) { + if (updateRequestDto.getConnectId() == null || updateRequestDto.getConnectId().equals("")) { + //컨텐츠 or 게시판을 선택해 주세요 + throw new BusinessMessageException(getMessage("valid.selection.format", new Object[]{updateRequestDto.getMenuTypeName()})); + } + }else if ("inside".equals(updateRequestDto.getMenuType()) || "outside".equals(updateRequestDto.getMenuType())) { + // 내부링크 or 외부링크인 경우 링크 url 필수 + if (updateRequestDto.getUrlPath() == null || updateRequestDto.getUrlPath().equals("")) { + //링크 Url 값은 필수 입니다. + throw new BusinessMessageException(getMessage("valid.required", new Object[]{getMessage("menu.url_path")})); + + } + } + + menu.updateDetail(updateRequestDto); + + return MenuResponseDto.builder() + .entity(menu).build(); + } + + /** + * 메뉴 한건 삭제 + * + * @param menuId + */ + @Transactional + public void delete(Long menuId) { + Menu menu = menuRepository.findById(menuId) + .orElseThrow(() -> + new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu")}) + " ID= " + menuId)); + menuRepository.delete(menu); + } + + /** + * 트리 드래그 앤드 드랍 시 children 데이터 재귀호출 저장 + * + * @param dto + * @param parent + * @param sortSeq + * @param level + */ + private void recursive(MenuDnDRequestDto dto, Menu parent, Integer sortSeq, Integer level) { + Menu menu = menuRepository.findById(dto.getMenuId()) + .orElseThrow(() -> + new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("menu")}) + " ID= " + dto.getMenuId())); + menu.updateDnD(parent, sortSeq, level); + if (dto.getChildren() == null || dto.getChildren().size() <= 0) { + return; + } + + for (int i = 0; i < dto.getChildren().size(); i++) { + MenuDnDRequestDto child = dto.getChildren().get(i); + recursive(child, menu, child.getSortSeq(), menu.getLevel()+1); + } + } + + /** + * 트리 드래그 앤드 드랍 저장 + * + * @param siteId + * @param menuDnDRequestDtoList + * @return + */ + @Transactional + public Long updateDnD(Long siteId, List menuDnDRequestDtoList) { + for (int i = 0; i < menuDnDRequestDtoList.size(); i++) { + recursive(menuDnDRequestDtoList.get(i), null, i+1, 1); + } + return siteId; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/policy/PolicyService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/policy/PolicyService.java new file mode 100644 index 0000000..f607c0b --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/policy/PolicyService.java @@ -0,0 +1,136 @@ +package org.egovframe.cloud.portalservice.service.policy; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyResponseDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicySaveRequestDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyUpdateRequestDto; +import org.egovframe.cloud.portalservice.domain.policy.Policy; +import org.egovframe.cloud.portalservice.domain.policy.PolicyRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * org.egovframe.cloud.portalservice.service.policy.PolicyService + *

+ * 이용약관/개인정보수집동의(Policy) service class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/06    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class PolicyService extends AbstractService { + private final PolicyRepository policyRepository; + + /** + * 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Transactional(readOnly = true) + public Page search(RequestDto requestDto, Pageable pageable){ + return policyRepository.search(requestDto, pageable); + } + + /** + * 회원가입시 가장 최근에 등록한 한건 조회 + * + * @param type + * @return + */ + @Transactional(readOnly = true) + public PolicyResponseDto searchOne(String type) { + return policyRepository.searchOne(type); + } + + /** + * 단건 조회 + * + * @param id + * @return + */ + @Transactional(readOnly = true) + public PolicyResponseDto findById(Long id) { + Policy policy = policyRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("policy")}) + " ID= " + id)); + + + + return new PolicyResponseDto(policy); + } + + /** + * 등록 + * + * @param saveRequestDto + * @return + */ + public Long save(PolicySaveRequestDto saveRequestDto) { + return policyRepository.save(saveRequestDto.toEntity()).getId(); + } + + /** + * 수정 + * + * @param id + * @param updateRequestDto + * @return + */ + public Long update(Long id, PolicyUpdateRequestDto updateRequestDto) { + Policy policy = policyRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("policy")}) + " ID= " + id)); + + policy.update(updateRequestDto.getTitle(), updateRequestDto.getIsUse(), updateRequestDto.getContents()); + + return id; + } + + /** + * 삭제 + * + * @param id + */ + public void delete(Long id) { + Policy policy = policyRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("policy")}) + " ID= " + id)); + policyRepository.delete(policy); + } + + /** + * 사용여부 toggle + * + * @param id + * @param isUse + * @return + */ + public Long updateIsUse(Long id, boolean isUse) { + Policy policy = policyRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("policy")}) + " ID= " + id)); + + policy.updateIsUSe(isUse); + + return id; + } + + + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/privacy/PrivacyService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/privacy/PrivacyService.java new file mode 100644 index 0000000..2c96daf --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/privacy/PrivacyService.java @@ -0,0 +1,151 @@ +package org.egovframe.cloud.portalservice.service.privacy; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyListResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacySaveRequestDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyUpdateRequestDto; +import org.egovframe.cloud.portalservice.domain.privacy.Privacy; +import org.egovframe.cloud.portalservice.domain.privacy.PrivacyRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.service.privacy.PrivacyService + *

+ * 개인정보처리방침 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class PrivacyService extends AbstractService { + + /** + * 개인정보처리방침 레파지토리 인터페이스 + */ + private final PrivacyRepository privacyRepository; + + /** + * 조회 조건에 일치하는 개인정보처리방침 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 개인정보처리방침 목록 응답 DTO + */ + public Page findPage(RequestDto requestDto, Pageable pageable) { + return privacyRepository.findPage(requestDto, pageable); + } + + /** + * 사용여부로 개인정보처리방침 내림차순 전체 목록 조회 + * + * @param useAt 사용 여부 + * @return List 개인정보처리방침 상세 응답 DTO List + */ + public List findAllByUseAt(Boolean useAt) { + return privacyRepository.findAllByUseAt(useAt); + } + + /** + * 개인정보처리방침 단건 조회 + * + * @param privacyNo 개인정보처리방침 번호 + * @return PrivacyResponseDto 개인정보처리방침 응답 DTO + */ + public PrivacyResponseDto findById(Integer privacyNo) { + Privacy entity = findPrivacy(privacyNo); + + return new PrivacyResponseDto(entity); + } + + /** + * 개인정보처리방침 등록 + * + * @param requestDto 개인정보처리방침 등록 요청 DTO + * @return PrivacyResponseDto 개인정보처리방침 응답 DTO + */ + @Transactional + public PrivacyResponseDto save(PrivacySaveRequestDto requestDto) { + Privacy entity = privacyRepository.save(requestDto.toEntity()); + + return new PrivacyResponseDto(entity); + } + + /** + * 개인정보처리방침 수정 + * + * @param privacyNo 개인정보처리방침 번호 + * @param requestDto 개인정보처리방침 수정 요청 DTO + * @return PrivacyResponseDto 개인정보처리방침 응답 DTO + */ + @Transactional + public PrivacyResponseDto update(Integer privacyNo, PrivacyUpdateRequestDto requestDto) { + Privacy entity = findPrivacy(privacyNo); + + // 수정 + entity.update(requestDto.getPrivacyTitle(), requestDto.getPrivacyContent(), requestDto.getUseAt()); + + return new PrivacyResponseDto(entity); + } + + /** + * 개인정보처리방침 사용 여부 수정 + * + * @param privacyNo 개인정보처리방침 번호 + * @param useAt 사용 여부 + * @return PrivacyResponseDto 개인정보처리방침 응답 DTO + */ + @Transactional + public PrivacyResponseDto updateUseAt(Integer privacyNo, Boolean useAt) { + Privacy entity = findPrivacy(privacyNo); + + // 수정 + entity.updateUseAt(useAt); + + return new PrivacyResponseDto(entity); + } + + /** + * 개인정보처리방침 삭제 + * + * @param privacyNo 개인정보처리방침 번호 + */ + @Transactional + public void delete(Integer privacyNo) { + Privacy entity = findPrivacy(privacyNo); + + // 삭제 + privacyRepository.delete(entity); + } + + /** + * 개인정보처리방침 번호로 개인정보처리방침 엔티티 조회 + * + * @param privacyNo 개인정보처리방침 번호 + * @return Privacy 개인정보처리방침 엔티티 + */ + private Privacy findPrivacy(Integer privacyNo) { + return privacyRepository.findById(privacyNo) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("privacy")}))); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/statistics/StatisticsService.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/statistics/StatisticsService.java new file mode 100644 index 0000000..f844734 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/service/statistics/StatisticsService.java @@ -0,0 +1,77 @@ +package org.egovframe.cloud.portalservice.service.statistics; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.common.util.LogUtil; +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsResponseDto; +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsYMRequestDto; +import org.egovframe.cloud.portalservice.domain.statistics.Statistics; +import org.egovframe.cloud.portalservice.domain.statistics.StatisticsRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +/** + * org.egovframe.cloud.portalservice.service.statistics.StatisticsApiController + *

+ * 통계 service class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/07    shinmj      최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@Service +public class StatisticsService extends AbstractService { + + private final StatisticsRepository statisticsRepository; + + /** + * 접속 통계 월별 조회 + * + * @param siteId + * @return + */ + public List findMonthlyBySiteId(Long siteId) { + return statisticsRepository.findMonthBySiteId(siteId); + } + + /** + * 접속 통계 일별 조회 + * + * @param siteId + * @param requestDto + * @return + */ + public List findDailyBySiteId(Long siteId, StatisticsYMRequestDto requestDto) { + return statisticsRepository.findDayBySiteId(siteId, requestDto); + } + + + /** + * 접속통계 log 입력 + * + * @param request + */ + @Transactional + public void save(HttpServletRequest request, String statisticsId) { + statisticsRepository.save( + Statistics.builder() + .siteId(LogUtil.getSiteId(request)) + .statisticsId(statisticsId) + .remoteIp(LogUtil.getUserIp()) + .build() + ); + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FileStorageUtils.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FileStorageUtils.java new file mode 100644 index 0000000..2139b6a --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FileStorageUtils.java @@ -0,0 +1,329 @@ +package org.egovframe.cloud.portalservice.utils; + +import lombok.Getter; +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.util.MessageUtil; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentBase64RequestDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentImageResponseDto; +import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import java.io.*; +import java.net.MalformedURLException; +import java.net.URLConnection; +import java.nio.file.*; +import java.util.Base64; +import java.util.List; + +import static org.egovframe.cloud.portalservice.utils.PortalUtils.getPhysicalFileName; + +/** + * org.egovframe.cloud.portalservice.utils.FileStorageUtils + *

+ * 파일 유틸리티 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/07/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/13    shinmj  최초 생성
+ * 
+ */ +@Slf4j +@Getter +@Component +public class FileStorageUtils implements StorageUtils { + + private final Path fileStorageLocation; + private final Environment environment; + private final MessageUtil messageUtil; + + public FileStorageUtils(Environment environment, MessageUtil messageUtil) { + this.environment = environment; + this.fileStorageLocation = Paths.get(environment.getProperty("file.directory")).toAbsolutePath().normalize(); + this.messageUtil = messageUtil; + } + + @PostConstruct + public void init() { + try { + String ftpEnabled = environment.getProperty("ftp.enabled"); + if (!StringUtils.hasLength(ftpEnabled) || !"true".equals(ftpEnabled)) { + log.info("FileStorageUtils createDirectories! ftpEnabled: {}", ftpEnabled); + Files.createDirectories(this.fileStorageLocation); + } + } catch (Exception ex) { + throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR, "Could not create the directory where the uploaded files will be stored."); + } + } + + /** + * 저장 경로 + * + * @param basePath + * @return + */ + public Path getStorePath(String basePath) { + + StringBuffer sb = new StringBuffer(); + sb.append(fileStorageLocation.toString()); + if (!basePath.equals("")) { + sb.append("/"); + sb.append(StringUtils.cleanPath(basePath)); + } + + try { + Path path = Paths.get(StringUtils.cleanPath(sb.toString())) + .toAbsolutePath().normalize(); + Files.createDirectories(path); + return path; + } catch (IOException ex) { + log.error("Could not create file store directory.", ex); + // 파일을 저장할 수 없습니다. 다시 시도해 주세요. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } + } + + + public String getContentType(String filename) { + Path filePath = this.fileStorageLocation.resolve(StringUtils.cleanPath("/" + filename)); + String mimeType = null; + try { + mimeType = Files.probeContentType(filePath); + } catch (IOException ex) { + //ignore + } + + return mimeType == null ? URLConnection.guessContentTypeFromName(filePath.toString()) : mimeType; + } + + /** + * .temp 제거 + * + * @param physicalFileName + * @return + */ + public String renameTemp(String physicalFileName) { + String rename = physicalFileName.replace(".temp", ""); + //물리적 파일 처리 + Path path = getStorePath(""); + File file = new File(path + "/" + physicalFileName); + File renameFile = new File(path + "/" + rename); + try { + file.renameTo(renameFile); + } catch (Exception ex) { + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } + return rename; + } + + /** + * base64 encoding 된 파일 저장 + * + * @param requestDto + * @param basePath + * @return + */ + public String storeBase64File(AttachmentBase64RequestDto requestDto, String basePath) { + String filename = getPhysicalFileName(requestDto.getOriginalName(), false); + + try { + Path path = getStorePath(basePath); + + File file = new File(path + "/" + filename); + + Base64.Decoder decoder = Base64.getDecoder(); + byte[] decodeBytes = decoder.decode(requestDto.getFileBase64().getBytes()); + + FileOutputStream outputStream = new FileOutputStream(file); + outputStream.write(decodeBytes); + outputStream.close(); + + return filename; + + } catch (IOException ex) { + log.error("Could not stored base 64 file.", ex); + // 파일을 저장할 수 없습니다. 다시 시도해 주세요. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } + } + + /** + * base64 encoding 된 파일 저장 + * + * @param requestDto + * @return + */ + public String storeBase64File(AttachmentBase64RequestDto requestDto) { + return storeBase64File(requestDto, ""); + } + + /** + * MultipartFile -> 물리적 파일 저장 + * + * @param file MultipartFile + * @param basePath 기본 root외 파일이 저장될 경로 + * @param isTemp .temp 파일 생성 여부 + * @return + */ + public String storeFile(MultipartFile file, String basePath, boolean isTemp) { + String filename = getPhysicalFileName(file.getOriginalFilename(), isTemp); + + try { + if (filename.contains("..")) { + log.error("Filename contains invalid path sequence : " + filename); + // 파일명이 잘못되었습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.invalid_name") + " : " + filename); + } + + Path path = getStorePath(basePath); + Path target = path.resolve(filename); + Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING); + + return filename; + } catch (IOException ex) { + log.error("Could not stored file", ex); + // 파일을 저장할 수 없습니다. 다시 시도해 주세요. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } + } + + @Override + public void storeFiles(List files, String basePath) { + } + + /** + * .temp 파일 생성하여 MultipartFile 저장 + * + * @param file + * @return + */ + public String storeFileTemp(MultipartFile file) { + return storeFile(file, "", true); + } + + /** + * .temp 파일 생성하여 MultipartFile 저장 + * + * @param file + * @param basePath + * @return + */ + public String storeFileTemp(MultipartFile file, String basePath) { + return storeFile(file, basePath, true); + } + + /** + * MultipartFile 저장. + * + * @param file + * @return + */ + public String storeFile(MultipartFile file) { + return storeFile(file, "", false); + } + + /** + * MultipartFile 저장. + * + * @param file + * @param basePath + * @return + */ + public String storeFile(MultipartFile file, String basePath) { + return storeFile(file, basePath, false); + } + + /** + * download file + * + * @param filename + * @return + */ + public Resource downloadFile(String filename) { + Path path = getStorePath(""); + try { + Path filePath = Paths.get(StringUtils.cleanPath(path.toString() + StringUtils.cleanPath("/" + filename))) + .toAbsolutePath().normalize(); + Resource resource = new UrlResource(filePath.toUri()); + + if (resource.exists()) { + return resource; + } else { + log.error("Could not found resource"); + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } + } catch (MalformedURLException ex) { + log.error("Could not found file.", ex); + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } + } + + /** + * image 태그에서 호출 시 byte 배열로 return + * + * @param imagename + * @return + * @throws IOException + */ + public AttachmentImageResponseDto loadImage(String imagename) { + try { + Path imagePath = this.fileStorageLocation.resolve(imagename).normalize(); + InputStream is = new FileInputStream(imagePath.toFile()); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int read; + byte[] data = new byte[(int) imagePath.toFile().length()]; + while ((read = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, read); + } + is.close(); + + return AttachmentImageResponseDto.builder() + .mimeType(getContentType(imagename)) + .data(data) + .build(); + } catch (FileNotFoundException | NoSuchFileException ex) { + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } catch (IOException iex) { + log.error("Could not read file.", iex); + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } + } + + /** + * 물리적 파일 삭제 + * + * @param filename + * @return + */ + public boolean deleteFile(String filename) { + Path path = getStorePath(""); + try { + Path filePath = Paths.get(StringUtils.cleanPath(path.toString() + StringUtils.cleanPath("/" + filename))) + .toAbsolutePath().normalize(); + return Files.deleteIfExists(filePath); + } catch (IOException e) { + log.error("Could not deleted file.", e); + return false; + } + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FtpClientDto.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FtpClientDto.java new file mode 100644 index 0000000..8993aa8 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FtpClientDto.java @@ -0,0 +1,93 @@ +package org.egovframe.cloud.portalservice.utils; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.apache.commons.net.ftp.FTPClient; +import org.springframework.core.env.Environment; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.utils.FtpClientDto + *

+ * FTP Client dto + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/07    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class FtpClientDto { + private String hostname; + private int port; + private String username; + private String password; + private String directory; // 기본 상위 저장 경로 + private String path; // 하위 저장 경로 + + private FTPClient ftpClient; + private MultipartFile file; + private List files; + private String filename; + + public FtpClientDto(Environment env) { + this.ftpClient = new FTPClient(); + this.hostname = env.getProperty("ftp.hostname"); + this.port = Integer.parseInt(env.getProperty("ftp.port")); + this.username = env.getProperty("ftp.username"); + this.password = env.getProperty("ftp.password"); + this.directory = env.getProperty("ftp.directory"); + } + + public FtpClientDto addFile(MultipartFile file, String path, String filename) { + this.filename = filename; + this.path = path; + this.file = file; + return this; + } + + public FtpClientDto addFiles(List files, String path) { + this.files = files; + this.path = path; + return this; + } + + /** + * 파일 저장 경로 + * + * @return + */ + public String getPathname() { + return this.directory + "/" + this.path; + } + + /** + * 파일명을 포함한 원격 경로 + * + * @return + */ + public String getRemote() { + return getPathname() + "/" + this.filename; + } + + @Override + public String toString() { + return "FtpClientDto{" + + "hostname='" + hostname + '\'' + + ", username='" + username + '\'' + + ", password='" + password + '\'' + + ", directory='" + directory + '\'' + + '}'; + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FtpStorageUtils.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FtpStorageUtils.java new file mode 100644 index 0000000..78e62d7 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/FtpStorageUtils.java @@ -0,0 +1,451 @@ +package org.egovframe.cloud.portalservice.utils; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.commons.net.ftp.FTP; +import org.apache.commons.net.ftp.FTPClient; +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.util.MessageUtil; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentBase64RequestDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentImageResponseDto; +import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import java.io.*; +import java.net.MalformedURLException; +import java.net.SocketException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +import static org.egovframe.cloud.portalservice.utils.PortalUtils.getPhysicalFileName; + +/** + * org.egovframe.cloud.portalservice.utils.FtpStorageUtils + *

+ * FTP 서버에 파일을 관리한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/09    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@Getter +@Component +public class FtpStorageUtils implements StorageUtils { + + private final Environment environment; + private final MessageUtil messageUtil; + + public FtpStorageUtils(Environment environment, MessageUtil messageUtil) { + this.environment = environment; + this.messageUtil = messageUtil; + } + + /** + * FTP 서버에 최상위 디렉토리 생성 + */ + @PostConstruct + public void init() { + try { + String ftpEnabled = environment.getProperty("ftp.enabled"); + // ftp server 사용하는 환경에서만 처리 + if (StringUtils.hasLength(ftpEnabled) && "true".equals(ftpEnabled)) { + FtpClientDto ftpClientDto = new FtpClientDto(environment); + this.connect(ftpClientDto); + FTPClient ftpClient = ftpClientDto.getFtpClient(); + + // 업로드 기본 디렉토리 생성 및 권한 부여 + String rootDir = ftpClientDto.getDirectory(); + makePermissionDirectory(ftpClient, rootDir); + + // editor, messages 디렉토리 생성 및 권한 부여 + makePermissionDirectory(ftpClient, rootDir + "/editor"); + makePermissionDirectory(ftpClient, rootDir + "/messages"); + + this.disconnect(ftpClient); + } + } catch (Exception ex) { + throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR, "Could not create the directory where the uploaded files will be stored."); + } + } + + private void makePermissionDirectory(FTPClient ftpClient, String directory) throws IOException { + // 디렉토리 생성 + ftpClient.makeDirectory(directory); + ftpClient.sendSiteCommand("chmod " + "755 " + directory); + } + + private void setPermission(FTPClient ftpClient, String path) throws IOException { + ftpClient.sendSiteCommand("chmod " + "644 " + path); + } + + /** + * FTP 서버에 접속한다 + * + * @param ftpClientDto + * @return + */ + private FtpClientDto connect(FtpClientDto ftpClientDto) { + FTPClient ftpClient = ftpClientDto.getFtpClient(); + + try { + // connect + ftpClient.setControlEncoding(StandardCharsets.UTF_8.name()); + ftpClient.setConnectTimeout(3000); + ftpClient.connect(ftpClientDto.getHostname(), ftpClientDto.getPort()); + + boolean login = ftpClient.login(ftpClientDto.getUsername(), ftpClientDto.getPassword()); + log.info("FTPClient login: {}", login); + ftpClient.setFileType(FTP.BINARY_FILE_TYPE); + ftpClient.setFileTransferMode(FTP.BINARY_FILE_TYPE); + ftpClient.enterLocalPassiveMode(); + + } catch (SocketException e) { + log.error("FTPClient SocketException connect", e); + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } catch (IOException e) { + log.error("FTPClient IOException connect", e); + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } + + return ftpClientDto; + } + + /** + * FTP 서버 접속을 종료한다 + * + * @param ftpClient + */ + private void disconnect(FTPClient ftpClient) { + // logout & disconnect + if (ftpClient.isConnected()) { + try { + ftpClient.logout(); + ftpClient.disconnect(); + } catch (IOException e) { + log.error("FTPClient IOException disconnect", e); + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } + } + } + + /** + * FTP 서버에 접속하여 파일을 저장한 후 접속을 종료한다 + * + * @param ftpClientDto + * @param isList + */ + public void storeFile(FtpClientDto ftpClientDto, boolean isList) { + FTPClient ftpClient = ftpClientDto.getFtpClient(); + try { + // connect + this.connect(ftpClientDto); + + // 디렉토리 생성 및 권한 부여 + this.makePermissionDirectory(ftpClient, ftpClientDto.getPathname()); + + // upload + if (isList) { + List files = ftpClientDto.getFiles(); + for (File file : files) { + try (InputStream inputStream = new FileInputStream(file)) { + String remote = ftpClientDto.getPathname() + "/" + file.getName(); +// String remote = "/mnt/messages/" + file.getName(); + boolean storeFile = ftpClient.storeFile(remote, inputStream); + // 파일 권한 부여 + this.setPermission(ftpClient, remote); + log.info("FTPClient storeFile '{}' SUCCESS? {}", remote, storeFile); + } + } + } else { + MultipartFile file = ftpClientDto.getFile(); + try (InputStream inputStream = file.getInputStream()) { + String remote = ftpClientDto.getRemote(); + boolean storeFile = ftpClient.storeFile(remote, inputStream); + // 파일 권한 부여 + this.setPermission(ftpClient, remote); + log.info("FTPClient storeFile remote={}, SUCCESS? {}", remote, storeFile); + } + } + + } catch (Exception e) { + log.error("FTPClient Exception", e); + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } finally { + // logout & disconnect + this.disconnect(ftpClient); + } + } + + /** + * MultipartFile -> 물리적 파일 저장 + * + * @param file MultipartFile + * @param basePath 기본 root외 파일이 저장될 경로 + * @param isTemp .temp 파일 생성 여부 + * @return + */ + public String storeFile(MultipartFile file, String basePath, boolean isTemp) { + String filename = getPhysicalFileName(file.getOriginalFilename(), isTemp); + + if (filename.contains("..")) { + log.error("Filename contains invalid path sequence : " + filename); + // 파일명이 잘못되었습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.invalid_name") + " : " + filename); + } + + FtpClientDto ftpClientDto = new FtpClientDto(environment); + ftpClientDto.addFile(file, basePath, filename); + this.storeFile(ftpClientDto, false); + + return filename; + } + + /** + * 여러 파일 업로드 + * + * @param files + * @param basePath + */ + @Override + public void storeFiles(List files, String basePath) { + FtpClientDto ftpClientDto = new FtpClientDto(environment); + ftpClientDto.addFiles(files, basePath); + this.storeFile(ftpClientDto, true); + } + + /** + * .temp 파일 생성하여 MultipartFile 저장 + * + * @param file + * @return + */ + public String storeFileTemp(MultipartFile file) { + return storeFile(file, "", true); + } + + /** + * .temp 파일 생성하여 MultipartFile 저장 + * + * @param file + * @param basePath + * @return + */ + public String storeFileTemp(MultipartFile file, String basePath) { + return storeFile(file, basePath, true); + } + + /** + * MultipartFile 저장. + * + * @param file + * @return + */ + public String storeFile(MultipartFile file) { + return storeFile(file, "", false); + } + + /** + * MultipartFile 저장. + * + * @param file + * @param basePath + * @return + */ + public String storeFile(MultipartFile file, String basePath) { + return storeFile(file, basePath, false); + } + + + /** + * base64 encoding 된 파일 저장 + * + * @param requestDto + * @param basePath + * @return + */ + public String storeBase64File(AttachmentBase64RequestDto requestDto, String basePath) { + try { + // ftp connect + FtpClientDto ftpClientDto = new FtpClientDto(environment); + this.connect(ftpClientDto); + FTPClient ftpClient = ftpClientDto.getFtpClient(); + + String filename = getPhysicalFileName(requestDto.getOriginalName(), false); + Base64.Decoder decoder = Base64.getDecoder(); + byte[] decodeBytes = decoder.decode(requestDto.getFileBase64().getBytes()); + +// FileOutputStream outputStream = new FileOutputStream(file); + // 디렉토리 생성 및 권한 부여 + String directory = ftpClientDto.getDirectory() + StringUtils.cleanPath("/" + basePath); + this.makePermissionDirectory(ftpClient, directory); + + String remote = directory + StringUtils.cleanPath("/" + filename); + OutputStream outputStream = ftpClient.storeFileStream(remote); + outputStream.write(decodeBytes); + outputStream.close(); + + // 파일 권한 부여 + this.setPermission(ftpClient, remote); + // 접속 종료 + this.disconnect(ftpClient); + + return filename; + + } catch (IOException ex) { + log.error("Could not stored base 64 file.", ex); + // 파일을 저장할 수 없습니다. 다시 시도해 주세요. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } + } + + /** + * base64 encoding 된 파일 저장 + * + * @param requestDto + * @return + */ + public String storeBase64File(AttachmentBase64RequestDto requestDto) { + return storeBase64File(requestDto, ""); + } + + + /** + * download file + * + * @param filename + * @return + */ + public Resource downloadFile(String filename) { + try { + Resource resource = new UrlResource(environment.getProperty("file.url") + StringUtils.cleanPath("/" + filename)); + + if (resource.exists()) { + return resource; + } else { + log.error("Could not found resource"); + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } + } catch (MalformedURLException ex) { + log.error("Could not found file.", ex); + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } + } + + /** + * image 태그에서 호출 시 byte 배열로 return + * + * @param imagename + * @return + * @throws IOException + */ + public AttachmentImageResponseDto loadImage(String imagename) { + try { + Resource resource = new UrlResource(environment.getProperty("file.url") + StringUtils.cleanPath("/" + imagename)); + InputStream inputStream = new URL(resource.getURL().toString()).openStream(); + + byte[] data = IOUtils.toByteArray(inputStream); + inputStream.close(); + + // get mime type + URLConnection connection = new URL(resource.getURL().toString()).openConnection(); + String contentType = connection.getContentType(); + + return AttachmentImageResponseDto.builder() + .mimeType(contentType) + .data(data) + .build(); + } catch (FileNotFoundException | NoSuchFileException ex) { + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } catch (IOException iex) { + log.error("Could not read file.", iex); + // 파일을 찾을 수 없습니다. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_found")); + } + } + + /** + * 물리적 파일 삭제 + * + * @param filename + * @return + */ + public boolean deleteFile(String filename) { + try { + FtpClientDto ftpClientDto = new FtpClientDto(environment); + this.connect(ftpClientDto); + FTPClient ftpClient = ftpClientDto.getFtpClient(); + // 삭제 + ftpClient.deleteFile(ftpClientDto.getDirectory() + StringUtils.cleanPath("/" + filename)); + this.disconnect(ftpClient); + return true; + } catch (Exception e) { + log.error("Could not deleted file.", e); + return false; + } + } + + /** + * .temp 제거 + * + * @param physicalFileName + * @return + */ + @Deprecated + public String renameTemp(String physicalFileName) { + return physicalFileName; + } + + /** + * 저장 경로 + * + * @param basePath + * @return + */ + @Override + public Path getStorePath(String basePath) { + try { + FtpClientDto ftpClientDto = new FtpClientDto(environment); + this.connect(ftpClientDto); + FTPClient ftpClient = ftpClientDto.getFtpClient(); + + // 디렉토리 생성 및 권한 부여 + String directory = StringUtils.cleanPath(ftpClientDto.getDirectory() + "/" + basePath); + makePermissionDirectory(ftpClient, directory); + + this.disconnect(ftpClient); + + return Paths.get(directory).toAbsolutePath().normalize(); + } catch (IOException ex) { + log.error("Could not create file store directory.", ex); + // 파일을 저장할 수 없습니다. 다시 시도해 주세요. + throw new BusinessMessageException(messageUtil.getMessage("valid.file.not_saved_try_again")); + } + } +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/PortalUtils.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/PortalUtils.java new file mode 100644 index 0000000..b4952d2 --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/PortalUtils.java @@ -0,0 +1,69 @@ +package org.egovframe.cloud.portalservice.utils; + +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; +import java.util.UUID; + + +/** + * org.egovframe.cloud.portalservice.utils.PortalUtils + *

+ * 포털 서비스 사용하는 유틸 클래스 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/09    jaeyeolkim  최초 생성
+ * 
+ */ +public class PortalUtils { + + /** + * '-'을 제거한 uuid 생성 + * + * @return + */ + public static String getUUID() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } + + /** + * 물리적 파일 이름 생성 + * + * @param originalFileName + * @param isTemp + * @return + */ + public static String getPhysicalFileName(String originalFileName, boolean isTemp) { + String ext = StringUtils.getFilenameExtension(originalFileName); + StringBuffer sb = new StringBuffer(); + sb.append(getUUID()); + sb.append("."); + sb.append(ext); + if(isTemp){ + sb.append(".temp"); + } + return StringUtils.cleanPath(sb.toString()); + } + + /** + * 물리적 파일 이름 생성 (.temp) + * + * @param originalFileName + * @return + */ + public static String getPhysicalFileName(String originalFileName) { + return getPhysicalFileName(originalFileName, true); + } + +} diff --git a/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/StorageUtils.java b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/StorageUtils.java new file mode 100644 index 0000000..42b006d --- /dev/null +++ b/backend/portal-service/src/main/java/org/egovframe/cloud/portalservice/utils/StorageUtils.java @@ -0,0 +1,102 @@ +package org.egovframe.cloud.portalservice.utils; + +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentBase64RequestDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentImageResponseDto; +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.utils.StorageUtils + *

+ * StorageUtils + * ftp 서버 사용 여부에 따라 StorageUtils 에 주입하는 빈이 달라진다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/09/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/08    jaeyeolkim  최초 생성
+ * 
+ */ +public interface StorageUtils { + + /** + * MultipartFile -> 물리적 파일 저장 + * + * @param file MultipartFile + * @param basePath 기본 root외 파일이 저장될 경로 + * @param isTemp .temp 파일 생성 여부 + * @return + */ + String storeFile(MultipartFile file, String basePath, boolean isTemp); + + /** + * file 저장 + * + * @param files + * @param basePath + * @return + */ + void storeFiles(List files, String basePath); + + + /** + * base64 encoding 된 파일 저장 + * + * @param requestDto + * @param basePath + * @return + */ + String storeBase64File(AttachmentBase64RequestDto requestDto, String basePath); + + /** + * image 태그에서 호출 시 byte 배열로 return + * + * @param imagename + * @return + * @throws IOException + */ + AttachmentImageResponseDto loadImage(String imagename); + + /** + * download file + * + * @param filename + * @return + */ + Resource downloadFile(String filename); + + /** + * .temp 제거 + * + * @param physicalFileName + * @return + */ + String renameTemp(String physicalFileName); + + /** + * 물리적 파일 삭제 + * + * @param filename + * @return + */ + boolean deleteFile(String filename); + + /** + * 저장 경로 + * + * @param basePath + * @return + */ + public Path getStorePath(String basePath); +} diff --git a/backend/portal-service/src/main/resources/application.yml b/backend/portal-service/src/main/resources/application.yml new file mode 100644 index 0000000..6e68eff --- /dev/null +++ b/backend/portal-service/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 0 # random port + +spring: + application: + name: portal-service + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL57Dialect + storage_engine: innodb + format_sql: true + default_batch_fetch_size: 1000 + show-sql: true + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 50MB + +# config server actuator +management: + endpoints: + web: + exposure: + include: refresh, health, beans + +logging: + level: + org.egovframe.cloud.portalservice.client: DEBUG # Feign log를 확인하기 위해 해당 패키지를 디버그 모드로 설정 diff --git a/backend/portal-service/src/main/resources/bootstrap.yml b/backend/portal-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..aa350be --- /dev/null +++ b/backend/portal-service/src/main/resources/bootstrap.yml @@ -0,0 +1,8 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: portal-service # portal-service.yml이 있으면 불러오게 된다 +# name: config-service # config-service의 application.yml 을 불러오게 된다 +# profiles: +# active: prod # application-prod.yml \ No newline at end of file diff --git a/backend/portal-service/src/main/resources/logback-spring.xml b/backend/portal-service/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..03aa0f4 --- /dev/null +++ b/backend/portal-service/src/main/resources/logback-spring.xml @@ -0,0 +1,34 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n + + + + + + + + + + + + + + + + + ${destination} + + {"app.name":"${app_name}"} + + + + + + + + + \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/PortalServiceApplicationTests.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/PortalServiceApplicationTests.java new file mode 100644 index 0000000..908dcdc --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/PortalServiceApplicationTests.java @@ -0,0 +1,13 @@ +package org.egovframe.cloud.portalservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class PortalServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/attachment/AttachmentApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/attachment/AttachmentApiControllerTest.java new file mode 100644 index 0000000..09db073 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/attachment/AttachmentApiControllerTest.java @@ -0,0 +1,593 @@ +package org.egovframe.cloud.portalservice.api.attachment; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.util.encoders.Base64; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentBase64RequestDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentEditorResponseDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentFileResponseDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentResponseDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentTempSaveRequestDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentUpdateRequestDto; +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentUploadRequestDto; +import org.egovframe.cloud.portalservice.domain.attachment.Attachment; +import org.egovframe.cloud.portalservice.domain.attachment.AttachmentRepository; +import org.egovframe.cloud.portalservice.service.attachment.AttachmentService; +import org.egovframe.cloud.portalservice.util.RestResponsePage; +import org.egovframe.cloud.portalservice.utils.FileStorageUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class AttachmentApiControllerTest { + @LocalServerPort + private int port; + + @Autowired + TestRestTemplate restTemplate; + + @Autowired + FileStorageUtils fileStorageUtils; + + @Autowired + AttachmentService attachmentService; + + @Autowired + AttachmentRepository attachmentRepository; + + @AfterEach + public void teardown() { + List all = attachmentRepository.findAll(); + + for (int i = 0; i < all.size(); i++) { + Attachment attachment = all.get(i); + attachmentService.delete(attachment.getUniqueId()); + } + } + + /** + * file to byte[] + * + * @param file + * @return + */ + public byte[] getByteFile(File file) { + byte[] data = new byte[(int) file.length()]; + try { + FileInputStream inputStream = new FileInputStream(file); + inputStream.read(data, 0, data.length); + inputStream.close(); + + } catch (FileNotFoundException e) { + log.debug("file not found = {}", e); + } catch (IOException e) { + log.debug("file IO exception = {}", e); + } + return data; + } + + /** + * test.txt 파일 생성 + * + * @return + * @throws IOException + */ + public static Resource getTestFile() throws IOException { + Path testFile = Files.createTempFile("test-file", ".txt"); + System.out.println("Creating and Uploading Test File: " + testFile); + Files.write(testFile, "Hello World !!, This is a test file.".getBytes()); + testFile.toFile().deleteOnExit(); + return new FileSystemResource(testFile.toFile()); + } + + /** + * 하나의 멀티파트 파일 생성 + * + * @return + * @throws IOException + */ + private MultipartFile getMultipartFile() throws IOException { + Resource resource = getTestFile(); + //String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content + return new MockMultipartFile("files", resource.getFilename(), + Files.probeContentType(resource.getFile().toPath()), resource.getInputStream()); + } + + /** + * 여러 건의 멀티파트 파일 생성 + * + * @param size + * @return + * @throws IOException + */ + private List getMultipartFileList(int size) throws IOException { + List multipartFiles = new ArrayList<>(); + for (int i = 0; i < size; i++) { + multipartFiles.add(getMultipartFile()); + } + + return multipartFiles; + } + + + /** + * 여러 건의 .temp 파일 생성 후 AttachmentSaveRequestDto List return + * + * @param size + * @return + * @throws IOException + */ + private List getTempSaveDto(int size) throws IOException { + List multipartFiles = getMultipartFileList(size); + List responseDtos = attachmentService.uploadFiles(multipartFiles); + + List saveRequestDtoList = new ArrayList<>(); + for (int i = 0; i < responseDtos.size(); i++) { + AttachmentFileResponseDto responseDto = responseDtos.get(i); + saveRequestDtoList.add(AttachmentTempSaveRequestDto.builder() + .physicalFileName(responseDto.getPhysicalFileName()) + .originalName(responseDto.getOriginalFileName()) + .size(responseDto.getSize()) + .fileType(responseDto.getFileType()) + .entityName("Policy") + .entityId("testEntityId_"+i) + .build() + ); + } + + return saveRequestDtoList; + } + + + @Test + public void 이미지_BASE64인코딩후_업로드_정상() throws Exception { + //given + String url = "/api/v1/upload/editor"; + + Path testFile = Paths.get("/Users/violet/Desktop/test/300.jpg") + .toAbsolutePath().normalize(); + + String base64data = Base64.toBase64String(getByteFile(testFile.toFile())); + AttachmentBase64RequestDto requestDto = AttachmentBase64RequestDto.builder() + .fieldName("upload") + .fileType("image/jpg") + .fileBase64(base64data) + .originalName("300.jpg") + .size(testFile.toFile().length()) + .build(); + + + ResponseEntity responseEntity = + restTemplate.postForEntity(url, requestDto, AttachmentEditorResponseDto.class); + + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody().getOriginalFileName()).isEqualTo("300.jpg"); + } + + @Test + public void 첨부파일_싱글_업로드_정상() throws Exception { + //given + String url = "/api/v1/upload"; + ObjectMapper objectMapper = new ObjectMapper(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", getTestFile()); + + HttpEntity> requestEntity = new HttpEntity(body, headers); + + //when + ResponseEntity responseEntity = + restTemplate.postForEntity(url, requestEntity, AttachmentFileResponseDto.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + } + + @Test + public void 첨부파일_멀티_업로드_정상() throws Exception { + //given + String url = "/api/v1/upload/multi"; + ObjectMapper objectMapper = new ObjectMapper(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("files", getTestFile()); + body.add("files", getTestFile()); + body.add("files", getTestFile()); + + HttpEntity> requestEntity = new HttpEntity(body, headers); + + //when + ResponseEntity> responseEntity = + restTemplate.exchange(url, HttpMethod.POST, requestEntity, + new ParameterizedTypeReference>() {}); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + } + + @Test + public void 에디터이미지업로드_후_이미지태그에서_이미지파일_조회_정상() throws Exception { + //given + Path testFile = Paths.get("/Users/violet/Desktop/test/300.jpg") + .toAbsolutePath().normalize(); + + String base64data = Base64.toBase64String(getByteFile(testFile.toFile())); + AttachmentBase64RequestDto requestDto = AttachmentBase64RequestDto.builder() + .fieldName("upload") + .fileType("image/jpg") + .fileBase64(base64data) + .originalName("300.jpg") + .size(testFile.toFile().length()) + .build(); + AttachmentEditorResponseDto responseDto = attachmentService.uploadEditor(requestDto); + + String url = "/api/v1/images/editor/"+responseDto.getUrl(); + + //when + ResponseEntity responseEntity = restTemplate.getForEntity(url, byte[].class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void 새로운_첨부파일_temp파일_목록_저장_정상() throws Exception { + //given + List saveRequestDtoList = getTempSaveDto(2); + + String url = "/api/v1/attachments/temp"; + + //when + ResponseEntity responseEntity = restTemplate.postForEntity(url, saveRequestDtoList, String.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void 첨부파일코드로_목록조회() throws Exception { + //given + List saveRequestDtoList = getTempSaveDto(2); + String attachmentCode = attachmentService.save(saveRequestDtoList); + + String url = "/api/v1/attachments/"+attachmentCode; + + //when + ResponseEntity> responseEntity = + restTemplate.exchange(url, HttpMethod.GET, null, + new ParameterizedTypeReference>() {}); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody().size()).isEqualTo(2); + } + + @Test + public void 첨부파일코드가_있는_경우_temp파일에대해_새로운파일저장_and_삭제여부_Y_정상() throws Exception { + //given + List saveRequestDtoList = getTempSaveDto(3); + String attachmentCode = attachmentService.save(saveRequestDtoList); + List attachmentList = attachmentService.findByCode(attachmentCode); + + List updateRequestDtoList = new ArrayList<>(); + + //짝수 index 첨부파일 삭제 = Y + for (int i = 0; i < attachmentList.size(); i++) { + AttachmentResponseDto attachmentResponseDto = attachmentList.get(i); + updateRequestDtoList.add( + AttachmentTempSaveRequestDto.builder() + .uniqueId(attachmentResponseDto.getId()) + .physicalFileName(attachmentResponseDto.getPhysicalFileName()) + .originalName(attachmentResponseDto.getOriginalFileName()) + .size(attachmentResponseDto.getSize()) + .entityName(attachmentResponseDto.getEntityName()) + .entityId(attachmentResponseDto.getEntityId()) + .isDelete(i%2==0) + .build() + ); + } + + //2개 첨부파일 더하기 + updateRequestDtoList.addAll(getTempSaveDto(2)); + + HttpEntity> requestEntity = new HttpEntity<>(updateRequestDtoList); + + //when + String url = "/api/v1/attachments/temp/"+attachmentCode; + + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, String.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void 관리자_첨부파일_목록_조회_정상() throws Exception { + //given + List saveRequestDtoList1 = getTempSaveDto(2); + attachmentService.save(saveRequestDtoList1); + + List saveRequestDtoList2 = getTempSaveDto(3); + attachmentService.save(saveRequestDtoList2); + + String url = "/api/v1/attachments/admin"; + + //when + ResponseEntity> responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + RestResponsePage page = responseEntity.getBody(); + assertThat(page).isNotNull(); + assertThat(page.getTotalElements()).isEqualTo(5); + + } + + @Test + public void 관리자_첨부파일_목록_검색조회_정상() throws Exception { + //given + List saveRequestDtoList1 = getTempSaveDto(2); + attachmentService.save(saveRequestDtoList1); + + List saveRequestDtoList2 = getTempSaveDto(3); + String attachmentCode = attachmentService.save(saveRequestDtoList2); + + String url = "/api/v1/attachments/admin?keywordType=id&keyword="+attachmentCode; + + //when + ResponseEntity> responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + RestResponsePage page = responseEntity.getBody(); + assertThat(page).isNotNull(); + assertThat(page.getTotalElements()).isEqualTo(3); + + } + + @Test + public void 관리자_삭제여부_Y_토글_정상() throws Exception { + //given + List saveRequestDtoList2 = getTempSaveDto(3); + String attachmentCode = attachmentService.save(saveRequestDtoList2); + + List results = attachmentService.findByCode(attachmentCode); + + String uniqueId = results.get(1).getId(); + String url = "/api/v1/attachments/admin/"+uniqueId+"/true"; + + //when + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, null, String.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List saved = attachmentService.findByCode(attachmentCode); + AttachmentResponseDto updated = saved.stream() + .filter(attachmentResponseDto -> attachmentResponseDto.getId().equals(uniqueId)) + .findAny().get(); + assertThat(updated.getIsDelete()).isTrue(); + } + + @Test + public void 관리자_첨부파일_한건_완전삭제_정상() throws Exception { + //given + List saveRequestDtoList2 = getTempSaveDto(2); + String attachmentCode = attachmentService.save(saveRequestDtoList2); + List results = attachmentService.findByCode(attachmentCode); + + String url = "/api/v1/attachments/admin/"+results.get(1).getId(); + //when + restTemplate.delete(url); + + //then + List deleted = attachmentService.findByCode(attachmentCode); + assertThat(deleted.size()).isEqualTo(1); + } + + @Test + public void 첨부파일_업로드_저장_정상() throws Exception { + //given + String url = "/api/v1/attachments/upload"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("files", getTestFile()); + body.add("files", getTestFile()); + body.add("files", getTestFile()); + + AttachmentUploadRequestDto uploadRequestDto = + AttachmentUploadRequestDto.builder() + .entityName("test") + .entityId("testid") + .build(); + body.add("info", uploadRequestDto); + + HttpEntity> requestEntity = new HttpEntity(body, headers); + + //when + ResponseEntity responseEntity = restTemplate.postForEntity(url, requestEntity, String.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List attachmentList = attachmentRepository.findByCode(responseEntity.getBody()); + attachmentList.stream().forEach(attachment -> { + Path filePath = Paths.get(fileStorageUtils.getFileStorageLocation()+"/" +attachment.getPhysicalFileName()) + .toAbsolutePath().normalize(); + assertThat(Files.exists(filePath)); + }); + } + + @Test + public void 첨부파일코드가_있는_경우_새로운파일_업로드_및_저장_and_삭제여부_Y_정상() throws Exception { + //given + List multipartFiles = getMultipartFileList(3); + AttachmentUploadRequestDto uploadRequestDto = + AttachmentUploadRequestDto.builder() + .entityName("test") + .entityId("testid") + .build(); + String attachmentCode = attachmentService.uploadAndSave(multipartFiles, uploadRequestDto); + List attachmentList = attachmentService.findByCode(attachmentCode); + + List saveRequestDtoList = new ArrayList<>(); + + //모두 삭제 = Y + for (int i = 0; i < attachmentList.size(); i++) { + AttachmentResponseDto attachmentResponseDto = attachmentList.get(i); + saveRequestDtoList.add( + AttachmentUpdateRequestDto.builder() + .uniqueId(attachmentResponseDto.getId()) + .isDelete(true) + .build() + ); + } + + saveRequestDtoList.stream().forEach(System.out::println); + //2개 첨부파일 더하기 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("files", getTestFile()); + body.add("files", getTestFile()); + body.add("info", uploadRequestDto); + body.add("list", saveRequestDtoList); + + HttpEntity> requestEntity = new HttpEntity(body, headers); + + //when + String url = "/api/v1/attachments/upload/"+attachmentCode; + + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, String.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List updateAttachments = attachmentRepository.findByCode(responseEntity.getBody()); + updateAttachments.stream().forEach(System.out::println); + + + } + + @Test + public void 첨부파일업로드없이_기존파일삭제여부_Y_정상() throws Exception { + //given + List multipartFiles = getMultipartFileList(3); + AttachmentUploadRequestDto uploadRequestDto = + AttachmentUploadRequestDto.builder() + .entityName("test") + .entityId("testid") + .build(); + String attachmentCode = attachmentService.uploadAndSave(multipartFiles, uploadRequestDto); + List attachmentList = attachmentService.findByCode(attachmentCode); + List saveRequestDtoList = new ArrayList<>(); + //모두 삭제 = Y + for (int i = 0; i < attachmentList.size(); i++) { + AttachmentResponseDto attachmentResponseDto = attachmentList.get(i); + saveRequestDtoList.add( + AttachmentUpdateRequestDto.builder() + .uniqueId(attachmentResponseDto.getId()) + .isDelete(true) + .build() + ); + } + + saveRequestDtoList.stream().forEach(System.out::println); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("info", uploadRequestDto); + body.add("list", saveRequestDtoList); + HttpEntity> requestEntity = new HttpEntity(body, headers); + + //when + String url = "/api/v1/attachments/"+attachmentCode; + + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, String.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List updateAttachments = attachmentRepository.findByCode(responseEntity.getBody()); + updateAttachments.stream().forEach(System.out::println); + + } + + +// +// @Test +// public void 첨부파일_다운로드_정상() throws Exception { +// //given +// List saveRequestDtoList2 = getTempSaveDto(1); +// String attachmentCode = attachmentService.save(saveRequestDtoList2); +// +// List byCode = attachmentService.findByCode(attachmentCode); +// +// String uniqueId = byCode.get(0).getUniqueId(); +// String url = "/api/v1/download/"+uniqueId; +// +// //when +// ResponseEntity responseEntity = restTemplate.getForEntity(url, ResponseEntity.class); +// +// //then +// re +// +// } + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/banner/BannerApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/banner/BannerApiControllerTest.java new file mode 100644 index 0000000..9c56a5a --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/banner/BannerApiControllerTest.java @@ -0,0 +1,459 @@ +package org.egovframe.cloud.portalservice.api.banner; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.lang.StringUtils; +import org.assertj.core.api.Condition; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerImageResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerListResponseDto; +import org.egovframe.cloud.portalservice.api.banner.dto.BannerResponseDto; +import org.egovframe.cloud.portalservice.domain.banner.Banner; +import org.egovframe.cloud.portalservice.domain.banner.BannerRepository; +import org.egovframe.cloud.portalservice.domain.menu.Site; +import org.egovframe.cloud.portalservice.domain.menu.SiteRepository; +import org.egovframe.cloud.portalservice.util.RestResponsePage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +/** + * org.egovframe.cloud.portalservice.api.banner.BannerApiControllerTest + *

+ * 배너 Rest API 컨트롤러 테스트 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/08/18 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/18    jooho       최초 생성
+ * 
+ */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class BannerApiControllerTest { + + /** + * test rest template + */ + @Autowired + TestRestTemplate restTemplate; + + /** + * 배너 레파지토리 인터페이스 + */ + @Autowired + BannerRepository bannerRepository; + + /** + * 사이트 레파지토리 인터페이스 + */ + @Autowired + SiteRepository siteRepository; + + /** + * 배너 API 경로 + */ + private static final String URL = "/api/v1/banners"; + + /** + * 테스트 데이터 등록 횟수 + */ + private final Integer GIVEN_DATA_COUNT = 10; + + /** + * 테스트 데이터 + */ + private final String BANNER_TYPE_CODE_PREFIX = "000"; + private final String BANNER_TITLE_PREFIX = "배너 제목"; + private final String ATTACHMENT_CODE_PREFIX = "000000000"; + private final String URL_ADDR_PREFIX = "http://localhost:8000"; + private final String BANNER_CONTENT_PREFIX = "배너 내용"; + + private final Integer BANNER_NO = GIVEN_DATA_COUNT + 1; + private final String INSERT_BANNER_TYPE_CODE = BANNER_TYPE_CODE_PREFIX + BANNER_NO; + private final String INSERT_BANNER_TITLE = BANNER_TITLE_PREFIX + "_" + BANNER_NO; + private final String INSERT_ATTACHMENT_CODE = ATTACHMENT_CODE_PREFIX + BANNER_NO; + private final String INSERT_URL_ADDR = URL_ADDR_PREFIX + BANNER_NO; + private final String INSERT_BANNER_CONTENT = BANNER_CONTENT_PREFIX + "_" + BANNER_NO; + private final Boolean INSERT_NEW_WINDOW_AT = true; + private final Integer INSERT_SORT_SEQ = 1; + + private final String UPDATE_BANNER_TYPE_CODE = BANNER_TYPE_CODE_PREFIX + (BANNER_NO + 1); + private final String UPDATE_BANNER_TITLE = BANNER_TITLE_PREFIX + "_" + (BANNER_NO + 1); + private final String UPDATE_ATTACHMENT_CODE = ATTACHMENT_CODE_PREFIX + (BANNER_NO + 1); + private final String UPDATE_URL_ADDR = URL_ADDR_PREFIX + (BANNER_NO + 1); + private final String UPDATE_BANNER_CONTENT = BANNER_CONTENT_PREFIX + "_" + (BANNER_NO + 1); + private final Boolean UPDATE_NEW_WINDOW_AT = true; + private final Integer UPDATE_SORT_SEQ = 2; + + /** + * 테스트 데이터 + */ + private Site site; + private final List banners = new ArrayList<>(); + + /** + * 테스트 시작 전 수행 + */ + @BeforeEach + void setUp() { + // 사이트 등록 + site = siteRepository.save(Site.builder() + .name("TEST_SITE") + .isUse(true) + .sortSeq(1) + .build()); + } + + /** + * 테스트 종료 후 수행 + */ + @AfterEach + void tearDown() { + // 배너 삭제 + bannerRepository.deleteAll(); + banners.clear(); + + // 사이트 삭제 + siteRepository.deleteAll(); + } + + /** + * 배너 페이지 목록 조회 테스트 + */ + @Test + void 배너_페이지_목록_조회() { + // given + insertBanners(); + + String queryString = "?keywordType=bannerName&keyword=" + BANNER_TITLE_PREFIX; // 검색 조건 + queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보 + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + URL + queryString, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + RestResponsePage page = responseEntity.getBody(); + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT); + assertThat(page.getContent()) + .isNotEmpty() + .has(new Condition<>(l -> (BANNER_TITLE_PREFIX + "_1").equals(l.get(0).getBannerTitle()), "BannerApiControllerTest.findPage contains " + BANNER_TITLE_PREFIX + "_1")) + .has(new Condition<>(l -> (BANNER_TITLE_PREFIX + "_2").equals(l.get(1).getBannerTitle()), "BannerApiControllerTest.findPage contains " + BANNER_TITLE_PREFIX + "_2")); + } + + /** + * 유형별 배너 목록 조회 테스트 + */ + @Test + void 유형별_배너_목록_조회() { + // given + insertBanners(); + + // when + ResponseEntity>> responseEntity = restTemplate.exchange( + "/api/v1/" + site.getId() + "/banners/0001,0002/3", + HttpMethod.GET, + null, + new ParameterizedTypeReference>>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Map> data = responseEntity.getBody(); + assertThat(data).isNotNull(); + assertThat(data.get("0001").size()).isEqualTo(1); // 사용중인 0001타입 배너 1개 + } + + /** + * 배너 상세 조회 테스트 + */ + @Test + void 배너_상세_조회() { + // given + Banner entity = insertBanner(); + + final Integer bannerNo = entity.getBannerNo(); + + String url = URL + "/" + bannerNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + BannerResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + assertThat(dto.getSiteId()).isEqualTo(site.getId()); + assertThat(dto.getBannerNo()).isEqualTo(bannerNo); + assertThat(dto.getBannerTypeCode()).isEqualTo(INSERT_BANNER_TYPE_CODE); + assertThat(dto.getBannerTitle()).isEqualTo(INSERT_BANNER_TITLE); + assertThat(dto.getAttachmentCode()).isEqualTo(INSERT_ATTACHMENT_CODE); + assertThat(dto.getUrlAddr()).isEqualTo(INSERT_URL_ADDR); + assertThat(dto.getBannerContent()).isEqualTo(INSERT_BANNER_CONTENT); + } + + /** + * 배너 다음 정렬 순서 조회 테스트 + */ + @Test + void 배너_다음_정렬_순서_조회() { + // given + Banner entity = insertBanner(); + + String url = URL + "/" + site.getId() + "/sort-seq/next"; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Integer nextSortSeq = responseEntity.getBody(); + assertThat(nextSortSeq).isNotNull(); + assertThat(nextSortSeq).isEqualTo(entity.getSortSeq() + 1); + } + + /** + * 배너 등록 테스트 + */ + @Test + void 배너_등록() { + // given + Map params = new HashMap<>(); + params.put("siteId", site.getId()); + params.put("bannerTypeCode", INSERT_BANNER_TYPE_CODE); + params.put("bannerTitle", INSERT_BANNER_TITLE); + params.put("attachmentCode", INSERT_ATTACHMENT_CODE); + params.put("urlAddr", INSERT_URL_ADDR); + params.put("bannerContent", INSERT_BANNER_CONTENT); + params.put("newWindowAt", INSERT_NEW_WINDOW_AT); + params.put("sortSeq", INSERT_SORT_SEQ); + HttpEntity> httpEntity = new HttpEntity<>(params); + + // when + //ResponseEntity responseEntity = restTemplate.postForEntity(URL, requestDto, BoardResponseDto.class); + ResponseEntity responseEntity = restTemplate.exchange( + URL, + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + BannerResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + final Integer bannerNo = dto.getBannerNo(); + + Optional banner = selectData(bannerNo); + assertThat(banner).isPresent(); + + Banner entity = banner.get(); + assertThat(entity.getSite().getId()).isEqualTo(site.getId()); + assertThat(entity.getBannerNo()).isEqualTo(bannerNo); + assertThat(entity.getBannerTypeCode()).isEqualTo(INSERT_BANNER_TYPE_CODE); + assertThat(entity.getBannerTitle()).isEqualTo(INSERT_BANNER_TITLE); + assertThat(entity.getAttachmentCode()).isEqualTo(INSERT_ATTACHMENT_CODE); + assertThat(entity.getUrlAddr()).isEqualTo(INSERT_URL_ADDR); + assertThat(entity.getBannerContent()).isEqualTo(INSERT_BANNER_CONTENT); + assertThat(entity.getNewWindowAt()).isEqualTo(INSERT_NEW_WINDOW_AT); + assertThat(entity.getSortSeq()).isEqualTo(INSERT_SORT_SEQ); + } + + /** + * 배너 수정 테스트 + */ + @Test + void 배너_수정() { + // given + Banner entity = insertBanner(); + + final Integer bannerNo = entity.getBannerNo(); + + Map params = new HashMap<>(); + params.put("siteId", site.getId()); + params.put("bannerTypeCode", UPDATE_BANNER_TYPE_CODE); + params.put("bannerTitle", UPDATE_BANNER_TITLE); + params.put("attachmentCode", UPDATE_ATTACHMENT_CODE); + params.put("urlAddr", UPDATE_URL_ADDR); + params.put("bannerContent", UPDATE_BANNER_CONTENT); + params.put("newWindowAt", UPDATE_NEW_WINDOW_AT); + params.put("sortSeq", UPDATE_SORT_SEQ); + HttpEntity> httpEntity = new HttpEntity<>(params); + + String url = URL + "/" + bannerNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + BannerResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + Optional banner = selectData(bannerNo); + assertThat(banner).isPresent(); + + Banner updatedBanner = banner.get(); + assertThat(updatedBanner.getSite().getId()).isEqualTo(site.getId()); + assertThat(updatedBanner.getBannerNo()).isEqualTo(bannerNo); + assertThat(updatedBanner.getBannerTypeCode()).isEqualTo(UPDATE_BANNER_TYPE_CODE); + assertThat(updatedBanner.getBannerTitle()).isEqualTo(UPDATE_BANNER_TITLE); + assertThat(updatedBanner.getAttachmentCode()).isEqualTo(UPDATE_ATTACHMENT_CODE); + assertThat(updatedBanner.getUrlAddr()).isEqualTo(UPDATE_URL_ADDR); + assertThat(updatedBanner.getBannerContent()).isEqualTo(UPDATE_BANNER_CONTENT); + assertThat(updatedBanner.getNewWindowAt()).isEqualTo(UPDATE_NEW_WINDOW_AT); + assertThat(updatedBanner.getUseAt()).isTrue(); + assertThat(updatedBanner.getSortSeq()).isEqualTo(UPDATE_SORT_SEQ); + } + + /** + * 배너 삭제 테스트 + */ + @Test + void 배너_삭제() { + // given + Banner entity = insertBanner(); + + final Integer bannerNo = entity.getBannerNo(); + + String url = URL + "/" + bannerNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.DELETE, + null, + BannerResponseDto.class + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Optional banner = selectData(bannerNo); + assertThat(banner).isNotPresent(); + } + + /** + * 테스트 데이터 등록 + */ + private void insertBanners() { + for (int i = 1; i <= GIVEN_DATA_COUNT; i++) { + banners.add(bannerRepository.save(Banner.builder() + .site(site) + .bannerTypeCode(BANNER_TYPE_CODE_PREFIX + (i % 3 + 1)) + .bannerTitle(BANNER_TITLE_PREFIX + "_" + i) + .attachmentCode(StringUtils.leftPad(String.valueOf(i), 10, '0')) + .urlAddr(URL_ADDR_PREFIX + i) + .bannerContent(BANNER_CONTENT_PREFIX + "_" + i) + .useAt(i % 2 == 0) + .newWindowAt(i % 2 == 0) + .sortSeq(i) + .build())); + } + } + + /** + * 테스트 데이터 삭제 + */ + /*private void deleteBanners() { + bannerRepository.deleteAll(banners); + + banners.clear(); + }*/ + + /** + * 테스트 데이터 단건 등록 + * + * @return Banner 배너 엔티티 + */ + private Banner insertBanner() { + return bannerRepository.save(Banner.builder() + .site(site) + .bannerTypeCode(INSERT_BANNER_TYPE_CODE) + .bannerTitle(INSERT_BANNER_TITLE) + .attachmentCode(INSERT_ATTACHMENT_CODE) + .urlAddr(INSERT_URL_ADDR) + .bannerContent(INSERT_BANNER_CONTENT) + .useAt(true) + .newWindowAt(INSERT_NEW_WINDOW_AT) + .sortSeq(INSERT_SORT_SEQ) + .build()); + } + + /** + * 테스트 데이터 단건 삭제 + */ + /*private void deleteBanner(Integer bannerNo) { + bannerRepository.deleteById(bannerNo); + }*/ + + /** + * 테스트 데이터 단건 조회 + * + * @param bannerNo 배너 번호 + * @return Optional 배너 엔티티 + */ + private Optional selectData(Integer bannerNo) { + return bannerRepository.findById(bannerNo); + } + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/code/CodeApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/code/CodeApiControllerTest.java new file mode 100644 index 0000000..97b99d1 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/code/CodeApiControllerTest.java @@ -0,0 +1,225 @@ +package org.egovframe.cloud.portalservice.api.code; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeResponseDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeSaveRequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeUpdateRequestDto; +import org.json.JSONObject; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; + +/** + * org.egovframe.cloud.portalservice.api.code.CodeApiControllerTest + *

+ * 공통코드 CRUD 요청을 처리하는 REST API Controller 테스트 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class CodeApiControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + private static final String TEST_CODE_ID = "/TEST"; + private static final String TEST_COM = "@test.com"; + private static final String TEST_EMAIL = System.currentTimeMillis() + TEST_COM; + private static final String TEST_PASSWORD = "test1234!"; +// private static final String PORTAL_SERVICE_URL = "http://localhost:8000/portal-service"; + private static final String USER_SERVICE_URL = "http://localhost:8000/user-service"; + private static final String API_URL = "/api/v1/codes"; + + // login 후 발급된 토큰 값이 입력된다 + private static String ACCESS_TOKEN = ""; + + @Test + @Order(Integer.MIN_VALUE) + public void setup() throws Exception { + // 사용자 등록 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("email", TEST_EMAIL); + jsonObject.put("password", TEST_PASSWORD); + jsonObject.put("userName", "테스터"); + + String url = USER_SERVICE_URL + "/api/v1/users"; + HttpEntity httpEntity = new HttpEntity<>(jsonObject.toString(), headers); + ResponseEntity userResponseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Long.class); + assertThat(userResponseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + // 로그인 후 토큰 값 세팅 + jsonObject.remove("userName"); + ResponseEntity responseEntity = restTemplate.postForEntity(USER_SERVICE_URL + "/login", jsonObject.toString(), String.class); + ACCESS_TOKEN = responseEntity.getHeaders().get("access-token").get(0); + log.info("token ={}", ACCESS_TOKEN); + } + + @Test + @Order(Integer.MAX_VALUE) + public void cleanup() throws Exception { + // 테스트 후 데이터 삭제 + String url = API_URL + TEST_CODE_ID; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity httpEntity = new HttpEntity<>(headers); + restTemplate.exchange(url, HttpMethod.DELETE, httpEntity, String.class); + + // TODO 사용자 데이터 삭제(어드민 유저 서비스) + } + + @Test + @Order(1) + void 공통코드_저장된다() throws Exception { + // given +// String url = PORTAL_SERVICE_URL + API_URL; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + CodeSaveRequestDto requestDto = CodeSaveRequestDto.builder() + .codeId("TEST") + .codeName("테스트") + .codeDescription("테스트 공통코드입니다") + .sortSeq(1) + .useAt(true) + .build(); + + // when + HttpEntity httpEntity = new HttpEntity<>(new ObjectMapper().writeValueAsString(requestDto), headers); + ResponseEntity responseEntity = restTemplate.exchange(API_URL, HttpMethod.POST, httpEntity, String.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + @Order(2) + public void 공통코드_목록_검색어로_조회된다() throws Exception { + // given +// String url = PORTAL_SERVICE_URL + API_URL; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + RequestDto requestDto = RequestDto.builder() + .keywordType("codeId") + .keyword("ES") + .build(); + + // when + HttpEntity httpEntity = new HttpEntity<>(new ObjectMapper().writeValueAsString(requestDto), headers); + ResponseEntity responseEntity = restTemplate.exchange(API_URL, HttpMethod.GET, httpEntity, String.class); + log.info("responseEntity.getBody() ={}", responseEntity.getBody()); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).contains("pageable"); + } + + @Test + @Order(3) + public void 공통코드_단건_조회된다() throws Exception { + // given +// String url = PORTAL_SERVICE_URL + API_URL + TEST_CODE_ID; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity httpEntity = new HttpEntity<>(headers); + + // when + ResponseEntity responseEntity = restTemplate.exchange(API_URL + TEST_CODE_ID, HttpMethod.GET, httpEntity, CodeResponseDto.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody().getCodeName()).isEqualTo("테스트"); + } + + @Test + @Order(4) + void 공통코드_수정된다() throws Exception { + // given +// String url = PORTAL_SERVICE_URL + API_URL + TEST_CODE_ID; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + CodeUpdateRequestDto requestDto = CodeUpdateRequestDto.builder() + .codeName("테스트2") + .codeDescription("테스트 공통코드입니다2") + .sortSeq(2) + .useAt(false) + .build(); + + // when + HttpEntity httpEntity = new HttpEntity<>(new ObjectMapper().writeValueAsString(requestDto), headers); + ResponseEntity responseEntity = restTemplate.exchange(API_URL + TEST_CODE_ID, HttpMethod.PUT, httpEntity, String.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + @Order(5) + void 공통코드_사용여부_토글된다() throws Exception { + // given + String url = API_URL + TEST_CODE_ID + "/toggle-use?useAt=true"; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + // when + HttpEntity httpEntity = new HttpEntity<>(headers); + log.info("url={}", url); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, String.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + // 수정된 데이터 확인 + url = API_URL + TEST_CODE_ID; + ResponseEntity gerResponseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, CodeResponseDto.class); + assertThat(gerResponseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(gerResponseEntity.getBody().getUseAt()).isTrue(); + } + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/code/CodeDetailApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/code/CodeDetailApiControllerTest.java new file mode 100644 index 0000000..9137f4d --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/code/CodeDetailApiControllerTest.java @@ -0,0 +1,255 @@ +package org.egovframe.cloud.portalservice.api.code; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.egovframe.cloud.portalservice.api.code.dto.CodeDetailRequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeDetailResponseDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeDetailSaveRequestDto; +import org.egovframe.cloud.portalservice.api.code.dto.CodeUpdateRequestDto; +import org.json.JSONObject; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; + +/** + * org.egovframe.cloud.portalservice.api.code.CodeDetailApiControllerTest + *

+ * 공통코드 상세 CRUD 요청을 처리하는 REST API Controller 테스트 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/14 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/14    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class CodeDetailApiControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + private static final String PARENT_CODE_ID = "TEST"; + private static final String TEST_CODE_ID = "TEST_1"; + private static final String TEST_CODE_NAME = "테스트상세1"; + private static final String TEST_COM = "@test.com"; + private static final String TEST_EMAIL = System.currentTimeMillis() + TEST_COM; + private static final String TEST_PASSWORD = "test1234!"; +// private static final String PORTAL_SERVICE_URL = "http://localhost:8000/portal-service"; + private static final String USER_SERVICE_URL = "http://localhost:8000/user-service"; + private static final String API_URL = "/api/v1/code-details"; + + // login 후 발급된 토큰 값이 입력된다 + private static String ACCESS_TOKEN = ""; + + @Test + @Order(Integer.MIN_VALUE) + public void setup() throws Exception { + // 사용자 등록 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + JSONObject jsonObject = new JSONObject(); + jsonObject.put("email", TEST_EMAIL); + jsonObject.put("password", TEST_PASSWORD); + jsonObject.put("userName", "테스터"); + + String url = USER_SERVICE_URL + "/api/v1/users"; + HttpEntity httpEntity = new HttpEntity<>(jsonObject.toString(), headers); + ResponseEntity userResponseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Long.class); + assertThat(userResponseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + // 로그인 후 토큰 값 세팅 + jsonObject.remove("userName"); + ResponseEntity responseEntity = restTemplate.postForEntity(USER_SERVICE_URL + "/login", jsonObject.toString(), String.class); + ACCESS_TOKEN = responseEntity.getHeaders().get("access-token").get(0); + log.info("token ={}", ACCESS_TOKEN); + } + + @Test + @Order(Integer.MAX_VALUE) + public void cleanup() throws Exception { + // 테스트 후 데이터 삭제 + String url = API_URL + "/" + TEST_CODE_ID; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity httpEntity = new HttpEntity<>(headers); + restTemplate.exchange(url, HttpMethod.DELETE, httpEntity, String.class); + + // TODO 사용자 데이터 삭제(어드민 유저 서비스) + } + + @Test + @Order(1) + void 공통코드상세_저장된다() throws Exception { + // given + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + CodeDetailSaveRequestDto requestDto = CodeDetailSaveRequestDto.builder() + .parentCodeId(PARENT_CODE_ID) + .codeId(TEST_CODE_ID) + .codeName(TEST_CODE_NAME) + .codeDescription("테스트 상세 공통코드입니다") + .sortSeq(1) + .useAt(true) + .build(); + + // when + HttpEntity httpEntity = new HttpEntity<>(new ObjectMapper().writeValueAsString(requestDto), headers); + ResponseEntity responseEntity = restTemplate.exchange(API_URL, HttpMethod.POST, httpEntity, String.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + @Order(2) + public void 공통코드상세_목록_검색어로_조회된다() throws Exception { + // given + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + CodeDetailRequestDto requestDto = CodeDetailRequestDto.builder() + .parentCodeId(PARENT_CODE_ID) + .keywordType("codeId") + .keyword("ES") + .build(); + + // when + HttpEntity httpEntity = new HttpEntity<>(new ObjectMapper().writeValueAsString(requestDto), headers); + ResponseEntity responseEntity = restTemplate.exchange(API_URL, HttpMethod.GET, httpEntity, String.class); + log.info("responseEntity.getBody() ={}", responseEntity.getBody()); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).contains("pageable"); + } + + @Test + @Order(3) + public void 공통코드상세_단건_조회된다() throws Exception { + // given + String url = API_URL + "/" + TEST_CODE_ID; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity httpEntity = new HttpEntity<>(headers); + + // when + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, CodeDetailResponseDto.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody().getCodeName()).isEqualTo(TEST_CODE_NAME); + } + + @Test + @Order(4) + public void 공통코드상세_목록_조회된다() throws Exception { + // given + String url = API_URL + "/" + PARENT_CODE_ID + "/codes"; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity httpEntity = new HttpEntity<>(headers); + + // when + ResponseEntity> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, new ParameterizedTypeReference>() {}); + + // then + List list = responseEntity.getBody(); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(list.size()).isEqualTo(1); + } + + @Test + @Order(5) + public void 공통코드상세_수정된다() throws Exception { + // given + String url = API_URL + "/" + TEST_CODE_ID; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + CodeUpdateRequestDto requestDto = CodeUpdateRequestDto.builder() + .codeName(TEST_CODE_NAME + "2") + .codeDescription("테스트 공통코드입니다2") + .sortSeq(2) + .useAt(false) + .build(); + + // when + HttpEntity httpEntity = new HttpEntity<>(new ObjectMapper().writeValueAsString(requestDto), headers); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, String.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + // 수정된 데이터 확인 + ResponseEntity gerResponseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, CodeDetailResponseDto.class); + assertThat(gerResponseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(gerResponseEntity.getBody().getCodeName()).isEqualTo(TEST_CODE_NAME + "2"); + } + + @Test + @Order(6) + void 공통코드상세_사용여부_토글된다() throws Exception { + // given + String url = API_URL + "/" + TEST_CODE_ID + "/toggle-use?useAt=true"; + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, ACCESS_TOKEN); + headers.setContentType(MediaType.APPLICATION_JSON); + + // when + HttpEntity httpEntity = new HttpEntity<>(headers); + log.info("url={}", url); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, String.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + // 수정된 데이터 확인 + url = API_URL + "/" + TEST_CODE_ID; + ResponseEntity gerResponseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, CodeDetailResponseDto.class); + assertThat(gerResponseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(gerResponseEntity.getBody().getUseAt()).isTrue(); + } + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/content/ContentApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/content/ContentApiControllerTest.java new file mode 100644 index 0000000..a037c32 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/content/ContentApiControllerTest.java @@ -0,0 +1,326 @@ +package org.egovframe.cloud.portalservice.api.content; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.assertj.core.api.Condition; +import org.egovframe.cloud.portalservice.api.content.dto.ContentListResponseDto; +import org.egovframe.cloud.portalservice.api.content.dto.ContentResponseDto; +import org.egovframe.cloud.portalservice.domain.content.Content; +import org.egovframe.cloud.portalservice.domain.content.ContentRepository; +import org.egovframe.cloud.portalservice.util.RestResponsePage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +/** + * org.egovframe.cloud.portalservice.api.content.ContentApiControllerTest + *

+ * 컨텐츠 Rest API 컨트롤러 테스트 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ *        
+ */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class ContentApiControllerTest { + + /** + * test rest template + */ + @Autowired + TestRestTemplate restTemplate; + + /** + * 컨텐츠 레파지토리 인터페이스 + */ + @Autowired + ContentRepository contentRepository; + + /** + * 컨텐츠 API 경로 + */ + private static final String URL = "/api/v1/contents"; + + /** + * 테스트 데이터 등록 횟수 + */ + private final Integer GIVEN_DATA_COUNT = 10; + + /** + * 테스트 데이터 + */ + private final String CONTENT_NAME_PREFIX = "컨텐츠 명"; + private final String CONTENT_REMARK_PREFIX = "컨텐츠 비고"; + private final String CONTENT_VALUE_PREFIX = "컨텐츠 값"; + + private final Integer CONTENT_NO = GIVEN_DATA_COUNT + 1; + private final String INSERT_CONTENT_NAME = CONTENT_NAME_PREFIX + "_" + CONTENT_NO; + private final String INSERT_CONTENT_REMARK = CONTENT_REMARK_PREFIX + "_" + CONTENT_NO; + private final String INSERT_CONTENT_VALUE = CONTENT_VALUE_PREFIX + "_" + CONTENT_NO; + + private final String UPDATE_CONTENT_NAME = CONTENT_NAME_PREFIX + "_" + (CONTENT_NO + 1); + private final String UPDATE_CONTENT_REMARK = CONTENT_REMARK_PREFIX + "_" + (CONTENT_NO + 1); + private final String UPDATE_CONTENT_VALUE = CONTENT_VALUE_PREFIX + "_" + (CONTENT_NO + 1); + + /** + * 테스트 데이터 + */ + private final List contents = new ArrayList<>(); + + /** + * 테스트 시작 전 수행 + */ + @BeforeEach + void setUp() { + } + + /** + * 테스트 종료 후 수행 + */ + @AfterEach + void tearDown() { + // 컨텐츠 삭제 + contentRepository.deleteAll(); + } + + /** + * 컨텐츠 페이지 목록 조회 테스트 + */ + @Test + void 컨텐츠_페이지_목록_조회() { + // given + insertContents(); + + String queryString = "?keywordType=contentName&keyword=" + CONTENT_NAME_PREFIX; // 검색 조건 + queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보 + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + URL + queryString, HttpMethod.GET, null, + new ParameterizedTypeReference>() { + }); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + RestResponsePage page = responseEntity.getBody(); + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT); + assertThat(page.getContent()).isNotEmpty() + .has(new Condition<>(l -> (CONTENT_NAME_PREFIX + "_10").equals(l.get(0).getContentName()), + "ContentApiControllerTest.findPage contains " + CONTENT_NAME_PREFIX + "_10")) + .has(new Condition<>(l -> (CONTENT_NAME_PREFIX + "_9").equals(l.get(1).getContentName()), + "ContentApiControllerTest.findPage contains " + CONTENT_NAME_PREFIX + "_9")); + + deleteContents(); + } + + /** + * 컨텐츠 상세 조회 테스트 + */ + @Test + void 컨텐츠_상세_조회() { + // given + Content entity = insertContent(); + + final Integer contentNo = entity.getContentNo(); + + String url = URL + "/" + contentNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.GET, null, + new ParameterizedTypeReference() { + }); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + ContentResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + assertThat(dto.getContentNo()).isEqualTo(contentNo); + assertThat(dto.getContentName()).isEqualTo(INSERT_CONTENT_NAME); + assertThat(dto.getContentRemark()).isEqualTo(INSERT_CONTENT_REMARK); + assertThat(dto.getContentValue()).isEqualTo(INSERT_CONTENT_VALUE); + + deleteContent(contentNo); + } + + /** + * 컨텐츠 등록 테스트 + */ + @Test + void 컨텐츠_등록() { + // given + Map params = new HashMap<>(); + params.put("contentName", INSERT_CONTENT_NAME); + params.put("contentRemark", INSERT_CONTENT_REMARK); + params.put("contentValue", INSERT_CONTENT_VALUE); + HttpEntity> httpEntity = new HttpEntity<>(params); + + // when + // ResponseEntity responseEntity = + // restTemplate.postForEntity(URL, requestDto, BoardResponseDto.class); + ResponseEntity responseEntity = restTemplate.exchange(URL, HttpMethod.POST, httpEntity, + new ParameterizedTypeReference() { + }); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + ContentResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + final Integer contentNo = dto.getContentNo(); + + Optional content = selectData(contentNo); + assertThat(content).isPresent(); + + Content entity = content.get(); + assertThat(entity.getContentNo()).isEqualTo(contentNo); + assertThat(entity.getContentName()).isEqualTo(INSERT_CONTENT_NAME); + assertThat(entity.getContentRemark()).isEqualTo(INSERT_CONTENT_REMARK); + assertThat(entity.getContentValue()).isEqualTo(INSERT_CONTENT_VALUE); + + deleteContent(contentNo); + } + + /** + * 컨텐츠 수정 테스트 + */ + @Test + void 컨텐츠_수정() { + // given + Content entity = insertContent(); + + final Integer contentNo = entity.getContentNo(); + + Map params = new HashMap<>(); + params.put("contentName", UPDATE_CONTENT_NAME); + params.put("contentRemark", UPDATE_CONTENT_REMARK); + params.put("contentValue", UPDATE_CONTENT_VALUE); + HttpEntity> httpEntity = new HttpEntity<>(params); + + String url = URL + "/" + contentNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, + new ParameterizedTypeReference() { + }); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + ContentResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + Optional content = selectData(contentNo); + assertThat(content).isPresent(); + + Content updatedContent = content.get(); + assertThat(updatedContent.getContentNo()).isEqualTo(contentNo); + assertThat(updatedContent.getContentName()).isEqualTo(UPDATE_CONTENT_NAME); + assertThat(updatedContent.getContentRemark()).isEqualTo(UPDATE_CONTENT_REMARK); + assertThat(updatedContent.getContentValue()).isEqualTo(UPDATE_CONTENT_VALUE); + + deleteContent(contentNo); + } + + /** + * 컨텐츠 삭제 테스트 + */ + @Test + void 컨텐츠_삭제() { + // given + Content entity = insertContent(); + + final Integer contentNo = entity.getContentNo(); + + String url = URL + "/" + contentNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.DELETE, null, + ContentResponseDto.class); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Optional content = selectData(contentNo); + assertThat(content).isNotPresent(); + } + + /** + * 테스트 데이터 등록 + */ + private void insertContents() { + for (int i = 1; i <= GIVEN_DATA_COUNT; i++) { + contents.add(contentRepository.save(Content.builder().contentName(CONTENT_NAME_PREFIX + "_" + i) + .contentRemark(CONTENT_REMARK_PREFIX + "_" + i).contentValue(CONTENT_VALUE_PREFIX + "_" + i) + .build())); + } + } + + /** + * 테스트 데이터 삭제 + */ + private void deleteContents() { + contentRepository.deleteAll(contents); + + contents.clear(); + } + + /** + * 테스트 데이터 단건 등록 + * + * @return Content 컨텐츠 엔티티 + */ + private Content insertContent() { + return contentRepository.save(Content.builder().contentName(INSERT_CONTENT_NAME) + .contentRemark(INSERT_CONTENT_REMARK).contentValue(INSERT_CONTENT_VALUE).build()); + } + + /** + * 테스트 데이터 단건 삭제 + */ + private void deleteContent(Integer contentNo) { + contentRepository.deleteById(contentNo); + } + + /** + * 테스트 데이터 단건 조회 + * + * @param contentNo 컨텐츠 번호 + * @return Optional 컨텐츠 엔티티 + */ + private Optional selectData(Integer contentNo) { + return contentRepository.findById(contentNo); + } + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/menu/MenuApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/menu/MenuApiControllerTest.java new file mode 100644 index 0000000..dd49ac0 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/menu/MenuApiControllerTest.java @@ -0,0 +1,439 @@ +package org.egovframe.cloud.portalservice.api.menu; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.egovframe.cloud.portalservice.api.menu.dto.MenuDnDRequestDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuTreeRequestDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuTreeResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuUpdateRequestDto; +import org.egovframe.cloud.portalservice.api.menu.dto.SiteResponseDto; +import org.egovframe.cloud.portalservice.domain.menu.Menu; +import org.egovframe.cloud.portalservice.domain.menu.MenuRepository; +import org.egovframe.cloud.portalservice.domain.menu.Site; +import org.egovframe.cloud.portalservice.domain.menu.SiteRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class MenuApiControllerTest { + + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private MenuRepository menuRepository; + + @Autowired + private SiteRepository siteRepository; + + + @BeforeEach + public void setup() throws Exception { + siteRepository.save(Site.builder() + .name("site") + .isUse(true) + .build() + ); + } + + @AfterEach + public void cleanup() throws Exception { + menuRepository.deleteAll(); + siteRepository.deleteAll(); + } + + @Test + public void 메뉴목록을조회한다_bySiteId() throws Exception { + Site site = siteRepository.findAll().get(0); + + Menu parentMenu = menuRepository.save(Menu.builder() + .menuKorName("parent") + .sortSeq(1) + .site(site) + .build()); + + for (int i = 0; i < 3; i++) { + Menu childMenu = Menu.builder() + .menuKorName("child_" + i) + .site(site) + .parent(parentMenu) + .sortSeq(i + 1) + .build(); + childMenu.setParentMenu(parentMenu); + menuRepository.save(childMenu); + } + + //when + ResponseEntity> responseEntity = restTemplate.exchange("/api/v1/"+site.getId()+"/menus", HttpMethod.GET, null, new ParameterizedTypeReference>(){}); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List body = responseEntity.getBody(); + assertThat(body.size()).isEqualTo(1); + body.stream().forEach(System.out::println); + assertThat(body.get(0).getChildren().size()).isEqualTo(3); + body.stream().forEach(menuTreeResponseDto -> { + menuTreeResponseDto.getChildren().stream().forEach(System.out::println); + }); + } + + @Test + public void 메뉴관리_사이트콤보_목록_조회한다() throws Exception { + //given + siteRepository.save(Site.builder() + .name("portal") + .isUse(true) + .build() + ); + siteRepository.save(Site.builder() + .name("admin") + .isUse(true) + .build() + ); + + //when + ResponseEntity> responseEntity = restTemplate.exchange("/api/v1/sites", HttpMethod.GET, null, new ParameterizedTypeReference>(){}); + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).extracting("name").contains("portal", "admin"); + responseEntity.getBody().stream().forEach(System.out::println); + + } + + @Test + public void 메뉴관리_왼쪽트리_조회한다() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + + Menu parentMenu1 = menuRepository.save(Menu.builder() + .menuKorName("parent_1") + .sortSeq(1) + .site(site) + .build()); + Menu parentMenu2 = menuRepository.save(Menu.builder() + .menuKorName("parent_2") + .sortSeq(2) + .site(site) + .build()); + + for (int i = 0; i < 3; i++) { + Menu childMenu1 = Menu.builder() + .menuKorName("child_1_" + i) + .site(site) + .parent(parentMenu1) + .sortSeq(i + 1) + .build(); + childMenu1.setParentMenu(parentMenu1); + menuRepository.save(childMenu1); + if (i == 1) { + Menu childChildMenu = Menu.builder() + .menuKorName("child_child_1") + .site(site) + .parent(childMenu1) + .sortSeq(1) + .build(); + childChildMenu.setParentMenu(childMenu1); + menuRepository.save(childChildMenu); + Menu childChildMenu2 = Menu.builder() + .menuKorName("child_child_1") + .site(site) + .parent(childMenu1) + .sortSeq(2) + .build(); + childChildMenu2.setParentMenu(childMenu1); + menuRepository.save(childChildMenu2); + } + + Menu childMenu2 = Menu.builder() + .menuKorName("child_2_" + i) + .site(site) + .parent(parentMenu2) + .sortSeq(i + 1) + .build(); + childMenu1.setParentMenu(parentMenu2); + menuRepository.save(childMenu2); + } + + ResponseEntity> responseEntity = restTemplate.exchange("/api/v1/menus/"+site.getId()+"/tree", HttpMethod.GET, null, new ParameterizedTypeReference>(){}); + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List body = responseEntity.getBody(); + assertThat(body).extracting("name").contains(parentMenu1.getMenuKorName(), parentMenu2.getMenuKorName()); + body.stream().forEach(menuTreeResponseDto -> { + System.out.println(menuTreeResponseDto); + menuTreeResponseDto.getChildren().stream().forEach(menuTreeResponseDto1 -> { + System.out.println(menuTreeResponseDto1); + menuTreeResponseDto1.getChildren().stream().forEach(System.out::println); + }); + + }); + + + } + + @Test + public void 메뉴관리_오른쪽_상세정보_조회한다() throws Exception { + Site site = siteRepository.findAll().get(0); + + Menu parentMenu = menuRepository.save(Menu.builder() + .menuKorName("parent") + .sortSeq(1) + .site(site) + .build()); + + //when + String url = "/api/v1/manager/menus/"+parentMenu.getId(); + ResponseEntity responseEntity = restTemplate.getForEntity(url, MenuResponseDto.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).extracting("name").isEqualTo(parentMenu.getMenuKorName()); + System.out.println(responseEntity.getBody()); + + } + + @Test + public void 메뉴관리_트리메뉴_추가한다() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + + MenuTreeRequestDto menuTreeRequestDto = MenuTreeRequestDto.builder() + .parentId(null) + .siteId(site.getId()) + .name("parent") + .sortSeq(1) + .build(); + + String url = "/api/v1/menus"; + + //when + ResponseEntity responseEntity = restTemplate.postForEntity(url, menuTreeRequestDto, MenuTreeResponseDto.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + System.out.println(responseEntity.getBody()); + assertThat(responseEntity.getBody()).extracting("name").isEqualTo(menuTreeRequestDto.getName()); + + } + + /** + * @TODO + * bulk update 수정 필요 + * @throws Exception + */ + @Test + public void 메뉴관리_트리_드래그앤드랍_순서_및_부모메뉴_변경() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + + Menu parentMenu1 = menuRepository.save(Menu.builder() + .menuKorName("parent_1") + .sortSeq(1) + .site(site) + .build()); + Menu parentMenu2 = menuRepository.save(Menu.builder() + .menuKorName("parent_2") + .sortSeq(2) + .site(site) + .build()); + + Long menuId = 0L; + for (int i = 0; i < 3; i++) { + Menu childMenu1 = Menu.builder() + .menuKorName("child_1_" + i) + .site(site) + .parent(parentMenu1) + .sortSeq(i + 1) + .build(); + childMenu1.setParentMenu(parentMenu1); + Menu save = menuRepository.save(childMenu1); + menuId = save.getId(); + } + + List updateList = new ArrayList<>(); + + updateList.add(MenuDnDRequestDto.builder() + .menuId(menuId) + .sortSeq(1) + .parentId(parentMenu2.getId()) + .build()); + + HttpEntity> httpEntity = new HttpEntity<>( + updateList + ); + + + String url = "/api/v1/menus/"+site.getId()+"/tree"; + + //when + ResponseEntity> responseEntity = + restTemplate.exchange(url, HttpMethod.PUT, httpEntity, new ParameterizedTypeReference>(){}); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + responseEntity.getBody().stream().forEach(System.out::println); + + } + + @Test + public void 메뉴관리_트리_이름변경한다() throws Exception { + Site site = siteRepository.findAll().get(0); + + Menu parentMenu1 = menuRepository.save(Menu.builder() + .menuKorName("parent_1") + .sortSeq(1) + .site(site) + .build()); + + Long menuId = 0L; + for (int i = 0; i < 3; i++) { + Menu childMenu1 = Menu.builder() + .menuKorName("child_1_" + i) + .site(site) + .parent(parentMenu1) + .sortSeq(i + 1) + .build(); + childMenu1.setParentMenu(parentMenu1); + Menu save = menuRepository.save(childMenu1); + menuId = save.getId(); + } + + String url = "/api/v1/menus/"+menuId+"/updateName"; + + //when + ResponseEntity responseEntity = + restTemplate.exchange(url, HttpMethod.PUT, null, MenuTreeResponseDto.class); + + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody().getName()).isEqualTo("updateName"); + + } + + @Test + public void 메뉴관리_기본설정_저장한다() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + + Menu parentMenu1 = menuRepository.save(Menu.builder() + .menuKorName("parent_1") + .sortSeq(1) + .site(site) + .build()); + + String url = "/api/v1/menus/"+parentMenu1.getId(); + + + HttpEntity httpEntity = new HttpEntity<>( + MenuUpdateRequestDto.builder() + .description("상위메뉴") + .connectId(1) + .menuType("menuType") + .urlPath("/index") + .subName("subname") + .isUse(true) + .isShow(true) + .isBlank(false) + .icon("icon") + .build() + ); + + //when + ResponseEntity responseEntity = + restTemplate.exchange(url, HttpMethod.PUT, httpEntity, MenuResponseDto.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).extracting("description", "isUse").containsExactly("상위메뉴", true); + System.out.println(responseEntity.getBody()); + + } + + @Test + public void 메뉴관리_한건_삭제한다() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + + Menu parentMenu1 = menuRepository.save(Menu.builder() + .menuKorName("parent_1") + .sortSeq(1) + .site(site) + .build()); + + Long menuId = 0L; + for (int i = 0; i < 3; i++) { + Menu childMenu1 = Menu.builder() + .menuKorName("child_1_" + i) + .site(site) + .parent(parentMenu1) + .sortSeq(i + 1) + .build(); + childMenu1.setParentMenu(parentMenu1); + Menu save = menuRepository.save(childMenu1); + menuId = save.getId(); + } + + String url = "/api/v1/menus/"+menuId; + //when + restTemplate.delete(url); + + //then + Optional optional = menuRepository.findById(menuId); + assertThat(optional.isPresent()).isFalse(); + + } + + @Test + public void 메뉴관리_부모메뉴로_모두_삭제한다() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + + Menu parentMenu1 = menuRepository.save(Menu.builder() + .menuKorName("parent_1") + .sortSeq(1) + .site(site) + .build()); + + Long menuId = 0L; + for (int i = 0; i < 3; i++) { + Menu childMenu1 = Menu.builder() + .menuKorName("child_1_" + i) + .site(site) + .parent(parentMenu1) + .sortSeq(i + 1) + .build(); + childMenu1.setParentMenu(parentMenu1); + Menu save = menuRepository.save(childMenu1); + menuId = save.getId(); + } + + String url = "/api/v1/menus/"+parentMenu1.getId(); + //when + restTemplate.delete(url); + + //then + List menus = menuRepository.findAll(); + assertThat(menus.size()).isEqualTo(0); + + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/menu/MenuRoleApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/menu/MenuRoleApiControllerTest.java new file mode 100644 index 0000000..933fa40 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/menu/MenuRoleApiControllerTest.java @@ -0,0 +1,242 @@ +package org.egovframe.cloud.portalservice.api.menu; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleRequestDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleResponseDto; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuSideResponseDto; +import org.egovframe.cloud.portalservice.domain.menu.Menu; +import org.egovframe.cloud.portalservice.domain.menu.MenuRepository; +import org.egovframe.cloud.portalservice.domain.menu.MenuRole; +import org.egovframe.cloud.portalservice.domain.menu.MenuRoleRepository; +import org.egovframe.cloud.portalservice.domain.menu.Site; +import org.egovframe.cloud.portalservice.domain.menu.SiteRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class MenuRoleApiControllerTest { + + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private MenuRepository menuRepository; + + @Autowired + private SiteRepository siteRepository; + + @Autowired + private MenuRoleRepository menuRoleRepository; + + @BeforeEach + public void setup() throws Exception { + Site site = Site.builder() + .name("site") + .isUse(true) + .build(); + siteRepository.save(site); + + Menu parentMenu = menuRepository.save(Menu.builder() + .menuKorName("parent") + .sortSeq(1) + .site(site) + .build()); + + for (int i = 0; i < 3; i++) { + Menu childMenu = Menu.builder() + .menuKorName("child_" + i) + .site(site) + .parent(parentMenu) + .sortSeq(i + 1) + .build(); + childMenu.setParentMenu(parentMenu); + menuRepository.save(childMenu); + } + } + + @AfterEach + public void cleanup() throws Exception { + menuRoleRepository.deleteAll(); + menuRepository.deleteAll(); + siteRepository.deleteAll(); + } + + @Test + public void 권한별메뉴_데이터없이_메뉴outerjoin_하여_조회한다() throws Exception { + Site site = siteRepository.findAll().get(0); + //when + ResponseEntity> responseEntity = + restTemplate.exchange("/api/v1/menu-roles/role/"+site.getId(), HttpMethod.GET, null, new ParameterizedTypeReference>(){}); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List body = responseEntity.getBody(); + assertThat(body.size()).isEqualTo(1); + assertThat(body.get(0).getChildren().size()).isEqualTo(3); + body.stream().forEach(menuTreeResponseDto -> { + System.out.println(menuTreeResponseDto.toString()); + menuTreeResponseDto.getChildren().stream().forEach(System.out::println); + }); + } + + @Test + public void 권한별메뉴_데이터있는경우_조회한다() throws Exception { + + List menus = menuRepository.findAll(); + Menu parent = menus.stream().filter(menu -> menu.getMenuKorName().equals("parent")).collect(Collectors.toList()).get(0); + Menu child1 = menus.stream().filter(menu -> menu.getMenuKorName().equals("child_1")).collect(Collectors.toList()).get(0); + + List menuRoles = new ArrayList<>(); + menuRoles.add(MenuRole.builder().roleId("role").menu(parent).build()); + menuRoles.add(MenuRole.builder().roleId("role").menu(child1).build()); + menuRoleRepository.saveAll(menuRoles); + + Site site = siteRepository.findAll().get(0); + //when + ResponseEntity> responseEntity = + restTemplate.exchange("/api/v1/menu-roles/role/"+site.getId(), HttpMethod.GET, null, new ParameterizedTypeReference>(){}); + + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List body = responseEntity.getBody(); + assertThat(body.size()).isEqualTo(1); + assertThat(body.get(0).getIsChecked()).isTrue(); + body.stream().forEach(System.out::println); + assertThat(body.get(0).getChildren().size()).isEqualTo(3); + body.stream().forEach(menuTreeResponseDto -> { + menuTreeResponseDto.getChildren().stream().forEach(child -> { + System.out.println(child); + if (child.getKorName().equals("child_1")) { + assertThat(child.getIsChecked()).isTrue(); + }else { + assertThat(child.getIsChecked()).isFalse(); + } + }); + + }); + + } + + @Test + public void 권한별메뉴관리_저장한다() throws Exception { + Site site = siteRepository.findAll().get(0); + List list = menuRoleRepository.findTree("role", site.getId()); + + List requestDtoList = new ArrayList<>(); + List children = new ArrayList<>(); + list.get(0).getChildren().stream().forEach(menuRoleResponseDto -> { + if (menuRoleResponseDto.getKorName().equals("child_1")) { + children.add(MenuRoleRequestDto.builder() + .menuRoleId(menuRoleResponseDto.getMenuRoleId()) + .isChecked(true) + .roleId("role") + .id(menuRoleResponseDto.getId()) + .build()); + + }else { + children.add(MenuRoleRequestDto.builder() + .menuRoleId(menuRoleResponseDto.getMenuRoleId()) + .isChecked(false) + .roleId("role") + .id(menuRoleResponseDto.getId()) + .build()); + } + }); + + requestDtoList.add(MenuRoleRequestDto.builder() + .menuRoleId(list.get(0).getMenuRoleId()) + .isChecked(true) + .id(list.get(0).getId()) + .children(children) + .build()); + + HttpEntity> httpEntity = new HttpEntity<>( + requestDtoList + ); + + + + //when + ResponseEntity responseEntity = + restTemplate.exchange("/api/v1/menu-roles", HttpMethod.POST, httpEntity, String.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).isEqualTo("Success"); + + List roles = menuRoleRepository.findAll(); + roles.stream().forEach(System.out::println); + assertThat(roles.size()).isEqualTo(2); + + } + + @Test + public void 로그인하지_않은_사용자의_메뉴조회() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + Menu parentMenu = menuRepository.save(Menu.builder() + .menuKorName("parent-any") + .sortSeq(1) + .site(site) + .build()); + MenuRole parentMenuRole = MenuRole.builder() + .roleId("ROLE_ANONYMOUS") + .menu(parentMenu) + .build(); + parentMenuRole.setMenu(parentMenu); + menuRoleRepository.save(parentMenuRole); + + for (int i = 0; i < 3; i++) { + Menu childMenu = Menu.builder() + .menuKorName("child-any_" + i) + .site(site) + .parent(parentMenu) + .sortSeq(i + 1) + .build(); + childMenu.setParentMenu(parentMenu); + menuRepository.save(childMenu); + MenuRole role_any = MenuRole.builder() + .roleId("ROLE_ANONYMOUS") + .menu(childMenu) + .build(); + role_any.setMenu(childMenu); + menuRoleRepository.save(role_any); + } + //when + ResponseEntity> responseEntity = + restTemplate.exchange("/api/v1/menu-roles/"+site.getId(), HttpMethod.GET, null, new ParameterizedTypeReference>(){}); + + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + List body = responseEntity.getBody(); + assertThat(body.size()).isEqualTo(1); + body.stream().forEach(menuSideResponseDto -> { + System.out.println(menuSideResponseDto); + menuSideResponseDto.getChildren().stream().forEach(System.out::println); + }); + + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/message/MessageApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/message/MessageApiControllerTest.java new file mode 100644 index 0000000..05104ba --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/message/MessageApiControllerTest.java @@ -0,0 +1,62 @@ +package org.egovframe.cloud.portalservice.api.message; + +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.egovframe.cloud.portalservice.api.message.dto.MessageListResponseDto; +import org.egovframe.cloud.portalservice.domain.message.Message; +import org.egovframe.cloud.portalservice.domain.message.MessageRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class MessageApiControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private MessageRepository messageRepository; + + private final static String API_URL = "/api/v1/messages/"; + + @BeforeEach + public void setup() throws Exception { + messageRepository.save(Message.builder().messageId("test.one").messageKoName("테스트1").build()); + messageRepository.save(Message.builder().messageId("test.two").messageKoName("테스트2").build()); + messageRepository.save(Message.builder().messageId("test.three").messageKoName("테스트3").messageEnName("TEST3").build()); + } + + @AfterEach + public void cleanup() throws Exception { + messageRepository.deleteAll(); + } + + @Test + public void 메시지_한글명_목록_조회된다() throws Exception { + // given + String lang = "ko"; + + // when + ResponseEntity> responseEntity = restTemplate.exchange(API_URL + lang, HttpMethod.GET, null, new ParameterizedTypeReference>(){}); + + // then + responseEntity.getBody().forEach(messageListResponseDto -> System.out.println("id = " + messageListResponseDto.getMessageId() + ", messageName() = " + messageListResponseDto.getMessageName())); + Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + Assertions.assertThat(responseEntity.getBody().size()).isEqualTo(3); + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/policy/PolicyApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/policy/PolicyApiControllerTest.java new file mode 100644 index 0000000..855f5d9 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/policy/PolicyApiControllerTest.java @@ -0,0 +1,218 @@ +package org.egovframe.cloud.portalservice.api.policy; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZonedDateTime; +import java.util.Optional; + +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyResponseDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicySaveRequestDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyUpdateRequestDto; +import org.egovframe.cloud.portalservice.domain.policy.Policy; +import org.egovframe.cloud.portalservice.domain.policy.PolicyRepository; +import org.egovframe.cloud.portalservice.util.RestResponsePage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class PolicyApiControllerTest { + + @LocalServerPort + private int port; + + private static final String API_URL = "/api/v1/policies"; + + @Autowired + TestRestTemplate restTemplate; + + @Autowired + private PolicyRepository policyRepository; + + @BeforeEach + public void setUp() { + //given + for (int i = 1; i <= 10; i++) { + String title = "title_"+i; + String contents = "contents " + i; + String type = "TOS"; + if(i % 2 == 0){ + type = "PP"; + } + + policyRepository.save(Policy.builder() + .type(type) + .title(title) + .isUse(true) + .regDate(ZonedDateTime.now()) + .contents(contents) + .build()); + } + } + + @AfterEach + public void teardown() { + policyRepository.deleteAll(); + } + + @Test + public void 이용약관_등록_정상() throws Exception { + //given + String type = "PP"; + String title= "test title"; + String contents = "test contents"; + + PolicySaveRequestDto requestDto = PolicySaveRequestDto.builder() + .type(type) + .title(title) + .isUse(true) + .regDate(ZonedDateTime.now()) + .contents(contents) + .build(); + + String url = "http://localhost:"+port+API_URL; + + //when + ResponseEntity responseEntity = restTemplate.postForEntity(url, requestDto, Long.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + Optional policy = policyRepository.findById(responseEntity.getBody().longValue()); + + System.out.println(policy.get().toString()); + + } + + @Test + public void 목록조회한다() throws Exception { + + String url = "http://localhost:"+port+API_URL+"?size=3%page=0"; + //when + ResponseEntity> responseEntity = + restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference>() {}); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + RestResponsePage body = responseEntity.getBody(); + body.stream().forEach(System.out::println); + + } + + @Test + public void 회원가입시_가장최근_이용약관_조회_된다() throws Exception { + + String url = "http://localhost:"+port+API_URL + "/latest/TOS"; + //when + ResponseEntity responseEntity = restTemplate.getForEntity(url, PolicyResponseDto.class); + + //then + System.out.println(responseEntity.getBody().toString()); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody().getTitle()).isEqualTo("title_9"); + + + } + + @Test + public void ID로_한건조회_정상() throws Exception { + + String url = "http://localhost:"+port+API_URL +"/9"; + + //when + ResponseEntity responseEntity = restTemplate.getForEntity(url, PolicyResponseDto.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + System.out.println(responseEntity.getBody().toString()); + } + + @Test + public void 이용약관_수정_된다() throws Exception { + //given + Long id = policyRepository.save(Policy.builder() + .type("TOS") + .title("title") + .contents("contents!!!!") + .build() + ).getId(); + String url = "http://localhost:"+port+API_URL +"/"+id; + + PolicyUpdateRequestDto requestDto = PolicyUpdateRequestDto.builder() + .title("update title") + .contents("update Details") + .build(); + + //when + HttpEntity requestEntity = new HttpEntity<>(requestDto); + + //when + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).isEqualTo(id); + Policy result = policyRepository.findById(id).get(); + System.out.println(result); + + } + + @Test + public void 이용약관_삭제_한다() { + //given + Long id = policyRepository.save(Policy.builder() + .type("TOS") + .title("title") + .contents("contents!!!!") + .build() + ).getId(); + String url = "http://localhost:"+port+API_URL +"/"+id; + + //when + restTemplate.delete(url); + + //then + Optional terms = policyRepository.findById(id); + assertThat(terms.isPresent()).isFalse(); + } + + @Test + public void 사용여부_수정_한다() throws Exception { + //given + Long id = policyRepository.save(Policy.builder() + .type("TOS") + .title("title") + .isUse(true) + .contents("contents!!!") + .build() + ).getId(); + String url = "http://localhost:"+port+API_URL +"/"+id+"/"+false; + + //when + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.PUT, null, Long.class); + + //then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody()).isEqualTo(id); + Policy result = policyRepository.findById(id).get(); + System.out.println(result); + assertThat(result.getIsUse()).isFalse(); + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/privacy/PrivacyApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/privacy/PrivacyApiControllerTest.java new file mode 100644 index 0000000..d399f77 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/privacy/PrivacyApiControllerTest.java @@ -0,0 +1,415 @@ +package org.egovframe.cloud.portalservice.api.privacy; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.assertj.core.api.Condition; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyListResponseDto; +import org.egovframe.cloud.portalservice.api.privacy.dto.PrivacyResponseDto; +import org.egovframe.cloud.portalservice.domain.privacy.Privacy; +import org.egovframe.cloud.portalservice.domain.privacy.PrivacyRepository; +import org.egovframe.cloud.portalservice.util.RestResponsePage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +/** + * org.egovframe.cloud.portalservice.api.privacy.PrivacyApiControllerTest + * + * 개인정보처리방침 Rest API 컨트롤러 테스트 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/23 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/23    jooho       최초 생성
+ * 
+ */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class PrivacyApiControllerTest { + + /** + * test rest template + */ + @Autowired + TestRestTemplate restTemplate; + + /** + * 개인정보처리방침 레파지토리 인터페이스 + */ + @Autowired + PrivacyRepository privacyRepository; + + /** + * 개인정보처리방침 API 경로 + */ + private static final String URL = "/api/v1/privacies"; + + /** + * 테스트 데이터 등록 횟수 + */ + private final Integer GIVEN_DATA_COUNT = 10; + + /** + * 테스트 데이터 + */ + private final String CONTENT_TITLE_PREFIX = "개인정보처리방침 제목"; + private final String CONTENT_CONTENT_PREFIX = "개인정보처리방침 내용"; + + private final Integer CONTENT_NO = GIVEN_DATA_COUNT + 1; + private final String INSERT_CONTENT_TITLE = CONTENT_TITLE_PREFIX + "_" + CONTENT_NO; + private final String INSERT_CONTENT_CONTENT = CONTENT_CONTENT_PREFIX + "_" + CONTENT_NO; + private final Boolean INSERT_USE_AT = true; + + private final String UPDATE_CONTENT_TITLE = CONTENT_TITLE_PREFIX + "_" + (CONTENT_NO + 1); + private final String UPDATE_CONTENT_CONTENT = CONTENT_CONTENT_PREFIX + "_" + (CONTENT_NO + 1); + private final Boolean UPDATE_USE_AT = false; + + /** + * 테스트 데이터 + */ + private final List privacies = new ArrayList<>(); + + /** + * 테스트 시작 전 수행 + */ + @BeforeEach + void setUp() { + } + + /** + * 테스트 종료 후 수행 + */ + @AfterEach + void tearDown() { + //개인정보처리방침 삭제 + privacyRepository.deleteAll(); + } + + /** + * 개인정보처리방침 페이지 목록 조회 테스트 + */ + @Test + void 개인정보처리방침_페이지_목록_조회() { + // given + insertPrivacies(); + + String queryString = "?keywordType=privacyTitle&keyword=" + CONTENT_TITLE_PREFIX; // 검색 조건 + queryString += "&page=0&size=" + GIVEN_DATA_COUNT; // 페이지 정보 + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + URL + queryString, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + RestResponsePage page = responseEntity.getBody(); + assertThat(page).isNotNull(); + assertThat(page.getNumberOfElements()).isEqualTo(GIVEN_DATA_COUNT); + assertThat(page.getContent()) + .isNotEmpty() + .has(new Condition<>(l -> (CONTENT_TITLE_PREFIX + "_10").equals(l.get(0).getPrivacyTitle()), "PrivacyApiControllerTest.findPage contains " + CONTENT_TITLE_PREFIX + "_10")) + .has(new Condition<>(l -> (CONTENT_TITLE_PREFIX + "_9").equals(l.get(1).getPrivacyTitle()), "PrivacyApiControllerTest.findPage contains " + CONTENT_TITLE_PREFIX + "_9")); + + deletePrivacies(); + } + + /** + * 개인정보처리방침 사용중 전체 목록 조회 테스트 + */ + @Test + void 개인정보처리방침_사용중_전체_목록_조회() { + // given + insertPrivacies(); + + // when + ResponseEntity> responseEntity = restTemplate.exchange( + URL + "/all/use", + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + List list = responseEntity.getBody(); + assertThat(list).isNotNull(); + assertThat(list.size()).isEqualTo(GIVEN_DATA_COUNT / 2); + assertThat(list) + .isNotEmpty() + .has(new Condition<>(l -> (CONTENT_TITLE_PREFIX + "_9").equals(l.get(0).getPrivacyTitle()), "PrivacyApiControllerTest.findAllByUseAtOrderByPrivacyNoDesc contains " + CONTENT_TITLE_PREFIX + "_9")) + .has(new Condition<>(l -> (CONTENT_TITLE_PREFIX + "_7").equals(l.get(1).getPrivacyTitle()), "PrivacyApiControllerTest.findAllByUseAtOrderByPrivacyNoDesc contains " + CONTENT_TITLE_PREFIX + "_7")); + + deletePrivacies(); + } + + /** + * 개인정보처리방침 상세 조회 테스트 + */ + @Test + void 개인정보처리방침_상세_조회() { + // given + Privacy entity = insertPrivacy(); + + final Integer privacyNo = entity.getPrivacyNo(); + + String url = URL + "/" + privacyNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + PrivacyResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + assertThat(dto.getPrivacyNo()).isEqualTo(privacyNo); + assertThat(dto.getPrivacyTitle()).isEqualTo(INSERT_CONTENT_TITLE); + assertThat(dto.getPrivacyContent()).isEqualTo(INSERT_CONTENT_CONTENT); + assertThat(dto.getUseAt()).isEqualTo(INSERT_USE_AT); + + deletePrivacy(privacyNo); + } + + /** + * 개인정보처리방침 등록 테스트 + */ + @Test + void 개인정보처리방침_등록() { + // given + Map params = new HashMap<>(); + params.put("privacyTitle", INSERT_CONTENT_TITLE); + params.put("privacyContent", INSERT_CONTENT_CONTENT); + params.put("useAt", INSERT_USE_AT); + HttpEntity> httpEntity = new HttpEntity<>(params); + + // when + //ResponseEntity responseEntity = restTemplate.postForEntity(URL, requestDto, BoardResponseDto.class); + ResponseEntity responseEntity = restTemplate.exchange( + URL, + HttpMethod.POST, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + PrivacyResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + final Integer privacyNo = dto.getPrivacyNo(); + + Optional privacy = selectData(privacyNo); + assertThat(privacy).isPresent(); + + Privacy entity = privacy.get(); + assertThat(entity.getPrivacyNo()).isEqualTo(privacyNo); + assertThat(entity.getPrivacyTitle()).isEqualTo(INSERT_CONTENT_TITLE); + assertThat(entity.getPrivacyContent()).isEqualTo(INSERT_CONTENT_CONTENT); + assertThat(entity.getUseAt()).isEqualTo(INSERT_USE_AT); + + deletePrivacy(privacyNo); + } + + /** + * 개인정보처리방침 수정 테스트 + */ + @Test + void 개인정보처리방침_수정() { + // given + Privacy entity = insertPrivacy(); + + final Integer privacyNo = entity.getPrivacyNo(); + + Map params = new HashMap<>(); + params.put("privacyTitle", UPDATE_CONTENT_TITLE); + params.put("privacyContent", UPDATE_CONTENT_CONTENT); + params.put("useAt", UPDATE_USE_AT); + HttpEntity> httpEntity = new HttpEntity<>(params); + + String url = URL + "/" + privacyNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + httpEntity, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + PrivacyResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + Optional privacy = selectData(privacyNo); + assertThat(privacy).isPresent(); + + Privacy updatedPrivacy = privacy.get(); + assertThat(updatedPrivacy.getPrivacyNo()).isEqualTo(privacyNo); + assertThat(updatedPrivacy.getPrivacyTitle()).isEqualTo(UPDATE_CONTENT_TITLE); + assertThat(updatedPrivacy.getPrivacyContent()).isEqualTo(UPDATE_CONTENT_CONTENT); + assertThat(updatedPrivacy.getUseAt()).isEqualTo(UPDATE_USE_AT); + + deletePrivacy(privacyNo); + } + + /** + * 개인정보처리방침 사용 여부 수정 테스트 + */ + @Test + void 개인정보처리방침_사용여부_수정() { + // given + Privacy entity = insertPrivacy(); + + final Integer privacyNo = entity.getPrivacyNo(); + + String url = URL + "/" + privacyNo + "/" + UPDATE_USE_AT; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.PUT, + null, + new ParameterizedTypeReference() { + } + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + PrivacyResponseDto dto = responseEntity.getBody(); + assertThat(dto).isNotNull(); + + Optional privacy = selectData(privacyNo); + assertThat(privacy).isPresent(); + + Privacy updatedPrivacy = privacy.get(); + assertThat(updatedPrivacy.getPrivacyNo()).isEqualTo(privacyNo); + assertThat(updatedPrivacy.getUseAt()).isEqualTo(UPDATE_USE_AT); + + deletePrivacy(privacyNo); + } + + /** + * 개인정보처리방침 삭제 테스트 + */ + @Test + void 개인정보처리방침_삭제() { + // given + Privacy entity = insertPrivacy(); + + final Integer privacyNo = entity.getPrivacyNo(); + + String url = URL + "/" + privacyNo; + + // when + ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.DELETE, + null, + PrivacyResponseDto.class + ); + + // then + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + + Optional privacy = selectData(privacyNo); + assertThat(privacy).isNotPresent(); + } + + /** + * 테스트 데이터 등록 + */ + private void insertPrivacies() { + for (int i = 1; i <= GIVEN_DATA_COUNT; i++) { + privacies.add(privacyRepository.save(Privacy.builder() + .privacyTitle(CONTENT_TITLE_PREFIX + "_" + i) + .privacyContent(CONTENT_CONTENT_PREFIX + "_" + i) + .useAt(i % 2 == 1) + .build())); + } + } + + /** + * 테스트 데이터 삭제 + */ + private void deletePrivacies() { + privacyRepository.deleteAll(privacies); + + privacies.clear(); + } + + /** + * 테스트 데이터 단건 등록 + * + * @return Privacy 개인정보처리방침 엔티티 + */ + private Privacy insertPrivacy() { + return privacyRepository.save(Privacy.builder() + .privacyTitle(INSERT_CONTENT_TITLE) + .privacyContent(INSERT_CONTENT_CONTENT) + .useAt(INSERT_USE_AT) + .build()); + } + + /** + * 테스트 데이터 단건 삭제 + */ + private void deletePrivacy(Integer privacyNo) { + privacyRepository.deleteById(privacyNo); + } + + /** + * 테스트 데이터 단건 조회 + * + * @param privacyNo 개인정보처리방침 번호 + * @return Optional 개인정보처리방침 엔티티 + */ + private Optional selectData(Integer privacyNo) { + return privacyRepository.findById(privacyNo); + } + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/statistics/StatisticsApiControllerTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/statistics/StatisticsApiControllerTest.java new file mode 100644 index 0000000..7da28b8 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/api/statistics/StatisticsApiControllerTest.java @@ -0,0 +1,87 @@ +package org.egovframe.cloud.portalservice.api.statistics; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.egovframe.cloud.portalservice.api.statistics.dto.StatisticsResponseDto; +import org.egovframe.cloud.portalservice.domain.statistics.Statistics; +import org.egovframe.cloud.portalservice.domain.statistics.StatisticsRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class StatisticsApiControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private StatisticsRepository statisticsRepository; + + + @BeforeEach + public void setup() { + for (int i = 0; i < 10; i++) { + statisticsRepository.save(Statistics.builder() + .siteId(1L) + .remoteIp("testip") + .build()); + } + + } + + @AfterEach + public void tearDown() { + statisticsRepository.deleteAll(); + } + + @Test + public void 월별접속통계_조회_성공() throws Exception { + Long siteId = 1L; + // when + ResponseEntity< List> responseEntity = + restTemplate.exchange("/api/v1/statistics/monthly/"+siteId, + HttpMethod.GET, + null, + new ParameterizedTypeReference>(){}); + + responseEntity.getBody().forEach(System.out::println); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody().size()).isEqualTo(1); + assertThat(responseEntity.getBody().get(0).getY()).isEqualTo(10); + } + + @Test + public void 일별접속통계_조회_성공() throws Exception { + Long siteId = 1L; + + // when + ResponseEntity< List> responseEntity = + restTemplate.exchange("/api/v1/statistics/daily/"+siteId+"?year=2021&month=9", + HttpMethod.GET, + null, + new ParameterizedTypeReference>(){}); + + responseEntity.getBody().forEach(System.out::println); + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(responseEntity.getBody().size()).isEqualTo(1); + assertThat(responseEntity.getBody().get(0).getY()).isEqualTo(10); + } + + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/ExceptionResponseTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/ExceptionResponseTest.java new file mode 100644 index 0000000..0cce9a3 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/ExceptionResponseTest.java @@ -0,0 +1,53 @@ +package org.egovframe.cloud.portalservice.config; + +import org.egovframe.cloud.common.exception.dto.ErrorResponse; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * org.egovframe.cloud.portalservice.config.ExceptionResponseTest + *

+ * ExceptionResponse 정상 동작 확인 테스트 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/16    jaeyeolkim  최초 생성
+ * 
+ */ +@SpringBootTest(webEnvironment = RANDOM_PORT) +public class ExceptionResponseTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private MessageSource messageSource; + + @Test + public void 해당_데이터가_존재하지_않습니다_오류메시지가_일치한다() throws Exception { + // given + String nonExistCode = "99999"; + + // when + ResponseEntity responseEntity = restTemplate.getForEntity("/api/v1/codes/" + nonExistCode, ErrorResponse.class); + ErrorResponse errorResponse = responseEntity.getBody(); + System.out.println("errorResponse.getStatus() =" + errorResponse.getStatus() + ", errorResponse.getMessage() =" + errorResponse.getMessage()); + assertThat(responseEntity.getStatusCode().value()).isEqualTo(errorResponse.getStatus()); + assertThat(errorResponse.getMessage()).isEqualTo(messageSource.getMessage("err.entity.not.found", null, LocaleContextHolder.getLocale())); + } +} diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/MessageSourceConfigTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/MessageSourceConfigTest.java new file mode 100644 index 0000000..c7c8477 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/MessageSourceConfigTest.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.portalservice.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.MessageSource; + +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +class MessageSourceConfigTest { + + @Autowired + TestRestTemplate restTemplate; + + @Test + public void 메세지를_외부위치에서_읽어온다() throws Exception { + // when + String message = restTemplate.getForObject("http://localhost:8000/portal-service/api/v1/messages/common.login/ko", String.class); + + // then + assertThat(message).isEqualTo("로그인"); + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/MessageSourceTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/MessageSourceTest.java new file mode 100644 index 0000000..1f242c2 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/config/MessageSourceTest.java @@ -0,0 +1,44 @@ +package org.egovframe.cloud.portalservice.config; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; + +/** + * org.egovframe.cloud.portalservice.config.MessageSourceTest + *

+ * MessageSource 정상 동작 확인 테스트 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/16    jaeyeolkim  최초 생성
+ * 
+ */ +@SpringBootTest +public class MessageSourceTest { + + @Autowired + MessageSource messageSource; + + @Test + public void 메세지_읽어온다() throws Exception { + // given + String messageCode = "err.invalid.input.value"; + String messageName = "입력값이 올바르지 않습니다."; + + // then + String message = messageSource.getMessage(messageCode, new Object[]{}, "default", LocaleContextHolder.getLocale()); + System.out.println("message = " + message); + Assertions.assertThat(message).isEqualTo(messageName); + } +} diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryTest.java new file mode 100644 index 0000000..779ad07 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/attachment/AttachmentRepositoryTest.java @@ -0,0 +1,195 @@ +package org.egovframe.cloud.portalservice.domain.attachment; + +import org.egovframe.cloud.portalservice.api.attachment.dto.AttachmentResponseDto; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import javax.persistence.EntityManager; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +class AttachmentRepositoryTest { + @Autowired + EntityManager em; + + @Autowired + AttachmentRepository attachmentRepository; + + /** + * 단위 테스트가 끝날때마다 수행되는 메소드 + * 테스트 데이터간 침범을 막기 위해 사용 + */ + @AfterEach + public void cleanUp() { + attachmentRepository.deleteAll(); + } + + @Test + public void 첨부파일_등록() throws Exception { + //given + AttachmentId attachmentId = AttachmentId.builder() + .code(UUID.randomUUID().toString()) + .seq(1L).build(); + + Attachment attachment = Attachment.builder() + .attachmentId(attachmentId) + .uniqueId(UUID.randomUUID().toString()) + .originalFileName("test.png") + .physicalFileName(UUID.randomUUID().toString()) + .size(1232L) + .build(); + + //when + Attachment save = attachmentRepository.save(attachment); + + //then + System.out.println(save); + assertThat(save.getAttachmentId().getSeq()).isEqualTo(1); + + } + + @Test + public void 여러건_등록() throws Exception { + //given + String code = UUID.randomUUID().toString(); + AttachmentId attachmentId1 = AttachmentId.builder() + .code(code) + .seq(1L).build(); + Attachment attachment1 = Attachment.builder() + .attachmentId(attachmentId1) + .uniqueId(UUID.randomUUID().toString()) + .originalFileName("test1.png") + .physicalFileName(UUID.randomUUID().toString()) + .size(1232L) + .build(); + + AttachmentId attachmentId2 = AttachmentId.builder() + .code(code) + .seq(2L) + .build(); + Attachment attachment2 = Attachment.builder() + .attachmentId(attachmentId2) + .uniqueId(UUID.randomUUID().toString()) + .originalFileName("test2.png") + .physicalFileName(UUID.randomUUID().toString()) + .size(1232L) + .build(); + + //when + attachmentRepository.save(attachment1); + attachmentRepository.save(attachment2); + + //then + List all = attachmentRepository.findAll(); + all.stream().forEach(System.out::println); + + } + + @Test + public void code로다건조회() throws Exception { + //given + String code = UUID.randomUUID().toString(); + for (Long i = 1L; i <= 5L; i++) { + AttachmentId attachmentId = AttachmentId.builder() + .code(code) + .seq(i) + .build(); + attachmentRepository.save( + Attachment.builder() + .attachmentId(attachmentId) + .uniqueId(UUID.randomUUID().toString()) + .physicalFileName(UUID.randomUUID().toString()) + .originalFileName("test_"+i+".txt") + .size(123L) + .build() + ); + } + //when + List attachments = attachmentRepository.findByCode(code); + + //then + assertThat(attachments.size()).isEqualTo(5); + attachments.stream().forEach(System.out::println); + } + + @Test + public void 대체키로한건조회() throws Exception { + //given + String code = UUID.randomUUID().toString(); + String id = ""; + for (Long i = 1L; i <= 5L; i++) { + AttachmentId attachmentId = AttachmentId.builder() + .code(code) + .seq(i) + .build(); + id = UUID.randomUUID().toString(); + + attachmentRepository.save( + Attachment.builder() + .attachmentId(attachmentId) + .uniqueId(id) + .physicalFileName(UUID.randomUUID().toString()) + .originalFileName("test_"+i+".txt") + .size(123L) + .build() + ); + } + + //when + Optional byId = attachmentRepository.findAllByUniqueId(id); + + //then + assertThat(byId.isPresent()).isTrue(); + assertThat(byId.get().getAttachmentId().getCode()).isEqualTo(code); + System.out.println(byId.get()); + } + + @Test + public void 중복된유니크인덱스_오류() throws Exception { + //given + String code = UUID.randomUUID().toString(); + String uniqueId = UUID.randomUUID().toString(); + AttachmentId attachmentId1 = AttachmentId.builder() + .code(code) + .seq(1L).build(); + Attachment attachment1 = Attachment.builder() + .attachmentId(attachmentId1) + .uniqueId(uniqueId) + .originalFileName("test1.png") + .physicalFileName(UUID.randomUUID().toString()) + .size(1232L) + .build(); + + AttachmentId attachmentId2 = AttachmentId.builder() + .code(code) + .seq(2L) + .build(); + Attachment attachment2 = Attachment.builder() + .attachmentId(attachmentId2) + .uniqueId(uniqueId) + .originalFileName("test2.png") + .physicalFileName(UUID.randomUUID().toString()) + .size(1232L) + .build(); + + //when + attachmentRepository.save(attachment1); + attachmentRepository.save(attachment2); + + //then + + } + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryTest.java new file mode 100644 index 0000000..0fda822 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/code/CodeRepositoryTest.java @@ -0,0 +1,132 @@ +package org.egovframe.cloud.portalservice.domain.code; + +import org.egovframe.cloud.portalservice.api.code.dto.*; +import org.egovframe.cloud.portalservice.service.code.CodeDetailService; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * org.egovframe.cloud.portalservice.api.code.CodeRepositoryTest + *

+ * 공통코드 CRUD 요청을 처리하는 JPA 테스트 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ * 
+ */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +class CodeRepositoryTest { + + @Autowired + EntityManager em; + + @Autowired + CodeRepository codeRepository; + + @Autowired + CodeDetailService codeDetailService; + + private static final String PARENT_CODE_ID = "TEST"; + private static final String PARENT_CODE_NAME = "테스트"; + + @Test + @Order(Integer.MIN_VALUE) + public void given() throws Exception { + codeRepository.save( + Code.builder() + .codeId(PARENT_CODE_ID) + .codeName(PARENT_CODE_NAME) + .readonly(false) + .sortSeq(1) + .useAt(true) + .build() + ); + + for (int i = 1; i <= 30; i++) { + codeRepository.save( + Code.builder() + .parentCodeId(PARENT_CODE_ID) + .codeId(PARENT_CODE_ID + "_" + i) + .codeName(PARENT_CODE_NAME + "_" + i) + .readonly(false) + .sortSeq(i) + .useAt(true) + .build() + ); + } + } + + @Test + @Order(Integer.MAX_VALUE) + public void cleanup() throws Exception { + codeRepository.deleteAll(); + } + + @Test + public void 공통코드상세_목록_조회된다() throws Exception { + // given + CodeDetailRequestDto requestDto = CodeDetailRequestDto.builder() + .parentCodeId(PARENT_CODE_ID) + .keywordType("codeId") + .keyword(PARENT_CODE_ID) + .build(); + + // when + Page results = codeRepository.findAllDetailByKeyword(requestDto, PageRequest.of(1, 5)); + for (CodeDetailListResponseDto result : results) { + System.out.println("result = " + result.getCodeId()); + } + + // then + assertThat(results.getTotalPages()).isEqualTo(30/5); + assertThat(results.getTotalElements()).isEqualTo(30); + } + + @Test + public void 공통코드상세_단건_조회된다() throws Exception { + // when + Code code = codeRepository.findByCodeId(PARENT_CODE_ID + "_1").get(); + + // then + assertThat(code.getParentCodeId()).isEqualTo(PARENT_CODE_ID); + } + + @Transactional + @Test + public void 공통코드상세_수정된다() throws Exception { + // given + + // when + Code code = codeRepository.findByCodeId(PARENT_CODE_ID + "_1").get(); + code.updateDetail(code.getParentCodeId(), "수정", code.getCodeDescription(), 100, true); + em.persist(code); + + Code updateCode = codeRepository.findByCodeId(PARENT_CODE_ID + "_1").get(); + + // then + assertThat(updateCode.getCodeName()).isEqualTo("수정"); + assertThat(updateCode.getSortSeq()).isEqualTo(100); + assertThat(updateCode.getUseAt()).isTrue(); + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryTest.java new file mode 100644 index 0000000..9409c02 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/menu/MenuRepositoryTest.java @@ -0,0 +1,131 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +//@Import(TestConfig.class) +class MenuRepositoryTest { + + @Autowired + private EntityManager em; + + @Autowired + MenuRepository menuRepository; + + @Autowired + SiteRepository siteRepository; + + @BeforeEach + public void setup() { + //given + Site site1 = Site.builder().name("site1").isUse(true).build(); + Site site2 = Site.builder().name("site2").isUse(true).build(); + List sites = new ArrayList<>(); + sites.add(site1); + sites.add(site2); + siteRepository.saveAll(sites); + } + + @Test + public void 새로운메뉴_한건_등록() throws Exception { + Site site = siteRepository.findAll().get(0); + + Menu menu = Menu.builder() + .menuKorName("testMenu") + .sortSeq(1) + .parent(null) + .site(site) + .build(); + + //when + Menu savedMenu = menuRepository.save(menu); + + //then + assertThat(savedMenu.getMenuKorName()).isEqualTo(menu.getMenuKorName()); + System.out.println(savedMenu.toString()); + } + + @Test + @Transactional + public void 새로운_하위메뉴여러개_등록() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + + Menu parentMenu = Menu.builder() + .menuKorName("parent") + .sortSeq(1) + .site(site) + .parent(null) + .build(); + + em.persist(parentMenu); + em.flush(); + + //when + List menus = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + Menu child = Menu.builder() + .menuKorName("child_" + i) + .site(site) + .parent(parentMenu) + .sortSeq(i + 1) + .build(); + child.setParentMenu(parentMenu); + menus.add(child); + } + + List savedMenus = menuRepository.saveAll(menus); + + //then + assertThat(savedMenus.size()).isEqualTo(3); + savedMenus.stream().forEach(System.out::println); + assertThat(savedMenus.get(0).getParent().getId()).isEqualTo(parentMenu.getId()); + assertThat(savedMenus.get(0).getParent()).isSameAs(parentMenu); + + } + + @Test + @Transactional + public void 계층구조_메뉴_조회() throws Exception { + //given + Site site = siteRepository.findAll().get(0); + + Menu parentMenu = Menu.builder() + .menuKorName("parent") + .sortSeq(1) + .site(site) + .build(); + em.persist(parentMenu); + em.flush(); + + for (int i = 0; i < 3; i++) { + Menu childMenu = Menu.builder() + .menuKorName("child_" + i) + .site(site) + .parent(parentMenu) + .sortSeq(i + 1) + .build(); + childMenu.setParentMenu(parentMenu); + em.persist(childMenu); + } + em.flush(); + + Menu menu = menuRepository.findById(parentMenu.getId()).get(); + + menu.getChildren().stream().forEach(System.out::println); + assertThat(menu.getChildren().size()).isEqualTo(3); + + } + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryTest.java new file mode 100644 index 0000000..105401d --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/menu/MenuRoleRepositoryTest.java @@ -0,0 +1,92 @@ +package org.egovframe.cloud.portalservice.domain.menu; + +import org.assertj.core.api.Assertions; +import org.egovframe.cloud.portalservice.api.menu.dto.MenuRoleResponseDto; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class MenuRoleRepositoryTest { + @Autowired + private EntityManager em; + + @Autowired + MenuRepository menuRepository; + + @Autowired + SiteRepository siteRepository; + + @Autowired + MenuRoleRepository menuRoleRepository; + + @BeforeEach + public void setup() { + //given + Site site = Site.builder().name("site1").isUse(true).build(); + siteRepository.save(site); + + + } + + @AfterEach + public void tearDown() { + siteRepository.deleteAll(); + menuRepository.deleteAll(); + menuRoleRepository.deleteAll(); + } + + @Test + @Transactional + public void 권한별메뉴_트리_조회한다() throws Exception { + Site site = siteRepository.findAll().get(0); + //given + Menu parent = Menu.builder() + .menuKorName("parent") + .sortSeq(1) + .site(site) + .isShow(true) + .isUse(true) + .level(1) + .parent(null) + .build(); + + em.persist(parent); + em.flush(); + + //when + for (int i = 0; i < 3; i++) { + Menu child = Menu.builder() + .menuKorName("child_" + i) + .site(site) + .parent(parent) + .sortSeq(i + 1) + .level(2) + .build(); + child.setParentMenu(parent); + em.persist(child); + } + em.flush(); + + List tree = menuRoleRepository.findTree("role", site.getId()); + + Assertions.assertThat(tree.size()).isEqualTo(1); + Assertions.assertThat(tree.get(0).getChildren().size()).isEqualTo(3); + tree.stream().forEach(menuRoleResponseDto -> { + System.out.println(menuRoleResponseDto); + menuRoleResponseDto.getChildren().stream().forEach(System.out::println); + }); + } + + +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryTest.java new file mode 100644 index 0000000..d1c053a --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/message/MessageRepositoryTest.java @@ -0,0 +1,62 @@ +package org.egovframe.cloud.portalservice.domain.message; + +import org.egovframe.cloud.portalservice.api.message.dto.MessageListResponseDto; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +class MessageRepositoryTest { + + @Autowired + private MessageRepository messageRepository; + + @BeforeEach + @Transactional + public void 메세지_입력된다() throws Exception { + messageRepository.save(Message.builder().messageId("test.one").messageKoName("테스트1").build()); + messageRepository.save(Message.builder().messageId("test.two").messageKoName("테스트2").build()); + messageRepository.save(Message.builder().messageId("test.three").messageKoName("테스트3").messageEnName("TEST3").build()); + } + + @AfterEach + @Transactional + public void 메세지_삭제된다() throws Exception { + messageRepository.deleteAll(); + } + + @Test + public void 메세지_한글명_목록_조회된다() throws Exception { + List results = messageRepository.findAllMessages("ko"); + System.out.println("results.size() = " + results.size()); + assertThat(results.size()).isEqualTo(3); + assertThat(results.get(0).getMessageId()).isEqualTo("test.one"); + results.forEach(messageListResponseDto -> System.out.println("id = " + messageListResponseDto.getMessageId() + ", messageName() = " + messageListResponseDto.getMessageName())); + } + + @Test + public void 메세지_영문명_목록_조회된다() throws Exception { + List results = messageRepository.findAllMessages("en"); + System.out.println("results.size() = " + results.size()); + assertThat(results.size()).isEqualTo(3); + assertThat(results.get(1).getMessageName()).isEqualTo("TEST3"); + results.forEach(messageListResponseDto -> System.out.println("id = " + messageListResponseDto.getMessageId() + ", messageName() = " + messageListResponseDto.getMessageName())); + } + + @Test + public void 메세지_한글명_목록_Map으로_조회된다() throws Exception { + Map results = messageRepository.findAllMessagesMap("ko"); + System.out.println("results.size() = " + results.size()); + assertThat(results.size()).isEqualTo(3); + results.keySet().forEach(key -> System.out.println(key + "=" + results.get(key))); + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryTest.java new file mode 100644 index 0000000..6ef7be7 --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/policy/PolicyRepositoryTest.java @@ -0,0 +1,194 @@ +package org.egovframe.cloud.portalservice.domain.policy; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.portalservice.api.policy.dto.PolicyResponseDto; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; + +import java.time.ZonedDateTime; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@SpringBootTest +class PolicyRepositoryTest { + + @Autowired + EntityManager em; + + @Autowired + PolicyRepository policyRepository; + + /** + * 단위 테스트가 끝날때마다 수행되는 메소드 + * 테스트 데이터간 침범을 막기 위해 사용 + */ + @AfterEach + public void cleanUp() { + policyRepository.deleteAll(); + } + + + @Test + public void 목록조회한다() throws Exception { + //given + for (int i = 0; i < 2; i++) { + String title = "title_"+i; + String contentStr = "contents " + i; + String type = "TOS"; + if(i > 0){ + type = "PP"; + } + + policyRepository.save(Policy.builder() + .type(type) + .title(title) + .contents(contentStr) + .build()); + } + + //when + Page search = policyRepository.search(RequestDto.builder().build(), PageRequest.of(1, 10)); + + //then + assertThat(search.getTotalElements()).isEqualTo(2); + + } + + @Test + public void 이용약관_조건부조회한다() { + //given + for (int i = 1; i <= 10; i++) { + String title = "title_"+i; + String contentStr = "contents " + i; + String type = "TOS"; + if(i % 2 == 0){ + type = "PP"; + } + + policyRepository.save(Policy.builder() + .type(type) + .title(title) + .contents(contentStr) + .build()); + } + + RequestDto requestDto = RequestDto.builder() + .keywordType("title") + .keyword("title_2") + .build(); + + //when + Page search = policyRepository.search(requestDto, PageRequest.of(1, 10)); + + //then + assertThat(search.getTotalElements()).isEqualTo(1); + + } + + @Test + @Transactional + public void 회원가입시_이용약관_한건조회_정상() throws Exception { + //given + for (int i = 1; i <= 10; i++) { + String title = "title_"+i; + String contentStr = "contents " + i; + String type = "TOS"; + if(i % 2 == 0){ + type = "PP"; + } + + + policyRepository.save(Policy.builder() + .type(type) + .title(title) + .isUse(true) + .regDate(ZonedDateTime.now()) + .contents(contentStr) + .build()); + } + + //when + PolicyResponseDto responseDto = policyRepository.searchOne("TOS"); + + //then + System.out.println(responseDto.toString()); + + } + + @Test + public void ID로_한건조회() throws Exception { + //given + for (int i = 1; i <= 10; i++) { + String title = "title_"+i; + String contentStr = "contents " + i; + String type = "TOS"; + if(i % 2 == 0){ + type = "PP"; + } + + policyRepository.save(Policy.builder() + .type(type) + .title(title) + .contents(contentStr) + .build()); + } + + //when + Optional optionalTerms = policyRepository.findById(1L); + + //then + System.out.println(optionalTerms.get()); + assertThat(optionalTerms.get().getTitle()).isEqualTo("title_1"); + assertThat(optionalTerms.get().getContents()).isEqualTo("contents 1"); + + } + + @Test + @Transactional + public void 이용약관_수정한다() throws Exception { + String updateTitle = "update title"; + String updateContents = "update Contents"; + Long id = 1L; + //given + for (int i = 1; i <= 10; i++) { + String title = "title_"+i; + String contentStr = "contents " + i; + String type = "TOS"; + if(i % 2 == 0){ + type = "PP"; + } + + id = policyRepository.save(Policy.builder() + .type(type) + .title(title) + .contents(contentStr) + .build()).getId(); + } + + //when + Optional optional = policyRepository.findById(id); + Policy policy = optional.get(); + + policy.update(updateTitle, true, updateContents); + em.persist(policy); + + //then + Optional updated = policyRepository.findById(id); + + assertThat(updated.get().getTitle()).isEqualTo(updateTitle); + assertThat(updated.get().getContents()).isEqualTo(updateContents); + + System.out.println(updated.get()); + + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryTest.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryTest.java new file mode 100644 index 0000000..656508d --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/domain/statistics/StatisticsRepositoryTest.java @@ -0,0 +1,45 @@ +package org.egovframe.cloud.portalservice.domain.statistics; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.persistence.EntityManager; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class StatisticsRepositoryTest { + @Autowired + EntityManager em; + + @Autowired + StatisticsRepository statisticsRepository; + + /** + * 단위 테스트가 끝날때마다 수행되는 메소드 + * 테스트 데이터간 침범을 막기 위해 사용 + */ + @AfterEach + public void cleanUp() { + statisticsRepository.deleteAll(); + } + + @Test + public void 접속통계로그_입력된다() throws Exception { + // given + String statisticsId = UUID.randomUUID().toString(); + Statistics log = Statistics.builder() + .statisticsId(statisticsId) + .siteId(1L) + .build(); + + // when + Statistics analyticsLog = statisticsRepository.save(log); + + // then + assertThat(analyticsLog.getStatisticsId()).isEqualTo(statisticsId); + } +} \ No newline at end of file diff --git a/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/util/RestResponsePage.java b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/util/RestResponsePage.java new file mode 100644 index 0000000..811491d --- /dev/null +++ b/backend/portal-service/src/test/java/org/egovframe/cloud/portalservice/util/RestResponsePage.java @@ -0,0 +1,87 @@ +package org.egovframe.cloud.portalservice.util; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.Collections; +import java.util.List; + +/** + * org.egovframe.cloud.portalservice.util.RestResponsePage + *

+ * 페이지 API 조회 시 JSON 형식의 응답 데이터를 페이지 객체를 구현하여 마이그레이션 해주는 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +public class RestResponsePage extends PageImpl { + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + * @param number 페이지 번호 + * @param size 조회할 데이터 수 + * @param totalElements 총 데이터 수 + * @param pageable 페이지 정보 + * @param last 마지막 + * @param totalPages 총 페이지 + * @param sort 정렬 + * @param first 처음 + * @param numberOfElements 조회된 데이터 수 + */ + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public RestResponsePage(@JsonProperty("content") List content, + @JsonProperty("number") int number, + @JsonProperty("size") int size, + @JsonProperty("totalElements") Long totalElements, + @JsonProperty("pageable") JsonNode pageable, + @JsonProperty("last") boolean last, + @JsonProperty("totalPages") int totalPages, + @JsonProperty("sort") JsonNode sort, + @JsonProperty("first") boolean first, + @JsonProperty("numberOfElements") int numberOfElements) { + super(content, PageRequest.of(number, size), totalElements); + } + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + * @param pageable 페이지 정보 + * @param total 총 데이터 수 + */ + public RestResponsePage(List content, Pageable pageable, long total) { + super(content, pageable, total); + } + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + */ + public RestResponsePage(List content) { + super(content); + } + + /** + * Rest 응답 페이지 생성자 + */ + public RestResponsePage() { + super(Collections.emptyList()); + } + +} diff --git a/backend/portal-service/src/test/resources/application-test.yml b/backend/portal-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..0f8ec39 --- /dev/null +++ b/backend/portal-service/src/test/resources/application-test.yml @@ -0,0 +1,44 @@ +spring: + application: + name: portal-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: + hibernate: + generate-ddl: true + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/backend/reserve-check-service/Dockerfile b/backend/reserve-check-service/Dockerfile new file mode 100644 index 0000000..bd86ccf --- /dev/null +++ b/backend/reserve-check-service/Dockerfile @@ -0,0 +1,15 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine + +# config server uri: dockder run --e 로 변경 가능 +ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar app.jar +# application port +#EXPOSE 8000 +# 실행 (application-cf.yml 프로필이 기본값) +CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "app.jar"] diff --git a/backend/reserve-check-service/build.gradle b/backend/reserve-check-service/build.gradle new file mode 100644 index 0000000..8403626 --- /dev/null +++ b/backend/reserve-check-service/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'org.springframework.boot' version '2.4.5' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'org.egovframe.cloud' +version = '0.1' +sourceCompatibility = '1.8' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() + maven { url "https://maven.egovframe.go.kr/maven/" } // egovframe maven 원격 저장소 +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { +// implementation files('../../module-common/build/libs/module-common-0.1.jar') // @ComponentScan(basePackages={"org.egovframe.cloud"}) 추가해야 적용된다 + implementation 'org.egovframe.cloud:module-common:0.1' + 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-r2dbc' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-config' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j' + implementation 'com.playtika.reactivefeign:feign-reactor-spring-cloud-starter:3.1.0' + + //messaging + implementation 'org.springframework.cloud:spring-cloud-stream' + implementation 'org.springframework.cloud:spring-cloud-stream-binder-rabbit' + implementation 'net.java.dev.jna:jna:5.9.0' // byte-buddy (No compatible attachment provider is available.) + + implementation 'io.jsonwebtoken:jjwt:0.9.1' + + implementation 'dev.miku:r2dbc-mysql:0.8.2.RELEASE' + implementation 'mysql:mysql-connector-java' + + // swagger api docs + implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.5.10' + + // bolcking 호출 감지 + implementation 'io.projectreactor:reactor-tools:3.4.9' + implementation 'io.projectreactor.tools:blockhound:1.0.6.RELEASE' + + //lombok + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + testImplementation 'com.h2database:h2' + testImplementation 'io.r2dbc:r2dbc-h2' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +test { + useJUnitPlatform() +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} \ No newline at end of file diff --git a/backend/reserve-check-service/gradlew b/backend/reserve-check-service/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/backend/reserve-check-service/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 + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + 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/reserve-check-service/gradlew.bat b/backend/reserve-check-service/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/backend/reserve-check-service/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/reserve-check-service/manifest.yml b/backend/reserve-check-service/manifest.yml new file mode 100644 index 0000000..c9b631c --- /dev/null +++ b/backend/reserve-check-service/manifest.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: egov-reserve-check-service # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-reserve-check-service # host 명으로 유일해야 함 + path: build/libs/reserve-check-service-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + services: + - egov-discovery-provided-service # discovery service binding + env: + spring_profiles_active: cf + spring_cloud_config_uri: https://egov-config.paas-ta.org + app_name: egov-reserve-check-service # logstash custom app name + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/backend/reserve-check-service/settings.gradle b/backend/reserve-check-service/settings.gradle new file mode 100644 index 0000000..6428ab3 --- /dev/null +++ b/backend/reserve-check-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'reserve-check-service' diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/ReserveCheckSeviceApplication.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/ReserveCheckSeviceApplication.java new file mode 100644 index 0000000..fd7bfdb --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/ReserveCheckSeviceApplication.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.reservechecksevice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.ComponentScan; + +import reactivefeign.spring.config.EnableReactiveFeignClients; +import reactor.blockhound.BlockHound; + +import java.security.Security; + +@ComponentScan({"org.egovframe.cloud.common", "org.egovframe.cloud.reactive", "org.egovframe.cloud.reservechecksevice"}) // org.egovframe.cloud.common package 포함하기 위해 +@EnableDiscoveryClient +@EnableReactiveFeignClients +@SpringBootApplication +public class ReserveCheckSeviceApplication { + + public static void main(String[] args) { + // TLSv1/v1.1 No longer works after upgrade, "No appropriate protocol" error + String property = Security.getProperty("jdk.tls.disabledAlgorithms").replace(", TLSv1", "").replace(", TLSv1.1", ""); + Security.setProperty("jdk.tls.disabledAlgorithms", property); + + //blocking 코드 감지 + BlockHound.builder() + //mysql r2dbc 에서 호출되는 FileInputStream.readBytes() 가 블로킹코드인데 이를 허용해주도록 한다. + //해당 코드가 어디서 호출되는지 알지 못하는 상태에서 FileInputStream.readBytes() 자체를 허용해주는 것은 좋지 않다. + // 누군가 무분별하게 사용하게 되면 검출해 낼 수ㅂ 없어 시스템의 위험요소로 남게 된다. + // r2dbc를 사용하기 위해 해당 호출부분만 허용하고 나머지는 여전히 검출대상으로 남기도록 한다. + .allowBlockingCallsInside("dev.miku.r2dbc.mysql.client.ReactorNettyClient", "init") + .install(); + + SpringApplication.run(ReserveCheckSeviceApplication.class, args); + } + +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/ReserveApiController.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/ReserveApiController.java new file mode 100644 index 0000000..0e3c9c4 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/ReserveApiController.java @@ -0,0 +1,174 @@ +package org.egovframe.cloud.reservechecksevice.api.reserve; + +import java.time.LocalDate; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.*; +import org.egovframe.cloud.reservechecksevice.domain.reserve.Category; +import org.egovframe.cloud.reservechecksevice.service.reserve.ReserveService; +import org.springframework.core.env.Environment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.validation.Valid; + +/** + * org.egovframe.cloud.reservechecksevice.api.reserve.ReserveApiController + *

+ * 예약 확인 rest controller class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class ReserveApiController { + + private final ReserveService reserveService; + + private final Environment env; + + /** + * 서비스 상태 확인 + * + * @return + */ + @GetMapping("/actuator/health-info") + public String status() { + return String.format("GET Reserve Check Service on" + + "\n local.server.port :" + env.getProperty("local.server.port") + + "\n egov.message :" + env.getProperty("egov.message") + ); + } + + /** + * 예약 확인(신청) 목록 조회 + * 관리자인 경우 모두 조회 + * + * @param requestDto + * @param page + * @param size + * @return + */ + @GetMapping("/api/v1/reserves") + @ResponseStatus(HttpStatus.OK) + public Mono> search(ReserveRequestDto requestDto, + @RequestParam(name = "page") int page, + @RequestParam(name = "size") int size) { + return reserveService.search(requestDto, PageRequest.of(page, size)); + } + + /** + * 사용자별 예약 목록 조회 + * + * @param userId + * @param requestDto + * @param page + * @param size + * @return + */ + @GetMapping("/api/v1/{userId}/reserves") + @ResponseStatus(HttpStatus.OK) + public Mono> searchForUser(@PathVariable String userId, + ReserveRequestDto requestDto, + @RequestParam(name = "page") int page, + @RequestParam(name = "size") int size) { + return reserveService.searchForUser(userId, requestDto, PageRequest.of(page, size)); + } + + /** + * 예약 한건 조회 + * + * @param reserveId + * @return + */ + @GetMapping("/api/v1/reserves/{reserveId}") + @ResponseStatus(HttpStatus.OK) + public Mono findById(@PathVariable String reserveId) { + return reserveService.findReserveById(reserveId); + } + + /** + * 예약 취소 + * + * @param reserveId + * @return + */ + @PutMapping("/api/v1/reserves/cancel/{reserveId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public Mono cancel(@PathVariable String reserveId, @RequestBody ReserveCancelRequestDto cancelRequestDto) { + return reserveService.cancel(reserveId, cancelRequestDto); + } + + /** + * 예약 승인 + * + * @param reserveId + * @return + */ + @PutMapping("/api/v1/reserves/approve/{reserveId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public Mono approve(@PathVariable String reserveId) { + return reserveService.approve(reserveId); + } + + /** + * 예약 정보 수정 + * + * @param reserveId + * @param updateRequestDto + * @return + */ + @PutMapping("/api/v1/reserves/{reserveId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public Mono update(@PathVariable String reserveId, @Valid @RequestBody ReserveUpdateRequestDto updateRequestDto) { + return reserveService.update(reserveId, updateRequestDto).then(); + } + + /** + * 관리자 예약 신청 + * 관리자의 경우 실시간이어도 이벤트 스트림 거치지 않고 바로 예약 처리 + * + * @param saveRequestDto + * @return + */ + @PostMapping("/api/v1/reserves") + @ResponseStatus(HttpStatus.CREATED) + public Mono create(@Valid @RequestBody ReserveSaveRequestDto saveRequestDto) { + return reserveService.create(saveRequestDto); + } + + /** + * 예약물품 별 조회기간 내 예약 목록 조회 + * + * @param reserveItemId + * @return + */ + @GetMapping("/api/v1/reserves/{reserveItemId}/inventories") + @ResponseStatus(HttpStatus.OK) + public Mono countInventory(@PathVariable Long reserveItemId, + @RequestParam(name = "startDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, + @RequestParam(name = "endDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) { + return reserveService.countInventory(reserveItemId,startDate.atTime(1,1), endDate.atTime(1,1)); + } + +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveCancelRequestDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveCancelRequestDto.java new file mode 100644 index 0000000..cdc0a0a --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveCancelRequestDto.java @@ -0,0 +1,38 @@ +package org.egovframe.cloud.reservechecksevice.api.reserve.dto; + +import javax.validation.constraints.NotBlank; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveCancelRequestDto + *

+ * 예약 취소 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/10/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/10/06    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveCancelRequestDto { + @NotBlank + private String reasonCancelContent; + + @Builder + public ReserveCancelRequestDto(String reasonCancelContent) { + this.reasonCancelContent = reasonCancelContent; + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveListResponseDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveListResponseDto.java new file mode 100644 index 0000000..0d6577f --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveListResponseDto.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.reservechecksevice.api.reserve.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reservechecksevice.domain.reserve.Reserve; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveListResponseDto + *

+ * 예약 목록 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveListResponseDto { + + private String reserveId; + + private Long locationId; + private String categoryId; + private Long reserveItemId; + private String reserveItemName; + private Integer totalQty; + private Integer reserveQty; + + private String userId; + private String userName; + + private String reserveStatusId; + private LocalDateTime createDate; + + @Builder + public ReserveListResponseDto(Reserve entity) { + this.reserveId = entity.getReserveId(); + this.locationId = entity.getLocationId(); + this.categoryId = entity.getCategoryId(); + this.reserveItemId = entity.getReserveItemId(); + this.reserveItemName = entity.getReserveItem().getReserveItemName(); + this.totalQty = entity.getReserveItem().getTotalQty(); + this.reserveQty = entity.getReserveQty(); + this.userId = entity.getUserId(); + this.userName = entity.getUser().getUserName(); + this.reserveStatusId = entity.getReserveStatusId(); + this.createDate = entity.getCreateDate(); + } + +} + + diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveRequestDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveRequestDto.java new file mode 100644 index 0000000..9c9e361 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveRequestDto.java @@ -0,0 +1,29 @@ +package org.egovframe.cloud.reservechecksevice.api.reserve.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; + +/** + * org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveRequestDto + *

+ * 얘약 목록 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/27 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/27    shinmj      최초 생성
+ * 
+ */ +@NoArgsConstructor +@Getter +public class ReserveRequestDto extends RequestDto { + private Long locationId; + private String categoryId; +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveResponseDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveResponseDto.java new file mode 100644 index 0000000..52c73cf --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveResponseDto.java @@ -0,0 +1,69 @@ +package org.egovframe.cloud.reservechecksevice.api.reserve.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reservechecksevice.client.dto.ReserveItemRelationResponseDto; +import org.egovframe.cloud.reservechecksevice.domain.reserve.Reserve; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveResponseDto + *

+ * 예약 확인(신청) 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveResponseDto { + + private String reserveId; + + private Long reserveItemId; + private ReserveItemRelationResponseDto reserveItem; + + private Integer reserveQty; + private LocalDateTime reserveStartDate; + private LocalDateTime reserveEndDate; + private String reservePurposeContent; + private String attachmentCode; + + private String reserveStatusId; + + private String userId; + private String userName; + private String userContactNo; + private String userEmail; + + @Builder + public ReserveResponseDto(Reserve entity) { + this.reserveId = entity.getReserveId(); + this.reserveItemId = entity.getReserveItemId(); + this.reserveItem = ReserveItemRelationResponseDto.builder().entity(entity.getReserveItem()).build(); + this.reserveQty = entity.getReserveQty(); + this.reserveStartDate = entity.getReserveStartDate(); + this.reserveEndDate = entity.getReserveEndDate(); + this.reservePurposeContent = entity.getReservePurposeContent(); + this.attachmentCode = entity.getAttachmentCode(); + this.reserveStatusId = entity.getReserveStatusId(); + this.userId = entity.getUserId(); + this.userName = entity.getUser().getUserName(); + this.userContactNo = entity.getUserContactNo(); + this.userEmail = entity.getUserEmail(); + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveSaveRequestDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveSaveRequestDto.java new file mode 100644 index 0000000..7eaa3b6 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveSaveRequestDto.java @@ -0,0 +1,99 @@ +package org.egovframe.cloud.reservechecksevice.api.reserve.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.With; + +import org.egovframe.cloud.reservechecksevice.domain.reserve.Reserve; +import org.egovframe.cloud.reservechecksevice.validator.annotation.ReserveSaveValid; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveSaveRequestDto + *

+ * 예약 신청 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@ReserveSaveValid +public class ReserveSaveRequestDto { + + @Setter + private String reserveId; + + @NotNull + private Long reserveItemId; + private Long locationId; + private String categoryId; + private Integer reserveQty; //예약 신청 인원/수량 + + @NotNull + private String reservePurposeContent; //예약 목적 + private String attachmentCode; //첨부파일 코드 + private LocalDateTime reserveStartDate; //예약 신청 시작일 + private LocalDateTime reserveEndDate; //예약 신청 종료일 + private String reserveStatusId; //예약상태 - 공통코드(reserve-status) + + @NotNull + private String userId; //예약자 + + @NotNull + private String userContactNo; //예약자 연락처 + + @NotNull + private String userEmail; //예약자 이메일 + + @Builder + public ReserveSaveRequestDto(Long reserveItemId, Long locationId, String categoryId, Integer reserveQty, String reservePurposeContent, String attachmentCode, LocalDateTime reserveStartDate, LocalDateTime reserveEndDate, String reserveStatusId, String userId, String userContactNo, String userEmail) { + this.reserveItemId = reserveItemId; + this.locationId = locationId; + this.categoryId = categoryId; + this.reserveQty = reserveQty; + this.reservePurposeContent = reservePurposeContent; + this.attachmentCode = attachmentCode; + this.reserveStartDate = reserveStartDate; + this.reserveEndDate = reserveEndDate; + this.reserveStatusId = reserveStatusId; + this.userId = userId; + this.userContactNo = userContactNo; + this.userEmail = userEmail; + } + + public Reserve toEntity() { + Reserve reserve = Reserve.builder() + .reserveId(this.reserveId) + .reserveItemId(this.reserveItemId) + .reserveQty(this.reserveQty) + .reservePurposeContent(this.reservePurposeContent) + .attachmentCode(this.attachmentCode) + .reserveStartDate(this.reserveStartDate) + .reserveEndDate(this.reserveEndDate) + .reserveStatusId(this.reserveStatusId) + .userId(this.userId) + .userContactNo(this.userContactNo) + .userEmail(this.userEmail) + .build(); + reserve.setLocationId(this.locationId); + reserve.setCategoryId(this.categoryId); + + return reserve; + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveUpdateRequestDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveUpdateRequestDto.java new file mode 100644 index 0000000..50efc06 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/api/reserve/dto/ReserveUpdateRequestDto.java @@ -0,0 +1,68 @@ +package org.egovframe.cloud.reservechecksevice.api.reserve.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reservechecksevice.validator.annotation.ReserveSaveValid; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveUpdateRequestDto + *

+ * 예약 신청 정보 수정 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@ReserveSaveValid +public class ReserveUpdateRequestDto { + + @NotNull + private Long reserveItemId; + private String categoryId; + private Integer reserveQty; //예약 신청 인원/수량 + + @NotNull + private String reservePurposeContent; //예약 목적 + private String attachmentCode; //첨부파일 코드 + private LocalDateTime reserveStartDate; //예약 신청 시작일 + private LocalDateTime reserveEndDate; //예약 신청 종료일 + + @NotNull + private String userId; //예약자 + + @NotNull + private String userContactNo; //예약자 연락처 + + @NotNull + private String userEmail; //예약자 이메일 + + @Builder + public ReserveUpdateRequestDto(Long reserveItemId, String categoryId, Integer reserveQty, String reservePurposeContent, String attachmentCode, LocalDateTime reserveStartDate, LocalDateTime reserveEndDate, String userId, String userContactNo, String userEmail) { + this.reserveItemId = reserveItemId; + this.categoryId = categoryId; + this.reserveQty = reserveQty; + this.reservePurposeContent = reservePurposeContent; + this.attachmentCode = attachmentCode; + this.reserveStartDate = reserveStartDate; + this.reserveEndDate = reserveEndDate; + this.userId = userId; + this.userContactNo = userContactNo; + this.userEmail = userEmail; + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/ReserveItemServiceClient.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/ReserveItemServiceClient.java new file mode 100644 index 0000000..2bbb0b9 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/ReserveItemServiceClient.java @@ -0,0 +1,59 @@ +package org.egovframe.cloud.reservechecksevice.client; + +import org.egovframe.cloud.reservechecksevice.client.dto.ReserveItemRelationResponseDto; +import org.egovframe.cloud.reservechecksevice.client.dto.ReserveItemResponseDto; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import reactivefeign.spring.config.ReactiveFeignClient; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reservechecksevice.client.ReserveItemServiceClient + *

+ * 예약 물품 서비스와 통신하는 feign client interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/23    shinmj  최초 생성
+ * 
+ */ +@ReactiveFeignClient(value = "reserve-item-service") +public interface ReserveItemServiceClient { + /** + * 예약 물품 한건 조회 + * + * @param reserveItemId + * @return + */ + @GetMapping("/api/v1/reserve-items/{reserveItemId}") + Mono findById(@PathVariable("reserveItemId") Long reserveItemId); + + /** + * 예약 물품 한건 조회 시 연결된 공통코드, 지역 정보 조회 + * + * @param reserveItemId + * @return + */ + @GetMapping("/api/v1/reserve-items/relations/{reserveItemId}") + Mono findByIdWithRelations(@PathVariable("reserveItemId") Long reserveItemId); + + /** + * 관리자가 예약 신청 시 이벤트 스트림 없이 바로 재고 변경 + * + * @param reserveItemId + * @param reserveQty + * @return + */ + @PutMapping("/api/v1/reserve-items/{reserveItemId}/inventories") + Mono updateInventory(@PathVariable("reserveItemId") Long reserveItemId, @RequestBody Integer reserveQty); +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/UserServiceClient.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/UserServiceClient.java new file mode 100644 index 0000000..26346fa --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/UserServiceClient.java @@ -0,0 +1,39 @@ +package org.egovframe.cloud.reservechecksevice.client; + +import org.egovframe.cloud.reservechecksevice.client.dto.UserResponseDto; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import reactivefeign.spring.config.ReactiveFeignClient; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reservechecksevice.client.UserServiceClient + *

+ * 사용자 서비스와 통신하는 feign client interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/30    shinmj  최초 생성
+ * 
+ */ +@ReactiveFeignClient(value = "user-service") +public interface UserServiceClient { + + + /** + * 사용자 단 건 조회 + * + * @param userId + * @return + */ + @GetMapping("/api/v1/users/{userId}") + Mono findByUserId(@PathVariable("userId") String userId); +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/ReserveItemRelationResponseDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/ReserveItemRelationResponseDto.java new file mode 100644 index 0000000..e951eb9 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/ReserveItemRelationResponseDto.java @@ -0,0 +1,147 @@ +package org.egovframe.cloud.reservechecksevice.client.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import org.egovframe.cloud.reservechecksevice.domain.location.Location; +import org.egovframe.cloud.reservechecksevice.domain.reserve.ReserveItem; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reservechecksevice.client.dto.ReserveItemRelationResponseDto + *

+ * 얘약 물품 feign client 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/27 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/27    shinmj      최초 생성
+ * 
+ */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +public class ReserveItemRelationResponseDto { + private Long reserveItemId; // 예약 물품 id + private String reserveItemName; //예약 물품 명 + private Long locationId; + private Location location; + private String categoryId; //예약유형 - 공통코드 reserve-category + private String categoryName; + private Integer totalQty; //총 재고/수용인원 수 + private Integer inventoryQty; // 재고/수용인원 수 + private LocalDateTime operationStartDate; //운영 시작 일 + private LocalDateTime operationEndDate; //운영 종료 일 + private String reserveMethodId; // 예약 방법 - 공통코드 reserve-method + private String reserveMethodName; + private String reserveMeansId; // 예약 구분 (인터넷 예약 시) - 공통코드 reserve-means + private String reserveMeansName; + private LocalDateTime requestStartDate; //예약 신청 시작 일시 + private LocalDateTime requestEndDate; //예약 신청 종료 일시 + private Boolean isPeriod; //기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가 + private Integer periodMaxCount; // 최대 예약 가능 일 수 + private String externalUrl; //외부링크 + private String selectionMeansId; //선별 방법 - 공통코드 reserve-selection + private String selectionMeansName; + private Boolean isPaid; // 유/무료 - false: 무료, true: 유료 + private BigDecimal usageCost; //이용 요금 + private Boolean isUse; //사용여부 + private String purpose; //용도 + private String address; //주소 + private String targetId; //이용 대상 - 공통코드 reserve-target + private String targetName; + private String excluded; // 사용허가 제외대상 + private String homepage; //홈페이지 주소 + private String contact; //문의처 + private String managerDept; //담당자 소속 + private String managerName; //담당자 이름 + private String managerContact; //담당자 연락처 + + @Builder + public ReserveItemRelationResponseDto(ReserveItem entity) { + this.reserveItemId = entity.getReserveItemId(); + this.reserveItemName = entity.getReserveItemName(); + this.locationId = entity.getLocationId(); + this.location = entity.getLocation(); + this.categoryId = entity.getCategoryId(); + this.categoryName = entity.getCategoryName(); + this.totalQty = entity.getTotalQty(); + this.inventoryQty = entity.getInventoryQty(); + this.operationStartDate = entity.getOperationStartDate(); + this.operationEndDate = entity.getOperationEndDate(); + this.reserveMethodId = entity.getReserveMethodId(); + this.reserveMethodName = entity.getReserveMethodName(); + this.reserveMeansId = entity.getReserveMeansId(); + this.reserveMeansName = entity.getReserveMeansName(); + this.requestStartDate = entity.getRequestStartDate(); + this.requestEndDate = entity.getRequestEndDate(); + this.isPeriod = entity.getIsPeriod(); + this.periodMaxCount = entity.getPeriodMaxCount(); + this.externalUrl = entity.getExternalUrl(); + this.selectionMeansId = entity.getSelectionMeansId(); + this.selectionMeansName = entity.getSelectionMeansName(); + this.isPaid = entity.getIsPaid(); + this.usageCost = entity.getUsageCost(); + this.isUse = entity.getIsUse(); + this.purpose = entity.getPurpose(); + this.address = entity.getAddress(); + this.targetId = entity.getTargetId(); + this.targetName = entity.getTargetName(); + this.excluded = entity.getExcluded(); + this.homepage = entity.getHomepage(); + this.contact = entity.getContact(); + this.managerDept = entity.getManagerDept(); + this.managerName = entity.getManagerName(); + this.managerContact = entity.getManagerContact(); + } + + public ReserveItem toEntity() { + return ReserveItem.builder() + .reserveItemName(this.reserveItemName) + .locationId(this.locationId) + .location(this.location) + .categoryId(this.categoryId) + .categoryName(this.categoryName) + .totalQty(this.totalQty) + .inventoryQty(this.inventoryQty) + .operationStartDate(this.operationStartDate) + .operationEndDate(this.operationEndDate) + .reserveMethodId(this.reserveMethodId) + .reserveMethodName(this.reserveMethodName) + .reserveMeansId(this.reserveMeansId) + .reserveMeansName(this.reserveMeansName) + .requestStartDate(this.requestStartDate) + .requestEndDate(this.requestEndDate) + .isPeriod(this.isPeriod) + .periodMaxCount(this.periodMaxCount) + .externalUrl(this.externalUrl) + .selectionMeansId(this.selectionMeansId) + .selectionMeansName(this.selectionMeansName) + .isPaid(this.isPaid) + .usageCost(this.usageCost) + .isUse(this.isUse) + .purpose(this.purpose) + .address(this.address) + .targetId(this.targetId) + .targetName(this.targetName) + .excluded(this.excluded) + .homepage(this.homepage) + .contact(this.contact) + .managerDept(this.managerDept) + .managerName(this.managerName) + .managerContact(this.managerContact) + .build(); + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/ReserveItemResponseDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/ReserveItemResponseDto.java new file mode 100644 index 0000000..b68c3a7 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/ReserveItemResponseDto.java @@ -0,0 +1,93 @@ +package org.egovframe.cloud.reservechecksevice.client.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reservechecksevice.domain.reserve.ReserveItem; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemResponseDto + *

+ * 예약 물품 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveItemResponseDto { + private Long reserveItemId; // 예약 물품 id + private String reserveItemName; //예약 물품 명 + private Long locationId; + private String categoryId; //예약유형 - 공통코드 reserve-category + private Integer totalQty; //총 재고/수용인원 수 + private Integer inventoryQty; // 재고/수용인원 수 + private LocalDateTime operationStartDate; //운영 시작 일 + private LocalDateTime operationEndDate; //운영 종료 일 + private String reserveMethodId; // 예약 방법 - 공통코드 reserve-method + private String reserveMeansId; // 예약 구분 (인터넷 예약 시) - 공통코드 reserve-means + private LocalDateTime requestStartDate; //예약 신청 시작 일시 + private LocalDateTime requestEndDate; //예약 신청 종료 일시 + private Boolean isPeriod; //기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가 + private Integer periodMaxCount; // 최대 예약 가능 일 수 + private String externalUrl; //외부링크 + private String selectionMeansId; //선별 방법 - 공통코드 reserve-selection + private Boolean isPaid; // 유/무료 - true: 무료, false: 유료 + private BigDecimal usageCost; //이용 요금 + private Boolean isUse; //사용여부 + private String purpose; //용도 + private String address; //주소 + private String targetId; //이용 대상 - 공통코드 reserve-target + private String excluded; // 사용허가 제외대상 + private String homepage; //홈페이지 주소 + private String contact; //문의처 + private String managerDept; //담당자 소속 + private String managerName; //담당자 이름 + private String managerContact; //담당자 연락처 + + @Builder + public ReserveItemResponseDto(ReserveItem reserveItem) { + this.reserveItemId = reserveItem.getReserveItemId(); + this.reserveItemName = reserveItem.getReserveItemName(); + this.locationId = reserveItem.getLocationId(); + this.categoryId = reserveItem.getCategoryId(); + this.totalQty = reserveItem.getTotalQty(); + this.inventoryQty = reserveItem.getInventoryQty(); + this.operationStartDate = reserveItem.getOperationStartDate(); + this.operationEndDate = reserveItem.getOperationEndDate(); + this.reserveMethodId = reserveItem.getReserveMethodId(); + this.reserveMeansId = reserveItem.getReserveMeansId(); + this.requestStartDate = reserveItem.getRequestStartDate(); + this.requestEndDate = reserveItem.getRequestEndDate(); + this.isPeriod = reserveItem.getIsPeriod(); + this.periodMaxCount = reserveItem.getPeriodMaxCount(); + this.externalUrl = reserveItem.getExternalUrl(); + this.selectionMeansId = reserveItem.getSelectionMeansId(); + this.isPaid = reserveItem.getIsPaid(); + this.usageCost = reserveItem.getUsageCost(); + this.isUse = reserveItem.getIsUse(); + this.purpose = reserveItem.getPurpose(); + this.address = reserveItem.getAddress(); + this.targetId = reserveItem.getTargetId(); + this.excluded = reserveItem.getExcluded(); + this.homepage = reserveItem.getHomepage(); + this.contact = reserveItem.getContact(); + this.managerDept = reserveItem.getManagerDept(); + this.managerName = reserveItem.getManagerName(); + this.managerContact = reserveItem.getManagerContact(); + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/UserResponseDto.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/UserResponseDto.java new file mode 100644 index 0000000..3eb78c1 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/client/dto/UserResponseDto.java @@ -0,0 +1,50 @@ +package org.egovframe.cloud.reservechecksevice.client.dto; + +import lombok.Builder; +import lombok.Getter; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserResponseDto + *

+ * 사용자 정보 요청시 사용되는 필요한 정보만 담긴 DTO + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +public class UserResponseDto { + + private String userId; + private String userName; + private String email; + private String roleId; + private String userStateCode; + private String googleId; + private String kakaoId; + private String naverId; + private Boolean isSocialUser; + private Boolean hasPassword; + + @Builder + public UserResponseDto(String userId, String userName, String email, String roleId, String userStateCode, String googleId, String kakaoId, String naverId, Boolean isSocialUser, Boolean hasPassword) { + this.userId = userId; + this.userName = userName; + this.email = email; + this.roleId = roleId; + this.userStateCode = userStateCode; + this.googleId = googleId; + this.kakaoId = kakaoId; + this.naverId = naverId; + this.isSocialUser = isSocialUser; + this.hasPassword = hasPassword; + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/config/Resilience4JConfig.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/config/Resilience4JConfig.java new file mode 100644 index 0000000..89881a7 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/config/Resilience4JConfig.java @@ -0,0 +1,45 @@ +package org.egovframe.cloud.reservechecksevice.config; + +import java.time.Duration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; + +/** + * org.egovframe.cloud.portalservice.config.Resilience4JConfig + *

+ * Resilience4J Configuration + * 기본 설정값으로 운영되어도 무방하다. 이 클래스는 필수는 아니다. + * retry 기본값은 최대 3회이고, fallback 이 없는 경우에만 동작하므로 설정하지 않았다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/31 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/31    jaeyeolkim  최초 생성
+ *  2021/10/05    shinmj      reactive로 변경
+ * 
+ */ +@Configuration +public class Resilience4JConfig { + + @Bean + public CircuitBreakerRegistry circuitBreakerRegistry() { + CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() + .failureRateThreshold(50) // Circuit 열지 말지 결정하는 실패 threshold 퍼센테이지 + .waitDurationInOpenState(Duration.ofSeconds(5)) // (half closed 전에) circuitBreaker가 open 되기 전에 기다리는 기간 + .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) // circuit breaker count 기반 처리 + .slidingWindowSize(10) // 통계 대상 건수 -> N건의 요청중.. + .build(); + return CircuitBreakerRegistry.of(circuitBreakerConfig); + } + +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/location/Location.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/location/Location.java new file mode 100644 index 0000000..f9001e8 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/location/Location.java @@ -0,0 +1,52 @@ +package org.egovframe.cloud.reservechecksevice.domain.location; + +import lombok.*; +import org.egovframe.cloud.reactive.domain.BaseEntity; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import javax.validation.constraints.Size; + +/** + * org.egovframe.cloud.reserveitemservice.domain.location.Location + * + * 예약 지역 도메인 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class Location extends BaseEntity { + + @Id + private Long locationId; + + @Size(max = 200) + @Column + private String locationName; + @Column + private Integer sortSeq; + + @Column("use_at") + private Boolean isUse; + + @Builder + public Location(Long locationId, String locationName, Integer sortSeq, Boolean isUse) { + this.locationId = locationId; + this.locationName = locationName; + this.sortSeq = sortSeq; + this.isUse = isUse; + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/Category.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/Category.java new file mode 100644 index 0000000..525f7ac --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/Category.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.reservechecksevice.domain.reserve; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.reservechecksevice.domain.reserve.Category + * + * 예약 유형 enum class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +@Getter +@RequiredArgsConstructor +public enum Category { + EDUCATION("education", "교육"), + EQUIPMENT("equipment", "장비"), + SPACE("space", "공간"); + + private final String key; + private final String title; + + public boolean isEquals(String compare) { + return this.getKey().equals(compare); + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/Reserve.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/Reserve.java new file mode 100644 index 0000000..66076b4 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/Reserve.java @@ -0,0 +1,203 @@ +package org.egovframe.cloud.reservechecksevice.domain.reserve; + +import lombok.*; +import org.egovframe.cloud.reactive.domain.BaseEntity; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveUpdateRequestDto; +import org.egovframe.cloud.reservechecksevice.client.dto.UserResponseDto; +import org.egovframe.cloud.reservechecksevice.domain.location.Location; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reservechecksevice.domain.reserve.Reserve + * + * 예약 도메인 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@With +@Table("reserve") +public class Reserve extends BaseEntity { + + @Id + @Column + private String reserveId; //예약 id + + @Column + private Long reserveItemId; //예약 물품 id + + @Transient + private ReserveItem reserveItem; + + @Setter + @Column + private Long locationId; //지역 id + + @Setter + @Column + private String categoryId; //예약유형 - 공통코드 reserve-category + + @Column + private Integer reserveQty; //예약 신청 인원/수량 + + @Column + private String reservePurposeContent; //예약 목적 + + @Column + private String attachmentCode; //첨부파일 코드 + + @Column + private LocalDateTime reserveStartDate; //예약 신청 시작일 + @Column + private LocalDateTime reserveEndDate; //예약 신청 종료일 + + @Column + private String reserveStatusId; //예약상태 - 공통코드(reserve-status) + + @Column + private String reasonCancelContent; //예약 취소 사유 + + @Column + private String userId; //예약자 + + @Transient + private UserResponseDto user; + + @Column + private String userContactNo; //예약자 연락처 + + @Column("user_email_addr") + private String userEmail; //예약자 이메일 + + @Builder + public Reserve(String reserveId, Long reserveItemId, + ReserveItem reserveItem, Long locationId, String categoryId, Integer reserveQty, + String reservePurposeContent, String attachmentCode, LocalDateTime reserveStartDate, + LocalDateTime reserveEndDate, String reserveStatusId, String reasonCancelContent, String userId, + UserResponseDto user, String userContactNo, String userEmail) { + this.reserveId = reserveId; + this.reserveItemId = reserveItemId; + this.reserveItem = reserveItem; + this.locationId = locationId; + this.categoryId = categoryId; + this.reserveQty = reserveQty; + this.reservePurposeContent = reservePurposeContent; + this.attachmentCode = attachmentCode; + this.reserveStartDate = reserveStartDate; + this.reserveEndDate = reserveEndDate; + this.reserveStatusId = reserveStatusId; + this.reasonCancelContent = reasonCancelContent; + this.userId = userId; + this.user = user; + this.userContactNo = userContactNo; + this.userEmail = userEmail; + } + + /** + * 물품 정보 세팅 + * + * @param reserveItem + * @return + */ + public Reserve setReserveItem(ReserveItem reserveItem) { + this.reserveItem = reserveItem; + this.reserveItemId = reserveItem.getReserveItemId(); + return this; + } + + /** + * 예약자 정보 세팅 + * + * @param user + * @return + */ + public Reserve setUser(UserResponseDto user) { + this.user = user; + this.userId = user.getUserId(); + return this; + } + + /** + * 예약 상태 업데이트 + * + * @param reserveStatusId + * @return + */ + public Reserve updateStatus(String reserveStatusId) { + this.reserveStatusId = reserveStatusId; + return this; + } + + /** + * 취소 사유 업데이트 + * + * @param reasonCancelContent + * @return + */ + public Reserve updateReasonCancel(String reasonCancelContent) { + this.reasonCancelContent = reasonCancelContent; + return this; + } + + /** + * 예약 정보 업데이트 + * + * @param updateRequestDto + * @return + */ + public Reserve update(ReserveUpdateRequestDto updateRequestDto) { + this.reserveQty = updateRequestDto.getReserveQty(); + this.reservePurposeContent = updateRequestDto.getReservePurposeContent(); + this.attachmentCode = updateRequestDto.getAttachmentCode(); + this.reserveStartDate = updateRequestDto.getReserveStartDate(); + this.reserveEndDate = updateRequestDto.getReserveEndDate(); + this.userId = updateRequestDto.getUserId(); + this.userEmail = updateRequestDto.getUserEmail(); + this.userContactNo = updateRequestDto.getUserContactNo(); + return this; + } + + /** + * create 정보 세팅 + * insert 시 필요 + * + * @param createdDate + * @param createdBy + * @return + */ + public Reserve setCreatedInfo(LocalDateTime createdDate, String createdBy) { + this.createdBy = createdBy; + this.createDate = createdDate; + return this; + } + + /** + * 예약 수량 양수, 음수 변환 + * 예약 취소 시 재고 카운트를 위해 + * + * @return + */ + public Reserve conversionReserveQty() { + this.reserveQty = (this.reserveQty * -1); + return this; + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveItem.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveItem.java new file mode 100644 index 0000000..fe43fc1 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveItem.java @@ -0,0 +1,208 @@ +package org.egovframe.cloud.reservechecksevice.domain.reserve; + + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reactive.domain.BaseEntity; +import org.egovframe.cloud.reservechecksevice.domain.location.Location; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem + * + * 예약 물품 도메인 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/09 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/09    shinmj       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveItem extends BaseEntity { + + @Id + @Column("reserve_item_id") + private Long reserveItemId; // 예약 물품 id + + @Size(max = 200) + @NotNull + @Column("reserve_item_name") + private String reserveItemName; //예약 물품 명 + + @ToString.Exclude + private Long locationId; + + @ToString.Exclude + @Transient + private Location location; //지역 + + @Size(max = 20) + @NotNull + @Column + private String categoryId; //예약유형 - 공통코드 reserve-category + + @Transient + private String categoryName; + + @Size(max = 5) + @NotNull + @Column + private Integer totalQty; //총 재고/수용인원 수 + + @Size(max = 5) + @Column + private Integer inventoryQty; //현재 재고/수용인원 수 + + @Column + private LocalDateTime operationStartDate; //운영 시작 일 + + @Column + private LocalDateTime operationEndDate; //운영 종료 일 + + @Size(max = 20) + @NotNull + @Column + private String reserveMethodId; // 예약 방법 - 공통코드 reserve-method + + @Transient + private String reserveMethodName; + + @Size(max = 20) + @Column + private String reserveMeansId; // 예약 구분 (인터넷 예약 시) - 공통코드 reserve-means + + @Transient + private String reserveMeansName; + + @Column + private LocalDateTime requestStartDate; //예약 신청 시작 일시 + + @Column + private LocalDateTime requestEndDate; //예약 신청 종료 일시 + + @Column("period_at") + private Boolean isPeriod; //기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가 + + @Size(max = 3) + @Column + private Integer periodMaxCount; // 최대 예약 가능 일 수 + + @Size(max = 500) + @Column + private String externalUrl; //외부링크 + + @Size(max = 20) + @NotNull + @Column + private String selectionMeansId; //선별 방법 - 공통코드 reserve-selection-means + + @Transient + private String selectionMeansName; + + @Column("paid_at") + private Boolean isPaid; // 유/무료 - false: 무료, true: 유료 + + @Column + private BigDecimal usageCost; //이용 요금 + + @Column("use_at") + private Boolean isUse; //사용여부 + + @Size(max = 4000) + @Column("purpose_content") + private String purpose; //용도 + + @Size(max = 500) + @Column("item_addr") + private String address; //주소 + + @Size(max = 20) + @Column + private String targetId; //이용 대상 - 공통코드 reserve-target + + @Transient + private String targetName; + + @Size(max = 2000) + @Column("excluded_content") + private String excluded; // 사용허가 제외대상 + + @Size(max = 500) + @Column("homepage_url") + private String homepage; //홈페이지 주소 + + @Size(max = 50) + @Column("contact_no") + private String contact; //문의처 + + @Size(max = 200) + @Column("manager_dept_name") + private String managerDept; //담당자 소속 + + @Size(max = 200) + @Column("manager_name") + private String managerName; //담당자 이름 + + @Size(max = 50) + @Column("manager_contact_no") + private String managerContact; //담당자 연락처 + + @Builder + public ReserveItem(Long reserveItemId, String reserveItemName, Long locationId, Location location, String categoryId, String categoryName, Integer totalQty, Integer inventoryQty, LocalDateTime operationStartDate, LocalDateTime operationEndDate, String reserveMethodId, String reserveMethodName, String reserveMeansId, String reserveMeansName, LocalDateTime requestStartDate, LocalDateTime requestEndDate, Boolean isPeriod, Integer periodMaxCount, String externalUrl, String selectionMeansId, String selectionMeansName, Boolean isPaid, BigDecimal usageCost, Boolean isUse, String purpose, String address, String targetId, String targetName, String excluded, String homepage, String contact, String managerDept, String managerName, String managerContact) { + this.reserveItemId = reserveItemId; + this.reserveItemName = reserveItemName; + this.locationId = locationId; + this.location = location; + this.categoryId = categoryId; + this.categoryName = categoryName; + this.totalQty = totalQty; + this.inventoryQty = inventoryQty; + this.operationStartDate = operationStartDate; + this.operationEndDate = operationEndDate; + this.reserveMethodId = reserveMethodId; + this.reserveMethodName = reserveMethodName; + this.reserveMeansId = reserveMeansId; + this.reserveMeansName = reserveMeansName; + this.requestStartDate = requestStartDate; + this.requestEndDate = requestEndDate; + this.isPeriod = isPeriod; + this.periodMaxCount = periodMaxCount; + this.externalUrl = externalUrl; + this.selectionMeansId = selectionMeansId; + this.selectionMeansName = selectionMeansName; + this.isPaid = isPaid; + this.usageCost = usageCost; + this.isUse = isUse; + this.purpose = purpose; + this.address = address; + this.targetId = targetId; + this.targetName = targetName; + this.excluded = excluded; + this.homepage = homepage; + this.contact = contact; + this.managerDept = managerDept; + this.managerName = managerName; + this.managerContact = managerContact; + } +} + + diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepository.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepository.java new file mode 100644 index 0000000..837aafa --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepository.java @@ -0,0 +1,23 @@ +package org.egovframe.cloud.reservechecksevice.domain.reserve; + +import org.springframework.data.r2dbc.repository.R2dbcRepository; + +/** + * org.egovframe.cloud.reservechecksevice.domain.reserve.ReserveRepository + * + * 예약 도메인 Repository interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +public interface ReserveRepository extends R2dbcRepository, ReserveRepositoryCustom { +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepositoryCustom.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepositoryCustom.java new file mode 100644 index 0000000..787dff9 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepositoryCustom.java @@ -0,0 +1,44 @@ +package org.egovframe.cloud.reservechecksevice.domain.reserve; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveRequestDto; +import org.springframework.data.domain.Pageable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reservechecksevice.domain.reserve.ReserveRepositoryCustom + * + * 예약 도메인 custom Repository interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +public interface ReserveRepositoryCustom { + Flux search(ReserveRequestDto requestDto, Pageable pageable); + Mono searchCount(ReserveRequestDto requestDto, Pageable pageable); + Mono findReserveById(String reserveId); + + Flux searchForUser(ReserveRequestDto requestDto, Pageable pageable, String userId); + Mono searchCountForUser(ReserveRequestDto requestDto, Pageable pageable, String userId); + + Mono loadRelations(Reserve reserve); + + Flux findAllByReserveDate(Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate); + Flux findAllByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate); + Mono findAllByReserveDateWithoutSelfCount(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate); + + Mono insert(Reserve reserve); + +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepositoryImpl.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepositoryImpl.java new file mode 100644 index 0000000..de835ad --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveRepositoryImpl.java @@ -0,0 +1,296 @@ +package org.egovframe.cloud.reservechecksevice.domain.reserve; + +import static org.springframework.data.relational.core.query.Criteria.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveRequestDto; +import org.egovframe.cloud.reservechecksevice.client.ReserveItemServiceClient; +import org.egovframe.cloud.reservechecksevice.client.UserServiceClient; +import org.egovframe.cloud.reservechecksevice.client.dto.UserResponseDto; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.relational.core.query.Criteria; +import org.springframework.data.relational.core.query.Query; +import org.springframework.util.StringUtils; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reservechecksevice.domain.reserve.ReserveRepositoryImpl + * + * 예약 도메인 custom repository 구현 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class ReserveRepositoryImpl implements ReserveRepositoryCustom{ + private static final String RESERVE_ITEM_CIRCUIT_BREAKER_NAME = "reserve-item"; + private static final String USER_CIRCUIT_BREAKER_NAME = "user"; + + private final R2dbcEntityTemplate entityTemplate; + private final ReserveItemServiceClient reserveItemServiceClient; + private final UserServiceClient userServiceClient; + private final CircuitBreakerRegistry circuitBreakerRegistry; + + /** + * 조회조건 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Override + public Flux search(ReserveRequestDto requestDto, Pageable pageable) { + + return entityTemplate.select(Reserve.class) + .matching(Query.query(Criteria.from(whereQuery(requestDto))) + .sort(Sort.by(Sort.Direction.DESC, "create_date")) + .with(pageable)) + .all() + .flatMap(this::loadRelations) + .switchIfEmpty(Flux.empty()); + } + + /** + * 조회조건 목록 조회시 총 count 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Override + public Mono searchCount(ReserveRequestDto requestDto, Pageable pageable) { + return entityTemplate.select(Reserve.class) + .matching(Query.query(Criteria.from(whereQuery(requestDto))) + .sort(Sort.by(Sort.Direction.DESC, "create_date")) + .with(pageable)) + .count(); + } + + /** + * 예약정보 한건 조회시 relation 같이 조회 + * + * @param reserveId + * @return + */ + @Override + public Mono findReserveById(String reserveId) { + return entityTemplate.selectOne(Query.query(where("reserve_id").is(reserveId)), Reserve.class) + .flatMap(this::loadRelations) + .switchIfEmpty(Mono.empty()); + + } + + /** + * 사용자 예약 목록 조회 + * + * @param requestDto + * @param pageable + * @param userId + * @return + */ + @Override + public Flux searchForUser(ReserveRequestDto requestDto, Pageable pageable, String userId) { + Criteria where = Criteria.from(whereQuery(requestDto)); + return entityTemplate.select(Reserve.class) + .matching(Query.query(where.and(where("user_id").is(userId))) + .sort(Sort.by(Sort.Direction.DESC, "create_date")) + .with(pageable)) + .all() + .flatMap(this::loadRelations) + .switchIfEmpty(Flux.empty()); + } + + /** + * 사용자 예약 목록 건수 조회 + * + * @param requestDto + * @param pageable + * @param userId + * @return + */ + @Override + public Mono searchCountForUser(ReserveRequestDto requestDto, Pageable pageable, String userId) { + Criteria where = Criteria.from(whereQuery(requestDto)); + return entityTemplate.select(Reserve.class) + .matching(Query.query(where.and(where("user_id").is(userId))) + .sort(Sort.by(Sort.Direction.DESC, "create_date")) + .with(pageable)) + .count(); + } + + /** + * relation 조회 + * + * @param reserve + * @return + */ + @Override + public Mono loadRelations(final Reserve reserve) { + //load user + Mono mono = Mono.just(reserve) + .zipWith(findUserByUserId(reserve.getUserId())) + .map(tuple -> tuple.getT1().setUser(tuple.getT2())) + .switchIfEmpty(Mono.just(reserve)); + + //load reserveItem + mono = mono.zipWith(findReserveItemWithRelation(reserve.getReserveItemId())) + .map(tuple -> tuple.getT1().setReserveItem(tuple.getT2())) + .switchIfEmpty(Mono.just(reserve)); + + return mono; + } + + /** + * 조회 기간에 예약된 건 조회 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + @Override + public Flux findAllByReserveDate(Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + return entityTemplate.select(Reserve.class) + .matching(Query.query(where("reserve_item_id").is(reserveItemId) + .and ("reserve_start_date").lessThanOrEquals(endDate) + .and("reserve_end_date").greaterThanOrEquals(startDate) + )) + .all(); + } + + /** + * 조회 기간에 예약된 건 조회 + * 현 예약건은 제외 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + @Override + public Flux findAllByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + return entityTemplate.select(Reserve.class) + .matching(Query.query(where("reserve_item_id").is(reserveItemId) + .and ("reserve_start_date").lessThanOrEquals(endDate) + .and("reserve_end_date").greaterThanOrEquals(startDate) + .and("reserve_id").not(reserveId) + )) + .all(); + } + + /** + * 조회 기간에 예약된 건수 조회 + * 현 예약건은 제외 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + @Override + public Mono findAllByReserveDateWithoutSelfCount(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + return entityTemplate.select(Reserve.class) + .matching(Query.query(where("reserve_item_id").is(reserveItemId) + .and ("reserve_start_date").lessThanOrEquals(endDate) + .and("reserve_end_date").greaterThanOrEquals(startDate) + .and("reserve_id").not(reserveId) + )) + .count(); + } + + /** + * 예약 insert + * pk(reserveId)를 서비스에서 생성하여 insert 하기 위함. + * + * @param reserve + * @return + */ + @Override + public Mono insert(Reserve reserve) { + return entityTemplate.insert(reserve); + } + + /** + * 예약 물품 정보 조회 + * + * @param reserveItemId + * @return + */ + private Mono findReserveItemWithRelation(Long reserveItemId) { + CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(RESERVE_ITEM_CIRCUIT_BREAKER_NAME); + + return reserveItemServiceClient.findByIdWithRelations(reserveItemId) + .transform(CircuitBreakerOperator.of(circuitBreaker)) + .onErrorResume(throwable -> Mono.empty()) + .switchIfEmpty(Mono.empty()) + .flatMap(reserveItemRelationResponseDto -> Mono.just(reserveItemRelationResponseDto.toEntity())); + } + + /** + * 예약자 정보 조회 + * + * @param userId + * @return + */ + private Mono findUserByUserId(String userId ) { + CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(USER_CIRCUIT_BREAKER_NAME); + return userServiceClient.findByUserId(userId) + .transform(CircuitBreakerOperator.of(circuitBreaker)) + .onErrorResume(throwable -> Mono.empty()); + } + + /** + * 조회조건 쿼리 + * + * @param requestDto + * @return + */ + private List whereQuery(ReserveRequestDto requestDto) { + ListcriteriaList = new ArrayList<>(); + + if (requestDto.getLocationId() != null) { + criteriaList.add(where("location_id").is(requestDto.getLocationId())); + } + + if (requestDto.getCategoryId() != null) { + criteriaList.add(where("category_id").is(requestDto.getCategoryId())); + } + + if (StringUtils.hasText(requestDto.getKeyword())) { + if ("item".equals(requestDto.getKeywordType())) { + criteriaList.add(where("reserve_item_id").like(likeText(requestDto.getKeyword()))); + } + } + return criteriaList; + } + + /** + * like 검색 + * + * @param keyword + * @return + */ + private String likeText(String keyword) { + return "%" + keyword + "%"; + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveStatus.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveStatus.java new file mode 100644 index 0000000..9568c96 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/domain/reserve/ReserveStatus.java @@ -0,0 +1,37 @@ +package org.egovframe.cloud.reservechecksevice.domain.reserve; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.reservechecksevice.domain.reserve.ReserveStatus + * + * 예약 상태 enum class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +@Getter +@RequiredArgsConstructor +public enum ReserveStatus { + REQUEST("request", "예약 신청"), + APPROVE("approve", "예약 승인"), + CANCEL("cancel", "예약 취소"), + DONE("done", "완료"); + + private final String key; + private final String title; + + public boolean isEquals(String status) { + return this.getKey().equals(status); + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/service/reserve/ReserveService.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/service/reserve/ReserveService.java new file mode 100644 index 0000000..5f6a236 --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/service/reserve/ReserveService.java @@ -0,0 +1,626 @@ +package org.egovframe.cloud.reservechecksevice.service.reserve; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.stream.IntStream; + +import org.egovframe.cloud.common.domain.Role; +import org.egovframe.cloud.common.dto.AttachmentEntityMessage; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.reactive.service.ReactiveAbstractService; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveCancelRequestDto; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveListResponseDto; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveRequestDto; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveResponseDto; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveSaveRequestDto; +import org.egovframe.cloud.reservechecksevice.api.reserve.dto.ReserveUpdateRequestDto; +import org.egovframe.cloud.reservechecksevice.client.ReserveItemServiceClient; +import org.egovframe.cloud.reservechecksevice.client.dto.ReserveItemResponseDto; +import org.egovframe.cloud.reservechecksevice.domain.reserve.Category; +import org.egovframe.cloud.reservechecksevice.domain.reserve.Reserve; +import org.egovframe.cloud.reservechecksevice.domain.reserve.ReserveRepository; +import org.egovframe.cloud.reservechecksevice.domain.reserve.ReserveStatus; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reservechecksevice.service.reserve.ReserveService + * + * 예약 service 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReserveService extends ReactiveAbstractService { + private static final String RESERVE_ITEM_CIRCUIT_BREAKER_NAME = "reserve-item"; + private static final String CHECK_RESERVE_MEANS = "realtime"; + + private final ReserveRepository reserveRepository; + private final ReserveItemServiceClient reserveItemServiceClient; + private final CircuitBreakerRegistry circuitBreakerRegistry; + private final StreamBridge streamBridge; + + /** + * entity -> dto 변환 + * + * @param reserve + * @return + */ + private Mono convertReserveResponseDto(Reserve reserve) { + return Mono.just(ReserveResponseDto.builder() + .entity(reserve) + .build()); + } + + /** + * entity -> 목록 dto 변환 + * + * @param reserve + * @return + */ + private Mono convertReserveListResponseDto(Reserve reserve) { + return Mono.just(ReserveListResponseDto.builder() + .entity(reserve) + .build()); + } + + /** + * 현재 로그인 사용자가 관리자인지 체크 + * + * @return + */ + private Mono getIsAdmin() { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getAuthorities) + .map(grantedAuthorities -> { + List authorities = + new ArrayList<>((Collection) grantedAuthorities); + SimpleGrantedAuthority adminRole = new SimpleGrantedAuthority(Role.ADMIN.getKey()); + return authorities.contains(adminRole); + }); + } + + /** + * 현재 로그인 사용자 id + * + * @return + */ + private Mono getUserId() { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getPrincipal) + .map(String.class::cast); + } + + /** + * 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Transactional(readOnly = true) + public Mono> search(ReserveRequestDto requestDto, Pageable pageable) { + return reserveRepository.search(requestDto, pageable) + .switchIfEmpty(Flux.empty()) + .flatMap(this::convertReserveListResponseDto) + .collectList() + .zipWith(reserveRepository.searchCount(requestDto, pageable)) + .flatMap(tuple -> Mono.just(new PageImpl<>(tuple.getT1(), pageable, tuple.getT2()))); + } + + /** + * 한건 조회 dto return + * + * @param reserveId + * @return + */ + @Transactional(readOnly = true) + public Mono findReserveById(String reserveId) { + return reserveRepository.findReserveById(reserveId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(reserveId)) + .flatMap(this::convertReserveResponseDto); + } + + /** + * 사용자용 예약 목록 조회 (로그인 사용자의 예약정보만 조회) + * + * @param userId + * @param requestDto + * @param pageable + * @return + */ + @Transactional(readOnly = true) + public Mono> searchForUser(String userId, ReserveRequestDto requestDto, Pageable pageable) { + return reserveRepository.searchForUser(requestDto, pageable, userId) + .switchIfEmpty(Flux.empty()) + .flatMap(this::convertReserveListResponseDto) + .collectList() + .zipWith(reserveRepository.searchCountForUser(requestDto, pageable, userId)) + .flatMap(tuple -> Mono.just(new PageImpl<>(tuple.getT1(), pageable, tuple.getT2()))); + } + + /** + * 예약 정보 취소 + * + * @param reserveId + * @param cancelRequestDto + * @return + */ + public Mono cancel(String reserveId, ReserveCancelRequestDto cancelRequestDto) { + return getIsAdmin().flatMap(isAdmin -> { + if (isAdmin) { + return reserveCancel(reserveId, cancelRequestDto); + } + return findById(reserveId) + .zipWith(getUserId()) + .flatMap(tuple -> { + if (tuple.getT1().getUserId().equals(tuple.getT2())) { + return Mono.just(tuple.getT1()); + } + //해당 예약은 취소할 수 없습니다. + return Mono.error(new BusinessMessageException(getMessage("valid.cant_cancel"))); + }) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(reserve -> reserveCancel(reserveId, cancelRequestDto)); + }); + + } + + /** + * 예약 상태 취소로 변경 + * + * @param reserveId + * @param cancelRequestDto + * @return + */ + private Mono reserveCancel(String reserveId, ReserveCancelRequestDto cancelRequestDto) { + return findById(reserveId) + .map(reserve -> { + if (ReserveStatus.DONE.isEquals(reserve.getReserveStatusId())) { + //해당 예약은 이미 실행되어 취소할 수 없습니다. + throw new BusinessMessageException(getMessage("valid.cant_cancel_because_done")); + }else { + return reserve.updateStatus(ReserveStatus.CANCEL.getKey()) + .updateReasonCancel(cancelRequestDto.getReasonCancelContent()); + } + }) + .flatMap(reserve -> Mono.just(reserve.conversionReserveQty())) + .flatMap(this::updateInventory) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(reserve -> Mono.just(reserve.conversionReserveQty())) + .flatMap(reserveRepository::save) + .then(); + } + + /** + * 예약 정보 승인 + * + * @param reserveId + * @return + */ + public Mono approve(String reserveId) { + return getIsAdmin() + .flatMap(isAdmin -> { + if (isAdmin) { + return Mono.just(reserveId); + } + //관리자만 승인할 수 있습니다. + return Mono.error(new BusinessMessageException(getMessage("valid.manager_approve"))); + }) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(this::checkApprove) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(reserveRepository::save).then(); + } + + /** + * 승인 전 validate check 및 교육인 경우 재고 업데이트 + * + * @param reserveId + * @return + */ + private Mono checkApprove(String reserveId) { + return findById(reserveId) + .flatMap(this::checkReserveItems) + .onErrorResume(throwable -> Mono.error(throwable)) + .map(reserve -> reserve.updateStatus(ReserveStatus.APPROVE.getKey())) + .flatMap(this::updateInventory); + } + + /** + * 예약 물품 재고 및 예약 일자 체크 + * + * @param reserve + * @return + */ + private Mono checkReserveItems(Reserve reserve) { + return reserveItemServiceClient.findById(reserve.getReserveItemId()) + .transform(CircuitBreakerOperator.of(circuitBreakerRegistry.circuitBreaker(RESERVE_ITEM_CIRCUIT_BREAKER_NAME))) + .onErrorResume(throwable -> Mono.empty()) + .flatMap(reserveItemResponseDto -> { + // validation check + if (Category.SPACE.isEquals(reserveItemResponseDto.getCategoryId())) { + return this.checkSpace(reserveItemResponseDto, reserve); + }else if (Category.EQUIPMENT.isEquals(reserveItemResponseDto.getCategoryId())) { + return this.checkEquipment(reserveItemResponseDto, reserve); + }else if (Category.EDUCATION.isEquals(reserveItemResponseDto.getCategoryId())) { + return this.checkEducation(reserveItemResponseDto, reserve); + } + return Mono.just(reserve); + }); + } + + /** + * 예약 날짜 validation + * + * @param reserveItem + * @param reserve + * @return + */ + private Mono checkReserveDate(ReserveItemResponseDto reserveItem, Reserve reserve) { + LocalDateTime startDate = reserveItem.getReserveMeansId().equals(CHECK_RESERVE_MEANS) ? + reserveItem.getRequestStartDate() : reserveItem.getOperationStartDate(); + LocalDateTime endDate = reserveItem.getReserveMeansId().equals(CHECK_RESERVE_MEANS) ? + reserveItem.getRequestEndDate() : reserveItem.getOperationEndDate(); + + if (reserve.getReserveStartDate().isBefore(startDate)) { + //{0}이 {1} 보다 빠릅니다. 시작일, 운영/예약 시작일 + return Mono.error(new BusinessMessageException(getMessage("valid.to_be_fast.format", new Object[]{getMessage("common.start_date"), + getMessage("reserve_item.operation")+getMessage("reserve")+" "+getMessage("common.start_date")}))); + } + + if (reserve.getReserveEndDate().isAfter(endDate)) { + //{0}이 {1} 보다 늦습니다. 종료일, 운영/예약 종료일 + return Mono.error(new BusinessMessageException(getMessage("valid.to_be_slow.format", new Object[]{getMessage("common.end_date"), + getMessage("reserve_item.operation")+getMessage("reserve")+" "+getMessage("common.end_date")}))); + } + + if (reserveItem.getIsPeriod()) { + long between = ChronoUnit.DAYS.between(reserve.getReserveStartDate(), reserve.getReserveEndDate()); + if (reserveItem.getPeriodMaxCount() < between) { + //최대 예약 가능 일수보다 예약기간이 깁니다. (최대 예약 가능일 수 : {0}) + return Mono.error(new BusinessMessageException(getMessage("valid.reserve_period", new Object[]{reserveItem.getPeriodMaxCount()}))); + } + } + + return Mono.just(reserve); + } + + /** + * 공간 예약 시 예약 날짜에 다른 예약이 있는지 체크 + * + * @param reserveItem + * @param reserve + * @return + */ + private Mono checkSpace(ReserveItemResponseDto reserveItem, Reserve reserve) { + return this.checkReserveDate(reserveItem, reserve) + .flatMap(isValid -> reserveRepository.findAllByReserveDateWithoutSelfCount( + reserve.getReserveId(), + reserveItem.getReserveItemId(), + reserve.getReserveStartDate(), + reserve.getReserveEndDate()) + .flatMap(count -> { + if (count > 0) { + //"해당 날짜에는 예약할 수 없습니다." + return Mono.error(new BusinessMessageException(getMessage("valid.reserve_date"))); + } + return Mono.just(reserve); + }) + ); + } + + /** + * 장비 예약 시 예약 날짜에 예약 가능한 재고 체크 + * + * @param reserveItem + * @param reserve + * @return + */ + private Mono checkEquipment(ReserveItemResponseDto reserveItem, Reserve reserve) { + return this.checkReserveDate(reserveItem, reserve) + .flatMap(entity -> this.getMaxByReserveDateWithoutSelf( + entity.getReserveId(), + reserveItem.getReserveItemId(), + entity.getReserveStartDate(), + entity.getReserveEndDate()) + .flatMap(max -> { + if ((reserveItem.getTotalQty() - max) < reserve.getReserveQty()) { + return Mono.just(false); + } + return Mono.just(true); + }) + .flatMap(isValid -> { + if (!isValid) { + //해당 날짜에 예약할 수 있는 재고수량이 없습니다. + return Mono.error(new BusinessMessageException(getMessage("valid.reserve_count"))); + } + return Mono.just(reserve); + }) + ); + } + + /** + * 교육 예약 시 재고 체크 + * + * @param reserveItem + * @param reserve + * @return + */ + private Mono checkEducation(ReserveItemResponseDto reserveItem, Reserve reserve) { + return Mono.just(reserveItem) + .flatMap(reserveItemResponseDto -> { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime startDate = reserveItemResponseDto.getReserveMeansId().equals(CHECK_RESERVE_MEANS) ? + reserveItemResponseDto.getRequestStartDate() : reserveItemResponseDto.getOperationStartDate(); + LocalDateTime endDate = reserveItemResponseDto.getReserveMeansId().equals(CHECK_RESERVE_MEANS) ? + reserveItemResponseDto.getRequestEndDate() : reserveItemResponseDto.getOperationEndDate(); + + if (!(now.isAfter(startDate) && now.isBefore(endDate))) { + //해당 날짜에는 예약할 수 없습니다. + return Mono.error(new BusinessMessageException(getMessage("valid.reserve_date"))); + } + + if (reserveItemResponseDto.getInventoryQty() <= 0) { + //"예약이 마감되었습니다." + return Mono.error(new BusinessMessageException(getMessage("valid.reserve_close"))); + } + + if (reserveItemResponseDto.getInventoryQty() < reserve.getReserveQty()) { + //예약가능한 인원이 부족합니다. (남은 인원 : {0}) + return Mono.error(new BusinessMessageException(getMessage("valid.reserve_number_of_people", new Object[]{reserveItemResponseDto.getInventoryQty()}))); + } + return Mono.just(reserve); + }); + } + + /** + * 예약 정보 수정 + * + * @param reserveId + * @return + */ + public Mono update(String reserveId, ReserveUpdateRequestDto updateRequestDto) { + return getIsAdmin().flatMap(isAdmin -> { + if (isAdmin) { + return updateReserve(reserveId, updateRequestDto); + } + return updateReserveForUser(reserveId, updateRequestDto); + }); + } + + /** + * 사용자 예약 수정 + * + * @param reserveId + * @param updateRequestDto + * @return + */ + private Mono updateReserveForUser(String reserveId, ReserveUpdateRequestDto updateRequestDto) { + return findById(reserveId) + .zipWith(getUserId()) + .map(tuple -> { + if (!tuple.getT1().getUserId().equals(tuple.getT2())) { + //"해당 예약은 수정할 수 없습니다." + throw new BusinessMessageException(getMessage("valid.reserve_not_update")); + } + + if (!ReserveStatus.REQUEST.getKey().equals(tuple.getT1().getReserveStatusId())) { + //예약 신청 상태인 경우에만 수정 가능합니다. + throw new BusinessMessageException(getMessage("valid.reserve_not_update_status")); + } + + return tuple.getT1().update(updateRequestDto); + }) + .flatMap(this::checkReserveItems) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(this::updateInventory) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(reserveRepository::save); + } + + /** + * 관리자 예약 수정 + * + * @param reserveId + * @param updateRequestDto + * @return + */ + private Mono updateReserve(String reserveId, ReserveUpdateRequestDto updateRequestDto) { + return findById(reserveId) + .map(reserve -> { + if (!ReserveStatus.REQUEST.getKey().equals(reserve.getReserveStatusId())) { + //예약 신청 상태인 경우에만 수정 가능합니다. + throw new BusinessMessageException(getMessage("valid.reserve_not_update_status")); + } + return reserve.update(updateRequestDto); + }) + .flatMap(this::checkReserveItems) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(this::updateInventory) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(reserveRepository::save); + } + + /** + * 한건 정보 조회 entity return + * + * @param reserveId + * @return + */ + private Mono findById(String reserveId) { + return reserveRepository.findById(reserveId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(reserveId)); + } + + /** + * 관리자 예약 신청 + * 관리자의 경우 실시간이어도 이벤트 스트림 거치지 않고 바로 예약 처리 + * + * @param saveRequestDto + * @return + */ + public Mono create(ReserveSaveRequestDto saveRequestDto) { + return Mono.just(saveRequestDto) + .map(dto -> { + String uuid = UUID.randomUUID().toString(); + dto.setReserveId(uuid); + return dto.toEntity(); + }) + .zipWith(getUserId()) + .flatMap(tuple -> Mono.just(tuple.getT1().setCreatedInfo(LocalDateTime.now(), tuple.getT2()))) + .flatMap(this::checkReserveItems) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(this::updateInventory) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(reserveRepository::insert) + .flatMap(reserveRepository::loadRelations) + .doOnNext(reserve -> sendAttachmentEntityInfo(streamBridge, + AttachmentEntityMessage.builder() + .attachmentCode(reserve.getAttachmentCode()) + .entityName(reserve.getClass().getName()) + .entityId(reserve.getReserveId()) + .build())) + .flatMap(this::convertReserveResponseDto); + + + } + + /** + * 예약 정보 저장 시 재고 변경 + * + * @param reserve + * @return + */ + private Mono updateInventory(Reserve reserve) { + return Mono.just(reserve) + .flatMap(reserve1 -> { + if (!Category.EDUCATION.isEquals(reserve1.getCategoryId())) { + return Mono.just(reserve1); + } +// return reserveItemServiceClient.updateInventory(reserve.getReserveItemId(), reserve.getReserveQty()) +// .transform(CircuitBreakerOperator.of(circuitBreakerRegistry.circuitBreaker(RESERVE_ITEM_CIRCUIT_BREAKER_NAME))) +// .onErrorResume(throwable -> Mono.just(false)) +// .flatMap(isSuccess -> { +// if (isSuccess) { +// return Mono.just(reserve); +// } +// //재고 업데이트에 실패했습니다. +// return Mono.error(new BusinessMessageException(getMessage("msg.inventory_failed"))); +// }); + return null; + }); + } + + /** + * 예약 물품별 기간안에 있는 예약된 수량 max 조회 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + public Mono countInventory(Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + return reserveItemServiceClient.findById(reserveItemId) + .transform(CircuitBreakerOperator.of(circuitBreakerRegistry.circuitBreaker(RESERVE_ITEM_CIRCUIT_BREAKER_NAME))) + .onErrorResume(throwable -> Mono.empty()) + .zipWith(getMaxByReserveDate(reserveItemId, startDate, endDate)) + .flatMap(tuple -> Mono.just(tuple.getT1().getTotalQty() - tuple.getT2())); + } + + /** + * 예약물품에 대해 날짜별 예약된 수량 max 조회 + * 현 예약 건 제외 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + private Mono getMaxByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + Flux reserveFlux = reserveRepository.findAllByReserveDateWithoutSelf(reserveId, reserveItemId, startDate, endDate) + .switchIfEmpty(Flux.empty()); + return countMax(reserveFlux, startDate, endDate); + } + + /** + * 예약물품에 대해 날짜별 예약된 수량 max 조회 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + private Mono getMaxByReserveDate(Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + Flux reserveFlux = reserveRepository.findAllByReserveDate(reserveItemId, startDate, endDate) + .switchIfEmpty(Flux.empty()); + return countMax(reserveFlux, startDate, endDate); + } + + /** + * get max + * + * @param reserveFlux + * @param startDate + * @param endDate + * @return + */ + private Mono countMax(Flux reserveFlux, LocalDateTime startDate, LocalDateTime endDate) { + if (reserveFlux.equals(Flux.empty())) { + return Mono.just(0); + } + + long between = ChronoUnit.DAYS.between(startDate, endDate); + return Flux.fromStream(IntStream.iterate(0, i -> i + 1) + .limit(between) + .mapToObj(i -> startDate.plusDays(i))) + .flatMap(localDateTime -> + reserveFlux.map(findReserve -> { + if (localDateTime.isAfter(findReserve.getReserveStartDate()) + || localDateTime.isBefore(findReserve.getReserveEndDate())) { + return findReserve.getReserveQty(); + } + return 0; + }).reduce(0, (x1, x2) -> x1 + x2)) + .groupBy(integer -> integer) + .flatMap(group -> group.reduce((x1,x2) -> x1 > x2?x1:x2)) + .last(); + } + +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/validator/ReserveSaveValidator.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/validator/ReserveSaveValidator.java new file mode 100644 index 0000000..dd673fa --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/validator/ReserveSaveValidator.java @@ -0,0 +1,176 @@ +package org.egovframe.cloud.reservechecksevice.validator; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; + +import javax.annotation.Resource; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.egovframe.cloud.common.util.MessageUtil; +import org.egovframe.cloud.reservechecksevice.validator.annotation.ReserveSaveValid; +import org.springframework.util.StringUtils; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +/** + * org.egovframe.cloud.reservechecksevice.validator.ReserveSaveValidator + * + * 예약 신청 시 validation check를 하기 위한 custom validator + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/23 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/23    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +public class ReserveSaveValidator implements ConstraintValidator { + + @Resource( + name = "messageUtil" + ) + protected MessageUtil messageUtil; + + private String message; + + + @Override + public void initialize(ReserveSaveValid constraintAnnotation) { + message = constraintAnnotation.message(); + } + + /** + * 예약 신청 시 비지니스 로직에 의한 validation check + * + * @param value + * @param context + * @return + */ + @SneakyThrows + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + boolean fieldValid = true; + + String categoryId = String.valueOf(getFieldValue(value, "categoryId")); + if ("education".equals(categoryId)) { + //교육인 경우 + //신청인원 + fieldValid = checkReserveQty(value, context); + + }else if ("equipment".equals(categoryId)) { + //장비인 경우 + //신청일자(기간), 신청수량 + fieldValid = checkReserveDate(value, context); + fieldValid = checkReserveQty(value, context); + + }else if ("place".equals(categoryId)) { + //공간인 경우 + //신청일자(기간) + fieldValid = checkReserveDate(value, context); + } + + return fieldValid; + } + + /** + * 예약 수량 체크 + * + * @param value + * @param context + * @return + */ + @SneakyThrows + private boolean checkReserveQty(Object value, ConstraintValidatorContext context) { + if (isNull(value, "reserveQty")) { + context.disableDefaultConstraintViolation(); + //예약 수량 값은 필수 입니다. + context.buildConstraintViolationWithTemplate(messageUtil.getMessage("reserve")+" "+messageUtil.getMessage("reserve.count") + messageUtil.getMessage("valid.required")) + .addPropertyNode("reserveQty") + .addConstraintViolation(); + return false; + } + return true; + } + + /** + * 예약 신청 기간 체크 + * + * @param value + * @param context + * @return + */ + @SneakyThrows + private boolean checkReserveDate(Object value, ConstraintValidatorContext context) { + // 예약 신청 기간 필수 + if (isNull(value, "reserveStartDate")) { + context.disableDefaultConstraintViolation(); + // 예약 신청 시작일 값은 필수 입니다. + context.buildConstraintViolationWithTemplate(messageUtil.getMessage("reserve_item.request")+" "+messageUtil.getMessage("common.start_datetime") + messageUtil.getMessage("valid.required")) + .addPropertyNode("reserveStartDate") + .addConstraintViolation(); + return false; + } else if (isNull(value, "reserveEndDate")) { + context.disableDefaultConstraintViolation(); + // 예약 신청 종료일 값은 필수 입니다. + context.buildConstraintViolationWithTemplate(messageUtil.getMessage("reserve_item.request")+" "+messageUtil.getMessage("common.end_datetime") + messageUtil.getMessage("valid.required")) + .addPropertyNode("reserveEndDate") + .addConstraintViolation(); + return false; + }else { + // 예약 시작일, 종료일 체크 + LocalDateTime reserveStartDate = (LocalDateTime) getFieldValue(value, "reserveStartDate"); + LocalDateTime reserveEndDate = (LocalDateTime) getFieldValue(value, "reserveEndDate"); + if (reserveStartDate.isAfter(reserveEndDate)) { + context.disableDefaultConstraintViolation(); + //시작일, 종료일, {0}이 {1}보다 늦습니다. + context.buildConstraintViolationWithTemplate(messageUtil.getMessage("valid.to_be_slow.format", new Object[]{messageUtil.getMessage("common.start_date"), messageUtil.getMessage("common.end_date")})) + .addPropertyNode("reserveStartDate") + .addConstraintViolation(); + return false; + } + } + return true; + } + + /** + * 해당하는 field의 값 조회 + * + * @param object + * @param fieldName + * @return + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + private Object getFieldValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException { + Class clazz = object.getClass(); + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } + + /** + * 해당하는 Field가 null인지 체크 + * + * @param object + * @param fieldName + * @return + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + private boolean isNull(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException { + Class clazz = object.getClass(); + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object) == null || !StringUtils.hasLength(String.valueOf(field.get(object))); + } +} diff --git a/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/validator/annotation/ReserveSaveValid.java b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/validator/annotation/ReserveSaveValid.java new file mode 100644 index 0000000..42e224c --- /dev/null +++ b/backend/reserve-check-service/src/main/java/org/egovframe/cloud/reservechecksevice/validator/annotation/ReserveSaveValid.java @@ -0,0 +1,37 @@ +package org.egovframe.cloud.reservechecksevice.validator.annotation; + + +import org.egovframe.cloud.reservechecksevice.validator.ReserveSaveValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * org.egovframe.cloud.reservechecksevice.validator.annotation.ReserveSaveValid + * + * 예약 신청 시 validation check를 하기 위한 custom annotation + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/23 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/23    shinmj       최초 생성
+ * 
+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ReserveSaveValidator.class) +public @interface ReserveSaveValid { + String message() default "저장할 수 없습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/backend/reserve-check-service/src/main/resources/application.yml b/backend/reserve-check-service/src/main/resources/application.yml new file mode 100644 index 0000000..e6ec619 --- /dev/null +++ b/backend/reserve-check-service/src/main/resources/application.yml @@ -0,0 +1,14 @@ +spring: + application: + name: reserve-check-service + +server: + port: 0 + +# config server actuator +management: + endpoints: + web: + exposure: + include: refresh, health, beans + diff --git a/backend/reserve-check-service/src/main/resources/bootstrap.yml b/backend/reserve-check-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..220e76d --- /dev/null +++ b/backend/reserve-check-service/src/main/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: reserve-check-service \ No newline at end of file diff --git a/backend/reserve-check-service/src/main/resources/schema.sql b/backend/reserve-check-service/src/main/resources/schema.sql new file mode 100644 index 0000000..45682bb --- /dev/null +++ b/backend/reserve-check-service/src/main/resources/schema.sql @@ -0,0 +1,26 @@ +-- reserve Table Create SQL +CREATE TABLE IF NOT EXISTS reserve +( + `reserve_id` VARCHAR(255) NOT NULL COMMENT '예약 id', + `reserve_item_id` BIGINT NULL COMMENT '예약 물품 id', + `location_id` BIGINT NULL COMMENT '예약 물품-지역 id', + `category_id` VARCHAR(255) NULL COMMENT '예약 물품-유형 id', + `reserve_qty` BIGINT(18) NULL COMMENT '예약 신청인원/수량', + `reserve_purpose_content` VARCHAR(4000) NULL COMMENT '예약신청 목적', + `attachment_code` VARCHAR(255) NULL COMMENT '첨부파일 코드', + `reserve_start_date` DATETIME NULL COMMENT '예약 신청 시작일', + `reserve_end_date` DATETIME NULL COMMENT '예약 신청 종료일', + `reserve_status_id` VARCHAR(20) NULL COMMENT '예약상태 - 공통코드(reserve-status)', + `reason_cancel_content` VARCHAR(4000) NULL COMMENT '예약 취소 사유', + `user_id` VARCHAR(255) NULL COMMENT '예약자 id', + `user_contact_no` VARCHAR(50) NULL COMMENT '예약자 연락처', + `user_email_addr` VARCHAR(500) NULL COMMENT '예약자 이메일', + `create_date` DATETIME NULL COMMENT '생성일', + `created_by` VARCHAR(255) NULL COMMENT '생성자', + `modified_date` DATETIME NULL COMMENT '수정일', + `last_modified_by` VARCHAR(255) NULL COMMENT '수정자', + PRIMARY KEY (reserve_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE reserve COMMENT '예약 신청&확인'; + diff --git a/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/ReserveCheckSeviceApplicationTests.java b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/ReserveCheckSeviceApplicationTests.java new file mode 100644 index 0000000..5074ff6 --- /dev/null +++ b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/ReserveCheckSeviceApplicationTests.java @@ -0,0 +1,13 @@ +package org.egovframe.cloud.reservechecksevice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ReserveCheckSeviceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/api/ReserveApiControllerTest.java b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/api/ReserveApiControllerTest.java new file mode 100644 index 0000000..8e1966d --- /dev/null +++ b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/api/ReserveApiControllerTest.java @@ -0,0 +1,17 @@ +package org.egovframe.cloud.reservechecksevice.api; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class ReserveApiControllerTest { + + + + +} \ No newline at end of file diff --git a/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/config/R2dbcConfig.java b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/config/R2dbcConfig.java new file mode 100644 index 0000000..1f02372 --- /dev/null +++ b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/config/R2dbcConfig.java @@ -0,0 +1,39 @@ +package org.egovframe.cloud.reservechecksevice.config; + +import io.r2dbc.h2.H2ConnectionConfiguration; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.h2.H2ConnectionOption; +import io.r2dbc.spi.ConnectionFactory; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.r2dbc.connection.init.CompositeDatabasePopulator; +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; + +@Profile("test") +@TestConfiguration +@EnableR2dbcRepositories +public class R2dbcConfig { + @Bean + public H2ConnectionFactory connectionFactory() { + return new H2ConnectionFactory(H2ConnectionConfiguration.builder() + .tcp("localhost", "~/querydsl") + .property(H2ConnectionOption.DB_CLOSE_DELAY, "-1") + .username("sa") + .build()); + } + + @Bean + public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); + initializer.setConnectionFactory(connectionFactory); + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); + populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("schema-h2.sql"))); + initializer.setDatabasePopulator(populator); + + return initializer; + } +} diff --git a/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/RestResponsePage.java b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/RestResponsePage.java new file mode 100644 index 0000000..bfb7a9b --- /dev/null +++ b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/RestResponsePage.java @@ -0,0 +1,87 @@ +package org.egovframe.cloud.reservechecksevice.util; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.Collections; +import java.util.List; + +/** + * org.egovframe.cloud.boardservice.util.RestResponsePage + *

+ * 페이지 API 조회 시 JSON 형식의 응답 데이터를 페이지 객체를 구현하여 마이그레이션 해주는 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +public class RestResponsePage extends PageImpl { + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + * @param number 페이지 번호 + * @param size 조회할 데이터 수 + * @param totalElements 총 데이터 수 + * @param pageable 페이지 정보 + * @param last 마지막 + * @param totalPages 총 페이지 + * @param sort 정렬 + * @param first 처음 + * @param numberOfElements 조회된 데이터 수 + */ + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public RestResponsePage(@JsonProperty("content") List content, + @JsonProperty("number") int number, + @JsonProperty("size") int size, + @JsonProperty("totalElements") Long totalElements, + @JsonProperty("pageable") JsonNode pageable, + @JsonProperty("last") boolean last, + @JsonProperty("totalPages") int totalPages, + @JsonProperty("sort") JsonNode sort, + @JsonProperty("first") boolean first, + @JsonProperty("numberOfElements") int numberOfElements) { + super(content, PageRequest.of(number, size), totalElements); + } + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + * @param pageable 페이지 정보 + * @param total 총 데이터 수 + */ + public RestResponsePage(List content, Pageable pageable, long total) { + super(content, pageable, total); + } + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + */ + public RestResponsePage(List content) { + super(content); + } + + /** + * Rest 응답 페이지 생성자 + */ + public RestResponsePage() { + super(Collections.emptyList()); + } + +} diff --git a/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/WithCustomMockUser.java b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/WithCustomMockUser.java new file mode 100644 index 0000000..1626d25 --- /dev/null +++ b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/WithCustomMockUser.java @@ -0,0 +1,16 @@ +package org.egovframe.cloud.reservechecksevice.util; + +import org.egovframe.cloud.common.domain.Role; +import org.springframework.security.test.context.support.WithSecurityContext; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) +public @interface WithCustomMockUser { + + String userId() default "user"; + Role role() default Role.ADMIN; + +} diff --git a/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/WithMockCustomUserSecurityContextFactory.java b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/WithMockCustomUserSecurityContextFactory.java new file mode 100644 index 0000000..31f55e3 --- /dev/null +++ b/backend/reserve-check-service/src/test/java/org/egovframe/cloud/reservechecksevice/util/WithMockCustomUserSecurityContextFactory.java @@ -0,0 +1,25 @@ +package org.egovframe.cloud.reservechecksevice.util; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +import java.util.ArrayList; +import java.util.List; + +public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithCustomMockUser mockUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + List roleList = new ArrayList<>(); + roleList.add(new SimpleGrantedAuthority(mockUser.role().getKey())); + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(mockUser.userId(), null, roleList); + context.setAuthentication(authenticationToken); + + return context; + } +} diff --git a/backend/reserve-check-service/src/test/resources/application-test.yml b/backend/reserve-check-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..d978abb --- /dev/null +++ b/backend/reserve-check-service/src/test/resources/application-test.yml @@ -0,0 +1,44 @@ +spring: + application: + name: reserve-check-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: + hibernate: + generate-ddl: true + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/backend/reserve-check-service/src/test/resources/bootstrap.yml b/backend/reserve-check-service/src/test/resources/bootstrap.yml new file mode 100644 index 0000000..220e76d --- /dev/null +++ b/backend/reserve-check-service/src/test/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: reserve-check-service \ No newline at end of file diff --git a/backend/reserve-check-service/src/test/resources/schema-h2.sql b/backend/reserve-check-service/src/test/resources/schema-h2.sql new file mode 100644 index 0000000..159dad6 --- /dev/null +++ b/backend/reserve-check-service/src/test/resources/schema-h2.sql @@ -0,0 +1,81 @@ +-- location Table Create SQL +CREATE TABLE IF NOT EXISTS location +( + location_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '지역 id', + location_name VARCHAR(200) NULL COMMENT '지역 이름', + sort_seq SMALLINT(3) NULL COMMENT '정렬 순서', + use_at TINYINT(1) NULL DEFAULT 1 COMMENT '사용 여부', + created_by VARCHAR(255) NULL COMMENT '생성자', + create_date DATETIME NULL COMMENT '생성일', + last_modified_by VARCHAR(255) NULL COMMENT '수정자', + modified_date DATETIME NULL COMMENT '수정일', + PRIMARY KEY (location_id) +) ; + + + +-- reserve_item Table Create SQL +CREATE TABLE IF NOT EXISTS reserve_item +( + reserve_item_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '예약 물품 id', + reserve_item_name VARCHAR(200) NULL COMMENT '예약 물품 이름', + location_id BIGINT NULL COMMENT '지역 id', + category_id VARCHAR(20) NULL COMMENT '예약유형 - 공통코드 reserve-category', + total_qty BIGINT(18) NULL COMMENT '총 재고/수용인원 수', + inventory_qty BIGINT(18) NULL COMMENT '현재 남은 재고/수용인원 수', + operation_start_date DATETIME NULL COMMENT '운영 시작 일', + operation_end_date DATETIME NULL COMMENT '운영 종료 일', + reserve_method_id VARCHAR(20) NULL COMMENT '예약 방법 - 공통코드 reserve-method', + reserve_means_id VARCHAR(20) NULL COMMENT '예약 구분 (인터넷 예약 시) - 공통코드 reserve-means', + request_start_date DATETIME NULL COMMENT '예약 신청 시작 일시', + request_end_date DATETIME NULL COMMENT '예약 신청 종료 일시', + period_at TINYINT(1) NULL DEFAULT 0 COMMENT '기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가', + period_max_count SMALLINT(3) NULL COMMENT '최대 예약 가능 일 수', + external_url VARCHAR(500) NULL COMMENT '외부링크', + selection_means_id VARCHAR(20) NULL COMMENT '선별 방법 - 공통코드 reserve-selection-means', + free_at TINYINT(1) NULL DEFAULT 1 COMMENT '유/무료 - true: 무료, false: 유료', + usage_cost DECIMAL(18, 0) NULL COMMENT '이용 요금', + use_at TINYINT(1) NULL DEFAULT 1 COMMENT '사용 여부', + purpose_content VARCHAR(4000) NULL COMMENT '용도', + item_addr VARCHAR(500) NULL COMMENT '주소', + target_id VARCHAR(20) NULL COMMENT '이용 대상 - 공통코드 reserve-target', + excluded_content VARCHAR(2000) NULL COMMENT '사용허가 제외대상', + homepage_url VARCHAR(500) NULL COMMENT '홈페이지 url', + contact_no VARCHAR(50) NULL COMMENT '문의처', + manager_dept_name VARCHAR(200) NULL COMMENT '담당자 소속', + manager_name VARCHAR(200) NULL COMMENT '담당자 이름', + manager_contact_no VARCHAR(50) NULL COMMENT '담당자 연락처', + create_date DATETIME NULL COMMENT '생성일', + created_by VARCHAR(255) NULL COMMENT '생성자', + modified_date DATETIME NULL COMMENT '수정일', + last_modified_by VARCHAR(255) NULL COMMENT '수정자', + PRIMARY KEY (reserve_item_id), + CONSTRAINT FK_reserve_item_location_id FOREIGN KEY (location_id) + REFERENCES location (location_id) ON DELETE RESTRICT ON UPDATE RESTRICT +) ; + + + +-- reserve Table Create SQL +CREATE TABLE IF NOT EXISTS reserve +( + reserve_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '예약 id', + reserve_item_id BIGINT NULL COMMENT '예약 물품 id', + reserve_qty BIGINT(18) NULL COMMENT '예약 신청인원/수량', + reserve_purpose_content VARCHAR(4000) NULL COMMENT '예약신청 목적', + attachment_code VARCHAR(255) NULL COMMENT '첨부파일 코드', + reserve_start_date DATETIME NULL COMMENT '예약 신청 시작일', + reserve_end_date DATETIME NULL COMMENT '예약 신청 종료일', + reserve_status_id VARCHAR(20) NULL COMMENT '예약상태 - 공통코드(reserve-status)', + user_id VARCHAR(255) NULL COMMENT '예약자 id', + user_contact_no VARCHAR(50) NULL COMMENT '예약자 연락처', + user_email_addr VARCHAR(500) NULL COMMENT '예약자 이메일', + create_date DATETIME NULL COMMENT '생성일', + created_by VARCHAR(255) NULL COMMENT '생성자', + modified_date DATETIME NULL COMMENT '수정일', + last_modified_by VARCHAR(255) NULL COMMENT '수정자', + PRIMARY KEY (reserve_id), + CONSTRAINT FK_reserve_reserve_item_id FOREIGN KEY (reserve_item_id) + REFERENCES reserve_item (reserve_item_id) ON DELETE RESTRICT ON UPDATE RESTRICT +) ; + diff --git a/backend/reserve-item-service/Dockerfile b/backend/reserve-item-service/Dockerfile new file mode 100644 index 0000000..bd86ccf --- /dev/null +++ b/backend/reserve-item-service/Dockerfile @@ -0,0 +1,15 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine + +# config server uri: dockder run --e 로 변경 가능 +ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar app.jar +# application port +#EXPOSE 8000 +# 실행 (application-cf.yml 프로필이 기본값) +CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "app.jar"] diff --git a/backend/reserve-item-service/build.gradle b/backend/reserve-item-service/build.gradle new file mode 100644 index 0000000..6ed4ec5 --- /dev/null +++ b/backend/reserve-item-service/build.gradle @@ -0,0 +1,84 @@ +plugins { + id 'org.springframework.boot' version '2.4.5' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'org.egovframe.cloud' +version = '0.1' +sourceCompatibility = '1.8' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() + maven { url "https://maven.egovframe.go.kr/maven/" } // egovframe maven 원격 저장소 +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { +// implementation files('../../module-common/build/libs/module-common-0.1.jar') // @ComponentScan(basePackages={"org.egovframe.cloud"}) 추가해야 적용된다 + implementation 'org.egovframe.cloud:module-common:0.1' + 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-r2dbc' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-config' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j' + implementation 'com.playtika.reactivefeign:feign-reactor-spring-cloud-starter:3.1.0' + + implementation 'io.jsonwebtoken:jjwt:0.9.1' + + implementation 'dev.miku:r2dbc-mysql:0.8.2.RELEASE' + implementation 'mysql:mysql-connector-java' + + // swagger api docs + implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.5.10' + + // bolcking 호출 감지 + implementation 'io.projectreactor:reactor-tools:3.4.9' + implementation 'io.projectreactor.tools:blockhound:1.0.6.RELEASE' + + //messaging + implementation 'org.springframework.cloud:spring-cloud-stream' + implementation 'org.springframework.cloud:spring-cloud-stream-binder-rabbit' + implementation 'net.java.dev.jna:jna:5.9.0' // byte-buddy (No compatible attachment provider is available.) + + //lombok + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + testImplementation 'com.h2database:h2' + testImplementation 'io.r2dbc:r2dbc-h2' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +test { + useJUnitPlatform() +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + diff --git a/backend/reserve-item-service/gradlew b/backend/reserve-item-service/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/reserve-item-service/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/reserve-item-service/gradlew.bat b/backend/reserve-item-service/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/backend/reserve-item-service/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/reserve-item-service/manifest.yml b/backend/reserve-item-service/manifest.yml new file mode 100644 index 0000000..c35ed8d --- /dev/null +++ b/backend/reserve-item-service/manifest.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: egov-reserve-item-service # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-reserve-item-service # host 명으로 유일해야 함 + path: build/libs/reserve-item-service-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + services: + - egov-discovery-provided-service # discovery service binding + env: + spring_profiles_active: cf + spring_cloud_config_uri: https://egov-config.paas-ta.org + app_name: egov-reserve-item-service # logstash custom app name + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/backend/reserve-item-service/settings.gradle b/backend/reserve-item-service/settings.gradle new file mode 100644 index 0000000..ecdddf9 --- /dev/null +++ b/backend/reserve-item-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'reserve-item-service' diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/ReserveItemServiceApplication.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/ReserveItemServiceApplication.java new file mode 100644 index 0000000..db90a43 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/ReserveItemServiceApplication.java @@ -0,0 +1,40 @@ +package org.egovframe.cloud.reserveitemservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.ComponentScan; + +import reactivefeign.spring.config.EnableReactiveFeignClients; +import reactor.blockhound.BlockHound; + +import java.security.Security; + +@ComponentScan({"org.egovframe.cloud.common", "org.egovframe.cloud.reactive", "org.egovframe.cloud.reserveitemservice"}) // org.egovframe.cloud.common package 포함하기 위해 +@EnableDiscoveryClient +@EnableReactiveFeignClients +@SpringBootApplication +public class ReserveItemServiceApplication { + + public static void main(String[] args) { + // TLSv1/v1.1 No longer works after upgrade, "No appropriate protocol" error + String property = Security.getProperty("jdk.tls.disabledAlgorithms").replace(", TLSv1", "").replace(", TLSv1.1", ""); + Security.setProperty("jdk.tls.disabledAlgorithms", property); + + //blocking 코드 감지 + BlockHound.builder() + /** + * mysql r2dbc 에서 호출되는 FileInputStream.readBytes() 가 블로킹코드인데 이를 허용해주도록 한다. + * 해당 코드가 어디서 호출되는지 알지 못하는 상태에서 FileInputStream.readBytes() 자체를 허용해주는 것은 좋지 않다. + * 누군가 무분별하게 사용하게 되면 검출해 낼 수 없어 시스템의 위험요소로 남게 된다. + * r2dbc를 사용하기 위해 FileInputStream.readBytes()를 호출하는 부분만 허용하고 나머지는 여전히 검출대상으로 남기도록 한다. + */ + .allowBlockingCallsInside("dev.miku.r2dbc.mysql.client.ReactorNettyClient", "init") + .install(); + + SpringApplication.run(ReserveItemServiceApplication.class, args); + } + + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/LocationApiController.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/LocationApiController.java new file mode 100644 index 0000000..4a48935 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/LocationApiController.java @@ -0,0 +1,135 @@ +package org.egovframe.cloud.reserveitemservice.api.location; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.reserveitemservice.api.location.dto.LocationResponseDto; +import org.egovframe.cloud.reserveitemservice.api.location.dto.LocationSaveRequestDto; +import org.egovframe.cloud.reserveitemservice.api.location.dto.LocationUpdateRequestDto; +import org.egovframe.cloud.reserveitemservice.service.location.LocationService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.validation.Valid; + + +/** + * org.egovframe.cloud.reserveitemservice.api.location.LocationApiController + *

+ * 예약 지역 api contoller class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj      최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class LocationApiController { + + private final LocationService locationService; + + /** + * 목록 조회 + * + * @param requestDto + * @param page + * @param size + * @return + */ + @GetMapping("/api/v1/locations") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "page of location") + public Mono> search(RequestDto requestDto, + @RequestParam(name = "page") int page, + @RequestParam(name = "size") int size) { + return locationService.search(requestDto, PageRequest.of(page, size)); + } + + /** + * 한건 조회 + * + * @param locationId + * @return + */ + @GetMapping("/api/v1/locations/{locationId}") + @ResponseStatus(HttpStatus.OK) + public Mono findById(@PathVariable Long locationId) { + return locationService.findById(locationId); + } + + /** + * 지역 목록 조회 (사용여부 = true) + * 예약 목록 등록 시 + * + * @return + */ + @GetMapping("/api/v1/locations/combo") + @ResponseStatus(HttpStatus.OK) + public Flux findAll() { + return locationService.findAll(); + } + + /** + * 지역 한건 저장 + * + * @param saveRequestDto + * @return + */ + @PostMapping("/api/v1/locations") + @ResponseStatus(HttpStatus.CREATED) + public Mono save(@Valid @RequestBody LocationSaveRequestDto saveRequestDto) { + return locationService.save(saveRequestDto); + } + + /** + * 지역 한건 수정 + * + * @param locationId + * @param updateRequestDto + * @return + */ + @PutMapping("/api/v1/locations/{locationId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public Mono update(@PathVariable Long locationId, @Valid @RequestBody LocationUpdateRequestDto updateRequestDto) { + return locationService.update(locationId, updateRequestDto); + } + + /** + * 지역 한건 삭제 + * + * @param locationId + * @return + */ + @DeleteMapping("/api/v1/locations/{locationId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public Mono delete(@PathVariable Long locationId) { + return locationService.delete(locationId); + } + + /** + * 지역 사용여부 toggle + * + * @param locationId + * @param isUse + * @return + */ + @PutMapping("/api/v1/locations/{locationId}/{isUse}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public Mono updateIsUse(@PathVariable Long locationId, @PathVariable Boolean isUse) { + return locationService.updateIsUse(locationId, isUse); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationResponseDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationResponseDto.java new file mode 100644 index 0000000..bb51565 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationResponseDto.java @@ -0,0 +1,43 @@ +package org.egovframe.cloud.reserveitemservice.api.location.dto; + +import lombok.*; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.api.location.dto.LocationResponseDto + *

+ * 예약 지역 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class LocationResponseDto { + private Long locationId; + private String locationName; + private Integer sortSeq; + private Boolean isUse; + private LocalDateTime createDate; + + @Builder + public LocationResponseDto(Location entity) { + this.locationId = entity.getLocationId(); + this.locationName = entity.getLocationName(); + this.sortSeq = entity.getSortSeq(); + this.isUse = entity.getIsUse(); + this.createDate = entity.getCreateDate(); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationSaveRequestDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationSaveRequestDto.java new file mode 100644 index 0000000..1285d92 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationSaveRequestDto.java @@ -0,0 +1,56 @@ +package org.egovframe.cloud.reserveitemservice.api.location.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; + +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.reserveitemservice.api.location.dto.LocationSaveRequestDto + *

+ * 예약 지역 저장 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class LocationSaveRequestDto { + @NotNull + private String locationName; + private Integer sortSeq; + private Boolean isUse; + + @Builder + public LocationSaveRequestDto(String locationName, Integer sortSeq, Boolean isUse) { + this.locationName = locationName; + this.sortSeq = sortSeq; + this.isUse = isUse; + } + + /** + * dto -> entity + * + * @return + */ + public Location toEntity() { + return Location.builder() + .locationName(this.locationName) + .sortSeq(this.sortSeq) + .isUse(this.isUse) + .build(); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationUpdateRequestDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationUpdateRequestDto.java new file mode 100644 index 0000000..8f94449 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/location/dto/LocationUpdateRequestDto.java @@ -0,0 +1,55 @@ +package org.egovframe.cloud.reserveitemservice.api.location.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; + +import javax.validation.constraints.NotNull; +/** + * org.egovframe.cloud.reserveitemservice.api.location.dto.LocationUpdateRequestDto + *

+ * 예약 지역 수정 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/08    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class LocationUpdateRequestDto { + @NotNull + private String locationName; + private Integer sortSeq; + private Boolean isUse; + + @Builder + public LocationUpdateRequestDto(String locationName, Integer sortSeq, Boolean isUse) { + this.locationName = locationName; + this.sortSeq = sortSeq; + this.isUse = isUse; + } + + /** + * dto -> entity + * + * @return + */ + public Location toEntity() { + return Location.builder() + .locationName(this.locationName) + .sortSeq(this.sortSeq) + .isUse(this.isUse) + .build(); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/ReserveItemApiController.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/ReserveItemApiController.java new file mode 100644 index 0000000..ceb9b56 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/ReserveItemApiController.java @@ -0,0 +1,180 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.validation.Valid; + +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemListResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemMainResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRelationResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRequestDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemSaveRequestDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemUpdateRequestDto; +import org.egovframe.cloud.reserveitemservice.service.reserveItem.ReserveItemService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reserveitemservice.api.reserveItem.ReserveItemApiController + *

+ * 예약 물품 api controller class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj      최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class ReserveItemApiController { + + private final ReserveItemService reserveItemService; + + /** + * 목록 조회 + * + * @param requestDto + * @param page + * @param size + * @return + */ + @GetMapping("/api/v1/reserve-items") + @ResponseStatus(HttpStatus.OK) + public Mono> search(ReserveItemRequestDto requestDto, + @RequestParam(name = "page") int page, + @RequestParam(name = "size") int size) { + return reserveItemService.search(requestDto, PageRequest.of(page, size)); + } + + /** + * 목록 조회 - 사용자 조회 시 + * + * @param requestDto + * @param page + * @param size + * @return + */ + @GetMapping("/api/v1/{categoryId}/reserve-items") + @ResponseStatus(HttpStatus.OK) + public Mono> searchForUser(@PathVariable String categoryId, + ReserveItemRequestDto requestDto, + @RequestParam(name = "page") int page, + @RequestParam(name = "size") int size) { + return reserveItemService.searchForUser(categoryId, requestDto, PageRequest.of(page, size)); + } + + /** + * 한건 조회 + * + * @param reserveItemId + * @return + */ + @GetMapping("/api/v1/reserve-items/{reserveItemId}") + @ResponseStatus(HttpStatus.OK) + public Mono findById(@PathVariable Long reserveItemId) { + System.out.println("findById : " + reserveItemId); + return reserveItemService.findById(reserveItemId); + } + + /** + * 한건 등록 + * + * @param saveRequestDto + * @return + */ + @PostMapping("/api/v1/reserve-items") + @ResponseStatus(HttpStatus.CREATED) + public Mono save(@Valid @RequestBody ReserveItemSaveRequestDto saveRequestDto) { + return reserveItemService.save(saveRequestDto); + } + + /** + * 한건 수정 + * + * @param reserveItemId + * @param updateRequestDto + * @return + */ + @PutMapping("/api/v1/reserve-items/{reserveItemId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public Mono update(@PathVariable Long reserveItemId, @Valid @RequestBody ReserveItemUpdateRequestDto updateRequestDto) { + return reserveItemService.update(reserveItemId, updateRequestDto); + } + + /** + * 사용여부 업데이트 + * + * @param reserveItemId + * @param isUse + * @return + */ + @PutMapping("/api/v1/reserve-items/{reserveItemId}/{isUse}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public Mono updateIsUse(@PathVariable Long reserveItemId, @PathVariable Boolean isUse) { + return reserveItemService.updateIsUse(reserveItemId, isUse); + } + + /** + * 한건 조회 시 연관관계(지역, 공통코드) 데이터까지 모두 조회 + * + * @param reserveItemId + * @return + */ + @GetMapping("/api/v1/reserve-items/relations/{reserveItemId}") + @ResponseStatus(HttpStatus.OK) + public Mono findByIdWithRelations(@PathVariable Long reserveItemId) { + return reserveItemService.findByIdWithRelations(reserveItemId).log(); + } + + /** + * 관리자가 예약 신청 시 이벤트 스트림 없이 바로 재고 변경 + * + * @param reserveItemId + * @param reserveQty + * @return + */ + @PutMapping("/api/v1/reserve-items/{reserveItemId}/inventories") + @ResponseStatus(HttpStatus.OK) + public Mono updateInventory(@PathVariable Long reserveItemId, @RequestBody Integer reserveQty) { + System.out.println("update inventories : " + reserveItemId+" : " + reserveQty); + return reserveItemService.updateInventory(reserveItemId, reserveQty); + } + + /** + * 각 카테고리별 최신 예약 물품 조회 + * 파라미터로 받는 갯수만큼 조회한다. + * + * @param count 조회할 갯수 0:전체 + * @return + */ + @GetMapping("/api/v1/reserve-items/latest/{count}") + @ResponseStatus(HttpStatus.OK) + public Mono>> findLatest(@PathVariable Integer count) { + return reserveItemService.findLatest(count); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemListResponseDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemListResponseDto.java new file mode 100644 index 0000000..49c49ce --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemListResponseDto.java @@ -0,0 +1,93 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; + + +/** + * org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemListResponseDto + *

+ * 예약 물품 목록 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveItemListResponseDto { + private Long reserveItemId; // 예약 물품 id + private String reserveItemName; //예약 물품 명 + private Long locationId; //지역 id + private String locationName; + private String categoryId; //예약유형 - 공통코드 reserve-category + private String categoryName; + private Integer totalQty; //총 재고/수용인원 수 + private Integer inventoryQty; //총 재고/수용인원 수 + private Boolean isUse; //사용여부 + private LocalDateTime createDate; //등록일 + private Boolean isPossible; //예약 가능 여부 + + @Builder + public ReserveItemListResponseDto(ReserveItem reserveItem) { + this.reserveItemId = reserveItem.getReserveItemId(); + this.reserveItemName = reserveItem.getReserveItemName(); + this.locationId = reserveItem.getLocationId(); + this.locationName = reserveItem.getLocation().getLocationName(); + this.categoryId = reserveItem.getCategoryId(); + this.categoryName = reserveItem.getCategoryName(); + this.totalQty = reserveItem.getTotalQty(); + this.inventoryQty = reserveItem.getInventoryQty(); + this.isUse = reserveItem.getIsUse(); + this.createDate = reserveItem.getCreateDate(); + this.isPossible = isReservationPossible(reserveItem); + } + + /** + * 예약 가능 여부 체크 + * + * @param reserveItem + * @return + */ + private boolean isReservationPossible(ReserveItem reserveItem) { + LocalDateTime now = LocalDateTime.now(); + if (!reserveItem.getIsUse()) { + return false; + } + + if (reserveItem.getInventoryQty() <= 0) { + return false; + } + + if (reserveItem.getIsPeriod()) { + if (reserveItem.getRequestStartDate().isBefore(now) && reserveItem.getRequestEndDate().isAfter(now)) { + return true; + }else { + return false; + } + } else { + if (reserveItem.getOperationStartDate().isBefore(now) && reserveItem.getOperationEndDate().isAfter(now)) { + return true; + }else { + return false; + } + } + } + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemMainResponseDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemMainResponseDto.java new file mode 100644 index 0000000..28ad80b --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemMainResponseDto.java @@ -0,0 +1,73 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem.dto; + +import java.time.LocalDateTime; + +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@ToString +public class ReserveItemMainResponseDto { + + private Long reserveItemId; // 예약 물품 id + private String reserveItemName; //예약 물품 명 + private String categoryId; //예약유형 - 공통코드 reserve-category + private String categoryName; + private LocalDateTime startDate; //운영 시작 일 or 예약 신청 시작일 + private LocalDateTime endDate; //운영 종료 일 or 예약 신청 종료일 + private Boolean isPossible; + + @Builder + public ReserveItemMainResponseDto (ReserveItem entity) { + this.reserveItemId = entity.getReserveItemId(); + this.reserveItemName = entity.getReserveItemName(); + this.categoryId = entity.getCategoryId(); + this.categoryName = entity.getCategoryName(); + this.startDate = entity.getOperationStartDate(); + this.endDate = entity.getOperationEndDate(); + if (entity.getReserveMethodId().equals("internet")) { + if (entity.getReserveMeansId().equals("realtime")) { + this.startDate = entity.getRequestStartDate(); + this.endDate = entity.getRequestEndDate(); + } + } + this.isPossible = isReservationPossible(entity); + } + + /** + * 예약 가능 여부 체크 + * + * @param entity + * @return + */ + private boolean isReservationPossible(ReserveItem entity) { + LocalDateTime now = LocalDateTime.now(); + if (!entity.getIsUse()) { + return false; + } + + if (entity.getInventoryQty() <= 0) { + return false; + } + + if (entity.getIsPeriod()) { + if (entity.getRequestStartDate().isBefore(now) && entity.getRequestEndDate().isAfter(now)) { + return true; + }else { + return false; + } + } else { + if (entity.getOperationStartDate().isBefore(now) && entity.getOperationEndDate().isAfter(now)) { + return true; + }else { + return false; + } + } + } + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemRelationResponseDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemRelationResponseDto.java new file mode 100644 index 0000000..d3e8cc3 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemRelationResponseDto.java @@ -0,0 +1,110 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import org.egovframe.cloud.reserveitemservice.domain.location.Location; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRelationResponseDto + *

+ * 예약 물품 relation 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/27 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/27    shinmj      최초 생성
+ * 
+ */ +@NoArgsConstructor +@Getter +@ToString +public class ReserveItemRelationResponseDto { + private Long reserveItemId; // 예약 물품 id + private String reserveItemName; //예약 물품 명 + private Long locationId; + private Location location; + private String categoryId; //예약유형 - 공통코드 reserve-category + private String categoryName; + private Integer totalQty; //총 재고/수용인원 수 + private Integer inventoryQty; // 재고/수용인원 수 + private LocalDateTime operationStartDate; //운영 시작 일 + private LocalDateTime operationEndDate; //운영 종료 일 + private String reserveMethodId; // 예약 방법 - 공통코드 reserve-method + private String reserveMethodName; + private String reserveMeansId; // 예약 구분 (인터넷 예약 시) - 공통코드 reserve-means + private String reserveMeansName; + private LocalDateTime requestStartDate; //예약 신청 시작 일시 + private LocalDateTime requestEndDate; //예약 신청 종료 일시 + private Boolean isPeriod; //기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가 + private Integer periodMaxCount; // 최대 예약 가능 일 수 + private String externalUrl; //외부링크 + private String selectionMeansId; //선별 방법 - 공통코드 reserve-selection + private String selectionMeansName; + private Boolean isPaid; // 유/무료 - false: 무료, true: 유료 + private BigDecimal usageCost; //이용 요금 + private Boolean isUse; //사용여부 + private String purpose; //용도 + private String address; //주소 + private String targetId; //이용 대상 - 공통코드 reserve-target + private String targetName; + private String excluded; // 사용허가 제외대상 + private String homepage; //홈페이지 주소 + private String contact; //문의처 + private String managerDept; //담당자 소속 + private String managerName; //담당자 이름 + private String managerContact; //담당자 연락처 + + @Builder + public ReserveItemRelationResponseDto(ReserveItem entity) { + this.reserveItemId = entity.getReserveItemId(); + this.reserveItemName = entity.getReserveItemName(); + this.locationId = entity.getLocationId(); + this.location = entity.getLocation(); + this.categoryId = entity.getCategoryId(); + this.categoryName = entity.getCategoryName(); + this.totalQty = entity.getTotalQty(); + this.inventoryQty = entity.getInventoryQty(); + this.operationStartDate = entity.getOperationStartDate(); + this.operationEndDate = entity.getOperationEndDate(); + this.reserveMethodId = entity.getReserveMethodId(); + this.reserveMethodName = entity.getReserveMethodName(); + this.reserveMeansId = entity.getReserveMeansId(); + this.reserveMeansName = entity.getReserveMeansName(); + this.requestStartDate = entity.getRequestStartDate(); + this.requestEndDate = entity.getRequestEndDate(); + this.isPeriod = entity.getIsPeriod(); + this.periodMaxCount = entity.getPeriodMaxCount(); + this.externalUrl = entity.getExternalUrl(); + this.selectionMeansId = entity.getSelectionMeansId(); + this.selectionMeansName = entity.getSelectionMeansName(); + this.isPaid = entity.getIsPaid(); + this.usageCost = entity.getUsageCost(); + this.isUse = entity.getIsUse(); + this.purpose = entity.getPurpose(); + this.address = entity.getAddress(); + this.targetId = entity.getTargetId(); + this.targetName = entity.getTargetName(); + this.excluded = entity.getExcluded(); + this.homepage = entity.getHomepage(); + this.contact = entity.getContact(); + this.managerDept = entity.getManagerDept(); + this.managerName = entity.getManagerName(); + this.managerContact = entity.getManagerContact(); + + } + + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemRequestDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemRequestDto.java new file mode 100644 index 0000000..00929ac --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemRequestDto.java @@ -0,0 +1,31 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem.dto; + +import lombok.*; +import org.egovframe.cloud.common.dto.RequestDto; + +/** + * org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRequestDto + *

+ * 예약 목록 조회 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/27 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/27    shinmj      최초 생성
+ * 
+ */ +@NoArgsConstructor +@Getter +@Setter +@ToString +public class ReserveItemRequestDto extends RequestDto { + private Long locationId; + private String categoryId; + private Boolean isUse; +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemResponseDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemResponseDto.java new file mode 100644 index 0000000..5d217b7 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemResponseDto.java @@ -0,0 +1,94 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem.dto; + +import lombok.*; +import lombok.experimental.Accessors; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemResponseDto + *

+ * 예약 물품 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveItemResponseDto { + private Long reserveItemId; // 예약 물품 id + private String reserveItemName; //예약 물품 명 + private Long locationId; + private String categoryId; //예약유형 - 공통코드 reserve-category + private Integer prevTotalQty; //총 재고/수용인원 수 + private Integer totalQty; //총 재고/수용인원 수 + private Integer inventoryQty; // 재고/수용인원 수 + private LocalDateTime operationStartDate; //운영 시작 일 + private LocalDateTime operationEndDate; //운영 종료 일 + private String reserveMethodId; // 예약 방법 - 공통코드 reserve-method + private String reserveMeansId; // 예약 구분 (인터넷 예약 시) - 공통코드 reserve-means + private LocalDateTime requestStartDate; //예약 신청 시작 일시 + private LocalDateTime requestEndDate; //예약 신청 종료 일시 + private Boolean isPeriod; //기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가 + private Integer periodMaxCount; // 최대 예약 가능 일 수 + private String externalUrl; //외부링크 + private String selectionMeansId; //선별 방법 - 공통코드 reserve-selection + private Boolean isPaid; // 유/무료 - false: 무료, true: 유료 + private BigDecimal usageCost; //이용 요금 + private Boolean isUse; //사용여부 + private String purpose; //용도 + private String address; //주소 + private String targetId; //이용 대상 - 공통코드 reserve-target + private String excluded; // 사용허가 제외대상 + private String homepage; //홈페이지 주소 + private String contact; //문의처 + private String managerDept; //담당자 소속 + private String managerName; //담당자 이름 + private String managerContact; //담당자 연락처 + + @Builder + public ReserveItemResponseDto(ReserveItem reserveItem) { + this.reserveItemId = reserveItem.getReserveItemId(); + this.reserveItemName = reserveItem.getReserveItemName(); + this.locationId = reserveItem.getLocationId(); + this.categoryId = reserveItem.getCategoryId(); + this.prevTotalQty = reserveItem.getTotalQty(); + this.totalQty = reserveItem.getTotalQty(); + this.inventoryQty = reserveItem.getInventoryQty(); + this.operationStartDate = reserveItem.getOperationStartDate(); + this.operationEndDate = reserveItem.getOperationEndDate(); + this.reserveMethodId = reserveItem.getReserveMethodId(); + this.reserveMeansId = reserveItem.getReserveMeansId(); + this.requestStartDate = reserveItem.getRequestStartDate(); + this.requestEndDate = reserveItem.getRequestEndDate(); + this.isPeriod = reserveItem.getIsPeriod(); + this.periodMaxCount = reserveItem.getPeriodMaxCount(); + this.externalUrl = reserveItem.getExternalUrl(); + this.selectionMeansId = reserveItem.getSelectionMeansId(); + this.isPaid = reserveItem.getIsPaid(); + this.usageCost = reserveItem.getUsageCost(); + this.isUse = reserveItem.getIsUse(); + this.purpose = reserveItem.getPurpose(); + this.address = reserveItem.getAddress(); + this.targetId = reserveItem.getTargetId(); + this.excluded = reserveItem.getExcluded(); + this.homepage = reserveItem.getHomepage(); + this.contact = reserveItem.getContact(); + this.managerDept = reserveItem.getManagerDept(); + this.managerName = reserveItem.getManagerName(); + this.managerContact = reserveItem.getManagerContact(); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemSaveRequestDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemSaveRequestDto.java new file mode 100644 index 0000000..7e1108d --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemSaveRequestDto.java @@ -0,0 +1,126 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem; +import org.egovframe.cloud.reserveitemservice.validator.annotation.ReserveItemSaveValid; + +import javax.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemSaveRequestDto + *

+ * 예약 물품 저장 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@ReserveItemSaveValid +public class ReserveItemSaveRequestDto { + + @NotBlank + @Size(max = 200) + private String reserveItemName; //예약 물품 명 + @NotNull + private Long locationId; + @NotBlank + private String categoryId; //예약유형 - 공통코드 reserve-category + @NotNull + @PositiveOrZero + private Integer totalQty; //재고/수용인원 수 + @NotNull + @PositiveOrZero + private Integer inventoryQty; //재고/수용인원 수 + + @NotNull + private LocalDateTime operationStartDate; //운영 시작 일 + @NotNull + private LocalDateTime operationEndDate; //운영 종료 일 + @NotBlank + private String reserveMethodId; // 예약 방법 - 공통코드 reserve-method + private String reserveMeansId; // 예약 구분 (인터넷 예약 시) - 공통코드 reserve-means + private LocalDateTime requestStartDate; //예약 신청 시작 일시 + private LocalDateTime requestEndDate; //예약 신청 종료 일시 + private Boolean isPeriod; //기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가 + private Integer periodMaxCount; // 최대 예약 가능 일 수 + + @Size(max = 500) + private String externalUrl; //외부링크 + @NotBlank + private String selectionMeansId; //선별 방법 - 공통코드 reserve-selection + private Boolean isPaid; // 유/무료 - false: 무료, true: 유료 + private BigDecimal usageCost; //이용 요금 + private Boolean isUse; //사용여부 + + @Size(max = 4000) + private String purpose; //용도 + + @Size(max = 500) + private String address; //주소 + private String targetId; //이용 대상 - 공통코드 reserve-target + + @Size(max = 2000) + private String excluded; // 사용허가 제외대상 + + @Size(max = 500) + private String homepage; //홈페이지 주소 + + @Size(max = 50) + private String contact; //문의처 + + @Size(max = 200) + private String managerDept; //담당자 소속 + + @Size(max = 200) + private String managerName; //담당자 이름 + + @Size(max = 50) + private String managerContact; //담당자 연락처 + + public ReserveItem toEntity() { + return ReserveItem.builder() + .reserveItemName(this.reserveItemName) + .locationId(this.locationId) + .categoryId(this.categoryId) + .totalQty(this.totalQty) + .inventoryQty(this.inventoryQty) + .operationStartDate(this.operationStartDate) + .operationEndDate(this.operationEndDate) + .reserveMethodId(this.reserveMethodId) + .reserveMeansId(this.reserveMeansId) + .requestStartDate(this.requestStartDate) + .requestEndDate(this.requestEndDate) + .isPeriod(this.isPeriod) + .periodMaxCount(this.periodMaxCount) + .externalUrl(this.externalUrl) + .selectionMeansId(this.selectionMeansId) + .isPaid(this.isPaid) + .usageCost(this.usageCost) + .isUse(this.isUse) + .purpose(this.purpose) + .address(this.address) + .targetId(this.targetId) + .excluded(this.excluded) + .homepage(this.homepage) + .contact(this.contact) + .managerDept(this.managerDept) + .managerName(this.managerName) + .managerContact(this.managerContact) + .build(); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemUpdateRequestDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemUpdateRequestDto.java new file mode 100644 index 0000000..5145919 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveItemUpdateRequestDto.java @@ -0,0 +1,117 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem; +import org.egovframe.cloud.reserveitemservice.validator.annotation.ReserveItemSaveValid; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemUpdateRequestDto + *

+ * 예약 물품 수정 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@ReserveItemSaveValid +public class ReserveItemUpdateRequestDto { + @NotBlank + @Size(max = 200) + private String reserveItemName; //예약 물품 명 + @NotNull + private Long locationId; + @NotBlank + private String categoryId; //예약유형 - 공통코드 reserve-category + @NotNull + @PositiveOrZero + private Integer totalQty; //총 재고/수용인원 수 + @NotNull + @PositiveOrZero + private Integer inventoryQty; //재고/수용인원 수 + @NotNull + private LocalDateTime operationStartDate; //운영 시작 일 + @NotNull + private LocalDateTime operationEndDate; //운영 종료 일 + @NotBlank + private String reserveMethodId; // 예약 방법 - 공통코드 reserve-method + private String reserveMeansId; // 예약 구분 (인터넷 예약 시) - 공통코드 reserve-means + private LocalDateTime requestStartDate; //예약 신청 시작 일시 + private LocalDateTime requestEndDate; //예약 신청 종료 일시 + private Boolean isPeriod; //기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가 + private Integer periodMaxCount; // 최대 예약 가능 일 수 + @Size(max = 500) + private String externalUrl; //외부링크 + @NotBlank + private String selectionMeansId; //선별 방법 - 공통코드 reserve-selection + @NotNull + private Boolean isPaid; // 유/무료 - false: 무료, true: 유료 + private BigDecimal usageCost; //이용 요금 + private Boolean isUse; //사용여부 + @Size(max = 4000) + private String purpose; //용도 + @Size(max = 500) + private String address; //주소 + private String targetId; //이용 대상 - 공통코드 reserve-target + @Size(max = 2000) + private String excluded; // 사용허가 제외대상 + @Size(max = 500) + private String homepage; //홈페이지 주소 + @Size(max = 50) + private String contact; //문의처 + @Size(max = 200) + private String managerDept; //담당자 소속 + @Size(max = 200) + private String managerName; //담당자 이름 + @Size(max = 50) + private String managerContact; //담당자 연락처 + + public ReserveItem toEntity() { + return ReserveItem.builder() + .reserveItemName(this.reserveItemName) + .locationId(this.locationId) + .categoryId(this.categoryId) + .totalQty(this.totalQty) + .inventoryQty(this.inventoryQty) + .operationStartDate(this.operationStartDate) + .operationEndDate(this.operationEndDate) + .reserveMethodId(this.reserveMethodId) + .reserveMeansId(this.reserveMeansId) + .requestStartDate(this.requestStartDate) + .requestEndDate(this.requestEndDate) + .isPeriod(this.isPeriod) + .periodMaxCount(this.periodMaxCount) + .externalUrl(this.externalUrl) + .selectionMeansId(this.selectionMeansId) + .isPaid(this.isPaid) + .usageCost(this.usageCost) + .isUse(this.isUse) + .purpose(this.purpose) + .address(this.address) + .targetId(this.targetId) + .excluded(this.excluded) + .homepage(this.homepage) + .contact(this.contact) + .managerDept(this.managerDept) + .managerName(this.managerName) + .managerContact(this.managerContact) + .build(); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveSaveRequestDto.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveSaveRequestDto.java new file mode 100644 index 0000000..7514c5e --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/dto/ReserveSaveRequestDto.java @@ -0,0 +1,64 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem.dto; + +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserverequestservice.api.dto.ReserveSaveRequestDto + *

+ * 예약 신청 저장 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveSaveRequestDto { + + @Setter + private String reserveId; + @NotNull + private Long reserveItemId; + private Integer reserveQty; //예약 신청 인원/수량 + @NotNull + private String reservePurposeContent; //예약 목적 + private String attachmentCode; //첨부파일 코드 + private LocalDateTime reserveStartDate; //예약 신청 시작일 + private LocalDateTime reserveEndDate; //예약 신청 종료일 + @Setter + private String reserveStatusId; //예약상태 - 공통코드(reserve-status) + @NotNull + private String userId; //예약자 + @NotNull + private String userContactNo; //예약자 연락처 + @NotNull + private String userEmail; //예약자 이메일 + + @Builder + public ReserveSaveRequestDto(Long reserveItemId, Integer reserveQty, String reservePurposeContent, String attachmentCode, LocalDateTime reserveStartDate, LocalDateTime reserveEndDate, String reserveStatusId, String userId, String userContactNo, String userEmail) { + this.reserveItemId = reserveItemId; + this.reserveQty = reserveQty; + this.reservePurposeContent = reservePurposeContent; + this.attachmentCode = attachmentCode; + this.reserveStartDate = reserveStartDate; + this.reserveEndDate = reserveEndDate; + this.reserveStatusId = reserveStatusId; + this.userId = userId; + this.userContactNo = userContactNo; + this.userEmail = userEmail; + } + + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/RequestMessage.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/RequestMessage.java new file mode 100644 index 0000000..0edf1e9 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/RequestMessage.java @@ -0,0 +1,39 @@ +package org.egovframe.cloud.reserveitemservice.config; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.reserverequestservice.config.RequestMessage + * + * 예약 신청 후 이벤트 스트림 message VO class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    shinmj       최초 생성
+ * 
+ */ +@NoArgsConstructor +@Getter +@ToString +public class RequestMessage { + private String reserveId; + private Boolean isItemUpdated; + private String uuid; + + @Builder + public RequestMessage(String reserveId, Boolean isItemUpdated, String uuid) { + this.reserveId = reserveId; + this.isItemUpdated = isItemUpdated; + this.uuid = uuid; + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/ReserveEventConfig.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/ReserveEventConfig.java new file mode 100644 index 0000000..8cafd6d --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/ReserveEventConfig.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.reserveitemservice.config; + + +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveSaveRequestDto; +import org.egovframe.cloud.reserveitemservice.service.reserveItem.ReserveItemService; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * org.egovframe.cloud.reserverequestservice.config.ReserveEventConfig + * + * event stream 설정 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@Configuration +public class ReserveEventConfig { + + @Autowired + private ReserveItemService reserveItemService; + + /** + * 예약 신청 후 재고 변경에 대한 consumer + * + * @return + */ + @Bean + public Consumer reserveRequest() { + return reserveSaveRequestDto -> { + log.info("receive data => {}", reserveSaveRequestDto); + reserveItemService.updateInventoryThenSendMessage( + reserveSaveRequestDto.getReserveItemId(), + reserveSaveRequestDto.getReserveQty(), + reserveSaveRequestDto.getReserveId()) + .subscribe(); + }; + } + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/Resilience4JConfig.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/Resilience4JConfig.java new file mode 100644 index 0000000..f92c6c0 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/config/Resilience4JConfig.java @@ -0,0 +1,45 @@ +package org.egovframe.cloud.reserveitemservice.config; + +import java.time.Duration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; + +/** + * org.egovframe.cloud.portalservice.config.Resilience4JConfig + *

+ * Resilience4J Configuration + * 기본 설정값으로 운영되어도 무방하다. 이 클래스는 필수는 아니다. + * retry 기본값은 최대 3회이고, fallback 이 없는 경우에만 동작하므로 설정하지 않았다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/31 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/31    jaeyeolkim  최초 생성
+ *  2021/10/05    shinmj      reactive로 변경
+ * 
+ */ +@Configuration +public class Resilience4JConfig { + + @Bean + public CircuitBreakerRegistry circuitBreakerRegistry() { + CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() + .failureRateThreshold(50) // Circuit 열지 말지 결정하는 실패 threshold 퍼센테이지 + .waitDurationInOpenState(Duration.ofSeconds(5)) // (half closed 전에) circuitBreaker가 open 되기 전에 기다리는 기간 + .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) // circuit breaker count 기반 처리 + .slidingWindowSize(10) // 통계 대상 건수 -> N건의 요청중.. + .build(); + return CircuitBreakerRegistry.of(circuitBreakerConfig); + } + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/code/Code.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/code/Code.java new file mode 100644 index 0000000..c0d8f90 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/code/Code.java @@ -0,0 +1,51 @@ +package org.egovframe.cloud.reserveitemservice.domain.code; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reactive.domain.BaseEntity; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +/** + * org.egovframe.cloud.boardservice.domain.org.egovframe.cloud.reserveitemservice.domain.org.egovframe.cloud.reserveitemservice.domain.code.Code + *

+ * 공통코드 엔티티 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jaeyeolkim  최초 생성
+ *  2021/09/15    shinmj      r2dbc 변경
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@Table("code") +public class Code extends BaseEntity { + @Id + @Column + private String codeId; // 코드ID + + @Column + private String parentCodeId; // 상위 코드ID + + @Column + private String codeName; // 코드 명 + + @Builder + public Code(String codeId, String parentCodeId, String codeName) { + this.codeId = codeId; + this.parentCodeId = parentCodeId; + this.codeName = codeName; + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/location/Location.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/location/Location.java new file mode 100644 index 0000000..92d1af0 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/location/Location.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.reserveitemservice.domain.location; + +import lombok.*; +import org.egovframe.cloud.reactive.domain.BaseEntity; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import javax.validation.constraints.Size; + +/** + * org.egovframe.cloud.reserveitemservice.domain.location.Location + * + * 예약 지역 도메인 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@Table("location") +public class Location extends BaseEntity { + + @Id + private Long locationId; + + @Size(max = 200) + @Column + private String locationName; + @Column + private Integer sortSeq; + + @Column("use_at") + private Boolean isUse; + + @Builder + public Location(Long locationId, String locationName, Integer sortSeq, Boolean isUse) { + this.locationId = locationId; + this.locationName = locationName; + this.sortSeq = sortSeq; + this.isUse = isUse; + } + + public Location update(String locationName, Integer sortSeq, Boolean isUse) { + this.locationName = locationName; + this.sortSeq = sortSeq; + this.isUse = isUse; + return this; + } + + public Location updateIsUse(Boolean isUse) { + this.isUse = isUse; + return this; + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/location/LocationRepository.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/location/LocationRepository.java new file mode 100644 index 0000000..3733913 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/location/LocationRepository.java @@ -0,0 +1,58 @@ +package org.egovframe.cloud.reserveitemservice.domain.location; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reserveitemservice.domain.location.LocationRepository + * + * 예약 지역 R2dbc repository 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj       최초 생성
+ * 
+ */ +@Repository +public interface LocationRepository extends R2dbcRepository { + + /** + * 검색조건(지역이름)을 포함한 목록조회 + * + * @param locationName + * @param pageable + * @return + */ + Flux findAllByLocationNameContainingOrderBySortSeq(String locationName, Pageable pageable); + + /** + * 검색조건(지역이름)을 포함한 count + * paging 처리를 하기 위해서 조회 + * + * @param locationName + * @return + */ + Mono countAllByLocationNameContaining(String locationName); + + /** + * paging 처리를 하기 위한 목록 조회 + * + * @param pageable + * @return + */ + Flux findAllByOrderBySortSeq (Pageable pageable); + + Flux findAllByIsUseTrueOrderBySortSeq(); + +} + diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/Category.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/Category.java new file mode 100644 index 0000000..117693e --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/Category.java @@ -0,0 +1,19 @@ +package org.egovframe.cloud.reserveitemservice.domain.reserveItem; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Category { + EDUCATION("education", "교육"), + EQUIPMENT("equipment", "장비"), + SPACE("space", "공간"); + + private final String key; + private final String title; + + public boolean isEquals(String compare) { + return this.getKey().equals(compare); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItem.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItem.java new file mode 100644 index 0000000..d8cedb6 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItem.java @@ -0,0 +1,341 @@ +package org.egovframe.cloud.reserveitemservice.domain.reserveItem; + + +import lombok.*; +import lombok.experimental.Accessors; +import org.egovframe.cloud.reactive.domain.BaseEntity; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemUpdateRequestDto; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem + * + * 예약 물품 도메인 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/09 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/09    shinmj       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@Table("reserve_item") +public class ReserveItem extends BaseEntity { + + @Id + @Column("reserve_item_id") + private Long reserveItemId; // 예약 물품 id + + @Size(max = 200) + @NotNull + @Column("reserve_item_name") + private String reserveItemName; //예약 물품 명 + + @Column + private Long locationId; + + @ToString.Exclude + @Transient + private Location location; //지역 + + @Size(max = 20) + @NotNull + @Column + private String categoryId; //예약유형 - 공통코드 reserve-category + + @Transient + private String categoryName; + + @Size(max = 5) + @NotNull + @Column + private Integer totalQty; //총 재고/수용인원 수 + + @Size(max = 5) + @Column + private Integer inventoryQty; //현재 재고/수용인원 수 + + @Column + private LocalDateTime operationStartDate; //운영 시작 일 + + @Column + private LocalDateTime operationEndDate; //운영 종료 일 + + @Size(max = 20) + @NotNull + @Column + private String reserveMethodId; // 예약 방법 - 공통코드 reserve-method + + @Transient + private String reserveMethodName; + + @Size(max = 20) + @Column + private String reserveMeansId; // 예약 구분 (인터넷 예약 시) - 공통코드 reserve-means + + @Transient + private String reserveMeansName; + + @Column + private LocalDateTime requestStartDate; //예약 신청 시작 일시 + + @Column + private LocalDateTime requestEndDate; //예약 신청 종료 일시 + + @Column("period_at") + private Boolean isPeriod; //기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가 + + @Size(max = 3) + @Column + private Integer periodMaxCount; // 최대 예약 가능 일 수 + + @Size(max = 500) + @Column + private String externalUrl; //외부링크 + + @Size(max = 20) + @NotNull + @Column + private String selectionMeansId; //선별 방법 - 공통코드 reserve-selection + + @Transient + private String selectionMeansName; + + @Column("paid_at") + private Boolean isPaid; // 유/무료 - false: 무료, true: 유료 + + @Column + private BigDecimal usageCost; //이용 요금 + + @Column("use_at") + private Boolean isUse; //사용여부 + + @Size(max = 4000) + @Column("purpose_content") + private String purpose; //용도 + + @Size(max = 500) + @Column("item_addr") + private String address; //주소 + + @Size(max = 20) + @Column + private String targetId; //이용 대상 - 공통코드 reserve-target + + @Transient + private String targetName; + + @Size(max = 2000) + @Column("excluded_content") + private String excluded; // 사용허가 제외대상 + + @Size(max = 500) + @Column("homepage_url") + private String homepage; //홈페이지 주소 + + @Size(max = 50) + @Column("contact_no") + private String contact; //문의처 + + @Size(max = 200) + @Column("manager_dept_name") + private String managerDept; //담당자 소속 + + @Size(max = 200) + @Column("manager_name") + private String managerName; //담당자 이름 + + @Size(max = 50) + @Column("manager_contact_no") + private String managerContact; //담당자 연락처 + + @Builder + public ReserveItem(Long reserveItemId, String reserveItemName, Long locationId, Location location, String categoryId, String categoryName, Integer totalQty, Integer inventoryQty, LocalDateTime operationStartDate, LocalDateTime operationEndDate, String reserveMethodId, String reserveMethodName, String reserveMeansId, String reserveMeansName, LocalDateTime requestStartDate, LocalDateTime requestEndDate, Boolean isPeriod, Integer periodMaxCount, String externalUrl, String selectionMeansId, String selectionMeansName, Boolean isPaid, BigDecimal usageCost, Boolean isUse, String purpose, String address, String targetId, String targetName, String excluded, String homepage, String contact, String managerDept, String managerName, String managerContact) { + this.reserveItemId = reserveItemId; + this.reserveItemName = reserveItemName; + this.locationId = locationId; + this.location = location; + this.categoryId = categoryId; + this.categoryName = categoryName; + this.totalQty = totalQty; + this.inventoryQty = inventoryQty; + this.operationStartDate = operationStartDate; + this.operationEndDate = operationEndDate; + this.reserveMethodId = reserveMethodId; + this.reserveMethodName = reserveMethodName; + this.reserveMeansId = reserveMeansId; + this.reserveMeansName = reserveMeansName; + this.requestStartDate = requestStartDate; + this.requestEndDate = requestEndDate; + this.isPeriod = isPeriod; + this.periodMaxCount = periodMaxCount; + this.externalUrl = externalUrl; + this.selectionMeansId = selectionMeansId; + this.selectionMeansName = selectionMeansName; + this.isPaid = isPaid; + this.usageCost = usageCost; + this.isUse = isUse; + this.purpose = purpose; + this.address = address; + this.targetId = targetId; + this.targetName = targetName; + this.excluded = excluded; + this.homepage = homepage; + this.contact = contact; + this.managerDept = managerDept; + this.managerName = managerName; + this.managerContact = managerContact; + } + + /** + * 예약 지역 정보 조회 + * + * @param location + * @return + */ + public ReserveItem setLocation(Location location) { + this.location = location; + return this; + } + + /** + * 예약 유형 명칭 조회 세팅 + * + * @param categoryName + * @return + */ + public ReserveItem setCategoryName(String categoryName) { + this.categoryName = categoryName; + return this; + } + + /** + * 예약 방법 명칭 + * + * @param reserveMethodName + * @return + */ + public ReserveItem setReserveMethodName(String reserveMethodName) { + this.reserveMethodName = reserveMethodName; + return this; + } + + /** + * 예약 구분 명칭 + * + * @param reserveMeansName + * @return + */ + public ReserveItem setReserveMeansName(String reserveMeansName) { + this.reserveMeansName = reserveMeansName; + return this; + } + + /** + * 선별 방법 명칭 + * + * @param selectionMeansName + * @return + */ + public ReserveItem setSelectionMeansName(String selectionMeansName) { + this.selectionMeansName = selectionMeansName; + return this; + } + + /** + * 이용 대상 명칭 + * + * @param targetName + * @return + */ + public ReserveItem setTargetName(String targetName) { + this.targetName = targetName; + return this; + } + + + /** + * 예약 물품 정보 업데이트 + * + * @param updateRequestDto + * @return + */ + public ReserveItem update(ReserveItemUpdateRequestDto updateRequestDto) { + System.out.println("============ ?? : " + updateRequestDto.toString()); + this.reserveItemName = updateRequestDto.getReserveItemName(); + this.locationId = updateRequestDto.getLocationId(); + this.categoryId = updateRequestDto.getCategoryId(); + this.totalQty = updateRequestDto.getTotalQty(); + this.inventoryQty = updateRequestDto.getInventoryQty(); + this.operationStartDate = updateRequestDto.getOperationStartDate(); + this.operationEndDate = updateRequestDto.getOperationEndDate(); + this.reserveMethodId = updateRequestDto.getReserveMethodId(); + this.reserveMeansId = updateRequestDto.getReserveMeansId(); + this.requestStartDate = updateRequestDto.getRequestStartDate(); + this.requestEndDate = updateRequestDto.getRequestEndDate(); + this.isPeriod = updateRequestDto.getIsPeriod(); + this.periodMaxCount = updateRequestDto.getPeriodMaxCount(); + this.externalUrl = updateRequestDto.getExternalUrl(); + this.selectionMeansId = updateRequestDto.getSelectionMeansId(); + this.isPaid = updateRequestDto.getIsPaid(); + this.usageCost = updateRequestDto.getUsageCost(); + this.isUse = updateRequestDto.getIsUse(); + this.purpose = updateRequestDto.getPurpose(); + this.address = updateRequestDto.getAddress(); + this.targetId = updateRequestDto.getTargetId(); + this.excluded = updateRequestDto.getExcluded(); + this.homepage = updateRequestDto.getHomepage(); + this.contact = updateRequestDto.getContact(); + this.managerDept = updateRequestDto.getManagerDept(); + this.managerName = updateRequestDto.getManagerName(); + this.managerContact = updateRequestDto.getManagerContact(); + + return this; + } + + /** + * 재고 변경 + * + * @param inventoryQty + * @return + */ + public ReserveItem updateInventoryQty(Integer inventoryQty) { + this.inventoryQty = inventoryQty; + return this; + } + + /** + * 사용여부 변경 + * + * @param isUse + * @return + */ + public ReserveItem updateIsUse(Boolean isUse) { + this.isUse = isUse; + return this; + } + + public ReserveItem setCreateDate(LocalDateTime createDate) { + this.createDate = createDate; + return this; + } +} + + diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepository.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepository.java new file mode 100644 index 0000000..6eb0808 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepository.java @@ -0,0 +1,25 @@ +package org.egovframe.cloud.reserveitemservice.domain.reserveItem; + +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import reactor.core.publisher.Flux; + +/** + * org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItemRepository + * + * 예약 물품 도메인 repository interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/09 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/09    shinmj       최초 생성
+ * 
+ */ +public interface ReserveItemRepository extends R2dbcRepository, ReserveItemRepositoryCustom { + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepositoryCustom.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepositoryCustom.java new file mode 100644 index 0000000..55d55b5 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepositoryCustom.java @@ -0,0 +1,43 @@ +package org.egovframe.cloud.reserveitemservice.domain.reserveItem; + +import java.time.LocalDateTime; + +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemMainResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRequestDto; +import org.egovframe.cloud.reserveitemservice.domain.code.Code; +import org.springframework.data.domain.Pageable; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItemRepositoryCustom + * + * 예약 물품 도메인 repository custom(query) interface + * R2DBCEntityTemplate을 이용하여 쿼리하기 위한 Interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj       최초 생성
+ * 
+ */ +public interface ReserveItemRepositoryCustom { + + Flux search(ReserveItemRequestDto requestDto, Pageable pageable); + Mono searchCount(ReserveItemRequestDto requestDto, Pageable pageable); + + Flux searchForUser(String categoryId, ReserveItemRequestDto requestDto, Pageable pageable); + Mono searchCountForUser(String categoryId, ReserveItemRequestDto requestDto, Pageable pageable); + + Mono findWithRelation(Long reserveItemId); + + Flux findLatestByCategory(Integer count, String categoryId); + Flux findCodeDetail(String codeId); +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepositoryImpl.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepositoryImpl.java new file mode 100644 index 0000000..c8937cb --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/domain/reserveItem/ReserveItemRepositoryImpl.java @@ -0,0 +1,287 @@ +package org.egovframe.cloud.reserveitemservice.domain.reserveItem; + +import static org.springframework.data.relational.core.query.Criteria.*; + +import java.util.ArrayList; +import java.util.List; + +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRequestDto; +import org.egovframe.cloud.reserveitemservice.domain.code.Code; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.relational.core.query.Criteria; +import org.springframework.data.relational.core.query.Query; +import org.springframework.util.StringUtils; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItemRepositoryImpl + * + * 예약 물품 도메인 repository custom(query) 구현체 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +public class ReserveItemRepositoryImpl implements ReserveItemRepositoryCustom{ + + private final R2dbcEntityTemplate entityTemplate; + + /** + * page 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Override + public Flux search(ReserveItemRequestDto requestDto, Pageable pageable) { + return entityTemplate.select(ReserveItem.class) + .matching(Query.query(Criteria.from(whereQuery(requestDto))) + .sort(Sort.by(Sort.Direction.DESC, "create_date")) + .with(pageable)) + .all() + .flatMap(this::loadRelations) + .switchIfEmpty(Flux.empty()); + } + + /** + * 목록 total count 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Override + public Mono searchCount(ReserveItemRequestDto requestDto, Pageable pageable) { + return entityTemplate.select(ReserveItem.class) + .matching(Query.query(Criteria.from(whereQuery(requestDto))) + .sort(Sort.by(Sort.Direction.DESC, "create_date")) + .with(pageable)) + .count(); + } + + @Override + public Flux searchForUser(String categoryId, ReserveItemRequestDto requestDto, Pageable pageable) { + Criteria where = Criteria.from(whereQuery(requestDto)); + + if (!"all".equals(categoryId)) { + where = where.and(where("category_id").is(categoryId)); + } + + Query query = Query.query(where("use_at").isTrue().and(where)) + .sort(Sort.by(Sort.Direction.DESC, "create_date")) + .with(pageable); + + return entityTemplate.select(ReserveItem.class) + .matching(query) + .all() + .flatMap(this::loadRelations) + .switchIfEmpty(Flux.empty()); + } + + + @Override + public Mono searchCountForUser(String categoryId, ReserveItemRequestDto requestDto, Pageable pageable) { + Criteria where = Criteria.from(whereQuery(requestDto)); + + if (!"all".equals(categoryId)) { + where = where.and(where("category_id").is(categoryId)); + } + + Query query = Query.query(where("use_at").isTrue().and(where)) + .sort(Sort.by(Sort.Direction.DESC, "create_date")) + .with(pageable); + return entityTemplate.select(ReserveItem.class) + .matching(query) + .count(); + } + + /** + * relation 걸린 table 정보도 같이 조회 + * 공통코드, 지역 + * + * @param reserveItemId + * @return + */ + @Override + public Mono findWithRelation(Long reserveItemId) { + return entityTemplate.selectOne(Query.query(where("reserve_item_id").is(reserveItemId)), ReserveItem.class) + .flatMap(this::loadRelationsAll) + .switchIfEmpty(Mono.empty()); + } + + /** + * 카테고리별 예약 물품 최신 데이터 count 만큼 조회 + * + * @param count 조회할 갯수 0:전체 + * @param categoryId 카테고리 아이디 + * @return + */ + @Override + public Flux findLatestByCategory(Integer count, String categoryId) { + Query query =Query.query(where("category_id").is(categoryId) + .and("use_at").isTrue()).sort(Sort.by(Sort.Order.desc("create_date"))); + + if (count > 0) { + query.limit(count); + } + + return entityTemplate.select(ReserveItem.class) + .matching(query) + .all() + .flatMap(this::loadRelations); + } + + /** + * 공통코드 조회 + * + * @param codeId + * @return + */ + @Override + public Flux findCodeDetail(String codeId) { + return entityTemplate.select(Code.class) + .matching(Query.query(where("parent_code_id").is(codeId).and("use_at").isTrue())) + .all(); + } + + /** + * 유형만 공통코드 조회 + * + * @param reserveItem + * @return + */ + private Mono loadRelations(final ReserveItem reserveItem) { + //load common code + Mono mono = Mono.just(reserveItem) + .zipWith(findCodeById(reserveItem.getCategoryId())) + .map(tuple -> tuple.getT1().setCategoryName(tuple.getT2().getCodeName())) + .switchIfEmpty(Mono.just(reserveItem)); + + // load location + mono = mono.zipWith(findLocationById(reserveItem.getLocationId())) + .map(tuple -> tuple.getT1().setLocation(tuple.getT2())) + .switchIfEmpty(mono); + + return mono; + } + + /** + * 공통코드 이름 조회 (모든 공통코드에 대해 조회) + * + * @param reserveItem + * @return + */ + private Mono loadRelationsAll(final ReserveItem reserveItem) { + //load common code + Mono mono = Mono.just(reserveItem) + .zipWith(findCodeById(reserveItem.getCategoryId())) + .map(tuple -> tuple.getT1().setCategoryName(tuple.getT2().getCodeName())) + .zipWith(findCodeById(reserveItem.getReserveMethodId())) + .map(tuple -> tuple.getT1().setReserveMethodName(tuple.getT2().getCodeName())) + .zipWith(findCodeById(reserveItem.getReserveMeansId())) + .map(tuple -> tuple.getT1().setReserveMeansName(tuple.getT2().getCodeName())) + .zipWith(findCodeById(reserveItem.getSelectionMeansId())) + .map(tuple -> tuple.getT1().setSelectionMeansName(tuple.getT2().getCodeName())) + .zipWith(findCodeById(reserveItem.getTargetId())) + .map(tuple -> tuple.getT1().setTargetName(tuple.getT2().getCodeName())) + .switchIfEmpty(Mono.just(reserveItem)); + + // load location + mono = mono.zipWith(findLocationById(reserveItem.getLocationId())) + .map(tuple -> tuple.getT1().setLocation(tuple.getT2())) + .switchIfEmpty(mono); + + return mono; + } + + /** + * 지역 조회 + * + * @param locationId + * @return + */ + private Mono findLocationById(Long locationId ) { + return entityTemplate.select(Location.class) + .matching(Query.query(where("location_id").is(locationId))) + .one() + .switchIfEmpty(Mono.empty()); + } + + /** + * 공통 코드 조회 + * + * @param codeId + * @return + */ + private Mono findCodeById(String codeId ) { + return entityTemplate.select(Code.class) + .matching(Query.query(where("code_id").is(codeId))) + .one() + .switchIfEmpty(Mono.empty()); + + } + + /** + * 조회조건 쿼리 + * + * @param requestDto + * @return + */ + private List whereQuery(ReserveItemRequestDto requestDto) { + String keywordType = requestDto.getKeywordType(); + String keyword = requestDto.getKeyword(); + + + List whereCriteria = new ArrayList<>(); + + if (StringUtils.hasText(keyword)) { + if ("item".equals(keywordType)) { + whereCriteria.add(where("reserve_item_name").like(likeText(keyword))); + } + } + + if (requestDto.getLocationId() != null) { + whereCriteria.add(where("location_id").in(requestDto.getLocationId())); + } + + if (requestDto.getCategoryId() != null ) { + whereCriteria.add(where("category_id").in(requestDto.getCategoryId())); + } + + // 물품 팝업에서 조회하는 경우 인터넷 예약이 가능한 물품만 조회 + if (requestDto.getIsUse()) { + whereCriteria.add(where("use_at").isTrue()); + whereCriteria.add(where("reserve_method_id").is("internet")); + } + + return whereCriteria; + } + + /** + * like 검색 + * + * @param keyword + * @return + */ + private String likeText(String keyword) { + return "%" + keyword + "%"; + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/service/location/LocationService.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/service/location/LocationService.java new file mode 100644 index 0000000..5c73685 --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/service/location/LocationService.java @@ -0,0 +1,185 @@ +package org.egovframe.cloud.reserveitemservice.service.location; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.common.util.MessageUtil; +import org.egovframe.cloud.reactive.service.ReactiveAbstractService; +import org.egovframe.cloud.reserveitemservice.api.location.dto.LocationResponseDto; +import org.egovframe.cloud.reserveitemservice.api.location.dto.LocationSaveRequestDto; +import org.egovframe.cloud.reserveitemservice.api.location.dto.LocationUpdateRequestDto; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; +import org.egovframe.cloud.reserveitemservice.domain.location.LocationRepository; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * org.egovframe.cloud.reserveitemservice.service.location.LocationService + * + * 예약 지역 service 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/06 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/06    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class LocationService extends ReactiveAbstractService { + + private final LocationRepository locationRepository; + + /** + * 검색조건 없을 경우 전체 목록 조회 + * + * @param pageable + * @return + */ + private Mono> findAll(Pageable pageable) { + return locationRepository.findAllByOrderBySortSeq(pageable) + .flatMap(this::convertLocationResponseDto) + .collectList() + .zipWith(locationRepository.count()) + .flatMap(tuple -> Mono.just(new PageImpl<>(tuple.getT1(), pageable, tuple.getT2()))); + } + + /** + * entity를 응답 dto 형태로 변환 + * + * @param location + * @return + */ + private Mono convertLocationResponseDto(Location location) { + return Mono.just(LocationResponseDto.builder() + .entity(location) + .build()); + } + + /** + * 예약 지역 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Transactional(readOnly = true) + public Mono> search(RequestDto requestDto, Pageable pageable) { + if (!StringUtils.hasText(requestDto.getKeywordType()) || !StringUtils.hasText(requestDto.getKeyword())) { + return findAll(pageable); + } + + if ("locationName".equals(requestDto.getKeywordType()) + && StringUtils.hasText(requestDto.getKeyword()) + ) { + return locationRepository.findAllByLocationNameContainingOrderBySortSeq(requestDto.getKeyword(), pageable) + .flatMap(this::convertLocationResponseDto) + .collectList() + .zipWith(locationRepository.countAllByLocationNameContaining(requestDto.getKeyword())) + .flatMap(tuple -> Mono.just(new PageImpl<>(tuple.getT1(), pageable, tuple.getT2()))); + } + + return findAll(pageable); + } + + /** + * 예약 지역 한건 조회 + * + * @param locationId + * @return + */ + @Transactional(readOnly = true) + public Mono findById(Long locationId) { + return locationRepository.findById(locationId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(locationId)) + .flatMap(this::convertLocationResponseDto); + } + + /** + * 지역 목록 조회 - 사용여부 = true + * + * @return + */ + @Transactional(readOnly = true) + public Flux findAll() { + return locationRepository.findAllByIsUseTrueOrderBySortSeq() + .flatMap(this::convertLocationResponseDto); + } + + /** + * 예약 지역 저장 + * + * @param saveRequestDto + * @return + */ + public Mono save(LocationSaveRequestDto saveRequestDto) { + return locationRepository.save(saveRequestDto.toEntity()) + .flatMap(this::convertLocationResponseDto); + } + + /** + * 예약 지역 한건 저장 + * + * @param locationId + * @param updateRequestDto + * @return + */ + public Mono update(Long locationId, LocationUpdateRequestDto updateRequestDto) { + return locationRepository.findById(locationId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(locationId)) + .map(location -> + location.update(updateRequestDto.getLocationName(), + updateRequestDto.getSortSeq(), + updateRequestDto.getIsUse()) + ) + .flatMap(locationRepository::save) + .then(); + } + + /** + * 예약 지역 한건 삭제 + * + * @param locationId + * @return + */ + public Mono delete(Long locationId) { + return locationRepository.findById(locationId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(locationId)) + .flatMap(locationRepository::delete) + .onErrorResume(DataIntegrityViolationException.class, + throwable -> Mono.error(new BusinessMessageException("참조하는 데이터가 있어 삭제할 수 없습니다."))); + } + + /** + * 예약 지역 사용여부 토글 + * + * @param locationId + * @param isUse + * @return + */ + public Mono updateIsUse(Long locationId, Boolean isUse) { + return locationRepository.findById(locationId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(locationId)) + .map(location -> location.updateIsUse(isUse)) + .flatMap(locationRepository::save) + .then(); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/service/reserveItem/ReserveItemService.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/service/reserveItem/ReserveItemService.java new file mode 100644 index 0000000..1466c7e --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/service/reserveItem/ReserveItemService.java @@ -0,0 +1,278 @@ +package org.egovframe.cloud.reserveitemservice.service.reserveItem; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.reactive.service.ReactiveAbstractService; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemListResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemMainResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRelationResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRequestDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemResponseDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemSaveRequestDto; +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemUpdateRequestDto; +import org.egovframe.cloud.reserveitemservice.config.RequestMessage; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.Category; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItemRepository; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +/** + * org.egovframe.cloud.reserveitemservice.service.reserveItem.ReserveItemService + * + * 예약 물품 service class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReserveItemService extends ReactiveAbstractService { + + private static final String RESERVE_CATEGORY_CODE = "reserve-category"; + + private final ReserveItemRepository reserveItemRepository; + private final StreamBridge streamBridge; + + + /** + * entity -> dto 변환 + * + * @param reserveItem + * @return + */ + private Mono convertReserveItemResponseDto(ReserveItem reserveItem) { + return Mono.just(ReserveItemResponseDto.builder().reserveItem(reserveItem).build()); + } + + /** + * entity -> dto 변환 + * + * @param reserveItem + * @return + */ + private Mono convertReserveItemListResponseDto(ReserveItem reserveItem) { + return Mono.just(ReserveItemListResponseDto.builder().reserveItem(reserveItem).build()); + } + + /** + * 목록 조회 + * + * @param requestDto + * @param pageable + * @return + */ + @Transactional(readOnly = true) + public Mono> search(ReserveItemRequestDto requestDto, Pageable pageable) { + return reserveItemRepository.search(requestDto, pageable) + .flatMap(this::convertReserveItemListResponseDto) + .collectList() + .zipWith(reserveItemRepository.searchCount(requestDto, pageable)) + .flatMap(tuple -> Mono.just(new PageImpl<>(tuple.getT1(), pageable, tuple.getT2()))); + } + + /** + * 목록 조회 - 사용자가 조회 시 + * + * @param categoryId + * @param requestDto + * @param pageable + * @return + */ + @Transactional(readOnly = true) + public Mono> searchForUser(String categoryId, ReserveItemRequestDto requestDto, Pageable pageable) { + return reserveItemRepository.searchForUser(categoryId, requestDto, pageable) + .flatMap(this::convertReserveItemListResponseDto) + .collectList() + .zipWith(reserveItemRepository.searchCountForUser(categoryId, requestDto, pageable)) + .flatMap(tuple -> Mono.just(new PageImpl<>(tuple.getT1(), pageable, tuple.getT2()))); + } + + /** + * 한건 조회 + * + * @param reserveItemId + * @return + */ + @Transactional(readOnly = true) + public Mono findById(Long reserveItemId) { + return reserveItemRepository.findById(reserveItemId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(reserveItemId)) + .flatMap(this::convertReserveItemResponseDto); + } + + /** + * 저장 + * + * @param saveRequestDto + * @return + */ + public Mono save(ReserveItemSaveRequestDto saveRequestDto) { + return reserveItemRepository.save(saveRequestDto.toEntity()) + .flatMap(this::convertReserveItemResponseDto); + } + + /** + * 수정 + * + * @param id + * @param updateRequestDto + * @return + */ + public Mono update(Long id, ReserveItemUpdateRequestDto updateRequestDto) { + return reserveItemRepository.findById(id) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(id)) + .map(reserveItem -> reserveItem.update(updateRequestDto)) + .flatMap(reserveItemRepository::save) + .then(); + } + + /** + * 사용여부 업데이트 + * + * @param reserveItemId + * @param isUse + * @return + */ + public Mono updateIsUse(Long reserveItemId, Boolean isUse) { + return reserveItemRepository.findById(reserveItemId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(reserveItemId)) + .map(reserveItem -> reserveItem.updateIsUse(isUse)) + .flatMap(reserveItemRepository::save) + .then(); + } + + /** + * 예약 신청(관리자) 시 재고 변경 + * + * @param reserveItemId + * @param reserveQty + * @return + */ + public Mono updateInventory(Long reserveItemId, Integer reserveQty) { + return reserveItemRepository.findById(reserveItemId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(reserveItemId)) + .flatMap(reserveItem -> { + int qty = reserveItem.getInventoryQty() - reserveQty; + if (qty < 0) { + return Mono.just(false); + } + return reserveItemRepository.save(reserveItem.updateInventoryQty(qty)).thenReturn(true); + }); + } + + /** + * 예약 신청(사용자) 시 재고 변경 + * + * @param reserveItemId + * @param reserveQty + * @return + */ + public Mono updateInventoryThenSendMessage(Long reserveItemId, Integer reserveQty, String reserveId) { + return reserveItemRepository.findById(reserveItemId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(reserveItemId)) + .flatMap(reserveItem -> { + if (!Category.EDUCATION.isEquals(reserveItem.getCategoryId())) { + return Mono.error(new BusinessMessageException("저장할 수 없습니다.")); + } + + LocalDateTime now = LocalDateTime.now(); + if (!(now.isAfter(reserveItem.getRequestStartDate()) && now.isBefore(reserveItem.getRequestEndDate()))) { + return Mono.error(new BusinessMessageException("예약 가능 일자가 아닙니다.")); + } + + int qty = reserveItem.getInventoryQty() - reserveQty; + if (qty < 0) { + return Mono.error(new BusinessMessageException("재고가 없습니다.")); + } + + return Mono.just(reserveItem.updateInventoryQty(qty)); + }) + .flatMap(reserveItemRepository::save) + .publishOn(Schedulers.boundedElastic()) + .doOnNext(reserveItem -> { + log.info("reserve item inventory updated success"); + sendMessage(reserveId, true); + }) + .doOnError(throwable -> { + log.info("reserve item inventory updated fail = {}", throwable.getMessage()); + sendMessage(reserveId, false); + }).then(); + + } + + + /** + * 재고 변경 성공 여부 이벤트 발생 + * + * @param reserveId + * @param isItemUpdated + */ + private void sendMessage(String reserveId, Boolean isItemUpdated) { + streamBridge.send("inventoryUpdated-out-0", + MessageBuilder.withPayload( + RequestMessage.builder() + .reserveId(reserveId) + .isItemUpdated(isItemUpdated) + .build()) + .setHeader("reserveUUID", reserveId).build()); + } + + /** + * 한건 조회 - 연관된 데이터도 같이 조회 (e.g. codename, location) + * + * @param reserveItemId + * @return + */ + public Mono findByIdWithRelations(Long reserveItemId) { + return reserveItemRepository.findWithRelation(reserveItemId) + .switchIfEmpty(monoResponseStatusEntityNotFoundException(reserveItemId)) + .flatMap(reserveItem -> + Mono.just(ReserveItemRelationResponseDto.builder().entity(reserveItem).build())); + } + + /** + * 각 카테고리별 최신 예약 물품 조회 + * 파라미터로 받는 갯수만큼 조회한다. + * + * @param count 조회할 갯수 0:전체 + * @return + */ + public Mono>> findLatest(Integer count) { + return reserveItemRepository.findCodeDetail( + RESERVE_CATEGORY_CODE) + .flatMap(code -> reserveItemRepository.findLatestByCategory(count, code.getCodeId())) + .map(reserveItem -> ReserveItemMainResponseDto.builder().entity(reserveItem).build()) + .collectMultimap(reserveItem -> reserveItem.getCategoryName()); + + } + + +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/validator/ReserveItemSaveValidator.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/validator/ReserveItemSaveValidator.java new file mode 100644 index 0000000..746fade --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/validator/ReserveItemSaveValidator.java @@ -0,0 +1,195 @@ +package org.egovframe.cloud.reserveitemservice.validator; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.reserveitemservice.validator.annotation.ReserveItemSaveValid; +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.lang.reflect.Field; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserveitemservice.validator.ReserveItemSaveValidator + * + * 예약 물품 저장 시 validation check를 하기 위한 custom validator + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj       최초 생성
+ * 
+ */ +@Slf4j +public class ReserveItemSaveValidator implements ConstraintValidator { + + private String message; + + @Override + public void initialize(ReserveItemSaveValid constraintAnnotation) { + message = constraintAnnotation.message(); + } + + /** + * 예약 물품 저장 시 비지니스 로직에 의한 validation check + * + * @param value + * @param context + * @return + */ + @SneakyThrows + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + boolean fieldValid = true; + + // 운영 시작일, 종료일 체크 + LocalDateTime operationStartDate = (LocalDateTime) getFieldValue(value, "operationStartDate"); + LocalDateTime operationEndDate = (LocalDateTime) getFieldValue(value, "operationEndDate"); + if (operationStartDate.isAfter(operationEndDate)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("시작일이 종료일 보다 큽니다.") + .addPropertyNode("operationStartDate") + .addConstraintViolation(); + fieldValid = false; + } + + String reserveMethodId = String.valueOf(getFieldValue(value, "reserveMethodId")); + //예약 방법이 '인터넷' 인경우 + if ("internet".equals(reserveMethodId)) { + // 예약 구분 필수 + if (isNull(value, "reserveMeansId")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("인터넷 예약인 경우 예약 구분은 필수입니다.") + .addPropertyNode("reserveMeansId") + .addConstraintViolation(); + fieldValid = false; + }else { + String reserveMeansId = String.valueOf(getFieldValue(value, "reserveMeansId")); + //예약 구분이 실시간 인 경우 + if ("realtime".equals(reserveMeansId)) { + // 예약 신청 기간 필수 + if (isNull(value, "requestStartDate")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("인터넷 예약인 경우 예약 신청 시작 기간은 필수입니다.") + .addPropertyNode("requestStartDate") + .addConstraintViolation(); + fieldValid = false; + } else if (isNull(value, "requestEndDate")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("인터넷 예약인 경우 예약 신청 종료 기간은 필수입니다.") + .addPropertyNode("requestEndDate") + .addConstraintViolation(); + fieldValid = false; + }else { + LocalDateTime requestStartDate = (LocalDateTime) getFieldValue(value, "requestStartDate"); + LocalDateTime requestEndDate = (LocalDateTime) getFieldValue(value, "requestEndDate"); + if (requestStartDate.isAfter(requestEndDate)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("시작일이 종료일 보다 큽니다.") + .addPropertyNode("requestStartDate") + .addConstraintViolation(); + fieldValid = false; + } + } + + //기간 지정 필수 + if (isNull(value, "isPeriod")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("인터넷 예약인 경우 기간 지정 여부는 필수입니다.") + .addPropertyNode("requestEndDate") + .addConstraintViolation(); + fieldValid = false; + }else { + Boolean isPeriod = Boolean.valueOf(String.valueOf(getFieldValue(value, "isPeriod"))); + // 기간 지정 가능인 경우 최대 얘약일 수 필수 + if (isPeriod && isNull(value, "periodMaxCount")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("기간 지정이 가능인 경우 최대 예약 일수는 필수입니다.") + .addPropertyNode("periodMaxCount") + .addConstraintViolation(); + fieldValid = false; + } + } + }else if ("external".equals(reserveMeansId)) { + //예약 구분이 외부 링크인 경우 외부 링크 url 필수 + if (isNull(value, "externalUrl")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("예약 구분이 외부링크인 경우 외부링크 url 값은 필수입니다.") + .addPropertyNode("externalUrl") + .addConstraintViolation(); + fieldValid = false; + } + } + } + } else if ("telephone".equals(reserveMethodId)) { + //예약 방법인 '전화'인 경우 contact 필수 + if (isNull(value, "contact")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("전화예약인 경우 문의처는 필수입니다.") + .addPropertyNode("contact") + .addConstraintViolation(); + fieldValid = false; + } + }else if ("visit".equals(reserveMethodId)) { + //예약 방법인 '방문'인 경우 주소 필수 + if (isNull(value, "address")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("방문예약인 경우 주소는 필수입니다.") + .addPropertyNode("address") + .addConstraintViolation(); + fieldValid = false; + } + } + + // 유료인 경우 이용 요금 필수 + Boolean isPaid = Boolean.valueOf(String.valueOf(getFieldValue(value, "isPaid"))); + if (isPaid && isNull(value, "usageCost")) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("유료인 경우 이용 요금은 필수입니다.") + .addPropertyNode("usageCost") + .addConstraintViolation(); + fieldValid = false; + } + + return fieldValid; + } + + /** + * 해당하는 field의 값 조회 + * + * @param object + * @param fieldName + * @return + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + private Object getFieldValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException { + Class clazz = object.getClass(); + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } + + /** + * 해당하는 Field가 null인지 체크 + * + * @param object + * @param fieldName + * @return + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + private boolean isNull(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException { + Class clazz = object.getClass(); + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object) == null || !StringUtils.hasLength(String.valueOf(field.get(object))); + } +} diff --git a/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/validator/annotation/ReserveItemSaveValid.java b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/validator/annotation/ReserveItemSaveValid.java new file mode 100644 index 0000000..d16a4ef --- /dev/null +++ b/backend/reserve-item-service/src/main/java/org/egovframe/cloud/reserveitemservice/validator/annotation/ReserveItemSaveValid.java @@ -0,0 +1,38 @@ +package org.egovframe.cloud.reserveitemservice.validator.annotation; + +import org.egovframe.cloud.reserveitemservice.validator.ReserveItemSaveValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * org.egovframe.cloud.reserveitemservice.validator.annotation.ReserveItemSaveValid + * + * 예약 물품 저장 시 validation check를 하기 위한 custom annotation + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/13 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/13    shinmj       최초 생성
+ * 
+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ReserveItemSaveValidator.class) +public @interface ReserveItemSaveValid { + + String message() default "저장할 수 없습니다."; + Class[] groups() default {}; + Class[] payload() default {}; + +} diff --git a/backend/reserve-item-service/src/main/resources/application.yml b/backend/reserve-item-service/src/main/resources/application.yml new file mode 100644 index 0000000..4f3062b --- /dev/null +++ b/backend/reserve-item-service/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + application: + name: reserve-item-service + +server: + port: 0 + +# config server actuator +management: + endpoints: + web: + exposure: + include: refresh, health, beans diff --git a/backend/reserve-item-service/src/main/resources/bootstrap.yml b/backend/reserve-item-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..c35bdae --- /dev/null +++ b/backend/reserve-item-service/src/main/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: reserve-item-service \ No newline at end of file diff --git a/backend/reserve-item-service/src/main/resources/schema.sql b/backend/reserve-item-service/src/main/resources/schema.sql new file mode 100644 index 0000000..f153c1d --- /dev/null +++ b/backend/reserve-item-service/src/main/resources/schema.sql @@ -0,0 +1,60 @@ +-- location Table Create SQL +CREATE TABLE IF NOT EXISTS location +( + location_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '지역 id', + location_name VARCHAR(200) NULL COMMENT '지역 이름', + sort_seq SMALLINT(3) NULL COMMENT '정렬 순서', + use_at TINYINT(1) NULL DEFAULT 1 COMMENT '사용 여부', + created_by VARCHAR(255) NULL COMMENT '생성자', + create_date DATETIME NULL COMMENT '생성일', + last_modified_by VARCHAR(255) NULL COMMENT '수정자', + modified_date DATETIME NULL COMMENT '수정일', + PRIMARY KEY (location_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE location COMMENT '예약 지역'; + + +-- reserve_item Table Create SQL +CREATE TABLE IF NOT EXISTS reserve_item +( + reserve_item_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '예약 물품 id', + reserve_item_name VARCHAR(200) NULL COMMENT '예약 물품 이름', + location_id BIGINT NULL COMMENT '지역 id', + category_id VARCHAR(20) NULL COMMENT '예약유형 - 공통코드 reserve-category', + total_qty BIGINT(18) NULL COMMENT '총 재고/수용인원 수', + inventory_qty BIGINT(18) NULL COMMENT '현재 남은 재고/수용인원 수', + operation_start_date DATETIME NULL COMMENT '운영 시작 일', + operation_end_date DATETIME NULL COMMENT '운영 종료 일', + reserve_method_id VARCHAR(20) NULL COMMENT '예약 방법 - 공통코드 reserve-method', + reserve_means_id VARCHAR(20) NULL COMMENT '예약 구분 (인터넷 예약 시) - 공통코드 reserve-means', + request_start_date DATETIME NULL COMMENT '예약 신청 시작 일시', + request_end_date DATETIME NULL COMMENT '예약 신청 종료 일시', + period_at TINYINT(1) NULL DEFAULT 0 COMMENT '기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가', + period_max_count SMALLINT(3) NULL COMMENT '최대 예약 가능 일 수', + external_url VARCHAR(500) NULL COMMENT '외부링크', + selection_means_id VARCHAR(20) NULL COMMENT '선별 방법 - 공통코드 reserve-selection-means', + paid_at TINYINT(1) NULL DEFAULT 0 COMMENT '유/무료 - false: 무료, true: 유료', + usage_cost DECIMAL(18, 0) NULL COMMENT '이용 요금', + use_at TINYINT(1) NULL DEFAULT 1 COMMENT '사용 여부', + purpose_content VARCHAR(4000) NULL COMMENT '용도', + item_addr VARCHAR(500) NULL COMMENT '주소', + target_id VARCHAR(20) NULL COMMENT '이용 대상 - 공통코드 reserve-target', + excluded_content VARCHAR(2000) NULL COMMENT '사용허가 제외대상', + homepage_url VARCHAR(500) NULL COMMENT '홈페이지 url', + contact_no VARCHAR(50) NULL COMMENT '문의처', + manager_dept_name VARCHAR(200) NULL COMMENT '담당자 소속', + manager_name VARCHAR(200) NULL COMMENT '담당자 이름', + manager_contact_no VARCHAR(50) NULL COMMENT '담당자 연락처', + create_date DATETIME NULL COMMENT '생성일', + created_by VARCHAR(255) NULL COMMENT '생성자', + modified_date DATETIME NULL COMMENT '수정일', + last_modified_by VARCHAR(255) NULL COMMENT '수정자', + PRIMARY KEY (reserve_item_id), + CONSTRAINT FK_reserve_item_location_id FOREIGN KEY (location_id) + REFERENCES location (location_id) ON DELETE RESTRICT ON UPDATE RESTRICT + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE reserve_item COMMENT '예약 물품'; + + diff --git a/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/ReserveItemServiceApplicationTests.java b/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/ReserveItemServiceApplicationTests.java new file mode 100644 index 0000000..0046b87 --- /dev/null +++ b/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/ReserveItemServiceApplicationTests.java @@ -0,0 +1,13 @@ +package org.egovframe.cloud.reserveitemservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ReserveItemServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/api/location/LocationApiControllerTest.java b/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/api/location/LocationApiControllerTest.java new file mode 100644 index 0000000..9ea7595 --- /dev/null +++ b/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/api/location/LocationApiControllerTest.java @@ -0,0 +1,144 @@ +package org.egovframe.cloud.reserveitemservice.api.location; + +import org.egovframe.cloud.reserveitemservice.api.location.dto.LocationSaveRequestDto; +import org.egovframe.cloud.reserveitemservice.api.location.dto.LocationUpdateRequestDto; +import org.egovframe.cloud.reserveitemservice.config.R2dbcConfig; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; +import org.egovframe.cloud.reserveitemservice.domain.location.LocationRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.BodyInserters; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles("test") +@Import({R2dbcConfig.class}) +class LocationApiControllerTest { + + @MockBean + private LocationRepository locationRepository; + + @Autowired + private WebTestClient webTestClient; + + private final static String API_URL = "/api/v1/locations"; + + private Location location = Location.builder() + .locationId(1L) + .locationName("location") + .isUse(true) + .sortSeq(1) + .build(); + + @BeforeEach + public void setup() { + BDDMockito.when(locationRepository.findById(ArgumentMatchers.anyLong())) + .thenReturn(Mono.just(location)); + //조회조건 있는 경우 + BDDMockito.when(locationRepository.findAllByLocationNameContainingOrderBySortSeq( + ArgumentMatchers.anyString(), ArgumentMatchers.any(Pageable.class))) + .thenReturn(Flux.just(location)); + BDDMockito.when(locationRepository.countAllByLocationNameContaining(ArgumentMatchers.anyString())) + .thenReturn(Mono.just(1L)); + //조회조건 없는 경우 + BDDMockito.when(locationRepository.findAllByOrderBySortSeq(ArgumentMatchers.any(Pageable.class))) + .thenReturn(Flux.just(location)); + BDDMockito.when(locationRepository.count()).thenReturn(Mono.just(1L)); + + BDDMockito.when(locationRepository.save(ArgumentMatchers.any(Location.class))) + .thenReturn(Mono.just(location)); + + } + + @Test + public void 한건조회_성공() throws Exception { + webTestClient.get() + .uri(API_URL+"/{locationId}", location.getLocationId()) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.locationName").isEqualTo(location.getLocationName()); + } + + @Test + public void 조회조건있는경우_페이지목록조회_성공() throws Exception { + webTestClient.get() + .uri(API_URL+"?keywordType=locationName&keyword=location&page=0&size=3") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.totalElements").isEqualTo(1) + .jsonPath("$.content[0].locationName").isEqualTo(location.getLocationName()); + } + + @Test + public void 조회조건없는경우_페이지목록조회_성공() throws Exception { + webTestClient.get() + .uri(API_URL+"?page=0&size=3") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.totalElements").isEqualTo(1) + .jsonPath("$.content[0].locationName").isEqualTo(location.getLocationName()); + } + + @Test + public void 한건저장_성공() throws Exception { + webTestClient.post() + .uri(API_URL) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(LocationSaveRequestDto.builder() + .locationName(location.getLocationName()) + .isUse(location.getIsUse()) + .sortSeq(location.getSortSeq()) + .build())) + .exchange() + .expectStatus().isCreated() + .expectBody().jsonPath("$.locationName").isEqualTo(location.getLocationName()); + + } + + @Test + public void 한건수정_성공() throws Exception { + webTestClient.put() + .uri(API_URL+"/{locationId}", location.getLocationId()) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(LocationUpdateRequestDto.builder() + .locationName("updateLocation") + .isUse(location.getIsUse()) + .sortSeq(location.getSortSeq()) + .build())) + .exchange() + .expectStatus().isNoContent(); + } + + @Test + public void 한건삭제_참조데이터존재_삭제실패() throws Exception { + BDDMockito.when(locationRepository.delete(ArgumentMatchers.any(Location.class))) + .thenReturn(Mono.error(new DataIntegrityViolationException("integrity test"))); + webTestClient.delete() + .uri(API_URL+"/{locationId}", 1L) + .exchange() + .expectStatus().isBadRequest(); + + } + + +} \ No newline at end of file diff --git a/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/ReserveItemApiControllerTest.java b/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/ReserveItemApiControllerTest.java new file mode 100644 index 0000000..f62e265 --- /dev/null +++ b/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/api/reserveItem/ReserveItemApiControllerTest.java @@ -0,0 +1,164 @@ +package org.egovframe.cloud.reserveitemservice.api.reserveItem; + +import java.time.LocalDateTime; +import java.util.Arrays; + +import org.egovframe.cloud.reserveitemservice.api.reserveItem.dto.ReserveItemRequestDto; +import org.egovframe.cloud.reserveitemservice.config.R2dbcConfig; +import org.egovframe.cloud.reserveitemservice.domain.code.Code; +import org.egovframe.cloud.reserveitemservice.domain.location.Location; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItem; +import org.egovframe.cloud.reserveitemservice.domain.reserveItem.ReserveItemRepository; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.reactive.server.WebTestClient; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +@Import({R2dbcConfig.class}) +class ReserveItemApiControllerTest { + + + @MockBean + ReserveItemRepository reserveItemRepository; + + @Autowired + WebTestClient webTestClient; + + private final static String API_URL = "/api/v1/reserve-items"; + + + @Test + public void 사용자별_검색_목록_조회_성공() throws Exception { + LocalDateTime startDate = LocalDateTime.of(2021, 1, 28, 1,1); + LocalDateTime endDate = LocalDateTime.of(2021, 12, 6, 1,1); + + Location location = Location.builder() + .locationId(1L) + .locationName("location") + .sortSeq(1) + .isUse(true) + .build(); + ReserveItem reserveItem = ReserveItem.builder() + .reserveItemId(1L) + .location(location) + .locationId(location.getLocationId()) + .reserveItemName("test") + .categoryId("education") + .categoryName("교육") + .totalQty(100) + .inventoryQty(80) + .operationEndDate(endDate) + .operationStartDate(startDate) + .isPeriod(false) + .isUse(true) + .build(); + + BDDMockito.when(reserveItemRepository.searchForUser(ArgumentMatchers.anyString(), + ArgumentMatchers.any(ReserveItemRequestDto.class), ArgumentMatchers.any(Pageable.class))) + .thenReturn(Flux.just(reserveItem)); + + BDDMockito.when(reserveItemRepository.searchCountForUser(ArgumentMatchers.anyString(), + ArgumentMatchers.any(ReserveItemRequestDto.class), ArgumentMatchers.any(Pageable.class))) + .thenReturn(Mono.just(1L)); + + webTestClient.get() + .uri("/api/v1/{categoryId}/reserve-items?keywordType=locationName&keyword=location&page=0&size=3", "education") + .exchange() + .expectStatus().isOk(); + + } + @Test + public void main_예약물품조회_성공() throws Exception { + BDDMockito.when(reserveItemRepository.findCodeDetail(ArgumentMatchers.anyString())) + .thenReturn(Flux.fromIterable(Arrays.asList(Code.builder().codeId("education").codeName("교육").build(), + Code.builder().codeId("equipment").codeName("장비").build(), + Code.builder().codeId("space").codeName("장소").build()))); + + LocalDateTime startDate = LocalDateTime.of(2021, 1, 28, 1,1); + LocalDateTime endDate = LocalDateTime.of(2021, 12, 6, 1,1); + + Location location = Location.builder() + .locationId(1L) + .locationName("location") + .sortSeq(1) + .isUse(true) + .build(); + ReserveItem reserveItem1 = ReserveItem.builder() + .reserveItemId(1L) + .location(location) + .locationId(location.getLocationId()) + .reserveItemName("test") + .categoryId("education") + .categoryName("교육") + .totalQty(100) + .inventoryQty(80) + .operationEndDate(endDate) + .operationStartDate(startDate) + .reserveMethodId("visit") + .isPeriod(false) + .isUse(true) + .build(); + ReserveItem reserveItem2 = ReserveItem.builder() + .reserveItemId(1L) + .location(location) + .locationId(location.getLocationId()) + .reserveItemName("test") + .categoryId("education") + .categoryName("장비") + .totalQty(100) + .inventoryQty(80) + .operationEndDate(endDate) + .operationStartDate(startDate) + .reserveMethodId("visit") + .isPeriod(false) + .isUse(true) + .build(); + + ReserveItem reserveItem3 = ReserveItem.builder() + .reserveItemId(1L) + .location(location) + .locationId(location.getLocationId()) + .reserveItemName("test") + .categoryId("education") + .categoryName("공간") + .totalQty(100) + .inventoryQty(80) + .operationEndDate(endDate) + .operationStartDate(startDate) + .reserveMethodId("visit") + .isPeriod(false) + .isUse(true) + .build(); + reserveItem1.setCreateDate(LocalDateTime.now()); + reserveItem2.setCreateDate(LocalDateTime.now()); + reserveItem3.setCreateDate(LocalDateTime.now()); + + BDDMockito.when(reserveItemRepository.findLatestByCategory(ArgumentMatchers.anyInt(), ArgumentMatchers.anyString())) + .thenReturn(Flux.fromIterable(Arrays.asList(reserveItem1, reserveItem2, reserveItem3))); + + + webTestClient.get() + .uri("/api/v1/reserve-items/latest/3") + .exchange() + .expectStatus().isOk(); + + + } + +} \ No newline at end of file diff --git a/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/config/R2dbcConfig.java b/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/config/R2dbcConfig.java new file mode 100644 index 0000000..5a20540 --- /dev/null +++ b/backend/reserve-item-service/src/test/java/org/egovframe/cloud/reserveitemservice/config/R2dbcConfig.java @@ -0,0 +1,40 @@ +package org.egovframe.cloud.reserveitemservice.config; + +import io.r2dbc.h2.H2ConnectionConfiguration; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.h2.H2ConnectionOption; +import io.r2dbc.spi.ConnectionFactory; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.r2dbc.connection.init.CompositeDatabasePopulator; +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; + +@Profile("test") +@TestConfiguration +@EnableR2dbcRepositories +public class R2dbcConfig{ + @Bean + public H2ConnectionFactory connectionFactory() { + return new H2ConnectionFactory(H2ConnectionConfiguration.builder() + .tcp("localhost", "~/querydsl") + .property(H2ConnectionOption.DB_CLOSE_DELAY, "-1") + .username("sa") + .build()); + } + + @Bean + public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); + initializer.setConnectionFactory(connectionFactory); + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); + populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("schema-h2.sql"))); + initializer.setDatabasePopulator(populator); + + return initializer; + } +} diff --git a/backend/reserve-item-service/src/test/resources/application-test.yml b/backend/reserve-item-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..56696d7 --- /dev/null +++ b/backend/reserve-item-service/src/test/resources/application-test.yml @@ -0,0 +1,44 @@ +spring: + application: + name: reserve-item-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: + hibernate: + generate-ddl: true + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/backend/reserve-item-service/src/test/resources/application.yml b/backend/reserve-item-service/src/test/resources/application.yml new file mode 100644 index 0000000..8dabc6b --- /dev/null +++ b/backend/reserve-item-service/src/test/resources/application.yml @@ -0,0 +1,19 @@ +spring: + application: + name: reserve-item-service + +logging: + level: + org: + springramework: + data: + r2dbc: DEBUG + + +file: + location: ${user.home}/msa-attach-volume + + +# jwt token +token: + secret: egovframe_user_token \ No newline at end of file diff --git a/backend/reserve-item-service/src/test/resources/bootstrap.yml b/backend/reserve-item-service/src/test/resources/bootstrap.yml new file mode 100644 index 0000000..c35bdae --- /dev/null +++ b/backend/reserve-item-service/src/test/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: reserve-item-service \ No newline at end of file diff --git a/backend/reserve-item-service/src/test/resources/schema-h2.sql b/backend/reserve-item-service/src/test/resources/schema-h2.sql new file mode 100644 index 0000000..ff09d07 --- /dev/null +++ b/backend/reserve-item-service/src/test/resources/schema-h2.sql @@ -0,0 +1,49 @@ +CREATE TABLE IF NOT EXISTS location( + location_id BIGINT AUTO_INCREMENT, + location_name VARCHAR(200), + use_at tinyint(1) default 1 null, + sort_seq smallint(3) null, + create_date DATE null, + modified_date DATE null, + created_by VARCHAR(255) null, + last_modified_by VARCHAR(255) null, + CONSTRAINT PERSON_PK PRIMARY KEY (location_id) +); + + +-- reserve_item Table Create SQL +CREATE TABLE IF NOT EXISTS reserve_item +( + reserve_item_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '예약 물품 id', + reserve_item_name VARCHAR(200) NULL COMMENT '예약 물품 이름', + location_id BIGINT NULL COMMENT '지역 id', + category_id VARCHAR(20) NULL COMMENT '예약유형 - 공통코드 reserve-category', + capacity_count MEDIUMINT(5) NULL COMMENT '재고/수용인원 수', + operation_start_date DATETIME NULL COMMENT '운영 시작 일', + operation_end_date DATETIME NULL COMMENT '운영 종료 일', + reserve_method_id VARCHAR(20) NULL COMMENT '예약 방법 - 공통코드 reserve-method', + reserve_means_id VARCHAR(20) NULL COMMENT '예약 구분 (인터넷 예약 시) - 공통코드 reserve-means', + request_start_date DATETIME NULL COMMENT '예약 신청 시작 일시', + request_end_date DATETIME NULL COMMENT '예약 신청 종료 일시', + period_at TINYINT(1) NULL DEFAULT 0 COMMENT '기간 지정 가능 여부 - true: 지정 가능, false: 지정 불가', + period_max_count SMALLINT(3) NULL COMMENT '최대 예약 가능 일 수', + external_url VARCHAR(500) NULL COMMENT '외부링크', + selection_means_id VARCHAR(20) NULL COMMENT '선별 방법 - 공통코드 reserve-selection-means', + free_at TINYINT(1) NULL DEFAULT 1 COMMENT '유/무료 - true: 무료, false: 유료', + usage_cost DECIMAL(18, 0) NULL COMMENT '이용 요금', + use_at TINYINT(1) NULL DEFAULT 1 COMMENT '사용 여부', + purpose_content VARCHAR(4000) NULL COMMENT '용도', + item_addr VARCHAR(500) NULL COMMENT '주소', + target_id VARCHAR(20) NULL COMMENT '이용 대상 - 공통코드 reserve-target', + excluded_content VARCHAR(2000) NULL COMMENT '사용허가 제외대상', + homepage_url VARCHAR(500) NULL COMMENT '홈페이지 url', + contact_no VARCHAR(50) NULL COMMENT '문의처', + manager_dept_name VARCHAR(200) NULL COMMENT '담당자 소속', + manager_name VARCHAR(200) NULL COMMENT '담당자 이름', + manager_contact_no VARCHAR(50) NULL COMMENT '담당자 연락처', + create_date DATETIME NULL COMMENT '생성일', + created_by VARCHAR(255) NULL COMMENT '생성자', + modified_date DATETIME NULL COMMENT '수정일', + last_modified_by VARCHAR(255) NULL COMMENT '수정자', + PRIMARY KEY (reserve_item_id) + ); diff --git a/backend/reserve-request-service/Dockerfile b/backend/reserve-request-service/Dockerfile new file mode 100644 index 0000000..bd86ccf --- /dev/null +++ b/backend/reserve-request-service/Dockerfile @@ -0,0 +1,15 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine + +# config server uri: dockder run --e 로 변경 가능 +ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar app.jar +# application port +#EXPOSE 8000 +# 실행 (application-cf.yml 프로필이 기본값) +CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "app.jar"] diff --git a/backend/reserve-request-service/build.gradle b/backend/reserve-request-service/build.gradle new file mode 100644 index 0000000..f109d83 --- /dev/null +++ b/backend/reserve-request-service/build.gradle @@ -0,0 +1,87 @@ +plugins { + id 'org.springframework.boot' version '2.4.5' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'org.egovframe.cloud' +version = '0.1' +sourceCompatibility = '1.8' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() + maven { url "https://maven.egovframe.go.kr/maven/" } // egovframe maven 원격 저장소 +} + +ext { + set('springCloudVersion', "2020.0.3") +} + + +dependencies { +// implementation files('../../module-common/build/libs/module-common-0.1.jar') // @ComponentScan(basePackages={"org.egovframe.cloud"}) 추가해야 적용된다 + implementation 'org.egovframe.cloud:module-common:0.1' + 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-r2dbc' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-config' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j' + implementation 'com.playtika.reactivefeign:feign-reactor-spring-cloud-starter:3.1.0' + + //messaging + implementation 'org.springframework.cloud:spring-cloud-stream' + implementation 'org.springframework.cloud:spring-cloud-stream-binder-rabbit' + implementation 'org.springframework.boot:spring-boot-starter-amqp' + implementation 'net.java.dev.jna:jna:5.9.0' // byte-buddy (No compatible attachment provider is available.) + + + implementation 'io.jsonwebtoken:jjwt:0.9.1' + + implementation 'dev.miku:r2dbc-mysql:0.8.2.RELEASE' + implementation 'mysql:mysql-connector-java' + + // swagger api docs + implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.5.10' + + // bolcking 호출 감지 + implementation 'io.projectreactor:reactor-tools:3.4.9' + implementation 'io.projectreactor.tools:blockhound:1.0.6.RELEASE' + + //lombok + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + testImplementation 'com.h2database:h2' + testImplementation 'io.r2dbc:r2dbc-h2' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.springframework.amqp:spring-rabbit-test' +} + +test { + useJUnitPlatform() +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} diff --git a/backend/reserve-request-service/gradlew b/backend/reserve-request-service/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/reserve-request-service/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/reserve-request-service/gradlew.bat b/backend/reserve-request-service/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/backend/reserve-request-service/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/reserve-request-service/manifest.yml b/backend/reserve-request-service/manifest.yml new file mode 100644 index 0000000..727016c --- /dev/null +++ b/backend/reserve-request-service/manifest.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: egov-reserve-request-service # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-reserve-request-service # host 명으로 유일해야 함 + path: build/libs/reserve-request-service-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + services: + - egov-discovery-provided-service # discovery service binding + env: + spring_profiles_active: cf + spring_cloud_config_uri: https://egov-config.paas-ta.org + app_name: egov-reserve-request-service # logstash custom app name + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/backend/reserve-request-service/settings.gradle b/backend/reserve-request-service/settings.gradle new file mode 100644 index 0000000..9787096 --- /dev/null +++ b/backend/reserve-request-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'reserve-request-service' diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/ReserveRequestServiceApplication.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/ReserveRequestServiceApplication.java new file mode 100644 index 0000000..59e9405 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/ReserveRequestServiceApplication.java @@ -0,0 +1,33 @@ +package org.egovframe.cloud.reserverequestservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.ComponentScan; +import reactor.blockhound.BlockHound; + +import java.security.Security; + +@ComponentScan({"org.egovframe.cloud.common", "org.egovframe.cloud.reactive", "org.egovframe.cloud.reserverequestservice"}) // org.egovframe.cloud.common package 포함하기 위해 +@EnableDiscoveryClient +@SpringBootApplication +public class ReserveRequestServiceApplication { + + public static void main(String[] args) { + // TLSv1/v1.1 No longer works after upgrade, "No appropriate protocol" error + String property = Security.getProperty("jdk.tls.disabledAlgorithms").replace(", TLSv1", "").replace(", TLSv1.1", ""); + Security.setProperty("jdk.tls.disabledAlgorithms", property); + + //blocking 코드 감지 + BlockHound.builder() + //mysql r2dbc 에서 호출되는 FileInputStream.readBytes() 가 블로킹코드인데 이를 허용해주도록 한다. + //해당 코드가 어디서 호출되는지 알지 못하는 상태에서 FileInputStream.readBytes() 자체를 허용해주는 것은 좋지 않다. + // 누군가 무분별하게 사용하게 되면 검출해 낼 수ㅂ 없어 시스템의 위험요소로 남게 된다. + // r2dbc를 사용하기 위해 해당 호출부분만 허용하고 나머지는 여전히 검출대상으로 남기도록 한다. + .allowBlockingCallsInside("dev.miku.r2dbc.mysql.client.ReactorNettyClient", "init") + .install(); + + SpringApplication.run(ReserveRequestServiceApplication.class, args); + } + +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/ReserveApiController.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/ReserveApiController.java new file mode 100644 index 0000000..8af1330 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/ReserveApiController.java @@ -0,0 +1,124 @@ +package org.egovframe.cloud.reserverequestservice.api; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.reserverequestservice.api.dto.ReserveResponseDto; +import org.egovframe.cloud.reserverequestservice.api.dto.ReserveSaveRequestDto; +import org.egovframe.cloud.reserverequestservice.config.MessageListenerContainerFactory; +import org.egovframe.cloud.reserverequestservice.domain.Category; +import org.egovframe.cloud.reserverequestservice.service.ReserveService; +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * org.egovframe.cloud.reserverequestservice.api.ReserveApiController + * + * 예약 신청 rest controller class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class ReserveApiController { + + private final ReserveService reserveService; + private final MessageListenerContainerFactory messageListenerContainerFactory; + private final AmqpAdmin amqpAdmin; + + /** + * 예약 신청 - 심사 + * + * @param saveRequestDtoMono + * @return + */ + @PostMapping("/api/v1/requests/audit") + @ResponseStatus(HttpStatus.CREATED) + public Mono create(@RequestBody Mono saveRequestDtoMono) { + return saveRequestDtoMono.flatMap(reserveService::create); + } + + /** + * 예약 신청 - 실시간 + * + * @param saveRequestDtoMono + * @return + */ + @PostMapping("/api/v1/requests") + @ResponseStatus(HttpStatus.CREATED) + public Mono save(@RequestBody Mono saveRequestDtoMono) { + return saveRequestDtoMono.flatMap(saveRequestDto -> { + if (Category.EDUCATION.isEquals(saveRequestDto.getCategoryId())) { + return reserveService.saveForEvent(saveRequestDto); + } + return reserveService.save(saveRequestDto); + }); + } + + /** + * 실시간 예약 신청 후 결과 여부 subscribe + * + * @param reserveId + * @return + */ + @GetMapping(value = "/api/v1/requests/direct/{reserveId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux receiveReservationResult(@PathVariable String reserveId) { + MessageListenerContainer mlc = messageListenerContainerFactory.createMessageListenerContainer(reserveId); + Flux f = Flux.create(emitter -> { + mlc.setupMessageListener((MessageListener) m -> { + String qname = m.getMessageProperties().getConsumerQueue(); + log.info("message received, queue={}", qname); + + if (emitter.isCancelled()) { + log.info("cancelled, queue={}", qname); + mlc.stop(); + return; + } + + String payload = new String(m.getBody()); + log.info("message data = {}", payload); + emitter.next(payload); + + log.info("message sent to client, queue={}", qname); + }); + + emitter.onRequest(v -> { + log.info("starting container, queue={}", reserveId); + mlc.start(); + }); + + emitter.onDispose(() -> { + log.info("on dispose, queue={}", reserveId); + mlc.stop(); + amqpAdmin.deleteQueue(reserveId); + }); + + log.info("container started, queue={}", reserveId); + }); + + return Flux.interval(Duration.ofSeconds(5)) + .map(v -> { + log.info("sending keepalive message..."); + return "no news is good news"; + }).mergeWith(f); + } + +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/dto/ReserveResponseDto.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/dto/ReserveResponseDto.java new file mode 100644 index 0000000..91cf478 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/dto/ReserveResponseDto.java @@ -0,0 +1,64 @@ +package org.egovframe.cloud.reserverequestservice.api.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reserverequestservice.domain.Reserve; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserverequestservice.api.dto.ReserveResponseDto + *

+ * 예약 신청 응답 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveResponseDto { + + private String reserveId; + + private Long reserveItemId; + private Long locationId; + private String categoryId; + + private Integer reserveQty; + private LocalDateTime reserveStartDate; + private LocalDateTime reserveEndDate; + private String reservePurposeContent; + private String attachmentCode; + + private String userId; + private String userContactNo; + private String userEmail; + + @Builder + public ReserveResponseDto(Reserve entity) { + this.reserveId = entity.getReserveId(); + this.reserveItemId = entity.getReserveItemId(); + this.locationId = entity.getLocationId(); + this.categoryId = entity.getCategoryId(); + this.reserveQty = entity.getReserveQty(); + this.reserveStartDate = entity.getReserveStartDate(); + this.reserveEndDate = entity.getReserveEndDate(); + this.reservePurposeContent = entity.getReservePurposeContent(); + this.attachmentCode = entity.getAttachmentCode(); + this.userId = entity.getUserId(); + this.userContactNo = entity.getUserContactNo(); + this.userEmail = entity.getUserEmail(); + } +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/dto/ReserveSaveRequestDto.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/dto/ReserveSaveRequestDto.java new file mode 100644 index 0000000..7071f81 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/api/dto/ReserveSaveRequestDto.java @@ -0,0 +1,111 @@ +package org.egovframe.cloud.reserverequestservice.api.dto; + +import lombok.*; +import org.egovframe.cloud.reserverequestservice.domain.Reserve; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserverequestservice.api.dto.ReserveSaveRequestDto + *

+ * 예약 신청 저장 요청 dto class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +public class ReserveSaveRequestDto { + + @Setter + private String reserveId; + @NotNull + private Long reserveItemId; + private Long locationId; + private String categoryId; + private Integer totalQty; + private String reserveMethodId; + private String reserveMeansId; + private LocalDateTime operationStartDate; + private LocalDateTime operationEndDate; + private LocalDateTime requestStartDate; + private LocalDateTime requestEndDate; + private Boolean isPeriod; + private Integer periodMaxCount; + + private Integer reserveQty; //예약 신청 인원/수량 + @NotNull + private String reservePurposeContent; //예약 목적 + private String attachmentCode; //첨부파일 코드 + private LocalDateTime reserveStartDate; //예약 신청 시작일 + private LocalDateTime reserveEndDate; //예약 신청 종료일 + @Setter + private String reserveStatusId; //예약상태 - 공통코드(reserve-status) + @NotNull + private String userId; //예약자 + @NotNull + private String userContactNo; //예약자 연락처 + @NotNull + private String userEmail; //예약자 이메일 + + @Builder + public ReserveSaveRequestDto(String reserveId, Long reserveItemId, Long locationId, String categoryId, + Integer totalQty, String reserveMethodId, String reserveMeansId, LocalDateTime operationStartDate, + LocalDateTime operationEndDate, LocalDateTime requestStartDate, LocalDateTime requestEndDate, + Boolean isPeriod, Integer periodMaxCount, Integer reserveQty, String reservePurposeContent, + String attachmentCode, LocalDateTime reserveStartDate, LocalDateTime reserveEndDate, + String reserveStatusId, String userId, String userContactNo, String userEmail) { + this.reserveId = reserveId; + this.reserveItemId = reserveItemId; + this.locationId = locationId; + this.categoryId = categoryId; + this.totalQty = totalQty; + this.reserveMethodId = reserveMethodId; + this.reserveMeansId = reserveMeansId; + this.operationStartDate = operationStartDate; + this.operationEndDate = operationEndDate; + this.requestStartDate = requestStartDate; + this.requestEndDate = requestEndDate; + this.isPeriod = isPeriod; + this.periodMaxCount = periodMaxCount; + this.reserveQty = reserveQty; + this.reservePurposeContent = reservePurposeContent; + this.attachmentCode = attachmentCode; + this.reserveStartDate = reserveStartDate; + this.reserveEndDate = reserveEndDate; + this.reserveStatusId = reserveStatusId; + this.userId = userId; + this.userContactNo = userContactNo; + this.userEmail = userEmail; + } + + public Reserve toEntity() { + return Reserve.builder() + .reserveId(this.reserveId) + .reserveItemId(this.reserveItemId) + .locationId(this.locationId) + .categoryId(this.categoryId) + .reserveQty(this.reserveQty) + .reservePurposeContent(this.reservePurposeContent) + .attachmentCode(this.attachmentCode) + .reserveStartDate(this.reserveStartDate) + .reserveEndDate(this.reserveEndDate) + .reserveStatusId(this.reserveStatusId) + .userId(this.userId) + .userContactNo(this.userContactNo) + .userEmail(this.userEmail) + .build(); + } + +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/MessageListenerContainerFactory.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/MessageListenerContainerFactory.java new file mode 100644 index 0000000..03fc2c4 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/MessageListenerContainerFactory.java @@ -0,0 +1,44 @@ +package org.egovframe.cloud.reserverequestservice.config; + +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * org.egovframe.cloud.reserverequestservice.config.MessageListenerContainerFactory + * + * 동적으로 이벤트 큐 생성하기 위한 component + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/30 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/30    shinmj       최초 생성
+ * 
+ */ +@NoArgsConstructor +@Component +public class MessageListenerContainerFactory { + + @Autowired + private ConnectionFactory connectionFactory; + + public MessageListenerContainer createMessageListenerContainer(String queueName) { + SimpleMessageListenerContainer mlc = new SimpleMessageListenerContainer(); + mlc.setConnectionFactory(connectionFactory); + mlc.addQueueNames(queueName); + mlc.setAcknowledgeMode(AcknowledgeMode.AUTO); + + return mlc; + } +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/RequestMessage.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/RequestMessage.java new file mode 100644 index 0000000..64e093d --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/RequestMessage.java @@ -0,0 +1,37 @@ +package org.egovframe.cloud.reserverequestservice.config; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * org.egovframe.cloud.reserverequestservice.config.RequestMessage + * + * 예약 신청 후 이벤트 스트림 message VO class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    shinmj       최초 생성
+ * 
+ */ +@NoArgsConstructor +@Getter +@ToString +public class RequestMessage { + private String reserveId; + private Boolean isItemUpdated; + + @Builder + public RequestMessage(String reserveId, Boolean isItemUpdated) { + this.reserveId = reserveId; + this.isItemUpdated = isItemUpdated; + } +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/ReserveEventConfig.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/ReserveEventConfig.java new file mode 100644 index 0000000..e572d0b --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/config/ReserveEventConfig.java @@ -0,0 +1,75 @@ +package org.egovframe.cloud.reserverequestservice.config; + +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.config.GlobalConstant; +import org.egovframe.cloud.reserverequestservice.domain.ReserveStatus; +import org.egovframe.cloud.reserverequestservice.service.ReserveService; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; + +import java.util.function.Consumer; + +/** + * org.egovframe.cloud.reserverequestservice.config.ReserveEventConfig + * + * event stream 설정 class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    shinmj       최초 생성
+ * 
+ */ +@Slf4j +@Configuration +public class ReserveEventConfig { + @Autowired + private ReserveService reserveService; + @Autowired + private ConnectionFactory connectionFactory; + + /** + * 예약 신청(실시간) 후 재고 변경에 대한 성공 여부 consumer function + * + * @return + */ + @Bean + public Consumer> inventoryUpdated() { + return message -> { + log.info("receive message: {}, headers: {}", message.getPayload(), message.getHeaders()); + if (message.getPayload().getIsItemUpdated()) { + reserveService.updateStatus(message.getPayload().getReserveId(), ReserveStatus.APPROVE).subscribe(); + }else { + reserveService.delete(message.getPayload().getReserveId()).subscribe(); + } + + RabbitTemplate rabbitTemplate = rabbitTemplate(connectionFactory); + rabbitTemplate.convertAndSend(GlobalConstant.SUCCESS_OR_NOT_EX_NAME, + message.getPayload().getReserveId(), message.getPayload().getIsItemUpdated()); + }; + } + + @Bean + public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) { + final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.setMessageConverter(messageConverter()); + return rabbitTemplate; + } + + @Bean + public Jackson2JsonMessageConverter messageConverter() { + return new Jackson2JsonMessageConverter(); + } + +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/Category.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/Category.java new file mode 100644 index 0000000..ab4ed6c --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/Category.java @@ -0,0 +1,19 @@ +package org.egovframe.cloud.reserverequestservice.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Category { + EDUCATION("education", "교육"), + EQUIPMENT("equipment", "장비"), + SPACE("space", "공간"); + + private final String key; + private final String title; + + public boolean isEquals(String compare) { + return this.getKey().equals(compare); + } +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/Reserve.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/Reserve.java new file mode 100644 index 0000000..820d249 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/Reserve.java @@ -0,0 +1,119 @@ +package org.egovframe.cloud.reserverequestservice.domain; + + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.egovframe.cloud.reactive.domain.BaseEntity; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.reserverequestservice.domain.Reserve + * + * 예약 도메인 클래스 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@ToString +@Table("reserve") +public class Reserve extends BaseEntity { + + @Id + @Column + private String reserveId; //예약 id + + @Column + private Long reserveItemId; //예약 물품 id + + @Column + private Long locationId; //예약 물품 - 지역 id + + @Column + private String categoryId; //예약 물품 - 유형 id + + @Column + private Integer reserveQty; //예약 신청 인원/수량 + + @Column + private String reservePurposeContent; //예약 목적 + + @Column + private String attachmentCode; //첨부파일 코드 + + @Column + private LocalDateTime reserveStartDate; //예약 신청 시작일 + @Column + private LocalDateTime reserveEndDate; //예약 신청 종료일 + + @Column + private String reserveStatusId; //예약상태 - 공통코드(reserve-status) + + @Column + private String userId; //예약자 + + @Column + private String userContactNo; //예약자 연락처 + + @Column("user_email_addr") + private String userEmail; //예약자 이메일 + + @Builder + public Reserve(String reserveId, Long reserveItemId, Long locationId, String categoryId, Integer reserveQty, String reservePurposeContent, String attachmentCode, LocalDateTime reserveStartDate, LocalDateTime reserveEndDate, String reserveStatusId, String userId, String userContactNo, String userEmail) { + this.reserveId = reserveId; + this.reserveItemId = reserveItemId; + this.locationId = locationId; + this.categoryId = categoryId; + this.reserveQty = reserveQty; + this.reservePurposeContent = reservePurposeContent; + this.attachmentCode = attachmentCode; + this.reserveStartDate = reserveStartDate; + this.reserveEndDate = reserveEndDate; + this.reserveStatusId = reserveStatusId; + this.userId = userId; + this.userContactNo = userContactNo; + this.userEmail = userEmail; + } + + /** + * 예약 상태 업데이트 + * + * @param reserveStatusId + * @return + */ + public Reserve updateStatus(String reserveStatusId) { + this.reserveStatusId = reserveStatusId; + return this; + } + + /** + * create 정보 세팅 + * insert 시 필요 + * + * @param createdDate + * @param createdBy + * @return + */ + public Reserve setCreatedInfo(LocalDateTime createdDate, String createdBy) { + this.createdBy = createdBy; + this.createDate = createdDate; + return this; + } + +} \ No newline at end of file diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepository.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepository.java new file mode 100644 index 0000000..c4a2e98 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepository.java @@ -0,0 +1,25 @@ +package org.egovframe.cloud.reserverequestservice.domain; + +import org.egovframe.cloud.reserverequestservice.domain.Reserve; +import org.egovframe.cloud.reserverequestservice.domain.ReserveRepositoryCustom; +import org.springframework.data.r2dbc.repository.R2dbcRepository; + +/** + * org.egovframe.cloud.reserverequestservice.domain.Reserve + * + * 예약 도메인 repository interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    shinmj       최초 생성
+ * 
+ */ +public interface ReserveRepository extends R2dbcRepository, ReserveRepositoryCustom { +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepositoryCustom.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepositoryCustom.java new file mode 100644 index 0000000..584d374 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepositoryCustom.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.reserverequestservice.domain; + +import java.time.LocalDateTime; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +/** + * org.egovframe.cloud.reserverequestservice.domain.ReserveRepositoryCustom + * + * 예약 도메인 repository custom interface + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/27 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/27    shinmj       최초 생성
+ * 
+ */ +public interface ReserveRepositoryCustom { + Mono insert(Reserve reserve); + Flux findAllByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate); + Mono findAllByReserveDateWithoutSelfCount(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate); +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepositoryImpl.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepositoryImpl.java new file mode 100644 index 0000000..1958b1f --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveRepositoryImpl.java @@ -0,0 +1,86 @@ +package org.egovframe.cloud.reserverequestservice.domain; + +import static org.springframework.data.relational.core.query.Criteria.*; + +import java.time.LocalDateTime; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.relational.core.query.Query; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +/** + * org.egovframe.cloud.reserverequestservice.domain.ReserveRepositoryImpl + * + * 예약 도메인 repository custom interface 구현체 + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/27 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/27    shinmj       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class ReserveRepositoryImpl implements ReserveRepositoryCustom { + + private final R2dbcEntityTemplate entityTemplate; + + /** + * 예약 insert + * pk(reserveId)를 서비스에서 생성하여 insert 하기 위함. + * + * @param reserve + * @return + */ + @Override + public Mono insert(Reserve reserve) { + return entityTemplate.insert(reserve); + } + + /** + * 조회 기간에 예약된 건 조회 + * 현 예약건은 제외 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + @Override + public Flux findAllByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + return entityTemplate.select(Reserve.class) + .matching(Query.query(where("reserve_item_id").is(reserveItemId) + .and ("reserve_start_date").lessThanOrEquals(endDate) + .and("reserve_end_date").greaterThanOrEquals(startDate) + .and("reserve_id").not(reserveId) + )) + .all(); + } + + /** + * 조회 기간에 예약된 건수 조회 + * 현 예약건은 제외 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + @Override + public Mono findAllByReserveDateWithoutSelfCount(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + return entityTemplate.select(Reserve.class) + .matching(Query.query(where("reserve_item_id").is(reserveItemId) + .and ("reserve_start_date").lessThanOrEquals(endDate) + .and("reserve_end_date").greaterThanOrEquals(startDate) + .and("reserve_id").not(reserveId) + )) + .count(); + } +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveStatus.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveStatus.java new file mode 100644 index 0000000..741d4f4 --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/domain/ReserveStatus.java @@ -0,0 +1,16 @@ +package org.egovframe.cloud.reserverequestservice.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ReserveStatus { + REQUEST("request", "예약 신청"), + APPROVE("approve", "예약 승인"), + CANCEL("cancel", "예약 취소"), + DONE("done", "완료"); + + private final String key; + private final String title; +} diff --git a/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/service/ReserveService.java b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/service/ReserveService.java new file mode 100644 index 0000000..4e6bc9a --- /dev/null +++ b/backend/reserve-request-service/src/main/java/org/egovframe/cloud/reserverequestservice/service/ReserveService.java @@ -0,0 +1,311 @@ +package org.egovframe.cloud.reserverequestservice.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.config.GlobalConstant; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.reactive.service.ReactiveAbstractService; +import org.egovframe.cloud.reserverequestservice.api.dto.ReserveResponseDto; +import org.egovframe.cloud.reserverequestservice.api.dto.ReserveSaveRequestDto; +import org.egovframe.cloud.reserverequestservice.domain.Category; +import org.egovframe.cloud.reserverequestservice.domain.Reserve; +import org.egovframe.cloud.reserverequestservice.domain.ReserveRepository; +import org.egovframe.cloud.reserverequestservice.domain.ReserveStatus; +import org.springframework.amqp.core.*; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import java.util.stream.IntStream; + +/** + * org.egovframe.cloud.reserverequestservice.service.ReserveService + *

+ * 예약 신청 service class + * + * @author 표준프레임워크센터 shinmj + * @version 1.0 + * @since 2021/09/17 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    shinmj      최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Transactional +@Service +public class ReserveService extends ReactiveAbstractService { + private final ReserveRepository reserveRepository; + private final StreamBridge streamBridge; + private final AmqpAdmin amqpAdmin; + + /** + * entity -> dto 변환 + * + * @param reserve + * @return + */ + private Mono convertReserveResponseDto(Reserve reserve) { + return Mono.just(ReserveResponseDto.builder() + .entity(reserve) + .build()); + } + + /** + * 현재 로그인 사용자 id + * + * @return + */ + private Mono getUserId() { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getPrincipal) + .map(String.class::cast); + } + + /** + * 예약 신청 저장 + * + * @param saveRequestDto + * @return + */ + public Mono create(ReserveSaveRequestDto saveRequestDto) { + return Mono.just(saveRequestDto) + .flatMap(dto -> { + String uuid = UUID.randomUUID().toString(); + dto.setReserveId(uuid); + dto.setReserveStatusId(ReserveStatus.REQUEST.getKey()); + return Mono.just(dto.toEntity()); + }) + .zipWith(getUserId()) + .flatMap(tuple -> { + tuple.getT1().setCreatedInfo(LocalDateTime.now(), tuple.getT2()); + return Mono.just(tuple.getT1()); + }) + .flatMap(reserveRepository::insert) + .flatMap(this::convertReserveResponseDto); + } + + /** + * 예약 신청 - 실시간 + * 예약 정보 저장 후 재고 변경을 위해 이벤트 publish + * + * @param saveRequestDto + * @return + */ + public Mono saveForEvent(ReserveSaveRequestDto saveRequestDto) { + return create(saveRequestDto) + .flatMap(reserveResponseDto -> + Mono.fromCallable(() -> { + //예약 저장 후 해당 id로 queue 생성 + Exchange ex = ExchangeBuilder.directExchange(GlobalConstant.SUCCESS_OR_NOT_EX_NAME) + .durable(true).build(); + amqpAdmin.declareExchange(ex); + + Queue queue = QueueBuilder.durable(reserveResponseDto.getReserveId()).build(); + amqpAdmin.declareQueue(queue); + + Binding binding = BindingBuilder.bind(queue) + .to(ex) + .with(reserveResponseDto.getReserveId()) + .noargs(); + amqpAdmin.declareBinding(binding); + + log.info("Biding successfully created"); + + streamBridge.send("reserveRequest-out-0", reserveResponseDto); + + return reserveResponseDto; + }).subscribeOn(Schedulers.boundedElastic()) + ); + } + + /** + * 예약 신청 - 실시간 + * 이벤트 스트림을 타지 않는 경우 (재고 변경 이벤트가 없는 경우: 공간, 장비) + * + * @param saveRequestDto + * @return + */ + public Mono save(ReserveSaveRequestDto saveRequestDto) { + return Mono.just(saveRequestDto) + .flatMap(this::checkValidation) + .onErrorResume(throwable -> Mono.error(throwable)) + .flatMap(dto -> { + String uuid = UUID.randomUUID().toString(); + dto.setReserveId(uuid); + dto.setReserveStatusId(ReserveStatus.APPROVE.getKey()); + return Mono.just(dto.toEntity()); + }).zipWith(getUserId()) + .flatMap(tuple -> Mono.just(tuple.getT1().setCreatedInfo(LocalDateTime.now(), tuple.getT2()))) + .flatMap(reserveRepository::insert) + .flatMap(this::convertReserveResponseDto); + } + + private Mono checkValidation(ReserveSaveRequestDto saveRequestDto) { + if (Category.EQUIPMENT.isEquals(saveRequestDto.getCategoryId())) { + return checkEquipment(saveRequestDto); + }else if (Category.SPACE.isEquals(saveRequestDto.getCategoryId())) { + return checkSpace(saveRequestDto); + } + return Mono.error(new BusinessMessageException("저장 할 수 없습니다.")); + } + + /** + * 예약 날자 validation + * + * @param saveRequestDto + * @return + */ + private Mono checkReserveDate(ReserveSaveRequestDto saveRequestDto) { + LocalDateTime startDate = saveRequestDto.getReserveMeansId().equals("realtime") ? + saveRequestDto.getRequestStartDate() : saveRequestDto.getOperationStartDate(); + LocalDateTime endDate = saveRequestDto.getReserveMeansId().equals("realtime") ? + saveRequestDto.getRequestEndDate() : saveRequestDto.getOperationEndDate(); + + if (saveRequestDto.getReserveStartDate().isBefore(startDate)) { + return Mono.error(new BusinessMessageException("시작일이 운영/예약 시작일 이전입니다.")); + } + + if (saveRequestDto.getReserveEndDate().isAfter(endDate)) { + return Mono.error(new BusinessMessageException("종료일이 운영/예약 종료일 이후입니다.")); + } + if (saveRequestDto.getIsPeriod()) { + long between = ChronoUnit.DAYS.between(saveRequestDto.getReserveStartDate(), + saveRequestDto.getReserveEndDate()); + if (saveRequestDto.getPeriodMaxCount() < between) { + return Mono.error(new BusinessMessageException("최대 예약 가능 일수보다 예약기간이 깁니다. (최대 예약 가능일 수 : "+saveRequestDto.getPeriodMaxCount()+")")); + } + } + return Mono.just(saveRequestDto); + } + + /** + * 공간 예약 시 예약 날짜에 다른 예약이 있는지 체크 + * + * @param saveRequestDto + * @return + */ + private Mono checkSpace(ReserveSaveRequestDto saveRequestDto) { + return this.checkReserveDate(saveRequestDto) + .flatMap(result -> reserveRepository.findAllByReserveDateWithoutSelfCount( + result.getReserveId(), + result.getReserveItemId(), + result.getReserveStartDate(), + result.getReserveEndDate()) + .flatMap(count -> { + if (count > 0) { + return Mono.error(new BusinessMessageException("해당 날짜에는 예약할 수 없습니다.")); + } + return Mono.just(result); + }) + ); + } + + /** + * 장비 예약 시 예약 날짜에 예약 가능한 재고 체크 + * + * @param saveRequestDto + * @return + */ + private Mono checkEquipment(ReserveSaveRequestDto saveRequestDto) { + return this.checkReserveDate(saveRequestDto) + .flatMap(result -> this.getMaxByReserveDateWithoutSelf( + result.getReserveId(), + result.getReserveItemId(), + result.getReserveStartDate(), + result.getReserveEndDate()) + .flatMap(max -> { + if ((result.getTotalQty() - max) < result.getReserveQty()) { + return Mono.just(false); + } + return Mono.just(true); + }) + .flatMap(isValid -> { + if (!isValid) { + return Mono.error(new BusinessMessageException("해당 날짜에 예약할 수 있는 재고수량이 없습니다.")); + } + return Mono.just(saveRequestDto); + }) + ); + } + + /** + * 예약물품에 대해 날짜별 예약된 수량 max 조회 + * 현 예약 건 제외 + * + * @param reserveItemId + * @param startDate + * @param endDate + * @return + */ + private Mono getMaxByReserveDateWithoutSelf(String reserveId, Long reserveItemId, LocalDateTime startDate, LocalDateTime endDate) { + Flux reserveFlux = reserveRepository.findAllByReserveDateWithoutSelf(reserveId, reserveItemId, startDate, endDate) + .switchIfEmpty(Flux.empty()); + + if (reserveFlux.equals(Flux.empty())) { + return Mono.just(0); + } + + long between = ChronoUnit.DAYS.between(startDate, endDate); + return Flux.fromStream(IntStream.iterate(0, i -> i + 1) + .limit(between) + .mapToObj(i -> startDate.plusDays(i))) + .flatMap(localDateTime -> + reserveFlux.map(findReserve -> { + if (localDateTime.isAfter(findReserve.getReserveStartDate()) + || localDateTime.isBefore(findReserve.getReserveEndDate())) { + return findReserve.getReserveQty(); + } + return 0; + }).reduce(0, (x1, x2) -> x1 + x2)) + .groupBy(integer -> integer) + .flatMap(group -> group.reduce((x1,x2) -> x1 > x2?x1:x2)) + .last(); + } + + /** + * 예약 신청 후 예약 물품 재고 변경 성공 시 예약승인으로 상태 변경 + * + * @param reserveId + * @param reserveStatus + * @return + */ + public Mono updateStatus(String reserveId, ReserveStatus reserveStatus) { + log.info("update : {} , {}", reserveId, reserveStatus); + return reserveRepository.findById(reserveId) + .map(reserve -> reserve.updateStatus(reserveStatus.getKey())) + .flatMap(reserveRepository::save) + .then(); + } + + /** + * 예약 신청 후 예약 물품 재고 변경 실패 시 해당 예약 건 삭제 + * + * @param reserveId + * @return + */ + public Mono delete(String reserveId) { + log.info("delete {}", reserveId); + return reserveRepository.findById(reserveId) + .flatMap(reserveRepository::delete) + .then(); + } + +} diff --git a/backend/reserve-request-service/src/main/resources/application.yml b/backend/reserve-request-service/src/main/resources/application.yml new file mode 100644 index 0000000..cc9a42f --- /dev/null +++ b/backend/reserve-request-service/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + application: + name: reserve-request-service + +server: + port: 0 + +# config server actuator +management: + endpoints: + web: + exposure: + include: refresh, health, beans \ No newline at end of file diff --git a/backend/reserve-request-service/src/main/resources/bootstrap.yml b/backend/reserve-request-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..16d04da --- /dev/null +++ b/backend/reserve-request-service/src/main/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: reserve-request-service \ No newline at end of file diff --git a/backend/reserve-request-service/src/main/resources/schema.sql b/backend/reserve-request-service/src/main/resources/schema.sql new file mode 100644 index 0000000..45682bb --- /dev/null +++ b/backend/reserve-request-service/src/main/resources/schema.sql @@ -0,0 +1,26 @@ +-- reserve Table Create SQL +CREATE TABLE IF NOT EXISTS reserve +( + `reserve_id` VARCHAR(255) NOT NULL COMMENT '예약 id', + `reserve_item_id` BIGINT NULL COMMENT '예약 물품 id', + `location_id` BIGINT NULL COMMENT '예약 물품-지역 id', + `category_id` VARCHAR(255) NULL COMMENT '예약 물품-유형 id', + `reserve_qty` BIGINT(18) NULL COMMENT '예약 신청인원/수량', + `reserve_purpose_content` VARCHAR(4000) NULL COMMENT '예약신청 목적', + `attachment_code` VARCHAR(255) NULL COMMENT '첨부파일 코드', + `reserve_start_date` DATETIME NULL COMMENT '예약 신청 시작일', + `reserve_end_date` DATETIME NULL COMMENT '예약 신청 종료일', + `reserve_status_id` VARCHAR(20) NULL COMMENT '예약상태 - 공통코드(reserve-status)', + `reason_cancel_content` VARCHAR(4000) NULL COMMENT '예약 취소 사유', + `user_id` VARCHAR(255) NULL COMMENT '예약자 id', + `user_contact_no` VARCHAR(50) NULL COMMENT '예약자 연락처', + `user_email_addr` VARCHAR(500) NULL COMMENT '예약자 이메일', + `create_date` DATETIME NULL COMMENT '생성일', + `created_by` VARCHAR(255) NULL COMMENT '생성자', + `modified_date` DATETIME NULL COMMENT '수정일', + `last_modified_by` VARCHAR(255) NULL COMMENT '수정자', + PRIMARY KEY (reserve_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE reserve COMMENT '예약 신청&확인'; + diff --git a/backend/reserve-request-service/src/test/java/org/egovframe/cloud/reserverequestservice/ReserveRequestServiceApplicationTests.java b/backend/reserve-request-service/src/test/java/org/egovframe/cloud/reserverequestservice/ReserveRequestServiceApplicationTests.java new file mode 100644 index 0000000..4c12109 --- /dev/null +++ b/backend/reserve-request-service/src/test/java/org/egovframe/cloud/reserverequestservice/ReserveRequestServiceApplicationTests.java @@ -0,0 +1,13 @@ +package org.egovframe.cloud.reserverequestservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ReserveRequestServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/reserve-request-service/src/test/resources/application-test.yml b/backend/reserve-request-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..e12cefb --- /dev/null +++ b/backend/reserve-request-service/src/test/resources/application-test.yml @@ -0,0 +1,44 @@ +spring: + application: + name: reserve-request-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: + hibernate: + generate-ddl: true + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/backend/user-service/Dockerfile b/backend/user-service/Dockerfile new file mode 100644 index 0000000..3ef9942 --- /dev/null +++ b/backend/user-service/Dockerfile @@ -0,0 +1,16 @@ +# openjdk8 base image +FROM openjdk:8-jre-alpine + +# config server uri: dockder run --e 로 변경 가능 +#ENV SPRING_CLOUD_CONFIG_URI https://egov-config.paas-ta.org +RUN mkdir -p /usr/app/msa-attach-volume +# jar 파일이 복사되는 위치 +ENV APP_HOME=/usr/app/ +# 작업 시작 위치 +WORKDIR $APP_HOME +# jar 파일 복사 +COPY build/libs/*.jar app.jar +# cf docker push, random port 사용할 수 없다 +#EXPOSE 80 +# 실행 (application-cf.yml 프로필이 기본값) +CMD ["java", "-Dspring.profiles.active=${profile:cf}", "-jar", "app.jar"] diff --git a/backend/user-service/README.md b/backend/user-service/README.md new file mode 100644 index 0000000..1f8aef2 --- /dev/null +++ b/backend/user-service/README.md @@ -0,0 +1,39 @@ +# Getting Started + +### OAUTH2 설정 +> 각 사이트에서 애플리케이션 API 이용을 신청하여 Client ID를 발급 받아야 한다.
+> 현재 구글과 네이버를 지원한다. +- [Google](https://console.cloud.google.com) +- [Naver](https://developers.naver.com/apps/#/register?api=nvlogin) +- Kakao - @todo + +### application-oauth.yml +- resources/application-oauth.yml 파일을 생성한다. +- 아래 내용을 넣고 각 client-id, client-secret 를 입력한다. +```yaml +spring: + security: + oauth2: + client: + registration: + google: + client-id: @TODO https://console.cloud.google.com + client-secret: @TODO + scope: profile,email + # 네이버는 Spring Security를 공식 지원하지 않기 때문에 CommonOAuth2Provider 에서 해주는 값들을 수동으로 입력한다. + naver: + client-id: @TODO https://developers.naver.com/apps/#/register?api=nvlogin + client-secret: @TODO + redirect_uri_template: "{baseUrl}/{action}/oauth2/code/{registrationId}" + authorization_grant_type: authorization_code + scope: name,email,profile_image + client-name: Naver + provider: + naver: + authorization_uri: https://nid.naver.com/oauth2.0/authorize + token_uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + # 기준이 되는 user_name 의 이름을 네이버에서는 response로 지정해야한다. (네이버 회원 조회시 반환되는 JSON 형태 때문이다) + # response를 user_name으로 지정하고 이후 자바 코드로 response의 id를 user_name으로 지정한다. (스프링 시큐리티에서 하위 필드를 명시할 수 없기 때문) + user_name_attribute: response +``` \ No newline at end of file diff --git a/backend/user-service/build.gradle b/backend/user-service/build.gradle new file mode 100644 index 0000000..1cf52b9 --- /dev/null +++ b/backend/user-service/build.gradle @@ -0,0 +1,100 @@ +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' +sourceCompatibility = '1.8' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() + maven { url "https://maven.egovframe.go.kr/maven/" } // egovframe maven 원격 저장소 +} + +ext { + set('springCloudVersion', "2020.0.3") +} + +dependencies { +// implementation files('../../module-common/build/libs/module-common-0.1.jar') // 공통 모듈, @ComponentScan(basePackages={"org.egovframe.cloud"}) 추가해야 적용된다 + implementation 'org.egovframe.cloud:module-common:0.1' + 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-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-config' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap' // config + implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // bus + implementation 'org.springframework.cloud:spring-cloud-starter-sleuth' + implementation 'org.springframework.cloud:spring-cloud-sleuth-zipkin' + implementation 'net.logstash.logback:logstash-logback-encoder:6.6' // logstash logback + implementation 'mysql:mysql-connector-java' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + // querydsl + implementation 'com.querydsl:querydsl-jpa' + // cache + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'org.ehcache:ehcache' + implementation 'javax.cache:cache-api' // expiry를 위해 필요 + + implementation 'com.google.api-client:google-api-client:1.32.1' + + // swagger + implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' + implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + // lombok + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'com.h2database:h2' + testImplementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +test { + useJUnitPlatform() +} + +// 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/user-service/gradlew b/backend/user-service/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/backend/user-service/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/user-service/gradlew.bat b/backend/user-service/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/backend/user-service/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/user-service/manifest.yml b/backend/user-service/manifest.yml new file mode 100644 index 0000000..8188fed --- /dev/null +++ b/backend/user-service/manifest.yml @@ -0,0 +1,16 @@ +--- +applications: + - name: egov-user-service # CF push 시 생성되는 이름 +# memory: 512M # 메모리 + instances: 1 # 인스턴스 수 + host: egov-user-service # host 명으로 유일해야 함 + path: build/libs/user-service-0.1.jar # build 후 생성된 jar 위치 + buildpack: java_buildpack # cf buildpacks 명령어로 java buildpack 이름 확인 + services: + - egov-discovery-provided-service # discovery service binding + env: + spring_profiles_active: cf + spring_cloud_config_uri: https://egov-config.paas-ta.org + app_name: egov-user-service # logstash custom app name + TZ: Asia/Seoul + JAVA_OPTS: -Xss349k diff --git a/backend/user-service/settings.gradle b/backend/user-service/settings.gradle new file mode 100644 index 0000000..eb2f57d --- /dev/null +++ b/backend/user-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'user-service' diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/UserServiceApplication.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/UserServiceApplication.java new file mode 100644 index 0000000..41e9656 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/UserServiceApplication.java @@ -0,0 +1,43 @@ +package org.egovframe.cloud.userservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * org.egovframe.cloud.userservice.UserApplication + *

+ * 유저 서비스 어플리케이션 클래스 + * Eureka Client 로 설정했기 때문에 Eureka Server 가 먼저 기동되어야 한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@ComponentScan({"org.egovframe.cloud.common", "org.egovframe.cloud.servlet", "org.egovframe.cloud.userservice"}) // org.egovframe.cloud.common package 포함하기 위해 +@EntityScan({"org.egovframe.cloud.servlet.domain", "org.egovframe.cloud.userservice.domain"}) +@EnableDiscoveryClient +@SpringBootApplication +public class UserServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(UserServiceApplication.class, args); + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/MessageSourceApiController.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/MessageSourceApiController.java new file mode 100644 index 0000000..6b67891 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/MessageSourceApiController.java @@ -0,0 +1,42 @@ +package org.egovframe.cloud.userservice.api; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.MessageSource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Locale; + +/** + * org.egovframe.cloud.userservice.api.MessageSourceApiController + *

+ * MessageSource 정상 확인을 위한 컨트롤러 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/08/10 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/08/10    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class MessageSourceApiController { + private final MessageSource messageSource; + + @GetMapping("/api/v1/messages/{code}/{lang}") + public String getMessage(@PathVariable String code, @PathVariable String lang) { + Locale locale = "en".equals(lang)? Locale.ENGLISH : Locale.KOREAN; + String message = messageSource.getMessage(code, null, locale); + log.info("code={}, lang={}, message={}", code, lang, message); + return message; + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/AuthorizationApiController.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/AuthorizationApiController.java new file mode 100644 index 0000000..dbe7e8d --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/AuthorizationApiController.java @@ -0,0 +1,153 @@ +package org.egovframe.cloud.userservice.api.role; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationListResponseDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationResponseDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationSaveRequestDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationUpdateRequestDto; +import org.egovframe.cloud.userservice.service.role.AuthorizationService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * org.egovframe.cloud.userservice.api.role.AuthorizationApiController + *

+ * API Gateway 의 RestApiAuthorization.check 메소드에 의해 호출된다. + * 요청 url에 대한 사용자 인가 서비스를 수행하는 클래스 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/19 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/19    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@RestController +public class AuthorizationApiController { + + /** + * 인가 서비스 + */ + private final AuthorizationService authorizationService; + + /** + * 인가 여부 확인 + * + * @param httpMethod Http Method + * @param requestPath 요청 경로 + * @return Boolean 인가 여부 + */ + @GetMapping("/api/v1/authorizations/check") + public Boolean isAuthorization(@RequestParam("httpMethod") String httpMethod, @RequestParam("requestPath") String requestPath) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + String userId = authentication.getName(); + List roles = authentication.getAuthorities().stream().map(GrantedAuthority::toString).collect(Collectors.toList()); + + // 사용자 아이디로 조회 + // return authorizationService.isAuthorization(userId, httpMethod, requestPath); + + // 권한으로 조회 + Boolean isAuth = authorizationService.isAuthorization(roles, httpMethod, requestPath); + + log.info("[isAuthorization={}] authentication.isAuthenticated()={}, userId={}, httpMethod={}, requestPath={}, roleList={}", isAuth, authentication.isAuthenticated(), userId, httpMethod, requestPath, roles); + + return isAuth; + } + + /** + * 인가 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 인가 목록 응답 DTO + */ + @GetMapping("/api/v1/authorizations") + public Page findPage(RequestDto requestDto, + @PageableDefault(sort = "sort_seq", direction = Sort.Direction.ASC) Pageable pageable) { + return authorizationService.findPage(requestDto, pageable); + } + + /** + * 인가 단건 조회 + * + * @param authorizationNo 인가 번호 + * @return AuthorizationResponseDto 인가 상세 응답 DTO + */ + @GetMapping("/api/v1/authorizations/{authorizationNo}") + public AuthorizationResponseDto findById(@PathVariable Integer authorizationNo) { + return authorizationService.findById(authorizationNo); + } + + /** + * 인가 다음 정렬 순서 조회 + * + * @return Integer 다음 정렬 순서 + */ + @GetMapping("/api/v1/authorizations/sort-seq/next") + public Integer findNextSortSeq() { + return authorizationService.findNextSortSeq(); + } + + /** + * 인가 등록 + * + * @param requestDto 인가 등록 요청 DTO + * @return AuthorizationResponseDto 인가 상세 응답 DTO + */ + @PostMapping("/api/v1/authorizations") + public AuthorizationResponseDto save(@RequestBody @Valid AuthorizationSaveRequestDto requestDto) { + return authorizationService.save(requestDto); + } + + /** + * 인가 수정 + * + * @param authorizationNo 인가 번호 + * @param requestDto 인가 수정 요청 DTO + * @return AuthorizationResponseDto 인가 상세 응답 DTO + */ + @PutMapping("/api/v1/authorizations/{authorizationNo}") + public AuthorizationResponseDto update(@PathVariable Integer authorizationNo, @RequestBody @Valid AuthorizationUpdateRequestDto requestDto) { + return authorizationService.update(authorizationNo, requestDto); + } + + /** + * 인가 삭제 + * + * @param authorizationNo 인가 번호 + */ + @DeleteMapping("/api/v1/authorizations/{authorizationNo}") + public void delete(@PathVariable Integer authorizationNo) { + authorizationService.delete(authorizationNo); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/RoleApiController.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/RoleApiController.java new file mode 100644 index 0000000..2e4d51c --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/RoleApiController.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.userservice.api.role; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleListResponseDto; +import org.egovframe.cloud.userservice.service.role.RoleService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * org.egovframe.cloud.userservice.api.role.RoleApiController + *

+ * 권한 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class RoleApiController { + + /** + * 권한 서비스 + */ + private final RoleService roleService; + + /** + * 권한 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 권한 목록 응답 DTO + */ + @GetMapping("/api/v1/roles") + public Page findPage(RequestDto requestDto, + @PageableDefault(sort = "sort_seq", direction = Sort.Direction.ASC) Pageable pageable) { + return roleService.findPage(requestDto, pageable); + } + + /** + * 권한 정렬 순서 오름차순 전체 목록 조회 + * + * @return List + */ + @GetMapping("/api/v1/roles/all") + public List findAll() { + return roleService.findAllBySort(Sort.by(Sort.Direction.ASC, "sortSeq")); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/RoleAuthorizationApiController.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/RoleAuthorizationApiController.java new file mode 100644 index 0000000..73aff61 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/RoleAuthorizationApiController.java @@ -0,0 +1,76 @@ +package org.egovframe.cloud.userservice.api.role; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationDeleteRequestDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListRequestDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListResponseDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationSaveRequestDto; +import org.egovframe.cloud.userservice.service.role.RoleAuthorizationService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * org.egovframe.cloud.userservice.api.role.RoleAuthorizationApiController + *

+ * 권한 인가 Rest API 컨트롤러 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@RestController +public class RoleAuthorizationApiController { + + /** + * 권한 인가 서비스 + */ + private final RoleAuthorizationService roleAuthorizationService; + + /** + * 권한 인가 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 권한 인가 목록 응답 DTO + */ + @GetMapping("/api/v1/role-authorizations") + public Page findPageAuthorizationList(@Valid RoleAuthorizationListRequestDto requestDto, + Pageable pageable) { + return roleAuthorizationService.findPageAuthorizationList(requestDto, pageable); + } + + /** + * 권한 인가 다건 등록 + * + * @param requestDtoList 권한 인가 등록 요청 DTO List + * @return List 등록한 권한 인가 목록 응답 DTO + */ + @PostMapping("/api/v1/role-authorizations") + public List save(@RequestBody @Valid List requestDtoList) { + return roleAuthorizationService.save(requestDtoList); + } + + /** + * 권한 인가 다건 삭제 + * + * @param requestDtoList 권한 인가 삭제 요청 DTO List + */ + @PutMapping("/api/v1/role-authorizations") + public void delete(@RequestBody @Valid List requestDtoList) { + roleAuthorizationService.delete(requestDtoList); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationListResponseDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationListResponseDto.java new file mode 100644 index 0000000..170362b --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationListResponseDto.java @@ -0,0 +1,78 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * org.egovframe.cloud.userservice.api.role.dto.AuthorizationListResponseDto + *

+ * 인가 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class AuthorizationListResponseDto implements Serializable { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = 7400347728171964946L; + + /** + * 인가 번호 + */ + private Integer authorizationNo; + + /** + * 인가 명 + */ + private String authorizationName; + + /** + * URL 패턴 값 + */ + private String urlPatternValue; + + /** + * Http Method 코드 + */ + private String httpMethodCode; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * 인가 목록 응답 DTO 생성자 + * + * @param authorizationNo 인가 번호 + * @param authorizationName 인가 명 + * @param urlPatternValue URL 패턴 값 + * @param httpMethodCode Http Method 코드 + * @param sortSeq 정렬 순서 + */ + @QueryProjection + public AuthorizationListResponseDto(Integer authorizationNo, String authorizationName, String urlPatternValue, String httpMethodCode, Integer sortSeq) { + this.authorizationNo = authorizationNo; + this.authorizationName = authorizationName; + this.urlPatternValue = urlPatternValue; + this.httpMethodCode = httpMethodCode; + this.sortSeq = sortSeq; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationResponseDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationResponseDto.java new file mode 100644 index 0000000..00d393f --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationResponseDto.java @@ -0,0 +1,98 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.userservice.domain.role.Authorization; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.userservice.api.role.dto.AuthorizationResponseDto + *

+ * 인가 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class AuthorizationResponseDto { + + /** + * 인가 번호 + */ + private Integer authorizationNo; + + /** + * 인가 명 + */ + private String authorizationName; + + /** + * URL 패턴 값 + */ + private String urlPatternValue; + + /** + * Http Method 코드 + */ + private String httpMethodCode; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * 권한 인가 목록 응답 DTO + */ + private List roleAuthorizations; + + /** + * 인가 엔티티를 생성자로 주입 받아서 인가 상세 응답 DTO 속성 값 세팅 + * + * @param entity 인가 엔티티 + */ + public AuthorizationResponseDto(Authorization entity) { + this.authorizationNo = entity.getAuthorizationNo(); + this.authorizationName = entity.getAuthorizationName(); + this.urlPatternValue = entity.getUrlPatternValue(); + this.httpMethodCode = entity.getHttpMethodCode(); + this.sortSeq = entity.getSortSeq(); + if (entity.getRoleAuthorizations() != null) { + this.roleAuthorizations = entity.getRoleAuthorizations().stream() +// .map(RoleAuthorizationListResponseDto::new) + .map(e -> RoleAuthorizationListResponseDto.builder() + .roleId(e.getRoleAuthorizationId().getRoleId()) + .authorizationNo(e.getRoleAuthorizationId().getAuthorizationNo()) + .build()) + .collect(Collectors.toList()); + } + } + + /** + * 인가 상세 응답 DTO 속성 값으로 인가 엔티티 빌더를 사용하여 객체 생성 + * + * @return Authorization 인가 엔티티 + */ + public Authorization toEntity() { + return Authorization.builder() + .authorizationNo(authorizationNo) + .authorizationName(authorizationName) + .urlPatternValue(urlPatternValue) + .httpMethodCode(httpMethodCode) + .sortSeq(sortSeq) + .build(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationSaveRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationSaveRequestDto.java new file mode 100644 index 0000000..5aea9b6 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationSaveRequestDto.java @@ -0,0 +1,67 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import lombok.Getter; +import org.egovframe.cloud.userservice.domain.role.Authorization; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.userservice.api.role.dto.AuthorizationSaveRequestDto + *

+ * 인가 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +public class AuthorizationSaveRequestDto { + + /** + * 인가 명 + */ + @NotBlank(message = "{authorization.authorizationName} {err.required}") + private String authorizationName; + + /** + * URL 패턴 값 + */ + @NotBlank(message = "{authorization.urlPatternValue} {err.required}") + private String urlPatternValue; + + /** + * Http Method 코드 + */ + @NotBlank(message = "{authorization.httpMethodCode} {err.required}") + private String httpMethodCode; + + /** + * 정렬 순서 + */ + @NotNull(message = "{authorization.sortSeq} {err.required}") + private Integer sortSeq; + + /** + * 인가 등록 요청 DTO 속성 값으로 인가 엔티티 빌더를 사용하여 객체 생성 + * + * @return Authorization 인가 엔티티 + */ + public Authorization toEntity() { + return Authorization.builder() + .authorizationName(authorizationName) + .urlPatternValue(urlPatternValue) + .httpMethodCode(httpMethodCode) + .sortSeq(sortSeq) + .build(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationUpdateRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationUpdateRequestDto.java new file mode 100644 index 0000000..f47509b --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/AuthorizationUpdateRequestDto.java @@ -0,0 +1,52 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import lombok.Getter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.userservice.api.role.dto.AuthorizationUpdateRequestDto + *

+ * 인가 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +public class AuthorizationUpdateRequestDto { + + /** + * 인가 명 + */ + @NotBlank(message = "{authorization.authorizationName} {err.required}") + private String authorizationName; + + /** + * URL 패턴 값 + */ + @NotBlank(message = "{authorization.urlPatternValue} {err.required}") + private String urlPatternValue; + + /** + * Http Method 코드 + */ + @NotBlank(message = "{authorization.httpMethodCode} {err.required}") + private String httpMethodCode; + + /** + * 정렬 순서 + */ + @NotNull(message = "{authorization.sortSeq} {err.required}") + private Integer sortSeq; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationDeleteRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationDeleteRequestDto.java new file mode 100644 index 0000000..1d7adc6 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationDeleteRequestDto.java @@ -0,0 +1,69 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorization; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationDeleteRequestDto + *

+ * 권한 인가 삭제 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/13 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/13    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class RoleAuthorizationDeleteRequestDto { + + /** + * 권한 id + */ + @NotBlank(message = "{role.roleId} {err.required}") + private String roleId; + + /** + * 인가 번호 + */ + @NotNull(message = "{authorization.authorizationNo} {err.required}") + private Integer authorizationNo; + + /** + * 권한 인가 삭제 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param roleId 권한 id + * @param authorizationNo 인가 번호 + */ + @Builder + public RoleAuthorizationDeleteRequestDto(String roleId, Integer authorizationNo) { + this.roleId = roleId; + this.authorizationNo = authorizationNo; + } + + /** + * 권한 인가 삭제 DTO 속성 값으로 권한 인가 엔티티 빌더를 사용하여 객체 생성 + * + * @return RoleAuthorization 권한 인가 엔티티 + */ + public RoleAuthorization toEntity() { + return RoleAuthorization.builder() + .roleId(roleId) + .authorizationNo(authorizationNo) + .build(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationListRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationListRequestDto.java new file mode 100644 index 0000000..b0b3403 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationListRequestDto.java @@ -0,0 +1,35 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import lombok.Getter; +import org.egovframe.cloud.common.dto.RequestDto; + +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListRequestDto + *

+ * 권한 인가 목록 요청 DTO 클래스 + * 인가 목록 요청 DTO 클래스(org.egovframe.cloud.portalservice.api.authorization.dto.AuthorizationListRequestDto) 상속? + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/09    jooho       최초 생성
+ * 
+ */ +@Getter +public class RoleAuthorizationListRequestDto extends RequestDto { + + /** + * 권한 id + */ + @NotBlank(message = "{role.roleId} {err.required}") + private String roleId; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationListResponseDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationListResponseDto.java new file mode 100644 index 0000000..a391a17 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationListResponseDto.java @@ -0,0 +1,88 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListResponseDto + *

+ * 권한 인가 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class RoleAuthorizationListResponseDto { + + /** + * 권한 id + */ + private String roleId; + + /** + * 인가 번호 + */ + private Integer authorizationNo; + + /** + * 인가 명 + */ + private String authorizationName; + + /** + * URL 패턴 값 + */ + private String urlPatternValue; + + /** + * Http Method 코드 + */ + private String httpMethodCode; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * 생성 여부 + */ + private Boolean createdAt; + + /** + * 권한 인가 목록 응답 DTO 클래스 생성자 + * + * @param roleId 권한 id + * @param authorizationNo 인가 번호 + * @param authorizationName 인가 명 + * @param urlPatternValue URL 패턴 값 + * @param httpMethodCode Http Method 코드 + * @param sortSeq 정렬 순서 + * @param createdAt 생성 여부 + */ + @QueryProjection + @Builder + public RoleAuthorizationListResponseDto(String roleId, Integer authorizationNo, String authorizationName, + String urlPatternValue, String httpMethodCode, Integer sortSeq, Boolean createdAt) { + this.roleId = roleId; + this.authorizationNo = authorizationNo; + this.authorizationName = authorizationName; + this.urlPatternValue = urlPatternValue; + this.httpMethodCode = httpMethodCode; + this.sortSeq = sortSeq; + this.createdAt = createdAt; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationResponseDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationResponseDto.java new file mode 100644 index 0000000..23695f6 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationResponseDto.java @@ -0,0 +1,78 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorization; + +/** + * org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationResponseDto + *

+ * 권한 인가 상세 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class RoleAuthorizationResponseDto { + + /** + * 권한 id + */ + private String roleId; + + /** + * 인가 번호 + */ + private Integer authorizationNo; + + /** + * 인가 명 + */ + private String authorizationName; + + /** + * URL 패턴 값 + */ + private String urlPatternValue; + + /** + * Http Method 코드 + */ + private String httpMethodCode; + + /** + * 정렬 순서 + */ + private Integer sortSeq; + + /** + * 생성 여부 + */ + private Integer createdAt; + + /** + * 권한 인가 엔티티를 생성자로 주입 받아서 권한 인가 상세 응답 DTO 속성 값 세팅 + * + * @param entity 권한 인가 엔티티 + */ + public RoleAuthorizationResponseDto(RoleAuthorization entity) { + this.roleId = entity.getRoleAuthorizationId().getRoleId(); + this.authorizationNo = entity.getAuthorization().getAuthorizationNo(); + this.authorizationName = entity.getAuthorization().getAuthorizationName(); + this.urlPatternValue = entity.getAuthorization().getUrlPatternValue(); + this.httpMethodCode = entity.getAuthorization().getHttpMethodCode(); + this.sortSeq = entity.getAuthorization().getSortSeq(); + this.createdAt = 1; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationSaveRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationSaveRequestDto.java new file mode 100644 index 0000000..646299a --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleAuthorizationSaveRequestDto.java @@ -0,0 +1,53 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import lombok.Getter; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorization; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationSaveRequestDto + *

+ * 권한 인가 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jooho       최초 생성
+ * 
+ */ +@Getter +public class RoleAuthorizationSaveRequestDto { + + /** + * 권한 id + */ + @NotBlank(message = "{role.roleId} {err.required}") + private String roleId; + + /** + * 인가 번호 + */ + @NotNull(message = "{authorization.authorizationNo} {err.required}") + private Integer authorizationNo; + + /** + * 권한 인가 등록 요청 DTO 속성 값으로 권한 인가 엔티티 빌더를 사용하여 객체 생성 + * + * @return RoleAuthorization 권한 인가 엔티티 + */ + public RoleAuthorization toEntity() { + return RoleAuthorization.builder() + .roleId(roleId) + .authorizationNo(authorizationNo) + .build(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleListResponseDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleListResponseDto.java new file mode 100644 index 0000000..6a0e27a --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/role/dto/RoleListResponseDto.java @@ -0,0 +1,68 @@ +package org.egovframe.cloud.userservice.api.role.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.userservice.api.role.dto.RoleListResponseDto + *

+ * 권한 목록 응답 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class RoleListResponseDto { + + /** + * 권한 id + */ + private String roleId; + + /** + * 권한 명 + */ + private String roleName; + + /** + * 권한 내용 + */ + private String roleContent; + + /** + * 생성 일시 + */ + private LocalDateTime createdDate; + + /** + * 권한 목록 응답 DTO 클래스 생성자 + * + * @param roleId 권한 id + * @param roleName 권한 명 + * @param roleContent 권한 내용 + * @param createdDate 생성 일시 + */ + @QueryProjection + @Builder + public RoleListResponseDto(String roleId, String roleName, String roleContent, LocalDateTime createdDate) { + this.roleId = roleId; + this.roleName = roleName; + this.roleContent = roleContent; + this.createdDate = createdDate; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/UserApiController.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/UserApiController.java new file mode 100644 index 0000000..abd95b1 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/UserApiController.java @@ -0,0 +1,277 @@ +package org.egovframe.cloud.userservice.api.user; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.util.MessageUtil; +import org.egovframe.cloud.userservice.api.user.dto.UserEmailRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserFindPasswordSaveRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserFindPasswordUpdateRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserJoinRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserListResponseDto; +import org.egovframe.cloud.userservice.api.user.dto.UserPasswordMatchRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserPasswordUpdateRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserResponseDto; +import org.egovframe.cloud.userservice.api.user.dto.UserSaveRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserUpdateInfoRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserUpdateRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserVerifyRequestDto; +import org.egovframe.cloud.userservice.config.TokenProvider; +import org.egovframe.cloud.userservice.service.user.UserService; +import org.springframework.core.env.Environment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.userservice.api.user.UserApiController + *

+ * 사용자 CRUD 요청을 처리하는 REST API Controller + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor // final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성하여, 빈을 생성자로 주입받게 한다. +@RestController +public class UserApiController { + private final UserService userService; + private final Environment env; + private final TokenProvider tokenProvider; + + private final MessageUtil messageUtil; + + /** + * 유저 서비스 상태 확인 + * + * @return + */ + @GetMapping("/actuator/health-user") + public String status() { + return String.format("GET User Service on" + + "\n local.server.port :" + env.getProperty("local.server.port") + + "\n token expiration time :" + env.getProperty("token.expiration_time") + + "\n spring.datasource.username :" + env.getProperty("spring.datasource.username") + + "\n egov.message :" + env.getProperty("egov.message") + ); + } + + @PostMapping("/actuator/health-user") + public String poststatus() { + return String.format("POST User Service on" + + "\n local.server.port :" + env.getProperty("local.server.port") + + "\n token expiration time :" + env.getProperty("token.expiration_time") + + "\n spring.datasource.username :" + env.getProperty("spring.datasource.username") + + "\n egov.message :" + env.getProperty("egov.message") + ); + } + + /** + * 사용자 정보 입력 + * + * @param requestDto + * @return + */ + @PostMapping("/api/v1/users") + public Long save(@RequestBody @Valid UserSaveRequestDto requestDto) { + return userService.save(requestDto); + } + + /** + * 사용자 정보 업데이트 + * + * @param userId + * @param requestDto + * @return + */ + @PutMapping("/api/v1/users/{userId}") + public String update(@PathVariable String userId, @RequestBody @Valid UserUpdateRequestDto requestDto) { + return userService.update(userId, requestDto); + } + + /** + * 사용자 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 사용자 목록 응답 DTO + */ + @GetMapping("/api/v1/users") + public Page findPage(RequestDto requestDto, Pageable pageable) { + return userService.findPage(requestDto, pageable); + } + + /** + * 사용자 단 건 조회 + * + * @param userId + * @return + */ + @GetMapping("/api/v1/users/{userId}") + public UserResponseDto findByUserId(@PathVariable String userId) { + return userService.findByUserId(userId); + } + + /** + * refresh token 과 일치하는 사용자가 있으면 access token 을 새로 발급하여 리턴한다. + * + * @param request + * @param response + * @return + */ + @PutMapping("/api/v1/users/token/refresh") + public void refreshToken(HttpServletRequest request, HttpServletResponse response) { + String refreshToken = request.getHeader(HttpHeaders.AUTHORIZATION); + tokenProvider.refreshToken(refreshToken, response); + } + + /** + * 이메일 중복 확인 + * + * @param requestDto 사용자 이메일 확인 요청 DTO + * @return Boolean 중복 여부 + */ + @PostMapping("/api/v1/users/exists") + public Boolean existsEmail(@RequestBody UserEmailRequestDto requestDto) { + return userService.existsEmail(requestDto.getEmail(), requestDto.getUserId()); + } + + /** + * 사용자 회원 가입 + * + * @param requestDto 사용자 가입 요청 DTO + * @return Boolean 성공 여부 + */ + @PostMapping("/api/v1/users/join") + public Boolean join(@RequestBody @Valid UserJoinRequestDto requestDto) { + return userService.join(requestDto); + } + + /** + * 사용자 비밀번호 찾기 + * + * @param requestDto 사용자 비밀번호 찾기 등록 요청 DTO + * @return Boolean 메일 전송 여부 + */ + @PostMapping("/api/v1/users/password/find") + public Boolean findPassword(@RequestBody @Valid UserFindPasswordSaveRequestDto requestDto) { + return userService.findPassword(requestDto); + } + + /** + * 사용자 비밀번호 찾기 유효성 확인 + * + * @param token 토큰 + * @return Boolean 유효 여부 + */ + @GetMapping("/api/v1/users/password/valid/{token}") + public Boolean validPassword(@PathVariable String token) { + return userService.validPassword(token); + } + + /** + * 사용자 비밀번호 찾기 변경 + * + * @param requestDto 사용자 비밀번호 수정 요청 DTO + * @return Boolean 수정 여부 + */ + @PutMapping("/api/v1/users/password/change") + public Boolean changePassword(@RequestBody @Valid UserFindPasswordUpdateRequestDto requestDto) { + return userService.changePassword(requestDto); + } + + /** + * 사용자 비밀번호 변경 + * + * @param requestDto 사용자 비밀번호 수정 요청 DTO + * @return Boolean 수정 여부 + */ + @PutMapping("/api/v1/users/password/update") + public Boolean updatePassword(@RequestBody @Valid UserPasswordUpdateRequestDto requestDto) { + final String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + return userService.updatePassword(userId, requestDto); + } + + /** + * 사용자 비밀번호 확인 + * + * @param requestDto 사용자 비밀번호 확인 요청 DTO + * @return Boolean 일치 여부 + */ + @PostMapping("/api/v1/users/password/match") + public Boolean matchPassword(@RequestBody @Valid UserPasswordMatchRequestDto requestDto) { + final String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + final String password = requestDto.getPassword(); + + return userService.matchPassword(userId, password); + } + + /** + * 사용자 회원정보 변경 + * + * @param userId 사용자 id + * @param requestDto 사용자 수정 요청 DTO + * @return String 사용자 id + */ + @PutMapping("/api/v1/users/info/{userId}") + public String updateInfo(@PathVariable String userId, @RequestBody @Valid UserUpdateInfoRequestDto requestDto) { + final String authUserId = SecurityContextHolder.getContext().getAuthentication().getName(); + if (!authUserId.equals(userId)) { + throw new BusinessMessageException(messageUtil.getMessage("err.access.denied")); + } + + return userService.updateInfo(userId, requestDto); + } + + /** + * 사용자 회원탈퇴 + * + * @param requestDto 사용자 비밀번호 확인 요청 DTO + * @return Boolean 일치 여부 + * @throws GeneralSecurityException 보안 예외 + * @throws IOException 입출력 예외 + */ + @PostMapping("/api/v1/users/leave") + public Boolean leave(@RequestBody @Valid UserVerifyRequestDto requestDto) throws GeneralSecurityException, IOException { + final String userId = SecurityContextHolder.getContext().getAuthentication().getName(); + + return userService.leave(userId, requestDto); + } + + /** + * 사용자 회원탈퇴 + * + * @param userId 사용자 비밀번호 확인 요청 DTO + * @return Boolean 일치 여부 + */ + @DeleteMapping("/api/v1/users/delete/{userId}") + public Boolean delete(@PathVariable String userId) { + return userService.delete(userId); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserEmailRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserEmailRequestDto.java new file mode 100644 index 0000000..ed3d346 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserEmailRequestDto.java @@ -0,0 +1,42 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserEmailRequestDto + * + * 사용자 이메일 확인 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserEmailRequestDto { + + /** + * 사용자 id + */ + private String userId; + + /** + * 이메일 + */ + @NotBlank(message = "{user.email}{valid.required}") + @Email + private String email; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserFindPasswordSaveRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserFindPasswordSaveRequestDto.java new file mode 100644 index 0000000..b95359d --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserFindPasswordSaveRequestDto.java @@ -0,0 +1,69 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.userservice.domain.user.UserFindPassword; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserFindPasswordSaveRequestDto + * + * 사용자 비밀번호 찾기 등록 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserFindPasswordSaveRequestDto { + + /** + * 사용자 명 + */ + @NotBlank(message = "{user.user_name}{valid.required}") + private String userName; + + /** + * 이메일 주소 + */ + @NotBlank(message = "{user.email}{valid.required}") + @Email + private String emailAddr; + + /** + * 메인 주소 + */ + @NotBlank + private String mainUrl; + + /** + * 비밀번호 변경 주소 + */ + @NotBlank + private String changePasswordUrl; + + /** + * 사용자 비밀번호 찾기 등록 요청 DTO 속성 값으로 개인정보처리방침 엔티티 빌더를 사용하여 객체 생성 + * + * @return UserFindPassword 사용자 비밀번호 찾기 엔티티 + */ + public UserFindPassword toEntity(Integer requestNo, String tokenValue) { + return UserFindPassword.builder() + .emailAddr(emailAddr) + .requestNo(requestNo) + .tokenValue(tokenValue) + .build(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserFindPasswordUpdateRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserFindPasswordUpdateRequestDto.java new file mode 100644 index 0000000..2d3f2cc --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserFindPasswordUpdateRequestDto.java @@ -0,0 +1,43 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserFindPasswordUpdateRequestDto + * + * 사용자 비밀번호 찾기 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserFindPasswordUpdateRequestDto { + + /** + * 토큰 값 + */ + @NotBlank(message = "{common.token}{valid.required}") + private String tokenValue; + + /** + * 비밀번호 + */ + @NotBlank(message = "{user.password}{valid.required}") + @Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", message = "{valid.password}") // (숫자)(영문)(특수문자)(공백제거)(자리수) + private String password; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserInfoUpdateRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserInfoUpdateRequestDto.java new file mode 100644 index 0000000..a1ef4b3 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserInfoUpdateRequestDto.java @@ -0,0 +1,43 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserInfoUpdateRequestDto + * + * 사용자 정보 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserInfoUpdateRequestDto { + + /** + * 이메일 + */ + @NotBlank(message = "{user.email}{valid.required}") + @Email + private String email; + + /** + * 사용자 명 + */ + @NotBlank(message = "{user.user_name}{valid.required}") + private String userName; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserJoinRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserJoinRequestDto.java new file mode 100644 index 0000000..0172255 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserJoinRequestDto.java @@ -0,0 +1,65 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.common.domain.Role; +import org.egovframe.cloud.userservice.domain.user.User; +import org.egovframe.cloud.userservice.domain.user.UserStateCode; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.util.UUID; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserJoinRequestDto + * + * 사용자 가입 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/23 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/23    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserJoinRequestDto { + + @NotBlank(message = "{user.user_name}{valid.required}") + private String userName; + + @NotBlank(message = "{user.email}{valid.required}") + @Email + private String email; + + // (숫자)(영문)(특수문자)(공백제거)(자리수) + @Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", + message = "{valid.password}") + private String password; + + /** + * UserSaveRequestDto 의 필드 값을 User Entity 빌더를 사용하여 주입 후 User를 리턴한다. + * UserSaveRequestDto 가 가지고 있는 User 의 필드만 세팅할 수 있게 된다. + * + * @param passwordEncoder + * @return + */ + public User toEntity(BCryptPasswordEncoder passwordEncoder) { + return User.builder() + .userName(userName) + .email(email) + .encryptedPassword(passwordEncoder.encode(password)) // 패스워드 인코딩 + .userId(UUID.randomUUID().toString()) // 사용자 아이디 랜덤하게 생성 + .role(Role.USER) // 가입 시 기본 권한 + .userStateCode(UserStateCode.NORMAL.getKey()) // 승인 절차 없이 정상 상태로 가입 + .build(); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserListResponseDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserListResponseDto.java new file mode 100644 index 0000000..4834b19 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserListResponseDto.java @@ -0,0 +1,77 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import org.egovframe.cloud.common.domain.Role; +import org.egovframe.cloud.userservice.domain.user.User; +import org.egovframe.cloud.userservice.domain.user.UserStateCode; + +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserListResponseDto + *

+ * 사용자 목록 요청시 사용되는 필요한 정보만 담긴 DTO + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +public class UserListResponseDto { + private String userId; + private String userName; + private String email; + private String roleId; + private String roleName; + private String userStateCode; + private String userStateCodeName; + private LocalDateTime lastLoginDate; + private Integer loginFailCount; + + /** + * UserListResponseDto 는 Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣는다. + * 굳이 모든 필드를 가진 생성자가 필요하지 않다. + * + * @param entity + */ + public UserListResponseDto(User entity) { + this.userId = entity.getUserId(); + this.userName = entity.getUserName(); + this.email = entity.getEmail(); + } + + /** + * 사용자 목록 응답 DTO 생성자 + * + * @param userId 사용자 id + * @param userName 사용자 명 + * @param email 이메일 주소 + * @param role 권한 + * @param userStateCode 사용자 상태 코드 + * @param lastLoginDate 마지막 로그인 일시 + * @param loginFailCount 로그인 실패 수 + */ + @QueryProjection + public UserListResponseDto(String userId, String userName, String email, Role role, String userStateCode, LocalDateTime lastLoginDate, Integer loginFailCount) { + this.userId = userId; + this.userName = userName; + this.email = email; + this.roleId = role.getKey(); + this.roleName = role.getTitle(); + UserStateCode usc = UserStateCode.findByKey(userStateCode); + this.userStateCode = usc.getKey(); + this.userStateCodeName = usc.getTitle(); + this.lastLoginDate = lastLoginDate; + this.loginFailCount = loginFailCount; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserLoginRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserLoginRequestDto.java new file mode 100644 index 0000000..a2bbf62 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserLoginRequestDto.java @@ -0,0 +1,91 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserLoginRequestDto + *

+ * 로그인 요청시 사용되는 필요한 정보만 담긴 DTO + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserLoginRequestDto { + + /** + * 이메일 + */ + @Email + private String email; + + /** + * 비밀번호 + */ + // (숫자)(영문)(특수문자)(공백제거)(자리수) + @Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", + message = "{valid.password}") + private String password; + + /** + * 공급자 + */ + @NotBlank(message = "{common.provider}{valid.required}") + private String provider; + + /** + * 토큰 + */ + private String token; + + /** + * 이름 + */ + private String name; + + /** + * 사용자 로그인 요청 DTO 클래스 생성자 + * 빌더 패턴으로 객체 생성 + * + * @param email 이메일 + * @param password 비밀번호 + * @param provider 공급자 + * @param token 토큰 + */ + @Builder + public UserLoginRequestDto(String email, String password, String provider, String token, String name) { + this.email = email; + this.password = password; + this.provider = provider; + this.token = token; + this.name = name; + } + + /** + * OAuth 로그인 정보 세팅 + * + * @param email + * @param password + */ + public void setOAuthLoginInfo(String email, String password) { + this.email = email; + this.password = password; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserPasswordMatchRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserPasswordMatchRequestDto.java new file mode 100644 index 0000000..0979a08 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserPasswordMatchRequestDto.java @@ -0,0 +1,37 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserPasswordMatchRequestDto + * + * 사용자 비밀번호 확인 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserPasswordMatchRequestDto { + + /** + * 비밀번호 + */ + @NotBlank(message = "{label.title.current_password}{valid.required}") + @Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", message = "{valid.password}") // (숫자)(영문)(특수문자)(공백제거)(자리수) + private String password; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserPasswordUpdateRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserPasswordUpdateRequestDto.java new file mode 100644 index 0000000..e93bef5 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserPasswordUpdateRequestDto.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Pattern; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserPasswordUpdateRequestDto + *

+ * 사용자 비밀번호 변경 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/16 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserPasswordUpdateRequestDto extends UserVerifyRequestDto { + + /** + * 신규 비밀번호 + */ + @Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", message = "{valid.password}") + // (숫자)(영문)(특수문자)(공백제거)(자리수) + private String newPassword; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserResponseDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserResponseDto.java new file mode 100644 index 0000000..586e24b --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserResponseDto.java @@ -0,0 +1,55 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import org.egovframe.cloud.userservice.domain.user.User; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserResponseDto + *

+ * 사용자 정보 요청시 사용되는 필요한 정보만 담긴 DTO + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +public class UserResponseDto { + + private String userId; + private String userName; + private String email; + private String roleId; + private String userStateCode; + private String googleId; + private String kakaoId; + private String naverId; + private Boolean isSocialUser; + private Boolean hasPassword; + + /** + * UserResponseDto는 Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣는다. + * 굳이 모든 필드를 가진 생성자가 필요하지 않다. + * + * @param entity + */ + public UserResponseDto(User entity) { + this.userId = entity.getUserId(); + this.userName = entity.getUserName(); + this.email = entity.getEmail(); + this.roleId = entity.getRoleKey(); + this.userStateCode = entity.getUserStateCode(); + this.googleId = entity.getGoogleId(); + this.kakaoId = entity.getKakaoId(); + this.naverId = entity.getNaverId(); + this.isSocialUser = entity.isSocialUser(); + this.hasPassword = entity.getEncryptedPassword() != null && !"".equals(entity.getEncryptedPassword()); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserSaveRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserSaveRequestDto.java new file mode 100644 index 0000000..46d65a7 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserSaveRequestDto.java @@ -0,0 +1,83 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import java.util.Arrays; +import java.util.UUID; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +import org.egovframe.cloud.common.domain.Role; +import org.egovframe.cloud.userservice.domain.user.User; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserSaveRequestDto + *

+ * 사용자 정보 생성 요청시 처리 가능한 정보만 담긴 DTO + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserSaveRequestDto { + + @NotBlank(message = "{user.user_name}{valid.required}") + private String userName; + + @NotBlank(message = "{user.email}{valid.required}") + @Email + private String email; + + // (숫자)(영문)(특수문자)(공백제거)(자리수) + @Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", + message = "{valid.password}") + private String password; + + @NotBlank(message = "{role}{valid.required}") + private String roleId; + + @NotBlank(message = "{user.user_state_code}{valid.required}") + private String userStateCode; + + @Builder + public UserSaveRequestDto(String userName, String email, String password, String roleId, String userStateCode) { + this.userName = userName; + this.email = email; + this.password = password; + this.roleId = roleId; + this.userStateCode = userStateCode; + } + + /** + * UserSaveRequestDto 의 필드 값을 User Entity 빌더를 사용하여 주입 후 User를 리턴한다. + * UserSaveRequestDto 가 가지고 있는 User 의 필드만 세팅할 수 있게 된다. + * + * @param passwordEncoder + * @return + */ + public User toEntity(BCryptPasswordEncoder passwordEncoder) { + return User.builder() + .userName(userName) + .email(email) + .encryptedPassword(passwordEncoder.encode(password)) // 패스워드 인코딩 + .userId(UUID.randomUUID().toString()) // 사용자 아이디 랜덤하게 생성 + .role(Arrays.stream(Role.values()).filter(c -> c.getKey().equals(roleId)).findAny().orElse(null)) + .userStateCode(userStateCode) + .build(); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserUpdateInfoRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserUpdateInfoRequestDto.java new file mode 100644 index 0000000..b48d568 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserUpdateInfoRequestDto.java @@ -0,0 +1,43 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserUpdateInfoRequestDto + *

+ * 사용자 정보 수정 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/23    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserUpdateInfoRequestDto extends UserVerifyRequestDto { + + @NotBlank(message = "{user.user_name}{valid.required}") + private String userName; + + @NotBlank(message = "{user.email}{valid.required}") + @Email + private String email; + + @Builder + public UserUpdateInfoRequestDto(String userName, String email) { + this.userName = userName; + this.email = email; + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserUpdateRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserUpdateRequestDto.java new file mode 100644 index 0000000..ebf10c6 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserUpdateRequestDto.java @@ -0,0 +1,59 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserUpdateRequestDto + *

+ * 사용자 정보 수정 요청시 처리 가능한 정보만 담긴 DTO + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserUpdateRequestDto { + + @NotBlank(message = "{user.user_name}{valid.required}") + private String userName; + + @NotBlank(message = "{user.email}{valid.required}") + @Email + private String email; + + // (숫자)(영문)(특수문자)(공백제거)(자리수) + @Pattern(regexp = "((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20})|()", + message = "{valid.password}") + private String password; + + @NotBlank(message = "{role}{valid.required}") + private String roleId; + + @NotBlank(message = "{user.user_state_code}{valid.required}") + private String userStateCode; + + @Builder + public UserUpdateRequestDto(String userName, String email, String password, String roleId, String userStateCode) { + this.userName = userName; + this.email = email; + this.password = password; + this.roleId = roleId; + this.userStateCode = userStateCode; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserVerifyRequestDto.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserVerifyRequestDto.java new file mode 100644 index 0000000..949501d --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/api/user/dto/UserVerifyRequestDto.java @@ -0,0 +1,47 @@ +package org.egovframe.cloud.userservice.api.user.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * org.egovframe.cloud.userservice.api.user.dto.UserVerifyRequestDto + * + * 사용자 탈퇴 요청 DTO 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/16 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/16    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +public class UserVerifyRequestDto { + + /** + * 비밀번호 + */ + @Pattern(regexp = "((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20})|()", message = "{valid.password}") // (숫자)(영문)(특수문자)(공백제거)(자리수) + private String password; + + /** + * 공급자 + */ + @NotBlank(message = "{common.provider}{valid.required}") + private String provider; + + /** + * 토큰 + */ + private String token; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/AuthenticationFilter.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/AuthenticationFilter.java new file mode 100644 index 0000000..60f5d0d --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/AuthenticationFilter.java @@ -0,0 +1,208 @@ +package org.egovframe.cloud.userservice.config; + +import static org.egovframe.cloud.common.config.GlobalConstant.LOGIN_URI; +import static org.springframework.util.StringUtils.hasLength; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +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 org.egovframe.cloud.common.util.LogUtil; +import org.egovframe.cloud.userservice.api.user.dto.UserLoginRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserResponseDto; +import org.egovframe.cloud.userservice.service.user.UserService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.jsonwebtoken.Claims; +import lombok.extern.slf4j.Slf4j; + +/** + * org.egovframe.cloud.userservice.config.AuthenticationFilter + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private final TokenProvider tokenProvider; + private final UserService userService; + + public AuthenticationFilter(AuthenticationManager authenticationManager, TokenProvider tokenProvider, UserService userService) { + super.setAuthenticationManager(authenticationManager); + this.tokenProvider = tokenProvider; + this.userService = userService; + } + + /** + * 로그인 요청 시 호출되는 메소드이다. + * 계정 정보를 받아 인증정보를 생성한다. + * + * @param request http 요청 + * @param response http 응답 + * @return Authentication 인증정보 + * @throws NullPointerException 널 포인터 예외 + * @throws Exception 예외 + */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + try { + // 사용자가 입력한 인증정보 받기, POST method 값이기 때문에 input stream으로 받았다. + UserLoginRequestDto creds = new ObjectMapper().readValue(request.getInputStream(), UserLoginRequestDto.class); + + UsernamePasswordAuthenticationToken upat = null; + if (creds.getProvider() != null && !"email".equals(creds.getProvider())) { + UserResponseDto userDto = userService.loadUserBySocial(creds); + + upat = new UsernamePasswordAuthenticationToken( + userDto.getEmail(), + null, + AuthorityUtils.createAuthorityList(userDto.getRoleId()) + ); + + SecurityContextHolder.getContext().setAuthentication(upat); + + return upat; + } else { + upat = new UsernamePasswordAuthenticationToken( + creds.getEmail(), + creds.getPassword(), + new ArrayList<>() + ); + + // 인증정보 만들기 + return getAuthenticationManager().authenticate(upat); + } + } catch (NullPointerException e) { + log.error(e.getLocalizedMessage()); + throw new RuntimeException(e); + } catch (Exception e) { + log.error(e.getLocalizedMessage()); + throw new RuntimeException(e); + } + } + + /** + * 로그인 인증 성공 후 호출된다. + * 토큰을 생성하여 헤더에 토큰 정보를 담는다. + * + * @param request + * @param response + * @param chain + * @param authResult + * @throws IOException + * @throws ServletException + */ + @Transactional + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + // 토큰 생성 및 response header add + tokenProvider.createTokenAndAddHeader(request, response, chain, authResult); + // 로그인 성공 후처리 + userService.loginCallback(LogUtil.getSiteId(request), authResult.getName(), true, ""); + } + + @Transactional + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { + String failContent = failed.getMessage(); + if (failed instanceof InternalAuthenticationServiceException) { + log.info("{} 해당 사용자가 없습니다", request.getAttribute("email")); + } else if (failed instanceof BadCredentialsException) { + failContent = "패스워드 인증에 실패하였습니다. " + failContent; + } + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + + // 로그인 실패 후처리 + String email = (String) request.getAttribute("email"); + userService.loginCallback(LogUtil.getSiteId(request), email, false, failContent); + super.unsuccessfulAuthentication(request, response, failed); + } + + + /** + * 로그인 요청 뿐만 아니라 모든 요청시마다 호출된다. + * 토큰에 담긴 정보로 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 (!hasLength(token) || "undefined".equals(token)) { + super.doFilter(request, response, chain); + } else { + try { + final String requestURI = httpRequest.getRequestURI(); + log.info("httpRequest.getRequestURI() ={}", requestURI); + + if (LOGIN_URI.equals(requestURI)) { + // 로그인 등 토큰 정보를 꺼낼 필요가 없는 경우 + SecurityContextHolder.getContext().setAuthentication(null); + } else { + // 토큰 유효성 검사는 API Gateway ReactiveAuthorization 클래스에서 미리 처리된다. + Claims claims = tokenProvider.getClaimsFromToken(token); + + String username = claims.getSubject(); + if (username == null) { + // refresh token 에는 subject, authorities 정보가 없다. + SecurityContextHolder.getContext().setAuthentication(null); + } else { + List roleList = Arrays.stream(claims.get(tokenProvider.TOKEN_CLAIM_NAME, String.class).split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, null, roleList)); + } + } + chain.doFilter(request, response); + + } catch (Exception e) { + SecurityContextHolder.getContext().setAuthentication(null); + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + log.error("AuthenticationFilter doFilter", e); + } + } + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CacheConfig.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CacheConfig.java new file mode 100644 index 0000000..9b4d974 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CacheConfig.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.userservice.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * org.egovframe.cloud.userservice.config.CacheConfig + * + * 캐시 설정 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/21 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/21    jooho       최초 생성
+ * 
+ */ +@Configuration +@EnableCaching +@EnableAspectJAutoProxy(exposeProxy=true) // AopContext.currentProxy() 사용 옵션 +public class CacheConfig { +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CacheEventLogger.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CacheEventLogger.java new file mode 100644 index 0000000..4a5f288 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CacheEventLogger.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.userservice.config; + +import lombok.extern.slf4j.Slf4j; +import org.ehcache.event.CacheEvent; +import org.ehcache.event.CacheEventListener; + +/** + * org.egovframe.cloud.userservice.config.CacheEventLogger + * + * 캐시 이밴트 로깅 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/22 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/22    jooho       최초 생성
+ * 
+ */ +@Slf4j +public class CacheEventLogger implements CacheEventListener { + + /** + * 캐시 이벤트 발생시 로깅 + * + * @param cacheEvent 캐시 이벤트 + */ + public void onEvent(CacheEvent cacheEvent) { + log.info("cache event ::: key: {} / oldvalue: {} / newvalue:{}", cacheEvent.getKey(), cacheEvent.getOldValue(), cacheEvent.getNewValue()); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CustomOAuth2SuccessHandler.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CustomOAuth2SuccessHandler.java new file mode 100644 index 0000000..3c326e9 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CustomOAuth2SuccessHandler.java @@ -0,0 +1,51 @@ +package org.egovframe.cloud.userservice.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * org.egovframe.cloud.userservice.config.CustomOAuth2SuccessHandler + *

+ * OAuth2 인증 성공 시 호출된다. + * 로그인 인증정보를 받아 토큰을 발급한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/01 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/01    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@Component +public class CustomOAuth2SuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + + private final TokenProvider tokenProvider; + + /** + * 인증 시 토큰 생성 + * @param request + * @param response + * @param chain + * @param authentication + * @throws IOException + * @throws ServletException + */ + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException { + tokenProvider.createTokenAndAddHeader(request, response, chain, authentication); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CustomOAuth2UserService.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CustomOAuth2UserService.java new file mode 100644 index 0000000..8c9dc7e --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/CustomOAuth2UserService.java @@ -0,0 +1,85 @@ +package org.egovframe.cloud.userservice.config; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.userservice.config.dto.OAuthAttributes; +import org.egovframe.cloud.userservice.domain.user.User; +import org.egovframe.cloud.userservice.domain.user.UserRepository; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +/** + * org.egovframe.cloud.userservice.config.CustomOAuth2UserService + *

+ * OAuth2 로그인 이후 가져온 사용자의 정보들을 기반으로 가입 및 정보수정, 세션 저장 등의 기능을 지원 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@Service +public class CustomOAuth2UserService implements OAuth2UserService { + private final UserRepository userRepository; +// private final HttpSession httpSession; + + /** + * OAuth2 로그인 이후 호출되어 사용자 정보를 DB에 입력한다. + * + * @param userRequest + * @return + * @throws OAuth2AuthenticationException + */ + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + + // 현재 로그인 진행 중인 서비스를 구분하는 코드(구글 or 네이버..) + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + // OAuth2 로그인 진행시 키가 되는 필드 값(PK). 구글만 기본적으로 기본 코드(sub)를 지원. + String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + + // OAuth2UserService를 통해 가져온 OAuth2User의 attributes를 담을 클래스 + OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); + + User user = saveOrUpdate(attributes); + // 세션에 저장하는 경우 활성화한다 +// httpSession.setAttribute("user", new SessionUser(user)); + + return new DefaultOAuth2User( + Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())), + attributes.getAttributes(), + attributes.getNameAttributeKey() + ); + } + + /** + * OAuth2 사용자 정보가 변경는 경우 반영 + * + * @param attributes + * @return + */ + private User saveOrUpdate(OAuthAttributes attributes) { + User user = userRepository.findByEmail(attributes.getEmail()) + .map(entity -> entity.updateInfo(attributes.getUserName(), attributes.getEmail())) + .orElse(attributes.toEntity()); + + return userRepository.save(user); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/RestTemplateConfig.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/RestTemplateConfig.java new file mode 100644 index 0000000..2021983 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/RestTemplateConfig.java @@ -0,0 +1,37 @@ +package org.egovframe.cloud.userservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * org.egovframe.cloud.userservice.config.RestTemplateConfig + * + * REST Template 설정 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/27 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/27    jooho       최초 생성
+ * 
+ */ +@Configuration +public class RestTemplateConfig { + + /** + * REST Template 빈 등록 + * + * @return RestTemplate REST Template + */ + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/SecurityConfig.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/SecurityConfig.java new file mode 100644 index 0000000..5431f51 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/SecurityConfig.java @@ -0,0 +1,87 @@ +package org.egovframe.cloud.userservice.config; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.userservice.service.user.UserService; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import static org.egovframe.cloud.common.config.GlobalConstant.SECURITY_PERMITALL_ANTPATTERNS; + +/** + * org.egovframe.cloud.userservice.SecurityConfig + *

+ * Spring Security Config 클래스 + * AuthenticationFilter 를 추가하고 로그인 인증처리를 한다 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/06/30 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@RequiredArgsConstructor +@EnableWebSecurity // Spring Security 설정들을 활성화시켜 준다 +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final TokenProvider tokenProvider; + private final UserService userService; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + /** + * 스프링 시큐리티 설정 + * + * @param http + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .headers().frameOptions().disable() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 사용하기 때문에 세션은 비활성화 + .and() + .authorizeRequests() + .antMatchers(SECURITY_PERMITALL_ANTPATTERNS).permitAll() + .anyRequest().access("@authorizationService.isAuthorization(request, authentication)") // 호출 시 권한 인가 데이터 확인 + .and() + .addFilter(getAuthenticationFilter()) + .logout() + .logoutSuccessUrl("/"); + } + + /** + * 로그인 인증정보를 받아 토큰을 발급할 수 있도록 필터를 등록해준다. + * + * @return + * @throws Exception + */ + private AuthenticationFilter getAuthenticationFilter() throws Exception { + return new AuthenticationFilter(authenticationManager(), tokenProvider, userService); + } + + /** + * 인증 관련 - 로그인 처리 + * DB 에서 조회하여 일치하는지 체크한다. + * + * @param auth + * @throws Exception + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // userService.loadUserByUsername 메소드 + auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/TokenProvider.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/TokenProvider.java new file mode 100644 index 0000000..c427e99 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/TokenProvider.java @@ -0,0 +1,153 @@ +package org.egovframe.cloud.userservice.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.egovframe.cloud.userservice.api.user.dto.UserResponseDto; +import org.egovframe.cloud.userservice.service.user.UserService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.userservice.config.TokenProvider + *

+ * 로그인 성공 인증정보로 토큰을 생성한다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/01 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/01    jaeyeolkim  최초 생성
+ * 
+ */ +@Component +public class TokenProvider { + + private final UserService userService; + + public TokenProvider(UserService userService) { + this.userService = userService; + } + + @Value("${token.secret}") + private String TOKEN_SECRET; + + @Value("${token.expiration_time}") + private String TOKEN_EXPIRATION_TIME; + + @Value("${token.refresh_time}") + private String TOKEN_REFRESH_TIME; + + final String TOKEN_CLAIM_NAME = "authorities"; + final String TOKEN_ACCESS_KEY = "access-token"; + final String TOKEN_REFRESH_KEY = "refresh-token"; + final String TOKEN_USER_ID = "token-id"; + + /** + * 로그인 후 토큰을 생성하고 헤더에 정보를 담는다. + * + * @param request + * @param response + * @param chain + * @param authResult + */ + public void createTokenAndAddHeader(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) { + // 로그인 성공 후 토큰 처리 + String email = authResult.getName(); + String authorities = authResult.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + // userid 가져오기 + UserResponseDto userResponseDto = userService.findByEmail(email); + String userId = userResponseDto.getUserId(); + + // JWT Access 토큰 생성 + String accessToken = createAccessToken(authorities, userId); + + // JWT Refresh 토큰 생성 후 사용자 도메인에 저장하여 토큰 재생성 요청시 활용한다. + String refreshToken = createRefreshToken(); + userService.updateRefreshToken(userId, refreshToken); + + // Header에 토큰 세팅 + response.addHeader(TOKEN_ACCESS_KEY, accessToken); + response.addHeader(TOKEN_REFRESH_KEY, refreshToken); + response.addHeader(TOKEN_USER_ID, userId); + } + + /** + * JWT Access Token 생성 + * + * @param authorities + * @param userId + * @return + */ + private String createAccessToken(String authorities, String userId) { + return Jwts.builder() + .setSubject(userId) + .claim(TOKEN_CLAIM_NAME, authorities) + .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(TOKEN_EXPIRATION_TIME))) + .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET) + .compact(); + } + + /** + * JWT Refresh Token 생성 + * 중복 로그인을 허용하려면 user domain 에 있는 refresh token 값을 반환하고 없는 경우에만 생성하도록 처리한다. + * + * @return + */ + private String createRefreshToken() { + return Jwts.builder() + .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(TOKEN_REFRESH_TIME))) + .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET) + .compact(); + } + + /** + * 사용자가 있으면 access token 을 새로 발급하여 리턴한다. + * + * @param refreshToken + * @param response + * @return + */ + public String refreshToken(String refreshToken, HttpServletResponse response) { + // refresh token 으로 유효한 사용자가 있는지 찾는다. + org.egovframe.cloud.userservice.domain.user.User user = userService.findByRefreshToken(refreshToken); + // 사용자가 있으면 access token 을 새로 발급하여 리턴한다. + String accessToken = createAccessToken(user.getRoleKey(), user.getUserId()); + + // Header에 토큰 세팅 + response.addHeader(TOKEN_ACCESS_KEY, accessToken); + response.addHeader(TOKEN_REFRESH_KEY, refreshToken); + response.addHeader(TOKEN_USER_ID, user.getUserId()); + return accessToken; + } + + /** + * AuthenticationFilter.doFilter 메소드에서 UsernamePasswordAuthenticationToken 정보를 세팅할 때 호출된다. + * + * @param token + * @return + */ + public Claims getClaimsFromToken(String token) { + return Jwts.parser() + .setSigningKey(TOKEN_SECRET) + .parseClaimsJws(token) + .getBody(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/UserPasswordChangeEmailTemplate.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/UserPasswordChangeEmailTemplate.java new file mode 100644 index 0000000..05b33f2 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/UserPasswordChangeEmailTemplate.java @@ -0,0 +1,73 @@ +package org.egovframe.cloud.userservice.config; + +public class UserPasswordChangeEmailTemplate { + + /** + * 객체 생성 금지 + */ + private UserPasswordChangeEmailTemplate() { + throw new IllegalStateException("user password change email template class"); + } + + public static final String html = "\n" + + "
\n" + + "
\"표준프레임워크
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
비밀번호 초기화 안내
\n" + + " 안녕하세요. %s 회원님.

\n" + + " 비밀번호 초기화 관련하여 안내드립니다.
\n" + + " 회원님의 계정 비밀번호를 초기화할 수 있는 URL을 알려드립니다.

\n" + + " [비밀번호 초기화] 버튼으로 접속하여 비밀번호를 초기화 하신 후
서비스를 계속해서 이용해주시기 바랍니다.

\n" + + " 해당 링크는 발송후 1시간 동안만 유효합니다.

\n" + + " 감사합니다.\n" + + "
\n" + + " 비밀번호 초기화\n" + + "
\n" + + "
\n" + + " (C) 표준프레임워크 포털 All Rights Reserved.\n" + + "
\n" + + "
\n"; + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/OAuthAttributes.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/OAuthAttributes.java new file mode 100644 index 0000000..87fc28f --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/OAuthAttributes.java @@ -0,0 +1,66 @@ +package org.egovframe.cloud.userservice.config.dto; + +import lombok.Builder; +import lombok.Getter; +import org.egovframe.cloud.common.domain.Role; +import org.egovframe.cloud.userservice.domain.user.User; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public class OAuthAttributes { + private Map attributes; + private String nameAttributeKey; + private String userName; + private String email; + + @Builder + public OAuthAttributes(Map attributes, + String nameAttributeKey, String userName, String email) { + // public으로 선언된 데이터가 private 선언된 배열에 저장되지 않도록 한다.(reference가 아닌, “값”을 할당함으로써 private 멤버로서의 접근권한을 유지 시켜준다.) + this.attributes = new HashMap<>(); + attributes.forEach((k, v) -> this.attributes.put(k, attributes.get(k))); + this.nameAttributeKey = nameAttributeKey; + this.userName = userName; + this.email = email; + } + + // OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야만 한다. + public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map attributes) { + if ("naver".equals(registrationId)) { + return ofNaver("id", attributes); + } + return ofGoogle(userNameAttributeName, attributes); + } + + private static OAuthAttributes ofGoogle(String userNameAttributeName, Map attributes) { + return OAuthAttributes.builder() + .userName((String) attributes.get("name")) + .email((String) attributes.get("email")) + .attributes(attributes) + .nameAttributeKey(userNameAttributeName) + .build(); + } + + private static OAuthAttributes ofNaver(String userNameAttributeName, Map attributes) { + Map response = (Map) attributes.get("response"); + return OAuthAttributes.builder() + .userName((String) response.get("name")) + .email((String) response.get("email")) + .attributes(response) + .nameAttributeKey(userNameAttributeName) + .build(); + } + + // User 엔티티 생성 + // OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입할 때이다. + // 가입할 때의 기본 권한을 USER 변경함 + public User toEntity() { + return User.builder() + .userName(userName) + .email(email) + .role(Role.ANONYMOUS) + .build(); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/SessionUser.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/SessionUser.java new file mode 100644 index 0000000..ce7d805 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/SessionUser.java @@ -0,0 +1,37 @@ +package org.egovframe.cloud.userservice.config.dto; + +import lombok.Getter; +import org.egovframe.cloud.userservice.domain.user.User; + +import java.io.Serializable; + +/** + * org.egovframe.cloud.userservice.config.dto.SessionUser + *

+ * session 에 담을 사용자 정보 + * session 에 저장하기 위해 직렬화를 구현하였다 + * 토큰 사용시에는 사용되지 않는다. + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/06 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/06    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +public class SessionUser implements Serializable { + private String userName; + private String email; + + // 인증된 사용자 정보만 필요하여 두 컬럼만 정의 + public SessionUser(User user) { + this.userName = user.getUserName(); + this.email = user.getEmail(); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/SocialUser.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/SocialUser.java new file mode 100644 index 0000000..3bb4522 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/config/dto/SocialUser.java @@ -0,0 +1,382 @@ +package org.egovframe.cloud.userservice.config.dto; + +import org.springframework.security.core.CredentialsContainer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.util.*; + +/** + * Models core user information retrieved by a {@link org.springframework.security.core.userdetails.UserDetailsService}. + *

+ * Developers may use this class directly, subclass it, or write their own + * {@link UserDetails} implementation from scratch. + *

+ * {@code equals} and {@code hashcode} implementations are based on the {@code username} + * property only, as the intention is that lookups of the same user principal object (in a + * user registry, for example) will match where the objects represent the same user, not + * just when all the properties (authorities, password for example) are the same. + *

+ * Note that this implementation is not immutable. It implements the + * {@code CredentialsContainer} interface, in order to allow the password to be erased + * after authentication. This may cause side-effects if you are storing instances + * in-memory and reusing them. If so, make sure you return a copy from your + * {@code UserDetailsService} each time it is invoked. + * + * @author Ben Alex + * @author Luke Taylor + */ +public class SocialUser implements UserDetails, CredentialsContainer { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private String password; + + private final String username; + + private final Set authorities; + + private final boolean accountNonExpired; + + private final boolean accountNonLocked; + + private final boolean credentialsNonExpired; + + private final boolean enabled; + + /** + * Calls the more complex constructor with all boolean arguments set to {@code true}. + */ + public SocialUser(String username, Collection authorities) { + this(username, true, true, true, true, authorities); + } + + /** + * Construct the User with the details required by + * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider}. + * @param username the username presented to the + * DaoAuthenticationProvider + * @param enabled set to true if the user is enabled + * @param accountNonExpired set to true if the account has not expired + * @param credentialsNonExpired set to true if the credentials have not + * expired + * @param accountNonLocked set to true if the account is not locked + * @param authorities the authorities that should be granted to the caller if they + * presented the correct username and password and the user is enabled. Not null. + * @throws IllegalArgumentException if a null value was passed either as + * a parameter or as an element in the GrantedAuthority collection + */ + public SocialUser(String username, boolean enabled, boolean accountNonExpired, + boolean credentialsNonExpired, boolean accountNonLocked, + Collection authorities) { +// Assert.isTrue(username != null && !"".equals(username) && password != null, +// "Cannot pass null or empty values to constructor"); + this.username = username; + this.enabled = enabled; + this.accountNonExpired = accountNonExpired; + this.credentialsNonExpired = credentialsNonExpired; + this.accountNonLocked = accountNonLocked; + this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); + } + + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + @Override + public boolean isAccountNonExpired() { + return this.accountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return this.accountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return this.credentialsNonExpired; + } + + @Override + public void eraseCredentials() { + this.password = null; + } + + private static SortedSet sortAuthorities(Collection authorities) { + Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection"); + // Ensure array iteration order is predictable (as per + // UserDetails.getAuthorities() contract and SEC-717) + SortedSet sortedAuthorities = new TreeSet<>(new SocialUser.AuthorityComparator()); + for (GrantedAuthority grantedAuthority : authorities) { + Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements"); + sortedAuthorities.add(grantedAuthority); + } + return sortedAuthorities; + } + + /** + * Returns {@code true} if the supplied object is a {@code User} instance with the + * same {@code username} value. + *

+ * In other words, the objects are equal if they have the same username, representing + * the same principal. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof SocialUser) { + return this.username.equals(((SocialUser) obj).username); + } + return false; + } + + /** + * Returns the hashcode of the {@code username}. + */ + @Override + public int hashCode() { + return this.username.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getName()).append(" ["); + sb.append("Username=").append(this.username).append(", "); + sb.append("Password=[PROTECTED], "); + sb.append("Enabled=").append(this.enabled).append(", "); + sb.append("AccountNonExpired=").append(this.accountNonExpired).append(", "); + sb.append("credentialsNonExpired=").append(this.credentialsNonExpired).append(", "); + sb.append("AccountNonLocked=").append(this.accountNonLocked).append(", "); + sb.append("Granted Authorities=").append(this.authorities).append("]"); + return sb.toString(); + } + + /** + * Creates a UserBuilder with a specified user name + * @param username the username to use + * @return the UserBuilder + */ + public static SocialUser.UserBuilder withUsername(String username) { + return builder().username(username); + } + + /** + * Creates a UserBuilder + * @return the UserBuilder + */ + public static SocialUser.UserBuilder builder() { + return new SocialUser.UserBuilder(); + } + + public static SocialUser.UserBuilder withUserDetails(UserDetails userDetails) { + // @formatter:off + return withUsername(userDetails.getUsername()) + .accountExpired(!userDetails.isAccountNonExpired()) + .accountLocked(!userDetails.isAccountNonLocked()) + .authorities(userDetails.getAuthorities()) + .credentialsExpired(!userDetails.isCredentialsNonExpired()) + .disabled(!userDetails.isEnabled()); + // @formatter:on + } + + private static class AuthorityComparator implements Comparator, Serializable { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + @Override + public int compare(GrantedAuthority g1, GrantedAuthority g2) { + // Neither should ever be null as each entry is checked before adding it to + // the set. If the authority is null, it is a custom authority and should + // precede others. + if (g2.getAuthority() == null) { + return -1; + } + if (g1.getAuthority() == null) { + return 1; + } + return g1.getAuthority().compareTo(g2.getAuthority()); + } + + } + + /** + * Builds the user to be added. At minimum the username, password, and authorities + * should provided. The remaining attributes have reasonable defaults. + */ + public static final class UserBuilder { + + private String username; + + private List authorities; + + private boolean accountExpired; + + private boolean accountLocked; + + private boolean credentialsExpired; + + private boolean disabled; + + /** + * Creates a new instance + */ + private UserBuilder() { + } + + /** + * Populates the username. This attribute is required. + * @param username the username. Cannot be null. + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + */ + public SocialUser.UserBuilder username(String username) { + Assert.notNull(username, "username cannot be null"); + this.username = username; + return this; + } + + /** + * Populates the roles. This method is a shortcut for calling + * {@link #authorities(String...)}, but automatically prefixes each entry with + * "ROLE_". This means the following: + * + * + * builder.roles("USER","ADMIN"); + * + * + * is equivalent to + * + * + * builder.authorities("ROLE_USER","ROLE_ADMIN"); + * + * + *

+ * This attribute is required, but can also be populated with + * {@link #authorities(String...)}. + *

+ * @param roles the roles for this user (i.e. USER, ADMIN, etc). Cannot be null, + * contain null values or start with "ROLE_" + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + */ + public SocialUser.UserBuilder roles(String... roles) { + List authorities = new ArrayList<>(roles.length); + for (String role : roles) { + Assert.isTrue(!role.startsWith("ROLE_"), + () -> role + " cannot start with ROLE_ (it is automatically added)"); + authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); + } + return authorities(authorities); + } + + /** + * Populates the authorities. This attribute is required. + * @param authorities the authorities for this user. Cannot be null, or contain + * null values + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + * @see #roles(String...) + */ + public SocialUser.UserBuilder authorities(GrantedAuthority... authorities) { + return authorities(Arrays.asList(authorities)); + } + + /** + * Populates the authorities. This attribute is required. + * @param authorities the authorities for this user. Cannot be null, or contain + * null values + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + * @see #roles(String...) + */ + public SocialUser.UserBuilder authorities(Collection authorities) { + this.authorities = new ArrayList<>(authorities); + return this; + } + + /** + * Populates the authorities. This attribute is required. + * @param authorities the authorities for this user (i.e. ROLE_USER, ROLE_ADMIN, + * etc). Cannot be null, or contain null values + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + * @see #roles(String...) + */ + public SocialUser.UserBuilder authorities(String... authorities) { + return authorities(AuthorityUtils.createAuthorityList(authorities)); + } + + /** + * Defines if the account is expired or not. Default is false. + * @param accountExpired true if the account is expired, false otherwise + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + */ + public SocialUser.UserBuilder accountExpired(boolean accountExpired) { + this.accountExpired = accountExpired; + return this; + } + + /** + * Defines if the account is locked or not. Default is false. + * @param accountLocked true if the account is locked, false otherwise + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + */ + public SocialUser.UserBuilder accountLocked(boolean accountLocked) { + this.accountLocked = accountLocked; + return this; + } + + /** + * Defines if the credentials are expired or not. Default is false. + * @param credentialsExpired true if the credentials are expired, false otherwise + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + */ + public SocialUser.UserBuilder credentialsExpired(boolean credentialsExpired) { + this.credentialsExpired = credentialsExpired; + return this; + } + + /** + * Defines if the account is disabled or not. Default is false. + * @param disabled true if the account is disabled, false otherwise + * @return the {@link SocialUser.UserBuilder} for method chaining (i.e. to populate + * additional attributes for this user) + */ + public SocialUser.UserBuilder disabled(boolean disabled) { + this.disabled = disabled; + return this; + } + + public UserDetails build() { + return new SocialUser(this.username, !this.disabled, !this.accountExpired, + !this.credentialsExpired, !this.accountLocked, this.authorities); + } + + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/log/LoginLog.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/log/LoginLog.java new file mode 100644 index 0000000..fab5b27 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/log/LoginLog.java @@ -0,0 +1,63 @@ +package org.egovframe.cloud.userservice.domain.log; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseTimeEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +import static javax.persistence.GenerationType.IDENTITY; + +/** + * org.egovframe.cloud.userservice.domain.log.LoginLog + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/01    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +public class LoginLog extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "log_id") + private Long id; + + private Long siteId; + + @Column(name = "email_addr", length = 100) + private String email; + + @Column(name = "ip_addr", length = 100) + private String remoteIp; + + private Boolean successAt; + + @Column(name = "fail_content", length = 200) + private String failContent; + + @Builder + public LoginLog(Long siteId, String email, Boolean successAt, String remoteIp, String failContent) { + this.siteId = siteId; + this.email = email; + this.successAt = successAt; + this.remoteIp = remoteIp; + this.failContent = failContent; + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/log/LoginLogRepository.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/log/LoginLogRepository.java new file mode 100644 index 0000000..5b04dd4 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/log/LoginLogRepository.java @@ -0,0 +1,23 @@ +package org.egovframe.cloud.userservice.domain.log; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.userservice.domain.log.LoginLogRepository + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/01    jaeyeolkim  최초 생성
+ * 
+ */ +public interface LoginLogRepository extends JpaRepository { +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/Authorization.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/Authorization.java new file mode 100644 index 0000000..212ced0 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/Authorization.java @@ -0,0 +1,113 @@ +package org.egovframe.cloud.userservice.domain.role; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseEntity; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import javax.persistence.*; +import java.util.List; + +/** + * org.egovframe.cloud.userservice.domain.role.Authorization + *

+ * 인가 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class) +public class Authorization extends BaseEntity { + + /** + * 인가 번호 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer authorizationNo; + + /** + * 인가 명 + */ + @Column(nullable = false, length = 50) + private String authorizationName; + + /** + * URL 패턴 값 + */ + @Column(nullable = false, length = 200) + private String urlPatternValue; + + /** + * Http Method 코드 + */ + @Column(nullable = false, length = 20) + private String httpMethodCode; + + /** + * 정렬 순서 + */ + @Column() + private Integer sortSeq; + + /** + * 권한 인가 엔티티 + */ + @OneToMany(mappedBy = "authorization", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @OnDelete(action = OnDeleteAction.CASCADE) + private List roleAuthorizations; + + /** + * 빌더 패턴 클래스 생성자 + * + * @param authorizationNo 인가 번호 + * @param authorizationName 인가 명 + * @param urlPatternValue URL 패턴 값 + * @param httpMethodCode Http Method 코드 + * @param sortSeq 정렬 순서 + */ + @Builder + public Authorization(Integer authorizationNo, String authorizationName, String urlPatternValue, String httpMethodCode, Integer sortSeq, List roleAuthorizations) { + this.authorizationNo = authorizationNo; + this.authorizationName = authorizationName; + this.urlPatternValue = urlPatternValue; + this.httpMethodCode = httpMethodCode; + this.sortSeq = sortSeq; + this.roleAuthorizations = roleAuthorizations; + } + + /** + * 인가 속성 값 수정 + * + * @param authorizationName 인가 명 + * @param urlPatternValue URL 패턴 값 + * @param httpMethodCode Http Method 코드 + * @param sortSeq 정렬 순서 + * @return Authorization 인가 엔티티 + */ + public Authorization update(String authorizationName, String urlPatternValue, String httpMethodCode, Integer sortSeq) { + this.authorizationName = authorizationName; + this.urlPatternValue = urlPatternValue; + this.httpMethodCode = httpMethodCode; + this.sortSeq = sortSeq; + + return this; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepository.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepository.java new file mode 100644 index 0000000..e6c84b9 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepository.java @@ -0,0 +1,34 @@ +package org.egovframe.cloud.userservice.domain.role; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * org.egovframe.cloud.userservice.domain.role.AuthorizationRepository + *

+ * 인가 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +public interface AuthorizationRepository extends JpaRepository, AuthorizationRepositoryCustom { + + /** + * 정렬 순서로 인가 단건 조회 + * + * @param sortSeq 정렬 순서 + * @return Optional 인가 엔티티 + */ + Optional findBySortSeq(Integer sortSeq); + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepositoryCustom.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepositoryCustom.java new file mode 100644 index 0000000..82062ad --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepositoryCustom.java @@ -0,0 +1,71 @@ +package org.egovframe.cloud.userservice.domain.role; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +/** + * org.egovframe.cloud.userservice.domain.role.AuthorizationRepositoryCustom + *

+ * 인가 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/15    jooho       최초 생성
+ * 
+ */ +public interface AuthorizationRepositoryCustom { + + /** + * 인가 페이지 목록 조회 + * + * @param requestDto 인가 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 인가 목록 응답 DTO + */ + Page findPage(RequestDto requestDto, Pageable pageable); + + /** + * 권한 목록의 인가 전체 목록 조회 + * + * @param roles 권한 목록 + * @return Page 페이지 인가 목록 응답 DTO + */ + List findByRoles(List roles); + + /** + * 사용자의 인가 목록 조회 + * + * @param userId 사용자 id + * @return List 인가 목록 응답 DTO + */ + List findByUserId(String userId); + + /** + * 인가 다음 정렬 순서 조회 + * + * @return Integer 다음 정렬 순서 + */ + Integer findNextSortSeq(); + + /** + * 인가 정렬 순서 수정 + * + * @param startSortSeq 시작 정렬 순서 + * @param endSortSeq 종료 정렬 순서 + * @param increaseSortSeq 증가 정렬 순서 + * @return Long 처리 건수 + */ + Long updateSortSeq(Integer startSortSeq, Integer endSortSeq, int increaseSortSeq); + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepositoryImpl.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepositoryImpl.java new file mode 100644 index 0000000..9f2cd53 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/AuthorizationRepositoryImpl.java @@ -0,0 +1,213 @@ +package org.egovframe.cloud.userservice.domain.role; + +import java.util.List; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationListResponseDto; +import org.egovframe.cloud.userservice.domain.user.QUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.google.common.base.CaseFormat; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.userservice.domain.role.AuthorizationRepositoryImpl + *

+ * 인가 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/15    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class AuthorizationRepositoryImpl implements AuthorizationRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 인가 페이지 목록 조회 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 인가 목록 응답 DTO + */ + @Override + public Page findPage(RequestDto requestDto, Pageable pageable) { + JPQLQuery query = getAuthorizationListJPQLQuery() + .where(getBooleanExpressionKeyword(requestDto)); + + //정렬 + pageable.getSort().stream().forEach(sort -> { + Order order = sort.isAscending() ? Order.ASC : Order.DESC; + String property = sort.getProperty(); + + Path target = Expressions.path(Object.class, QAuthorization.authorization, CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, property)); + @SuppressWarnings({ "unchecked", "rawtypes" }) + OrderSpecifier orderSpecifier = new OrderSpecifier(order, target); + query.orderBy(orderSpecifier); + }); + + QueryResults result = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) //페이징 + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 권한 목록의 인가 전체 목록 조회 + * + * @param roles 권한 목록 + * @return Page 페이지 인가 목록 응답 DTO + */ + @Override + public List findByRoles(List roles) { + JPQLQuery query = getAuthorizationListJPQLQuery() + .where(JPAExpressions + .selectFrom(QRoleAuthorization.roleAuthorization) + .where(QRoleAuthorization.roleAuthorization.roleAuthorizationId.authorizationNo.eq(QAuthorization.authorization.authorizationNo) + .and(QRoleAuthorization.roleAuthorization.roleAuthorizationId.roleId.in(roles))) + .exists()); + + QueryResults result = query.fetchResults(); + + return result.getResults(); + } + + /** + * 사용자의 인가 목록 조회 + * + * @param userId 사용자 id + * @return List 인가 목록 응답 DTO + */ + @Override + public List findByUserId(String userId) { + JPQLQuery query = getAuthorizationListJPQLQuery() + .where(JPAExpressions + .selectFrom(QRoleAuthorization.roleAuthorization) + .innerJoin(QUser.user) + .on(QUser.user.role.stringValue().eq(QRoleAuthorization.roleAuthorization.roleAuthorizationId.roleId)) + .where(QRoleAuthorization.roleAuthorization.roleAuthorizationId.authorizationNo.eq(QAuthorization.authorization.authorizationNo) + .and(QUser.user.userId.eq(userId))) + .exists()); + + QueryResults result = query.fetchResults(); + + return result.getResults(); + } + + /** + * 인가 목록 JPQL Query 반환 + * + * @return JPQLQuery 인가 목록 JPQL Query + */ + public JPQLQuery getAuthorizationListJPQLQuery() { + return jpaQueryFactory + .select(Projections.constructor(AuthorizationListResponseDto.class, + QAuthorization.authorization.authorizationNo, + QAuthorization.authorization.authorizationName, + QAuthorization.authorization.urlPatternValue, + QAuthorization.authorization.httpMethodCode, + QAuthorization.authorization.sortSeq + )) + .from(QAuthorization.authorization); + } + + /** + * 인가 다음 정렬 순서 조회 + * + * @return Integer 다음 정렬 순서 + */ + @Override + public Integer findNextSortSeq() { + return jpaQueryFactory + .select(QAuthorization.authorization.sortSeq.max().add(1).coalesce(1)) + .from(QAuthorization.authorization) + .fetchOne(); + } + + /** + * 인가 정렬 순서 수정 + * + * @param startSortSeq 시작 정렬 순서 + * @param endSortSeq 종료 정렬 순서 + * @param increaseSortSeq 증가 정렬 순서 + * @return Long 수정 건수 + */ + @Override + public Long updateSortSeq(Integer startSortSeq, Integer endSortSeq, int increaseSortSeq) { + return jpaQueryFactory.update(QAuthorization.authorization) + .set(QAuthorization.authorization.sortSeq, QAuthorization.authorization.sortSeq.add(increaseSortSeq)) + .where(isGoeSortSeq(startSortSeq), + isLoeSortSeq(endSortSeq)) + .execute(); + } + + /** + * 요청 DTO로 동적 검색 표현식 리턴 + * + * @param requestDto 요청 DTO + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpressionKeyword(RequestDto requestDto) { + if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null; + + switch (requestDto.getKeywordType()) { + case "authorizationName": // 인가 명 + return QAuthorization.authorization.authorizationName.containsIgnoreCase(requestDto.getKeyword()); + case "urlPatternValue": // URL 패턴 값 + return QAuthorization.authorization.urlPatternValue.containsIgnoreCase(requestDto.getKeyword()); + case "httpMethodCode": // Http Method 코드 + return QAuthorization.authorization.httpMethodCode.containsIgnoreCase(requestDto.getKeyword()); + default: + return null; + } + } + + /** + * 정렬 순서 이하 검색 표현식 + * + * @param sortSeq 정렬 순서 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression isLoeSortSeq(Integer sortSeq) { + return sortSeq == null ? null : QAuthorization.authorization.sortSeq.loe(sortSeq); + } + + /** + * 정렬 순서 이상 검색 표현식 + * + * @param sortSeq 정렬 순서 + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression isGoeSortSeq(Integer sortSeq) { + return sortSeq == null ? null : QAuthorization.authorization.sortSeq.goe(sortSeq); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/Role.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/Role.java new file mode 100644 index 0000000..9ae8759 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/Role.java @@ -0,0 +1,86 @@ +package org.egovframe.cloud.userservice.domain.role; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.Id; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.userservice.domain.role.Role + *

+ * 권한 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함 +public class Role { + + /** + * 권한 id + */ + @Id + @Column(nullable = false, length = 20, unique = true) + private String roleId; + + /** + * 권한 명 + */ + @Column(nullable = false, length = 50) + private String roleName; + + /** + * 권한 내용 + */ + @Column(length = 200) + private String roleContent; + + /** + * 정렬 순서 + */ + @Column + private Integer sortSeq; + + /** + * 생성 일시 + */ + @CreatedDate + @Column + private LocalDateTime createdDate; + + /** + * 빌드 패턴 클래스 생성자 + * + * @param roleId 권한 id + * @param roleName 권한 명 + * @param roleContent 권한 내용 + * @param sortSeq 정렬 순서 + */ + @Builder + public Role(String roleId, String roleName, String roleContent, Integer sortSeq) { + this.roleId = roleId; + this.roleName = roleName; + this.roleContent = roleContent; + this.sortSeq = sortSeq; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorization.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorization.java new file mode 100644 index 0000000..2e83f37 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorization.java @@ -0,0 +1,80 @@ +package org.egovframe.cloud.userservice.domain.role; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * org.egovframe.cloud.userservice.domain.role.RoleAuthorization + *

+ * 권한 인가 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/09    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +@EntityListeners(AuditingEntityListener.class) // Auditing 기능 포함 +public class RoleAuthorization { + + /** + * 권한 인가 복합키 + */ + @EmbeddedId + private RoleAuthorizationId roleAuthorizationId; + + /** + * 인가 엔티티 + */ + @MapsId("authorizationNo") // RoleAuthorizationId.authorizationNo 매핑 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "authorization_no") + private Authorization authorization; + + /** + * 생성자 id + */ + @CreatedBy + @Column(updatable = false) + private String createdBy; + + /** + * 생성 일시 + */ + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + + /** + * 빌드 패턴 클래스 생성자 + * + * @param roleId 권한 id + * @param authorizationNo 인가 번호 + */ + @Builder + public RoleAuthorization(String roleId, Integer authorizationNo) { + this.roleAuthorizationId = RoleAuthorizationId.builder() + .roleId(roleId) + .authorizationNo(authorizationNo) + .build(); + this.authorization = Authorization.builder() + .authorizationNo(authorizationNo).build(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationId.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationId.java new file mode 100644 index 0000000..82e6eb3 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationId.java @@ -0,0 +1,87 @@ +package org.egovframe.cloud.userservice.domain.role; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +/** + * org.egovframe.cloud.userservice.domain.role.RoleAuthorizationId + *

+ * 권한 인가 엔티티 복합키 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/09    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Embeddable +public class RoleAuthorizationId implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = 7191831905023135716L; + + /** + * 권한 id + */ + @Column(length = 20) + private String roleId; + + /** + * 인가 번호 + */ + private Integer authorizationNo; // @MapsId("authorizationNo")로 매핑 + + /** + * 빌드 패턴 클래스 생성자 + * + * @param roleId 권한 id + * @param authorizationNo 인가 번호 + */ + @Builder + public RoleAuthorizationId(String roleId, Integer authorizationNo) { + this.roleId = roleId; + this.authorizationNo = authorizationNo; + } + + /** + * Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap. + * + * @return int a hash code value for this object. + */ + @Override + public int hashCode() { + return Objects.hash(roleId, authorizationNo); + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param object the reference object with which to compare. + * @return {@code true} if this object is the same as the obj + */ + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof RoleAuthorizationId)) return false; + RoleAuthorizationId that = (RoleAuthorizationId) object; + return Objects.equals(roleId, that.getRoleId()) && + Objects.equals(authorizationNo, that.getAuthorizationNo()); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepository.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepository.java new file mode 100644 index 0000000..e48517e --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepository.java @@ -0,0 +1,23 @@ +package org.egovframe.cloud.userservice.domain.role; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.userservice.domain.role.RoleAuthorizationRepository + *

+ * 권한 인가 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/09 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/09    jooho       최초 생성
+ * 
+ */ +public interface RoleAuthorizationRepository extends JpaRepository, RoleAuthorizationRepositoryCustom { +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepositoryCustom.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepositoryCustom.java new file mode 100644 index 0000000..a46a945 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepositoryCustom.java @@ -0,0 +1,38 @@ +package org.egovframe.cloud.userservice.domain.role; + +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListRequestDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * org.egovframe.cloud.userservice.domain.role.RoleAuthorizationRepositoryCustom + *

+ * 권한 인가 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/15    jooho       최초 생성
+ * 
+ */ +public interface RoleAuthorizationRepositoryCustom { + + /** + * 권한 인가 페이지 목록 조회 + * 인가 기준으로 권한 인가 아우터 조인 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param requestDto 권한 인가 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 권한 인가 목록 응답 DTO + */ + Page findPageAuthorizationList(RoleAuthorizationListRequestDto requestDto, Pageable pageable); + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepositoryImpl.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepositoryImpl.java new file mode 100644 index 0000000..ed0807b --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleAuthorizationRepositoryImpl.java @@ -0,0 +1,114 @@ +package org.egovframe.cloud.userservice.domain.role; + +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.role.dto.QRoleAuthorizationListResponseDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListRequestDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +/** + * org.egovframe.cloud.userservice.domain.role.RoleAuthorizationRepositoryImpl + *

+ * 권한 인가 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/15    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class RoleAuthorizationRepositoryImpl implements RoleAuthorizationRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 권한 인가 페이지 목록 조회 + * 인가 기준으로 권한 인가 아우터 조인 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param requestDto 권한 인가 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 권한 인가 목록 응답 DTO + */ + public Page findPageAuthorizationList(RoleAuthorizationListRequestDto requestDto, Pageable pageable) { + JPQLQuery query = jpaQueryFactory + .select(new QRoleAuthorizationListResponseDto( + Expressions.as(Expressions.constant(requestDto.getRoleId()), "roleId"), + QAuthorization.authorization.authorizationNo, + QAuthorization.authorization.authorizationName, + QAuthorization.authorization.urlPatternValue, + QAuthorization.authorization.httpMethodCode, + QAuthorization.authorization.sortSeq, + Expressions.as(new CaseBuilder() + .when(QRoleAuthorization.roleAuthorization.roleAuthorizationId.roleId.isNotNull() + .and(QRoleAuthorization.roleAuthorization.roleAuthorizationId.authorizationNo.isNotNull())) + .then(true) + .otherwise(false) + , "createdAt") // 생성 여부 + )) + .from(QAuthorization.authorization) + .leftJoin(QRoleAuthorization.roleAuthorization).on(QAuthorization.authorization.authorizationNo.eq(QRoleAuthorization.roleAuthorization.roleAuthorizationId.authorizationNo) + .and(getBooleanExpressionRoleId(requestDto.getRoleId()))) // 권한 id + .fetchJoin() + .where(getBooleanExpressionKeyword(requestDto)) + .orderBy(QAuthorization.authorization.sortSeq.asc()); // 인가 정렬 순서 오름차순 + + QueryResults result = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) //페이징 + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 권한 id 검색 표현식 리턴 + * + * @param roleId 권한 id + * @return BooleanExpression + */ + private BooleanExpression getBooleanExpressionRoleId(String roleId) { + return roleId != null && !"".equals(roleId) ? QRoleAuthorization.roleAuthorization.roleAuthorizationId.roleId.eq(roleId) : null; + } + + /** + * 요청 DTO로 동적 검색 표현식 리턴 + * + * @param requestDto 요청 DTO + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpressionKeyword(RequestDto requestDto) { + if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null; + + switch (requestDto.getKeywordType()) { + case "authorizationName": // 인가 명 + return QAuthorization.authorization.authorizationName.containsIgnoreCase(requestDto.getKeyword()); + case "urlPatternValue": // URL 패턴 값 + return QAuthorization.authorization.urlPatternValue.containsIgnoreCase(requestDto.getKeyword()); + case "httpMethodCode": // Http Method 코드 + return QAuthorization.authorization.httpMethodCode.containsIgnoreCase(requestDto.getKeyword()); + default: + return null; + } + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepository.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepository.java new file mode 100644 index 0000000..6006753 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepository.java @@ -0,0 +1,23 @@ +package org.egovframe.cloud.userservice.domain.role; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * org.egovframe.cloud.userservice.domain.role.RoleRepository + *

+ * 권한 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    jooho       최초 생성
+ * 
+ */ +public interface RoleRepository extends JpaRepository, RoleRepositoryCustom { +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepositoryCustom.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepositoryCustom.java new file mode 100644 index 0000000..22b7ffa --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepositoryCustom.java @@ -0,0 +1,37 @@ +package org.egovframe.cloud.userservice.domain.role; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * org.egovframe.cloud.userservice.domain.role.RoleRepositoryCustom + *

+ * 권한 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/15    jooho       최초 생성
+ * 
+ */ +public interface RoleRepositoryCustom { + + /** + * 권한 페이지 목록 조회 + * 가급적 Entity 보다는 Dto를 리턴 - Entity 조회시 hibernate 캐시, 불필요 컬럼 조회, oneToOne N+1 문제 발생 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 권한 목록 응답 DTO + */ + Page findPage(RequestDto requestDto, Pageable pageable); + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepositoryImpl.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepositoryImpl.java new file mode 100644 index 0000000..8f55fe1 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/role/RoleRepositoryImpl.java @@ -0,0 +1,106 @@ +package org.egovframe.cloud.userservice.domain.role; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.role.dto.QRoleListResponseDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.google.common.base.CaseFormat; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.userservice.domain.role.RoleRepositoryImpl + *

+ * 권한 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/15    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class RoleRepositoryImpl implements RoleRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 권한 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 권한 목록 응답 DTO + */ + @Override + public Page findPage(RequestDto requestDto, Pageable pageable) { + JPQLQuery query = jpaQueryFactory + .select(new QRoleListResponseDto( + QRole.role.roleId, + QRole.role.roleName, + QRole.role.roleContent, + QRole.role.createdDate + )) + .from(QRole.role) + .where(getBooleanExpressionKeyword(requestDto)); + + //정렬 + pageable.getSort().stream().forEach(sort -> { + Order order = sort.isAscending() ? Order.ASC : Order.DESC; + String property = sort.getProperty(); + + Path target = Expressions.path(Object.class, QRole.role, CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, property)); + @SuppressWarnings({ "unchecked", "rawtypes" }) + OrderSpecifier orderSpecifier = new OrderSpecifier(order, target); + query.orderBy(orderSpecifier); + }); + + QueryResults result = query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) //페이징 + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 요청 DTO로 동적 검색 표현식 리턴 + * + * @param requestDto 요청 DTO + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpressionKeyword(RequestDto requestDto) { + if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null; + + switch (requestDto.getKeywordType()) { + case "roleId": // 권한 id + return QRole.role.roleId.containsIgnoreCase(requestDto.getKeyword()); + case "roleName": // 권한 명 + return QRole.role.roleName.containsIgnoreCase(requestDto.getKeyword()); + case "roleContent": // 권한 내용 + return QRole.role.roleContent.containsIgnoreCase(requestDto.getKeyword()); + default: + return null; + } + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/User.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/User.java new file mode 100644 index 0000000..01a9fbd --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/User.java @@ -0,0 +1,240 @@ +package org.egovframe.cloud.userservice.domain.user; + +import static javax.persistence.GenerationType.IDENTITY; + +import java.time.LocalDateTime; +import java.util.Arrays; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +import org.egovframe.cloud.common.domain.Role; +import org.egovframe.cloud.servlet.domain.BaseEntity; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * org.egovframe.cloud.userservice.domain.user.User + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/06/30    jaeyeolkim  최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@DynamicInsert +@DynamicUpdate +@Entity +public class User extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "user_no") + private Long id; + + @Column(nullable = false, unique = true) + private String userId; + + @Column(nullable = false, length = 50) + private String userName; + + @Column(nullable = false, name = "email_addr", length = 100, unique = true) + private String email; + + @Column(length = 100) + private String encryptedPassword; + + @Enumerated(EnumType.STRING) // Enum 값을 String 문자열로 저장 + @Column(name = "role_id", nullable = false) + private Role role; + + private String refreshToken; + + @Column(nullable = false, length = 20, columnDefinition = "varchar(20) default '00'") + private String userStateCode; + + @Column + private LocalDateTime lastLoginDate; + + @Column(nullable = false, columnDefinition = "tinyint default '0'") + private Integer loginFailCount; + + @Column(length = 100) + private String googleId; + + @Column(length = 100) + private String kakaoId; + + @Column(length = 100) + private String naverId; + + @Builder + public User(String userName, String email, String encryptedPassword, String userId, + Role role, String userStateCode, String googleId, String kakaoId, String naverId) { + this.userName = userName; + this.email = email; + this.encryptedPassword = encryptedPassword; + this.userId = userId; + this.role = role; + this.userStateCode = userStateCode; + this.googleId = googleId; + this.kakaoId = kakaoId; + this.naverId = naverId; + } + + /** + * 사용자 명과 이메일 정보를 수정한다. + * + * @param username 사용자 명 + * @param email 이메일 + * @param encryptedPassword 암호화 비밀번호 + * @param roleId 권한 id + * @param userStateCode 회원 상태 코드 + * @return + */ + public User update(String username, String email, String encryptedPassword, String roleId, String userStateCode) { + this.userName = username; + this.email = email; + this.encryptedPassword = encryptedPassword; + this.role = Arrays.stream(Role.values()).filter(c -> c.getKey().equals(roleId)).findAny().orElse(null); + this.userStateCode = userStateCode; + + return this; + } + + /** + * 사용자 refresh token 정보를 필드에 입력한다. + * + * @param refreshToken + * @return + */ + public User updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } + + /** + * 사용자 비밀번호 정보를 필드에 입력한다. + * + * @param encryptedPassword 암호화 비밀번호 + * @return User 사용자 엔티티 + */ + public User updatePassword(String encryptedPassword) { + this.encryptedPassword = encryptedPassword; + return this; + } + + /** + * 사용자 명과 이메일 정보를 수정한다. + * + * @param username + * @param email + * @return + */ + public User updateInfo(String username, String email) { + this.userName = username; + this.email = email; + + return this; + } + + /** + * 사용자 상태 코드 정보를 필드에 입력한다. + * + * @param userStateCode 상태 코드 + * @return User 사용자 엔티티 + */ + public User updateUserStateCode(String userStateCode) { + this.userStateCode = userStateCode; + return this; + } + + /** + * 로그인 실패 시 로그인실패수를 증가시키고 5회 이상 실패한 경우 회원상태를 정지로 변경 + * + * @return User 사용자 엔티티 + */ + public User failLogin() { + this.loginFailCount = loginFailCount + 1; + if (this.loginFailCount >= 5) { + this.userStateCode = UserStateCode.HALT.getKey(); + } + return this; + } + + /** + * 로그인 성공 시 로그인실패수와 마지막로그인일시 정보를 갱신 + * + * @return User 사용자 엔티티 + */ + public User successLogin() { + this.loginFailCount = 0; + this.lastLoginDate = LocalDateTime.now(); + return this; + } + + /** + * 구글 id 등록 + * + * @return User 사용자 엔티티 + */ + public User updateGoogleId(String googleId) { + this.googleId = googleId; + return this; + } + + /** + * 카카오 id 등록 + * + * @return User 사용자 엔티티 + */ + public User updateKakaoId(String kakaoId) { + this.kakaoId = kakaoId; + return this; + } + + /** + * 네이버 id 등록 + * + * @return User 사용자 엔티티 + */ + public User updateNaverId(String naverId) { + this.naverId = naverId; + return this; + } + + /** + * 소셜 사용자 여부 반환 + * + * @return boolean 소셜 사용자 여부 + */ + public boolean isSocialUser() { + if (this.googleId != null && !"".equals(this.googleId)) return true; + else if (this.kakaoId != null && !"".equals(this.kakaoId)) return true; + else if (this.naverId != null && !"".equals(this.naverId)) return true; + + return false; + } + + public String getRoleKey() { + return this.role.getKey(); + } +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPassword.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPassword.java new file mode 100644 index 0000000..14c9336 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPassword.java @@ -0,0 +1,86 @@ +package org.egovframe.cloud.userservice.domain.user; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.egovframe.cloud.servlet.domain.BaseTimeEntity; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import javax.persistence.Column; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; + +/** + * org.egovframe.cloud.userservice.domain.user.UserFindPassword + *

+ * 사용자 비밀번호 찾기 엔티티 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    jooho       최초 생성
+ * 
+ */ +@Getter +@NoArgsConstructor +@Entity +@DynamicInsert +@DynamicUpdate +public class UserFindPassword extends BaseTimeEntity { + + /** + * 복합키 + */ + @EmbeddedId + private UserFindPasswordId userFindPasswordId; + + /** + * 토큰 값 + */ + @Column(nullable = false, length = 50) + private String tokenValue; + + /** + * 변경 여부 + */ + @Column(nullable = false, columnDefinition = "tinyint(1) default '0'") + private Boolean changeAt; + + /** + * 빌드 패턴 클래스 생성자 + * + * @param emailAddr 이메일 주소 + * @param requestNo 요청 번호 + * @param tokenValue 토큰 값 + * @param changeAt 변경 여부 + */ + @Builder + public UserFindPassword(String emailAddr, Integer requestNo, String tokenValue, Boolean changeAt) { + this.userFindPasswordId = UserFindPasswordId.builder() + .emailAddr(emailAddr) + .requestNo(requestNo) + .build(); + this.tokenValue = tokenValue; + this.changeAt = changeAt; + } + + /** + * 변경 여부 수정 + * + * @param changeAt 변경 여부 + * @return UserFindPassword 사용자 비밀번호 찾기 엔티티 + */ + public UserFindPassword updateChangeAt(Boolean changeAt) { + this.changeAt = changeAt; + + return this; + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordId.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordId.java new file mode 100644 index 0000000..0404b56 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordId.java @@ -0,0 +1,70 @@ +package org.egovframe.cloud.userservice.domain.user; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Getter +@NoArgsConstructor +@Embeddable +public class UserFindPasswordId implements Serializable { + + /** + * SerialVersionUID + */ + private static final long serialVersionUID = -2267755880384011782L; + + /** + * 이메일 주소 + */ + @Column(length = 50) + private String emailAddr; + + /** + * 요청 번호 + */ + private Integer requestNo; + + /** + * 빌드 패턴 클래스 생성자 + * + * @param emailAddr 이메일 주소 + * @param requestNo 요청 번호 + */ + @Builder + public UserFindPasswordId(String emailAddr, Integer requestNo) { + this.emailAddr = emailAddr; + this.requestNo = requestNo; + } + + /** + * Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap. + * + * @return int a hash code value for this object. + */ + @Override + public int hashCode() { + return Objects.hash(emailAddr, requestNo); + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param object the reference object with which to compare. + * @return {@code true} if this object is the same as the obj + */ + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof UserFindPasswordId)) return false; + UserFindPasswordId that = (UserFindPasswordId) object; + return Objects.equals(emailAddr, that.getEmailAddr()) && + Objects.equals(requestNo, that.getRequestNo()); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepository.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepository.java new file mode 100644 index 0000000..84b0ef6 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepository.java @@ -0,0 +1,34 @@ +package org.egovframe.cloud.userservice.domain.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +/** + * org.egovframe.cloud.userservice.domain.user.UserFindPasswordRepository + * + * 사용자 비밀번호 찾기 레파지토리 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/15 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    jooho       최초 생성
+ * 
+ */ +public interface UserFindPasswordRepository extends JpaRepository, UserFindPasswordRepositoryCustom { + + /** + * 토큰 값이 일치하는 사용자 비밀번호 찾기 조회 + * + * @param tokenValue 토큰 값 + * @return Optional 사용자 비밀번호 번경 요청 엔티티 + */ + Optional findByTokenValue(String tokenValue); + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepositoryCustom.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepositoryCustom.java new file mode 100644 index 0000000..1b298c6 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepositoryCustom.java @@ -0,0 +1,30 @@ +package org.egovframe.cloud.userservice.domain.user; + +/** + * org.egovframe.cloud.userservice.domain.user.UserFindPasswordRepositoryCustom + *

+ * 사용자 비밀번호 찾기 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    jooho       최초 생성
+ * 
+ */ +public interface UserFindPasswordRepositoryCustom { + + /** + * 다음 요청 번호 조회 + * + * @param emailAddr 이메일 주소 + * @return Integer 다음 요청 번호 + */ + Integer findNextRequestNo(String emailAddr); + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepositoryImpl.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepositoryImpl.java new file mode 100644 index 0000000..e756718 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserFindPasswordRepositoryImpl.java @@ -0,0 +1,46 @@ +package org.egovframe.cloud.userservice.domain.user; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.userservice.domain.user.UserFindPasswordRepositoryImpl + *

+ * 사용자 비밀번호 찾기 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/15 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/15    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class UserFindPasswordRepositoryImpl implements UserFindPasswordRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 다음 요청 번호 조회 + * + * @param emailAddr 이메일 주소 + * @return Integer 다음 요청 번호 + */ + @Override + public Integer findNextRequestNo(String emailAddr) { + return jpaQueryFactory + .select(QUserFindPassword.userFindPassword.userFindPasswordId.requestNo.max().add(1).coalesce(1)) + .from(QUserFindPassword.userFindPassword) + .where(QUserFindPassword.userFindPassword.userFindPasswordId.emailAddr.eq(emailAddr)) + .fetchOne(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepository.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepository.java new file mode 100644 index 0000000..43d5dff --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepository.java @@ -0,0 +1,37 @@ +package org.egovframe.cloud.userservice.domain.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + + +/** + * org.egovframe.cloud.userservice.domain.user.UserRepository + *

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

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/01    jaeyeolkim  최초 생성
+ * 
+ */ +public interface UserRepository extends JpaRepository, UserRepositoryCustom { + // email을 통해 이미 생성된 사용자인지 판단하기 위한 메소드 + Optional findByEmail(String email); + Optional findByUserId(String userId); + Optional findByRefreshToken(String refreshToken); + List findByEmailContains(String email); + Optional findByEmailAndUserName(String email, String userName); + Optional findByEmailAndUserIdNot(String email, String userId); + Optional findByGoogleId(String googleId); + Optional findByKakaoId(String kakaoId); + Optional findByNaverId(String naverId); +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepositoryCustom.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepositoryCustom.java new file mode 100644 index 0000000..ca52a57 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepositoryCustom.java @@ -0,0 +1,36 @@ +package org.egovframe.cloud.userservice.domain.user; + +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * org.egovframe.cloud.userservice.domain.user.UserRepositoryCustom + *

+ * 사용자 Querydsl 인터페이스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/23    jooho       최초 생성
+ * 
+ */ +public interface UserRepositoryCustom { + + /** + * 사용자 페이지 목록 조회 + * + * @param requestDto 사용자 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 사용자 목록 응답 DTO + */ + Page findPage(RequestDto requestDto, Pageable pageable); + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepositoryImpl.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepositoryImpl.java new file mode 100644 index 0000000..6e2aeab --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserRepositoryImpl.java @@ -0,0 +1,86 @@ +package org.egovframe.cloud.userservice.domain.user; + +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +/** + * org.egovframe.cloud.userservice.domain.user.UserRepositoryImpl + *

+ * 사용자 Querydsl 구현 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/23 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/23    jooho       최초 생성
+ * 
+ */ +@RequiredArgsConstructor +public class UserRepositoryImpl implements UserRepositoryCustom { + + /** + * DML 생성을위한 Querydsl 팩토리 클래스 + */ + private final JPAQueryFactory jpaQueryFactory; + + /** + * 사용자 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 사용자 목록 응답 DTO + */ + public Page findPage(RequestDto requestDto, Pageable pageable) { + QueryResults result = jpaQueryFactory + .select(Projections.constructor(UserListResponseDto.class, + QUser.user.userId, + QUser.user.userName, + QUser.user.email, + QUser.user.role, + QUser.user.userStateCode, + QUser.user.lastLoginDate, + QUser.user.loginFailCount + )) + .from(QUser.user) + .where(getBooleanExpression(requestDto)) + .orderBy(QUser.user.userName.asc(), QUser.user.email.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetchResults(); + + return new PageImpl<>(result.getResults(), pageable, result.getTotal()); + } + + /** + * 요청 DTO로 동적 검색 표현식 리턴 + * + * @param requestDto 요청 DTO + * @return BooleanExpression 검색 표현식 + */ + private BooleanExpression getBooleanExpression(RequestDto requestDto) { + if (requestDto.getKeyword() == null || "".equals(requestDto.getKeyword())) return null; + + switch (requestDto.getKeywordType()) { + case "userName": // 사용자 명 + return QUser.user.userName.containsIgnoreCase(requestDto.getKeyword()); + case "email": // 이메일 주소 + return QUser.user.email.containsIgnoreCase(requestDto.getKeyword()); + default: + return null; + } + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserStateCode.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserStateCode.java new file mode 100644 index 0000000..52a148d --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/domain/user/UserStateCode.java @@ -0,0 +1,48 @@ +package org.egovframe.cloud.userservice.domain.user; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * org.egovframe.cloud.userservice.domain.user.UserStateCode + * + * 사용자 상태 코드 열거형 상수 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/09/17 + * + *
+ * << 개정이력(Modification Information) >>
+ *
+ *    수정일       수정자              수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/09/17    jooho       최초 생성
+ * 
+ */ +@Getter +@RequiredArgsConstructor +public enum UserStateCode { + + WAIT("00", "대기"), + NORMAL("01", "정상"), + HALT("07", "정지"), + LEAVE("08", "탈퇴"), + DELETE("09", "삭제"); + + private final String key; + private final String title; + + /** + * 사용자 상태 코드로 상수 검색 + * + * @param key 사용자 상태 코드 + * @return UserStateCode 사용자 상태 코드 상수 + */ + public static UserStateCode findByKey(String key) { + return Arrays.stream(UserStateCode.values()).filter(c -> c.getKey().equals(key)).findAny().orElse(null); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/AuthorizationService.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/AuthorizationService.java new file mode 100644 index 0000000..6da6342 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/AuthorizationService.java @@ -0,0 +1,298 @@ +package org.egovframe.cloud.userservice.service.role; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; + +import org.egovframe.cloud.common.config.GlobalConstant; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.EntityNotFoundException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationListResponseDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationResponseDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationSaveRequestDto; +import org.egovframe.cloud.userservice.api.role.dto.AuthorizationUpdateRequestDto; +import org.egovframe.cloud.userservice.domain.role.Authorization; +import org.egovframe.cloud.userservice.domain.role.AuthorizationRepository; +import org.springframework.aop.framework.AopContext; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.AntPathMatcher; + +import lombok.RequiredArgsConstructor; + +/** + * org.egovframe.cloud.userservice.service.role.AuthorizationService + *

+ * 인가 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class AuthorizationService extends AbstractService { + + /** + * 인가 레파지토리 인터페이스 + */ + private final AuthorizationRepository authorizationRepository; + + /** + * 캐시 관리자 + */ + private final CacheManager cacheManager; + + /** + * 조회 조건에 일치하는 인가 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 인가 목록 응답 DTO + */ + public Page findPage(RequestDto requestDto, Pageable pageable) { + return authorizationRepository.findPage(requestDto, pageable); + } + + /** + * 권한의 인가 여부 확인 + * 사용자 서비스 시큐리티 필터에서 호출 + * + * @param request http 요청 + * @param authentication 시큐리티 인증 토큰 + * @return Boolean 인가 여부 + */ + public Boolean isAuthorization(HttpServletRequest request, Authentication authentication) { + List roles = authentication.getAuthorities().stream().map(GrantedAuthority::toString).collect(Collectors.toList()); + + List authorizationList = ((AuthorizationService) AopContext.currentProxy()).findByRoles(roles); + + return isContainMatch(authorizationList, request.getMethod(), GlobalConstant.USER_SERVICE_URI + request.getRequestURI()); + } + + /** + * 권한의 인가 전체 목록 조회 + * + * @param roles 권한 목록 + * @return List 인가 목록 + */ + @Cacheable(value = "cache-user-authorization-by-roles", key = "#roles") + public List findByRoles(List roles) { + return authorizationRepository.findByRoles(roles); + } + + /** + * 권한의 인가 여부 확인 + * gateway 에서 호출 + *

+ * Spring Cache는 Spring AOP를 이용해서 proxy로 동작하기 때문에 외부 method 호출만 인터셉트해서 작동하고 self-invocation의 경우 동작하지 않음 + * 스프링에서는 AspectJ를 권장하지만 Load-time Weaving 방식은 퍼포먼스 문제가 있고 + * Compile-time Weaving 방식은 컴파일 시 수행되는 라이브러리(lombok)와 충돌 문제가 있음 + * AopContext.currentProxy()를 이용해서 proxy로 호출하도록 함 - CacheConfig @EnableAspectJAutoProxy(exposeProxy=true) + * + * @param roles 권한 목록 + * @param httpMethod Http Method + * @param requestPath 요청 경로 + * @return Boolean 인가 여부 + */ + public Boolean isAuthorization(List roles, String httpMethod, String requestPath) { + List authorizationList = ((AuthorizationService) AopContext.currentProxy()).findByRoles(roles); + + return isContainMatch(authorizationList, httpMethod, requestPath); + } + + /** + * 사용자의 인가 전체 목록 조회 + * + * @param userId 사용자 id + * @return List 인가 목록 + */ + @Cacheable(value = "cache-user-authorization-by-userid", key = "#roles") + public List findByUserId(String userId) { + return authorizationRepository.findByUserId(userId); + } + + /** + * 사용자의 인가 여부 확인 + * gateway 에서 호출 + * + * @param userId 사용자 id + * @param httpMethod Http Method + * @param requestPath 요청 경로 + * @return Boolean 인가 여부 + */ + public Boolean isAuthorization(String userId, String httpMethod, String requestPath) { + List authorizationList = ((AuthorizationService) AopContext.currentProxy()).findByUserId(userId); + + return isContainMatch(authorizationList, httpMethod, requestPath); + } + + /** + * 인가 여부 체크 + * + * @param authorizationList 인가 목록 + * @param httpMethod Http Method + * @param requestPath 요청 경로 + * @return Boolean 인가 여부 + */ + private Boolean isContainMatch(List authorizationList, String httpMethod, String requestPath) { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + + for (AuthorizationListResponseDto dto : authorizationList) { + if (antPathMatcher.match(dto.getUrlPatternValue(), requestPath) && dto.getHttpMethodCode().equals(httpMethod)) { + return true; + } + } + + return false; + } + + /** + * 인가 단건 조회 + * + * @param authorizationNo 인가 번호 + * @return AuthorizationResponseDto 인가 응답 DTO + */ + public AuthorizationResponseDto findById(Integer authorizationNo) { + Authorization entity = findAuthorization(authorizationNo); + + return new AuthorizationResponseDto(entity); + } + + /** + * 인가 다음 정렬 순서 조회 + * + * @return Integer 다음 정렬 순서 + */ + public Integer findNextSortSeq() { + return authorizationRepository.findNextSortSeq(); + } + + /** + * 인가 등록 + * + * @param requestDto 인가 등록 요청 DTO + * @return AuthorizationResponseDto 인가 응답 DTO + */ + @Transactional + public AuthorizationResponseDto save(AuthorizationSaveRequestDto requestDto) { + // 동일한 정렬 순서가 존재할 경우 +1 + Optional authorization = authorizationRepository.findBySortSeq(requestDto.getSortSeq()); + if (authorization.isPresent()) { + authorizationRepository.updateSortSeq(requestDto.getSortSeq(), null, 1); + } + + // 등록 + Authorization entity = authorizationRepository.save(requestDto.toEntity()); + + clearAuthorizationCache(); + + return new AuthorizationResponseDto(entity); + } + + /** + * 인가 수정 + * + * @param authorizationNo 인가 번호 + * @param requestDto 인가 수정 요청 DTO + * @return AuthorizationResponseDto 인가 응답 DTO + */ + @Transactional + public AuthorizationResponseDto update(Integer authorizationNo, AuthorizationUpdateRequestDto requestDto) { + Authorization entity = findAuthorization(authorizationNo); + + // 정렬 순서가 변경된 경우 사이 구간 정렬 순서 조정 + Integer beforeSortSeq = entity.getSortSeq(); + Integer afterSortSeq = requestDto.getSortSeq(); + Integer startSortSeq = null; + Integer endSortSeq = null; + int increaseSortSeq = 0; + if (beforeSortSeq == null && afterSortSeq != null) { + startSortSeq = afterSortSeq; + increaseSortSeq = 1; + } else if (beforeSortSeq != null && afterSortSeq == null) { + startSortSeq = beforeSortSeq + 1; + increaseSortSeq = -1; + } else if (beforeSortSeq != null && afterSortSeq != null && beforeSortSeq.compareTo(afterSortSeq) != 0) { + if (beforeSortSeq.compareTo(afterSortSeq) > 0) { + startSortSeq = afterSortSeq; + endSortSeq = beforeSortSeq - 1; + increaseSortSeq = 1; + } else { + startSortSeq = beforeSortSeq + 1; + endSortSeq = afterSortSeq; + increaseSortSeq = -1; + } + } + if (startSortSeq != null || endSortSeq != null) { + authorizationRepository.updateSortSeq(startSortSeq, endSortSeq, increaseSortSeq); + } + + // 수정 + entity.update(requestDto.getAuthorizationName(), requestDto.getUrlPatternValue(), requestDto.getHttpMethodCode(), requestDto.getSortSeq()); + + clearAuthorizationCache(); + + return new AuthorizationResponseDto(entity); + } + + /** + * 인가 삭제 + * 권한 인가도 같이 삭제됨 + * + * @param authorizationNo 인가 번호 + */ + @Transactional + public void delete(Integer authorizationNo) { + Authorization entity = findAuthorization(authorizationNo); + + // 삭제 + authorizationRepository.delete(entity); + + // 삭제한 데이터보다 정렬 순서가 더 큰 데이터 -1 + authorizationRepository.updateSortSeq(entity.getSortSeq() + 1, null, -1); + + clearAuthorizationCache(); + } + + /** + * 인가 번호로 인가 엔티티 조회 + * + * @param authorizationNo 인가 번호 + * @return Authorization 인가 엔티티 + */ + private Authorization findAuthorization(Integer authorizationNo) { + return authorizationRepository.findById(authorizationNo) + .orElseThrow(() -> new EntityNotFoundException(getMessage("valid.notexists.format", new Object[]{getMessage("authorization")}))); + } + + /** + * 인가 조회 캐시 클리어 + */ + private void clearAuthorizationCache() { + Cache useridCache = cacheManager.getCache("cache-user-authorization-by-userid"); + if (useridCache != null) useridCache.clear(); + Cache rolesCache = cacheManager.getCache("cache-user-authorization-by-roles"); + if (rolesCache != null) rolesCache.clear(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/RoleAuthorizationService.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/RoleAuthorizationService.java new file mode 100644 index 0000000..4f29074 --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/RoleAuthorizationService.java @@ -0,0 +1,120 @@ +package org.egovframe.cloud.userservice.service.role; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationDeleteRequestDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListRequestDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationListResponseDto; +import org.egovframe.cloud.userservice.api.role.dto.RoleAuthorizationSaveRequestDto; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorization; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorizationRepository; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.userservice.service.role.RoleAuthorizationService + *

+ * 권한 인가 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class RoleAuthorizationService extends AbstractService { + + /** + * 권한 인가 레파지토리 인터페이스 + */ + private final RoleAuthorizationRepository roleAuthorizationRepository; + + /** + * 캐시 관리자 + */ + private final CacheManager cacheManager; + + /** + * 조회 조건에 일치하는 권한 인가 페이지 목록 조회 + * + * @param requestDto 권한 인가 목록 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 권한 인가 목록 응답 DTO + */ + public Page findPageAuthorizationList(RoleAuthorizationListRequestDto requestDto, Pageable pageable) { + if (requestDto.getRoleId() == null || "".equals(requestDto.getRoleId())) { + return new PageImpl<>(Collections.emptyList(), pageable, 0); + } + + return roleAuthorizationRepository.findPageAuthorizationList(requestDto, pageable); + } + + /** + * 권한 인가 다건 등록 + * + * @param requestDtoList 권한 인가 등록 요청 DTO List + * @return List 등록 권한 인가 목록 + */ + @Transactional + public List save(List requestDtoList) { + List saveEntityList = requestDtoList.stream() + .map(RoleAuthorizationSaveRequestDto::toEntity) + .collect(Collectors.toList()); + + List savedEntityList = roleAuthorizationRepository.saveAll(saveEntityList); + + clearAuthorizationCache(); + + return savedEntityList.stream() + .map(m -> RoleAuthorizationListResponseDto.builder() + .roleId(m.getRoleAuthorizationId().getRoleId()) + .authorizationNo(m.getRoleAuthorizationId().getAuthorizationNo()) + .build()) + .collect(Collectors.toList()); + } + + /** + * 권한 인가 다건 삭제 + * + * @param requestDtoList 권한 인가 삭제 요청 DTO List + */ + @Transactional + public void delete(List requestDtoList) { + List deleteEntityList = requestDtoList.stream() + .map(RoleAuthorizationDeleteRequestDto::toEntity) + .collect(Collectors.toList()); + + roleAuthorizationRepository.deleteAll(deleteEntityList); + + clearAuthorizationCache(); + } + + /** + * 인가 조회 캐시 클리어 + */ + private void clearAuthorizationCache() { + Cache useridCache = cacheManager.getCache("cache-user-authorization-by-userid"); + if (useridCache != null) useridCache.clear(); + Cache rolesCache = cacheManager.getCache("cache-user-authorization-by-roles"); + if (rolesCache != null) rolesCache.clear(); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/RoleService.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/RoleService.java new file mode 100644 index 0000000..61dabbd --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/role/RoleService.java @@ -0,0 +1,72 @@ +package org.egovframe.cloud.userservice.service.role; + +import lombok.RequiredArgsConstructor; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.userservice.api.role.dto.RoleListResponseDto; +import org.egovframe.cloud.userservice.domain.role.RoleRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.userservice.service.role.RoleService + *

+ * 권한 서비스 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    jooho       최초 생성
+ * 
+ */ +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class RoleService extends AbstractService { + + /** + * 권한 레파지토리 인터페이스 + */ + private final RoleRepository roleRepository; + + /** + * 조회 조건에 일치하는 권한 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 권한 목록 응답 DTO + */ + public Page findPage(RequestDto requestDto, Pageable pageable) { + return roleRepository.findPage(requestDto, pageable); + } + + /** + * 권한 전체 조회 + * + * @param sort 정렬 + * @return List 권한 목록 응답 DTO + */ + public List findAllBySort(Sort sort) { + return roleRepository.findAll(sort).stream() + .map(m -> RoleListResponseDto.builder() + .roleId(m.getRoleId()) + .roleName(m.getRoleName()) + .roleContent(m.getRoleContent()) + .createdDate(m.getCreatedDate()) + .build()) + .collect(Collectors.toList()); + } + +} diff --git a/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/user/UserService.java b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/user/UserService.java new file mode 100644 index 0000000..f13280b --- /dev/null +++ b/backend/user-service/src/main/java/org/egovframe/cloud/userservice/service/user/UserService.java @@ -0,0 +1,861 @@ +package org.egovframe.cloud.userservice.service.user; + +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.egovframe.cloud.common.domain.Role; +import org.egovframe.cloud.common.dto.RequestDto; +import org.egovframe.cloud.common.exception.BusinessMessageException; +import org.egovframe.cloud.common.service.AbstractService; +import org.egovframe.cloud.common.util.LogUtil; +import org.egovframe.cloud.userservice.api.user.dto.*; +import org.egovframe.cloud.userservice.config.UserPasswordChangeEmailTemplate; +import org.egovframe.cloud.userservice.config.dto.SocialUser; +import org.egovframe.cloud.userservice.domain.log.LoginLog; +import org.egovframe.cloud.userservice.domain.log.LoginLogRepository; +import org.egovframe.cloud.userservice.domain.user.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.*; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * org.egovframe.cloud.userservice.service.user.UserService + *

+ * 사용자 정보 서비스 + * + * @author 표준프레임워크센터 jaeyeolkim + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jaeyeolkim  최초 생성
+ * 
+ */ +@Slf4j +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class UserService extends AbstractService implements UserDetailsService { + + /** + * 구글 클라이언트 ID + */ + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String GOOGLE_CLIENT_ID; + + /** + * 카카오 사용자 정보 URL + */ + @Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}") + private String KAKAO_USER_INFO_URI; + + /** + * 네이버 사용자 정보 URL + */ + @Value("${spring.security.oauth2.client.provider.naver.user-info-uri}") + private String NAVER_USER_INFO_URI; + + /** + * REST Template + */ + private final RestTemplate restTemplate; + + private final UserRepository userRepository; + private final UserFindPasswordRepository userFindPasswordRepository; + private final BCryptPasswordEncoder passwordEncoder; + private final LoginLogRepository loginLogRepository; + + /** + * 자바 메일 전송 인터페이스 + */ + private final JavaMailSender javaMailSender; + + /** + * 조회 조건에 일치하는 사용자 페이지 목록 조회 + * + * @param requestDto 요청 DTO + * @param pageable 페이지 정보 + * @return Page 페이지 사용자 목록 응답 DTO + */ + public Page findPage(RequestDto requestDto, Pageable pageable) { + return userRepository.findPage(requestDto, pageable); + } + + /** + * 사용자 등록 + * + * @param requestDto + * @return + */ + @Transactional + public Long save(UserSaveRequestDto requestDto) { + return userRepository.save(requestDto.toEntity(passwordEncoder)).getId(); + } + + /** + * 사용자 수정 + * + * @param userId 사용자 id + * @param requestDto 사용자 수정 요청 DTO + * @return String 사용자 id + */ + @Transactional + public String update(String userId, UserUpdateRequestDto requestDto) { + User user = getUserByUserId(userId); + + final String password = requestDto.getPassword() != null && !"".equals(requestDto.getPassword()) + ? passwordEncoder.encode(requestDto.getPassword()) + : user.getEncryptedPassword(); + + user.update(requestDto.getUserName(), requestDto.getEmail(), password, + requestDto.getRoleId(), requestDto.getUserStateCode()); + + return userId; + } + + /** + * 사용자 refresh token 정보를 필드에 입력한다 + * + * @param userId + * @param updateRefreshToken + * @return + */ + @Transactional + public String updateRefreshToken(String userId, String updateRefreshToken) { + User user = userRepository.findByUserId(userId) + .orElseThrow(() -> new UsernameNotFoundException("해당 사용자가 없습니다.")); + + user.updateRefreshToken(updateRefreshToken); + + return user.getRoleKey(); + } + + /** + * 토큰으로 사용자를 찾아 반환한다. + * + * @param refreshToken + * @return + */ + public User findByRefreshToken(String refreshToken) { + return userRepository.findByRefreshToken(refreshToken) + .orElseThrow(() -> new UsernameNotFoundException("해당 사용자가 없습니다.")); + } + + /** + * 아이디로 사용자를 찾아 반환한다. + * + * @param userId + * @return + */ + public UserResponseDto findByUserId(String userId) { + User user = userRepository.findByUserId(userId) + .orElseThrow(() -> new UsernameNotFoundException("해당 사용자가 없습니다.")); + + return new UserResponseDto(user); + } + + /** + * 이메일로 사용자를 찾아 반환한다. + * + * @param email + * @return + */ + public UserResponseDto findByEmail(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("해당 사용자가 없습니다.")); + + return new UserResponseDto(user); + } + + /** + * 모든 사용자를 생성일 역순으로 정렬하여 조회하여 List 형태로 반환한다. + * + * @return + */ + public List findAllDesc() { + return userRepository.findAll(Sort.by(Sort.Direction.DESC, "createdDate")).stream() + .map(UserListResponseDto::new) // User의 Stream을 map을 통해 UserListResponseDto로 변환한다. 실제로 .map(user -> new UserListResponseDto(user)) 과 같다. + .collect(Collectors.toList()); + } + + /** + * SecurityConfig > configure > UserDetailsService 메소드에서 호출된다. + * 스프링 시큐리티에 의해 로그인 대상 사용자의 패스워드와 권한 정보를 DB에서 조회하여 UserDetails 를 리턴한다. + * + * @param email + * @return + * @throws UsernameNotFoundException + */ + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + log.info("loadUserByUsername! email={}", email); + // 로그인 실패시 이메일 계정을 로그에 남기기 위해 세팅하고 unsuccessfulAuthentication 메소드에서 받아서 로그에 입력한다. + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + request.setAttribute("email", email); + + // UsernameNotFoundException 을 던지면 AbstractUserDetailsAuthenticationProvider 에서 BadCredentialsException 으로 처리하기 때문에 IllegalArgumentException 을 발생시켰다. + // 사용자가 없는 것인지 패스워드가 잘못된 것인지 구분하기 위함이다. + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException(getMessage("err.user.notexists"))); + log.info("{} 사용자 존재함", user); + + if (!UserStateCode.NORMAL.getKey().equals(user.getUserStateCode())) { + throw new IllegalArgumentException(getMessage("err.user.state.cantlogin")); + } + + // 로그인 유저의 권한 목록 주입 + ArrayList authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(user.getRoleKey())); + + if (user.isSocialUser() && user.getEncryptedPassword() == null || "".equals(user.getEncryptedPassword())) { // 소셜 회원이고 비밀번호가 등록되지 않은 경우 + return new SocialUser(user.getEmail(), authorities); + } else { + return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getEncryptedPassword(), authorities); + } + } + + /** + * 로그인 후처리 + * + * @param siteId 사이트 id + * @param email 이메일 + * @param successAt 성공 여부 + * @param failContent 실패 내용 + */ + @Transactional + public void loginCallback(Long siteId, String email, Boolean successAt, String failContent) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException(getMessage("err.user.notexists"))); + + if (Boolean.TRUE.equals(successAt)) { + user.successLogin(); + } else { + user.failLogin(); + } + + // 로그인 로그 입력 + loginLogRepository.save( + LoginLog.builder() + .siteId(siteId) + .email(email) + .remoteIp(LogUtil.getUserIp()) + .successAt(successAt) + .failContent(failContent) + .build() + ); + } + + /** + * 이메일 중복 확인 + * + * @param email 이메일 + * @param userId 사용자 id + * @return Boolean 중복 여부 + */ + public Boolean existsEmail(String email, String userId) { + if (email == null || "".equals(email)) { + throw new BusinessMessageException(getMessage("valid.required.format", new Object[]{getMessage("user.email")})); + } + + + if (userId == null || "".equals(userId)) { + return userRepository.findByEmail(email).isPresent(); + } else { + return userRepository.findByEmailAndUserIdNot(email, userId).isPresent(); + } + } + + /** + * 사용자 회원 가입 + * + * @param requestDto 사용자 가입 요청 DTO + * @return Boolean 성공 여부 + */ + @Transactional + public Boolean join(UserJoinRequestDto requestDto) { + boolean exists = existsEmail(requestDto.getEmail(), null); + if (exists) { + throw new BusinessMessageException(getMessage("msg.join.email.exists")); + } + + userRepository.save(requestDto.toEntity(passwordEncoder)); + + return true; + } + + /** + * 사용자 비밀번호 찾기 + * + * @param requestDto 사용자 비밀번호 찾기 등록 요청 DTO + * @return Boolean 메일 전송 여부 + */ + @Transactional + public Boolean findPassword(UserFindPasswordSaveRequestDto requestDto) { + final String emailAddr = requestDto.getEmailAddr(); + + Optional user = userRepository.findByEmailAndUserName(emailAddr, requestDto.getUserName()); + if (!user.isPresent()) { + throw new BusinessMessageException(getMessage("err.user.notexists")); + } + User entity = user.get(); + + // 이메일 전송 + try { + final String mainUrl = requestDto.getMainUrl(); + final String tokenValue = UUID.randomUUID().toString().replaceAll("-", ""); + + final String subject = getMessage("email.user.password.title"); + //final String text = getMessage("email.user.password.content"); // varchar(2000) + final String text = UserPasswordChangeEmailTemplate.html; + final String userName = entity.getUserName(); + final String changePasswordUrl = requestDto.getChangePasswordUrl() + "?token=" + tokenValue; + + MimeMessage message = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message); + + helper.setTo(emailAddr); + helper.setSubject(subject); + helper.setText(String.format(text, mainUrl, userName, changePasswordUrl), true); // String.format에서 %를 쓰려면 %%로 + + log.info("start send change password email: " + emailAddr); + javaMailSender.send(message); + + Integer requestNo = userFindPasswordRepository.findNextRequestNo(emailAddr); + UserFindPassword userFindPassword = requestDto.toEntity(requestNo, tokenValue); + + userFindPasswordRepository.save(userFindPassword); + + log.info("end send change password email - emailAddr: " + emailAddr + ", tokenValue: " + tokenValue); + } catch (MessagingException e) { + e.printStackTrace(); + String errorMessage = getMessage("err.user.find.password"); + log.error(errorMessage + ": " + e.getMessage()); + throw new BusinessMessageException(errorMessage); + } catch (Exception e) { + e.printStackTrace(); + String errorMessage = getMessage("err.user.find.password"); + log.error(errorMessage + ": " + e.getMessage()); + throw new BusinessMessageException(errorMessage); + } + + return true; + } + + /** + * 사용자 비밀번호 찾기 유효성 확인 + * + * @param tokenValue 토큰 값 + * @return Boolean 유효 여부 + */ + @Transactional + public Boolean validPassword(String tokenValue) { + if (tokenValue == null || "".equals(tokenValue)) { + throw new BusinessMessageException(getMessage("err.invalid.input.value")); + } + + Optional userPassword = userFindPasswordRepository.findByTokenValue(tokenValue); + if (userPassword.isPresent()) { + UserFindPassword entity = userPassword.get(); + + boolean isExpired = LocalDateTime.now().isAfter(entity.getCreatedDate().plusHours(1)); // 1시간 후 만료 + if (Boolean.FALSE.equals(entity.getChangeAt()) && !isExpired) return true; + } + + return false; + } + + /** + * 사용자 비밀번호 찾기 변경 + * + * @param requestDto 사용자 비밀번호 수정 요청 DTO + * @return Boolean 수정 여부 + */ + @Transactional + public Boolean changePassword(UserFindPasswordUpdateRequestDto requestDto) { + final String tokenValue = requestDto.getTokenValue(); + + Optional userPassword = userFindPasswordRepository.findByTokenValue(tokenValue); + + if (!userPassword.isPresent()) { + throw new BusinessMessageException(getMessage("err.user.change.password")); + } + + UserFindPassword entity = userPassword.get(); + if (Boolean.TRUE.equals(entity.getChangeAt()) || LocalDateTime.now().isAfter(entity.getCreatedDate().plusHours(1))) { // 1시간 후 만료 + throw new BusinessMessageException(getMessage("err.user.change.password")); + } + + User user = userRepository.findByEmail(entity.getUserFindPasswordId().getEmailAddr()) + .orElseThrow(() -> new UsernameNotFoundException("해당 사용자가 없습니다.")); + + user.updatePassword(passwordEncoder.encode(requestDto.getPassword())); // 비밀번호 수정 + + entity.updateChangeAt(Boolean.TRUE); // 변경 완료 + + return true; + } + + /** + * 사용자 비밀번호 변경 + * + * @param userId 사용자 id + * @param requestDto 사용자 비밀번호 변경 요청 DTO + * @return Boolean 수정 여부 + */ + @Transactional + public Boolean updatePassword(String userId, UserPasswordUpdateRequestDto requestDto) { + User entity = findUserVerify(userId, requestDto); + + entity.updatePassword(passwordEncoder.encode(requestDto.getNewPassword())); // 비밀번호 수정 + + return true; + } + + /** + * 사용자 비밀번호 확인 + * + * @param userId 사용자 id + * @param password 비밀번호 + * @return Boolean 일치 여부 + */ + public Boolean matchPassword(String userId, String password) { + try { + findUserVerifyPassword(userId, password); + } catch (BusinessMessageException e) { + return false; + } catch (Exception e) { + return false; + } + + return true; + } + + /** + * 사용자 id로 조회 + * + * @param userId 사용자 id + * @return User 사용자 엔티티 + */ + private User getUserByUserId(String userId) { + Optional user = userRepository.findByUserId(userId); + if (!user.isPresent()) { + throw new BusinessMessageException(getMessage("err.user.notexists")); + } + + return user.get(); + } + + /** + * 사용자 조회, 비밀번호 검증 + * + * @param userId 사용자 id + * @param password 비밀번호 + * @return User 사용자 엔티티 + */ + private User findUserVerifyPassword(String userId, String password) { + User entity = getUserByUserId(userId); + + if (!passwordEncoder.matches(password, entity.getEncryptedPassword())) { // 소셜 사용자가 아닌 경우 비밀번호 확인 + throw new BusinessMessageException(getMessage("err.user.password.notmatch")); + } + + return entity; + } + + /** + * 사용자 정보 수정 + * + * @param userId 사용자 id + * @param requestDto 사용자 정보 수정 요청 DTO + * @return String 사용자 id + */ + @Transactional + public String updateInfo(String userId, UserUpdateInfoRequestDto requestDto) { + User user = findUserVerify(userId, requestDto); + + user.updateInfo(requestDto.getUserName(), requestDto.getEmail()); + + return userId; + } + + /** + * 사용자 회원탈퇴 + * + * @param userId 사용자 id + * @param requestDto 회원 탈퇴 요청 DTO + * @return User 사용자 엔티티 + */ + @Transactional + public Boolean leave(String userId, UserVerifyRequestDto requestDto) { + User entity = findUserVerify(userId, requestDto); + + entity.updateUserStateCode(UserStateCode.LEAVE.getKey()); + + return true; + } + + /** + * 사용자 검증 및 조회 + * + * @param userId 사용자 id + * @param requestDto 회원 탈퇴 요청 DTO + * @return User 사용자 엔티티 + */ + private User findUserVerify(String userId, UserVerifyRequestDto requestDto) { + User user = null; + if ("password".equals(requestDto.getProvider())) { + user = findUserVerifyPassword(userId, requestDto.getPassword()); + } else { + user = findSocialUserByToken(requestDto.getProvider(), requestDto.getToken()); + + if (user == null) { + throw new BusinessMessageException(getMessage("err.user.socail.find")); + } + if (!userId.equals(user.getUserId())) { + throw new BusinessMessageException(getMessage("err.unauthorized")); + } + } + + return user; + } + + /** + * 사용자 삭제 + * + * @param userId 사용자 id + * @return User 사용자 엔티티 + */ + @Transactional + public Boolean delete(String userId) { + User user = getUserByUserId(userId); + + user.updateUserStateCode(UserStateCode.DELETE.getKey()); + + return true; + } + + /** + * OAuth 사용자 검색 + * + * @param requestDto 사용자 로그인 요청 DTO + * @return UserLoginRequestDto 사용자 로그인 요청 DTO + */ + @Transactional + public UserResponseDto loadUserBySocial(UserLoginRequestDto requestDto) { + String[] userInfo = getSocialUserInfo(requestDto.getProvider(), requestDto.getToken()); + + UserResponseDto userDto = getAndSaveSocialUser(requestDto.getProvider(), userInfo[0], userInfo[1], userInfo[2]); + + if (userDto == null) { + throw new BusinessMessageException(getMessage("err.user.join.social")); + } + if (!UserStateCode.NORMAL.getKey().equals(userDto.getUserStateCode())) { + throw new BusinessMessageException(getMessage("err.user.state.cantlogin")); + } + + return userDto; + } + + /** + * 토큰으로 사용자 엔티티 조회 + * + * @param provider 공급자 + * @param token 토큰 + * @return User 사용자 엔티티 + */ + private User findSocialUserByToken(String provider, String token) { + String[] userInfo = getSocialUserInfo(provider, token); + + return findSocialUser(provider, userInfo[0]); + } + + /** + * 토큰으로 소셜 사용자 정보 조회 + * + * @param provider 공급자 + * @param token 토큰 + * @return String[] 소셜 사용자 정보 + */ + private String[] getSocialUserInfo(String provider, String token) { + String[] userInfo = null; + + switch (provider) { + case "google": + userInfo = getGoogleUserInfo(token); + break; + case "naver": + userInfo = getNaverUserInfo(token); + break; + case "kakao": + userInfo = getKakaoUserInfo(token); + break; + default: + break; + } + + if (userInfo == null) throw new BusinessMessageException(getMessage("err.user.social.get")); + + return userInfo; + } + + /** + * 구글 사용자 정보 조회 + * + * @param token 토큰 + * @return String[] 구글 사용자 정보 + */ + private String[] getGoogleUserInfo(String token) { + try { + HttpTransport transport = new NetHttpTransport(); + GsonFactory gsonFactory = new GsonFactory(); + + GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, gsonFactory) + .setAudience(Collections.singletonList(GOOGLE_CLIENT_ID)) + .build(); + + GoogleIdToken idToken = verifier.verify(token); + + GoogleIdToken.Payload payload = idToken.getPayload(); + log.info("google oauth2: {}", payload.toString()); + + return new String[]{ + payload.getSubject(), + payload.getEmail(), + (String) payload.get("name") + }; + } catch (GeneralSecurityException e) { + throw new BusinessMessageException(getMessage("err.user.social.get")); + } catch (IOException e) { + throw new BusinessMessageException(getMessage("err.user.social.get")); + } catch (Exception e) { + throw new BusinessMessageException(getMessage("err.user.social.get")); + } + } + + /** + * 네이버 사용자 정보 조회 + * + * @param token 토큰 + * @return String[] 네이버 사용자 정보 + */ + private String[] getNaverUserInfo(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + token); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + HttpEntity> request = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.exchange(NAVER_USER_INFO_URI, HttpMethod.GET, request, String.class); + + if (response.getBody() != null && !"".equals(response.getBody())) { + JsonElement element = JsonParser.parseString(response.getBody()); + JsonObject object = element.getAsJsonObject(); + log.info("naver oauth2: {}", object.toString()); + + if (object.get("resultcode") != null && "00".equals(object.get("resultcode").getAsString())) { + return new String[]{ + object.get("response").getAsJsonObject().get("id").getAsString(), + object.get("response").getAsJsonObject().get("email").getAsString(), + object.get("response").getAsJsonObject().get("name").getAsString() + }; + } + } + + return null; + } + + /** + * 카카오 사용자 정보 조회 + * + * @param token 토큰 + * @return String[] 카카오 사용자 정보 + */ + private String[] getKakaoUserInfo(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + token); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + HttpEntity> request = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.exchange(KAKAO_USER_INFO_URI, HttpMethod.GET, request, String.class); + + if (response.getBody() != null && !"".equals(response.getBody())) { + JsonElement element = JsonParser.parseString(response.getBody()); + JsonObject object = element.getAsJsonObject(); + log.info("kakao oauth2: {}", object.toString()); + + if (object.get("id") != null && !"".equals(object.get("id").getAsString())) { + return new String[]{ + object.get("id").getAsString(), + object.get("kakao_account").getAsJsonObject().get("email").getAsString(), + object.get("kakao_account").getAsJsonObject().get("profile").getAsJsonObject().get("nickname").getAsString() + }; + } + } + + return null; + } + + /** + * 소셜 사용자 엔티티 조회 + * + * @param providerCode 공급자 코드 + * @param providerId 공급자 id + * @return User 사용자 엔티티 + */ + private User findSocialUser(String providerCode, String providerId) { + Optional user; + + // 공급자 id로 조회 + switch (providerCode) { + case "google": + user = userRepository.findByGoogleId(providerId); + break; + case "kakao": + user = userRepository.findByKakaoId(providerId); + break; + case "naver": + user = userRepository.findByNaverId(providerId); + break; + default: + user = Optional.empty(); + break; + } + + return user.orElse(null); + } + + /** + * 소셜 사용자 엔티티 조회 + * 등록되어 있지 않은 경우 사용자 등록 + * + * @param providerCode 공급자 코드 + * @param providerId 공급자 id + * @param email 이메일 + * @param userName 사용자 명 + * @return UserLoginRequestDto 사용자 로그인 요청 DTO + */ + private UserResponseDto getAndSaveSocialUser(String providerCode, String providerId, String email, String userName) { + User user = findSocialUser(providerCode, providerId); + + // 이메일로 조회 + // 공급자에서 동일한 이메일을 사용할 수 있고 + // 현재 시스템 구조 상 이메일을 사용자 식별키로 사용하고 있어서 이메일로 사용자를 한번 더 검색한다. + if (user == null) { + user = userRepository.findByEmail(email).orElse(null); + + // 공급자 id로 조회되지 않지만 이메일로 조회되는 경우 공급자 id 등록 + if (user != null) { + switch (providerCode) { + case "google": + user = user.updateGoogleId(providerId); + break; + case "kakao": + user = user.updateKakaoId(providerId); + break; + case "naver": + user = user.updateNaverId(providerId); + break; + default: + break; + } + } + } + + if (user == null) { + // 사용자 등록 + final String userId = UUID.randomUUID().toString(); + //final String password = makeRandomPassword(); // 임의 비밀번호 생성 시 복호화 불가능 + + User.UserBuilder userBuilder = User.builder() + .email(email) // 100byte + //.encryptedPassword(passwordEncoder.encode(password)) // 100 byte + .userName(userName) + .userId(userId) + .role(Role.USER) + .userStateCode(UserStateCode.NORMAL.getKey()); + + switch (providerCode) { + case "google": + user = userBuilder.googleId(providerId).build(); + break; + case "kakao": + user = userBuilder.kakaoId(providerId).build(); + break; + case "naver": + user = userBuilder.naverId(providerId).build(); + break; + default: + break; + } + + if (user != null) { + userRepository.save(user); + } + } + + return user == null ? null : new UserResponseDto(user); + } + + /** + * 임의 비밀번호 10자리 생성 + * + * @return String 비밀번호 + */ + private String makeRandomPassword() { + char[] terms = new char[]{ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '!', '@', '#', '$', '%', '^', '&', '*', '(', ')'}; + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < 10; i++) { + int index = (int) (Math.random() * terms.length); + sb.append(terms[index]); + } + + return sb.toString(); + } + +} \ No newline at end of file diff --git a/backend/user-service/src/main/resources/application-oauth.yml b/backend/user-service/src/main/resources/application-oauth.yml new file mode 100644 index 0000000..513b003 --- /dev/null +++ b/backend/user-service/src/main/resources/application-oauth.yml @@ -0,0 +1,42 @@ +# oauth2 를 사용하기 위해서는 아래의 TODO 를 등록해야 함 +spring: + security: + oauth2: + client: + registration: + # /oauth2/authorization/google + google: + client-id: google_client_id # TODO + client-secret: google_client_secret # TODO + scope: profile,email + # 네이버는 Spring Security를 공식 지원하지 않기 때문에 CommonOAuth2Provider 에서 해주는 값들을 수동으로 입력한다. + # /oauth2/authorization/naver + naver: + client-id: naver_client_id # TODO + client-secret: naver_client_secret # TODO + redirect_uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" + authorization_grant_type: authorization_code + scope: name,email,profile_image + client-name: Naver + # /oauth2/authorization/kakao + kakao: + client-id: kakao_client_id # TODO + client-secret: kakao_client_secret # TODO + redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" + client-authentication-method: POST + authorization-grant-type: authorization_code + scope: profile_nickname, account_email + client-name: Kakao + provider: + naver: + authorization_uri: https://nid.naver.com/oauth2.0/authorize + token_uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + # 기준이 되는 user_name 의 이름을 네이버에서는 response로 지정해야한다. (네이버 회원 조회시 반환되는 JSON 형태 때문이다) + # response를 user_name으로 지정하고 이후 자바 코드로 response의 id를 user_name으로 지정한다. (스프링 시큐리티에서 하위 필드를 명시할 수 없기 때문) + user_name_attribute: response + kakao: + authorization_uri: https://kauth.kakao.com/oauth/authorize + token_uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user_name_attribute: id diff --git a/backend/user-service/src/main/resources/application.yml b/backend/user-service/src/main/resources/application.yml new file mode 100644 index 0000000..6a703b2 --- /dev/null +++ b/backend/user-service/src/main/resources/application.yml @@ -0,0 +1,66 @@ +server: + port: 0 # random port + +spring: + application: + name: user-service + profiles: + group: + default: oauth + docker: oauth + cf: oauth + k8s: oauth + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL57Dialect + storage_engine: innodb + format_sql: true + default_batch_fetch_size: 1000 + jdbc: + batch_size: 1000 + order_inserts: true + order_updates: true + show-sql: true + cache: + jcache: + config: classpath:ehcache.xml + +# config server actuator +management: + endpoints: + web: + exposure: + include: refresh, health, beans + health: + mail: + enabled: false + +# @TODO application-oauth.yml +# spring: +# security: +# oauth2: +# client: +# registration: +# google: +# client-id: @TODO https://console.cloud.google.com +# client-secret: @TODO +# scope: profile,email +# # 네이버는 Spring Security를 공식 지원하지 않기 때문에 CommonOAuth2Provider 에서 해주는 값들을 수동으로 입력한다. +# naver: +# client-id: @TODO https://developers.naver.com/apps/#/register?api=nvlogin +# client-secret: @TODO +# redirect_uri_template: "{baseUrl}/{action}/oauth2/code/{registrationId}" +# authorization_grant_type: authorization_code +# scope: name,email,profile_image +# client-name: Naver +# provider: +# naver: +# authorization_uri: https://nid.naver.com/oauth2.0/authorize +# token_uri: https://nid.naver.com/oauth2.0/token +# user-info-uri: https://openapi.naver.com/v1/nid/me +# # 기준이 되는 user_name 의 이름을 네이버에서는 response로 지정해야한다. (네이버 회원 조회시 반환되는 JSON 형태 때문이다) +# # response를 user_name으로 지정하고 이후 자바 코드로 response의 id를 user_name으로 지정한다. (스프링 시큐리티에서 하위 필드를 명시할 수 없기 때문) +# user_name_attribute: response diff --git a/backend/user-service/src/main/resources/bootstrap.yml b/backend/user-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..a1ad056 --- /dev/null +++ b/backend/user-service/src/main/resources/bootstrap.yml @@ -0,0 +1,8 @@ +spring: + cloud: + config: + uri: http://localhost:8888 + name: user-service # user-service.yml이 있으면 불러오게 된다 +# name: config-service # config-service의 application.yml 을 불러오게 된다 +# profiles: +# active: prod # application-prod.yml \ No newline at end of file diff --git a/backend/user-service/src/main/resources/ehcache.xml b/backend/user-service/src/main/resources/ehcache.xml new file mode 100644 index 0000000..2e6c3d0 --- /dev/null +++ b/backend/user-service/src/main/resources/ehcache.xml @@ -0,0 +1,51 @@ + + + + java.util.List + java.util.List + + 3 + + + + + org.egovframe.cloud.userservice.config.CacheEventLogger + ASYNCHRONOUS + UNORDERED + CREATED + EXPIRED + + + + + 2000 + 1 + + + + + java.lang.String + java.util.List + + 3 + + + + + org.egovframe.cloud.userservice.config.CacheEventLogger + ASYNCHRONOUS + UNORDERED + CREATED + EXPIRED + + + + + 2000 + 1 + + + + \ No newline at end of file diff --git a/backend/user-service/src/main/resources/logback-spring.xml b/backend/user-service/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..03aa0f4 --- /dev/null +++ b/backend/user-service/src/main/resources/logback-spring.xml @@ -0,0 +1,34 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n + + + + + + + + + + + + + + + + + ${destination} + + {"app.name":"${app_name}"} + + + + + + + + + \ No newline at end of file diff --git a/backend/user-service/src/test/java/org/egovframe/cloud/userservice/UserServiceApplicationTests.java b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/UserServiceApplicationTests.java new file mode 100644 index 0000000..18adbf4 --- /dev/null +++ b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/UserServiceApplicationTests.java @@ -0,0 +1,13 @@ +package org.egovframe.cloud.userservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/UserApiControllerTest.java b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/UserApiControllerTest.java new file mode 100644 index 0000000..5613f0b --- /dev/null +++ b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/UserApiControllerTest.java @@ -0,0 +1,143 @@ +package org.egovframe.cloud.userservice.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.egovframe.cloud.common.domain.Role; +import org.egovframe.cloud.userservice.api.user.dto.UserResponseDto; +import org.egovframe.cloud.userservice.api.user.dto.UserSaveRequestDto; +import org.egovframe.cloud.userservice.api.user.dto.UserUpdateRequestDto; +import org.egovframe.cloud.userservice.domain.user.User; +import org.egovframe.cloud.userservice.domain.user.UserRepository; +import org.egovframe.cloud.userservice.service.user.UserService; +import org.json.JSONObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.client.RestClientException; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class UserApiControllerTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Autowired + private TestRestTemplate restTemplate; + +// private static final String USER_SERVICE_URL = "http://localhost:8000/user-service"; + private static final String TEST_COM = "@test.com"; + private static final String TEST_EMAIL = System.currentTimeMillis() + TEST_COM; + private static final String TEST_PASSWORD = "test1234!"; + + @Test + @Order(Integer.MAX_VALUE) + public void cleanup() throws Exception { + // 테스트 후 데이터 삭제 + List users = userRepository.findByEmailContains("test.com"); + users.forEach(user -> userRepository.deleteById(user.getId())); + } + + @Test + @Order(Integer.MIN_VALUE) + public void 사용자_등록된다() throws Exception { + // given + UserSaveRequestDto userSaveRequestDto = UserSaveRequestDto.builder() + .userName("사용자") + .email(TEST_EMAIL) + .password(TEST_PASSWORD) + .roleId(Role.USER.getKey()) + .userStateCode("01") + .build(); + userService.save(userSaveRequestDto); + + // when + UserResponseDto findUser = userService.findByEmail(TEST_EMAIL); + + // then + assertThat(findUser.getEmail()).isEqualTo(TEST_EMAIL); + } + + @Test + @Order(2) + public void 사용자_수정된다() throws Exception { + // given + UserResponseDto findUser = userService.findByEmail(TEST_EMAIL); + UserUpdateRequestDto userUpdateRequestDto = UserUpdateRequestDto.builder() + .userName("사용자수정") + .email(TEST_EMAIL) + .roleId(Role.USER.getKey()) + .userStateCode("01") + .build(); + + // when + userService.update(findUser.getUserId(), userUpdateRequestDto); + UserResponseDto updatedUser = userService.findByEmail(TEST_EMAIL); + + // then + assertThat(updatedUser.getUserName()).isEqualTo("사용자수정"); + } + + @Test + public void 사용자_등록오류() throws Exception { + // given + UserSaveRequestDto userSaveRequestDto = UserSaveRequestDto.builder() + .userName("사용자") + .email("email") + .password("test") + .build(); + + String url = "/api/v1/users"; + + RestClientException restClientException = Assertions.assertThrows(RestClientException.class, () -> { + restTemplate.postForEntity(url, userSaveRequestDto, Long.class); + }); + System.out.println("restClientException.getMessage() = " + restClientException.getMessage()); + } + + @Test + public void 사용자_로그인된다() throws Exception { + // given + JSONObject loginJson = new JSONObject(); + loginJson.put("email", TEST_EMAIL); + loginJson.put("password", TEST_PASSWORD); + + String url = "/login"; + + ResponseEntity responseEntity = restTemplate.postForEntity(url, loginJson.toString(), String.class); + responseEntity.getHeaders().entrySet().forEach(System.out::println); + assertThat(responseEntity.getHeaders().containsKey("access-token")).isTrue(); + } + + @Test + public void 사용자_로그인_오류발생한다() throws Exception { + // given + JSONObject loginJson = new JSONObject(); + loginJson.put("email", TEST_EMAIL); + loginJson.put("password", "test"); + + String url = "/login"; + + ResponseEntity responseEntity = restTemplate.postForEntity(url, loginJson.toString(), String.class); + System.out.println("responseEntity = " + responseEntity); + assertThat(responseEntity.getHeaders().containsKey("access-token")).isFalse(); + } + +} \ No newline at end of file diff --git a/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/AuthorizationApiControllerTest.java b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/AuthorizationApiControllerTest.java new file mode 100644 index 0000000..864e1b8 --- /dev/null +++ b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/AuthorizationApiControllerTest.java @@ -0,0 +1,403 @@ +package org.egovframe.cloud.userservice.api.role; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.egovframe.cloud.userservice.domain.role.Authorization; +import org.egovframe.cloud.userservice.domain.role.AuthorizationRepository; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorization; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorizationRepository; +import org.json.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * org.egovframe.cloud.userservice.api.role.AuthorizationApiControllerTest + *

+ * 인가 Rest API 컨트롤러 테스트 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class AuthorizationApiControllerTest { + + /** + * WebApplicationContext + */ + @Autowired + private WebApplicationContext context; + + /** + * MockMvc + */ + private MockMvc mvc; + + /** + * ObjectMapper + */ + @Autowired + private ObjectMapper objectMapper; + + /** + * test rest template + */ + @Autowired + TestRestTemplate restTemplate; + + /** + * 인가 레파지토리 인터페이스 + */ + @Autowired + AuthorizationRepository authorizationRepository; + + /** + * 권한 인가 레파지토리 인터페이스 + */ + @Autowired + RoleAuthorizationRepository roleAuthorizationRepository; + + /** + * 인가 API 경로 + */ + private static final String URL = "/api/v1/authorizations"; + + /** + * 테스트 데이터 등록 횟수 + */ + private final Integer GIVEN_DATA_COUNT = 10; + + /** + * 테스트 데이터 + */ + private final String AUTHORIZATION_NAME_PREFIX = "인가 명"; + //private final String URL_PATTERN_VALUE_PREFIX = "URL 패턴 값"; + private final String URL_PATTERN_VALUE_PREFIX = "/api/v1/authorizations"; + //private final String HTTP_METHOD_VALUE_PREFIX = "Http Method 코드"; + private final String HTTP_METHOD_VALUE_PREFIX = "GET"; + + private final String INSERT_AUTHORIZATION_NAME = AUTHORIZATION_NAME_PREFIX + "_1"; + private final String INSERT_URL_PATTERN_VALUE = URL_PATTERN_VALUE_PREFIX + "_1"; + private final String INSERT_HTTP_METHOD_VALUE = HTTP_METHOD_VALUE_PREFIX + "_1"; + private final Integer INSERT_SORT_SEQ = 2; + + private final String UPDATE_AUTHORIZATION_NAME = AUTHORIZATION_NAME_PREFIX + "_2"; + private final String UPDATE_URL_PATTERN_VALUE = URL_PATTERN_VALUE_PREFIX + "_"; + private final String UPDATE_HTTP_METHOD_VALUE = HTTP_METHOD_VALUE_PREFIX + "_"; + private final Integer UPDATE_SORT_SEQ = 2; + + /** + * 테스트 데이터 + */ + private List testDatas = new ArrayList<>(); + + /** + * 테스트 시작 전 수행 + */ + @BeforeEach + void setUp() { + mvc = MockMvcBuilders.webAppContextSetup(context) + .addFilter(new CharacterEncodingFilter("UTF-8")) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + } + + /** + * 테스트 종료 후 수행 + */ + @AfterEach + void tearDown() { + } + + /** + * 인가 페이지 목록 조회 테스트 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 인가_페이지_목록_조회() throws Exception { + // given + insertTestDatas(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("keywordType", "authorizationName"); + params.add("keyword", AUTHORIZATION_NAME_PREFIX); + params.add("page", "0"); + params.add("size", "10"); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(URL) + .params(params)); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) +// .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.numberOfElements").value(GIVEN_DATA_COUNT)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content[0].authorizationName").value(AUTHORIZATION_NAME_PREFIX + "_1")); + + deleteTestDatas(); + } + + /** + * 인가 상세 조회 테스트 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 인가_상세_조회() throws Exception { + // given + Authorization entity = insertTestData(); + + final Integer authorizationNo = entity.getAuthorizationNo(); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(URL + "/" + authorizationNo)); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.authorizationNo").value(authorizationNo)) + .andExpect(MockMvcResultMatchers.jsonPath("$.authorizationName").value(INSERT_AUTHORIZATION_NAME)) + .andExpect(MockMvcResultMatchers.jsonPath("$.urlPatternValue").value(INSERT_URL_PATTERN_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.httpMethodCode").value(INSERT_HTTP_METHOD_VALUE)) + .andExpect(MockMvcResultMatchers.jsonPath("$.sortSeq").value(INSERT_SORT_SEQ)); + + deleteTestData(authorizationNo); + } + + /** + * 인가 다음 정렬 순서 조회 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 인가_다음정렬순서_조회() throws Exception { + // given + insertTestDatas(); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(URL + "/sort-seq/next")); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + // .andExpect(MockMvcResultMatchers.content().string("11")); + .andExpect(MockMvcResultMatchers.content().string("129")); // /src/test/resources/h2/data.sql 초기화 데이터의 마지막 순번 + 1 + + deleteTestDatas(); + } + + /** + * 인가 등록 테스트 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 인가_등록() throws Exception { + // given + Map params = new HashMap<>(); + params.put("authorizationName", INSERT_AUTHORIZATION_NAME); + params.put("urlPatternValue", INSERT_URL_PATTERN_VALUE); + params.put("httpMethodCode", INSERT_HTTP_METHOD_VALUE); + params.put("sortSeq", INSERT_SORT_SEQ); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.post(URL) + .accept(MediaType.APPLICATION_JSON) + .contentType("application/json;charset=UTF-8") + .content(objectMapper.writeValueAsString(params))); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()); + + String responseData = resultActions.andReturn().getResponse().getContentAsString(); + JSONObject jsonObject = new JSONObject(responseData); + + final Integer authorizationNo = Integer.parseInt(jsonObject.get("authorizationNo").toString()); + + Optional authorization = selectData(authorizationNo); + assertThat(authorization).isPresent(); + + Authorization entity = authorization.get(); + assertThat(entity.getAuthorizationNo()).isEqualTo(authorizationNo); + assertThat(entity.getAuthorizationName()).isEqualTo(INSERT_AUTHORIZATION_NAME); + assertThat(entity.getUrlPatternValue()).isEqualTo(INSERT_URL_PATTERN_VALUE); + assertThat(entity.getHttpMethodCode()).isEqualTo(INSERT_HTTP_METHOD_VALUE); + assertThat(entity.getSortSeq()).isEqualTo(INSERT_SORT_SEQ); + + deleteTestData(authorizationNo); + } + + /** + * 인가 수정 테스트 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 인가_수정() throws Exception { + // given + Authorization entity = insertTestData(); + + final Integer authorizationNo = entity.getAuthorizationNo(); + + Map params = new HashMap<>(); + params.put("authorizationName", UPDATE_AUTHORIZATION_NAME); + params.put("urlPatternValue", UPDATE_URL_PATTERN_VALUE); + params.put("httpMethodCode", UPDATE_HTTP_METHOD_VALUE); + params.put("sortSeq", UPDATE_SORT_SEQ); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.put(URL + "/" + authorizationNo) + .accept(MediaType.APPLICATION_JSON) + .contentType("application/json;charset=UTF-8") + .content(objectMapper.writeValueAsString(params))); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()); + + Optional authorization = selectData(authorizationNo); + assertThat(authorization).isPresent(); + + Authorization updatedAuthorization = authorization.get(); + assertThat(updatedAuthorization.getAuthorizationNo()).isEqualTo(authorizationNo); + assertThat(updatedAuthorization.getAuthorizationName()).isEqualTo(UPDATE_AUTHORIZATION_NAME); + assertThat(updatedAuthorization.getUrlPatternValue()).isEqualTo(UPDATE_URL_PATTERN_VALUE); + assertThat(updatedAuthorization.getHttpMethodCode()).isEqualTo(UPDATE_HTTP_METHOD_VALUE); + assertThat(updatedAuthorization.getSortSeq()).isEqualTo(UPDATE_SORT_SEQ); + + deleteTestData(authorizationNo); + } + + /** + * 인가 삭제 테스트 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 인가_삭제() throws Exception { + // given + Authorization entity = insertTestData(); + + final Integer authorizationNo = entity.getAuthorizationNo(); + + // 권한 인가 2건 등록 후 같이 삭제 + roleAuthorizationRepository.save(RoleAuthorization.builder() + .roleId("ROLE_1") + .authorizationNo(authorizationNo) + .build()); + roleAuthorizationRepository.save(RoleAuthorization.builder() + .roleId("ROLE_2") + .authorizationNo(authorizationNo) + .build()); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.delete(URL + "/" + authorizationNo)); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()); + + Optional authorization = selectData(authorizationNo); + assertThat(authorization).isNotPresent(); + } + + /** + * 테스트 데이터 등록 + */ + private void insertTestDatas() { + for (int i = 1; i <= GIVEN_DATA_COUNT; i++) { + testDatas.add(authorizationRepository.save(Authorization.builder() + .authorizationName(AUTHORIZATION_NAME_PREFIX + "_" + i) + .urlPatternValue(URL_PATTERN_VALUE_PREFIX + "_" + i) + .httpMethodCode(HTTP_METHOD_VALUE_PREFIX + "_" + i) + .sortSeq(i) + .build())); + } + } + + /** + * 테스트 데이터 삭제 + */ + private void deleteTestDatas() { + if (testDatas != null) { + if (!testDatas.isEmpty()) authorizationRepository.deleteAll(testDatas); + + testDatas.clear(); + } + } + + /** + * 테스트 데이터 단건 등록 + * + * @return Authorization 인가 엔티티 + */ + private Authorization insertTestData() { + return authorizationRepository.save(Authorization.builder() + .authorizationName(INSERT_AUTHORIZATION_NAME) + .urlPatternValue(INSERT_URL_PATTERN_VALUE) + .httpMethodCode(INSERT_HTTP_METHOD_VALUE) + .sortSeq(INSERT_SORT_SEQ) + .build()); + } + + /** + * 테스트 데이터 단건 삭제 + */ + private void deleteTestData(Integer authorizationNo) { + authorizationRepository.deleteById(authorizationNo); + } + + /** + * 테스트 데이터 단건 조회 + * + * @param authorizationNo 인가 번호 + * @return Optional 인가 엔티티 + */ + private Optional selectData(Integer authorizationNo) { + return authorizationRepository.findById(authorizationNo); + } + +} \ No newline at end of file diff --git a/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/RoleApiControllerTest.java b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/RoleApiControllerTest.java new file mode 100644 index 0000000..f508cac --- /dev/null +++ b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/RoleApiControllerTest.java @@ -0,0 +1,182 @@ +package org.egovframe.cloud.userservice.api.role; + +import java.util.ArrayList; +import java.util.List; + +import org.egovframe.cloud.userservice.domain.role.Role; +import org.egovframe.cloud.userservice.domain.role.RoleRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +/** + * org.egovframe.cloud.userservice.api.role.RoleApiControllerTest + *

+ * 권한 Rest API 컨트롤러 테스트 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/07 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/07    jooho       최초 생성
+ * 
+ */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class RoleApiControllerTest { + + /** + * WebApplicationContext + */ + @Autowired + private WebApplicationContext context; + + /** + * MockMvc + */ + private MockMvc mvc; + + /** + * 권한 레파지토리 인터페이스 + */ + @Autowired + private RoleRepository roleRepository; + + /** + * 권한 API 경로 + */ + private static final String URL = "/api/v1/roles"; + + private static final Integer GIVEN_DATA_COUNT = 4; + + private static final String ROLE_ID_PREFIX = "_ROLE_"; + private static final String ROLE_NAME_PREFIX = "권한 명 테스트"; + private static final String ROLE_CONTENT_PREFIX = "권한 내용"; + + /** + * 테스트 데이터 + */ + private List testDatas = new ArrayList<>(); + + /** + * 테스트 시작 전 + */ + @BeforeEach + void setUp() { + mvc = MockMvcBuilders.webAppContextSetup(context) + .addFilter(new CharacterEncodingFilter("UTF-8")) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + } + + /** + * 테스트 종료 후 + */ + @BeforeEach + void tearDown() { + } + + /** + * 권한 페이지 목록 조회 테스트 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 권한_페이지_목록_조회() throws Exception { + // given + insertTestDatas(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("keywordType", "roleName"); + params.add("keyword", ROLE_NAME_PREFIX); + params.add("page", "0"); + params.add("size", "10"); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(URL) + .params(params)); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.numberOfElements").value(GIVEN_DATA_COUNT)) + .andExpect(MockMvcResultMatchers.jsonPath("$.content[0].roleId").value(ROLE_ID_PREFIX + "_1")); + + deleteTestDatas(); + } + + /** + * 권한 전체 목록 조회 테스트 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 권한_전체_목록_조회() throws Exception { + // given + insertTestDatas(); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(URL + "/all")); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$[0].roleId").value(ROLE_ID_PREFIX + "_1")); + + deleteTestDatas(); + } + + /** + * 테스트 데이터 등록 + */ + private void insertTestDatas() { + for (int i = 1; i <= GIVEN_DATA_COUNT; i++) { + String roleId = ROLE_ID_PREFIX + "_" + i; + String roleName = ROLE_NAME_PREFIX + "_" + i; + String roleContent = ROLE_CONTENT_PREFIX + "_" + i; + + testDatas.add(roleRepository.save(Role.builder() + .roleId(roleId) + .roleName(roleName) + .roleContent(roleContent) + .sortSeq(i) + .build())); + } + } + + /** + * 테스트 데이터 삭제 + */ + private void deleteTestDatas() { + roleRepository.deleteAll(testDatas); + + testDatas.clear(); + } + +} \ No newline at end of file diff --git a/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/RoleAuthorizationApiControllerTest.java b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/RoleAuthorizationApiControllerTest.java new file mode 100644 index 0000000..42d3ba2 --- /dev/null +++ b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/api/role/RoleAuthorizationApiControllerTest.java @@ -0,0 +1,370 @@ +package org.egovframe.cloud.userservice.api.role; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.assertj.core.api.Condition; +import org.egovframe.cloud.userservice.domain.role.Authorization; +import org.egovframe.cloud.userservice.domain.role.AuthorizationRepository; +import org.egovframe.cloud.userservice.domain.role.Role; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorization; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorizationId; +import org.egovframe.cloud.userservice.domain.role.RoleAuthorizationRepository; +import org.egovframe.cloud.userservice.domain.role.RoleRepository; +import org.json.JSONArray; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * org.egovframe.cloud.userservice.api.role.RoleAuthorizationApiControllerTest + *

+ * 권한 인가 Rest API 컨트롤러 테스트 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/12 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/12    jooho       최초 생성
+ * 
+ */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@EnableConfigurationProperties +@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"}) +@ActiveProfiles(profiles = "test") +class RoleAuthorizationApiControllerTest { + + /** + * WebApplicationContext + */ + @Autowired + private WebApplicationContext context; + + /** + * MockMvc + */ + private MockMvc mvc; + + /** + * ObjectMapper + */ + @Autowired + private ObjectMapper objectMapper; + + /** + * 권한 레파지토리 인터페이스 + */ + @Autowired + RoleRepository roleRepository; + + /** + * 인가 레파지토리 인터페이스 + */ + @Autowired + AuthorizationRepository authorizationRepository; + + /** + * 권한 인가 레파지토리 인터페이스 + */ + @Autowired + RoleAuthorizationRepository roleAuthorizationRepository; + + /** + * 인가 API 경로 + */ + private static final String URL = "/api/v1/role-authorizations"; + + /** + * 테스트 데이터 등록 횟수 + */ + private final Integer GIVEN_AUTHORIZATION_COUNT = 5; + + private final String ROLE_ID = "_ROLE_1"; + private final String ROLE_NAME = "권한 명_1"; + private final String ROLE_CONTENT = "권한 내용_1"; + + private final String AUTHORIZATION_NAME_PREFIX = "인가 명"; + private final String URL_PATTERN_VALUE_PREFIX = "/api/v1/test"; + private final String HTTP_METHOD_VALUE_PREFIX = "GET"; + + /** + * 테스트 데이터 + */ + private Role role = null; + private final List authorizations = new ArrayList<>(); + private final List testDatas = new ArrayList<>(); + + /** + * 테스트 시작 전 수행 + */ + @BeforeEach + void setUp() { + mvc = MockMvcBuilders.webAppContextSetup(context) + .addFilter(new CharacterEncodingFilter("UTF-8")) + .apply(SecurityMockMvcConfigurers.springSecurity()) + .build(); + + // 권한 등록 + role = roleRepository.save(Role.builder() + .roleId(ROLE_ID) + .roleName(ROLE_NAME) + .roleContent(ROLE_CONTENT) + .sortSeq(1) + .build()); + + // 인가 등록 + for (int i = 1; i <= GIVEN_AUTHORIZATION_COUNT; i++) { + authorizations.add(authorizationRepository.save(Authorization.builder() + .authorizationName(AUTHORIZATION_NAME_PREFIX + "_" + i) + .urlPatternValue(URL_PATTERN_VALUE_PREFIX + "_" + i) + .httpMethodCode(HTTP_METHOD_VALUE_PREFIX + "_" + i) + .sortSeq(i) + .build())); + } + } + + /** + * 테스트 종료 후 수행 + */ + @AfterEach + void tearDown() { + // 인가 삭제 + authorizationRepository.deleteAll(authorizations); + authorizations.clear(); + + // 권한 삭제 + roleRepository.delete(role); + } + + /** + * 권한 인가 페이지 목록 조회 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 권한_인가_페이지_목록_조회() throws Exception { + // given + insertTestDatas(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("roleId", role.getRoleId()); + params.add("keywordType", "urlPatternValue"); + params.add("keyword", URL_PATTERN_VALUE_PREFIX); + params.add("page", "0"); + params.add("size", "10"); + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(URL) + .params(params)); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.numberOfElements").value(authorizations.size())) + .andExpect(MockMvcResultMatchers.jsonPath("$.content[0].roleId").value(role.getRoleId())) + .andExpect(MockMvcResultMatchers.jsonPath("$.content[0].authorizationNo").value(authorizations.get(0).getAuthorizationNo())) + .andExpect(MockMvcResultMatchers.jsonPath("$.content[0].createdAt").value(true)); + + deleteTestDatas(); + } + + /** + * 권한 인가 다건 등록 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 권한_인가_다건_등록() throws Exception { + // given + List> requestDtoList = new ArrayList<>(); + for (int i = 1; i <= authorizations.size(); i++) { + if (i % 2 == 0) continue; //홀수만 등록 + + Map params = new HashMap<>(); + params.put("roleId", role.getRoleId()); + params.put("authorizationNo", authorizations.get(i - 1).getAuthorizationNo()); + + requestDtoList.add(params); + } + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.post(URL) + .accept(MediaType.APPLICATION_JSON) + .contentType("application/json;charset=UTF-8") + .content(objectMapper.writeValueAsString(requestDtoList))); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()); + + String responseData = resultActions.andReturn().getResponse().getContentAsString(); + JSONArray jsonArray = new JSONArray(responseData); + + assertThat(jsonArray.length()).isEqualTo(requestDtoList.size()); + + List entityList = roleAuthorizationRepository.findAll(Sort.by(Sort.Direction.ASC, "roleAuthorizationId.authorizationNo")); + for (int i = entityList.size() - 1; i >= 0; i--) { + if (!entityList.get(i).getRoleAuthorizationId().getRoleId().equals(role.getRoleId())) { + entityList.remove(i); + } + } + assertThat(entityList).isNotNull(); + assertThat(entityList.size()).isEqualTo(requestDtoList.size()); + assertThat(entityList) + .isNotEmpty() + .has(new Condition<>(l -> l.get(0).getRoleAuthorizationId().getRoleId().equals(role.getRoleId()) && l.get(0).getRoleAuthorizationId().getAuthorizationNo().compareTo(authorizations.get(0).getAuthorizationNo()) == 0, + "RoleAuthorizationApiControllerTest.saveList authorizationNo eq 1")) + .has(new Condition<>(l -> l.get(1).getRoleAuthorizationId().getRoleId().equals(role.getRoleId()) && l.get(1).getRoleAuthorizationId().getAuthorizationNo().compareTo(authorizations.get(2).getAuthorizationNo()) == 0, + "RoleAuthorizationApiControllerTest.saveList authorizationNo eq 3")); + + for (int i = entityList.size() - 1; i >= 0; i--) { + deleteTestData(entityList.get(i).getRoleAuthorizationId().getRoleId(), entityList.get(i).getRoleAuthorizationId().getAuthorizationNo()); + } + } + + /** + * 권한 인가 다건 삭제 + */ + @Test + @WithMockUser(roles = "ADMIN") + void 권한_인가_다건_삭제() throws Exception { + // given + insertTestDatas(); + + List> requestDtoList = new ArrayList<>(); + for (RoleAuthorization testData : testDatas) { + Map params = new HashMap<>(); + params.put("roleId", testData.getRoleAuthorizationId().getRoleId()); + params.put("authorizationNo", testData.getRoleAuthorizationId().getAuthorizationNo()); + + requestDtoList.add(params); + } + + // when + ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.put(URL) + .accept(MediaType.APPLICATION_JSON) + .contentType("application/json;charset=UTF-8") + .content(objectMapper.writeValueAsString(requestDtoList))); + + // then + resultActions + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()); + + List entityList = roleAuthorizationRepository.findAll(Sort.by(Sort.Direction.ASC, "roleAuthorizationId.authorizationNo")); + for (int i = entityList.size() - 1; i >= 0; i--) { + if (!entityList.get(i).getRoleAuthorizationId().getRoleId().equals(role.getRoleId())) { + entityList.remove(i); + } + } + assertThat(entityList).isNotNull(); + assertThat(entityList.size()).isZero(); + } + + /** + * 권한 인가 레파지토리 등록/조회 테스트 + */ + @Test + @Disabled + void 권한_인가_등록_조회() { + // given + final Integer authorizationNo = authorizations.get(0).getAuthorizationNo(); + + roleAuthorizationRepository.save(RoleAuthorization.builder() + .roleId(role.getRoleId()) + .authorizationNo(authorizationNo) + .build()); + + // when + Optional roleAuthorization = selectData(role.getRoleId(), authorizationNo); + + // then + assertThat(roleAuthorization).isPresent(); + + RoleAuthorization entity = roleAuthorization.get(); + assertThat(entity.getRoleAuthorizationId().getRoleId()).isEqualTo(role.getRoleId()); + assertThat(entity.getRoleAuthorizationId().getAuthorizationNo()).isEqualTo(authorizationNo); + } + + /** + * 테스트 데이터 등록 + */ + private void insertTestDatas() { + // 권한 인가 등록 + for (int i = 1; i <= authorizations.size(); i++) { + if (i % 2 == 0) continue; //인가 번호 홀수만 등록 + + testDatas.add(roleAuthorizationRepository.save(RoleAuthorization.builder() + .roleId(role.getRoleId()) + .authorizationNo(authorizations.get(i - 1).getAuthorizationNo()) + .build())); + } + } + + /** + * 테스트 데이터 삭제 + */ + private void deleteTestDatas() { + // 권한 인가 삭제 + roleAuthorizationRepository.deleteAll(testDatas); + testDatas.clear(); + } + + /** + * 테스트 데이터 단건 삭제 + */ + private void deleteTestData(String roleId, Integer authorizationNo) { + roleAuthorizationRepository.deleteById(RoleAuthorizationId.builder() + .roleId(roleId) + .authorizationNo(authorizationNo) + .build()); + } + + /** + * 테스트 데이터 단건 조회 + * + * @param roleId 권한 id + * @param authorizationNo 인가 번호 + * @return Optional 권한 인가 엔티티 + */ + private Optional selectData(String roleId, Integer authorizationNo) { + return roleAuthorizationRepository.findById(RoleAuthorizationId.builder() + .roleId(roleId) + .authorizationNo(authorizationNo) + .build()); + } + +} \ No newline at end of file diff --git a/backend/user-service/src/test/java/org/egovframe/cloud/userservice/config/MessageSourceConfigTest.java b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/config/MessageSourceConfigTest.java new file mode 100644 index 0000000..de203ff --- /dev/null +++ b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/config/MessageSourceConfigTest.java @@ -0,0 +1,28 @@ +package org.egovframe.cloud.userservice.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.MessageSource; + +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +class MessageSourceConfigTest { + + @Autowired + TestRestTemplate restTemplate; + + @Test + public void 메세지를_외부위치에서_읽어온다() throws Exception { + // when + String message = restTemplate.getForObject("http://localhost:8000/user-service/api/v1/messages/common.login/ko", String.class); + + // then + assertThat(message).isEqualTo("로그인"); + } +} \ No newline at end of file diff --git a/backend/user-service/src/test/java/org/egovframe/cloud/userservice/util/RestResponsePage.java b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/util/RestResponsePage.java new file mode 100644 index 0000000..0b44138 --- /dev/null +++ b/backend/user-service/src/test/java/org/egovframe/cloud/userservice/util/RestResponsePage.java @@ -0,0 +1,87 @@ +package org.egovframe.cloud.userservice.util; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.Collections; +import java.util.List; + +/** + * org.egovframe.cloud.userservice.util.RestResponsePage + *

+ * 페이지 API 조회 시 JSON 형식의 응답 데이터를 페이지 객체를 구현하여 마이그레이션 해주는 클래스 + * + * @author 표준프레임워크센터 jooho + * @version 1.0 + * @since 2021/07/08 + * + *

+ * << 개정이력(Modification Information) >>
+ *
+ *     수정일        수정자           수정내용
+ *  ----------    --------    ---------------------------
+ *  2021/07/08    jooho       최초 생성
+ * 
+ */ +public class RestResponsePage extends PageImpl { + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + * @param number 페이지 번호 + * @param size 조회할 데이터 수 + * @param totalElements 총 데이터 수 + * @param pageable 페이지 정보 + * @param last 마지막 + * @param totalPages 총 페이지 + * @param sort 정렬 + * @param first 처음 + * @param numberOfElements 조회된 데이터 수 + */ + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public RestResponsePage(@JsonProperty("content") List content, + @JsonProperty("number") int number, + @JsonProperty("size") int size, + @JsonProperty("totalElements") Long totalElements, + @JsonProperty("pageable") JsonNode pageable, + @JsonProperty("last") boolean last, + @JsonProperty("totalPages") int totalPages, + @JsonProperty("sort") JsonNode sort, + @JsonProperty("first") boolean first, + @JsonProperty("numberOfElements") int numberOfElements) { + super(content, PageRequest.of(number, size), totalElements); + } + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + * @param pageable 페이지 정보 + * @param total 총 데이터 수 + */ + public RestResponsePage(List content, Pageable pageable, long total) { + super(content, pageable, total); + } + + /** + * Rest 응답 페이지 생성자 + * + * @param content 목록 + */ + public RestResponsePage(List content) { + super(content); + } + + /** + * Rest 응답 페이지 생성자 + */ + public RestResponsePage() { + super(Collections.emptyList()); + } + +} diff --git a/backend/user-service/src/test/resources/application-test.yml b/backend/user-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..314b4f5 --- /dev/null +++ b/backend/user-service/src/test/resources/application-test.yml @@ -0,0 +1,107 @@ +spring: + application: + name: user-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + initialization-mode: always +# schema: classpath:h2/schema.sql + data: classpath:h2/data.sql + jpa: + hibernate: + generate-ddl: true + ddl-auto: create-drop + dialect: org.hibernate.dialect.MySQL5Dialect + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 + show-sql: true + h2: + console: + enabled: true + path: /h2 + cache: + jcache: + config: classpath:ehcache.xml + mail: # 비밀번호 변경 이메일 발송 + host: smtp.gmail.com # smtp host + port: 587 # smtp port + username: email_username # 계정 + password: 'email_password' # 비밀번호 - 구글 보안 2단계 인증 해제, 보안 수준이 낮은 앱의 액세스 허용(https://myaccount.google.com/lesssecureapps) + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + security: + # oauth2 를 사용하려면 아래 google, naver, kakao 의 client-id, client-secret 을 발급받아야 한다. + oauth2: + client: + registration: + # /oauth2/authorization/google + google: + client-id: google_client_id # TODO + client-secret: google_client_secret # TODO + scope: profile,email + # 네이버는 Spring Security를 공식 지원하지 않기 때문에 CommonOAuth2Provider 에서 해주는 값들을 수동으로 입력한다. + # /oauth2/authorization/naver + naver: + client-id: naver_client_id # TODO + client-secret: naver_client_secret # TODO + redirect_uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" + authorization_grant_type: authorization_code + scope: name,email,profile_image + client-name: Naver + # /oauth2/authorization/kakao + kakao: + client-id: kakao_client_id # TODO + client-secret: kakao_client_secret # TODO + redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" + client-authentication-method: POST + authorization-grant-type: authorization_code + scope: profile_nickname, account_email + client-name: Kakao + provider: + naver: + authorization_uri: https://nid.naver.com/oauth2.0/authorize + token_uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + # 기준이 되는 user_name 의 이름을 네이버에서는 response로 지정해야한다. (네이버 회원 조회시 반환되는 JSON 형태 때문이다) + # response를 user_name으로 지정하고 이후 자바 코드로 response의 id를 user_name으로 지정한다. (스프링 시큐리티에서 하위 필드를 명시할 수 없기 때문) + user_name_attribute: response + kakao: + authorization_uri: https://kauth.kakao.com/oauth/authorize + token_uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user_name_attribute: id + + logging.level: + org.hibernate.SQL: debug + org.hibernate.type: trace + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + expiration_time: 7200000 + refresh_time: 86400000 + secret: egovframe_token_secret + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/backend/user-service/src/test/resources/h2/data.sql b/backend/user-service/src/test/resources/h2/data.sql new file mode 100644 index 0000000..c34640d --- /dev/null +++ b/backend/user-service/src/test/resources/h2/data.sql @@ -0,0 +1,38 @@ +INSERT INTO `authorization` (authorization_name,url_pattern_value,http_method_code,sort_seq,created_by,created_date,last_modified_by,modified_date) VALUES + ('사용자 목록 조회','/user-service/api/v1/users','GET',101,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 단건 조회','/user-service/api/v1/users/?*','GET',102,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 등록','/user-service/api/v1/users','POST',103,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 수정','/user-service/api/v1/users/?*','PUT',104,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 토큰 갱신','/user-service/api/v1/users/token/refresh','GET',105,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('권한 페이지 목록 조회','/user-service/api/v1/roles','GET',106,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('권한 전체 목록 조회','/user-service/api/v1/roles/all','GET',107,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('인가 페이지 목록 조회','/user-service/api/v1/authorizations','GET',108,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('인가 단건 조회','/user-service/api/v1/authorizations/?*','GET',109,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('인가 다음 정렬 순서 조회','/user-service/api/v1/authorizations/sort-seq/next','GET',110,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('인가 등록','/user-service/api/v1/authorizations','POST',111,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('인가 수정','/user-service/api/v1/authorizations/?*','PUT',112,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('인가 삭제','/user-service/api/v1/authorizations/?*','DELETE',113,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('인가 여부 확인','/user-service/api/v1/authorizations/check','GET',114,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('권한 인가 페이지 목록 조회','/user-service/api/v1/role-authorizations','GET',115,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('권한 인가 다건 등록','/user-service/api/v1/role-authorizations','POST',116,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('권한 인가 다건 삭제','/user-service/api/v1/role-authorizations','PUT',117,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 이메일 중복 확인','/user-service/api/v1/users/exists','POST',118,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 회원 가입','/user-service/api/v1/users/join','POST',119,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 비밀번호 찾기','/user-service/api/v1/users/password/find','POST',120,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 비밀번호 찾기 유효성 확인','/user-service/api/v1/users/password/valid/?*','GET',121,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 비밀번호 찾기 변경','/user-service/api/v1/users/password/change','PUT',122,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 비밀번호 변경','/user-service/api/v1/users/password/update','PUT',123,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 비밀번호 확인','/user-service/api/v1/users/password/match','POST',124,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('예약지역 사용여부 토글','/reserve-item-service/api/v1/locations/?*/?*','PUT',125,'87638675-11fa-49e5-9bd1-d2524bf6fa45',now(),'87638675-11fa-49e5-9bd1-d2524bf6fa45',now()), + ('사용자 정보 수정','/user-service/api/v1/users/info/?*','PUT',126,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 회원탈퇴','/user-service/api/v1/users/leave','POST',127,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()), + ('사용자 삭제','/user-service/api/v1/users/delete/?*','DELETE',128,'65a00f65-8460-49af-98ec-042977e56f4b',now(),'65a00f65-8460-49af-98ec-042977e56f4b',now()); + +INSERT INTO `role` (role_id,role_name,role_content,sort_seq,created_date) VALUES + ('ROLE_ADMIN','시스템 관리자','시스템 관리자 권한',101,'2021-10-20 13:39:15'), + ('ROLE_ANONYMOUS','손님','손님 권한',104,'2021-10-20 13:39:15'), + ('ROLE_EMPLOYEE','내부 사용자','내부 사용자 권한',102,'2021-10-20 13:39:15'), + ('ROLE_USER','일반 사용자','일반 사용자 권한',103,'2021-10-20 13:39:15'); + +INSERT INTO role_authorization (role_id,authorization_no,created_by,created_date) +select 'ROLE_ADMIN', authorization_no, '65a00f65-8460-49af-98ec-042977e56f4b', now() from `authorization`; diff --git a/config/application.yml b/config/application.yml new file mode 100644 index 0000000..30ee56d --- /dev/null +++ b/config/application.yml @@ -0,0 +1,36 @@ +# 2시간(7,200,000), 1일(86,400,000) jwt token +token: + expiration_time: 7200000 + refresh_time: 86400000 + secret: 'token_secret' + +eureka: + instance: + instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} # random port 사용시 eureka server에 인스턴스가 각각 표시되지 않는다 + client: + register-with-eureka: true # eureka 서버에 등록 + fetch-registry: true # 외부 검색 가능 + service-url: + defaultZone: http://admin:admin@localhost:8761/eureka + +# file attach location - messages{lang}.properties 도 이 경로에 위치한다. +file: + directory: ${user.home}/msa-attach-volume # url 사용시에는 사용되지 않는다 + url: http://localhost:8080 # nginx 로 파일 다운로드 처리 +messages: + directory: ${file.directory}/messages +logstash: + url: localhost:8086 + +apigateway: + host: http://localhost:${server.port} + +# rabbitmq server +spring: + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + zipkin: + base-url: http://localhost:8085 diff --git a/config/board-service-test.yml b/config/board-service-test.yml new file mode 100644 index 0000000..f230e8d --- /dev/null +++ b/config/board-service-test.yml @@ -0,0 +1,46 @@ +spring: + application: + name: board-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: +# database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + hibernate: + generate-ddl: true + ddl-auto: create-drop +# dialect: org.hibernate.dialect.MySQL5InnoDBDialect + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 # 1:N 관계를 해결하기 위해 필요 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/config/board-service.yml b/config/board-service.yml new file mode 100644 index 0000000..48f3f59 --- /dev/null +++ b/config/board-service.yml @@ -0,0 +1,15 @@ +database: + url: jdbc:mysql://localhost:3306/msaportal + +spring: + datasource: + url: ${database.url}?serverTimezone=Asia/Seoul + username: msaportal + password: msaportal + driver-class-name: com.mysql.cj.jdbc.Driver + cloud: + stream: + bindings: + attachmentEntity-out-0: # 첨부파일 entity 정보 업데이트 하기 위한 이벤트 + destination: attachment-entity.topic # queue name + group: attachment diff --git a/config/portal-service-test.yml b/config/portal-service-test.yml new file mode 100644 index 0000000..afbbdd5 --- /dev/null +++ b/config/portal-service-test.yml @@ -0,0 +1,44 @@ +spring: + application: + name: portal-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: + hibernate: + generate-ddl: true + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 # 1:N 관계를 해결하기 위해 필요 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/config/portal-service.yml b/config/portal-service.yml new file mode 100644 index 0000000..50b7dbc --- /dev/null +++ b/config/portal-service.yml @@ -0,0 +1,29 @@ +database: + url: jdbc:mysql://localhost:3306/msaportal + +spring: + datasource: + url: ${database.url}?serverTimezone=Asia/Seoul + username: msaportal + password: msaportal + driver-class-name: com.mysql.cj.jdbc.Driver + cloud: + stream: + function: + definition: attachmentEntity # 첨부파일 entity 정보 업데이트 하기 위한 이벤트에 대한 consumer function + bindings: + attachmentEntity-in-0: # 첨부파일 entity 정보 업데이트 하기 위한 이벤트 + destination: attachment-entity.topic # queue name + group: attachment + attachmentEntity-out-0: # 첨부파일 entity 정보 업데이트 하기 위한 이벤트 + destination: attachment-entity.topic # queue name + group: attachment + +# ftp server +ftp: + hostname: 'ftp_server_hostname' + username: 'ftp_server_username' + password: 'ftp_server_password' + port: 21 + directory: /mnt + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. diff --git a/config/reserve-check-service-test.yml b/config/reserve-check-service-test.yml new file mode 100644 index 0000000..9130296 --- /dev/null +++ b/config/reserve-check-service-test.yml @@ -0,0 +1,46 @@ +spring: + application: + name: reserve-check-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: +# database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + hibernate: + generate-ddl: true + ddl-auto: create-drop +# dialect: org.hibernate.dialect.MySQL5InnoDBDialect + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 # 1:N 관계를 해결하기 위해 필요 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/config/reserve-check-service.yml b/config/reserve-check-service.yml new file mode 100644 index 0000000..09e6a6c --- /dev/null +++ b/config/reserve-check-service.yml @@ -0,0 +1,11 @@ +spring: + r2dbc: + url: r2dbc:mysql://localhost:3306/msaportal?serverTimezone=Asia/Seoul + username: msaportal + password: msaportal + cloud: + stream: + bindings: + attachmentEntity-out-0: # 첨부파일 entity 정보 업데이트 하기 위한 이벤트 + destination: attachment-entity.topic # queue name + group: attachment diff --git a/config/reserve-item-service-test.yml b/config/reserve-item-service-test.yml new file mode 100644 index 0000000..7abaac9 --- /dev/null +++ b/config/reserve-item-service-test.yml @@ -0,0 +1,46 @@ +spring: + application: + name: reserve-item-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: +# database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + hibernate: + generate-ddl: true + ddl-auto: create-drop +# dialect: org.hibernate.dialect.MySQL5InnoDBDialect + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 # 1:N 관계를 해결하기 위해 필요 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/config/reserve-item-service.yml b/config/reserve-item-service.yml new file mode 100644 index 0000000..e15f1d3 --- /dev/null +++ b/config/reserve-item-service.yml @@ -0,0 +1,16 @@ +spring: + r2dbc: + url: r2dbc:mysql://localhost:3306/msaportal?serverTimezone=Asia/Seoul + username: msaportal + password: msaportal +cloud: + stream: + function: + definition: reserveRequest # 예약 요청후 물품 재고업데이트 이벤트에 대한 consumer function + bindings: + reserveRequest-in-0: # 예약 요청후 물품 재고업데이트 이벤트에 대한 consumer binding + destination: reserve-request.topic # queue name + group: reserved + inventoryUpdated-out-0: # 예약 요청후 물품 재고업데이트 결과에 이벤트에 대한 supplier binding + destination: inventory-updated.topic # queue name + group: reserved diff --git a/config/reserve-request-service-test.yml b/config/reserve-request-service-test.yml new file mode 100644 index 0000000..76825e5 --- /dev/null +++ b/config/reserve-request-service-test.yml @@ -0,0 +1,46 @@ +spring: + application: + name: reserve-request-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + jpa: +# database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + hibernate: + generate-ddl: true + ddl-auto: create-drop +# dialect: org.hibernate.dialect.MySQL5InnoDBDialect + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 # 1:N 관계를 해결하기 위해 필요 + show-sql: true + h2: + console: + enabled: true + path: /h2 + + logging.level: + org.hibernate.SQL: debug + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + secret: egovframe_user_token + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/config/reserve-request-service.yml b/config/reserve-request-service.yml new file mode 100644 index 0000000..685e65f --- /dev/null +++ b/config/reserve-request-service.yml @@ -0,0 +1,16 @@ +spring: + r2dbc: + url: r2dbc:mysql://localhost:3306/msaportal?serverTimezone=Asia/Seoul + username: msaportal + password: msaportal +cloud: + stream: + function: + definition: inventoryUpdated # 예약 요청후 물품 재고업데이트 결과에 이벤트에 대한 consumer function + bindings: + reserveRequest-out-0: # 예약 요청후 물품 재고업데이트 이벤트에 대한 supplier binding + destination: reserve-request.topic # queue name + group: reserved + inventoryUpdated-in-0: # 예약 요청후 물품 재고업데이트 결과에 이벤트에 대한 consumer binding + destination: inventory-updated.topic # queue name + group: reserved diff --git a/config/user-service-test.yml b/config/user-service-test.yml new file mode 100644 index 0000000..6a8ad80 --- /dev/null +++ b/config/user-service-test.yml @@ -0,0 +1,107 @@ +spring: + application: + name: user-service + + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + initialization-mode: always +# schema: classpath:h2/schema.sql + data: classpath:h2/data.sql + jpa: + hibernate: + generate-ddl: true + ddl-auto: create-drop + dialect: org.hibernate.dialect.MySQL5Dialect + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 # 1:N 관계를 해결하기 위해 필요 + show-sql: true + h2: + console: + enabled: true + path: /h2 + cache: + jcache: + config: classpath:ehcache.xml + mail: # 비밀번호 변경 이메일 발송 + host: smtp.gmail.com # smtp host + port: 587 # smtp port + username: email_username # 계정 + password: 'email_password' # 비밀번호 - 구글 보안 2단계 인증 해제, 보안 수준이 낮은 앱의 액세스 허용(https://myaccount.google.com/lesssecureapps) + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + security: + # oauth2 를 사용하려면 아래 google, naver, kakao 의 client-id, client-secret 을 발급받아야 한다. + oauth2: + client: + registration: + # /oauth2/authorization/google + google: + client-id: google_client_id # TODO + client-secret: google_client_secret # TODO + scope: profile,email + # 네이버는 Spring Security를 공식 지원하지 않기 때문에 CommonOAuth2Provider 에서 해주는 값들을 수동으로 입력한다. + # /oauth2/authorization/naver + naver: + client-id: naver_client_id # TODO + client-secret: naver_client_secret # TODO + redirect_uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" + authorization_grant_type: authorization_code + scope: name,email,profile_image + client-name: Naver + # /oauth2/authorization/kakao + kakao: + client-id: kakao_client_id # TODO + client-secret: kakao_client_secret # TODO + redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" + client-authentication-method: POST + authorization-grant-type: authorization_code + scope: profile_nickname, account_email + client-name: Kakao + provider: + naver: + authorization_uri: https://nid.naver.com/oauth2.0/authorize + token_uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + # 기준이 되는 user_name 의 이름을 네이버에서는 response로 지정해야한다. (네이버 회원 조회시 반환되는 JSON 형태 때문이다) + # response를 user_name으로 지정하고 이후 자바 코드로 response의 id를 user_name으로 지정한다. (스프링 시큐리티에서 하위 필드를 명시할 수 없기 때문) + user_name_attribute: response + kakao: + authorization_uri: https://kauth.kakao.com/oauth/authorize + token_uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user_name_attribute: id + + logging.level: + org.hibernate.SQL: debug + org.hibernate.type: trace + +file: + directory: ${user.home}/msa-attach-volume +messages: + directory: ${file.directory}/messages + +# jwt token +token: + expiration_time: 7200000 + refresh_time: 86400000 + secret: egovframe_token_secret + +# ftp server +ftp: + enabled: false # ftp 사용 여부, FTP 서버에 최상위 디렉토리 자동 생성 및 구현체를 결정하게 된다. + +# eureka 가 포함되면 eureka server 도 등록되므로 해제한다. +eureka: + client: + register-with-eureka: false + fetch-registry: false diff --git a/config/user-service.yml b/config/user-service.yml new file mode 100644 index 0000000..83fb314 --- /dev/null +++ b/config/user-service.yml @@ -0,0 +1,21 @@ +database: + url: jdbc:mysql://localhost:3306/msaportal + +spring: + datasource: + url: ${database.url}?serverTimezone=Asia/Seoul + username: msaportal + password: msaportal + driver-class-name: com.mysql.cj.jdbc.Driver + mail: # 비밀번호 변경 이메일 발송 + host: smtp.gmail.com # smtp host + port: 587 # smtp port + username: email_username # 계정 + password: 'email_password' # 비밀번호 - 구글 보안 2단계 인증 해제, 보안 수준이 낮은 앱의 액세스 허용(https://myaccount.google.com/lesssecureapps) + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true