Initial commit

This commit is contained in:
jooho
2021-10-20 17:12:00 +09:00
parent 0c884beff8
commit 8caa4bbc5a
487 changed files with 44198 additions and 0 deletions

View File

@@ -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"]

View File

@@ -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()
}

185
backend/apigateway/gradlew vendored Executable file
View File

@@ -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" "$@"

89
backend/apigateway/gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
rootProject.name = 'apigateway'

View File

@@ -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
* <p>
* 게이트웨이 어플리케이션 클래스
* Eureka Client 로 설정했기 때문에 Eureka Server 가 먼저 기동되어야 한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@EnableDiscoveryClient
@SpringBootApplication
public class ApigatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApigatewayApplication.class, args);
}
}

View File

@@ -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
* <p>
* MessageSource 정상 확인을 위한 컨트롤러
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/08/10
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/10 jaeyeolkim 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/07 shinmj 최초 생성
* </pre>
*/
@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<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()),
HttpStatus.OK
));
}
@GetMapping("/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()),
HttpStatus.OK
));
}
@GetMapping("")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just(new ResponseEntity(
swaggerResources.get(), HttpStatus.OK
));
}
}

View File

@@ -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
* <p>
* Spring MessageSource 설정
* Message Domain 이 있는 portal-service 에서 messages.properties 를 공유 가능한 외부 위치에 생성한다.
* 각 서비스에서 해당 파일을 통해 다국어를 지원하도록 한다.
* module-common.jar 를 포함하지 않는 서비스에서는 이 configuration을 추가해주어야 한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/08/09
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/09 jaeyeolkim 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
* <p>
* Spring Security 에 의해 요청 url에 대한 사용자 인가 서비스를 수행하는 클래스
* 요청에 대한 사용자의 권한여부 체크하여 true/false 리턴한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/19
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/19 jaeyeolkim 최초 생성
* </pre>
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class ReactiveAuthorization implements ReactiveAuthorizationManager<AuthorizationContext> {
@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<AuthorizationDecision> check(Mono<Authentication> 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<Boolean> 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));
}
}

View File

@@ -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
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/07 shinmj 최초 생성
* </pre>
*/
@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<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> 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;
}
}

View File

@@ -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
* <p>
* Spring Security Config 클래스
* ReactiveAuthorizationManager<AuthorizationContext> 구현체 ReactiveAuthorization 클래스를 통해 인증/인가 처리를 구현한다.
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@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> authentication, AuthorizationContext context)
* @return
* @throws Exception
*/
@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http, ReactiveAuthorizationManager<AuthorizationContext> 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();
}
}

View File

@@ -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
* <p>
* 에러 발생 시 에러 정보 중 필요한 내용만 반환한다
* ErrorCode 에서 status, code, message 세 가지 속성을 의존한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
@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<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Map<String, Object> defaultMap = super.getErrorAttributes(request, options);
Map<String, Object> 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;
}
}
}

View File

@@ -0,0 +1,59 @@
package org.egovframe.cloud.apigateway.exception.dto;
/**
* org.egovframe.cloud.common.exception.dto.ErrorCode
* <p>
* REST API 요청에 대한 오류 반환값을 정의
* ErrorResponse 클래스에서 status, code, message 세 가지 속성을 의존한다
* message 는 MessageSource 의 키 값을 정의하여 다국어 처리를 지원한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/16
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/16 jaeyeolkim 최초 생성
* </pre>
*/
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;
}
}

View File

@@ -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<GlobalFilter.Config> {
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;
}
}

View File

@@ -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
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/07 shinmj 최초 생성
* </pre>
*/
@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);
};
}
}

View File

@@ -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>.*), /$\{segment}
- SwaggerHeaderFilter
- id: portal-service
uri: lb://PORTAL-SERVICE
predicates:
- Path=/portal-service/**
filters:
- RewritePath=/portal-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: board-service
uri: lb://BOARD-SERVICE
predicates:
- Path=/board-service/**
filters:
- RewritePath=/board-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: reserve-item-service
uri: lb://RESERVE-ITEM-SERVICE
predicates:
- Path=/reserve-item-service/**
filters:
- RewritePath=/reserve-item-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: reserve-check-service
uri: lb://RESERVE-CHECK-SERVICE
predicates:
- Path=/reserve-check-service/**
filters:
- RewritePath=/reserve-check-service/(?<segment>.*), /$\{segment}
- SwaggerHeaderFilter
- id: reserve-request-service
uri: lb://RESERVE-REQUEST-SERVICE
predicates:
- Path=/reserve-request-service/**
filters:
- RewritePath=/reserve-request-service/(?<segment>.*), /$\{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

View File

@@ -0,0 +1,5 @@
spring:
cloud:
config:
uri: http://localhost:8888
name: apigateway

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n</pattern>
</encoder>
</appender>
<!-- 로컬에서는 로그를 전송하지 않도록 설정 -->
<springProfile name="default">
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</springProfile>
<springProfile name="!default">
<!-- java -Ddestination="localhost:8088" 와 같이 변경할 수 있다. cf 환경에서는 manifest.yml 파일에 환경변수로 추가 -->
<property name="destination" value="localhost:8088" />
<property name="app_name" value="${app_name}" />
<!-- ELK - Logstash 로 로그를 전송하기 위한 appender -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${destination}</destination><!-- native profile => localhost:8088 -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app.name":"${app_name}"}</customFields>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="LOGSTASH" />
<appender-ref ref="STDOUT" />
</root>
</springProfile>
</configuration>

View File

@@ -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() {
}
}

View File

@@ -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("로그인");
}
}

View File

@@ -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()
;
}
}

View File

@@ -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"]

View File

@@ -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 추가 끝

185
backend/board-service/gradlew vendored Executable file
View File

@@ -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" "$@"

89
backend/board-service/gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
rootProject.name = 'board-service'

View File

@@ -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
* <p>
* 게시판 서비스 어플리케이션 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
@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);
}
}

View File

@@ -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
* <p>
* 게시판 Rest API 컨트롤러 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
@RequiredArgsConstructor
@RestController
public class BoardApiController {
/**
* 게시판 서비스
*/
private final BoardService boardService;
/**
* 게시판 페이지 목록 조회
*
* @param requestDto 요청 DTO
* @param pageable 페이지 정보
* @return Page<BoardListResponseDto> 페이지 게시판 목록 응답 DTO
*/
@GetMapping("/api/v1/boards")
public Page<BoardListResponseDto> 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);
}
}

View File

@@ -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
* <p>
* 게시판 목록 응답 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
* <p>
* 게시판 상세 응답 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
@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<PostsSimpleResponseDto> 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<PostsSimpleResponseDto> posts) {
this.posts = posts;
}
}

View File

@@ -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
* <p>
* 게시판 등록 요청 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
* <p>
* 게시판 수정 요청 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/08
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/08 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
* <p>
* 댓글 Rest API 컨트롤러 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/04
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
@RequiredArgsConstructor
@RestController
public class CommentApiController {
/**
* 댓글 서비스
*/
private final CommentService commentService;
/**
* 게시글의 전체 댓글 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
@GetMapping("/api/v1/comments/total/{boardNo}/{postsNo}")
public Map<String, Object> findTotal(@PathVariable Integer boardNo, @PathVariable Integer postsNo) {
return commentService.findAll(boardNo, postsNo, null);
}
/**
* 게시글의 전체 미삭제 댓글 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
@GetMapping("/api/v1/comments/all/{boardNo}/{postsNo}")
public Map<String, Object> findAll(@PathVariable Integer boardNo, @PathVariable Integer postsNo) {
return commentService.findAll(boardNo, postsNo, 0);
}
/**
* 게시글의 댓글 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @param pageable 페이지 정보
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
@GetMapping("/api/v1/comments/{boardNo}/{postsNo}")
public Map<String, Object> findPage(@PathVariable Integer boardNo, @PathVariable Integer postsNo, Pageable pageable) {
return commentService.findPage(boardNo, postsNo, null, pageable);
}
/**
* 게시글의 미삭제 댓글 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @param pageable 페이지 정보
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
@GetMapping("/api/v1/comments/list/{boardNo}/{postsNo}")
public Map<String, Object> 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);
}
}

View File

@@ -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
* <p>
* 댓글 목록 응답 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/04
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/11 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
* <p>
* 댓글 등록 요청 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/04
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
* <p>
* 게시물 Rest API 컨트롤러 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
@RequiredArgsConstructor
@RestController
public class PostsApiController {
/**
* 게시물 서비스
*/
private final PostsService postsService;
/**
* 게시물(삭제 포함) 페이지 목록 조회
*
* @param boardNo 게시판 번호
* @param requestDto 요청 DTO
* @param pageable 페이지 정보
* @return Page<PostsListResponseDto> 페이지 게시물 목록 응답 DTO
*/
@GetMapping("/api/v1/posts/{boardNo}")
public Page<PostsListResponseDto> 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<PostsListResponseDto> 페이지 게시물 목록 응답 DTO
*/
@GetMapping("/api/v1/posts/list/{boardNo}")
public Page<PostsListResponseDto> 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<Integer, BoardResponseDto> 최근 게시물이 포함된 게시판 상세 응답 DTO Map
*/
@GetMapping("/api/v1/posts/newest/{boardNos}/{postsCount}")
public Map<Integer, BoardResponseDto> findNewest(@PathVariable List<Integer> 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<PostsDeleteRequestDto> 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<PostsDeleteRequestDto> 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<PostsDeleteRequestDto> requestDtoList) {
postsService.delete(requestDtoList);
}
}

View File

@@ -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
* <p>
* 게시물 삭제 요청 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/29
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/29 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
* <p>
* 게시물 목록 응답 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
* <p>
* 게시물 상세 응답 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
@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<PostsSimpleResponseDto> prevPosts;
/**
* 다음 게시물 응답 DTO List
*/
private List<PostsSimpleResponseDto> 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<PostsSimpleResponseDto> prevPosts) {
this.prevPosts = prevPosts;
}
/**
* 다음 게시물
*/
public void setNextPosts(List<PostsSimpleResponseDto> nextPosts) {
this.nextPosts = nextPosts;
}
}

View File

@@ -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
* <p>
* 게시물 등록 요청 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
* <p>
* 게시물 응답 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/09/03
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/03 jooho 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/10 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
* <p>
* 게시물 수정 요청 DTO 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/08
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/08 jooho 최초 생성
* </pre>
*/
@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();
}
}

View File

@@ -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
* <p>
* Spring Security Config 클래스
* AuthenticationFilter 를 추가하고 토큰으로 setAuthentication 인증처리를 한다
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@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);
}
}

View File

@@ -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
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/07 jooho 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
* <p>
* 게시판 엔티티 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
@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> 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;
}
}

View File

@@ -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
* <p>
* 게시판 레파지토리 인터페이스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
public interface BoardRepository extends JpaRepository<Board, Integer>, BoardRepositoryCustom {
}

View File

@@ -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
* <p>
* 게시판 Querydsl 인터페이스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
public interface BoardRepositoryCustom {
/**
* 게시판 페이지 목록 조회
*
* @param requestDto 게시판 목록 요청 DTO
* @param pageable 페이지 정보
* @return Page<BoardListResponseDto> 페이지 게시판 목록 응답 DTO
*/
Page<BoardListResponseDto> findPage(RequestDto requestDto, Pageable pageable);
/**
* 게시판 목록 조회
*
* @param boardNos 게시판 번호 목록
* @return List<BoardResponseDto> 게시판 상세 응답 DTO List
*/
List<BoardResponseDto> findAllByBoardNoIn(List<Integer> boardNos);
}

View File

@@ -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
* <p>
* 게시판 Querydsl 구현 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
@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<BoardListResponseDto> 페이지 게시판 목록 응답 DTO
*/
@Override
public Page<BoardListResponseDto> findPage(RequestDto requestDto, Pageable pageable) {
JPQLQuery<BoardListResponseDto> 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<Object> 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<BoardListResponseDto> result = query
.offset(pageable.getOffset())
.limit(pageable.getPageSize()) //페이징
.fetchResults();
return new PageImpl<>(result.getResults(), pageable, result.getTotal());
}
/**
* 게시판 목록 조회
*
* @param boardNos 게시판 번호 목록
* @return List<BoardResponseDto> 게시판 상세 응답 DTO List
*/
@Override
public List<BoardResponseDto> findAllByBoardNoIn(List<Integer> 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;
}
}
}

View File

@@ -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
* <p>
* 공통코드 엔티티
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/07/12
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/12 jaeyeolkim 최초 생성
* </pre>
*/
@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; // 코드 명
}

View File

@@ -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
* <p>
* 댓글 엔티티 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/04
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
* <p>
* 댓글 엔티티 복합키 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/04
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
@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());
}
}

View File

@@ -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
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
public interface CommentRepository extends JpaRepository<Comment, CommentId>, CommentRepositoryCustom {
}

View File

@@ -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
* <p>
* 댓글 Querydsl 인터페이스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/04
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
public interface CommentRepositoryCustom {
/**
* 댓글 전체 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @param deleteAt 삭제 여부
* @return List<CommentListResponseDto> 댓글 목록 응답 DTO
*/
List<CommentListResponseDto> findAll(Integer boardNo, Integer postsNo, Integer deleteAt);
/**
* 댓글 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @param deleteAt 삭제 여부
* @param pageable 페이지 정보
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
Map<String, Object> 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);
}

View File

@@ -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
* <p>
* 댓글 Querydsl 구현 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/04
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
@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<CommentListResponseDto> 댓글 목록 응답 DTO
*/
public List<CommentListResponseDto> 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();
}
/**
* 댓글 목록 조회
* <p>
* JPQL 은 from 절에서 서브쿼리를 사용할 수 없어서 SQLQueryFactory 를 사용해 Native SQL 로 조회
* <p>
* Native SQL 을 사용하지 않고 서브쿼리를 먼저 조회한 후 JPQL 만을 사용해서 조회 가능
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @param deleteAt 삭제 여부
* @param pageable 페이지 정보
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
public Map<String, Object> 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<Comment> commentPath = Expressions.path(Comment.class, "comment");
NumberPath<Integer> boardNoPath = Expressions.numberPath(Integer.class, commentPath, "board_no");
NumberPath<Integer> postsNoPath = Expressions.numberPath(Integer.class, commentPath, "posts_no");
NumberPath<Integer> commentNoPath = Expressions.numberPath(Integer.class, commentPath, "comment_no");
NumberPath<Integer> groupNoPath = Expressions.numberPath(Integer.class, commentPath, "group_no");
NumberPath<Integer> parentCommentNoPath = Expressions.numberPath(Integer.class, commentPath, "parent_comment_no");
NumberPath<Integer> sortSeqPath = Expressions.numberPath(Integer.class, commentPath, "sort_seq");
NumberPath<Integer> 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<Comment> groupCommentPath = Expressions.path(Comment.class, "groupComment");
SubQueryExpression<Tuple> 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<CommentListResponseDto> 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<CommentListResponseDto> page = new PageImpl<>(comments, pageable, groupElements == null ? 0 : groupElements);
// 페이지 인터페이스와 동일한 속성의 맵 리턴
Map<String, Object> 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);
}
}

View File

@@ -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
* <p>
* 게시물 엔티티 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
@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<Comment> 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<Comment> 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;
}
}

View File

@@ -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
* <p>
* 게시물 엔티티 복합키 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/30 jooho 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
* <p>
* 게시물 조회 엔티티 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/02
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/02 jooho 최초 생성
* </pre>
*/
@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;
}
}

View File

@@ -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
* <p>
* 게시판 조회 엔티티 복합키 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/02
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/02 jooho 최초 생성
* </pre>
*/
@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());
}
}

View File

@@ -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
* <p>
* 게시물 조회 레파지토리 인터페이스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/02
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/02 jooho 최초 생성
* </pre>
*/
public interface PostsReadRepository extends JpaRepository<PostsRead, PostsReadId>, PostsReadRepositoryCustom {
}

View File

@@ -0,0 +1,42 @@
package org.egovframe.cloud.boardservice.domain.posts;
/**
* org.egovframe.cloud.boardservice.domain.posts.PostsReadRepositoryCustom
* <p>
* 게시물 조회 Querydsl 인터페이스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/02
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/02 jooho 최초 생성
* </pre>
*/
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);
}

View File

@@ -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
* <p>
* 게시물 조회 Querydsl 구현 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/02
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/02 jooho 최초 생성
* </pre>
*/
@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;
}
}
}

View File

@@ -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
* <p>
* 게시물 레파지토리 인터페이스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
public interface PostsRepository extends JpaRepository<Posts, PostsId>, PostsRepositoryCustom {
}

View File

@@ -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
* <p>
* 게시물 Querydsl 인터페이스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
public interface PostsRepositoryCustom {
/**
* 게시물 페이지 목록 조회
*
* @param boardNo 게시판 번호
* @param deleteAt 삭제 여부
* @param requestDto 요청 DTO
* @param pageable 페이지 정보
* @return Page<PostsListResponseDto> 페이지 게시물 목록 응답 DTO
*/
Page<PostsListResponseDto> findPage(Integer boardNo, Integer deleteAt, RequestDto requestDto, Pageable pageable);
/**
* 게시판별 최근 게시물 목록 조회
*
* @param boardNos 게시판 번호 목록
* @param postsCount 게시물 수
* @return List<PostsSimpleResponseDto> 게시물 응답 DTO List
*/
List<PostsSimpleResponseDto> findAllByBoardNosLimitCount(List<Integer> 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<PostsSimpleResponseDto> 게시물 상세 응답 DTO List
*/
List<PostsSimpleResponseDto> 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<Integer, List<Integer>> posts, Integer deleteAt, String userId);
}

View File

@@ -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
* <p>
* 게시물 Querydsl 구현 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
@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<PostsListResponseDto> 페이지 게시물 목록 응답 DTO
*/
@Override
public Page<PostsListResponseDto> findPage(Integer boardNo, Integer deleteAt, RequestDto requestDto, Pageable pageable) {
JPQLQuery<PostsListResponseDto> 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<Object> 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<PostsListResponseDto> result = query
.offset(pageable.getOffset())
.limit(pageable.getPageSize()) //페이징
.fetchResults();
return new PageImpl<>(result.getResults(), pageable, result.getTotal());
}
/**
* 게시판별 최근 게시물 목록 조회
* <p>
* JPQL 은 from 절에서 서브쿼리를 사용할 수 없어서 SQLQueryFactory 를 사용해 Native SQL 로 조회
* MySQL8 부터는 ROW_NUMBER, RANK 함수를 지원, 탬플릿에서는 MySQL5.7 로 개발해서 mysql 변수를 사용하는 방법으로 조회
* MySQL 문법이 포함되어있어서 다른 DBMS 를 사용하는 경우 수정 필요
* <p>
* 인프런 김영한 강사는 추천하지 않는다.
* SqlQueryFactory는 저는 권장하지 않습니다. DB에서 메타데이터를 다 뽑아내서 생성해야 하는데... 너무 복잡하고 기능에 한계도 많습니다.
* 따라서 JPA와 JPA용 Querydsl을 최대한 사용하고, 그래도 잘 안되는 부분은 네이티브 쿼리를 사용하는 것이 더 좋다 생각합니다.
* <p>
* 게시판별로 반복하여 게시물 조회하는 방법 등으로 JPQL 만을 사용해서 조회 가능
*
* @param boardNos 게시판 번호 목록
* @param postsCount 게시물 수
* @return List<PostsSimpleResponseDto> 게시물 응답 DTO List
*/
@Override
public List<PostsSimpleResponseDto> findAllByBoardNosLimitCount(List<Integer> boardNos, Integer postsCount) {
// path 정의
Path<Posts> postsPath = Expressions.path(Posts.class, "posts");
NumberPath<Integer> boardNoPath = Expressions.numberPath(Integer.class, postsPath, "board_no");
NumberPath<Integer> postsNoPath = Expressions.numberPath(Integer.class, postsPath, "posts_no");
// 게시판번호, 로우넘 변수
StringPath varPath = Expressions.stringPath("v");
SQLQuery<Tuple> varSql = SQLExpressions.select(
Expressions.stringTemplate("@boardNo := 0"),
Expressions.stringTemplate("@rn := 0"));
// 게시물 조회
SQLQuery<Tuple> 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<PostsSimpleResponseDto> 게시물 상세 응답 DTO List
*/
@Override
public List<PostsSimpleResponseDto> 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<Integer, List<Integer>> posts, Integer deleteAt, String userId) {
long updateCount = 0L;
Iterator<Integer> iterator = posts.keySet().iterator();
while (iterator.hasNext()) {
Integer boardNo = iterator.next();
List<Integer> 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<Long> 댓글 수 표현식
*/
private SimpleExpression<Long> 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;
}
}
}

View File

@@ -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
* <p>
* 사용자 정보 엔티티
*
* @author 표준프레임워크센터 jaeyeolkim
* @version 1.0
* @since 2021/06/30
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/06/30 jaeyeolkim 최초 생성
* </pre>
*/
@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;
}

View File

@@ -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
* <p>
* 게시판 서비스 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class BoardService extends AbstractService {
/**
* 게시판 레파지토리 인터페이스
*/
private final BoardRepository boardRepository;
/**
* 조회 조건에 일치하는 게시판 페이지 목록 조회
*
* @param requestDto 요청 DTO
* @param pageable 페이지 정보
* @return Page<BoardListResponseDto> 페이지 게시판 목록 응답 DTO
*/
public Page<BoardListResponseDto> findPage(RequestDto requestDto, Pageable pageable) {
return boardRepository.findPage(requestDto, pageable);
}
/**
* 게시판 목록 조회
*
* @param boardNos 게시판 번호 목록
* @return List<BoardResponseDto> 게시판 상세 응답 DTO List
*/
public List<BoardResponseDto> findAllByBoardNos(List<Integer> 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")})));
}
}

View File

@@ -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
* <p>
* 댓글 서비스 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/04
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/04 jooho 최초 생성
* </pre>
*/
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class CommentService extends AbstractService {
/**
* 게시물 레파지토리 인터페이스
*/
private final CommentRepository commentRepository;
/**
* 게시물 서비스
*/
private final PostsService postsService;
/**
* 게시글의 댓글 전체 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
public Map<String, Object> findAll(Integer boardNo, Integer postsNo) {
return findAll(boardNo, postsNo, null);
}
/**
* 게시글의 댓글 전체 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @param deleteAt 삭제 여부
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
public Map<String, Object> findAll(Integer boardNo, Integer postsNo, Integer deleteAt) {
List<CommentListResponseDto> comments = commentRepository.findAll(boardNo, postsNo, deleteAt);
// 페이지 인터페이스와 동일한 속성의 맵 리턴
Map<String, Object> 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<String, Object> 페이지 댓글 목록 응답 DTO
*/
public Map<String, Object> findPage(Integer boardNo, Integer postsNo, Pageable pageable) {
return commentRepository.findPage(boardNo, postsNo, null, pageable);
}
/**
* 게시글의 댓글 목록 조회
*
* @param boardNo 게시판 번호
* @param postsNo 게시물 번호
* @param deleteAt 삭제 여부
* @param pageable 페이지 정보
* @return Map<String, Object> 페이지 댓글 목록 응답 DTO
*/
public Map<String, Object> 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")); // 삭제된 게시물입니다.
}
}
}

View File

@@ -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
* <p>
* 게시물 서비스 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/28
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/28 jooho 최초 생성
* </pre>
*/
@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<PostsListResponseDto> 페이지 게시물 목록 응답 DTO
*/
public Page<PostsListResponseDto> 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<Integer, BoardResponseDto> 최근 게시물이 포함된 게시판 상세 응답 DTO Map
*/
public Map<Integer, BoardResponseDto> findNewest(List<Integer> boardNos, Integer postsCount) {
if (boardNos == null || boardNos.isEmpty())
throw new InvalidValueException(getMessage("err.invalid.input.value"));
List<BoardResponseDto> boards = boardService.findAllByBoardNos(boardNos);
List<PostsSimpleResponseDto> allPosts = postsRepository.findAllByBoardNosLimitCount(boardNos, postsCount);
Map<Integer, List<PostsSimpleResponseDto>> postsGroup = allPosts.stream().collect(Collectors.groupingBy(PostsSimpleResponseDto::getBoardNo, Collectors.toList()));
Map<Integer, BoardResponseDto> data = new HashMap<>(); // 요청한 게시판 순서로 리턴하기 위해서 map 리턴
for (BoardResponseDto board : boards) {
List<PostsSimpleResponseDto> 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<PostsSimpleResponseDto> prevPosts = postsRepository.findNearPost(boardNo, postsNo, -1, deleteAt, requestDto);
dto.setPrevPosts(prevPosts);
List<PostsSimpleResponseDto> 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<PostsDeleteRequestDto> requestDtoList, String userId) {
Map<Integer, List<Integer>> 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<PostsDeleteRequestDto> requestDtoList, String userId) {
Map<Integer, List<Integer>> 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<PostsDeleteRequestDto> requestDtoList) {
List<Posts> 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());
}
}

View File

@@ -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
* <p>
* HTTP 관련 유틸 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/09/09
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/09/09 jooho 최초 생성
* </pre>
*/
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()));
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n</pattern>
</encoder>
</appender>
<!-- 로컬에서는 로그를 전송하지 않도록 설정 -->
<springProfile name="default">
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</springProfile>
<springProfile name="!default">
<!-- java -Ddestination="localhost:8088" 와 같이 변경할 수 있다. cf 환경에서는 manifest.yml 파일에 환경변수로 추가 -->
<property name="destination" value="localhost:8088" />
<property name="app_name" value="${app_name}" />
<!-- ELK - Logstash 로 로그를 전송하기 위한 appender -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${destination}</destination><!-- native profile => localhost:8088 -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app.name":"${app_name}"}</customFields>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="LOGSTASH" />
<appender-ref ref="STDOUT" />
</root>
</springProfile>
</configuration>

View File

@@ -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
* <p>
* 게시판 Rest API 컨트롤러 테스트 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/26
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/26 jooho 최초 생성
* </pre>
*/
@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<RestResponsePage<BoardListResponseDto>> responseEntity = restTemplate.exchange(
URL + queryString,
HttpMethod.GET,
null,
new ParameterizedTypeReference<RestResponsePage<BoardListResponseDto>>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
RestResponsePage<BoardListResponseDto> 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<BoardResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<BoardResponseDto>() {
}
);
// 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<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(params);
// when
//ResponseEntity<PostsResponseDto> responseEntity = restTemplate.postForEntity(URL, requestDto, PostsResponseDto.class);
ResponseEntity<BoardResponseDto> responseEntity = restTemplate.exchange(
URL,
HttpMethod.POST,
httpEntity,
new ParameterizedTypeReference<BoardResponseDto>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
BoardResponseDto dto = responseEntity.getBody();
assertThat(dto).isNotNull();
final Integer boardNo = dto.getBoardNo();
Optional<Board> 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<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(params);
String url = URL + "/" + boardNo;
// when
ResponseEntity<BoardResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.PUT,
httpEntity,
new ParameterizedTypeReference<BoardResponseDto>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
BoardResponseDto dto = responseEntity.getBody();
assertThat(dto).isNotNull();
Optional<Board> 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<BoardResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.DELETE,
null,
BoardResponseDto.class
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Optional<Board> board = selectData(boardNo);
assertThat(board).isNotPresent();
}
/**
* 테스트 데이터 등록
*/
private void insertBoards() {
log.info("###테스트 데이터 등록");
List<Board> 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<Board> 게시판 엔티티
*/
private Optional<Board> selectData(Integer boardNo) {
log.info("###테스트 데이터 단건 조회");
return boardRepository.findById(boardNo);
}
}

View File

@@ -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
* <p>
* 댓글 Rest API 컨트롤러 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/11
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/11 jooho 최초 생성
* </pre>
*/
@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<Map<String, Object>> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<Map<String, Object>>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Map<String, Object> 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<Map<String, Object>> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<Map<String, Object>>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Map<String, Object> 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<Map<String, Object>> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<Map<String, Object>>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Map<String, Object> 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<Map<String, Object>> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<Map<String, Object>>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Map<String, Object> 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<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(params);
// when
ResponseEntity<CommentResponseDto> responseEntity = restTemplate.exchange(
URL,
HttpMethod.POST,
httpEntity,
new ParameterizedTypeReference<CommentResponseDto>() {
}
);
// 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> 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<String, Object> params = new HashMap<>();
params.put("boardNo", boardNo);
params.put("postsNo", postsNo);
params.put("commentNo", commentNo);
params.put("commentContent", UPDATE_COMMENT_CONTENT);
HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<>(params);
String url = URL + "/update";
// when
ResponseEntity<CommentResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.PUT,
httpEntity,
new ParameterizedTypeReference<CommentResponseDto>() {
}
);
// 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<BoardResponseDto> 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<String, Object> params = new HashMap<>();
params.put("boardNo", boardNo);
params.put("postsNo", postsNo);
params.put("commentNo", commentNo);
params.put("commentContent", UPDATE_COMMENT_CONTENT);
HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<>(params);
// when
ResponseEntity<CommentResponseDto> responseEntity = restTemplate.exchange(
URL,
HttpMethod.PUT,
httpEntity,
new ParameterizedTypeReference<CommentResponseDto>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
CommentResponseDto dto = responseEntity.getBody();
assertThat(dto).isNotNull();
Optional<Comment> 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<BoardResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.DELETE,
null,
BoardResponseDto.class
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Optional<Comment> 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<Comment> 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<Comment> 댓글 엔티티
*/
private Optional<Comment> 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());
}
}

View File

@@ -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
* <p>
* 게시물 Rest API 컨트롤러 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/08/10
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/08/10 jooho 최초 생성
* </pre>
*/
@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> 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<String> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<String>() {
}
);
// 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<RestResponsePage<PostsListResponseDto>> responseEntity = restTemplate.exchange(
url + queryString,
HttpMethod.GET,
null,
new ParameterizedTypeReference<RestResponsePage<PostsListResponseDto>>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
RestResponsePage<PostsListResponseDto> 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<RestResponsePage<PostsListResponseDto>> responseEntity = restTemplate.exchange(
url + queryString,
HttpMethod.GET,
null,
new ParameterizedTypeReference<RestResponsePage<PostsListResponseDto>>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
RestResponsePage<PostsListResponseDto> 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<PostsResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<PostsResponseDto>() {
}
);
// 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<PostsResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.GET,
null,
new ParameterizedTypeReference<PostsResponseDto>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 삭제 게시물 조회 불가
}
/**
* 게시물 등록 작성자체크
*/
@Test
void 게시물_등록_작성자체크() {
log.info("###게시물_등록_작성자체크");
// given
String url = URL + "/save/" + board.getBoardNo();
Map<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(params);
// when
ResponseEntity<PostsResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.POST,
httpEntity,
new ParameterizedTypeReference<PostsResponseDto>() {
}
);
// 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<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(params);
String url = URL + "/update/" + boardNo + "/" + postsNo;
// when
ResponseEntity<PostsResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.PUT,
httpEntity,
new ParameterizedTypeReference<PostsResponseDto>() {
}
);
// 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<Void> responseEntity = restTemplate.exchange(
url,
HttpMethod.DELETE,
null,
new ParameterizedTypeReference<Void>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); // 본인글 아닌 경우 예외 발생
}
/**
* 게시물 등록
*/
@Test
void 게시물_등록() {
log.info("###게시물_등록");
// given
String url = URL + "/" + board.getBoardNo();
Map<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(params);
// when
ResponseEntity<PostsResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.POST,
httpEntity,
new ParameterizedTypeReference<PostsResponseDto>() {
}
);
// 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> 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<String, Object> 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<Map<String, Object>> httpEntity = new HttpEntity<>(params);
String url = URL + "/" + boardNo + "/" + postsNo;
// when
ResponseEntity<PostsResponseDto> responseEntity = restTemplate.exchange(
url,
HttpMethod.PUT,
httpEntity,
new ParameterizedTypeReference<PostsResponseDto>() {
}
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
PostsResponseDto dto = responseEntity.getBody();
assertThat(dto).isNotNull();
Optional<Posts> 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<Map<String, Object>> params = new ArrayList<>();
for (Posts post : posts) {
Map<String, Object> param = new HashMap<>();
param.put("boardNo", post.getPostsId().getBoardNo());
param.put("postsNo", post.getPostsId().getPostsNo());
params.add(param);
}
HttpEntity<List<Map<String, Object>>> httpEntity = new HttpEntity<>(params);
String url = URL + "/remove";
// when
ResponseEntity<Long> responseEntity = restTemplate.exchange(
url,
HttpMethod.PUT,
httpEntity,
Long.class
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isEqualTo(posts.size());
List<Posts> 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<Map<String, Object>> params = new ArrayList<>();
for (Posts post : posts) {
Map<String, Object> param = new HashMap<>();
param.put("boardNo", post.getPostsId().getBoardNo());
param.put("postsNo", post.getPostsId().getPostsNo());
params.add(param);
}
HttpEntity<List<Map<String, Object>>> httpEntity = new HttpEntity<>(params);
String url = URL + "/restore";
// when
ResponseEntity<Long> responseEntity = restTemplate.exchange(
url,
HttpMethod.PUT,
httpEntity,
Long.class
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isEqualTo(posts.size());
List<Posts> 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<Map<String, Object>> params = new ArrayList<>();
for (Posts post : posts) {
Map<String, Object> param = new HashMap<>();
param.put("boardNo", post.getPostsId().getBoardNo());
param.put("postsNo", post.getPostsId().getPostsNo());
params.add(param);
}
HttpEntity<List<Map<String, Object>>> httpEntity = new HttpEntity<>(params);
String url = URL + "/delete";
// when
ResponseEntity<Long> responseEntity = restTemplate.exchange(
url,
HttpMethod.PUT,
httpEntity,
Long.class
);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
List<Posts> list = postsRepository.findAll();
assertThat(list).isNotNull();
assertThat(list.size()).isZero();
}
/**
* 테스트 데이터 등록
*/
private void insertPosts(Boolean deleteAt) {
log.info("###테스트 데이터 등록");
// 게시물 등록
List<Posts> 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<Posts> 게시물 엔티티
*/
private Optional<Posts> selectData(Integer boardNo, Integer postsNo) {
return postsRepository.findById(PostsId.builder().boardNo(boardNo).postsNo(postsNo).build());
}
}

View File

@@ -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
* <p>
* 페이지 API 조회 시 JSON 형식의 응답 데이터를 페이지 객체를 구현하여 마이그레이션 해주는 클래스
*
* @author 표준프레임워크센터 jooho
* @version 1.0
* @since 2021/07/08
*
* <pre>
* << 개정이력(Modification Information) >>
*
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2021/07/08 jooho 최초 생성
* </pre>
*/
public class RestResponsePage<T> extends PageImpl<T> {
/**
* 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<T> 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<T> content, Pageable pageable, long total) {
super(content, pageable, total);
}
/**
* Rest 응답 페이지 생성자
*
* @param content 목록
*/
public RestResponsePage(List<T> content) {
super(content);
}
/**
* Rest 응답 페이지 생성자
*/
public RestResponsePage() {
super(Collections.emptyList());
}
}

View File

@@ -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

13
backend/config/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -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()
}

185
backend/config/gradlew vendored Executable file
View File

@@ -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" "$@"

89
backend/config/gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
rootProject.name = 'config'

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %thread %-5level %logger - %m%n</pattern>
</encoder>
</appender>
<!-- 로컬에서는 로그를 전송하지 않도록 설정 -->
<springProfile name="default">
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</springProfile>
<springProfile name="!default">
<!-- java -Ddestination="localhost:8088" 와 같이 변경할 수 있다. cf 환경에서는 manifest.yml 파일에 환경변수로 추가 -->
<property name="destination" value="localhost:8088" />
<property name="app_name" value="${app_name}" />
<!-- ELK - Logstash 로 로그를 전송하기 위한 appender -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${destination}</destination><!-- native profile => localhost:8088 -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app.name":"${app_name}"}</customFields>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="LOGSTASH" />
<appender-ref ref="STDOUT" />
</root>
</springProfile>
<typeAliases></typeAliases>
</configuration>

View File

@@ -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() {
}
}

View File

@@ -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"]

View File

@@ -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()
}

185
backend/discovery/gradlew vendored Executable file
View File

@@ -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" "$@"

89
backend/discovery/gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
rootProject.name = 'discovery'

View File

@@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show More